@kolisachint/hoocode-agent 0.4.78 → 0.4.80

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-frontmatter.d.ts +5 -7
  7. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  8. package/dist/core/agent-frontmatter.js +2 -4
  9. package/dist/core/agent-frontmatter.js.map +1 -1
  10. package/dist/core/agent-registry.d.ts +12 -1
  11. package/dist/core/agent-registry.d.ts.map +1 -1
  12. package/dist/core/agent-registry.js +38 -3
  13. package/dist/core/agent-registry.js.map +1 -1
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +7 -3
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/dispatch-evaluator.d.ts +1 -1
  18. package/dist/core/dispatch-evaluator.d.ts.map +1 -1
  19. package/dist/core/dispatch-evaluator.js +1 -1
  20. package/dist/core/dispatch-evaluator.js.map +1 -1
  21. package/dist/core/messages.d.ts.map +1 -1
  22. package/dist/core/messages.js +24 -6
  23. package/dist/core/messages.js.map +1 -1
  24. package/dist/core/model-categories.d.ts +14 -8
  25. package/dist/core/model-categories.d.ts.map +1 -1
  26. package/dist/core/model-categories.js +13 -24
  27. package/dist/core/model-categories.js.map +1 -1
  28. package/dist/core/provider-health.d.ts +1 -1
  29. package/dist/core/provider-health.d.ts.map +1 -1
  30. package/dist/core/provider-health.js +1 -1
  31. package/dist/core/provider-health.js.map +1 -1
  32. package/dist/core/settings-defaults.d.ts +1 -0
  33. package/dist/core/settings-defaults.d.ts.map +1 -1
  34. package/dist/core/settings-defaults.js +1 -0
  35. package/dist/core/settings-defaults.js.map +1 -1
  36. package/dist/core/settings-manager.d.ts +7 -2
  37. package/dist/core/settings-manager.d.ts.map +1 -1
  38. package/dist/core/settings-manager.js +9 -0
  39. package/dist/core/settings-manager.js.map +1 -1
  40. package/dist/core/subagent-depth.d.ts +16 -0
  41. package/dist/core/subagent-depth.d.ts.map +1 -1
  42. package/dist/core/subagent-depth.js +22 -0
  43. package/dist/core/subagent-depth.js.map +1 -1
  44. package/dist/core/subagent-inbox.d.ts +92 -0
  45. package/dist/core/subagent-inbox.d.ts.map +1 -0
  46. package/dist/core/subagent-inbox.js +245 -0
  47. package/dist/core/subagent-inbox.js.map +1 -0
  48. package/dist/core/subagent-pool.d.ts +14 -7
  49. package/dist/core/subagent-pool.d.ts.map +1 -1
  50. package/dist/core/subagent-pool.js +35 -71
  51. package/dist/core/subagent-pool.js.map +1 -1
  52. package/dist/core/system-prompt.d.ts +1 -1
  53. package/dist/core/system-prompt.d.ts.map +1 -1
  54. package/dist/core/system-prompt.js +8 -8
  55. package/dist/core/system-prompt.js.map +1 -1
  56. package/dist/core/task-store.d.ts +6 -5
  57. package/dist/core/task-store.d.ts.map +1 -1
  58. package/dist/core/task-store.js +4 -3
  59. package/dist/core/task-store.js.map +1 -1
  60. package/dist/core/tools/index.d.ts +1 -1
  61. package/dist/core/tools/index.d.ts.map +1 -1
  62. package/dist/core/tools/index.js +1 -1
  63. package/dist/core/tools/index.js.map +1 -1
  64. package/dist/core/tools/subagent.d.ts +23 -20
  65. package/dist/core/tools/subagent.d.ts.map +1 -1
  66. package/dist/core/tools/subagent.js +357 -163
  67. package/dist/core/tools/subagent.js.map +1 -1
  68. package/dist/core/tools/todo.d.ts.map +1 -1
  69. package/dist/core/tools/todo.js +10 -6
  70. package/dist/core/tools/todo.js.map +1 -1
  71. package/dist/core/warm-subagent-pool-instance.d.ts +23 -0
  72. package/dist/core/warm-subagent-pool-instance.d.ts.map +1 -0
  73. package/dist/core/warm-subagent-pool-instance.js +53 -0
  74. package/dist/core/warm-subagent-pool-instance.js.map +1 -0
  75. package/dist/core/warm-subagent-pool.d.ts +135 -0
  76. package/dist/core/warm-subagent-pool.d.ts.map +1 -0
  77. package/dist/core/warm-subagent-pool.js +383 -0
  78. package/dist/core/warm-subagent-pool.js.map +1 -0
  79. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  80. package/dist/extensions/core/hoo-core.js +7 -0
  81. package/dist/extensions/core/hoo-core.js.map +1 -1
  82. package/dist/index.d.ts +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +1 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/init-templates.generated.d.ts.map +1 -1
  87. package/dist/init-templates.generated.js +2 -2
  88. package/dist/init-templates.generated.js.map +1 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +23 -8
  91. package/dist/main.js.map +1 -1
  92. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  93. package/dist/modes/interactive/components/task-panel.js +26 -3
  94. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  95. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  96. package/dist/modes/interactive/interactive-mode.js +1 -1
  97. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  98. package/dist/modes/rpc/rpc-client.d.ts +16 -0
  99. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  100. package/dist/modes/rpc/rpc-client.js +74 -9
  101. package/dist/modes/rpc/rpc-client.js.map +1 -1
  102. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  103. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  104. package/examples/extensions/sandbox/package.json +1 -1
  105. package/examples/extensions/with-deps/package.json +1 -1
  106. package/package.json +4 -4
  107. package/templates/agents/general-purpose.md +1 -1
  108. package/templates/agents/plan.md +1 -1
@@ -12,62 +12,29 @@
12
12
  */
13
13
  import { Text } from "@kolisachint/hoocode-tui";
14
14
  import { Type } from "typebox";
15
- import { getAgentDir } from "../../config.js";
16
- import { EXECUTE_TASK_TOOL_NAME, MODEL_INHERIT } from "../agent-frontmatter.js";
15
+ import { TASK_TOOL_NAME } from "../agent-frontmatter.js";
17
16
  import { loadAgentRegistry } from "../agent-registry.js";
18
17
  import { defineTool } from "../extensions/types.js";
19
- import { resolveModelReference } from "../model-categories.js";
20
18
  import { getProviderExhaustion } from "../provider-health.js";
21
19
  import { SessionManager } from "../session-manager.js";
22
- import { SettingsManager } from "../settings-manager.js";
23
20
  import { delegateAllowList, isDelegateAllowed } from "../subagent-depth.js";
21
+ import { subagentInbox } from "../subagent-inbox.js";
24
22
  import { getSubagentPool } from "../subagent-pool-instance.js";
25
23
  import { taskStore } from "../task-store.js";
26
- /**
27
- * Condense a (possibly multi-line, bulleted) agent description into a single
28
- * useful one-liner for the agent picker list.
29
- *
30
- * Built-in agent descriptions open with a boilerplate header ("Use this
31
- * subagent ONLY when:") followed by "when to use" bullets and a "DO NOT use"
32
- * section. Taking the first line alone yields that identical header for every
33
- * agent, so instead surface the first meaningful bullets (or the first prose
34
- * line) from the positive "when to use" region.
35
- */
36
- export function summarizeAgentDescription(description) {
37
- const lines = description
38
- .split("\n")
39
- .map((line) => line.trim())
40
- .filter((line) => line.length > 0);
41
- if (lines.length === 0)
42
- return "";
43
- // Keep only the positive region: everything before a "DO NOT use" section.
44
- const stop = lines.findIndex((line) => /^(do\s*not|don'?t|avoid)\b/i.test(line));
45
- const region = stop === -1 ? lines : lines.slice(0, stop);
46
- // Drop a leading header line (e.g. "Use this subagent ONLY when:").
47
- const body = region.length > 1 && region[0].endsWith(":") ? region.slice(1) : region;
48
- const stripBullet = (line) => line.replace(/^[-*\u2022]\s+/, "").trim();
49
- const bullets = body
50
- .filter((line) => /^[-*\u2022]\s+/.test(line))
51
- .map(stripBullet)
52
- .filter((line) => line.length > 0);
53
- const summary = bullets.length > 0 ? bullets.slice(0, 3).join("; ") : (body[0] ?? lines[0] ?? "").replace(/:$/, "");
54
- const MAX = 200;
55
- return summary.length > MAX ? `${summary.slice(0, MAX - 1).trimEnd()}\u2026` : summary;
56
- }
57
- /** Render the available agents as a "- name: description" list for prompts. */
58
- function describeAvailableAgents(cwd) {
59
- const agents = loadAgentRegistry({ cwd }).list();
60
- if (agents.length === 0)
61
- return "(no agents available)";
62
- return agents.map((a) => `- ${a.name}: ${summarizeAgentDescription(a.description)}`).join("\n");
63
- }
64
- /** System prompt appendix for the main session when the ExecuteTask tool is enabled.
65
- * Instructs the parent agent on when and how to delegate effectively. */
66
- export function buildTaskMainPrompt(cwd = process.cwd()) {
67
- return `You have access to the **ExecuteTask** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer.
68
-
69
- Available agents (choose one via \`subagent_type\`):
70
- ${describeAvailableAgents(cwd)}
24
+ import { WarmWorkerError } from "../warm-subagent-pool.js";
25
+ import { getWarmSubagentPool, warmSubagentsEnabled } from "../warm-subagent-pool-instance.js";
26
+ // Re-exported from its home in agent-registry (where formatAgentsForPrompt uses
27
+ // it to render the roster) so existing importers keep working without creating a
28
+ // tools -> registry -> tools cycle.
29
+ export { summarizeAgentDescription } from "../agent-registry.js";
30
+ /** System prompt appendix for the main session when the Task tool is enabled.
31
+ * Instructs the parent agent on when and how to delegate effectively. The
32
+ * available agents themselves are listed once, authoritatively, in the
33
+ * `<available_agents>` block the system prompt emits whenever the Task tool is
34
+ * active (see agent-session `_rebuildSystemPrompt`); this appendix references
35
+ * that list rather than re-rendering the roster and paying for it twice. */
36
+ export function buildTaskMainPrompt() {
37
+ return `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer. Pick an agent by name from the <available_agents> list in this prompt and pass it as \`subagent_type\`.
71
38
 
72
39
  When to delegate:
73
40
  1. The work is self-contained and you only need the final result, not intermediate steps.
@@ -75,11 +42,7 @@ When to delegate:
75
42
  3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).
76
43
  4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.
77
44
 
78
- Model categories:
79
- - \`complexity: "fast\`\` — quick reads, simple lookups, grep/find operations
80
- - \`complexity: "standard\`\` — multi-file edits, moderate reasoning, test runs
81
- - \`complexity: "capable\`\` — deep architecture changes, complex refactors, full PRs
82
- If omitted, the agent's default model is used.
45
+ Model tier (optional \`complexity\`): set \`fast\` for quick reads/lookups, \`standard\` for multi-file edits, \`capable\` for deep architecture work. It maps to a model from \`settings.modelCategories\`. Omit it to use the agent's default; an agent that pins its own model ignores \`complexity\`.
83
46
 
84
47
  Guidelines:
85
48
  - Choose the agent whose description best matches the task.
@@ -87,8 +50,10 @@ Guidelines:
87
50
  - Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.
88
51
  - The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.
89
52
  - Delegate proactively when work is self-contained or parallelizable: multi-step investigation, read-only exploration (use \`explore\`), research before changes (use \`plan\`), drafting a standalone file/section, or running a long command/test suite. Dispatch independent subtasks in the same turn. Handle only trivial single-step edits or tightly interactive back-and-forth inline.
90
- - All ExecuteTask calls run as background tasks (non-blocking). The agent loop continues while the subagent runs. Results arrive as follow-up messages automatically. Use TaskOutput to poll for results while a subagent is running.
91
- - To continue a previous subagent (for example one that returned partial results), call ExecuteTask again with \`resume_task_id\` set to its task_id; it resumes with its full prior transcript and \`prompt\` is your follow-up.`;
53
+ - Some agents run in the background (non-blocking); force it per call with \`background: true\` (or \`background: false\` to wait inline). A background Task does not block your turn and does not return its answer inline: you get a short notification ("explore#1 finished") and the full result is held for you to pull with \`TaskOutput\`. Keep working in the meantime.
54
+ - After dispatching background work, DO NOT stop and wait — that wastes the parallelism and looks stuck. Immediately continue with the next useful thing: read or edit an independent file, draft the parts of your answer that don't depend on the pending result, or dispatch more independent subtasks. Only barrier (with \`TaskOutput(wait: true)\`) when you genuinely cannot proceed without the result. Prefer background dispatch for any self-contained or parallelizable work so your turn never blocks on a subagent.
55
+ - Use **TaskOutput** to manage background subagents: \`TaskOutput(list: true)\` shows every running/finished subagent and what each is doing; \`TaskOutput("explore#1")\` reads a finished subagent's full result (or reports its status if still running); \`TaskOutput(wait: true)\` blocks until a named task — or, with no task_id, ALL outstanding subagents — finish. Dispatch a batch in one turn, then barrier on them with \`TaskOutput(wait: true)\` only once you've exhausted the work you can do without them.
56
+ - 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.`;
92
57
  }
93
58
  const taskParams = Type.Object({
94
59
  description: Type.String({
@@ -100,14 +65,14 @@ const taskParams = Type.Object({
100
65
  subagent_type: Type.String({
101
66
  description: "The name of the specialized agent to delegate to. Must be one of the available agents.",
102
67
  }),
103
- item_id: Type.Optional(Type.Number({
104
- description: "Optional. References a TodoWrite item id to link this dispatch to a plan item. The task panel shows the linkage.",
105
- })),
106
68
  complexity: Type.Optional(Type.Union([Type.Literal("fast"), Type.Literal("standard"), Type.Literal("capable")], {
107
- description: "Model category: fast (quick reads/lookups), standard (multi-file edits), capable (deep architecture). Omit to use the agent's default model.",
69
+ description: "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.",
70
+ })),
71
+ background: Type.Optional(Type.Boolean({
72
+ description: "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.",
108
73
  })),
109
74
  resume_task_id: Type.Optional(Type.String({
110
- description: "Optional. To continue a previous subagent run, pass its task_id (returned by an earlier ExecuteTask or TaskOutput call). The subagent resumes with its full prior transcript and `prompt` is your follow-up instruction.",
75
+ 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.",
111
76
  })),
112
77
  });
113
78
  /**
@@ -124,31 +89,30 @@ function summarize(task) {
124
89
  name = `${name.slice(0, 59)}…`;
125
90
  return name;
126
91
  }
127
- /** Create the ExecuteTask tool definition. Registered as a customTool when enabled. */
128
- export function createExecuteTaskToolDefinition(cwd = process.cwd()) {
129
- const agentList = describeAvailableAgents(cwd);
130
- // All ExecuteTask calls run as background tools. The agent loop runs the
131
- // dispatch detached: the parent keeps reasoning and the subagent's answer is
132
- // injected as a follow-up message when it finishes. TaskOutput can poll for
133
- // results while the subagent runs.
92
+ /** Create the Task tool definition. Registered as a customTool when enabled. */
93
+ export function createTaskToolDefinition(cwd = process.cwd()) {
94
+ // Agents whose definitions opt into background execution. The agent loop reads
95
+ // the tool's `background` flag per call and, for these, runs the dispatch
96
+ // detached: the parent keeps reasoning and the subagent's answer is injected as
97
+ // a follow-up message when it finishes (no polling needed). A per-call
98
+ // `background` argument overrides the agent's default in either direction.
99
+ const backgroundAgents = collectBackgroundAgentNames(cwd);
134
100
  return defineTool({
135
- name: EXECUTE_TASK_TOOL_NAME,
136
- label: EXECUTE_TASK_TOOL_NAME,
137
- background: () => true,
101
+ name: TASK_TOOL_NAME,
102
+ label: TASK_TOOL_NAME,
103
+ background: (toolCall) => {
104
+ const override = toolCall.arguments?.background;
105
+ if (typeof override === "boolean")
106
+ return override;
107
+ return backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? ""));
108
+ },
109
+ // Kept lean: the available agents are listed in the system prompt, and the
110
+ // `complexity`/`background` semantics live in their parameter descriptions —
111
+ // repeating them here would re-spend those tokens on every turn.
138
112
  description: [
139
- "Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation).",
140
- "Select the agent via `subagent_type`; pass everything it needs via `prompt`. The subagent returns only its final answer.",
141
- "Available agents:",
142
- agentList,
143
- "Model categories: fast (quick reads/lookups), standard (multi-file edits), capable (deep architecture). Omit to use agent default.",
144
- "All ExecuteTask calls run as background tasks. The agent loop continues while the subagent runs. Results arrive as follow-up messages.",
145
- "Use TaskOutput to poll for results while a subagent is running.",
146
- "WHEN TO USE: (1) self-contained work where you only need the final result;",
147
- "(2) parallel investigation/edits without losing your reasoning chain;",
148
- "(3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs);",
149
- "(4) a long command or test suite you want to run without blocking your reasoning.",
150
- "Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.",
151
- "Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.",
113
+ "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.",
114
+ "WHEN TO USE: (1) self-contained work where you only need the final result; (2) parallel investigation/edits without losing your reasoning chain; (3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs); (4) a long command or test suite you want to run without blocking your reasoning.",
115
+ "Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about. Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.",
152
116
  ].join("\n"),
153
117
  promptSnippet: "delegate a self-contained task to a specialized subagent (choose via subagent_type)",
154
118
  parameters: taskParams,
@@ -230,37 +194,128 @@ export function createExecuteTaskToolDefinition(cwd = process.cwd()) {
230
194
  agent: params.subagent_type,
231
195
  });
232
196
  registerSubagentDispatch(params.subagent_type);
233
- // Always dispatch and await the subagent's full result here. Background
234
- // agents (def.background) are made non-blocking by the agent loop via this
235
- // tool's `background` flag: the loop runs this execute() detached, answers
236
- // the call with a placeholder, and injects the answer below as a follow-up
237
- // message when it resolves. Foreground agents block the turn as usual.
238
197
  taskStore.update(task.id, { status: "in_progress" });
239
- // Resolve model: complexity category > agent definition default > parent model
240
- let resolvedModel = ctx.model?.id;
241
- if (params.complexity) {
242
- // Model category from ExecuteTask's complexity parameter
243
- const settingsManager = SettingsManager.create(ctx.cwd, getAgentDir());
244
- const globalSettings = settingsManager.getGlobalSettings();
245
- const projectSettings = settingsManager.getProjectSettings();
246
- const settings = { ...globalSettings, ...projectSettings };
247
- const categoryModel = resolveModelReference(params.complexity, settings);
248
- if (categoryModel)
249
- resolvedModel = categoryModel;
250
- }
251
- else if (def.model && def.model !== MODEL_INHERIT) {
252
- // Agent definition's default model (fallback)
253
- resolvedModel = def.model;
254
- }
255
198
  // Fork agents inherit the parent's conversation via a forked session.
256
199
  const forkSessionFile = def.fork
257
200
  ? resolveForkSessionFile(def, ctx.sessionManager?.getSessionFile(), ctx.cwd)
258
201
  : undefined;
202
+ // `complexity` is passed as the model: the pool's spawn() already lets a
203
+ // non-`inherit` agent model win, then resolves a category string (fast/
204
+ // standard/capable) via settings.modelCategories. So a pinned-model agent
205
+ // ignores complexity, and an `inherit` agent picks up the requested tier —
206
+ // no settings lookup needed here.
207
+ const dispatchModel = params.complexity ?? ctx.model?.id;
208
+ // Whether this call runs detached. The agent loop reads the tool's
209
+ // `background` flag (the same predicate) to run execute() detached; we
210
+ // recompute it here to choose the notify-and-pull return shape.
211
+ const isBackground = params.background ?? backgroundAgents.has(params.subagent_type);
212
+ if (isBackground) {
213
+ // Notify-and-pull: register the dispatch in the inbox under a pre-allocated
214
+ // id, await it, retain the body in the inbox, and return a compact
215
+ // notification (not the body). The model pulls the body with TaskOutput.
216
+ const poolTaskId = `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
217
+ const label = subagentInbox.nextLabel(params.subagent_type);
218
+ subagentInbox.observe(pool);
219
+ subagentInbox.start(poolTaskId, label, params.subagent_type);
220
+ // Warm path (opt-in): run on a reused RPC worker, retain the body in the
221
+ // inbox, and return the same notify-and-pull shape as the cold path. An
222
+ // infra failure falls through to the cold dispatch below.
223
+ if (warmSubagentsEnabled() && !forkSessionFile) {
224
+ const warm = getWarmSubagentPool(ctx.cwd);
225
+ if (warm.isPoolable(params.subagent_type)) {
226
+ try {
227
+ const warmResult = await warm.dispatch(params.prompt, {
228
+ agentType: params.subagent_type,
229
+ cwd: ctx.cwd,
230
+ model: dispatchModel,
231
+ provider: ctx.model?.provider,
232
+ }, (activity) => taskStore.patchAgent(params.subagent_type, { activity }));
233
+ const dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);
234
+ subagentInbox.finish(poolTaskId, dispatchResult);
235
+ return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
236
+ taskId: poolTaskId,
237
+ label,
238
+ });
239
+ }
240
+ catch (error) {
241
+ if (!(error instanceof WarmWorkerError))
242
+ throw error;
243
+ console.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);
244
+ }
245
+ }
246
+ }
247
+ try {
248
+ const dispatchResult = await pool.dispatch(params.prompt, {
249
+ forceAgent: params.subagent_type,
250
+ context: "",
251
+ model: dispatchModel,
252
+ provider: ctx.model?.provider,
253
+ sessionFile: forkSessionFile,
254
+ taskId: poolTaskId,
255
+ });
256
+ subagentInbox.finish(poolTaskId, dispatchResult);
257
+ return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
258
+ taskId: poolTaskId,
259
+ label,
260
+ });
261
+ }
262
+ catch (error) {
263
+ const reason = error instanceof Error ? error.message : String(error);
264
+ taskStore.update(task.id, { status: "failed" });
265
+ subagentInbox.fail(poolTaskId, reason);
266
+ // A background dispatch reports failure as a compact notification, not a
267
+ // thrown tool error — the call was already answered by a placeholder.
268
+ return {
269
+ content: [{ type: "text", text: `${label} failed ✗ — ${reason}` }],
270
+ details: {
271
+ subagent_type: params.subagent_type,
272
+ ok: false,
273
+ error: reason,
274
+ taskId: task.id,
275
+ poolTaskId,
276
+ background: true,
277
+ },
278
+ };
279
+ }
280
+ }
281
+ // Warm path (opt-in): for an eligible foreground dispatch, run on a reused
282
+ // RPC worker to skip the cold-boot. Fork agents (forkSessionFile set) and
283
+ // non-poolable types are excluded. Any infra failure falls through to the
284
+ // cold pool below, so enabling this can only change latency, never whether
285
+ // the task can run.
286
+ if (warmSubagentsEnabled() && !forkSessionFile) {
287
+ const warm = getWarmSubagentPool(ctx.cwd);
288
+ if (warm.isPoolable(params.subagent_type)) {
289
+ try {
290
+ const warmResult = await warm.dispatch(params.prompt, {
291
+ agentType: params.subagent_type,
292
+ cwd: ctx.cwd,
293
+ model: dispatchModel,
294
+ provider: ctx.model?.provider,
295
+ },
296
+ // Mirror the cold pool's live progress on the task panel roster so a
297
+ // warm dispatch reads as busy (⋯ grep), not stuck.
298
+ (activity) => taskStore.patchAgent(params.subagent_type, { activity }));
299
+ const dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);
300
+ return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, undefined);
301
+ }
302
+ catch (error) {
303
+ // A genuine infra failure (worker crash/timeout) retries cold; any other
304
+ // error is a real dispatch failure and propagates.
305
+ if (!(error instanceof WarmWorkerError)) {
306
+ taskStore.update(task.id, { status: "failed" });
307
+ throw error;
308
+ }
309
+ console.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);
310
+ }
311
+ }
312
+ }
313
+ // Foreground: block the turn and return the subagent's full answer inline.
259
314
  try {
260
315
  const dispatchResult = await pool.dispatch(params.prompt, {
261
316
  forceAgent: params.subagent_type,
262
317
  context: "",
263
- model: resolvedModel,
318
+ model: dispatchModel,
264
319
  provider: ctx.model?.provider,
265
320
  sessionFile: forkSessionFile,
266
321
  });
@@ -306,6 +361,15 @@ export function resolveForkSessionFile(def, parentSessionPath, cwd) {
306
361
  function registerSubagentDispatch(type) {
307
362
  taskStore.upsertAgent({ id: type, name: type, role: "subagent", kind: "subagent", state: "running" });
308
363
  }
364
+ /** Names of agents configured to run in the background (non-blocking). */
365
+ function collectBackgroundAgentNames(cwd) {
366
+ const names = new Set();
367
+ for (const agent of loadAgentRegistry({ cwd }).list()) {
368
+ if (agent.background)
369
+ names.add(agent.name);
370
+ }
371
+ return names;
372
+ }
309
373
  /**
310
374
  * Merge a child subagent's task subtree into the parent's task store, rooting
311
375
  * each top-level node under the dispatching task (`parentTaskId`). Recurses so a
@@ -328,8 +392,44 @@ function mergeChildTaskTree(nodes, parentTaskId) {
328
392
  }
329
393
  });
330
394
  }
331
- /** Extract the final answer from a finished dispatch, updating the task panel. */
332
- function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resumeHandle) {
395
+ /**
396
+ * Adapt a warm-worker run into the TaskResult shape finalizeDispatchResult
397
+ * consumes, so the warm and cold paths share one finish/render path. A warm run
398
+ * returns its answer inline (no result.json), so we synthesize an equivalent
399
+ * SubagentResult with the answer as the summary and the pulled usage. The warm
400
+ * path is clean (no persisted session), so there is no task_tree to merge and no
401
+ * resume handle.
402
+ */
403
+ function warmResultToTaskResult(warm, agentType, taskStoreId) {
404
+ const resultData = {
405
+ summary: warm.summary || "(subagent returned no output)",
406
+ files_changed: [],
407
+ confidence: warm.ok ? 1 : 0,
408
+ status: warm.status,
409
+ usage: warm.usage,
410
+ };
411
+ const result = {
412
+ task_id: String(taskStoreId),
413
+ ok: warm.ok,
414
+ stdout: "",
415
+ stderr: "",
416
+ exit_code: warm.ok ? 0 : 1,
417
+ status: warm.status,
418
+ error: warm.error,
419
+ result_data: resultData,
420
+ };
421
+ return { handled_inline: false, agent_type: agentType, result };
422
+ }
423
+ /**
424
+ * Update the task panel from a finished dispatch and shape the tool result.
425
+ *
426
+ * Foreground calls return the subagent's full answer inline and signal a hard
427
+ * failure by throwing (the agent loop derives a tool's error state from a thrown
428
+ * error). A background call passes `background`: the body already lives in the
429
+ * inbox, so it returns a compact, self-contained notification (success or
430
+ * failure) and never throws — the call was already answered by a placeholder.
431
+ */
432
+ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resumeHandle, background) {
333
433
  const result = dispatchResult.result;
334
434
  const resultData = result?.result_data;
335
435
  const usage = resultData?.usage;
@@ -342,12 +442,23 @@ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resum
342
442
  taskStore.addAgentStats(subagentType, { input: usage.input, output: usage.output, cost: usage.cost });
343
443
  }
344
444
  if (!result || !result.ok) {
345
- // Signal failure by throwing: the agent loop derives a tool's error state
346
- // from a thrown error, not from a returned flag.
347
445
  const failNote = result?.usedInheritedModelFallback ? "inherited-model retry failed" : undefined;
348
446
  taskStore.update(taskStoreId, { status: "failed", usage, note: failNote });
349
447
  taskStore.patchAgent(subagentType, { state: "failed" });
350
448
  const reason = result?.error ?? (result?.status ? `subagent ${result.status}` : "unknown error");
449
+ if (background) {
450
+ return {
451
+ content: [{ type: "text", text: `${background.label} failed ✗ — ${reason}` }],
452
+ details: {
453
+ subagent_type: subagentType,
454
+ ok: false,
455
+ error: reason,
456
+ taskId: taskStoreId,
457
+ poolTaskId: background.taskId,
458
+ background: true,
459
+ },
460
+ };
461
+ }
351
462
  const stderr = result?.stderr?.trim();
352
463
  throw new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\nstderr: ${stderr.slice(-500)}` : ""}`);
353
464
  }
@@ -367,90 +478,173 @@ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resum
367
478
  if (result.status === "partial" && resumeHandle) {
368
479
  answer += `\n\n[Partial result. To continue this subagent, call Task again with resume_task_id="${resumeHandle}".]`;
369
480
  }
481
+ if (background) {
482
+ // Compact notification: the body is retained in the inbox; the model pulls it
483
+ // with TaskOutput. Keeps a wide swarm from flooding the parent's context.
484
+ const partial = result.status === "partial" ? " (partial — resume to continue)" : "";
485
+ const outstanding = subagentInbox.outstanding().length;
486
+ const tail = outstanding > 0 ? ` ${outstanding} still running.` : "";
487
+ const text = `${background.label} finished ✓${partial} — ${summarize(answer)}.${tail}\n` +
488
+ `Read the full result with TaskOutput("${background.label}").`;
489
+ return {
490
+ content: [{ type: "text", text }],
491
+ details: {
492
+ subagent_type: subagentType,
493
+ ok: true,
494
+ taskId: taskStoreId,
495
+ poolTaskId: background.taskId,
496
+ background: true,
497
+ },
498
+ };
499
+ }
370
500
  return {
371
501
  content: [{ type: "text", text: answer }],
372
502
  details: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },
373
503
  };
374
504
  }
375
505
  const taskOutputParams = Type.Object({
376
- task_id: Type.String({
377
- description: "The task_id of a background (or previously dispatched) subagent, as returned by the Task tool.",
378
- }),
506
+ task_id: Type.Optional(Type.String({
507
+ description: '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.',
508
+ })),
509
+ list: Type.Optional(Type.Boolean({
510
+ description: "List all background subagents with their status (running/done/failed) and current activity. No result bodies are returned.",
511
+ })),
512
+ wait: Type.Optional(Type.Boolean({
513
+ description: "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.",
514
+ })),
515
+ timeout_ms: Type.Optional(Type.Number({ description: "Maximum time to block in wait mode, in milliseconds (default 120000)." })),
379
516
  });
517
+ const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 120_000;
518
+ /** Whole seconds a record has run (so far, or until it settled). */
519
+ function recordElapsed(rec) {
520
+ const end = rec.endedAt ?? Date.now();
521
+ return `${Math.max(0, Math.round((end - rec.startedAt) / 1000))}s`;
522
+ }
523
+ /** A compact roster of every known background subagent — status + activity, no bodies. */
524
+ function formatTaskRoster() {
525
+ const all = subagentInbox.list();
526
+ const outstanding = subagentInbox.outstanding().length;
527
+ if (all.length === 0) {
528
+ return {
529
+ content: [{ type: "text", text: "No background subagents have been dispatched." }],
530
+ details: { status: "empty", ok: true, outstanding: 0 },
531
+ };
532
+ }
533
+ const lines = all.map((r) => {
534
+ const when = recordElapsed(r);
535
+ switch (r.lifecycle) {
536
+ case "running":
537
+ return `- ${r.label} running ${when}${r.lastActivity ? ` · ${r.lastActivity}` : ""}`;
538
+ case "done":
539
+ return `- ${r.label} done (uncollected) ${when} — ${r.summaryLine ?? ""}`;
540
+ case "collected":
541
+ return `- ${r.label} collected ${when} — ${r.summaryLine ?? ""}`;
542
+ default:
543
+ return `- ${r.label} ${r.lifecycle} ✗ — ${r.error ?? "unknown error"}`;
544
+ }
545
+ });
546
+ const header = `${all.length} background subagent${all.length === 1 ? "" : "s"} (${outstanding} running):`;
547
+ const hint = all.some((r) => r.lifecycle === "done") ? '\nRead a finished one with TaskOutput("<label>").' : "";
548
+ return {
549
+ content: [{ type: "text", text: `${header}\n${lines.join("\n")}${hint}` }],
550
+ details: { status: "list", ok: true, outstanding },
551
+ };
552
+ }
380
553
  /**
381
- * TaskOutput tool: poll a background subagent and collect its final answer.
382
- * Returns the current status while running, or the subagent's final answer once
383
- * complete. Registered alongside the Task tool when subagents are enabled.
554
+ * TaskOutput tool: check on background subagents and pull their results.
555
+ *
556
+ * Background `Task` calls don't push their body into the conversation — they
557
+ * leave it in the inbox and post a compact notification. TaskOutput is how the
558
+ * model pulls a body, checks liveness, or waits. It never throws on a valid
559
+ * handle (an error tool result would only confuse the loop): it reports status
560
+ * instead. Modes: `list` (roster), a `task_id` to read/check one, and `wait` to
561
+ * block until one task — or all outstanding tasks — finish.
384
562
  */
385
563
  export function createTaskOutputToolDefinition() {
386
564
  return defineTool({
387
565
  name: "TaskOutput",
388
566
  label: "TaskOutput",
389
567
  description: [
390
- "Check the status of a background subagent and collect its final answer once it finishes.",
391
- "Pass the task_id returned by a background ExecuteTask call. While the subagent runs this reports its status; once complete it returns only the subagent's final answer.",
568
+ "Check on background subagents dispatched via Task, and pull their results.",
569
+ '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.',
570
+ "Set list:true (or omit task_id) to list every background subagent with its status and current activity.",
571
+ "Set wait:true to block until that task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier).",
572
+ "It never errors on a valid handle: a running task reports status, a finished one returns its result, an already-read one says so.",
392
573
  ].join("\n"),
393
- promptSnippet: "check status / collect the result of a background subagent",
574
+ promptSnippet: "check status / list / collect the results of background subagents",
394
575
  parameters: taskOutputParams,
395
576
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
396
- const pool = getSubagentPool(ctx.cwd);
397
- const status = pool.get_status(params.task_id);
398
- // If the task is still running, wait for it to finish (with a timeout)
399
- // instead of asking the model to poll. This eliminates the extra LLM
400
- // round-trip that polling would require.
401
- if (status === "running" || status === "queued") {
402
- const TASK_OUTPUT_TIMEOUT_MS = 120_000;
403
- const result = await Promise.race([
404
- pool.wait_for_completion(params.task_id),
405
- new Promise((resolve) => setTimeout(() => resolve(undefined), TASK_OUTPUT_TIMEOUT_MS)),
406
- ]);
407
- if (!result) {
408
- return {
409
- content: [
410
- {
411
- type: "text",
412
- text: `Subagent task "${params.task_id}" is still ${pool.get_status(params.task_id)}. Timed out after ${TASK_OUTPUT_TIMEOUT_MS / 1000}s.`,
413
- },
414
- ],
415
- details: { task_id: params.task_id, status: pool.get_status(params.task_id), ok: true },
416
- };
417
- }
418
- return formatTaskOutputResult(result, params.task_id);
577
+ // Touch the pool so the inbox is wired to its progress stream for activity.
578
+ subagentInbox.observe(getSubagentPool(ctx.cwd));
579
+ const handle = params.task_id?.trim();
580
+ // Barrier: wait for the target (or all outstanding) to settle first.
581
+ if (params.wait) {
582
+ const timeout = params.timeout_ms ?? TASK_OUTPUT_DEFAULT_TIMEOUT_MS;
583
+ if (handle)
584
+ await subagentInbox.waitFor(handle, timeout);
585
+ else
586
+ await subagentInbox.waitForAll(timeout);
419
587
  }
420
- if (status === "unknown") {
588
+ // Roster when asked, or when no specific task was named.
589
+ if (params.list || !handle) {
590
+ return formatTaskRoster();
591
+ }
592
+ const rec = subagentInbox.get(handle);
593
+ if (!rec) {
421
594
  return {
422
595
  content: [
423
596
  {
424
597
  type: "text",
425
- text: `No result available for task "${params.task_id}" (status: unknown). It may not exist or its result was already collected.`,
598
+ text: `No background task "${handle}". Call TaskOutput with list:true to see active tasks.`,
426
599
  },
427
600
  ],
428
- details: { task_id: params.task_id, status, ok: false },
601
+ details: { task_id: handle, status: "unknown", ok: false },
429
602
  };
430
603
  }
431
- const result = pool.collect(params.task_id);
432
- if (!result) {
433
- throw new Error(`No result available for task "${params.task_id}" (status: ${status}). It may not exist or its result was already collected.`);
604
+ if (rec.lifecycle === "running") {
605
+ const activity = rec.lastActivity ? ` (currently: ${rec.lastActivity})` : "";
606
+ return {
607
+ content: [
608
+ {
609
+ type: "text",
610
+ text: `${rec.label} is still running — ${recordElapsed(rec)} elapsed${activity}. Call TaskOutput again, or with wait:true to block until it finishes.`,
611
+ },
612
+ ],
613
+ details: { task_id: handle, status: "running", ok: true },
614
+ };
615
+ }
616
+ if (rec.lifecycle === "done") {
617
+ const collected = subagentInbox.collect(handle);
618
+ const body = collected?.body ?? rec.summaryLine ?? "(subagent returned no output)";
619
+ return {
620
+ content: [{ type: "text", text: body }],
621
+ details: { task_id: handle, status: "done", ok: true },
622
+ };
434
623
  }
435
- return formatTaskOutputResult(result, params.task_id);
624
+ if (rec.lifecycle === "collected") {
625
+ return {
626
+ content: [
627
+ {
628
+ type: "text",
629
+ text: `${rec.label} was already delivered — ${rec.summaryLine ?? "(no summary kept)"}.`,
630
+ },
631
+ ],
632
+ details: { task_id: handle, status: "collected", ok: true },
633
+ };
634
+ }
635
+ // failed / stalled / timeout
636
+ return {
637
+ content: [
638
+ { type: "text", text: `${rec.label} ${rec.lifecycle} ✗ — ${rec.error ?? "unknown error"}.` },
639
+ ],
640
+ details: { task_id: handle, status: rec.lifecycle, ok: false },
641
+ };
436
642
  },
437
643
  renderCall(args, theme) {
438
- const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("dim", String(args.task_id ?? ""));
644
+ const target = args.list ? "list" : String(args.task_id ?? "");
645
+ const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("dim", args.wait ? `${target} (wait)` : target);
439
646
  return new Text(text, 0, 0);
440
647
  },
441
648
  });
442
649
  }
443
- /** Format a completed subagent result for TaskOutput. */
444
- function formatTaskOutputResult(result, taskId) {
445
- if (!result.ok) {
446
- const reason = result.error ?? (result.status ? `subagent ${result.status}` : "unknown error");
447
- throw new Error(`Background subagent "${taskId}" failed: ${reason}`);
448
- }
449
- const resultData = result.result_data;
450
- const answer = resultData?.summary || "(subagent returned no output)";
451
- return {
452
- content: [{ type: "text", text: answer }],
453
- details: { task_id: taskId, status: result.status ?? "complete", ok: true },
454
- };
455
- }
456
650
  //# sourceMappingURL=subagent.js.map