Repository navigation
internals sub agent delegation
User guide: Sub-agent delegation.
-
cliRunner.run(cmd/steiner/runner.go) callsdelegation.BuildDelegateRegistry(internal/delegation/registry.go), which registerssub_agent,follow_up, and the advisor tool. - A
sub_agentcall withtype: visiongoes tonewVisionHandler(vision_tool.go). Other types go tonewSpecializedHandler(specialized_tools.go).follow_upgoes torunFollowUp(follow_up.go). -
newSpecializedHandler(specialized_tools.go) andnewVisionHandler(vision_tool.go) install aPreparehook. For a worktree-isolated type (UsesWorktreeinagent_type.go:codeanddocumentor), the hook provisions the worktree (provisionCodePlan,specialized_worktree.go). Every hook then builds the childagent.RunRequestthroughBuildChildRun(bootstrap.go).follow_uphas noPreparehook. -
superviseDelegate(specialized_execute.go) hands the job to theSupervisor, which callsSpawnin async mode orSpawnAndWaitotherwise. Specialized, vision, andfollow_upcalls all reach it. InSupervisor.execute(supervisor.go), the supervisor runsPrepareand then registers the child withActiveController. Jobs withoutPrepare(follow_up) register instartQueuedLocked(supervisor_queue.go) when they start. - The supervised job runs
SpawnDelegate(task.go), which returns atool.ExecutionResultwith retention metadata. - Session persistence, unregistration, and cleanup follow.
follow_up resumes the child session stored in SessionStore (internal/delegation/session.go).
- The
sub_agentschema inspecialized_tools.gorequirestypeand six brief fields:objective,context,deliverable,constraints,success_criteria, andchecks. -
researchis omitted whendeps.Searcheris nil (registry.go).visionis omitted whenModels.Effective.SubAgents["vision"]is empty orImageStoreis nil, and it requiresimage_id. - Per-type allowlists are in
agent_type.go(agentAllowlists).documentorclonescodeAgentAllowlist, so its narrower write surface is enforced bydocumentorWriteScope, not by a shorter tool list.mergedAllowedTools(specialized_tools.go) sorts and dedupes the allowed tool names.
-
tool.WriteScope(internal/tool/write_scope.go) matches writable files by final component: exact extension (leading dot included) or exact basename. Matching is case-sensitive and directory components never grant eligibility. -
Executor.WithWriteScope(internal/tool/executor.go) attaches a scope to a child executor.runPipeline(internal/tool/execution_pipeline.go) applies it to the effective policy after plan-mode restrictions, so both limits hold.PathPolicy.ensureWriteScopeAllowed(internal/tool/policy.go) checks root containment and the name rules against both the lexical path and its symlink-resolved form, denies an existing directory whose name matches, and fails closed when the policy has no root. -
mutateresolves bothmoveendpoints through the policy (planMove,internal/tool/builtin/mutate_ops.go), so a move inside the scope cannot escape it. -
agentWriteScope(internal/delegation/agent_type.go) returnsdocumentorWriteScopeforAgentTypeDocumentor.buildChildRunRequest(bootstrap.go) installs it on the child executor, so the initial run, follow-ups, and commit remediation reuse the same restriction. - The restriction covers
mutatepaths only.bash, MCP tools, and other tools that write through their own mechanisms are outside it; worktrees share git metadata with the parent repository, and hardlinks or races can bypass name checks. The user-facing rules and limits are in Sub-agent delegation.
Supervisor (internal/delegation/supervisor.go) owns child jobs. cmd/steiner/runtime_build.go builds it with NewSupervisor and builds the ActiveController with NewActiveController. MaxParallel comes from cfg.SubAgent.MaxParallel, with a floor of 1.
-
Caps. At most
MaxParallelchildren run at once. Others wait in the queue. Running plus queued children are capped at2 * MaxParallel. Beyond that,SpawnandSpawnAndWaitreturnErrOutstandingCap(supervisor_queue.go). -
Context. Children run on
context.WithoutCancel(handlerCtx)(supervisor_queue.go). Cancellation reaches them only through the supervisor's cancel methods. -
Cancellation.
CancelAgentandCancelAll(supervisor.go) take aCancelCause(cancel_cause.go). The interactive canceller passesCancelCauseUser(cmd/steiner/interactive_session.go). Phase shutdown passesCancelCauseSystem(cmd/steiner/phase_driver.go). A queued job cancelled before it starts gets its result fromOnCancelledBeforeStart. -
Shutdown.
Shutdown(supervisor_shutdown.go) rejects new spawns withErrSupervisorClosed, cancels children, and waits up to the shorter of the caller's context andJoinTimeout(10 seconds by default,supervisor.go). Jobs still unresolved getErrSupervisorClosedresults.
ActiveController (internal/delegation/active.go) tracks live children by agent ID. RegisterWithCancel stores each child's cancel function, agent type, and worktree, and returns ErrAgentAlreadyActive for a duplicate ID. The child's context is created in Supervisor.enqueue (context.WithCancel(context.WithoutCancel(handlerCtx))). CancelAgentWithDiscard cancels one child.
AsyncSubAgents is fixed for a session. newDelegateDeps (cmd/steiner/runner.go) sets it from asyncSubAgents() (cmd/steiner/runner_run.go), which is true for every run mode except exec. In exec, calls block until the child finishes.
In async mode, superviseDelegate returns an AckResult (ack.go) with status running or queued and the agent ID needed for follow_up.
The conversation driver (internal/agent/conversation_driver.go) calls Supervisor.MarkDelivered (supervisor_async.go) on delivery and coalesces completions within completionCoalesceWindow (five seconds, internal/agent/conversation_driver_policy.go). Deliveries emit output.SubAgentsDeliveredEvent. output.SubAgentResultsUnansweredEvent is emitted from internal/interactive/unanswered_results.go.
-
Ledger.
Supervisor.Ledger()lists outstanding children, andIsPendingreports whether one is still outstanding (supervisor_async.go). Sessions persist the ledger asSubAgentLedger(internal/agent/sub_agent_types.go,internal/session/session.go) and group state asDelegationGroups(internal/agent/delegation_groups.go,internal/session/session.go). -
Groups.
jobGroup(supervisor_groups.go) holds one label's members within a tool batch.sealBatchseals the batch's groups.releaseGroupLockedholds completions until a group is sealed and every member has a final result, then posts them together, ordered bySeq. -
follow_up admission.
follow_upis rejected atFollowUpCount >= SubAgent.MaxFollowUps. In async sessions it is also rejected while the agent is still pending (Supervisor.IsPending) (follow_up.go).
Supervisor.ClearFinishedSubAgents(scope) (supervisor_async.go) atomically discards unacknowledged finished results for one scope. It runs under Supervisor.mu and is all-or-nothing: every unacknowledged job must match scope exactly, already hold a completion, and have reached phaseDone. Otherwise it changes nothing and returns the outstanding count, leaving the completions and groups available for delivery. An empty scope matches jobs enqueued without a group scope; it is not a wildcard.
On success it acknowledges and prunes each job, removes it from its jobGroup (discardGroupMemberLocked, supervisor_groups.go), and deletes any group left empty, so a later SealGroupBatch cannot post the discarded members. It does not post to the completion sink itself, and a later MarkDelivered finds the jobs already acknowledged. The discarded jobs also leave Supervisor.Ledger(), so the old driver's retire save (writeLineageAs, internal/interactive/driver_host.go) omits them from the replaced session's SubAgentLedger. /clear calls this only when the live driver is waiting with completions held and every outstanding result is finished in that driver's group scope; /new, session switching, and forking still refuse every pending result through pendingRefusalLocked (internal/interactive/persistence.go).
For a worktree-isolated type (UsesWorktree in agent_type.go: code and documentor), the Prepare hook (provisionCodePlan, specialized_worktree.go) runs these steps at dequeue, before BuildChildRun:
-
DirtyPaths(worktree.go) reports uncommitted and untracked parent changes, best effort. Found paths add a warning, because the worktree starts from parent HEAD and omits them. -
ProvisionCodeWorktree(worktree.go) creates a git worktree on adelegate/branch from parent HEAD, at.steiner/worktrees/<processHash>/<sanitizedBranch>/<agentID>. A failure fails the call. -
codeRemediationConfig(specialized_worktree.go) sets post-run remediation for the worktree. -
WorktreePath,WorktreeBranch, andWarningsare host-only result fields. The model receives only the project-relativeworktree_path, for worktree-isolated results and follow-ups (see Result and retention).
Other types skip provisioning. denyFollowUpOnDeadCodeWorktree (follow_up.go) runs before the child is registered and rejects a worktree-isolated follow-up whose worktree is missing or invalid.
steiner worktrees (cmd/steiner/cmd_worktrees.go) supports --list, --prune <id>, and --prune-all. Pruning (worktree_prune.go) removes only git-reported worktrees on delegate/ branches. A directory that git does not track, such as one left by a crash during provisioning, must be removed by hand.
BuildChildRun (internal/delegation/bootstrap.go) builds the child RunRequest:
-
Limits.
SubAgentdefaults ininternal/config/defaults.goareMaxTurns120 andMaxTokensset fromDefaultSubAgentMaxTokens. Timeout defaults to none.DefaultLimits(limits.go) raisesMaxTurnsto at leastconfig.MinSubAgentMaxTurns. -
Prompt. The child receives one user message carrying the six-field brief. Sandbox state (
SandboxEnabledandsandbox.WritableHostMounts) is passed to the child as plain values (cmd/steiner/runner.go). -
Registries.
buildChildRegistriestakesRegistry.Subsetof the parent base registry and always excludesfollow_upandworkflow_handoff. Child tool events classify only asParallelClassToolorParallelClassNone(bootstrap.go). -
Executor.
scopedToolExecutor(bootstrap.go) sets the child agent ID withtool.WithApprovalAgentScope. Approval code ininternal/agent/approval.goandinternal/interactive/wiring.goreads that scope. Forexploreandevaluatechildren with the sandbox enabled,readOnlyBashappliestool.WithReadOnlyProjectBash. The parent's mode getter is wired in withWithModeGetter, andagentWriteScopeaddsWithWriteScopefor restricted types. -
Advisor. The advisor tool is added only when the child's allowlist contains it and
AdvisorForChildis set.code,documentor,review, andevaluateallowlist the advisor (agent_type.go). Uses are capped byAdvisorConfig.MaxUsesPerSubAgent(default 1) and counted inAdvisorBudgetStore(advisor_budget.go). -
Context flags.
SkipAgentsis true only forvision.SkipProjectContextis true for every type exceptcode,documentor,review, andevaluate(childContextSkips,bootstrap.go). -
Mode.
checkPlanModeWriteDenial(specialized_tools.go) denies worktree-isolated types (UsesWorktree, socodeanddocumentor) in plan mode.denyFollowUpInPlanMode(follow_up.go) deniesfollow_upwhen the child hasmutate. -
Workflow mode.
childWorkflowMode(bootstrap.go) mapscodetoDelegatedCodeSubAgentWorkflowModeanddocumentortoDelegatedDocumentorWorkflowMode.explore,research,evaluate,sanity_check,review, andvisionmap toDelegatedNonCodeChildWorkflowMode. Other types useDelegatedChildWorkflowMode. -
Cache key.
CacheKeyStore.KeyFor(cache_keys.go) assignsPromptCacheKey. Cache rules are in Prompt caching.
Child model resolution is in resolveModel (specialized_tools.go). It uses the per-type alias from Models.Effective.SubAgents, else the profile's DefaultModel, else the parent model. Assignments are read through CurrentEffectiveModelAssignments on each spawn, so a profile switch reaches later spawns. A vision child with no alias reuses the parent model.
SpawnDelegate (task.go):
- Applies
Limits.Timeoutwhen set and records the start trace. Callers emitdelegation_startedbefore calling it (emitDelegateStartedinspecialized_execute.goandexecuteFollowUpinfollow_up.go). - Runs the child, then maps its final state to a status.
- Emits
delegation_completeordelegation_failedand returns the result.
Stop reasons (result.go):
-
completemaps tocomplete. -
max_turnsandmax_tokensmap topartial. -
cancelledmaps topartialwhen the child had turns and produced output or tool calls, otherwise tocancelled. The child session stays resumable. -
errorandusage_limitmap tofailed. Ausage_limitfailure carriesusageLimitDelegateNotice(task.go), which tells the parent to delegate a fresh sub-agent. - Any other reason maps to
failedwithunknown stop reason: <reason>.
Failure classification reads the child context snapshot childCtx.Err() that SpawnDelegate (task.go) captures when runner.Run returns, before the deferred limit cancel. failedDelegateExecution and runStateFields use that snapshot, while the runner error supplies the failure message and usage-limit notice. A provider transport timeout that wraps context.DeadlineExceeded on a live child context is therefore failed; only a done child context maps to partial or cancelled and marks the session resumable. The provider retries that timeout inside the child only before visible stream output; once visible chunks have been emitted, it returns the timeout so the child run fails.
Token counters accumulate across follow-ups, seeded from Spec.PriorTokenUsage.
internal/agent appends turn-budget notices at 50, 75, and 90 percent of the budget (turnBudgetNoticeFractions, internal/agent/turn_budget_notice.go). A follow-up counts from its own start turn (RunState.BudgetStartTurn). The notices do not change the limit.
CacheKeyStore.beginDispatch (cache_keys.go) lets the first child for a cache key dispatch at once. Later children with the same key wait until the leader releases the gate or dispatchGateTimeout (10 seconds) elapses. Waiting emits delegation_cache_waiting; the end of the wait emits delegation_cache_released with waited_ms and a reason of leader_first_response (the leader emitted its first provider response, successful or not), leader_exited (the leader finished, failed, or was cancelled before one), timeout, or cancelled (the follower's context ended while waiting).
bootstrap.go sets RunRequest.ParallelClassOf. internal/agent/delegation_admission.go uses it to identify delegation calls.
Result (internal/delegation/contract.go) carries the terminal status (complete, partial, failed, or cancelled), output, turn and token counts, stop reason, and worktree fields. The provider receives only the projection DelegationResultEnvelope (internal/agent/delegation_projection.go), built by Result.ProjectToolResult (result.go):
| Field | Content |
|---|---|
output |
Exact child output |
status |
Omitted on success. Otherwise partial, cancelled, or failed
|
reason |
The error for failures (unknown failure when empty), or a fallback such as limit reached
|
continuation |
agent_id of a persisted child session that can be resumed. Set only when the child is persisted. |
worktree_path |
Project-relative path of a worktree-isolated child's worktree. |
Delegation results also set ToolRetention with kind delegate_summary (RetentionKindDelegateSummary, task.go).
Host-only diagnostics stay out of the provider payload described above.
-
NewTraceLoggerWithDiagnostics(trace_log.go) writes through the diagnostics writer asKindToolrecords fromSourceSubAgentwhen one is set. Otherwise it writes to the file path it is given, and writes nothing when that path is empty.LogPathderives that path from the log file as<base>-delegation<ext>, using.logwhen the log file has no extension. Treat the file as sensitive, like--log-file. -
SpawnDelegateandfailedDelegateExecution(task.go) add atool_callsentry with the trace file and tool-call counts. - Per-tool traces are JSONL files at
.steiner/traces/<session-id>/<agent-id>.jsonl(tool_call_trace.go). Traces are pruned aftertoolCallTraceRetention(seven days). The session ID is the threadedSessionID: the interactive session ID (cmd/steiner/runner.go) or the oneshot run ID (cmd/steiner/cmd_oneshot.go).traceSessionIDfalls back toprocessTraceSession, a random per-process ID, when it is empty. - Failure classes in trace records follow
mutateFailClasses, which mirrorsclassifyinscripts/mutate-session-stats.mjs. Keep the two in sync.
-
CancelDelegate{AgentID, Discard}andCancelAllDelegatesreachSupervisor.CancelAgentorSupervisor.CancelAllthrough the consumer-definedDelegateCanceller. - Finalization runs for a targeted cancel with
Discardset. After the child run ends, it:- Calls
SessionStore.Invalidate, which deletes the session and records an agent-ID tombstone.GetandSaverefuse tombstoned IDs, so a late save cannot restore the session. - Removes the delegate worktree with
PruneCodeWorktree(worktree_prune.go) undercancellationCleanupTimeout(30 seconds,cancellation_finalize.go).
- Calls
- Each disposal attempt emits
delegation_worktree_disposal(NewDelegationWorktreeDisposalEvent,internal/output/event_delegation.go).
newVisionHandler (vision_tool.go) validates the brief and image_id, resolves the image from ImageStore, and base64-encodes it (loadVisionImageBlock). It runs the child on the sub_agents.vision assignment with the image block in the request.
ImageStore (internal/agent/image_store.go) maps img-N IDs to ImageRef values. Its images directory is .steiner/tmp/images under the working directory.
Delegation events (internal/output/event_types.go): delegation_accepted, delegation_queued, delegation_started, delegation_cache_waiting, delegation_cache_released, delegation_complete, delegation_failed, and delegation_worktree_disposal.
Supervisor.enqueue (supervisor_queue.go) emits delegation_accepted for each accepted job and delegation_queued only when the job waits for a running slot.
A child cancelled while queued, or after dequeue but before its runner starts, emits delegation_started, a scoped cancelled stop_reason, and delegation_complete with status cancelled (emitDelegateCancelled in task.go). This covers a queued job cancelled in its OnCancelledBeforeStart callback (specialized_execute.go, follow_up.go), including queued shutdown, and a dequeued specialized child whose context is already done when executeRegisteredDelegate checks it after the dispatch gate (finishCancelledBeforeDispatch, specialized_execute.go). Neither path reaches SpawnDelegate, so this terminal event is the only completion the roster sees; without it the occurrence stays running. The child never ran, so usage counts are zero. A cancellation that surfaces as an error from the Prepare hook during worktree provisioning is outside this sequence: the prepare wrapper in runRegisteredDelegate (specialized_execute.go) emits delegation_failed and the call returns a setup error.
Grouped card boxes are cached in settledGroupRows and groupBoxCache (internal/tui). markDelegationDirty (content_events_delegation.go) sets renderDirty and calls invalidatePrefixIfCached. dirtyDelegationCard (delegation_jump.go) sets renderDirty and bumps gen. After any entry change, call one of them. A direct renderDirty writer must also call invalidatePrefixIfCached or bump gen, or the settled prefix serves stale output. AdvanceDelegationSpinners must not set renderDirty. Inputs that change a settled entry's box belong in settledRowsKey.
-
delegationInstructionsininternal/prompt/system.gois gated onDelegationEnabled. The## Your sub-agentsroster renders from thespecialistsslice ininternal/prompt/specialists.go. Edit the slice, not the rendered markdown. -
sectionDocumentorChild(internal/prompt/system.go) rendersdocumentor_child.md.tmplonly inDelegatedDocumentorWorkflowMode, so its duties and write-scope text stay isolated from the parent and other child modes. - Cache inputs in
internal/agent/context_manager_base.goincludeDelegationEnabled. - Consumer files (
skills/*/SKILL.mdandinternal/oneshot/prompts/*.md) reuse canon blocks. Drift rules are in canon drift checks. The shared blocks are pinned byskills/shared_blocks_test.goandinternal/oneshot/prompts_shared_test.go.
- Sub-agents cannot nest.
- Concurrent children are bounded by
MaxParallel. - Diagnostics and traces are host-only. The model receives only the projection.
- A write scope is a file-name guard on
mutate, not a security boundary. Tools that bypass the path policy, shared git metadata, and filesystem races are outside it.
Get started
Using Steiner
- How to work with steiner
- Interactive mode
- CLI reference
- Skills
- Sub-agent delegation
- Oneshot
- Context management
- Advisor
- Security audit
Tools and integrations
Safety
Usage and cost
Reference
Internals and maintenance