Skip to content

feat: plugin runtime primitives and agent foundation hooks (v2)#199

Merged
dgarson merged 6 commits intodgarson/forkfrom
dgarson/foundation-primitives-core-v2
Mar 3, 2026
Merged

feat: plugin runtime primitives and agent foundation hooks (v2)#199
dgarson merged 6 commits intodgarson/forkfrom
dgarson/foundation-primitives-core-v2

Conversation

@dgarson
Copy link
Owner

@dgarson dgarson commented Mar 3, 2026

Recreated from PR #190 with clean ancestry.

Features

  • Plugin runtime primitives: KV store, cron scheduler, and quota limit enforcement for plugin extensions
  • Extended hook/type surface: New hook points for agent/tool/routing interception
  • Agent runtime config types: Diagnostic event schema and runtime config type definitions
  • Plugin-contributed prompt sections: Plugins can inject sections into the agent system prompt
  • Tool adapter hooks: before-tool-call hook for tool invocation interception
  • Subagent spawn gate: Pre-spawn hook dispatch and cleanup-scope spawn gate for subagents
  • Before-message-route hook: Fire before_message_route plugin hook in auto-reply dispatch

Changes

This is a minimal re-application of the core feature commits from PR #190, excluding the cherry-picked main fixes that caused merge conflicts.

Original PR #190 had 3585 files changed with many cherry-picked commits. This version contains only the essential feature changes.

Commits included:

  • 11944bf plugins: add plugin runtime primitives (KV store, cron scheduler, quota limits)
  • 6b8d50f plugins: extend hook and type surface for agent/tool/routing interception
  • 9c81959 agents: inject plugin-contributed prompt sections + add session_score built-in tool
  • 528f398 agents: extend tool adapter with before-tool-call hook and embedded runner improvements
  • b5839cc agents: add pre-spawn hook dispatch and cleanup-scope spawn gate for subagents
  • 31de6b7 routing: fire before_message_route plugin hook in auto-reply dispatch

dgarson and others added 6 commits March 2, 2026 18:59
…ta limits)

Introduces three first-class runtime namespaces available to every plugin
via runtime.kv, runtime.cron, and runtime.quota.

KV store (runtime.kv): scoped key-value persistence backed by the agent
state directory. Plugins get full CRUD + TTL expiry without touching the
filesystem directly. Critical for extensions that need durable config or
cross-invocation memory (e.g. budget ledgers, mail stores, policy caches).

Cron scheduler (runtime.cron): declarative periodic tasks registered by
name. Runs inside the gateway process on the given cron expression;
scheduling survives gateway restarts because registrations are replayed
at startup. Enables extensions like the event-ledger retention sweep or
the observability health-check heartbeat.

Quota system (runtime.quota): per-scope token/request/cost counters with
configurable windows and soft/hard limits. Plugins can increment usage and
check headroom before expensive operations -- the budget-manager extension
is built entirely on top of this.

Type namespaces are extracted to dedicated files (types.kv.ts, types.cron.ts,
types.quota.ts, types.agents.ts, types.gateway.ts, types.sessions.ts) so the
main runtime/types.ts stays readable. All three implementations have unit tests.

pnpm-lock.yaml is updated for the otel/opentelemetry-api dependency pulled
in by the observability extension.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

Adds four new plugin type modules that define the full interception surface
plugins can register against:

types.hooks.primitives.ts -- before_agent_run and before_message_route hooks.
  before_agent_run fires before every agent turn and lets plugins inject
  extra context, modify the effective config, or abort the run with a
  structured response. before_message_route fires at inbound routing time so
  plugins can redirect or suppress messages before any agent is activated.

types.hooks.subagent-spawn.ts -- before_subagent_spawn hook. Called just
  before a subagent is forked, giving plugins the ability to deny, reroute,
  or annotate the spawn (e.g. budget checks, policy enforcement).

types.hooks.tool-mutation.ts -- after_tool_call hook. Called with the raw
  tool result; plugins can rewrite or enrich the result before the agent sees
  it (useful for observability spans, sanitization, or result caching).

types.prompt-sections.ts -- PromptSectionBuilder API. Plugins register named
  prompt sections with a slot (prepend / append / replace:*), a priority, and
  an optional condition predicate. The agent runner collects and injects them
  at system-prompt assembly time (see system-prompt.plugin-sections.ts).

hooks.ts is updated to dispatch the four new hook types through the plugin
registry. registry.ts gains registerHook, registerPromptSection, and the
corresponding query helpers. plugin-sdk/index.ts re-exports everything so
plugin authors get a single entry-point import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… built-in tool

system-prompt.plugin-sections.ts introduces collectPluginPromptSections(),
which queries the plugin registry for registered PromptSectionBuilders, runs
them with the current session context, and returns sorted+filtered sections
ready for injection. Sections support three slots: prepend (before the core
prompt), append (after it), and replace:<slot-name> (swap out a named block).
Only the highest-priority registration wins for replace slots; collisions are
logged as warnings. This is the mechanism that lets extensions like
ocx-orchestration inject a live sprint context or ocx-routing-policy inject
model instructions without forking the system-prompt builder.

system-prompt.ts is updated to accept pluginSections and splice them in at
the appropriate positions in the final assembled prompt. The change is purely
additive -- callers that pass no sections get the same output as before.

session-score-tool.ts adds the session_score built-in agent tool (tool #10).
Agents call it to emit a structured self-quality score (0.0-1.0) with a
rubric label, optional tags, and a free-text rationale note. The score is
forwarded to the diagnostic event pipeline as a session.score event, where
the observability extension can pick it up, tag it with OTel attributes, and
surface it in dashboards. No LLM call is made; the tool is a lightweight
event emitter so latency impact is negligible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unner improvements

pi-tool-definition-adapter.ts now wraps each tool's execute function to fire
the before_tool_call plugin hook before dispatch. This lets plugins (such as
ocx-budget-manager) intercept any tool call, inspect its arguments, and either
allow, modify, or deny it -- without the agent having any awareness. The
after_tool_call hook (types.hooks.tool-mutation.ts) is wired at the same
layer so result rewriting is symmetric.

pi-tools.before-tool-call.ts implements the dispatch logic: it walks the
hook registry, calls each registered handler in priority order, and surfaces
a combined allow/deny decision. An integration test covers the allow,
deny-with-message, and result-mutate paths end-to-end.

Embedded runner changes (pi-embedded-runner/run.ts, attempt.ts, types.ts):
  - pluginSections is threaded through RunAttemptContext so the assembled
    system prompt picks up plugin sections collected before the run starts.
  - compact.ts and tool-split.ts receive minor type-safety fixes exposed by
    the stricter RunAttemptContext shape.
  - system-prompt.ts in the runner now forwards pluginSections into the
    shared buildAgentSystemPrompt builder.

pi-embedded-subscribe.* gains the tool-call plumbing so subscribe-mode
sessions (used by background agents) apply the same before/after hooks.

sessions-spawn-tool.ts updates the spawn payload to include agentRuntimeConfig
so spawned subagents inherit the caller's model/tool overrides unless
explicitly overridden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…subagents

subagent-spawn.ts now fires the before_subagent_spawn plugin hook before
forking. Any registered plugin can inspect the spawn parameters (requested
agent ID, tool payload, parent session context) and return deny with a
reason -- the spawn is aborted and the reason is surfaced to the calling
agent as a tool error. This is the enforcement point used by
ocx-budget-manager to block spawns when a session is over budget, and by
ocx-routing-policy to reroute spawns to a lower-cost model tier.

cleanup-hook-gate.ts introduces an AsyncLocalStorage-based scope flag.
When an agent's error-cleanup hook runs, it wraps its execution in
runInCleanupHookScope(). isInCleanupHookScope() is checked at spawn time:
if true, the spawn is rejected immediately without calling plugin hooks.
This prevents runaway spawn chains triggered inside cleanup logic -- an
important safety property when cleanup hooks themselves use agent tools.

Tests (cleanup-hook-gate.test.ts, cleanup-hook-spawn-gate.test.ts,
subagent-spawn.cleanup-gate.test.ts) verify that:
  - Normal spawns pass through all hook handlers.
  - A deny result from any hook aborts the spawn.
  - Spawns attempted inside a cleanup scope are rejected unconditionally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
resolve-route.ts now accepts an optional hookRunner so the routing layer can
dispatch the before_message_route hook without coupling to the plugin registry
directly. The hook receives the resolved agent route and lets plugins redirect
to a different agent, suppress the message entirely (returning a canned
response), or annotate the routing context with extra metadata carried into
the agent run.

auto-reply/dispatch.ts is updated to pass the hookRunner through to
resolveAgentRoute so that the hook fires at the right point in the pipeline:
after channel-specific routing (pairing, allowlists, guild/team bindings) is
resolved but before the agent runner is started. This ordering is important
because the hook sees the authoritative resolved agent ID, not an intermediate
candidate.

dispatch.test.ts adds coverage for the suppress and redirect paths to ensure
a plugin returning a deny result terminates dispatch without starting an
agent run.

channel-plugins.ts is added to test-utils to provide a reusable channel
plugin fixture that registers mock hook handlers, reducing boilerplate in
routing and dispatch tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dgarson dgarson merged commit 8149260 into dgarson/fork Mar 3, 2026
1 of 3 checks passed
@dgarson dgarson deleted the dgarson/foundation-primitives-core-v2 branch March 3, 2026 02:01
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4192dabebb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +407 to +409
} catch (hookErr) {
log.warn(`before_agent_run hook failed: ${String(hookErr)}`);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block run when before_agent_run hook errors

before_agent_run is registered as a fail-closed hook in src/plugins/hooks.ts, but this catch path only logs and continues, so a thrown policy-hook error (for example, quota/HITL backend failure) lets the agent run proceed instead of denying safely. In production, that turns transient hook failures into policy bypasses.

Useful? React with 👍 / 👎.

Comment on lines +310 to +313
} catch (hookErr) {
// Non-critical — log and allow spawn to proceed.
console.warn(`[before_subagent_spawn] hook error: ${String(hookErr)}`);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject subagent spawn when policy hook fails

before_subagent_spawn is also marked fail-closed in the hook runner, but this block swallows thrown hook errors and allows spawning to continue. If a spawn-governance hook throws (e.g., external policy check outage), restricted subagent spawns are executed anyway rather than being denied.

Useful? React with 👍 / 👎.

}

const result = await tool.execute(resolvedToolCallId, executeParams, signal, onUpdate);
const result = await tool.execute(toolCallId, executeParams, signal, onUpdate);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Repair malformed tool args before executing tools

This execution path now calls tool.execute directly with incoming params, but the previous validation/repair layer was removed from the adapter. As a result, malformed or stringified tool arguments that were previously repaired now reach tools uncorrected, causing avoidable tool-call failures for the same model outputs.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant