@kolisachint/hoocode-agent 0.4.14 → 0.4.16
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 +24 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts +107 -0
- package/dist/core/agent-frontmatter.d.ts.map +1 -0
- package/dist/core/agent-frontmatter.js +189 -0
- package/dist/core/agent-frontmatter.js.map +1 -0
- package/dist/core/agent-registry.d.ts +52 -0
- package/dist/core/agent-registry.d.ts.map +1 -0
- package/dist/core/agent-registry.js +131 -0
- package/dist/core/agent-registry.js.map +1 -0
- package/dist/core/lifeguard.d.ts.map +1 -1
- package/dist/core/lifeguard.js +5 -5
- package/dist/core/lifeguard.js.map +1 -1
- package/dist/core/output-verifier.d.ts.map +1 -1
- package/dist/core/output-verifier.js +2 -2
- package/dist/core/output-verifier.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +54 -3
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +152 -62
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent-result.d.ts +11 -2
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +17 -4
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +2 -2
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +43 -15
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +266 -113
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +13 -5
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +6 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts +2 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +29 -2
- 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;;;;;;;GAOG;AAEH;0EAC0E;AAC1E,eAAO,MAAM,oBAAoB,gsEA2BuD,CAAC;AAKzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAqCnD,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAgBD,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED,oFAAoF;AACpF,wBAAgB,4BAA4B,IAAI,cAAc,CA+F7D","sourcesContent":["/**\n * Subagent tool: delegate a focused task to a fresh, isolated agent loop.\n *\n * The tool registers a task in the shared task store (visible in the task panel),\n * runs the subagent to completion, and returns ONLY the subagent's final\n * answer. It is an optional, opt-in tool (enabled via --subagent or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\n/** System prompt appendix for the main session when subagent tooling is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport const SUBAGENT_MAIN_PROMPT = `You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n- doc: write documentation, README, or comments (read, write, edit, grep, find, ls, bash).\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\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the \\`context\\` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.\n\nChoosing inline vs subagent:\n- Default to handling work inline. Delegate only when one of the \"When to delegate\" cases clearly applies.\n- Handle inline (do NOT delegate): quick lookups, single-file reads, edits under ~50 lines, anything needing tight back-and-forth with your current reasoning, and follow-ups on files you are actively reasoning about.\n- When you do call subagent, a deterministic dispatch evaluator confirms the task is worth delegating and selects the mode for you. Pass the mode you think fits; it is corrected automatically if a better match is detected.\n- Use force=true to bypass evaluation only when you are certain a subagent is required.`;\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { DispatchEvaluator } from \"../dispatch-evaluator.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport type { SubagentMode } from \"../subagent.js\";\nimport { getSubagentPool } from \"../subagent-pool-instance.js\";\nimport type { SubagentResultFile } from \"../subagent-result.js\";\nimport { taskStore } from \"../task-store.js\";\n\nconst subagentParams = Type.Object({\n\ttask: Type.String({\n\t\tdescription:\n\t\t\t\"The task to delegate. Make it specific and self-contained; the subagent cannot see this conversation.\",\n\t}),\n\tcontext: Type.String({\n\t\tdescription:\n\t\t\t'Context distilled from the conversation the subagent needs (files, constraints, prior findings). Pass \"\" if none.',\n\t}),\n\tmode: Type.Union(\n\t\t[\n\t\t\tType.Literal(\"explore\"),\n\t\t\tType.Literal(\"edit\"),\n\t\t\tType.Literal(\"test\"),\n\t\t\tType.Literal(\"fix\"),\n\t\t\tType.Literal(\"review\"),\n\t\t\tType.Literal(\"doc\"),\n\t\t],\n\t\t{\n\t\t\tdescription:\n\t\t\t\t\"explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review. doc: write documentation.\",\n\t\t},\n\t),\n\tforce: Type.Boolean({\n\t\tdescription:\n\t\t\t\"Bypass dispatch evaluation and spawn the subagent directly. Use when you are certain a subagent is required.\",\n\t\tdefault: false,\n\t}),\n});\n\ntype SubagentParams = Static<typeof subagentParams>;\n\nexport interface SubagentToolDetails {\n\tmode: SubagentMode;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** True when the evaluator handled the task inline instead of delegating. */\n\tinline?: boolean;\n}\n\n/**\n * A short, human-readable task name for the task panel: the first line of the\n * task limited to ~4–8 words so it stays glanceable in the pane. A character cap\n * guards against a single very 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/** Quick check: should this task go to a subagent? */\nexport function isSubagentRecommended(task: string): boolean {\n\tconst evaluator = new DispatchEvaluator();\n\treturn evaluator.evaluate(task).should_delegate;\n}\n\n/** Create the subagent tool definition. Registered as a customTool when enabled. */\nexport function createSubagentToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof subagentParams, SubagentToolDetails>({\n\t\tname: \"subagent\",\n\t\tlabel: \"Subagent\",\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Pass everything it needs via `context`. The subagent returns only its final answer.\",\n\t\t\t\"Modes: explore, edit, test, fix, review, doc.\",\n\t\t\t\"WHEN TO USE: (1) The work is self-contained and you do not need to see intermediate steps — only the final result.\",\n\t\t\t\"(2) You want to investigate or edit something in parallel without losing your current context or reasoning chain.\",\n\t\t\t\"(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs).\",\n\t\t\t\"(4) You need to run a long command or test suite and wait for its output without blocking your own reasoning.\",\n\t\t\t\"Do NOT use for tasks that require tight back-and-forth with your current reasoning or that change files you are actively reasoning about.\",\n\t\t\t\"Prefer handling small, quick, or single-file tasks yourself; delegate only self-contained units of work.\",\n\t\t\t\"The dispatch evaluator confirms whether delegation is warranted and picks the mode; force=true bypasses it when you are certain a subagent is required.\",\n\t\t].join(\" \"),\n\t\tpromptSnippet: \"delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review/doc)\",\n\t\tparameters: subagentParams,\n\n\t\tasync execute(_toolCallId, params: SubagentParams, _signal, _onUpdate, ctx) {\n\t\t\tconst forcedMode = params.mode as SubagentMode;\n\n\t\t\t// Dispatch evaluation: a single evaluator/analysis decides inline vs delegate\n\t\t\t// and (when not forced) which mode to use.\n\t\t\tconst evaluator = new DispatchEvaluator();\n\t\t\tconst analysis = evaluator.evaluate(params.task);\n\t\t\tif (!params.force && !analysis.should_delegate) {\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\",\n\t\t\t\t\t\t\ttext: `Task is simple enough for inline handling. Reason: ${analysis.reason}. Use force=true for subagent override.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { mode: forcedMode, ok: true, taskId: 0, inline: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst mode: SubagentMode = params.force ? forcedMode : ((analysis.agent_type as SubagentMode) ?? \"explore\");\n\t\t\tconst summary = summarize(params.task);\n\n\t\t\tconst task = taskStore.create(summary, { subagentMode: mode });\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\ttry {\n\t\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\t\t\t\tconst dispatchResult = await pool.dispatch(params.task, {\n\t\t\t\t\tforceAgent: mode,\n\t\t\t\t\tcontext: params.context,\n\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t});\n\n\t\t\t\tconst result = dispatchResult.result;\n\t\t\t\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\t\t\t\tconst usage = resultData?.usage;\n\n\t\t\t\tif (!result || !result.ok) {\n\t\t\t\t\t// Signal failure by throwing: the agent loop derives a tool's error\n\t\t\t\t\t// state from a thrown error, not from a returned flag.\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\", usage });\n\t\t\t\t\tconst reason =\n\t\t\t\t\t\tresult?.error ??\n\t\t\t\t\t\t(result?.budget_exceeded\n\t\t\t\t\t\t\t? \"token budget exceeded before producing a result\"\n\t\t\t\t\t\t\t: result?.status\n\t\t\t\t\t\t\t\t? `subagent ${result.status}`\n\t\t\t\t\t\t\t\t: \"unknown error\");\n\t\t\t\t\tthrow new Error(`Subagent (${mode}) failed: ${reason}`);\n\t\t\t\t}\n\n\t\t\t\t// Leave the task in the store with its final status. It stays visible in\n\t\t\t\t// the task panel until the next user message arrives (taskStore.reset is\n\t\t\t\t// called when the user starts the next turn).\n\t\t\t\ttaskStore.update(task.id, { status: \"done\", usage });\n\t\t\t\tconst answer = resultData?.summary || \"(subagent returned no output)\";\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: answer }],\n\t\t\t\t\tdetails: { mode, ok: true, taskId: task.id },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst mode = args.mode ?? \"explore\";\n\t\t\tconst preview = summarize(args.task ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"subagent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${mode}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 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;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAO7D;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAwBrE;AASD;0EAC0E;AAC1E,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAoBvE;AAuBD,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,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,OAAO,CAAC;CACZ;AAeD;iFACiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAsB,GAAG,cAAc,CAkHpF;AAmED;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,cAAc,CAiD/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 --subagent 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 { loadAgentRegistry } from \"../agent-registry.js\";\nimport { DispatchEvaluator } from \"../dispatch-evaluator.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport type { SubagentPool, TaskResult } from \"../subagent-pool.js\";\nimport { getSubagentPool } from \"../subagent-pool-instance.js\";\nimport type { SubagentResultFile } from \"../subagent-result.js\";\nimport { taskStore } from \"../task-store.js\";\n\n/**\n * Condense a (possibly multi-line, bulleted) agent description into a single\n * useful one-liner for the agent picker list.\n *\n * Built-in agent descriptions open with a boilerplate header (\"Use this\n * subagent ONLY when:\") followed by \"when to use\" bullets and a \"DO NOT use\"\n * section. Taking the first line alone yields that identical header for every\n * agent, so instead surface the first meaningful bullets (or the first prose\n * line) from the positive \"when to use\" region.\n */\nexport function summarizeAgentDescription(description: string): string {\n\tconst lines = description\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.trim())\n\t\t.filter((line) => line.length > 0);\n\tif (lines.length === 0) return \"\";\n\n\t// Keep only the positive region: everything before a \"DO NOT use\" section.\n\tconst stop = lines.findIndex((line) => /^(do\\s*not|don'?t|avoid)\\b/i.test(line));\n\tconst region = stop === -1 ? lines : lines.slice(0, stop);\n\n\t// Drop a leading header line (e.g. \"Use this subagent ONLY when:\").\n\tconst body = region.length > 1 && region[0]!.endsWith(\":\") ? region.slice(1) : region;\n\n\tconst stripBullet = (line: string) => line.replace(/^[-*\\u2022]\\s+/, \"\").trim();\n\tconst bullets = body\n\t\t.filter((line) => /^[-*\\u2022]\\s+/.test(line))\n\t\t.map(stripBullet)\n\t\t.filter((line) => line.length > 0);\n\n\tconst summary = bullets.length > 0 ? bullets.slice(0, 3).join(\"; \") : (body[0] ?? lines[0] ?? \"\").replace(/:$/, \"\");\n\n\tconst MAX = 200;\n\treturn summary.length > MAX ? `${summary.slice(0, MAX - 1).trimEnd()}\\u2026` : summary;\n}\n\n/** Render the available agents as a \"- name: description\" list for prompts. */\nfunction describeAvailableAgents(cwd: string): string {\n\tconst agents = loadAgentRegistry({ cwd }).list();\n\tif (agents.length === 0) return \"(no agents available)\";\n\treturn agents.map((a) => `- ${a.name}: ${summarizeAgentDescription(a.description)}`).join(\"\\n\");\n}\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. */\nexport function buildTaskMainPrompt(cwd: string = process.cwd()): 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.\n\nAvailable agents (choose one via \\`subagent_type\\`):\n${describeAvailableAgents(cwd)}\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\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- Default to handling small, quick, or single-file work inline; delegate only self-contained units.\n- Some agents are configured to run in the background (non-blocking). For those, Task returns immediately with a task_id; use the **TaskOutput** tool with that task_id to check status and collect the final answer.\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\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\ttask_id: string;\n\tstatus: string;\n\tok: boolean;\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/** Quick advisory check: would the dispatch evaluator delegate this task?\n * The evaluator is non-blocking; this is exposed for diagnostics/tools only. */\nexport function isSubagentRecommended(task: string): boolean {\n\treturn new DispatchEvaluator().evaluate(task).should_delegate;\n}\n\n/** Create the Task tool definition. Registered as a customTool when enabled. */\nexport function createTaskToolDefinition(cwd: string = process.cwd()): ToolDefinition {\n\tconst agentList = describeAvailableAgents(cwd);\n\treturn defineTool<typeof taskParams, TaskToolDetails>({\n\t\tname: \"Task\",\n\t\tlabel: \"Task\",\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).\",\n\t\t\t\"Select the agent via `subagent_type`; pass everything it needs via `prompt`. The subagent returns only its final answer.\",\n\t\t\t\"Available agents:\",\n\t\t\tagentList,\n\t\t\t\"WHEN TO USE: (1) self-contained work where you only need the final result;\",\n\t\t\t\"(2) parallel investigation/edits without losing your reasoning chain;\",\n\t\t\t\"(3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs);\",\n\t\t\t\"(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.\",\n\t\t\t\"Prefer handling small, quick, or single-file tasks yourself; delegate only self-contained units of work.\",\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// 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 task = taskStore.create(summary, { subagentMode: params.subagent_type });\n\t\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await pool.resume(resumeId, params.prompt, {\n\t\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\t\tprovider: ctx.model?.provider,\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 finalizeForegroundResult(dispatchResult, params.subagent_type, task.id, resumeId);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\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\tconst task = taskStore.create(summary, { subagentMode: params.subagent_type });\n\n\t\t\t// Background agents: dispatch detached and return a handle immediately so\n\t\t\t// the parent keeps reasoning. The parent polls via the TaskOutput tool.\n\t\t\tif (def.background) {\n\t\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t\tconst dispatched = pool.dispatchDetached(params.prompt, {\n\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\tcontext: \"\",\n\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t});\n\t\t\t\tconst poolTaskId = dispatched.task_id;\n\t\t\t\tif (poolTaskId) trackBackgroundTask(pool, poolTaskId, task.id);\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: `Background subagent (${params.subagent_type}) started with task_id \"${poolTaskId}\". It runs without blocking you. Call the TaskOutput tool with this task_id to check its status and collect the final answer.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tsubagent_type: params.subagent_type,\n\t\t\t\t\t\tok: true,\n\t\t\t\t\t\ttaskId: task.id,\n\t\t\t\t\t\tpoolTaskId,\n\t\t\t\t\t\tbackground: true,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\ttry {\n\t\t\t\tconst dispatchResult = await pool.dispatch(params.prompt, {\n\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\tcontext: \"\",\n\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t});\n\t\t\t\treturn finalizeForegroundResult(dispatchResult, params.subagent_type, task.id, dispatchResult.task_id);\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\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\tconst preview = summarize(args.description ?? args.prompt ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"Task \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${type}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n\n/** Extract the final answer from a finished dispatch, updating the task panel. */\nfunction finalizeForegroundResult(\n\tdispatchResult: TaskResult,\n\tsubagentType: string,\n\ttaskStoreId: number,\n\tresumeHandle: string | undefined,\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\tif (!result || !result.ok) {\n\t\t// Signal failure by throwing: the agent loop derives a tool's error state\n\t\t// from a thrown error, not from a returned flag.\n\t\ttaskStore.update(taskStoreId, { status: \"failed\", usage });\n\t\tconst reason = result?.error ?? (result?.status ? `subagent ${result.status}` : \"unknown error\");\n\t\tthrow new Error(`Subagent (${subagentType}) failed: ${reason}`);\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.\n\ttaskStore.update(taskStoreId, { status: \"done\", usage });\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\treturn {\n\t\tcontent: [{ type: \"text\", text: answer }],\n\t\tdetails: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },\n\t};\n}\n\n/**\n * Keep the task panel in sync for a detached background subagent: when the pool\n * reports the task finished, update the stored task's status and detach.\n */\nfunction trackBackgroundTask(pool: SubagentPool, poolTaskId: string, taskStoreId: number): void {\n\tfunction finish(status: \"done\" | \"failed\"): void {\n\t\ttaskStore.update(taskStoreId, { status });\n\t\tpool.off(\"task_done\", onDone);\n\t\tpool.off(\"task_failed\", onFail);\n\t\tpool.off(\"task_stalled\", onFail);\n\t\tpool.off(\"task_timeout\", onFail);\n\t}\n\tfunction onDone(data: { task_id?: string }): void {\n\t\tif (data?.task_id === poolTaskId) finish(\"done\");\n\t}\n\tfunction onFail(data: { task_id?: string }): void {\n\t\tif (data?.task_id === poolTaskId) finish(\"failed\");\n\t}\n\tpool.on(\"task_done\", onDone);\n\tpool.on(\"task_failed\", onFail);\n\tpool.on(\"task_stalled\", onFail);\n\tpool.on(\"task_timeout\", onFail);\n}\n\nconst taskOutputParams = Type.Object({\n\ttask_id: Type.String({\n\t\tdescription: \"The task_id of a background (or previously dispatched) subagent, as returned by the Task tool.\",\n\t}),\n});\n\ntype TaskOutputParams = Static<typeof taskOutputParams>;\n\n/**\n * TaskOutput tool: poll a background subagent and collect its final answer.\n * Returns the current status while running, or the subagent's final answer once\n * complete. Registered alongside the Task tool when subagents are enabled.\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 the status of a background subagent and collect its final answer once it finishes.\",\n\t\t\t\"Pass the task_id returned by a background Task call. While the subagent runs this reports its status; once complete it returns only the subagent's final answer.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"check status / collect the result of a background subagent\",\n\t\tparameters: taskOutputParams,\n\n\t\tasync execute(_toolCallId, params: TaskOutputParams, _signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\t\t\tconst status = pool.get_status(params.task_id);\n\t\t\tif (status === \"running\" || status === \"queued\") {\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: `Subagent task \"${params.task_id}\" is ${status}. Call TaskOutput again later to collect its result.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: params.task_id, status, ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = pool.collect(params.task_id);\n\t\t\tif (!result) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No result available for task \"${params.task_id}\" (status: ${status}). It may not exist or its result was already collected.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!result.ok) {\n\t\t\t\tconst reason = result.error ?? (result.status ? `subagent ${result.status}` : status);\n\t\t\t\tthrow new Error(`Background subagent \"${params.task_id}\" failed: ${reason}`);\n\t\t\t}\n\t\t\tconst resultData = result.result_data as SubagentResultFile | undefined;\n\t\t\tconst answer = resultData?.summary || \"(subagent returned no output)\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: answer }],\n\t\t\t\tdetails: { task_id: params.task_id, status: result.status ?? \"complete\", ok: true },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst text = theme.fg(\"toolTitle\", theme.bold(\"TaskOutput \")) + theme.fg(\"dim\", String(args.task_id ?? \"\"));\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
|
|
@@ -1,22 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Task tool: delegate a focused task to a specialized subagent.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Mirrors the Claude Code `Task` tool. The parent agent decides *when* to
|
|
5
|
+
* delegate based on each agent's `description` (there is no deterministic gate)
|
|
6
|
+
* and selects *which* agent via `subagent_type`. The chosen agent runs in a
|
|
7
|
+
* fresh, isolated child process (SubagentPool) and only its final answer is
|
|
8
|
+
* returned to the parent.
|
|
9
|
+
*
|
|
10
|
+
* It is an optional, opt-in tool (enabled via --subagent or the
|
|
7
11
|
* `enableSubagent` setting); see buildSessionOptions in main.ts.
|
|
8
12
|
*/
|
|
9
|
-
|
|
13
|
+
import { Text } from "@kolisachint/hoocode-tui";
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import { loadAgentRegistry } from "../agent-registry.js";
|
|
16
|
+
import { DispatchEvaluator } from "../dispatch-evaluator.js";
|
|
17
|
+
import { defineTool } from "../extensions/types.js";
|
|
18
|
+
import { getSubagentPool } from "../subagent-pool-instance.js";
|
|
19
|
+
import { taskStore } from "../task-store.js";
|
|
20
|
+
/**
|
|
21
|
+
* Condense a (possibly multi-line, bulleted) agent description into a single
|
|
22
|
+
* useful one-liner for the agent picker list.
|
|
23
|
+
*
|
|
24
|
+
* Built-in agent descriptions open with a boilerplate header ("Use this
|
|
25
|
+
* subagent ONLY when:") followed by "when to use" bullets and a "DO NOT use"
|
|
26
|
+
* section. Taking the first line alone yields that identical header for every
|
|
27
|
+
* agent, so instead surface the first meaningful bullets (or the first prose
|
|
28
|
+
* line) from the positive "when to use" region.
|
|
29
|
+
*/
|
|
30
|
+
export function summarizeAgentDescription(description) {
|
|
31
|
+
const lines = description
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((line) => line.trim())
|
|
34
|
+
.filter((line) => line.length > 0);
|
|
35
|
+
if (lines.length === 0)
|
|
36
|
+
return "";
|
|
37
|
+
// Keep only the positive region: everything before a "DO NOT use" section.
|
|
38
|
+
const stop = lines.findIndex((line) => /^(do\s*not|don'?t|avoid)\b/i.test(line));
|
|
39
|
+
const region = stop === -1 ? lines : lines.slice(0, stop);
|
|
40
|
+
// Drop a leading header line (e.g. "Use this subagent ONLY when:").
|
|
41
|
+
const body = region.length > 1 && region[0].endsWith(":") ? region.slice(1) : region;
|
|
42
|
+
const stripBullet = (line) => line.replace(/^[-*\u2022]\s+/, "").trim();
|
|
43
|
+
const bullets = body
|
|
44
|
+
.filter((line) => /^[-*\u2022]\s+/.test(line))
|
|
45
|
+
.map(stripBullet)
|
|
46
|
+
.filter((line) => line.length > 0);
|
|
47
|
+
const summary = bullets.length > 0 ? bullets.slice(0, 3).join("; ") : (body[0] ?? lines[0] ?? "").replace(/:$/, "");
|
|
48
|
+
const MAX = 200;
|
|
49
|
+
return summary.length > MAX ? `${summary.slice(0, MAX - 1).trimEnd()}\u2026` : summary;
|
|
50
|
+
}
|
|
51
|
+
/** Render the available agents as a "- name: description" list for prompts. */
|
|
52
|
+
function describeAvailableAgents(cwd) {
|
|
53
|
+
const agents = loadAgentRegistry({ cwd }).list();
|
|
54
|
+
if (agents.length === 0)
|
|
55
|
+
return "(no agents available)";
|
|
56
|
+
return agents.map((a) => `- ${a.name}: ${summarizeAgentDescription(a.description)}`).join("\n");
|
|
57
|
+
}
|
|
58
|
+
/** System prompt appendix for the main session when the Task tool is enabled.
|
|
10
59
|
* Instructs the parent agent on when and how to delegate effectively. */
|
|
11
|
-
export
|
|
60
|
+
export function buildTaskMainPrompt(cwd = process.cwd()) {
|
|
61
|
+
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.
|
|
12
62
|
|
|
13
|
-
Available
|
|
14
|
-
|
|
15
|
-
- edit: make a focused code change (read, edit, write, grep, find, ls, bash).
|
|
16
|
-
- test: run tests and report (read, bash, grep, find, ls).
|
|
17
|
-
- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).
|
|
18
|
-
- review: read-only code review (read, grep, find, ls, bash).
|
|
19
|
-
- doc: write documentation, README, or comments (read, write, edit, grep, find, ls, bash).
|
|
63
|
+
Available agents (choose one via \`subagent_type\`):
|
|
64
|
+
${describeAvailableAgents(cwd)}
|
|
20
65
|
|
|
21
66
|
When to delegate:
|
|
22
67
|
1. The work is self-contained and you only need the final result, not intermediate steps.
|
|
@@ -25,49 +70,31 @@ When to delegate:
|
|
|
25
70
|
4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.
|
|
26
71
|
|
|
27
72
|
Guidelines:
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
import { Text } from "@kolisachint/hoocode-tui";
|
|
40
|
-
import { Type } from "typebox";
|
|
41
|
-
import { DispatchEvaluator } from "../dispatch-evaluator.js";
|
|
42
|
-
import { defineTool } from "../extensions/types.js";
|
|
43
|
-
import { getSubagentPool } from "../subagent-pool-instance.js";
|
|
44
|
-
import { taskStore } from "../task-store.js";
|
|
45
|
-
const subagentParams = Type.Object({
|
|
46
|
-
task: Type.String({
|
|
47
|
-
description: "The task to delegate. Make it specific and self-contained; the subagent cannot see this conversation.",
|
|
48
|
-
}),
|
|
49
|
-
context: Type.String({
|
|
50
|
-
description: 'Context distilled from the conversation the subagent needs (files, constraints, prior findings). Pass "" if none.',
|
|
73
|
+
- Choose the agent whose description best matches the task.
|
|
74
|
+
- Make every task specific and self-contained. The subagent cannot see this conversation; pass all necessary context (files, constraints, prior findings) in \`prompt\`.
|
|
75
|
+
- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.
|
|
76
|
+
- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.
|
|
77
|
+
- Default to handling small, quick, or single-file work inline; delegate only self-contained units.
|
|
78
|
+
- Some agents are configured to run in the background (non-blocking). For those, Task returns immediately with a task_id; use the **TaskOutput** tool with that task_id to check status and collect the final answer.
|
|
79
|
+
- 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.`;
|
|
80
|
+
}
|
|
81
|
+
const taskParams = Type.Object({
|
|
82
|
+
description: Type.String({
|
|
83
|
+
description: "A short (3-5 word) description of the task, shown in the task panel.",
|
|
51
84
|
}),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Type.Literal("edit"),
|
|
55
|
-
Type.Literal("test"),
|
|
56
|
-
Type.Literal("fix"),
|
|
57
|
-
Type.Literal("review"),
|
|
58
|
-
Type.Literal("doc"),
|
|
59
|
-
], {
|
|
60
|
-
description: "explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review. doc: write documentation.",
|
|
85
|
+
prompt: Type.String({
|
|
86
|
+
description: "The full, self-contained task for the subagent. It cannot see this conversation, so include all needed context, files, and constraints.",
|
|
61
87
|
}),
|
|
62
|
-
|
|
63
|
-
description: "
|
|
64
|
-
default: false,
|
|
88
|
+
subagent_type: Type.String({
|
|
89
|
+
description: "The name of the specialized agent to delegate to. Must be one of the available agents.",
|
|
65
90
|
}),
|
|
91
|
+
resume_task_id: Type.Optional(Type.String({
|
|
92
|
+
description: "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.",
|
|
93
|
+
})),
|
|
66
94
|
});
|
|
67
95
|
/**
|
|
68
|
-
* A short, human-readable task name for the task panel: the first line
|
|
69
|
-
*
|
|
70
|
-
* guards against a single very long word.
|
|
96
|
+
* A short, human-readable task name for the task panel: the first line limited
|
|
97
|
+
* to ~8 words so it stays glanceable. A character cap guards a single long word.
|
|
71
98
|
*/
|
|
72
99
|
function summarize(task) {
|
|
73
100
|
const firstLine = (task.trim().split("\n")[0] ?? "").trim();
|
|
@@ -79,83 +106,104 @@ function summarize(task) {
|
|
|
79
106
|
name = `${name.slice(0, 59)}…`;
|
|
80
107
|
return name;
|
|
81
108
|
}
|
|
82
|
-
/** Quick check:
|
|
109
|
+
/** Quick advisory check: would the dispatch evaluator delegate this task?
|
|
110
|
+
* The evaluator is non-blocking; this is exposed for diagnostics/tools only. */
|
|
83
111
|
export function isSubagentRecommended(task) {
|
|
84
|
-
|
|
85
|
-
return evaluator.evaluate(task).should_delegate;
|
|
112
|
+
return new DispatchEvaluator().evaluate(task).should_delegate;
|
|
86
113
|
}
|
|
87
|
-
/** Create the
|
|
88
|
-
export function
|
|
114
|
+
/** Create the Task tool definition. Registered as a customTool when enabled. */
|
|
115
|
+
export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
116
|
+
const agentList = describeAvailableAgents(cwd);
|
|
89
117
|
return defineTool({
|
|
90
|
-
name: "
|
|
91
|
-
label: "
|
|
118
|
+
name: "Task",
|
|
119
|
+
label: "Task",
|
|
92
120
|
description: [
|
|
93
|
-
"Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).",
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
"(
|
|
99
|
-
"(
|
|
100
|
-
"
|
|
121
|
+
"Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation).",
|
|
122
|
+
"Select the agent via `subagent_type`; pass everything it needs via `prompt`. The subagent returns only its final answer.",
|
|
123
|
+
"Available agents:",
|
|
124
|
+
agentList,
|
|
125
|
+
"WHEN TO USE: (1) self-contained work where you only need the final result;",
|
|
126
|
+
"(2) parallel investigation/edits without losing your reasoning chain;",
|
|
127
|
+
"(3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs);",
|
|
128
|
+
"(4) a long command or test suite you want to run without blocking your reasoning.",
|
|
129
|
+
"Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.",
|
|
101
130
|
"Prefer handling small, quick, or single-file tasks yourself; delegate only self-contained units of work.",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
parameters: subagentParams,
|
|
131
|
+
].join("\n"),
|
|
132
|
+
promptSnippet: "delegate a self-contained task to a specialized subagent (choose via subagent_type)",
|
|
133
|
+
parameters: taskParams,
|
|
106
134
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
107
|
-
const
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
const pool = getSubagentPool(ctx.cwd);
|
|
136
|
+
// Resume path: continue a previously dispatched subagent with a follow-up
|
|
137
|
+
// prompt, reusing its persisted session (full prior transcript).
|
|
138
|
+
const resumeId = params.resume_task_id?.trim();
|
|
139
|
+
if (resumeId) {
|
|
140
|
+
const summary = params.description?.trim() || summarize(params.prompt);
|
|
141
|
+
const task = taskStore.create(summary, { subagentMode: params.subagent_type });
|
|
142
|
+
taskStore.update(task.id, { status: "in_progress" });
|
|
143
|
+
try {
|
|
144
|
+
const dispatchResult = await pool.resume(resumeId, params.prompt, {
|
|
145
|
+
model: ctx.model?.id,
|
|
146
|
+
provider: ctx.model?.provider,
|
|
147
|
+
});
|
|
148
|
+
// The session lives under the original task id; keep it as the resume handle.
|
|
149
|
+
return finalizeForegroundResult(dispatchResult, params.subagent_type, task.id, resumeId);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
taskStore.update(task.id, { status: "failed" });
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// The model has already decided to delegate and which agent to use; honor
|
|
157
|
+
// it. Validate the requested agent against the registry (no routing gate).
|
|
158
|
+
const registry = loadAgentRegistry({ cwd: ctx.cwd });
|
|
159
|
+
const def = registry.get(params.subagent_type);
|
|
160
|
+
if (!def) {
|
|
161
|
+
const available = registry
|
|
162
|
+
.list()
|
|
163
|
+
.map((a) => a.name)
|
|
164
|
+
.join(", ");
|
|
165
|
+
throw new Error(`Unknown subagent_type: "${params.subagent_type}". Available agents: ${available || "(none)"}.`);
|
|
166
|
+
}
|
|
167
|
+
const summary = params.description?.trim() || summarize(params.prompt);
|
|
168
|
+
const task = taskStore.create(summary, { subagentMode: params.subagent_type });
|
|
169
|
+
// Background agents: dispatch detached and return a handle immediately so
|
|
170
|
+
// the parent keeps reasoning. The parent polls via the TaskOutput tool.
|
|
171
|
+
if (def.background) {
|
|
172
|
+
taskStore.update(task.id, { status: "in_progress" });
|
|
173
|
+
const dispatched = pool.dispatchDetached(params.prompt, {
|
|
174
|
+
forceAgent: params.subagent_type,
|
|
175
|
+
context: "",
|
|
176
|
+
model: ctx.model?.id,
|
|
177
|
+
provider: ctx.model?.provider,
|
|
178
|
+
});
|
|
179
|
+
const poolTaskId = dispatched.task_id;
|
|
180
|
+
if (poolTaskId)
|
|
181
|
+
trackBackgroundTask(pool, poolTaskId, task.id);
|
|
113
182
|
return {
|
|
114
183
|
content: [
|
|
115
184
|
{
|
|
116
185
|
type: "text",
|
|
117
|
-
text: `
|
|
186
|
+
text: `Background subagent (${params.subagent_type}) started with task_id "${poolTaskId}". It runs without blocking you. Call the TaskOutput tool with this task_id to check its status and collect the final answer.`,
|
|
118
187
|
},
|
|
119
188
|
],
|
|
120
|
-
details: {
|
|
189
|
+
details: {
|
|
190
|
+
subagent_type: params.subagent_type,
|
|
191
|
+
ok: true,
|
|
192
|
+
taskId: task.id,
|
|
193
|
+
poolTaskId,
|
|
194
|
+
background: true,
|
|
195
|
+
},
|
|
121
196
|
};
|
|
122
197
|
}
|
|
123
|
-
const mode = params.force ? forcedMode : (analysis.agent_type ?? "explore");
|
|
124
|
-
const summary = summarize(params.task);
|
|
125
|
-
const task = taskStore.create(summary, { subagentMode: mode });
|
|
126
198
|
taskStore.update(task.id, { status: "in_progress" });
|
|
127
199
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
context: params.context,
|
|
200
|
+
const dispatchResult = await pool.dispatch(params.prompt, {
|
|
201
|
+
forceAgent: params.subagent_type,
|
|
202
|
+
context: "",
|
|
132
203
|
model: ctx.model?.id,
|
|
133
204
|
provider: ctx.model?.provider,
|
|
134
205
|
});
|
|
135
|
-
|
|
136
|
-
const resultData = result?.result_data;
|
|
137
|
-
const usage = resultData?.usage;
|
|
138
|
-
if (!result || !result.ok) {
|
|
139
|
-
// Signal failure by throwing: the agent loop derives a tool's error
|
|
140
|
-
// state from a thrown error, not from a returned flag.
|
|
141
|
-
taskStore.update(task.id, { status: "failed", usage });
|
|
142
|
-
const reason = result?.error ??
|
|
143
|
-
(result?.budget_exceeded
|
|
144
|
-
? "token budget exceeded before producing a result"
|
|
145
|
-
: result?.status
|
|
146
|
-
? `subagent ${result.status}`
|
|
147
|
-
: "unknown error");
|
|
148
|
-
throw new Error(`Subagent (${mode}) failed: ${reason}`);
|
|
149
|
-
}
|
|
150
|
-
// Leave the task in the store with its final status. It stays visible in
|
|
151
|
-
// the task panel until the next user message arrives (taskStore.reset is
|
|
152
|
-
// called when the user starts the next turn).
|
|
153
|
-
taskStore.update(task.id, { status: "done", usage });
|
|
154
|
-
const answer = resultData?.summary || "(subagent returned no output)";
|
|
155
|
-
return {
|
|
156
|
-
content: [{ type: "text", text: answer }],
|
|
157
|
-
details: { mode, ok: true, taskId: task.id },
|
|
158
|
-
};
|
|
206
|
+
return finalizeForegroundResult(dispatchResult, params.subagent_type, task.id, dispatchResult.task_id);
|
|
159
207
|
}
|
|
160
208
|
catch (error) {
|
|
161
209
|
taskStore.update(task.id, { status: "failed" });
|
|
@@ -163,13 +211,118 @@ export function createSubagentToolDefinition() {
|
|
|
163
211
|
}
|
|
164
212
|
},
|
|
165
213
|
renderCall(args, theme) {
|
|
166
|
-
const
|
|
167
|
-
const preview = summarize(args.
|
|
168
|
-
const text = theme.fg("toolTitle", theme.bold("
|
|
169
|
-
theme.fg("accent", `[${
|
|
214
|
+
const type = args.subagent_type ?? "agent";
|
|
215
|
+
const preview = summarize(args.description ?? args.prompt ?? "");
|
|
216
|
+
const text = theme.fg("toolTitle", theme.bold("Task ")) +
|
|
217
|
+
theme.fg("accent", `[${type}]`) +
|
|
170
218
|
theme.fg("dim", ` ${preview}`);
|
|
171
219
|
return new Text(text, 0, 0);
|
|
172
220
|
},
|
|
173
221
|
});
|
|
174
222
|
}
|
|
223
|
+
/** Extract the final answer from a finished dispatch, updating the task panel. */
|
|
224
|
+
function finalizeForegroundResult(dispatchResult, subagentType, taskStoreId, resumeHandle) {
|
|
225
|
+
const result = dispatchResult.result;
|
|
226
|
+
const resultData = result?.result_data;
|
|
227
|
+
const usage = resultData?.usage;
|
|
228
|
+
if (!result || !result.ok) {
|
|
229
|
+
// Signal failure by throwing: the agent loop derives a tool's error state
|
|
230
|
+
// from a thrown error, not from a returned flag.
|
|
231
|
+
taskStore.update(taskStoreId, { status: "failed", usage });
|
|
232
|
+
const reason = result?.error ?? (result?.status ? `subagent ${result.status}` : "unknown error");
|
|
233
|
+
throw new Error(`Subagent (${subagentType}) failed: ${reason}`);
|
|
234
|
+
}
|
|
235
|
+
// Leave the task in the store with its final status; it stays visible in the
|
|
236
|
+
// task panel until the next user message arrives.
|
|
237
|
+
taskStore.update(taskStoreId, { status: "done", usage });
|
|
238
|
+
let answer = resultData?.summary || "(subagent returned no output)";
|
|
239
|
+
// Partial results are resumable; surface the handle so the parent can continue.
|
|
240
|
+
if (result.status === "partial" && resumeHandle) {
|
|
241
|
+
answer += `\n\n[Partial result. To continue this subagent, call Task again with resume_task_id="${resumeHandle}".]`;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: answer }],
|
|
245
|
+
details: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Keep the task panel in sync for a detached background subagent: when the pool
|
|
250
|
+
* reports the task finished, update the stored task's status and detach.
|
|
251
|
+
*/
|
|
252
|
+
function trackBackgroundTask(pool, poolTaskId, taskStoreId) {
|
|
253
|
+
function finish(status) {
|
|
254
|
+
taskStore.update(taskStoreId, { status });
|
|
255
|
+
pool.off("task_done", onDone);
|
|
256
|
+
pool.off("task_failed", onFail);
|
|
257
|
+
pool.off("task_stalled", onFail);
|
|
258
|
+
pool.off("task_timeout", onFail);
|
|
259
|
+
}
|
|
260
|
+
function onDone(data) {
|
|
261
|
+
if (data?.task_id === poolTaskId)
|
|
262
|
+
finish("done");
|
|
263
|
+
}
|
|
264
|
+
function onFail(data) {
|
|
265
|
+
if (data?.task_id === poolTaskId)
|
|
266
|
+
finish("failed");
|
|
267
|
+
}
|
|
268
|
+
pool.on("task_done", onDone);
|
|
269
|
+
pool.on("task_failed", onFail);
|
|
270
|
+
pool.on("task_stalled", onFail);
|
|
271
|
+
pool.on("task_timeout", onFail);
|
|
272
|
+
}
|
|
273
|
+
const taskOutputParams = Type.Object({
|
|
274
|
+
task_id: Type.String({
|
|
275
|
+
description: "The task_id of a background (or previously dispatched) subagent, as returned by the Task tool.",
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
/**
|
|
279
|
+
* TaskOutput tool: poll a background subagent and collect its final answer.
|
|
280
|
+
* Returns the current status while running, or the subagent's final answer once
|
|
281
|
+
* complete. Registered alongside the Task tool when subagents are enabled.
|
|
282
|
+
*/
|
|
283
|
+
export function createTaskOutputToolDefinition() {
|
|
284
|
+
return defineTool({
|
|
285
|
+
name: "TaskOutput",
|
|
286
|
+
label: "TaskOutput",
|
|
287
|
+
description: [
|
|
288
|
+
"Check the status of a background subagent and collect its final answer once it finishes.",
|
|
289
|
+
"Pass the task_id returned by a background Task call. While the subagent runs this reports its status; once complete it returns only the subagent's final answer.",
|
|
290
|
+
].join("\n"),
|
|
291
|
+
promptSnippet: "check status / collect the result of a background subagent",
|
|
292
|
+
parameters: taskOutputParams,
|
|
293
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
294
|
+
const pool = getSubagentPool(ctx.cwd);
|
|
295
|
+
const status = pool.get_status(params.task_id);
|
|
296
|
+
if (status === "running" || status === "queued") {
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: `Subagent task "${params.task_id}" is ${status}. Call TaskOutput again later to collect its result.`,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
details: { task_id: params.task_id, status, ok: true },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const result = pool.collect(params.task_id);
|
|
308
|
+
if (!result) {
|
|
309
|
+
throw new Error(`No result available for task "${params.task_id}" (status: ${status}). It may not exist or its result was already collected.`);
|
|
310
|
+
}
|
|
311
|
+
if (!result.ok) {
|
|
312
|
+
const reason = result.error ?? (result.status ? `subagent ${result.status}` : status);
|
|
313
|
+
throw new Error(`Background subagent "${params.task_id}" failed: ${reason}`);
|
|
314
|
+
}
|
|
315
|
+
const resultData = result.result_data;
|
|
316
|
+
const answer = resultData?.summary || "(subagent returned no output)";
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: answer }],
|
|
319
|
+
details: { task_id: params.task_id, status: result.status ?? "complete", ok: true },
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
renderCall(args, theme) {
|
|
323
|
+
const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("dim", String(args.task_id ?? ""));
|
|
324
|
+
return new Text(text, 0, 0);
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
175
328
|
//# sourceMappingURL=subagent.js.map
|