Subagent runtime contracts
Use this page to understand what survives a delegate run, how reports reach the parent, and which runtime policies apply to child sessions. For tool parameters, start with Job control; for remote clients, use Integrations. These contracts track main.
Read a delegate result correctly
A delegate is a durable conversation identified by dlg_..., not a shell job. It can run, report, become idle and run again when its owner sends new input. Its internal generation is not another public handle. Keep the stable delegate ID for control and the session transcript reference for evidence.
Completion of one generation does not prove that the requested work is correct. Read the terminal message, structured output and cited artifacts, then perform the task's acceptance check. A detailed terminal message can differ from its structured summary; do not discard either. Failed, cancelled or exhausted runs are not successful reports. The report-preservation section below defines the accepted capture and recovery behavior.
An idle delegate is still available for follow-up. A stop request instead cancels its subtree. Do not infer cancellation from a disconnected client or confuse a metadata-only status read with consumption of a result packet.
This reference covers shipped runtime policy for delegates, plugins, hooks, helpers, and lineage. See job control for tool parameters, results, allowance, and availability; architecture for runtime mechanics; and the stable delegate model for the lifecycle contract. Earlier design specs are historical proposals, not current behavior. Source citations use paths and symbols so readers can locate the implementation after line numbers change.
Stable attention drives revalidate transcript-owned pending IDs after claiming the retained child for a start (Session.driveStableDelegateAttention). A delayed drive cannot reserve an ID an intervening generation already consumed and finished. A start already accepted into the transcript retains its exact reservation and generation across a failed delegate-journal commit; retry does not create a second marker or generation. Parents continue to drive the child's attention, and fresh owner work remains admissible after the attention settles.
Delegate report preservation
A successfully reported generation's terminal packet message is the accepted terminal communicate.message, with communicate's existing trimming and output.message fallback. Its structured result remains separate, including a different summary, data, artifacts, or a custom-schema value. Both come from the same accepted capture under the session lock (delegateTerminalRunInputs.captureCommunicateReport, agent/subagents.go). Competing terminal calls cannot replace that capture. Failed, cancelled, exhausted, and unreported runs retain their diagnostic result semantics, and root-session return values and CommunicateOutput keep their structured-output semantics.
Settlement persists an immutable terminal packet. Owner notifications and synchronous results consume that packet, and journal recovery reuses it across both prepared-before-settlement and settled-but-undelivered windows. Recovery does not ask the child to report again or allocate a new generation or delivery identity (delegateTreeController.Reconcile, agent/delegate_tree_restore.go, and deliverDelegatePacket, agent/delegate_delivery.go). Existing receipts, storage limits, and preview bounds still apply. This contract neither changes notification timing nor reconstructs detailed reports absent from historical packets.
Effective capability policy
A child agent can narrow but never expand the capability set available to its parent/session. This is enforced structurally by a single per-session tool registry, not by parallel policy checks:
- One registry, narrowed at child init.
initSessionState(agent/session_init.go) builds the full registry, then applies the spawn restrictions:RestrictKeepingResultTool(agent/internal/tool/registry.go) drops every tool not in the child's allowed set except the result tool, andRemovedrops each denied tool. Atools: allagent gets exactly the tools effective in this child session — not every tool registered in the process. - Visibility == execution. The advertised tool set (
rebuildToolDefsCache,agent/session_tools.go) and the execution gate (Registry.ExecuteCall,agent/internal/tool/registry.go) read the sames.reg. A tool removed from the registry is therefore both invisible to the model and unexecutable — a forged or stale model-returned call to a filtered tool hits the unknown-tool path and is denied before execution. The two surfaces cannot disagree because there is only one source. grant_toolsintersects, never expands. A parent can onlygrant_toolsa tool the session can already call; the spawn path rejects a grant of a root-only or not-currently-callable tool (agent/subagents.go, e.g."cannot grant tool %q via grant_tools…","cannot grant tool %q: it is not currently callable…").- Delegation is allowance-gated. A child with
delegation_allowance: 0cannot calldelegate; it retainsjob_watch, subject to tool policy and watch-source permissions. A positive allowance permits delegation, subject to tool policy, regardless of depth. See delegation allowance. - Close/cancel denies new child work. The stable delegate-tree controller admits starts and sends against one fenced lifecycle aggregate. Once a session or subtree is closing/stopping, new child/model/shell admission cannot commit below that fence (
delegateTreeController,agent/delegate_tree_controller.go;delegateRuntime,agent/delegate_runtime.go).
Not yet centralized (these hold today via code ordering / per-feature resolution, not a single enforced gate — see subagent-management/10-runtime-contracts.md Contract 1 for the target): policy immutability after a tool execution starts is a property of the sequential PreToolUse → execute → PostToolUse path, not a frozen-policy token; an agent definition naming an unknown MCP tool yields a silently-absent tool (a forged call later gets unknown-tool) rather than a deterministic spawn-time error; and an agent definition naming an unsupported provider feature is resolved/overridden rather than rejected with a policy error. MCP server unavailability is now non-fatal and parallel (previously fail-fast): each server's transport build, connect, and tools/list run concurrently in its own goroutine; a failure there, or later during tool registration, is isolated to that server rather than aborting the batch, reported as a ServerOutcome (agent/internal/mcp NewManager/RegisterTools). Each outcome becomes a pendingMCPWarnings entry classified SourceMCP (agent/diagnostic), flushed after SESSION_START (agent/session_init.goinitMCP → emitSessionStartEnvelope) — the session constructs with whichever servers came up healthy, and zero healthy servers is still a healthy session. A connection dropped mid-session gets exactly one call-driven lazy reconnect attempt, gated on errors.Is(err, mcpsdk.ErrConnectionClosed) (conn.reconnect). CLI --mcp-config/--mcp config-parse errors (agent/mcpconfig Discover) are the one thing that still stays fatal, since they're explicit user input for this invocation.
Plugin and agent loading
What validation actually runs when evener loads a plugin (agent/plugin/plugin.go, agent/plugin/agents.go):
- Plugin name is required and must be kebab-case (
validatePluginName/kebabCaseRe). - Claude manifest first.
Loadreads.claude-plugin/plugin.jsonand falls back to.codex-plugin/plugin.jsononly when the first is absent, not malformed. See the plugin package format. - Duplicate plugin names use the first valid manifest. Session loading uses
LoadAllFailSoft: later manifests with the same name are skipped with a warning, even if an unrelated component of the first plugin fails to load. The separateLoadAllAPI rejects duplicates; it is not the session-loading policy. - Agent
name/descriptionare required non-empty strings (getString).toolsaccepts string lists, comma-separated strings, single tool names, or scalarall; see role metadata and validation for accepted and rejected forms. Claude tool names are mapped to evener canonical names at load (toolname.ClaudeToEvener).model/colordefault toinherit/blue. - Agent
modelis a provider-local preference. Evener selects the first usable source in this order: an available plugin model, the delegate call's explicitmodel, then the parent model. Plugin metadata never switches providers.sonnet,opus, andhaikuare catalog aliases that resolve to concrete models before Evener checks the current provider instance's advertised model list. An exact custom model ID works when that instance advertises it. An unadvertised model, or one whose availability cannot be verified, falls through with a diagnostic warning. - Namespacing. Plugin agents are exposed as
plugin:agent, except acoordinator-workflowplugin exposes its bundled agents by bare name (exposedAgentCatalogKey,agent/coordinator_workflow_plugin.go). Plugin MCP servers are namespacedplugin_<plugin>_<server>(discoverPluginMCPConfigs);.mcp.jsonis the file source, merged with inline manifestmcpServers. - Missing component directories are tolerated — a plugin with no agents/hooks/MCP is valid.
Not yet (these are proposed in subagent-management/06-plugin-agent-validation.md, not built): agent-name kebab validation, duplicate agent-name rejection within a plugin (a later agent silently overwrites an earlier same-name one), manifest field-type validation, agents-override path-traversal/absolute-path rejection, model-value validation, empty-body warnings, unsupported-Claude-field rejection, and any Validate* Go API / evener plugin validate CLI. An inline mcpServers parse failure is no longer a silent gap: it now degrades to a plugin-level MCP warning (Instance.MCPConfigWarnings, agent/plugin/plugin.go discoverPluginMCPConfigs) — the plugin's MCP layer is skipped but its skills/agents/hooks still load; a malformed .mcp.json file and malformed inline JSON are warned the same way. One silent gap remains: skills/tasks entries are not value-validated (empty strings/fields pass).
Lifecycle hooks (Claude-compatible subset)
Evener fires a tested subset of the Claude hook contract; the rest is recognized but reserved. (agent/plugin/hooks.go, agent/internal/hooks/.)
- Nine events fire:
SessionStart,UserPromptSubmit,PreToolUse,PostToolUse,Notification,SubagentStop,Stop,PreCompact,SessionEnd(validHookEvents). The full ~30-name Claude vocabulary is recognized asreserved-placeholder— parsed, diagnosed, never fired (recognizedClaudeEvents); unknown names are bucketed separately and warned loudly at load. - Discovery: inline manifest object, manifest string path, else default
<dir>/hooks/hooks.json; both wrapper ({description?, hooks:{}}) and direct (events-at-top) JSON shapes parse. - Matchers (
agent/internal/hooks/matcher.go): empty/*= all;[A-Za-z0-9_|]= exact or pipe-list; otherwise Go RE2.Bashdoes not matchBashOutput. Matchers run against the Claude tool name (shell→Bash). An invalid regex is skipped and diagnosed once at load, not per dispatch. - Handlers: only
commandandpromptexecute;http/mcp_tool/agentare reserved and skipped-with-diagnostic.commandsupports shell-form (bash -c) and exec-form (args);${CLAUDE_PLUGIN_ROOT}/${PLUGIN_ROOT}expand in command/prompt/ args; the runner setsCLAUDE_PLUGIN_ROOT,PLUGIN_ROOT,CLAUDE_PROJECT_DIR, andCLAUDE_EFFORT(only when configured). It does not setCLAUDE_CODE_REMOTEorCLAUDE_PLUGIN_DATA.promptis evener-native sugar that runs an LLM call with$TOOL_INPUT/$TOOL_RESULT/$USER_PROMPT/$MESSAGE/$TOOL_NAMEsubstitution. PreToolUseruns before registry schema validation, can rewrite the tool input (applyUpdatedToolInput, applied before execution), and can deny (short-circuits the call).allow/denywithpermissionDecisionReasonare honored, including the deprecatedapprove→allow/block→denymapping (preferred keys win);ask/deferare recognized but not honored (the tool proceeds with a diagnostic).- Exit-code 2 blocks only for
PreToolUse,Stop,SubagentStop(agent/internal/hooks/exitcode.goexitBehavior); every other event is non-blocking. JSON output is parsed only on exit 0; on exit 2 stdout is treated as stderr and JSON is ignored. - Output routing:
additionalContextgoes to the model (wrapped as a system reminder);systemMessagegoes to the user only — distinct channels (routeOutput). - Verification: inspect startup warnings for load errors and use the startup-hook example's explicit warning to confirm execution. Browser
/statusopens Overview's About tab, not a hook tier/count display.
Known gaps (stated so you don't over-trust the compat surface): UserPromptSubmit and PreCompact do not enforce the exit-2 block even though Claude's contract blocks; and continue/suppressOutput/terminalSequence are parsed but have no consumer (a hook returning continue:false still runs to completion). if/async/statusMessage and the http/mcp_tool/agent handlers are parsed-but-inert. See subagent-management/07-lifecycle-hooks-claude-compat.md for the full reserved set.
Events and diagnostics
- Typed lifecycle payloads (
agent/events/payloads.go):JOB_STARTEDandJOB_FINISHEDcarry only shelljob_id/job_type="shell"state.DELEGATE_UPDATEDcarries the immutable stable delegate snapshot keyed bydelegate_id, with its monotonic projection revision. The emitting session's ID rides on the envelope (SessionEvent.SessionID). Model-facing shell and delegate notifications remain separately typed; seejob-control.md. - Warnings are first-class and surfaced, never silent.
WarningData(agent/events/payloads.go), carried on theEventWarningevent, flows on the session event stream that clients consume; emitting paths include plugin hook-config warnings (built inagent/session_init.go, emitted viaemitDiagnosticWarning) and delegate task-seed failures (agent/subagents.go). A warning with no display surface is treated as a bug. - Policy denials name the action and the boundary, not just "not allowed" — e.g. the
grant_toolsrejections above and the delegation-allowance and ownership errors in the stable delegate controller. A role's tool policy and the child's allowance govern which delegation tools it can receive. - Compatibility gaps are reported, not half-accepted. Recognized-but-unsupported and unknown hook events are recorded (
Instance.UnsupportedHooks/UnknownHooks) and turned into load-time warnings; reserved handler fields are captured for diagnostics rather than treated as working.
Do not assume warnings are secret-safe. Hook lifecycle event payloads contain metadata, not hook commands, environment values, or stdout/stderr. Free-form warnings have a different contract: a failed SessionStart warning includes the first non-empty line of stderr, or stdout when stderr is empty, truncated to 160 characters plus an ellipsis if longer (agent/internal/hooks/hooks.go, sessionStartFailureWarning). Hooks can also supply systemMessage warnings or model context. These messages have no dedicated secret-redaction layer. Keep secrets out of hook output and inspect warnings and transcripts before sharing them. See hook output routing.
A shared redaction helper and grouped validation issues remain proposals in subagent-management/10-runtime-contracts.md Contract 3.
Lightweight helper isolation
Several operations make a one-shot LLM call that is not a delegate. These helper calls route through llm.Client.Complete/llm.GenerateObject directly and never register a stable delegate, create a child transcript, mutate the task store, run tools, or appear as a delegate item in job_list. Verified call sites:
web_fetchcheap-model summarization (agent/tool_web_fetch.gowebFetch),- session auto-naming (
agent/session_namer.gonameSession, advisory — failure does not fail the session), - image description (
describeImage,agent/session_tools.go— side-channel call, noToolsset), - prompt hooks (
agent/internal/hooks/hooks.goclientAdapter/executePromptHook), - context-manager strategies (fork-summarize, recursive distill, checkpoint prediction, memory crystals —
agent/internal/contextmgr/*).
The isolation is structural: a delegate enters the stable controller only through the registered create path (agent/delegate_runtime.go and agent/delegate_tree_controller.go), which none of the helper sites reach. Because helpers all route through llm.Client.Complete (llm/client.go) — directly, or via llm.GenerateObject/Generate for the schema'd ones like session naming — they still inherit request validation, provider resolution, middleware/logging, and error stamping. The proposed shared helper API (GenerateHelperText, LoadLLMClient, …) in subagent-management/08-standalone-llm-calls.md is not built — these sites achieve tool-freeness simply by never populating Tools.
History and lineage
Fork and subagent are distinct relations sharing lineage fields, kept explicit:
- Fork copies history; delegates start fresh by default.
ForkSession(agent/fork.go) replays the parent's firstdivergenceTurn-1entries into the child transcript. A delegate starts with a fresh conversation unless the caller setsfork_context: true, which copies the parent's recorded conversation, excluding the unfinished tool round. The delegate keeps its own role and permissions. See forked context. - A fork may defer the diverging input.
ForkSessionAtUserTurn(agent/fork.go) writes only the prefix (no replacement user turn) and returns the original message text, so the web UI's fork-from-message flow can stage it in the composer for editing before explicit submission — the forked session never auto-runs it. The divergence turn must still be aUSER_INPUTentry; both variants record the sameParentSessionID/DivergenceTurnlineage.AsideSession(agent/aside.go) is the degenerate fork at the tip: it copies the parent's full transcript with no divergent edit, recordsDivergenceTurnone past the tip, and inherits the parent's config — the machinery behind the/asideside-thread command. Both are built on the sharedwriteForkChildhelper; the nil replacement-turn variant covers both the deferred-input fork and the aside. - Lineage lives in metadata and headers, discoverable without reading transcript bodies.
SessionMeta(agent/schema/snapshot.go) carriesParentSessionID,DivergenceTurn,ForkLabel, andIsSubagent; the transcriptHeader(agent/transcript/transcript.go) carriesParentSessionID(andParentToolCallID).ForkSession(agent/fork.go) writes the child header and meta inline from the same values; subagent spawn setsParentSessionIDat init (agent/session_init.go).IsSubagentis true when spawned with a parent and false for a fork. - Parent lineage is immutable and preserved across meta rewrite and resume (
ParentSessionID/DivergenceTurnare write-once; round-tripped by the meta save path; covered byagent/fork_test.goTestForkSession_ChildLineagePreservedAcrossMetaRewrite/TestForkSession_ParentForkLabelPreservedAcrossMetaRewrite). - The
ForkLabellives on the parent, not the child (the child's is empty), so the label marks which branch diverged. children_ofis a metadata-derived view, not a stored index.find_session_transcripts(children_of=…)(agent/session_tools_find.go) scans*.meta.jsonat query time, returns both forks and subagents, and tags each with akinddiscriminator; results are scoped to the parent's project and gated on a readable transcript. A missing parent reference is a diagnostic, not a crash.- Parents reference children, not inline their bodies — delegate results carry a
transcript_ref(encodeRef), and child transcripts are read explicitly and bounded via the transcript tools.
Not built (proposed/assessed, not committed): the session-tree API (ListSessionTree etc.) is deferred in subagent-management/09-session-tree-history-assessment.md in favor of the metadata + children_of baseline above. Two known characterization gaps: the transcript header alone cannot distinguish fork from subagent parentage (use the metadata kind), and a single parent forked more than once has only one ForkLabel slot (later forks overwrite it).
Where the design specs live
The proposals, target contracts, acceptance criteria, and test matrices behind these shipped behaviors are in subagent-management/: 06-plugin-agent-validation, 07-lifecycle-hooks-claude-compat, 08-standalone-llm-calls, 09-session-tree-history-assessment, and 10-runtime-contracts. Those are point-in-time design records. The shipped stable-delegate model is 11-delegate-resource-model; this doc remains the current-reality reference for the cross-cutting runtime around it. When a behavior described here changes, update both applicable evergreen authorities.