22 KiB
task
Spawn subagents — one per call, or a
tasks[]batch per call (task.batch, default on). Withasync.enabled=true, spawns run in the background; otherwise the call blocks until they finish. Execution mode is per item: an item whose agent type declaresblocking: true(e.g.scout) runs inline and returns its result in the call, while non-blocking items in the same call still spawn as background jobs.
Source
- Entry:
packages/coding-agent/src/task/index.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/task.md - Key collaborators:
packages/coding-agent/src/task/types.ts— dynamic schema, progress/result types, output caps.packages/coding-agent/src/task/discovery.ts— discover project/user/plugin/bundled agents.packages/coding-agent/src/task/agents.ts— bundled agent definitions and frontmatter parsing.packages/coding-agent/src/task/executor.ts— create child sessions, run subagents, collect output, hand finished sessions to the lifecycle manager.packages/coding-agent/src/registry/agent-lifecycle.ts— idle-TTL parking and revival of finished subagents.packages/coding-agent/src/registry/agent-registry.ts— process-global agent directory (running | idle | parked | aborted).packages/coding-agent/src/async/job-manager.ts— background job registration, progress, and result delivery.packages/coding-agent/src/task/parallel.ts—Semaphoreused for the session-scoped concurrency bound.@oh-my-pi/pi-natives(crates/pi-iso) — isolation PAL:isoResolve/isoStart/isoStopbackend resolution and fallback.packages/coding-agent/src/task/worktree.ts— isolation mode mapping (parseIsolationMode) and lifecycle (ensureIsolation/cleanupIsolation), patch capture, branch merge.packages/coding-agent/src/task/output-manager.ts— session-scopedagent://id allocation.packages/coding-agent/src/task/name-generator.ts— default AdjectiveNoun agent ids.packages/coding-agent/src/internal-urls/agent-protocol.ts— resolveagent://<id>to saved subagent output.packages/coding-agent/src/internal-urls/history-protocol.ts— resolvehistory://<id>to a concise transcript.packages/coding-agent/src/tools/index.ts— tool registration and recursion-depth gating.packages/coding-agent/src/sdk.ts— child-session router/tool wiring and per-subagentAgentOutputManager.docs/task-agent-discovery.md— deeper discovery and precedence notes.
Inputs
The wire schema is shape-swapped by task.batch (default on). One unit of work is the task item { name?, agent?, task, isolated? } (isolated only when task.isolation.mode is not none):
- Batch shape (
task.batchon):{ context, tasks: item[] }— one subagent per item, all run under the same fan-out rules; there is no top-level agent field.contextis required shared background rendered into every spawned subagent's system prompt (CONTEXTsection);agentandisolatedare per item, so one call may mix agent types. - Flat shape (
task.batchoff):{ ...item }— exactly one spawn per call. Shared background goes into alocal://file (e.g.local://ctx.md) that each spawn'staskreferences; subagents share the parent'slocal://root.
| Field | Type | Required | Description |
|---|---|---|---|
context |
string |
Yes (batch) | Shared background prepended to every spawn of the call via the subagent system prompt. Rejected when task.batch is off. |
tasks |
array |
Yes (batch) | One task item per subagent. Provided names must be unique within the call (case-insensitive). Rejected when task.batch is off. |
name |
string |
No | Stable agent name — becomes the registry/IRC id. Defaults to a generated AdjectiveNoun name. Uniquified per session by AgentOutputManager. Item field in batch shape, top-level in flat shape. |
agent |
string |
No | Agent type to run this item (e.g. scout). Defaults to the spawn policy's default agent (usually task); items in one batch call may use different agent types. Item field in batch shape, top-level in flat shape. |
task |
string |
Yes | The work — complete, self-contained instructions. Empty-after-trim is rejected. Item field in batch shape, top-level in flat shape. |
isolated |
boolean |
No | Run in an isolated workspace and return patches. Exists only when task.isolation.mode is not none; per item in batch shape, top-level in flat shape. Isolated agents are torn down at completion — not revivable. |
There is no wire label field: the one-line UI label shown in the TUI/registry is generated automatically from the task text by the tiny/title model (fire-and-forget), so callers never provide it.
Runtime stays permissive: the flat form is accepted even while task.batch is on (internal callers such as the commit flow's analyze_files, and stale transcripts). The model only ever sees one shape.
There is no per-call schema parameter. Structured output comes from the agent definition's output frontmatter, the inherited parent session schema, or — for ad-hoc workflows — the eval bridge's agent(prompt, schema).
Outputs
The tool returns one text block plus details: TaskToolDetails.
Background response (async.enabled=true):
content:Spawned agent `<id>` (job `<jobId>`). The result will be delivered when it yields. ...plus a coordination hint (ircDM when enabled, otherwisejob). A batch call instead returnsSpawned N background agents using <agent types>. ...(the deduped per-item agent types, comma-joined) with a per-agent-(job)listing.details:{ projectAgentsDir, results, totalDurationMs, progress: [<AgentProgress per spawn>], async: { state, jobId, type: "task" } }. The call keeps one sharedprogress[]snapshot;async.jobIdis the first started job andasync.stateaggregates over the async spawns ("running" until every job settles, "failed" if any spawn failed) — jobs that settled before the call returned are already reflected. A mixed call'sresultscarries the blocking spawns' inlineSingleResults (pure background calls returnresults: []).- Live progress keeps streaming into the same tool block via
onUpdate(...); each final result arrives later as an async-result injection into the parent conversation. The delivery text appends a follow-up hint:<id> is now idle — message it via `irc` to follow up; transcript at history://<id>(aborted variant points at the transcript only).
Settled response (async.enabled=false, no job manager, every item's agent blocking: true, or async job body):
content: summary rendered frompackages/coding-agent/src/prompts/tools/task-summary.mdwith a preview capped at 5000 chars;agent://<id>holds the full output. A sync batch concatenates the per-spawn summaries.details.results: oneSingleResultper spawn;usage,outputPathspopulated (aggregated across spawns for a sync batch).
SingleResult includes:
- identity:
index,id,agent,agentSource,description, optionalassignment(internal payload names; the wire fields arename/agent/task) - status:
exitCode, optionalerror, optionalaborted, optionalabortReason, optionalretryFailure - output:
output,stderr,truncated,durationMs,tokens,requests, optionalcontextTokens/contextWindow - artifact metadata:
outputPath?,patchPath?,branchName?,nestedPatches?,outputMeta? - extracted tool data:
extractedToolData?from registered subprocess tool handlers such asyieldandreport_finding
Artifacts and side channels:
- Every subagent with an artifacts dir writes
<id>.md;agent://<id>resolves to that file. - If the output file is JSON,
agent://<id>/<path>andagent://<id>?q=<query>perform JSON extraction. - Each subagent gets
<id>.jsonlsession history when the parent persists artifacts;history://<id>renders it as a concise transcript (works for live and parked agents). - Isolated patch mode writes
<id>.patchbefore merge.
Flow
TaskTool.create(...)discovers agents once per cwd through a process-level memo (discoverAgentsForCreate) to render the dynamic prompt description.execute(...)repairs raw params (repairTaskParams), then validates:schemais always rejected;tasks/contextare rejected unlesstask.batchis on; batch calls need a non-emptytasks(ataskper item, unique provided names), a non-empty sharedcontext, and no top-leveltaskalongsidetasks; flat calls needtask. The call is then normalized into its spawn list (resolveSpawnItems).- Per-item execution split: items whose agent type declares
blocking: truerun inline; the rest become background jobs. The whole call runs sync whenasync.enabled=false, the session has noAsyncJobManager(orphaned host), or every item is blocking; inline spawns run through#executeSync(...)under the session-scoped semaphore. - Background execution (any non-blocking item with
async.enabled=trueand anAsyncJobManager):- agent ids are allocated up front via
AgentOutputManager.allocate(...)— each item'sname, or a generated AdjectiveNoun name — one per spawn; - one
type: "task"job per spawn is registered withsession.asyncJobManager(id= agent id,queued: true,ownerId= caller agent id) and the tool returns immediately; - each job body acquires the session-scoped
Semaphore(one perTaskToolinstance, sized fromtask.maxConcurrencyat first use), marks the job running, runs#executeSync(...)with that spawn's params, and reports progress through the sharedbuildAsyncDetails/onUpdate; - a failed or aborted run throws
TaskJobErrorso the job landsfailed, but the agent itself stays registered and interrogable. - a mixed call registers the async jobs first, then runs its blocking items inline and returns once they settle — the text combines the inline summaries with the spawned-job listing, and the block keeps rendering the still-running background rows beside the inline results.
- agent ids are allocated up front via
#executeSync(...)runs the spawn path (#runSpawn), which rediscovers agents from disk, so runtime resolution can differ from the create-time description.- It resolves each spawn's requested
agenttype, rejects unknown or settings-disabled agents, and enforces parent spawn policy plusPI_BLOCKED_AGENTself-recursion prevention. - Output schema priority: agent frontmatter
output→ inherited parent session schema (the call itself never carries one). - Plan mode swaps in an
effectiveAgentwith a read-only tool subset and plan-mode prompt;runSubprocess(...)receives the effective agent. - If
isolated, it requires a git repo (getRepoRoot(...)/captureBaseline(...)), mapstask.isolation.modeto a backend-kind hint (parseIsolationMode), and materializes the workspace via the natives PAL (ensureIsolation→isoResolve/isoStart), walking the candidate list when a backend is unavailable. - Artifacts dir comes from the parent session file when available, otherwise a temp dir. When the session is executing an approved plan, the plan reference is handed to the subagent.
- Non-isolated spawns call
runSubprocess(...)directly with parent cwd; isolated spawns run inside the isolation workspace, then commit to a branch (mergeMode === "branch") or capture a patch, and always clean up the workspace. runSubprocess(...)creates a child agent session with an isolated settings snapshot (forcingasync.enabled = falseandbash.autoBackground.enabled = false— subagents are internally synchronous), childagentIdequal to the allocated id, child internal URL router/AgentOutputManager, output schema, the sharedcontext(batch calls) in the system prompt'sCONTEXTsection, and the IRC peer roster in the system prompt.- Child tool availability: explicit
agent.toolsif provided; auto-addtaskwhen the agent hasspawnsand depth allows; striptaskattask.maxRecursionDepth; ensureircis present in explicit tool lists; expandexectoeval+bash; strip parent-ownedtodo. - The child must finish through the hidden
yieldtool; up to 3 reminder prompts, the last forcingtoolChoice = yieldwhen supported.finalizeSubprocessOutput(...)reconciles raw text,yieldpayloads, structured schemas,report_findingdata, and abort states. - End-of-run lifecycle (keep-alive, in
runSubprocess's finalizer):- hard abort (caller signal / wall-clock / budget) → registry status
aborted, session disposed — terminal; - isolated run → status
parkedwithout a reviver (workspace is merged + cleaned, so the session is not revivable; transcript stays readable viahistory://), then session disposed and detached; - everything else (success and failure alike) → status
idlewith the live session attached, andAgentLifecycleManager.global().adopt(id, { idleTtlMs, revive })arms the park timer. The reviver reopens the session JSONL (park closed the writer, so the single-writer lock is taken cleanly).
- hard abort (caller signal / wall-clock / budget) → registry status
- Lifecycle thereafter:
idleagents are parked aftertask.agentIdleTtlMs(session disposed;AgentRef+ session file retained); messaging (irc) or the Agent Hub revives them back toidle."Main"is never parked.
Modes / Variants
- Execution mode
- Background job —
async.enabled=true; non-blocking spawns go throughAsyncJobManager. - Sync inline —
async.enabled=false, no job manager, or the item's agent declaresblocking: true(per item: a mixed call runs both modes).
- Background job —
- Batch mode (
task.batch, default on)- on —
{ context, tasks[] }: one independent spawn per item, requiredcontextshared across the call's spawns,agent/isolatedper item. Lifecycle, revival, and concurrency semantics match N parallel single calls. - off — single spawn per call;
tasks/contextare rejected and removed from the schema.
- on —
- Isolation mode (
task.isolation.mode):none,auto,apfs,btrfs,zfs,reflink,overlayfs,projfs,block-clone,rcopy(legacyworktree,fuse-overlay,fuse-projfsaccepted for back-compat); the PAL resolves the actual backend with fallback. - Isolation merge strategy: patch mode (capture/apply root patches) or branch mode (commit to
omp/task/<id>, cherry-pick into parent). - Agent source precedence: project custom agents, then user custom agents, then bundled agents (
scout,designer,reviewer,task,sonic,librarian).
Side Effects
- Filesystem
- Writes
<id>.jsonland<id>.mdunder the session artifacts dir or a temp task dir; isolated patch mode writes<id>.patch. - Creates/removes worktrees or overlay mount directories; branch mode creates temporary worktrees and task branches.
- Writes
- Network
- Child sessions may use whichever networked tools/models their active tool set permits.
- MCP proxy tools can call existing parent MCP connections with a 60_000 ms timeout.
- Subprocesses / native bindings
- Isolation backends run through the
pi-nativesPAL (crates/pi-iso): kerneloverlaywithfuse-overlayfs/fusermount[3]fallback on Linux, APFS/Btrfs/ZFS/reflink clones, ProjFS on Windows, recursive copy as last resort. - Git operations for baseline capture, patch apply, worktrees, branches, stash, cherry-pick, commits.
- Isolation backends run through the
- Session state (transcript, memory, jobs, checkpoints, registries)
- Creates child
AgentSessioninstances with isolated settings snapshots; finished sessions stay registered in the process-globalAgentRegistryasidle/parkeduntil process teardown or explicit release. - With
async.enabled=true, registers one async job per spawn insession.asyncJobManager; completion is injected into the parent as an async-result message. - Arms idle-TTL timers in
AgentLifecycleManager(unref'd; they never hold the process open). - Emits
task:subagent:event,task:subagent:progress, andtask:subagent:lifecycleon the parent event bus. - Allocates session-scoped output ids through
AgentOutputManagersoagent://stays unique across invocations. - Shares the parent
local://root andArtifactManagerwith subagents.
- Creates child
- Background work / cancellation
job cancel(or parent tool-call abort) cancels background jobs; parent tool-call abort cancels sync runs through the call signal. A hard-aborted run landsabortedand is torn down.- Missing-
yieldrecovery sends up to three internal reminder prompts to the child session.
Limits & Caps
- Concurrency: one session-scoped
Semaphoresized fromtask.maxConcurrencyat first use (later setting changes do not resize it) bounds concurrent subagents across paralleltaskcalls — both async job bodies and the sync fallback acquire it. - Idle TTL:
task.agentIdleTtlMs, default420_000ms (7 min);<= 0disables parking and keeps idle sessions live until exit. - Per-subagent output truncation:
MAX_OUTPUT_BYTES = 500_000andMAX_OUTPUT_LINES = 5000inpackages/coding-agent/src/task/types.ts(overridable viaPI_TASK_MAX_OUTPUT_BYTES/PI_TASK_MAX_OUTPUT_LINES). Full raw output is still written to<id>.md. - Progress coalescing:
PROGRESS_COALESCE_MS = 150; recent-output tail:RECENT_OUTPUT_TAIL_BYTES = 8 * 1024(last 8 non-empty lines). - Missing-
yieldreminder retries:MAX_YIELD_RETRIES = 3; MCP proxy timeout:MCP_CALL_TIMEOUT_MS = 60_000— both inpackages/coding-agent/src/task/executor.ts. - Name/label caps: the wire
namehas no schema length cap (prompt text suggests≤32chars — guidance only); one-line display text (roster line, registrydisplayName) is normalized byoneLineLabel(...)and capped atLABEL_MAX = 80chars inpackages/coding-agent/src/task/types.ts. - Soft request budget (
task.softRequestBudget) and wall clock (task.maxRuntimeMs) apply to every spawn. - Recursion depth gate:
task.maxRecursionDepth;packages/coding-agent/src/tools/index.tshides thetasktool at or beyond the limit, andrunSubprocess(...)also strips childtaskaccess at max depth. - Final inline summary preview uses
fullOutputThreshold = 5000chars inpackages/coding-agent/src/task/index.ts;agent://<id>points to the full artifact.
Errors
- Parameter validation failures are returned as normal tool text with empty
results:schema(never accepted)tasks/contextwhiletask.batchis disabled- batch calls: missing/empty
tasks, an item withouttask, duplicate provided names, missing sharedcontext, top-leveltaskalongsidetasks - flat calls: missing/empty
task - unknown or settings-disabled agent type, spawn-policy denial, requesting
isolatedwhile isolation mode isnone
- Isolated execution without a git repo returns
Isolated task execution requires a git repository. ...; unavailable backends fall back through the PAL candidate list (reported viafellBack/fallbackReason), other backend errors rethrow, and exhausting every candidate errors with the fallback reason. - Job registration failure returns
Failed to start background task job(s): ...; a batch that schedules only some jobs reports the failed ids in the immediate text and keeps the started ones running. - Child failures surface as
SingleResult.exitCode = 1withstderr/errorpopulated; the async job is marked failed but the delivery text still carries the output plus a follow-up/transcript hint. - If the child omits
yield,finalizeSubprocessOutput(...)injects warnings such asSYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders. agent://<id>resolution errors are model-visible when another tool reads them: no session, no artifacts dir, missing id, conflicting extraction syntax, or invalid JSON for extraction.
Notes
- Parallelism is parallel
taskcalls in one assistant message — or, withtask.batch, atasks[]batch in one call; either way the session-scoped semaphore bounds the fan-out. Withasync.enabled=true, each spawn is an independent background job. - Shared background convention without batch mode: write it once to a
local://file and reference that path in each spawn'stask— subagents share the parent'slocal://root. Withtask.batch, the requiredcontextparameter carries the shared background directly into each spawn's system prompt. - Prefer messaging an existing agent (
irc) over a fresh spawn for follow-up work: it already holds the relevant context.ircop:"list" shows idle/parked candidates; messaging a parked agent revives it.history://<id>shows what an agent has done. ircavailability is derived, not configured (isIrcEnabledinpackages/coding-agent/src/tools/irc.ts): it exists exactly when there is someone to message — the session can spawn subagents, or it is a subagent itself. Messaging is the only follow-up path to a finished subagent, so task without irc would strand idle agents.- Subagents are internally synchronous: the executor forces
async.enabled = falseandbash.autoBackground.enabled = falsein the child settings snapshot, so there are no fire-and-forget grandchildren. - Agent discovery precedence is first-wins by exact name: project
.ompagents dir before the user.ompdir (task agents only load from.omproots;.claude/.codex/.geminiagent dirs are skipped), Claude plugin agent dirs after config dirs, bundled agents last. Create-time discovery is memoized per cwd for the prompt description; execution-time discovery stays fresh. - Child sessions do not inherit conversation history. Built-in carry-over is the workspace tree/skills/context files, the shared
local://root, and the approved-plan reference when one exists. - When the parent passes
mcpManager, child sessions disable standalone MCP discovery and get proxy tools that reuse parent connections. - Branch-mode merge temporarily stashes the parent repo before cherry-picking; a stash-pop conflict does not unmerge the cherry-picked commits — they stay on HEAD, the stash entry is preserved, and the conflict is surfaced separately as
stashConflict. Patch mode only applies the combined root patch whengit.patch.canApplyText(...)succeeds; failures leave the.patchartifact for manual handling. - Nested git repos are diffed independently inside isolated workspaces and merged separately with
applyNestedPatches(...). agent://ids are name-based (Taskfirst,Task-2/Task-3only when the name repeats, nested likeParent.Child) byAgentOutputManager; this is what prevents artifact collisions across repeated or nested invocations.