@kolisachint/hoocode-agent 0.4.111 → 0.4.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +10 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +17 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +36 -5
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +63 -17
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/scheduler.d.ts +8 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +9 -3
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -2
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-events.d.ts +21 -0
- package/dist/core/subagent-events.d.ts.map +1 -0
- package/dist/core/subagent-events.js +25 -0
- package/dist/core/subagent-events.js.map +1 -0
- package/dist/core/subagent-pool.d.ts +5 -4
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +7 -9
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +15 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +27 -14
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/extensions/core/loop.d.ts.map +1 -1
- package/dist/extensions/core/loop.js +4 -1
- package/dist/extensions/core/loop.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +5 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +37 -15
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +27 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +150 -7
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +18 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +5 -2
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +17 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +78 -5
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +4 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +12 -3
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +12 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +75 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-controller.js +9 -5
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +10 -3
- package/dist/modes/print-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,yBAAyB,CAAC;AAE/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAiB7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEjE;;;;;6EAK6E;AAC7E,wBAAgB,mBAAmB,IAAI,MAAM,CAsB5C;AAmCD,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IACjC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,OAAO,CAAC;IACZ,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAeD,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAsB,GAAG,cAAc,CAiUpF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,GAAG,EAAE,MAAM,GACT,MAAM,GAAG,SAAS,CAQpB;AAwRD;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,IAAI,cAAc,CA2I/D","sourcesContent":["/**\n * Task tool: delegate a focused task to a specialized subagent.\n *\n * Mirrors the Claude Code `Task` tool. The parent agent decides *when* to\n * delegate based on each agent's `description` (there is no deterministic gate)\n * and selects *which* agent via `subagent_type`. The chosen agent runs in a\n * fresh, isolated child process (SubagentPool) and only its final answer is\n * returned to the parent.\n *\n * It is an optional, opt-in tool (enabled via --enable-subagents or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { agentColorFor } from \"../../modes/interactive/theme/theme.js\";\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { loadAgentRegistry } from \"../agent-registry.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { formatDurationSecs } from \"../format-duration.js\";\nimport { getProviderExhaustion } from \"../provider-health.js\";\nimport { SessionManager } from \"../session-manager.js\";\nimport { delegateAllowList, isDelegateAllowed } from \"../subagent-depth.js\";\nimport { type InboxRecord, subagentInbox } from \"../subagent-inbox.js\";\nimport type { SubagentResult, TaskResult } from \"../subagent-pool.js\";\nimport { getSubagentPool } from \"../subagent-pool-instance.js\";\nimport type { SubagentResultFile, SubagentTaskNode } from \"../subagent-result.js\";\nimport { taskStore } from \"../task-store.js\";\nimport { type WarmRunResult, WarmWorkerError } from \"../warm-subagent-pool.js\";\nimport { getWarmSubagentPool, warmSubagentsEnabled } from \"../warm-subagent-pool-instance.js\";\n\n// Re-exported from its home in agent-registry (where formatAgentsForPrompt uses\n// it to render the roster) so existing importers keep working without creating a\n// tools -> registry -> tools cycle.\nexport { summarizeAgentDescription } from \"../agent-registry.js\";\n\n/** System prompt appendix for the main session when the Task tool is enabled.\n * Instructs the parent agent on when and how to delegate effectively. The\n * available agents themselves are listed once, authoritatively, in the\n * `<available_agents>` block the system prompt emits whenever the Task tool is\n * active (see agent-session `_rebuildSystemPrompt`); this appendix references\n * that list rather than re-rendering the roster and paying for it twice. */\nexport function buildTaskMainPrompt(): string {\n\treturn `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer. Pick an agent by name from the <available_agents> list in this prompt and pass it as \\`subagent_type\\`.\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nModel tier (optional \\`complexity\\`): set \\`fast\\` for quick reads/lookups, \\`standard\\` for multi-file edits, \\`capable\\` for deep architecture work. It maps to a model from \\`settings.modelCategories\\`. Omit it to use the agent's default; an agent that pins its own model ignores \\`complexity\\`.\n\nGuidelines:\n- Choose the agent whose description best matches the task.\n- Make every task specific and self-contained. The subagent cannot see this conversation; pass all necessary context (files, constraints, prior findings) in \\`prompt\\`.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.\n- Delegate proactively when work is self-contained or parallelizable: multi-step investigation, read-only exploration (use \\`explore\\`), research before changes (use \\`plan\\`), drafting a standalone file/section, or running a long command/test suite. Dispatch independent subtasks in the same turn. Handle only trivial single-step edits or tightly interactive back-and-forth inline.\n- Some agents run in the background (non-blocking); force it per call with \\`background: true\\` (or \\`background: false\\` to wait inline). A background Task does not block your turn and does not return its answer inline: you get a short notification (\"explore#1 finished\") and the full result is held for you to pull with \\`TaskOutput\\`. Keep working in the meantime.\n- After dispatching background work, DO NOT stop and wait — that wastes the parallelism and looks stuck. Immediately continue with the next useful thing: read or edit an independent file, draft the parts of your answer that don't depend on the pending result, or dispatch more independent subtasks. Only barrier (with \\`TaskOutput(wait: true)\\`) when you genuinely cannot proceed without the result. Prefer background dispatch for any self-contained or parallelizable work so your turn never blocks on a subagent.\n- Use **TaskOutput** to manage background subagents: \\`TaskOutput(list: true)\\` shows every running/finished subagent and what each is doing; \\`TaskOutput(\"explore#1\")\\` reads a finished subagent's full result (or reports its status if still running); \\`TaskOutput(wait: true)\\` blocks until a named task — or, with no task_id, ALL outstanding subagents — finish. Dispatch a batch in one turn, then barrier on them with \\`TaskOutput(wait: true)\\` only once you've exhausted the work you can do without them.\n- When working through a TodoWrite plan, mark the plan item in_progress BEFORE dispatching subagents for it: each dispatch is attributed to the current in_progress item in the user's task panel, so dispatching first (or with several items in_progress) leaves the run unattributed.\n- To continue a previous subagent (for example one that returned partial results), call Task again with \\`resume_task_id\\` set to its task_id; it resumes with its full prior transcript and \\`prompt\\` is your follow-up.`;\n}\n\nconst taskParams = Type.Object({\n\tdescription: Type.String({\n\t\tdescription: \"A short (3-5 word) description of the task, shown in the task panel.\",\n\t}),\n\tprompt: Type.String({\n\t\tdescription:\n\t\t\t\"The full, self-contained task for the subagent. It cannot see this conversation, so include all needed context, files, and constraints.\",\n\t}),\n\tsubagent_type: Type.String({\n\t\tdescription: \"The name of the specialized agent to delegate to. Must be one of the available agents.\",\n\t}),\n\tcomplexity: Type.Optional(\n\t\tType.Union([Type.Literal(\"fast\"), Type.Literal(\"standard\"), Type.Literal(\"capable\")], {\n\t\t\tdescription:\n\t\t\t\t\"Model tier for this dispatch: fast (quick reads/lookups), standard (multi-file edits), capable (deep architecture). Maps to settings.modelCategories. Ignored if the chosen agent pins its own model; omit to use the agent's default.\",\n\t\t}),\n\t),\n\tbackground: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Set true to run non-blocking: you get a short notification when it finishes and pull the full result with TaskOutput; set false to wait and get the answer inline. Defaults to the agent's own background setting.\",\n\t\t}),\n\t),\n\tresume_task_id: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional. To continue a previous subagent run, pass its task_id (returned by an earlier Task or TaskOutput call). The subagent resumes with its full prior transcript and `prompt` is your follow-up instruction.\",\n\t\t}),\n\t),\n});\n\ntype TaskParams = Static<typeof taskParams>;\n\nexport interface TaskToolDetails {\n\tsubagent_type: string;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** Pool-level task id usable for resume/polling. */\n\tpoolTaskId?: string;\n\t/** True when dispatched as a non-blocking background task. */\n\tbackground?: boolean;\n}\n\nexport interface TaskOutputDetails {\n\t/** The handle queried, when a specific task was named. */\n\ttask_id?: string;\n\t/** running | done | collected | failed | stalled | timeout | cancelled | list | empty | unknown */\n\tstatus: string;\n\tok: boolean;\n\t/** Number of subagents still running, included on roster/list responses. */\n\toutstanding?: number;\n}\n\n/**\n * A short, human-readable task name for the task panel: the first line limited\n * to ~8 words so it stays glanceable. A character cap guards a single long word.\n */\nfunction summarize(task: string): string {\n\tconst firstLine = (task.trim().split(\"\\n\")[0] ?? \"\").trim();\n\tif (!firstLine) return \"(task)\";\n\tconst words = firstLine.split(/\\s+/);\n\tlet name = words.length > 8 ? `${words.slice(0, 8).join(\" \")}…` : firstLine;\n\tif (name.length > 60) name = `${name.slice(0, 59)}…`;\n\treturn name;\n}\n\n/** Create the Task tool definition. Registered as a customTool when enabled. */\nexport function createTaskToolDefinition(cwd: string = process.cwd()): ToolDefinition {\n\t// Agents whose definitions opt into background execution. The agent loop reads\n\t// the tool's `background` flag per call and, for these, runs the dispatch\n\t// detached: the parent keeps reasoning and the subagent's answer is injected as\n\t// a follow-up message when it finishes (no polling needed). A per-call\n\t// `background` argument overrides the agent's default in either direction.\n\tconst backgroundAgents = collectBackgroundAgentNames(cwd);\n\treturn defineTool<typeof taskParams, TaskToolDetails>({\n\t\tname: TASK_TOOL_NAME,\n\t\tlabel: TASK_TOOL_NAME,\n\t\tbackground: (toolCall) => {\n\t\t\tconst override = toolCall.arguments?.background;\n\t\t\tif (typeof override === \"boolean\") return override;\n\t\t\treturn backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? \"\"));\n\t\t},\n\t\t// Kept lean: the available agents are listed in the system prompt, and the\n\t\t// `complexity`/`background` semantics live in their parameter descriptions —\n\t\t// repeating them here would re-spend those tokens on every turn.\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation). Choose one of the available agents (listed in the system prompt) via `subagent_type` and pass everything it needs via `prompt`; the subagent returns only its final answer.\",\n\t\t\t\"WHEN TO USE: (1) self-contained work where you only need the final result; (2) parallel investigation/edits without losing your reasoning chain; (3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs); (4) a long command or test suite you want to run without blocking your reasoning.\",\n\t\t\t\"Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about. Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"delegate a self-contained task to a specialized subagent (choose via subagent_type)\",\n\t\tparameters: taskParams,\n\n\t\tasync execute(_toolCallId, params: TaskParams, signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\n\t\t\t// User-initiated cancel (Esc/abort): kill the dispatched run's whole\n\t\t\t// process tree and let the dispatch settle with status \"cancelled\" —\n\t\t\t// distinct from \"failed\" everywhere it surfaces (panel, inbox, result).\n\t\t\t// Cold dispatches only: a warm worker has no cancel surface (it is\n\t\t\t// reused), so warm runs still complete server-side and are discarded.\n\t\t\tconst cancelOnAbort = async (poolRunId: string, run: () => Promise<TaskResult>): Promise<TaskResult> => {\n\t\t\t\tif (!signal) return run();\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tpool.cancel?.(poolRunId);\n\t\t\t\t};\n\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\ttry {\n\t\t\t\t\treturn await run();\n\t\t\t\t} finally {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Pre-flight: if the inherited provider recently exhausted its quota (the\n\t\t\t// parent's own turn failed with a usage/rate-limit error that did not\n\t\t\t// recover), skip the spawn. Subagents run on the same provider, so this\n\t\t\t// would only burn another failed attempt. The signal self-expires and is\n\t\t\t// cleared on the next successful response.\n\t\t\tconst provider = ctx.model?.provider;\n\t\t\tconst exhaustion = provider ? getProviderExhaustion(provider) : undefined;\n\t\t\tif (exhaustion) {\n\t\t\t\tconst skippedRunId = newDispatchTaskId();\n\t\t\t\tconst skipped = taskStore.create(params.description?.trim() || summarize(params.prompt), {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: skippedRunId,\n\t\t\t\t\tlinkedTaskId: linkedTodoId(),\n\t\t\t\t});\n\t\t\t\tregisterSubagentDispatch(skippedRunId, subagentInbox.nextLabel(params.subagent_type));\n\t\t\t\ttaskStore.update(skipped.id, { status: \"failed\", note: `${provider} exhausted` });\n\t\t\t\ttaskStore.patchAgent(skippedRunId, { state: \"failed\" });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t`Did not dispatch subagent \"${params.subagent_type}\": the \"${provider}\" provider appears ` +\n\t\t\t\t\t\t\t\t`exhausted or rate-limited (this session just failed with: ${exhaustion.message}). ` +\n\t\t\t\t\t\t\t\t`Subagents run on the same provider, so dispatching would fail too. Wait for the quota to ` +\n\t\t\t\t\t\t\t\t`reset or switch model/provider, then retry — or complete the work directly in this session.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { subagent_type: params.subagent_type, ok: false, taskId: skipped.id },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Scoped delegation: a delegating agent may be restricted to certain subagent\n\t\t\t// types (its `delegate: <types>` frontmatter). The root is unrestricted.\n\t\t\tif (!isDelegateAllowed(params.subagent_type)) {\n\t\t\t\tconst allowed = delegateAllowList()?.join(\", \") ?? \"\";\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`This agent may not delegate to \"${params.subagent_type}\". Allowed subagent types: ${allowed || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Resume path: continue a previously dispatched subagent with a follow-up\n\t\t\t// prompt, reusing its persisted session (full prior transcript).\n\t\t\tconst resumeId = params.resume_task_id?.trim();\n\t\t\tif (resumeId) {\n\t\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\t\tconst runId = newDispatchTaskId();\n\t\t\t\tconst label = subagentInbox.nextLabel(params.subagent_type);\n\t\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: runId,\n\t\t\t\t\tlinkedTaskId: linkedTodoId(),\n\t\t\t\t});\n\t\t\t\tregisterSubagentDispatch(runId, label);\n\t\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await cancelOnAbort(runId, () =>\n\t\t\t\t\t\tpool.resume(resumeId, params.prompt, {\n\t\t\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\ttaskId: runId,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\t// The session lives under the original task id; keep it as the resume handle.\n\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, runId, task.id, resumeId);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tmarkDispatchFailed(task.id, runId);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// The model has already decided to delegate and which agent to use; honor\n\t\t\t// it. Validate the requested agent against the registry (no routing gate).\n\t\t\tconst registry = loadAgentRegistry({ cwd: ctx.cwd });\n\t\t\tconst def = registry.get(params.subagent_type);\n\t\t\tif (!def) {\n\t\t\t\tconst available = registry\n\t\t\t\t\t.list()\n\t\t\t\t\t.map((a) => a.name)\n\t\t\t\t\t.join(\", \");\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Unknown subagent_type: \"${params.subagent_type}\". Available agents: ${available || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\t// One roster row per dispatch, keyed by the (pre-allocated) pool task id.\n\t\t\t// Keying by agent TYPE made concurrent same-type runs share a single row:\n\t\t\t// their activity, state, and stats collided last-writer-wins. The inbox\n\t\t\t// already keys by task id; this aligns the panel to the same model. The\n\t\t\t// pool's task_progress events carry the task id, so patching by this id\n\t\t\t// routes live activity to the right row.\n\t\t\tconst poolTaskId = newDispatchTaskId();\n\t\t\tconst label = subagentInbox.nextLabel(params.subagent_type);\n\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\tsource: \"subagent\",\n\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\tagent: poolTaskId,\n\t\t\t\t// Tie this run to the plan item it executes (the single in_progress\n\t\t\t\t// TodoWrite task, when unambiguous) so the panel can nest it there.\n\t\t\t\tlinkedTaskId: linkedTodoId(),\n\t\t\t});\n\t\t\tregisterSubagentDispatch(poolTaskId, label);\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t// Fork agents inherit the parent's conversation via a forked session.\n\t\t\tconst forkSessionFile = def.fork\n\t\t\t\t? resolveForkSessionFile(def, ctx.sessionManager?.getSessionFile(), ctx.cwd)\n\t\t\t\t: undefined;\n\n\t\t\t// `complexity` is passed as the model: the pool's spawn() already lets a\n\t\t\t// non-`inherit` agent model win, then resolves a category string (fast/\n\t\t\t// standard/capable) via settings.modelCategories. So a pinned-model agent\n\t\t\t// ignores complexity, and an `inherit` agent picks up the requested tier —\n\t\t\t// no settings lookup needed here.\n\t\t\tconst dispatchModel = params.complexity ?? ctx.model?.id;\n\n\t\t\t// Whether this call runs detached. The agent loop reads the tool's\n\t\t\t// `background` flag (the same predicate) to run execute() detached; we\n\t\t\t// recompute it here to choose the notify-and-pull return shape.\n\t\t\tconst isBackground = params.background ?? backgroundAgents.has(params.subagent_type);\n\n\t\t\tif (isBackground) {\n\t\t\t\t// Notify-and-pull: register the dispatch in the inbox under the\n\t\t\t\t// pre-allocated id, await it, retain the body in the inbox, and return a\n\t\t\t\t// compact notification (not the body). The model pulls it with TaskOutput.\n\t\t\t\tsubagentInbox.observe(pool);\n\t\t\t\tsubagentInbox.start(poolTaskId, label, params.subagent_type);\n\n\t\t\t\t// Warm path (opt-in): run on a reused RPC worker, retain the body in the\n\t\t\t\t// inbox, and return the same notify-and-pull shape as the cold path. An\n\t\t\t\t// infra failure falls through to the cold dispatch below.\n\t\t\t\tif (warmSubagentsEnabled() && !forkSessionFile) {\n\t\t\t\t\tconst warm = getWarmSubagentPool(ctx.cwd);\n\t\t\t\t\tif (warm.isPoolable(params.subagent_type)) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst warmResult = await warm.dispatch(\n\t\t\t\t\t\t\t\tparams.prompt,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tagentType: params.subagent_type,\n\t\t\t\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t(activity) => taskStore.patchAgent(poolTaskId, { activity }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);\n\t\t\t\t\t\t\tsubagentInbox.finish(poolTaskId, dispatchResult);\n\t\t\t\t\t\t\treturn finalizeDispatchResult(\n\t\t\t\t\t\t\t\tdispatchResult,\n\t\t\t\t\t\t\t\tparams.subagent_type,\n\t\t\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\t\t\ttask.id,\n\t\t\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tif (!(error instanceof WarmWorkerError)) throw error;\n\t\t\t\t\t\t\tconsole.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await cancelOnAbort(poolTaskId, () =>\n\t\t\t\t\t\tpool.dispatch(params.prompt, {\n\t\t\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\t\t\tcontext: \"\",\n\t\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\tsessionFile: forkSessionFile,\n\t\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tsubagentInbox.finish(poolTaskId, dispatchResult);\n\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, poolTaskId, {\n\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tmarkDispatchFailed(task.id, poolTaskId);\n\t\t\t\t\tsubagentInbox.fail(poolTaskId, reason);\n\t\t\t\t\t// A background dispatch reports failure as a compact notification, not a\n\t\t\t\t\t// thrown tool error — the call was already answered by a placeholder.\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `${label} failed ✗ — ${reason}` }],\n\t\t\t\t\t\tdetails: {\n\t\t\t\t\t\t\tsubagent_type: params.subagent_type,\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\terror: reason,\n\t\t\t\t\t\t\ttaskId: task.id,\n\t\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\t\tbackground: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Warm path (opt-in): for an eligible foreground dispatch, run on a reused\n\t\t\t// RPC worker to skip the cold-boot. Fork agents (forkSessionFile set) and\n\t\t\t// non-poolable types are excluded. Any infra failure falls through to the\n\t\t\t// cold pool below, so enabling this can only change latency, never whether\n\t\t\t// the task can run.\n\t\t\tif (warmSubagentsEnabled() && !forkSessionFile) {\n\t\t\t\tconst warm = getWarmSubagentPool(ctx.cwd);\n\t\t\t\tif (warm.isPoolable(params.subagent_type)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst warmResult = await warm.dispatch(\n\t\t\t\t\t\t\tparams.prompt,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tagentType: params.subagent_type,\n\t\t\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// Mirror the cold pool's live progress on the task panel roster so a\n\t\t\t\t\t\t\t// warm dispatch reads as busy (⋯ grep), not stuck.\n\t\t\t\t\t\t\t(activity) => taskStore.patchAgent(poolTaskId, { activity }),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);\n\t\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, undefined);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// A genuine infra failure (worker crash/timeout) retries cold; any other\n\t\t\t\t\t\t// error is a real dispatch failure and propagates.\n\t\t\t\t\t\tif (!(error instanceof WarmWorkerError)) {\n\t\t\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\t\t\ttaskStore.patchAgent(poolTaskId, { state: \"failed\", activity: \"\" });\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconsole.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Foreground: block the turn and return the subagent's full answer inline.\n\t\t\ttry {\n\t\t\t\tconst dispatchResult = await cancelOnAbort(poolTaskId, () =>\n\t\t\t\t\tpool.dispatch(params.prompt, {\n\t\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\t\tcontext: \"\",\n\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\tsessionFile: forkSessionFile,\n\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn finalizeDispatchResult(\n\t\t\t\t\tdispatchResult,\n\t\t\t\t\tparams.subagent_type,\n\t\t\t\t\tpoolTaskId,\n\t\t\t\t\ttask.id,\n\t\t\t\t\tdispatchResult.task_id,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tmarkDispatchFailed(task.id, poolTaskId);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst type = args.subagent_type ?? \"agent\";\n\t\t\t// The [type] tag carries the agent's identity color — the same hue this\n\t\t\t// agent has in the task panel and TaskOutput — so a transcript full of\n\t\t\t// concurrent dispatches is scannable by color.\n\t\t\tconst text = theme.fg(\"toolTitle\", theme.bold(\"Agent \")) + theme.fg(agentColorFor(type), `[${type}]`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n\n/**\n * For a `fork: true` agent, fork the parent's session so the subagent inherits the\n * full parent conversation (and its prompt cache) instead of starting fresh. Returns\n * the forked session file to dispatch the child with, or undefined to fall back to a\n * fresh session (non-fork agent, no parent session, or an empty/invalid source).\n */\nexport function resolveForkSessionFile(\n\tdef: Pick<AgentDefinition, \"fork\">,\n\tparentSessionPath: string | undefined,\n\tcwd: string,\n): string | undefined {\n\tif (!def.fork || !parentSessionPath) return undefined;\n\ttry {\n\t\treturn SessionManager.forkFrom(parentSessionPath, cwd).getSessionFile();\n\t} catch {\n\t\t// Empty/invalid parent session: fall back to a fresh subagent session.\n\t\treturn undefined;\n\t}\n}\n\n/** Pre-allocated pool task id for a dispatch (matches the pool's own format). */\nfunction newDispatchTaskId(): string {\n\treturn `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\n/**\n * The TodoWrite plan item this dispatch is (most plausibly) working on: the\n * single in_progress main-agent task. TodoWrite discipline keeps exactly one\n * item in_progress, so when that holds the link is unambiguous; with zero or\n * several in_progress items no link is recorded rather than guessing. Used to\n * nest the run under its plan item in the task panel's flat lens.\n */\nfunction linkedTodoId(): number | undefined {\n\tconst inProgress = taskStore\n\t\t.list()\n\t\t.filter(\n\t\t\t(t) =>\n\t\t\t\tt.source === undefined &&\n\t\t\t\tt.agent === undefined &&\n\t\t\t\tt.parentTaskId === undefined &&\n\t\t\t\tt.status === \"in_progress\",\n\t\t);\n\treturn inProgress.length === 1 ? inProgress[0]?.id : undefined;\n}\n\n/**\n * Register this dispatch in the task store's roster, keyed by its run id (the\n * pool task id) so concurrent runs of the same agent type each get their own\n * row, state, activity, and stats. The friendly label (\"explore#1\") names the\n * row the same way the inbox names background tasks.\n */\nfunction registerSubagentDispatch(runId: string, label: string): void {\n\ttaskStore.upsertAgent({ id: runId, name: label, role: \"subagent\", kind: \"subagent\", state: \"running\" });\n}\n\n/**\n * Catch-path bookkeeping: mark the dispatch failed unless it already settled.\n * finalizeDispatchResult records a terminal status (failed/cancelled) *before*\n * throwing, so a blanket \"failed\" here would clobber a cancellation with a\n * failure the moment the thrown error passed through.\n */\nfunction markDispatchFailed(taskStoreId: number, runId: string): void {\n\tconst current = taskStore.list().find((t) => t.id === taskStoreId);\n\tif (current && current.status !== \"pending\" && current.status !== \"in_progress\") return;\n\ttaskStore.update(taskStoreId, { status: \"failed\" });\n\ttaskStore.patchAgent(runId, { state: \"failed\", activity: \"\" });\n}\n\n/** Names of agents configured to run in the background (non-blocking). */\nfunction collectBackgroundAgentNames(cwd: string): Set<string> {\n\tconst names = new Set<string>();\n\tfor (const agent of loadAgentRegistry({ cwd }).list()) {\n\t\tif (agent.background) names.add(agent.name);\n\t}\n\treturn names;\n}\n\n/**\n * Merge a child subagent's task subtree into the parent's task store, rooting\n * each top-level node under the dispatching task (`parentTaskId`). Recurses so a\n * subagent that itself delegated shows its nested work — the subtree the child\n * could not surface across the process boundary on its own. Each node is its own\n * task (no key-by-type collapse), preserving the order the child created them.\n */\nfunction mergeChildTaskTree(nodes: readonly SubagentTaskNode[] | undefined, parentTaskId: number): void {\n\tif (!nodes) return;\n\ttaskStore.batch(() => {\n\t\tfor (const node of nodes) {\n\t\t\tconst created = taskStore.create(node.title, {\n\t\t\t\tsource: node.source,\n\t\t\t\tsubagentMode: node.subagentMode,\n\t\t\t\tparentTaskId,\n\t\t\t});\n\t\t\ttaskStore.update(created.id, { status: node.status, usage: node.usage });\n\t\t\tmergeChildTaskTree(node.children, created.id);\n\t\t}\n\t});\n}\n\n/**\n * Adapt a warm-worker run into the TaskResult shape finalizeDispatchResult\n * consumes, so the warm and cold paths share one finish/render path. A warm run\n * returns its answer inline (no result.json), so we synthesize an equivalent\n * SubagentResult with the answer as the summary and the pulled usage. The warm\n * path is clean (no persisted session), so there is no task_tree to merge and no\n * resume handle.\n */\nfunction warmResultToTaskResult(warm: WarmRunResult, agentType: string, taskStoreId: number): TaskResult {\n\tconst resultData: SubagentResultFile = {\n\t\tsummary: warm.summary || \"(subagent returned no output)\",\n\t\tfiles_changed: [],\n\t\tconfidence: warm.ok ? 1 : 0,\n\t\tstatus: warm.status,\n\t\tusage: warm.usage,\n\t};\n\tconst result: SubagentResult = {\n\t\ttask_id: String(taskStoreId),\n\t\tok: warm.ok,\n\t\tstdout: \"\",\n\t\tstderr: \"\",\n\t\texit_code: warm.ok ? 0 : 1,\n\t\tstatus: warm.status,\n\t\terror: warm.error,\n\t\tresult_data: resultData as unknown as Record<string, unknown>,\n\t};\n\treturn { handled_inline: false, agent_type: agentType, result };\n}\n\n/**\n * Update the task panel from a finished dispatch and shape the tool result.\n *\n * Foreground calls return the subagent's full answer inline and signal a hard\n * failure by throwing (the agent loop derives a tool's error state from a thrown\n * error). A background call passes `background`: the body already lives in the\n * inbox, so it returns a compact, self-contained notification (success or\n * failure) and never throws — the call was already answered by a placeholder.\n */\nfunction finalizeDispatchResult(\n\tdispatchResult: TaskResult,\n\tsubagentType: string,\n\trunAgentId: string,\n\ttaskStoreId: number,\n\tresumeHandle: string | undefined,\n\tbackground?: { taskId: string; label: string },\n): { content: Array<{ type: \"text\"; text: string }>; details: TaskToolDetails } {\n\tconst result = dispatchResult.result;\n\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\tconst usage = resultData?.usage;\n\n\t// Merge the child's own task subtree under the dispatching task so nested\n\t// delegation (depth >= 2) is visible in the subagents lens's task tree.\n\tmergeChildTaskTree(resultData?.task_tree, taskStoreId);\n\n\t// Roll this run's usage into its own roster row (rows are per-dispatch).\n\tif (usage) {\n\t\ttaskStore.addAgentStats(runAgentId, { input: usage.input, output: usage.output, cost: usage.cost });\n\t}\n\n\tif (!result || !result.ok) {\n\t\t// A user-initiated cancel is not a failure: it gets its own terminal\n\t\t// status so the panel shows ⊘ cancelled (dim) instead of ✗ failed (red).\n\t\tconst cancelled = result?.status === \"cancelled\";\n\t\tconst failNote = result?.usedInheritedModelFallback ? \"inherited-model retry failed\" : undefined;\n\t\ttaskStore.update(taskStoreId, { status: cancelled ? \"cancelled\" : \"failed\", usage, note: failNote });\n\t\ttaskStore.patchAgent(runAgentId, { state: cancelled ? \"cancelled\" : \"failed\", activity: \"\" });\n\t\tconst reason = result?.error ?? (result?.status ? `subagent ${result.status}` : \"unknown error\");\n\t\tif (background) {\n\t\t\tconst verdict = cancelled ? \"cancelled ⊘\" : \"failed ✗\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: `${background.label} ${verdict} — ${reason}` }],\n\t\t\t\tdetails: {\n\t\t\t\t\tsubagent_type: subagentType,\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: reason,\n\t\t\t\t\ttaskId: taskStoreId,\n\t\t\t\t\tpoolTaskId: background.taskId,\n\t\t\t\t\tbackground: true,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (cancelled) {\n\t\t\tthrow new Error(`Subagent (${subagentType}) cancelled by user.`);\n\t\t}\n\t\tconst stderr = result?.stderr?.trim();\n\t\tthrow new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\\nstderr: ${stderr.slice(-500)}` : \"\"}`);\n\t}\n\n\t// Leave the task in the store with its final status; it stays visible in the\n\t// task panel until the next user message arrives. Surface a ⚠ cue when the run\n\t// fell back to the inherited model rather than emitting a chat message — and\n\t// clear any stale note otherwise (note is clearable via an explicit undefined).\n\tconst fallbackNote = dispatchResult.result?.usedInheritedModelFallback ? \"ran on inherited model\" : undefined;\n\ttaskStore.update(taskStoreId, { status: \"done\", usage, note: fallbackNote });\n\t// Rows are per-run, so this run settles to done regardless of siblings.\n\ttaskStore.patchAgent(runAgentId, { state: \"done\", activity: \"\" });\n\tlet answer = resultData?.summary || \"(subagent returned no output)\";\n\t// Partial results are resumable; surface the handle so the parent can continue.\n\tif (result.status === \"partial\" && resumeHandle) {\n\t\tanswer += `\\n\\n[Partial result. To continue this subagent, call Task again with resume_task_id=\"${resumeHandle}\".]`;\n\t}\n\n\tif (background) {\n\t\t// Compact notification: the body is retained in the inbox; the model pulls it\n\t\t// with TaskOutput. Keeps a wide swarm from flooding the parent's context.\n\t\tconst partial = result.status === \"partial\" ? \" (partial — resume to continue)\" : \"\";\n\t\tconst outstanding = subagentInbox.outstanding().length;\n\t\tconst tail = outstanding > 0 ? ` ${outstanding} still running.` : \"\";\n\t\tconst text =\n\t\t\t`${background.label} finished ✓${partial} — ${summarize(answer)}.${tail}\\n` +\n\t\t\t`Read the full result with TaskOutput(\"${background.label}\").`;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\tdetails: {\n\t\t\t\tsubagent_type: subagentType,\n\t\t\t\tok: true,\n\t\t\t\ttaskId: taskStoreId,\n\t\t\t\tpoolTaskId: background.taskId,\n\t\t\t\tbackground: true,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn {\n\t\tcontent: [{ type: \"text\", text: answer }],\n\t\tdetails: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },\n\t};\n}\n\nconst taskOutputParams = Type.Object({\n\ttask_id: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t'Handle of a background subagent — its task_id or friendly label (e.g. \"explore#1\") from a Task notification. Omit (or set list:true) to see every background task.',\n\t\t}),\n\t),\n\tlist: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"List all background subagents with their status (running/done/failed/cancelled) and current activity. No result bodies are returned.\",\n\t\t}),\n\t),\n\twait: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Block until the named task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier) — before returning. Bounded by timeout_ms.\",\n\t\t}),\n\t),\n\ttimeout_ms: Type.Optional(\n\t\tType.Number({ description: \"Maximum time to block in wait mode, in milliseconds (default 120000).\" }),\n\t),\n});\n\ntype TaskOutputParams = Static<typeof taskOutputParams>;\n\nconst TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 120_000;\n\n/**\n * Elapsed time a record has run (so far, or until it settled), in the same\n * format the task panel uses so the two surfaces always agree.\n */\nfunction recordElapsed(rec: InboxRecord): string {\n\tconst end = rec.endedAt ?? Date.now();\n\treturn formatDurationSecs((end - rec.startedAt) / 1000);\n}\n\n/** A compact roster of every known background subagent — status + activity, no bodies. */\nfunction formatTaskRoster(): { content: Array<{ type: \"text\"; text: string }>; details: TaskOutputDetails } {\n\tconst all = subagentInbox.list();\n\tconst outstanding = subagentInbox.outstanding().length;\n\tif (all.length === 0) {\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: \"No background subagents have been dispatched.\" }],\n\t\t\tdetails: { status: \"empty\", ok: true, outstanding: 0 },\n\t\t};\n\t}\n\tconst lines = all.map((r) => {\n\t\tconst when = recordElapsed(r);\n\t\tswitch (r.lifecycle) {\n\t\t\tcase \"running\":\n\t\t\t\treturn `- ${r.label} running ${when}${r.lastActivity ? ` · ${r.lastActivity}` : \"\"}`;\n\t\t\tcase \"done\":\n\t\t\t\treturn `- ${r.label} done (uncollected) ${when} — ${r.summaryLine ?? \"\"}`;\n\t\t\tcase \"collected\":\n\t\t\t\treturn `- ${r.label} collected ${when} — ${r.summaryLine ?? \"\"}`;\n\t\t\tcase \"cancelled\":\n\t\t\t\treturn `- ${r.label} cancelled ⊘ — ${r.error ?? \"cancelled by user\"}`;\n\t\t\tdefault:\n\t\t\t\treturn `- ${r.label} ${r.lifecycle} ✗ — ${r.error ?? \"unknown error\"}`;\n\t\t}\n\t});\n\tconst header = `${all.length} background subagent${all.length === 1 ? \"\" : \"s\"} (${outstanding} running):`;\n\tconst hint = all.some((r) => r.lifecycle === \"done\") ? '\\nRead a finished one with TaskOutput(\"<label>\").' : \"\";\n\treturn {\n\t\tcontent: [{ type: \"text\", text: `${header}\\n${lines.join(\"\\n\")}${hint}` }],\n\t\tdetails: { status: \"list\", ok: true, outstanding },\n\t};\n}\n\n/**\n * TaskOutput tool: check on background subagents and pull their results.\n *\n * Background `Task` calls don't push their body into the conversation — they\n * leave it in the inbox and post a compact notification. TaskOutput is how the\n * model pulls a body, checks liveness, or waits. It never throws on a valid\n * handle (an error tool result would only confuse the loop): it reports status\n * instead. Modes: `list` (roster), a `task_id` to read/check one, and `wait` to\n * block until one task — or all outstanding tasks — finish.\n */\nexport function createTaskOutputToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof taskOutputParams, TaskOutputDetails>({\n\t\tname: \"TaskOutput\",\n\t\tlabel: \"TaskOutput\",\n\t\tdescription: [\n\t\t\t\"Check on background subagents dispatched via Task, and pull their results.\",\n\t\t\t'Pass a task_id/label (e.g. \"explore#1\") to read a finished subagent\\'s full result, or to see its status while it runs.',\n\t\t\t\"Set list:true (or omit task_id) to list every background subagent with its status and current activity.\",\n\t\t\t\"Set wait:true to block until that task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier).\",\n\t\t\t\"It never errors on a valid handle: a running task reports status, a finished one returns its result, an already-read one says so.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"check status / list / collect the results of background subagents\",\n\t\tparameters: taskOutputParams,\n\n\t\tasync execute(_toolCallId, params: TaskOutputParams, _signal, _onUpdate, ctx) {\n\t\t\t// Touch the pool so the inbox is wired to its progress stream for activity.\n\t\t\tsubagentInbox.observe(getSubagentPool(ctx.cwd));\n\t\t\tconst handle = params.task_id?.trim();\n\n\t\t\t// Barrier: wait for the target (or all outstanding) to settle first.\n\t\t\tif (params.wait) {\n\t\t\t\tconst timeout = params.timeout_ms ?? TASK_OUTPUT_DEFAULT_TIMEOUT_MS;\n\t\t\t\tif (handle) await subagentInbox.waitFor(handle, timeout);\n\t\t\t\telse await subagentInbox.waitForAll(timeout);\n\t\t\t}\n\n\t\t\t// Roster when asked, or when no specific task was named.\n\t\t\tif (params.list || !handle) {\n\t\t\t\treturn formatTaskRoster();\n\t\t\t}\n\n\t\t\tconst rec = subagentInbox.get(handle);\n\t\t\tif (!rec) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `No background task \"${handle}\". Call TaskOutput with list:true to see active tasks.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: handle, status: \"unknown\", ok: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (rec.lifecycle === \"running\") {\n\t\t\t\tconst activity = rec.lastActivity ? ` (currently: ${rec.lastActivity})` : \"\";\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `${rec.label} is still running — ${recordElapsed(rec)} elapsed${activity}. Call TaskOutput again, or with wait:true to block until it finishes.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: handle, status: \"running\", ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (rec.lifecycle === \"done\") {\n\t\t\t\tconst collected = subagentInbox.collect(handle);\n\t\t\t\tconst body = collected?.body ?? rec.summaryLine ?? \"(subagent returned no output)\";\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: body }],\n\t\t\t\t\tdetails: { task_id: handle, status: \"done\", ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (rec.lifecycle === \"collected\") {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `${rec.label} was already delivered — ${rec.summaryLine ?? \"(no summary kept)\"}.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: handle, status: \"collected\", ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// failed / stalled / timeout / cancelled\n\t\t\tconst glyph = rec.lifecycle === \"cancelled\" ? \"⊘\" : \"✗\";\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `${rec.label} ${rec.lifecycle} ${glyph} — ${rec.error ?? \"unknown error\"}.`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { task_id: handle, status: rec.lifecycle, ok: false },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst target = args.list ? \"list\" : String(args.task_id ?? \"\");\n\t\t\t// A friendly label (\"explore#1\") gets its agent's identity color so the\n\t\t\t// pull visually pairs with the dispatch that produced it; raw ids and\n\t\t\t// \"list\" stay dim.\n\t\t\tconst hashIdx = target.indexOf(\"#\");\n\t\t\tconst styledTarget =\n\t\t\t\thashIdx > 0 ? theme.fg(agentColorFor(target.slice(0, hashIdx)), target) : theme.fg(\"dim\", target);\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"TaskOutput \")) +\n\t\t\t\tstyledTarget +\n\t\t\t\t(args.wait ? theme.fg(\"dim\", \" (wait)\") : \"\");\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\n\t\trenderResult(result, _options, theme) {\n\t\t\t// Display-only colorization of the roster/status lines (the text sent to\n\t\t\t// the model stays plain). Lines that aren't roster rows — collected\n\t\t\t// bodies, status sentences — render exactly like the default fallback.\n\t\t\tconst text = result.content\n\t\t\t\t.map((c) => (c.type === \"text\" ? c.text : \"\"))\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (!text) return new Text(\"\", 0, 0);\n\t\t\tconst rosterLine =\n\t\t\t\t/^- (\\S+)\\s{2}(running|done \\(uncollected\\)|collected|failed|stalled|timeout|cancelled)(.*)$/;\n\t\t\tconst styled = text\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.map((line) => {\n\t\t\t\t\tconst match = rosterLine.exec(line);\n\t\t\t\t\tif (!match) return theme.fg(\"toolOutput\", line);\n\t\t\t\t\tconst [, label, status, rest] = match as unknown as [string, string, string, string];\n\t\t\t\t\tconst hashIdx = label.indexOf(\"#\");\n\t\t\t\t\tconst labelColor = hashIdx > 0 ? agentColorFor(label.slice(0, hashIdx)) : \"accent\";\n\t\t\t\t\tconst statusColor =\n\t\t\t\t\t\tstatus === \"running\"\n\t\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t\t: status.startsWith(\"done\")\n\t\t\t\t\t\t\t\t? \"success\"\n\t\t\t\t\t\t\t\t: status === \"collected\" || status === \"cancelled\"\n\t\t\t\t\t\t\t\t\t? \"muted\"\n\t\t\t\t\t\t\t\t\t: \"error\";\n\t\t\t\t\treturn `- ${theme.fg(labelColor, label)} ${theme.fg(statusColor, status)}${theme.fg(\"dim\", rest)}`;\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\treturn new Text(styled, 0, 0);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,yBAAyB,CAAC;AAE/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAiB7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEjE;;;;;6EAK6E;AAC7E;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CA2BvE;AAmCD,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IACjC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,OAAO,CAAC;IACZ,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAeD,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAsB,GAAG,cAAc,CAgUpF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,GAAG,EAAE,MAAM,GACT,MAAM,GAAG,SAAS,CAQpB;AAwRD;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,IAAI,cAAc,CA2I/D","sourcesContent":["/**\n * Task tool: delegate a focused task to a specialized subagent.\n *\n * Mirrors the Claude Code `Task` tool. The parent agent decides *when* to\n * delegate based on each agent's `description` (there is no deterministic gate)\n * and selects *which* agent via `subagent_type`. The chosen agent runs in a\n * fresh, isolated child process (SubagentPool) and only its final answer is\n * returned to the parent.\n *\n * Enabled by default (the `enableSubagent` setting defaults to true); disable\n * with `enableSubagent: false`. The `--enable-subagents` flag still force-enables\n * it. Nesting is bounded by the tree-wide depth cap (maxSubagentDepth, default 2:\n * a spawned subagent may itself delegate one more level, and depth-2 grandchildren\n * cannot). See buildSessionOptions in main.ts.\n */\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { agentColorFor } from \"../../modes/interactive/theme/theme.js\";\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { loadAgentRegistry } from \"../agent-registry.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { formatDurationSecs } from \"../format-duration.js\";\nimport { getProviderExhaustion } from \"../provider-health.js\";\nimport { SessionManager } from \"../session-manager.js\";\nimport { delegateAllowList, isDelegateAllowed } from \"../subagent-depth.js\";\nimport { type InboxRecord, subagentInbox } from \"../subagent-inbox.js\";\nimport type { SubagentResult, TaskResult } from \"../subagent-pool.js\";\nimport { getSubagentPool } from \"../subagent-pool-instance.js\";\nimport type { SubagentResultFile, SubagentTaskNode } from \"../subagent-result.js\";\nimport { taskStore } from \"../task-store.js\";\nimport { type WarmRunResult, WarmWorkerError } from \"../warm-subagent-pool.js\";\nimport { getWarmSubagentPool, warmSubagentsEnabled } from \"../warm-subagent-pool-instance.js\";\n\n// Re-exported from its home in agent-registry (where formatAgentsForPrompt uses\n// it to render the roster) so existing importers keep working without creating a\n// tools -> registry -> tools cycle.\nexport { summarizeAgentDescription } from \"../agent-registry.js\";\n\n/** System prompt appendix for the main session when the Task tool is enabled.\n * Instructs the parent agent on when and how to delegate effectively. The\n * available agents themselves are listed once, authoritatively, in the\n * `<available_agents>` block the system prompt emits whenever the Task tool is\n * active (see agent-session `_rebuildSystemPrompt`); this appendix references\n * that list rather than re-rendering the roster and paying for it twice. */\n/**\n * Build the main-session subagent instructions appended to the system prompt.\n *\n * The detailed background/barrier guidance (the three heaviest bullets) is only\n * emitted when the project actually has background-capable agents; otherwise a\n * single concise line covers the per-call `background: true` escape hatch. This\n * keeps the always-on cost down for the common case where nothing runs in the\n * background. `cwd` is used only to detect those agents.\n */\nexport function buildTaskMainPrompt(cwd: string = process.cwd()): string {\n\tconst hasBackgroundAgents = collectBackgroundAgentNames(cwd).size > 0;\n\n\tconst backgroundGuidance = hasBackgroundAgents\n\t\t? `- Background agents run non-blocking (or force per call with \\`background: true\\`): you get a short \"explore#1 finished\" notification and pull the full result with \\`TaskOutput\\` (e.g. \\`TaskOutput(\"explore#1\")\\`); \\`TaskOutput(list: true)\\` shows what's running.\n- After dispatching, don't idle — keep doing independent work (read/edit unrelated files, draft, dispatch more); barrier with \\`TaskOutput(wait: true)\\` (a named task, or all outstanding when no id) only when you genuinely can't proceed.`\n\t\t: `- You can force any task to run in the background with \\`background: true\\` (non-blocking): keep working, then pull its result with \\`TaskOutput\\` (e.g. \\`TaskOutput(\"explore#1\")\\`) or block on it with \\`TaskOutput(wait: true)\\`.`;\n\n\treturn `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer. Pick an agent by name from the <available_agents> list in this prompt and pass it as \\`subagent_type\\`.\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nModel tier (optional \\`complexity\\`): set \\`fast\\` for quick reads/lookups, \\`standard\\` for multi-file edits, \\`capable\\` for deep architecture work. It maps to a model from \\`settings.modelCategories\\`. Omit it to use the agent's default; an agent that pins its own model ignores \\`complexity\\`.\n\nGuidelines:\n- Choose the agent whose description best matches the task.\n- Make every task specific and self-contained. The subagent cannot see this conversation; pass all necessary context (files, constraints, prior findings) in \\`prompt\\`.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.\n- Delegate proactively when work is self-contained or parallelizable: multi-step investigation, read-only exploration (use \\`explore\\`), research before changes (use \\`plan\\`), drafting a standalone file/section, or running a long command/test suite. Dispatch independent subtasks in the same turn. Handle only trivial single-step edits or tightly interactive back-and-forth inline.\n${backgroundGuidance}\n- When working through a TodoWrite plan, mark the plan item in_progress BEFORE dispatching subagents for it: each dispatch is attributed to the current in_progress item in the user's task panel, so dispatching first (or with several items in_progress) leaves the run unattributed.\n- To continue a previous subagent (for example one that returned partial results), call Task again with \\`resume_task_id\\` set to its task_id; it resumes with its full prior transcript and \\`prompt\\` is your follow-up.`;\n}\n\nconst taskParams = Type.Object({\n\tdescription: Type.String({\n\t\tdescription: \"A short (3-5 word) description of the task, shown in the task panel.\",\n\t}),\n\tprompt: Type.String({\n\t\tdescription:\n\t\t\t\"The full, self-contained task for the subagent. It cannot see this conversation, so include all needed context, files, and constraints.\",\n\t}),\n\tsubagent_type: Type.String({\n\t\tdescription: \"The name of the specialized agent to delegate to. Must be one of the available agents.\",\n\t}),\n\tcomplexity: Type.Optional(\n\t\tType.Union([Type.Literal(\"fast\"), Type.Literal(\"standard\"), Type.Literal(\"capable\")], {\n\t\t\tdescription:\n\t\t\t\t\"Model tier for this dispatch: fast (quick reads/lookups), standard (multi-file edits), capable (deep architecture). Maps to settings.modelCategories. Ignored if the chosen agent pins its own model; omit to use the agent's default.\",\n\t\t}),\n\t),\n\tbackground: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Set true to run non-blocking: you get a short notification when it finishes and pull the full result with TaskOutput; set false to wait and get the answer inline. Defaults to the agent's own background setting.\",\n\t\t}),\n\t),\n\tresume_task_id: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional. To continue a previous subagent run, pass its task_id (returned by an earlier Task or TaskOutput call). The subagent resumes with its full prior transcript and `prompt` is your follow-up instruction.\",\n\t\t}),\n\t),\n});\n\ntype TaskParams = Static<typeof taskParams>;\n\nexport interface TaskToolDetails {\n\tsubagent_type: string;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** Pool-level task id usable for resume/polling. */\n\tpoolTaskId?: string;\n\t/** True when dispatched as a non-blocking background task. */\n\tbackground?: boolean;\n}\n\nexport interface TaskOutputDetails {\n\t/** The handle queried, when a specific task was named. */\n\ttask_id?: string;\n\t/** running | done | collected | failed | stalled | timeout | cancelled | list | empty | unknown */\n\tstatus: string;\n\tok: boolean;\n\t/** Number of subagents still running, included on roster/list responses. */\n\toutstanding?: number;\n}\n\n/**\n * A short, human-readable task name for the task panel: the first line limited\n * to ~8 words so it stays glanceable. A character cap guards a single long word.\n */\nfunction summarize(task: string): string {\n\tconst firstLine = (task.trim().split(\"\\n\")[0] ?? \"\").trim();\n\tif (!firstLine) return \"(task)\";\n\tconst words = firstLine.split(/\\s+/);\n\tlet name = words.length > 8 ? `${words.slice(0, 8).join(\" \")}…` : firstLine;\n\tif (name.length > 60) name = `${name.slice(0, 59)}…`;\n\treturn name;\n}\n\n/** Create the Task tool definition. Registered as a customTool when enabled. */\nexport function createTaskToolDefinition(cwd: string = process.cwd()): ToolDefinition {\n\t// Agents whose definitions opt into background execution. The agent loop reads\n\t// the tool's `background` flag per call and, for these, runs the dispatch\n\t// detached: the parent keeps reasoning and the subagent's answer is injected as\n\t// a follow-up message when it finishes (no polling needed). A per-call\n\t// `background` argument overrides the agent's default in either direction.\n\tconst backgroundAgents = collectBackgroundAgentNames(cwd);\n\treturn defineTool<typeof taskParams, TaskToolDetails>({\n\t\tname: TASK_TOOL_NAME,\n\t\tlabel: TASK_TOOL_NAME,\n\t\tbackground: (toolCall) => {\n\t\t\tconst override = toolCall.arguments?.background;\n\t\t\tif (typeof override === \"boolean\") return override;\n\t\t\treturn backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? \"\"));\n\t\t},\n\t\t// Kept to mechanics only: the when-to-use / when-not-to guidance lives once in\n\t\t// the system-prompt block (buildTaskMainPrompt), the available agents are\n\t\t// listed there too, and the `complexity`/`background`/`resume_task_id`\n\t\t// semantics live in their parameter descriptions. Repeating any of that here\n\t\t// would re-spend those tokens on every turn.\n\t\tdescription:\n\t\t\t\"Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation). Choose one of the available agents (listed in the system prompt) via `subagent_type` and pass everything it needs via `prompt`; the subagent returns only its final answer.\",\n\t\tpromptSnippet: \"delegate a self-contained task to a specialized subagent (choose via subagent_type)\",\n\t\tparameters: taskParams,\n\n\t\tasync execute(_toolCallId, params: TaskParams, signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\n\t\t\t// User-initiated cancel (Esc/abort): kill the dispatched run's whole\n\t\t\t// process tree and let the dispatch settle with status \"cancelled\" —\n\t\t\t// distinct from \"failed\" everywhere it surfaces (panel, inbox, result).\n\t\t\t// Cold dispatches only: a warm worker has no cancel surface (it is\n\t\t\t// reused), so warm runs still complete server-side and are discarded.\n\t\t\tconst cancelOnAbort = async (poolRunId: string, run: () => Promise<TaskResult>): Promise<TaskResult> => {\n\t\t\t\tif (!signal) return run();\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tpool.cancel?.(poolRunId);\n\t\t\t\t};\n\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\ttry {\n\t\t\t\t\treturn await run();\n\t\t\t\t} finally {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Pre-flight: if the inherited provider recently exhausted its quota (the\n\t\t\t// parent's own turn failed with a usage/rate-limit error that did not\n\t\t\t// recover), skip the spawn. Subagents run on the same provider, so this\n\t\t\t// would only burn another failed attempt. The signal self-expires and is\n\t\t\t// cleared on the next successful response.\n\t\t\tconst provider = ctx.model?.provider;\n\t\t\tconst exhaustion = provider ? getProviderExhaustion(provider) : undefined;\n\t\t\tif (exhaustion) {\n\t\t\t\tconst skippedRunId = newDispatchTaskId();\n\t\t\t\tconst skipped = taskStore.create(params.description?.trim() || summarize(params.prompt), {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: skippedRunId,\n\t\t\t\t\tlinkedTaskId: linkedTodoId(),\n\t\t\t\t});\n\t\t\t\tregisterSubagentDispatch(skippedRunId, subagentInbox.nextLabel(params.subagent_type));\n\t\t\t\ttaskStore.update(skipped.id, { status: \"failed\", note: `${provider} exhausted` });\n\t\t\t\ttaskStore.patchAgent(skippedRunId, { state: \"failed\" });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t`Did not dispatch subagent \"${params.subagent_type}\": the \"${provider}\" provider appears ` +\n\t\t\t\t\t\t\t\t`exhausted or rate-limited (this session just failed with: ${exhaustion.message}). ` +\n\t\t\t\t\t\t\t\t`Subagents run on the same provider, so dispatching would fail too. Wait for the quota to ` +\n\t\t\t\t\t\t\t\t`reset or switch model/provider, then retry — or complete the work directly in this session.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { subagent_type: params.subagent_type, ok: false, taskId: skipped.id },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Scoped delegation: a delegating agent may be restricted to certain subagent\n\t\t\t// types (its `delegate: <types>` frontmatter). The root is unrestricted.\n\t\t\tif (!isDelegateAllowed(params.subagent_type)) {\n\t\t\t\tconst allowed = delegateAllowList()?.join(\", \") ?? \"\";\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`This agent may not delegate to \"${params.subagent_type}\". Allowed subagent types: ${allowed || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Resume path: continue a previously dispatched subagent with a follow-up\n\t\t\t// prompt, reusing its persisted session (full prior transcript).\n\t\t\tconst resumeId = params.resume_task_id?.trim();\n\t\t\tif (resumeId) {\n\t\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\t\tconst runId = newDispatchTaskId();\n\t\t\t\tconst label = subagentInbox.nextLabel(params.subagent_type);\n\t\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: runId,\n\t\t\t\t\tlinkedTaskId: linkedTodoId(),\n\t\t\t\t});\n\t\t\t\tregisterSubagentDispatch(runId, label);\n\t\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await cancelOnAbort(runId, () =>\n\t\t\t\t\t\tpool.resume(resumeId, params.prompt, {\n\t\t\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\ttaskId: runId,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\t// The session lives under the original task id; keep it as the resume handle.\n\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, runId, task.id, resumeId);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tmarkDispatchFailed(task.id, runId);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// The model has already decided to delegate and which agent to use; honor\n\t\t\t// it. Validate the requested agent against the registry (no routing gate).\n\t\t\tconst registry = loadAgentRegistry({ cwd: ctx.cwd });\n\t\t\tconst def = registry.get(params.subagent_type);\n\t\t\tif (!def) {\n\t\t\t\tconst available = registry\n\t\t\t\t\t.list()\n\t\t\t\t\t.map((a) => a.name)\n\t\t\t\t\t.join(\", \");\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Unknown subagent_type: \"${params.subagent_type}\". Available agents: ${available || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\t// One roster row per dispatch, keyed by the (pre-allocated) pool task id.\n\t\t\t// Keying by agent TYPE made concurrent same-type runs share a single row:\n\t\t\t// their activity, state, and stats collided last-writer-wins. The inbox\n\t\t\t// already keys by task id; this aligns the panel to the same model. The\n\t\t\t// pool's task_progress events carry the task id, so patching by this id\n\t\t\t// routes live activity to the right row.\n\t\t\tconst poolTaskId = newDispatchTaskId();\n\t\t\tconst label = subagentInbox.nextLabel(params.subagent_type);\n\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\tsource: \"subagent\",\n\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\tagent: poolTaskId,\n\t\t\t\t// Tie this run to the plan item it executes (the single in_progress\n\t\t\t\t// TodoWrite task, when unambiguous) so the panel can nest it there.\n\t\t\t\tlinkedTaskId: linkedTodoId(),\n\t\t\t});\n\t\t\tregisterSubagentDispatch(poolTaskId, label);\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t// Fork agents inherit the parent's conversation via a forked session.\n\t\t\tconst forkSessionFile = def.fork\n\t\t\t\t? resolveForkSessionFile(def, ctx.sessionManager?.getSessionFile(), ctx.cwd)\n\t\t\t\t: undefined;\n\n\t\t\t// `complexity` is passed as the model: the pool's spawn() already lets a\n\t\t\t// non-`inherit` agent model win, then resolves a category string (fast/\n\t\t\t// standard/capable) via settings.modelCategories. So a pinned-model agent\n\t\t\t// ignores complexity, and an `inherit` agent picks up the requested tier —\n\t\t\t// no settings lookup needed here.\n\t\t\tconst dispatchModel = params.complexity ?? ctx.model?.id;\n\n\t\t\t// Whether this call runs detached. The agent loop reads the tool's\n\t\t\t// `background` flag (the same predicate) to run execute() detached; we\n\t\t\t// recompute it here to choose the notify-and-pull return shape.\n\t\t\tconst isBackground = params.background ?? backgroundAgents.has(params.subagent_type);\n\n\t\t\tif (isBackground) {\n\t\t\t\t// Notify-and-pull: register the dispatch in the inbox under the\n\t\t\t\t// pre-allocated id, await it, retain the body in the inbox, and return a\n\t\t\t\t// compact notification (not the body). The model pulls it with TaskOutput.\n\t\t\t\tsubagentInbox.observe(pool);\n\t\t\t\tsubagentInbox.start(poolTaskId, label, params.subagent_type);\n\n\t\t\t\t// Warm path (opt-in): run on a reused RPC worker, retain the body in the\n\t\t\t\t// inbox, and return the same notify-and-pull shape as the cold path. An\n\t\t\t\t// infra failure falls through to the cold dispatch below.\n\t\t\t\tif (warmSubagentsEnabled() && !forkSessionFile) {\n\t\t\t\t\tconst warm = getWarmSubagentPool(ctx.cwd);\n\t\t\t\t\tif (warm.isPoolable(params.subagent_type)) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst warmResult = await warm.dispatch(\n\t\t\t\t\t\t\t\tparams.prompt,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tagentType: params.subagent_type,\n\t\t\t\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t(activity) => taskStore.patchAgent(poolTaskId, { activity }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);\n\t\t\t\t\t\t\tsubagentInbox.finish(poolTaskId, dispatchResult);\n\t\t\t\t\t\t\treturn finalizeDispatchResult(\n\t\t\t\t\t\t\t\tdispatchResult,\n\t\t\t\t\t\t\t\tparams.subagent_type,\n\t\t\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\t\t\ttask.id,\n\t\t\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tif (!(error instanceof WarmWorkerError)) throw error;\n\t\t\t\t\t\t\tconsole.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await cancelOnAbort(poolTaskId, () =>\n\t\t\t\t\t\tpool.dispatch(params.prompt, {\n\t\t\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\t\t\tcontext: \"\",\n\t\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\tsessionFile: forkSessionFile,\n\t\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tsubagentInbox.finish(poolTaskId, dispatchResult);\n\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, poolTaskId, {\n\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tmarkDispatchFailed(task.id, poolTaskId);\n\t\t\t\t\tsubagentInbox.fail(poolTaskId, reason);\n\t\t\t\t\t// A background dispatch reports failure as a compact notification, not a\n\t\t\t\t\t// thrown tool error — the call was already answered by a placeholder.\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `${label} failed ✗ — ${reason}` }],\n\t\t\t\t\t\tdetails: {\n\t\t\t\t\t\t\tsubagent_type: params.subagent_type,\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\terror: reason,\n\t\t\t\t\t\t\ttaskId: task.id,\n\t\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\t\tbackground: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Warm path (opt-in): for an eligible foreground dispatch, run on a reused\n\t\t\t// RPC worker to skip the cold-boot. Fork agents (forkSessionFile set) and\n\t\t\t// non-poolable types are excluded. Any infra failure falls through to the\n\t\t\t// cold pool below, so enabling this can only change latency, never whether\n\t\t\t// the task can run.\n\t\t\tif (warmSubagentsEnabled() && !forkSessionFile) {\n\t\t\t\tconst warm = getWarmSubagentPool(ctx.cwd);\n\t\t\t\tif (warm.isPoolable(params.subagent_type)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst warmResult = await warm.dispatch(\n\t\t\t\t\t\t\tparams.prompt,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tagentType: params.subagent_type,\n\t\t\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// Mirror the cold pool's live progress on the task panel roster so a\n\t\t\t\t\t\t\t// warm dispatch reads as busy (⋯ grep), not stuck.\n\t\t\t\t\t\t\t(activity) => taskStore.patchAgent(poolTaskId, { activity }),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);\n\t\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, undefined);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// A genuine infra failure (worker crash/timeout) retries cold; any other\n\t\t\t\t\t\t// error is a real dispatch failure and propagates.\n\t\t\t\t\t\tif (!(error instanceof WarmWorkerError)) {\n\t\t\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\t\t\ttaskStore.patchAgent(poolTaskId, { state: \"failed\", activity: \"\" });\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconsole.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Foreground: block the turn and return the subagent's full answer inline.\n\t\t\ttry {\n\t\t\t\tconst dispatchResult = await cancelOnAbort(poolTaskId, () =>\n\t\t\t\t\tpool.dispatch(params.prompt, {\n\t\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\t\tcontext: \"\",\n\t\t\t\t\t\tmodel: dispatchModel,\n\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t\tsessionFile: forkSessionFile,\n\t\t\t\t\t\ttaskId: poolTaskId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn finalizeDispatchResult(\n\t\t\t\t\tdispatchResult,\n\t\t\t\t\tparams.subagent_type,\n\t\t\t\t\tpoolTaskId,\n\t\t\t\t\ttask.id,\n\t\t\t\t\tdispatchResult.task_id,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tmarkDispatchFailed(task.id, poolTaskId);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst type = args.subagent_type ?? \"agent\";\n\t\t\t// The [type] tag carries the agent's identity color — the same hue this\n\t\t\t// agent has in the task panel and TaskOutput — so a transcript full of\n\t\t\t// concurrent dispatches is scannable by color.\n\t\t\tconst text = theme.fg(\"toolTitle\", theme.bold(\"Agent \")) + theme.fg(agentColorFor(type), `[${type}]`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n\n/**\n * For a `fork: true` agent, fork the parent's session so the subagent inherits the\n * full parent conversation (and its prompt cache) instead of starting fresh. Returns\n * the forked session file to dispatch the child with, or undefined to fall back to a\n * fresh session (non-fork agent, no parent session, or an empty/invalid source).\n */\nexport function resolveForkSessionFile(\n\tdef: Pick<AgentDefinition, \"fork\">,\n\tparentSessionPath: string | undefined,\n\tcwd: string,\n): string | undefined {\n\tif (!def.fork || !parentSessionPath) return undefined;\n\ttry {\n\t\treturn SessionManager.forkFrom(parentSessionPath, cwd).getSessionFile();\n\t} catch {\n\t\t// Empty/invalid parent session: fall back to a fresh subagent session.\n\t\treturn undefined;\n\t}\n}\n\n/** Pre-allocated pool task id for a dispatch (matches the pool's own format). */\nfunction newDispatchTaskId(): string {\n\treturn `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\n/**\n * The TodoWrite plan item this dispatch is (most plausibly) working on: the\n * single in_progress main-agent task. TodoWrite discipline keeps exactly one\n * item in_progress, so when that holds the link is unambiguous; with zero or\n * several in_progress items no link is recorded rather than guessing. Used to\n * nest the run under its plan item in the task panel's flat lens.\n */\nfunction linkedTodoId(): number | undefined {\n\tconst inProgress = taskStore\n\t\t.list()\n\t\t.filter(\n\t\t\t(t) =>\n\t\t\t\tt.source === undefined &&\n\t\t\t\tt.agent === undefined &&\n\t\t\t\tt.parentTaskId === undefined &&\n\t\t\t\tt.status === \"in_progress\",\n\t\t);\n\treturn inProgress.length === 1 ? inProgress[0]?.id : undefined;\n}\n\n/**\n * Register this dispatch in the task store's roster, keyed by its run id (the\n * pool task id) so concurrent runs of the same agent type each get their own\n * row, state, activity, and stats. The friendly label (\"explore#1\") names the\n * row the same way the inbox names background tasks.\n */\nfunction registerSubagentDispatch(runId: string, label: string): void {\n\ttaskStore.upsertAgent({ id: runId, name: label, role: \"subagent\", kind: \"subagent\", state: \"running\" });\n}\n\n/**\n * Catch-path bookkeeping: mark the dispatch failed unless it already settled.\n * finalizeDispatchResult records a terminal status (failed/cancelled) *before*\n * throwing, so a blanket \"failed\" here would clobber a cancellation with a\n * failure the moment the thrown error passed through.\n */\nfunction markDispatchFailed(taskStoreId: number, runId: string): void {\n\tconst current = taskStore.list().find((t) => t.id === taskStoreId);\n\tif (current && current.status !== \"pending\" && current.status !== \"in_progress\") return;\n\ttaskStore.update(taskStoreId, { status: \"failed\" });\n\ttaskStore.patchAgent(runId, { state: \"failed\", activity: \"\" });\n}\n\n/** Names of agents configured to run in the background (non-blocking). */\nfunction collectBackgroundAgentNames(cwd: string): Set<string> {\n\tconst names = new Set<string>();\n\tfor (const agent of loadAgentRegistry({ cwd }).list()) {\n\t\tif (agent.background) names.add(agent.name);\n\t}\n\treturn names;\n}\n\n/**\n * Merge a child subagent's task subtree into the parent's task store, rooting\n * each top-level node under the dispatching task (`parentTaskId`). Recurses so a\n * subagent that itself delegated shows its nested work — the subtree the child\n * could not surface across the process boundary on its own. Each node is its own\n * task (no key-by-type collapse), preserving the order the child created them.\n */\nfunction mergeChildTaskTree(nodes: readonly SubagentTaskNode[] | undefined, parentTaskId: number): void {\n\tif (!nodes) return;\n\ttaskStore.batch(() => {\n\t\tfor (const node of nodes) {\n\t\t\tconst created = taskStore.create(node.title, {\n\t\t\t\tsource: node.source,\n\t\t\t\tsubagentMode: node.subagentMode,\n\t\t\t\tparentTaskId,\n\t\t\t});\n\t\t\ttaskStore.update(created.id, { status: node.status, usage: node.usage });\n\t\t\tmergeChildTaskTree(node.children, created.id);\n\t\t}\n\t});\n}\n\n/**\n * Adapt a warm-worker run into the TaskResult shape finalizeDispatchResult\n * consumes, so the warm and cold paths share one finish/render path. A warm run\n * returns its answer inline (no result.json), so we synthesize an equivalent\n * SubagentResult with the answer as the summary and the pulled usage. The warm\n * path is clean (no persisted session), so there is no task_tree to merge and no\n * resume handle.\n */\nfunction warmResultToTaskResult(warm: WarmRunResult, agentType: string, taskStoreId: number): TaskResult {\n\tconst resultData: SubagentResultFile = {\n\t\tsummary: warm.summary || \"(subagent returned no output)\",\n\t\tfiles_changed: [],\n\t\tconfidence: warm.ok ? 1 : 0,\n\t\tstatus: warm.status,\n\t\tusage: warm.usage,\n\t};\n\tconst result: SubagentResult = {\n\t\ttask_id: String(taskStoreId),\n\t\tok: warm.ok,\n\t\tstdout: \"\",\n\t\tstderr: \"\",\n\t\texit_code: warm.ok ? 0 : 1,\n\t\tstatus: warm.status,\n\t\terror: warm.error,\n\t\tresult_data: resultData as unknown as Record<string, unknown>,\n\t};\n\treturn { handled_inline: false, agent_type: agentType, result };\n}\n\n/**\n * Update the task panel from a finished dispatch and shape the tool result.\n *\n * Foreground calls return the subagent's full answer inline and signal a hard\n * failure by throwing (the agent loop derives a tool's error state from a thrown\n * error). A background call passes `background`: the body already lives in the\n * inbox, so it returns a compact, self-contained notification (success or\n * failure) and never throws — the call was already answered by a placeholder.\n */\nfunction finalizeDispatchResult(\n\tdispatchResult: TaskResult,\n\tsubagentType: string,\n\trunAgentId: string,\n\ttaskStoreId: number,\n\tresumeHandle: string | undefined,\n\tbackground?: { taskId: string; label: string },\n): { content: Array<{ type: \"text\"; text: string }>; details: TaskToolDetails } {\n\tconst result = dispatchResult.result;\n\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\tconst usage = resultData?.usage;\n\n\t// Merge the child's own task subtree under the dispatching task so nested\n\t// delegation (depth >= 2) is visible in the subagents lens's task tree.\n\tmergeChildTaskTree(resultData?.task_tree, taskStoreId);\n\n\t// Roll this run's usage into its own roster row (rows are per-dispatch).\n\tif (usage) {\n\t\ttaskStore.addAgentStats(runAgentId, { input: usage.input, output: usage.output, cost: usage.cost });\n\t}\n\n\tif (!result || !result.ok) {\n\t\t// A user-initiated cancel is not a failure: it gets its own terminal\n\t\t// status so the panel shows ⊘ cancelled (dim) instead of ✗ failed (red).\n\t\tconst cancelled = result?.status === \"cancelled\";\n\t\tconst failNote = result?.usedInheritedModelFallback ? \"inherited-model retry failed\" : undefined;\n\t\ttaskStore.update(taskStoreId, { status: cancelled ? \"cancelled\" : \"failed\", usage, note: failNote });\n\t\ttaskStore.patchAgent(runAgentId, { state: cancelled ? \"cancelled\" : \"failed\", activity: \"\" });\n\t\tconst reason = result?.error ?? (result?.status ? `subagent ${result.status}` : \"unknown error\");\n\t\tif (background) {\n\t\t\tconst verdict = cancelled ? \"cancelled ⊘\" : \"failed ✗\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: `${background.label} ${verdict} — ${reason}` }],\n\t\t\t\tdetails: {\n\t\t\t\t\tsubagent_type: subagentType,\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: reason,\n\t\t\t\t\ttaskId: taskStoreId,\n\t\t\t\t\tpoolTaskId: background.taskId,\n\t\t\t\t\tbackground: true,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (cancelled) {\n\t\t\tthrow new Error(`Subagent (${subagentType}) cancelled by user.`);\n\t\t}\n\t\tconst stderr = result?.stderr?.trim();\n\t\tthrow new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\\nstderr: ${stderr.slice(-500)}` : \"\"}`);\n\t}\n\n\t// Leave the task in the store with its final status; it stays visible in the\n\t// task panel until the next user message arrives. Surface a ⚠ cue when the run\n\t// fell back to the inherited model rather than emitting a chat message — and\n\t// clear any stale note otherwise (note is clearable via an explicit undefined).\n\tconst fallbackNote = dispatchResult.result?.usedInheritedModelFallback ? \"ran on inherited model\" : undefined;\n\ttaskStore.update(taskStoreId, { status: \"done\", usage, note: fallbackNote });\n\t// Rows are per-run, so this run settles to done regardless of siblings.\n\ttaskStore.patchAgent(runAgentId, { state: \"done\", activity: \"\" });\n\tlet answer = resultData?.summary || \"(subagent returned no output)\";\n\t// Partial results are resumable; surface the handle so the parent can continue.\n\tif (result.status === \"partial\" && resumeHandle) {\n\t\tanswer += `\\n\\n[Partial result. To continue this subagent, call Task again with resume_task_id=\"${resumeHandle}\".]`;\n\t}\n\n\tif (background) {\n\t\t// Compact notification: the body is retained in the inbox; the model pulls it\n\t\t// with TaskOutput. Keeps a wide swarm from flooding the parent's context.\n\t\tconst partial = result.status === \"partial\" ? \" (partial — resume to continue)\" : \"\";\n\t\tconst outstanding = subagentInbox.outstanding().length;\n\t\tconst tail = outstanding > 0 ? ` ${outstanding} still running.` : \"\";\n\t\tconst text =\n\t\t\t`${background.label} finished ✓${partial} — ${summarize(answer)}.${tail}\\n` +\n\t\t\t`Read the full result with TaskOutput(\"${background.label}\").`;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\tdetails: {\n\t\t\t\tsubagent_type: subagentType,\n\t\t\t\tok: true,\n\t\t\t\ttaskId: taskStoreId,\n\t\t\t\tpoolTaskId: background.taskId,\n\t\t\t\tbackground: true,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn {\n\t\tcontent: [{ type: \"text\", text: answer }],\n\t\tdetails: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },\n\t};\n}\n\nconst taskOutputParams = Type.Object({\n\ttask_id: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t'Handle of a background subagent — its task_id or friendly label (e.g. \"explore#1\") from a Task notification. Omit (or set list:true) to see every background task.',\n\t\t}),\n\t),\n\tlist: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"List all background subagents with their status (running/done/failed/cancelled) and current activity. No result bodies are returned.\",\n\t\t}),\n\t),\n\twait: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Block until the named task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier) — before returning. Bounded by timeout_ms.\",\n\t\t}),\n\t),\n\ttimeout_ms: Type.Optional(\n\t\tType.Number({ description: \"Maximum time to block in wait mode, in milliseconds (default 120000).\" }),\n\t),\n});\n\ntype TaskOutputParams = Static<typeof taskOutputParams>;\n\nconst TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 120_000;\n\n/**\n * Elapsed time a record has run (so far, or until it settled), in the same\n * format the task panel uses so the two surfaces always agree.\n */\nfunction recordElapsed(rec: InboxRecord): string {\n\tconst end = rec.endedAt ?? Date.now();\n\treturn formatDurationSecs((end - rec.startedAt) / 1000);\n}\n\n/** A compact roster of every known background subagent — status + activity, no bodies. */\nfunction formatTaskRoster(): { content: Array<{ type: \"text\"; text: string }>; details: TaskOutputDetails } {\n\tconst all = subagentInbox.list();\n\tconst outstanding = subagentInbox.outstanding().length;\n\tif (all.length === 0) {\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: \"No background subagents have been dispatched.\" }],\n\t\t\tdetails: { status: \"empty\", ok: true, outstanding: 0 },\n\t\t};\n\t}\n\tconst lines = all.map((r) => {\n\t\tconst when = recordElapsed(r);\n\t\tswitch (r.lifecycle) {\n\t\t\tcase \"running\":\n\t\t\t\treturn `- ${r.label} running ${when}${r.lastActivity ? ` · ${r.lastActivity}` : \"\"}`;\n\t\t\tcase \"done\":\n\t\t\t\treturn `- ${r.label} done (uncollected) ${when} — ${r.summaryLine ?? \"\"}`;\n\t\t\tcase \"collected\":\n\t\t\t\treturn `- ${r.label} collected ${when} — ${r.summaryLine ?? \"\"}`;\n\t\t\tcase \"cancelled\":\n\t\t\t\treturn `- ${r.label} cancelled ⊘ — ${r.error ?? \"cancelled by user\"}`;\n\t\t\tdefault:\n\t\t\t\treturn `- ${r.label} ${r.lifecycle} ✗ — ${r.error ?? \"unknown error\"}`;\n\t\t}\n\t});\n\tconst header = `${all.length} background subagent${all.length === 1 ? \"\" : \"s\"} (${outstanding} running):`;\n\tconst hint = all.some((r) => r.lifecycle === \"done\") ? '\\nRead a finished one with TaskOutput(\"<label>\").' : \"\";\n\treturn {\n\t\tcontent: [{ type: \"text\", text: `${header}\\n${lines.join(\"\\n\")}${hint}` }],\n\t\tdetails: { status: \"list\", ok: true, outstanding },\n\t};\n}\n\n/**\n * TaskOutput tool: check on background subagents and pull their results.\n *\n * Background `Task` calls don't push their body into the conversation — they\n * leave it in the inbox and post a compact notification. TaskOutput is how the\n * model pulls a body, checks liveness, or waits. It never throws on a valid\n * handle (an error tool result would only confuse the loop): it reports status\n * instead. Modes: `list` (roster), a `task_id` to read/check one, and `wait` to\n * block until one task — or all outstanding tasks — finish.\n */\nexport function createTaskOutputToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof taskOutputParams, TaskOutputDetails>({\n\t\tname: \"TaskOutput\",\n\t\tlabel: \"TaskOutput\",\n\t\tdescription: [\n\t\t\t\"Check on background subagents dispatched via Task, and pull their results.\",\n\t\t\t'Pass a task_id/label (e.g. \"explore#1\") to read a finished subagent\\'s full result, or to see its status while it runs.',\n\t\t\t\"Set list:true (or omit task_id) to list every background subagent with its status and current activity.\",\n\t\t\t\"Set wait:true to block until that task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier).\",\n\t\t\t\"It never errors on a valid handle: a running task reports status, a finished one returns its result, an already-read one says so.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"check status / list / collect the results of background subagents\",\n\t\tparameters: taskOutputParams,\n\n\t\tasync execute(_toolCallId, params: TaskOutputParams, _signal, _onUpdate, ctx) {\n\t\t\t// Touch the pool so the inbox is wired to its progress stream for activity.\n\t\t\tsubagentInbox.observe(getSubagentPool(ctx.cwd));\n\t\t\tconst handle = params.task_id?.trim();\n\n\t\t\t// Barrier: wait for the target (or all outstanding) to settle first.\n\t\t\tif (params.wait) {\n\t\t\t\tconst timeout = params.timeout_ms ?? TASK_OUTPUT_DEFAULT_TIMEOUT_MS;\n\t\t\t\tif (handle) await subagentInbox.waitFor(handle, timeout);\n\t\t\t\telse await subagentInbox.waitForAll(timeout);\n\t\t\t}\n\n\t\t\t// Roster when asked, or when no specific task was named.\n\t\t\tif (params.list || !handle) {\n\t\t\t\treturn formatTaskRoster();\n\t\t\t}\n\n\t\t\tconst rec = subagentInbox.get(handle);\n\t\t\tif (!rec) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `No background task \"${handle}\". Call TaskOutput with list:true to see active tasks.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: handle, status: \"unknown\", ok: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (rec.lifecycle === \"running\") {\n\t\t\t\tconst activity = rec.lastActivity ? ` (currently: ${rec.lastActivity})` : \"\";\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `${rec.label} is still running — ${recordElapsed(rec)} elapsed${activity}. Call TaskOutput again, or with wait:true to block until it finishes.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: handle, status: \"running\", ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (rec.lifecycle === \"done\") {\n\t\t\t\tconst collected = subagentInbox.collect(handle);\n\t\t\t\tconst body = collected?.body ?? rec.summaryLine ?? \"(subagent returned no output)\";\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: body }],\n\t\t\t\t\tdetails: { task_id: handle, status: \"done\", ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (rec.lifecycle === \"collected\") {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `${rec.label} was already delivered — ${rec.summaryLine ?? \"(no summary kept)\"}.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: handle, status: \"collected\", ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// failed / stalled / timeout / cancelled\n\t\t\tconst glyph = rec.lifecycle === \"cancelled\" ? \"⊘\" : \"✗\";\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `${rec.label} ${rec.lifecycle} ${glyph} — ${rec.error ?? \"unknown error\"}.`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { task_id: handle, status: rec.lifecycle, ok: false },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst target = args.list ? \"list\" : String(args.task_id ?? \"\");\n\t\t\t// A friendly label (\"explore#1\") gets its agent's identity color so the\n\t\t\t// pull visually pairs with the dispatch that produced it; raw ids and\n\t\t\t// \"list\" stay dim.\n\t\t\tconst hashIdx = target.indexOf(\"#\");\n\t\t\tconst styledTarget =\n\t\t\t\thashIdx > 0 ? theme.fg(agentColorFor(target.slice(0, hashIdx)), target) : theme.fg(\"dim\", target);\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"TaskOutput \")) +\n\t\t\t\tstyledTarget +\n\t\t\t\t(args.wait ? theme.fg(\"dim\", \" (wait)\") : \"\");\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\n\t\trenderResult(result, _options, theme) {\n\t\t\t// Display-only colorization of the roster/status lines (the text sent to\n\t\t\t// the model stays plain). Lines that aren't roster rows — collected\n\t\t\t// bodies, status sentences — render exactly like the default fallback.\n\t\t\tconst text = result.content\n\t\t\t\t.map((c) => (c.type === \"text\" ? c.text : \"\"))\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (!text) return new Text(\"\", 0, 0);\n\t\t\tconst rosterLine =\n\t\t\t\t/^- (\\S+)\\s{2}(running|done \\(uncollected\\)|collected|failed|stalled|timeout|cancelled)(.*)$/;\n\t\t\tconst styled = text\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.map((line) => {\n\t\t\t\t\tconst match = rosterLine.exec(line);\n\t\t\t\t\tif (!match) return theme.fg(\"toolOutput\", line);\n\t\t\t\t\tconst [, label, status, rest] = match as unknown as [string, string, string, string];\n\t\t\t\t\tconst hashIdx = label.indexOf(\"#\");\n\t\t\t\t\tconst labelColor = hashIdx > 0 ? agentColorFor(label.slice(0, hashIdx)) : \"accent\";\n\t\t\t\t\tconst statusColor =\n\t\t\t\t\t\tstatus === \"running\"\n\t\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t\t: status.startsWith(\"done\")\n\t\t\t\t\t\t\t\t? \"success\"\n\t\t\t\t\t\t\t\t: status === \"collected\" || status === \"cancelled\"\n\t\t\t\t\t\t\t\t\t? \"muted\"\n\t\t\t\t\t\t\t\t\t: \"error\";\n\t\t\t\t\treturn `- ${theme.fg(labelColor, label)} ${theme.fg(statusColor, status)}${theme.fg(\"dim\", rest)}`;\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\treturn new Text(styled, 0, 0);\n\t\t},\n\t});\n}\n"]}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* fresh, isolated child process (SubagentPool) and only its final answer is
|
|
8
8
|
* returned to the parent.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* `enableSubagent
|
|
10
|
+
* Enabled by default (the `enableSubagent` setting defaults to true); disable
|
|
11
|
+
* with `enableSubagent: false`. The `--enable-subagents` flag still force-enables
|
|
12
|
+
* it. Nesting is bounded by the tree-wide depth cap (maxSubagentDepth, default 2:
|
|
13
|
+
* a spawned subagent may itself delegate one more level, and depth-2 grandchildren
|
|
14
|
+
* cannot). See buildSessionOptions in main.ts.
|
|
12
15
|
*/
|
|
13
16
|
import { Text } from "@kolisachint/hoocode-tui";
|
|
14
17
|
import { Type } from "typebox";
|
|
@@ -35,7 +38,21 @@ export { summarizeAgentDescription } from "../agent-registry.js";
|
|
|
35
38
|
* `<available_agents>` block the system prompt emits whenever the Task tool is
|
|
36
39
|
* active (see agent-session `_rebuildSystemPrompt`); this appendix references
|
|
37
40
|
* that list rather than re-rendering the roster and paying for it twice. */
|
|
38
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Build the main-session subagent instructions appended to the system prompt.
|
|
43
|
+
*
|
|
44
|
+
* The detailed background/barrier guidance (the three heaviest bullets) is only
|
|
45
|
+
* emitted when the project actually has background-capable agents; otherwise a
|
|
46
|
+
* single concise line covers the per-call `background: true` escape hatch. This
|
|
47
|
+
* keeps the always-on cost down for the common case where nothing runs in the
|
|
48
|
+
* background. `cwd` is used only to detect those agents.
|
|
49
|
+
*/
|
|
50
|
+
export function buildTaskMainPrompt(cwd = process.cwd()) {
|
|
51
|
+
const hasBackgroundAgents = collectBackgroundAgentNames(cwd).size > 0;
|
|
52
|
+
const backgroundGuidance = hasBackgroundAgents
|
|
53
|
+
? `- Background agents run non-blocking (or force per call with \`background: true\`): you get a short "explore#1 finished" notification and pull the full result with \`TaskOutput\` (e.g. \`TaskOutput("explore#1")\`); \`TaskOutput(list: true)\` shows what's running.
|
|
54
|
+
- After dispatching, don't idle — keep doing independent work (read/edit unrelated files, draft, dispatch more); barrier with \`TaskOutput(wait: true)\` (a named task, or all outstanding when no id) only when you genuinely can't proceed.`
|
|
55
|
+
: `- You can force any task to run in the background with \`background: true\` (non-blocking): keep working, then pull its result with \`TaskOutput\` (e.g. \`TaskOutput("explore#1")\`) or block on it with \`TaskOutput(wait: true)\`.`;
|
|
39
56
|
return `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer. Pick an agent by name from the <available_agents> list in this prompt and pass it as \`subagent_type\`.
|
|
40
57
|
|
|
41
58
|
When to delegate:
|
|
@@ -52,9 +69,7 @@ Guidelines:
|
|
|
52
69
|
- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.
|
|
53
70
|
- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.
|
|
54
71
|
- Delegate proactively when work is self-contained or parallelizable: multi-step investigation, read-only exploration (use \`explore\`), research before changes (use \`plan\`), drafting a standalone file/section, or running a long command/test suite. Dispatch independent subtasks in the same turn. Handle only trivial single-step edits or tightly interactive back-and-forth inline.
|
|
55
|
-
|
|
56
|
-
- After dispatching background work, DO NOT stop and wait — that wastes the parallelism and looks stuck. Immediately continue with the next useful thing: read or edit an independent file, draft the parts of your answer that don't depend on the pending result, or dispatch more independent subtasks. Only barrier (with \`TaskOutput(wait: true)\`) when you genuinely cannot proceed without the result. Prefer background dispatch for any self-contained or parallelizable work so your turn never blocks on a subagent.
|
|
57
|
-
- Use **TaskOutput** to manage background subagents: \`TaskOutput(list: true)\` shows every running/finished subagent and what each is doing; \`TaskOutput("explore#1")\` reads a finished subagent's full result (or reports its status if still running); \`TaskOutput(wait: true)\` blocks until a named task — or, with no task_id, ALL outstanding subagents — finish. Dispatch a batch in one turn, then barrier on them with \`TaskOutput(wait: true)\` only once you've exhausted the work you can do without them.
|
|
72
|
+
${backgroundGuidance}
|
|
58
73
|
- When working through a TodoWrite plan, mark the plan item in_progress BEFORE dispatching subagents for it: each dispatch is attributed to the current in_progress item in the user's task panel, so dispatching first (or with several items in_progress) leaves the run unattributed.
|
|
59
74
|
- To continue a previous subagent (for example one that returned partial results), call Task again with \`resume_task_id\` set to its task_id; it resumes with its full prior transcript and \`prompt\` is your follow-up.`;
|
|
60
75
|
}
|
|
@@ -109,14 +124,12 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
109
124
|
return override;
|
|
110
125
|
return backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? ""));
|
|
111
126
|
},
|
|
112
|
-
// Kept
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about. Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.",
|
|
119
|
-
].join("\n"),
|
|
127
|
+
// Kept to mechanics only: the when-to-use / when-not-to guidance lives once in
|
|
128
|
+
// the system-prompt block (buildTaskMainPrompt), the available agents are
|
|
129
|
+
// listed there too, and the `complexity`/`background`/`resume_task_id`
|
|
130
|
+
// semantics live in their parameter descriptions. Repeating any of that here
|
|
131
|
+
// would re-spend those tokens on every turn.
|
|
132
|
+
description: "Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation). Choose one of the available agents (listed in the system prompt) via `subagent_type` and pass everything it needs via `prompt`; the subagent returns only its final answer.",
|
|
120
133
|
promptSnippet: "delegate a self-contained task to a specialized subagent (choose via subagent_type)",
|
|
121
134
|
parameters: taskParams,
|
|
122
135
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|