{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,yBAAyB,CAAC;AAG/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAkB7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEjE;;;;;6EAK6E;AAC7E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CASvE;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,CAsUpF;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,CAwL/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 { EMBEDDED_PROMPTS } from \"../../init-templates.generated.js\";\nimport { agentColorFor } from \"../../modes/interactive/theme/theme.js\";\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { agentLog } from \"../agent-log.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 { formatTokens } from \"../format-tokens.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 prose lives in `templates/prompts/task-*.md` and reaches here through the\n * build-time embed. It is the largest block of always-on text hoocode emits\n * (~600 tok/turn once the Task tool is on) and it had no interpolation beyond\n * the single background slot, so keeping it as a TypeScript string constant\n * bought nothing and made it awkward to edit as the prose it is.\n *\n * The detailed background/barrier guidance (the two 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\tconst backgroundGuidance = (\n\t\tEMBEDDED_PROMPTS[hasBackgroundAgents ? \"task-background-agents\" : \"task-background-none\"] ?? \"\"\n\t).trim();\n\n\t// Trimmed at both ends: the template file ends with a newline, and this is\n\t// appended to a system prompt that manages its own separators.\n\treturn (EMBEDDED_PROMPTS[\"task-main\"] ?? \"\").replace(\"{{BACKGROUND_GUIDANCE}}\", backgroundGuidance).trim();\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\t// Snapshot the caller's available models so unconfigured model-category\n\t\t\t// tiers (fast/standard/capable) resolve to a derived default instead of a\n\t\t\t// no-op. Explicit settings.modelCategories still win inside the pool. The\n\t\t\t// registry can be absent in some tool contexts; an empty list just keeps\n\t\t\t// category derivation a no-op (inherit parent), so guard the access.\n\t\t\tconst availableModels = ctx.modelRegistry?.getAvailable() ?? [];\n\t\t\tconst pool = getSubagentPool(ctx.cwd, availableModels);\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, availableModels);\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\tagentLog(`[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, availableModels);\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 (⋯ search), 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\tagentLog(`[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\t// The pool is normally already created by a prior dispatch; pass available\n\t\t\t// models so a first-touch here still seeds category derivation.\n\t\t\tsubagentInbox.observe(getSubagentPool(ctx.cwd, ctx.modelRegistry?.getAvailable() ?? []));\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 framing/colorization (the text sent to the model stays\n\t\t\t// plain). A single-task pull renders as a result CARD — a status-stamped\n\t\t\t// header, a left spine down the body, and a closing corner — so a\n\t\t\t// collected result reads as a discrete artifact instead of blending into\n\t\t\t// the surrounding tool chatter. Roster/list responses keep the line\n\t\t\t// colorization.\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\n\t\t\tconst details = result.details as TaskOutputDetails | undefined;\n\t\t\tconst cardStatus = details?.task_id ? details.status : undefined;\n\t\t\tif (cardStatus && cardStatus !== \"list\" && cardStatus !== \"empty\") {\n\t\t\t\tconst presentation: Record<\n\t\t\t\t\tstring,\n\t\t\t\t\t{ glyph: string; color: \"success\" | \"warning\" | \"error\" | \"muted\" | \"dim\" }\n\t\t\t\t> = {\n\t\t\t\t\tdone: { glyph: \"✓\", color: \"success\" },\n\t\t\t\t\trunning: { glyph: \"◐\", color: \"warning\" },\n\t\t\t\t\tcollected: { glyph: \"✓\", color: \"muted\" },\n\t\t\t\t\tcancelled: { glyph: \"⊘\", color: \"dim\" },\n\t\t\t\t\tunknown: { glyph: \"?\", color: \"dim\" },\n\t\t\t\t};\n\t\t\t\tconst { glyph, color } = presentation[cardStatus] ?? { glyph: \"✗\", color: \"error\" as const };\n\t\t\t\tconst label = details?.task_id ?? \"\";\n\t\t\t\tconst hashIdx = label.indexOf(\"#\");\n\t\t\t\tconst labelColor = hashIdx > 0 ? agentColorFor(label.slice(0, hashIdx)) : \"accent\";\n\n\t\t\t\t// Look up inbox record for elapsed time\n\t\t\t\tconst inboxRecord = subagentInbox.get(label);\n\t\t\t\tconst elapsed = inboxRecord ? (inboxRecord.endedAt ?? Date.now()) - inboxRecord.startedAt : undefined;\n\t\t\t\tconst elapsedText = elapsed ? ` ${formatDurationSecs(elapsed / 1000)}` : \"\";\n\n\t\t\t\t// Look up task for token count\n\t\t\t\tconst task = inboxRecord ? taskStore.list().find((t) => t.agent === inboxRecord.taskId) : undefined;\n\t\t\t\tconst totalTokens = task?.usage ? task.usage.input + task.usage.output : undefined;\n\t\t\t\tconst tokenText = totalTokens !== undefined ? ` ${formatTokens(totalTokens)}` : \"\";\n\n\t\t\t\tconst spine = (s: string) => theme.fg(\"borderMuted\", s);\n\t\t\t\tconst header =\n\t\t\t\t\t`${spine(\"╭\")} ${theme.fg(color, glyph)} ` +\n\t\t\t\t\t`${theme.bold(theme.fg(color, cardStatus))} ${theme.fg(labelColor, label)}${tokenText}${elapsedText}`;\n\t\t\t\tconst body = text\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.map((line) => `${spine(\"│\")} ${theme.fg(\"toolOutput\", line)}`)\n\t\t\t\t\t.join(\"\\n\");\n\t\t\t\treturn new Text(`${header}\\n${body}\\n${spine(\"╰\")}`, 0, 0);\n\t\t\t}\n\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"]}