@kolisachint/hoocode-agent 0.4.77 → 0.4.79

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 (42) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/core/agent-registry.d.ts +11 -0
  3. package/dist/core/agent-registry.d.ts.map +1 -1
  4. package/dist/core/agent-registry.js +36 -1
  5. package/dist/core/agent-registry.js.map +1 -1
  6. package/dist/core/messages.d.ts.map +1 -1
  7. package/dist/core/messages.js +24 -6
  8. package/dist/core/messages.js.map +1 -1
  9. package/dist/core/model-categories.d.ts +14 -8
  10. package/dist/core/model-categories.d.ts.map +1 -1
  11. package/dist/core/model-categories.js +13 -24
  12. package/dist/core/model-categories.js.map +1 -1
  13. package/dist/core/settings-manager.d.ts +3 -2
  14. package/dist/core/settings-manager.d.ts.map +1 -1
  15. package/dist/core/settings-manager.js.map +1 -1
  16. package/dist/core/subagent-inbox.d.ts +92 -0
  17. package/dist/core/subagent-inbox.d.ts.map +1 -0
  18. package/dist/core/subagent-inbox.js +245 -0
  19. package/dist/core/subagent-inbox.js.map +1 -0
  20. package/dist/core/subagent-pool.d.ts +3 -0
  21. package/dist/core/subagent-pool.d.ts.map +1 -1
  22. package/dist/core/subagent-pool.js +4 -2
  23. package/dist/core/subagent-pool.js.map +1 -1
  24. package/dist/core/task-store.d.ts +7 -0
  25. package/dist/core/task-store.d.ts.map +1 -1
  26. package/dist/core/task-store.js +19 -0
  27. package/dist/core/task-store.js.map +1 -1
  28. package/dist/core/tools/subagent.d.ts +20 -17
  29. package/dist/core/tools/subagent.d.ts.map +1 -1
  30. package/dist/core/tools/subagent.js +250 -105
  31. package/dist/core/tools/subagent.js.map +1 -1
  32. package/dist/core/tools/todo.d.ts.map +1 -1
  33. package/dist/core/tools/todo.js +30 -18
  34. package/dist/core/tools/todo.js.map +1 -1
  35. package/dist/init-templates.generated.d.ts.map +1 -1
  36. package/dist/init-templates.generated.js +3 -3
  37. package/dist/init-templates.generated.js.map +1 -1
  38. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  39. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  40. package/examples/extensions/sandbox/package.json +1 -1
  41. package/examples/extensions/with-deps/package.json +1 -1
  42. package/package.json +4 -4
@@ -18,53 +18,21 @@ import { defineTool } from "../extensions/types.js";
18
18
  import { getProviderExhaustion } from "../provider-health.js";
19
19
  import { SessionManager } from "../session-manager.js";
20
20
  import { delegateAllowList, isDelegateAllowed } from "../subagent-depth.js";
21
+ import { subagentInbox } from "../subagent-inbox.js";
21
22
  import { getSubagentPool } from "../subagent-pool-instance.js";
22
23
  import { taskStore } from "../task-store.js";
23
- /**
24
- * Condense a (possibly multi-line, bulleted) agent description into a single
25
- * useful one-liner for the agent picker list.
26
- *
27
- * Built-in agent descriptions open with a boilerplate header ("Use this
28
- * subagent ONLY when:") followed by "when to use" bullets and a "DO NOT use"
29
- * section. Taking the first line alone yields that identical header for every
30
- * agent, so instead surface the first meaningful bullets (or the first prose
31
- * line) from the positive "when to use" region.
32
- */
33
- export function summarizeAgentDescription(description) {
34
- const lines = description
35
- .split("\n")
36
- .map((line) => line.trim())
37
- .filter((line) => line.length > 0);
38
- if (lines.length === 0)
39
- return "";
40
- // Keep only the positive region: everything before a "DO NOT use" section.
41
- const stop = lines.findIndex((line) => /^(do\s*not|don'?t|avoid)\b/i.test(line));
42
- const region = stop === -1 ? lines : lines.slice(0, stop);
43
- // Drop a leading header line (e.g. "Use this subagent ONLY when:").
44
- const body = region.length > 1 && region[0].endsWith(":") ? region.slice(1) : region;
45
- const stripBullet = (line) => line.replace(/^[-*\u2022]\s+/, "").trim();
46
- const bullets = body
47
- .filter((line) => /^[-*\u2022]\s+/.test(line))
48
- .map(stripBullet)
49
- .filter((line) => line.length > 0);
50
- const summary = bullets.length > 0 ? bullets.slice(0, 3).join("; ") : (body[0] ?? lines[0] ?? "").replace(/:$/, "");
51
- const MAX = 200;
52
- return summary.length > MAX ? `${summary.slice(0, MAX - 1).trimEnd()}\u2026` : summary;
53
- }
54
- /** Render the available agents as a "- name: description" list for prompts. */
55
- function describeAvailableAgents(cwd) {
56
- const agents = loadAgentRegistry({ cwd }).list();
57
- if (agents.length === 0)
58
- return "(no agents available)";
59
- return agents.map((a) => `- ${a.name}: ${summarizeAgentDescription(a.description)}`).join("\n");
60
- }
24
+ // Re-exported from its home in agent-registry (where formatAgentsForPrompt uses
25
+ // it to render the roster) so existing importers keep working without creating a
26
+ // tools -> registry -> tools cycle.
27
+ export { summarizeAgentDescription } from "../agent-registry.js";
61
28
  /** System prompt appendix for the main session when the Task tool is enabled.
62
- * Instructs the parent agent on when and how to delegate effectively. */
63
- export function buildTaskMainPrompt(cwd = process.cwd()) {
64
- 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.
65
-
66
- Available agents (choose one via \`subagent_type\`):
67
- ${describeAvailableAgents(cwd)}
29
+ * Instructs the parent agent on when and how to delegate effectively. The
30
+ * available agents themselves are listed once, authoritatively, in the
31
+ * `<available_agents>` block the system prompt emits whenever the Task tool is
32
+ * active (see agent-session `_rebuildSystemPrompt`); this appendix references
33
+ * that list rather than re-rendering the roster and paying for it twice. */
34
+ export function buildTaskMainPrompt() {
35
+ 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\`.
68
36
 
69
37
  When to delegate:
70
38
  1. The work is self-contained and you only need the final result, not intermediate steps.
@@ -72,13 +40,16 @@ When to delegate:
72
40
  3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).
73
41
  4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.
74
42
 
43
+ 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\`.
44
+
75
45
  Guidelines:
76
46
  - Choose the agent whose description best matches the task.
77
47
  - Make every task specific and self-contained. The subagent cannot see this conversation; pass all necessary context (files, constraints, prior findings) in \`prompt\`.
78
48
  - Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.
79
49
  - The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.
80
50
  - 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.
81
- - Some agents are configured to run in the background (non-blocking). For those, the Task call does not block your turn: you keep reasoning and producing output while the subagent runs, and its final answer is delivered to you automatically as a follow-up message once it finishes. You do not need to poll for it.
51
+ - 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.
52
+ - 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)\`.
82
53
  - 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.`;
83
54
  }
84
55
  const taskParams = Type.Object({
@@ -91,6 +62,12 @@ const taskParams = Type.Object({
91
62
  subagent_type: Type.String({
92
63
  description: "The name of the specialized agent to delegate to. Must be one of the available agents.",
93
64
  }),
65
+ complexity: Type.Optional(Type.Union([Type.Literal("fast"), Type.Literal("standard"), Type.Literal("capable")], {
66
+ 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.",
67
+ })),
68
+ background: Type.Optional(Type.Boolean({
69
+ 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.",
70
+ })),
94
71
  resume_task_id: Type.Optional(Type.String({
95
72
  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.",
96
73
  })),
@@ -111,27 +88,28 @@ function summarize(task) {
111
88
  }
112
89
  /** Create the Task tool definition. Registered as a customTool when enabled. */
113
90
  export function createTaskToolDefinition(cwd = process.cwd()) {
114
- const agentList = describeAvailableAgents(cwd);
115
91
  // Agents whose definitions opt into background execution. The agent loop reads
116
92
  // the tool's `background` flag per call and, for these, runs the dispatch
117
93
  // detached: the parent keeps reasoning and the subagent's answer is injected as
118
- // a follow-up message when it finishes (no polling needed).
94
+ // a follow-up message when it finishes (no polling needed). A per-call
95
+ // `background` argument overrides the agent's default in either direction.
119
96
  const backgroundAgents = collectBackgroundAgentNames(cwd);
120
97
  return defineTool({
121
98
  name: TASK_TOOL_NAME,
122
99
  label: TASK_TOOL_NAME,
123
- background: (toolCall) => backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? "")),
100
+ background: (toolCall) => {
101
+ const override = toolCall.arguments?.background;
102
+ if (typeof override === "boolean")
103
+ return override;
104
+ return backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? ""));
105
+ },
106
+ // Kept lean: the available agents are listed in the system prompt, and the
107
+ // `complexity`/`background` semantics live in their parameter descriptions —
108
+ // repeating them here would re-spend those tokens on every turn.
124
109
  description: [
125
- "Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation).",
126
- "Select the agent via `subagent_type`; pass everything it needs via `prompt`. The subagent returns only its final answer.",
127
- "Available agents:",
128
- agentList,
129
- "WHEN TO USE: (1) self-contained work where you only need the final result;",
130
- "(2) parallel investigation/edits without losing your reasoning chain;",
131
- "(3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs);",
132
- "(4) a long command or test suite you want to run without blocking your reasoning.",
133
- "Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.",
134
- "Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.",
110
+ "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.",
111
+ "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.",
112
+ "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.",
135
113
  ].join("\n"),
136
114
  promptSnippet: "delegate a self-contained task to a specialized subagent (choose via subagent_type)",
137
115
  parameters: taskParams,
@@ -213,21 +191,69 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
213
191
  agent: params.subagent_type,
214
192
  });
215
193
  registerSubagentDispatch(params.subagent_type);
216
- // Always dispatch and await the subagent's full result here. Background
217
- // agents (def.background) are made non-blocking by the agent loop via this
218
- // tool's `background` flag: the loop runs this execute() detached, answers
219
- // the call with a placeholder, and injects the answer below as a follow-up
220
- // message when it resolves. Foreground agents block the turn as usual.
221
194
  taskStore.update(task.id, { status: "in_progress" });
222
195
  // Fork agents inherit the parent's conversation via a forked session.
223
196
  const forkSessionFile = def.fork
224
197
  ? resolveForkSessionFile(def, ctx.sessionManager?.getSessionFile(), ctx.cwd)
225
198
  : undefined;
199
+ // `complexity` is passed as the model: the pool's spawn() already lets a
200
+ // non-`inherit` agent model win, then resolves a category string (fast/
201
+ // standard/capable) via settings.modelCategories. So a pinned-model agent
202
+ // ignores complexity, and an `inherit` agent picks up the requested tier —
203
+ // no settings lookup needed here.
204
+ const dispatchModel = params.complexity ?? ctx.model?.id;
205
+ // Whether this call runs detached. The agent loop reads the tool's
206
+ // `background` flag (the same predicate) to run execute() detached; we
207
+ // recompute it here to choose the notify-and-pull return shape.
208
+ const isBackground = params.background ?? backgroundAgents.has(params.subagent_type);
209
+ if (isBackground) {
210
+ // Notify-and-pull: register the dispatch in the inbox under a pre-allocated
211
+ // id, await it, retain the body in the inbox, and return a compact
212
+ // notification (not the body). The model pulls the body with TaskOutput.
213
+ const poolTaskId = `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
214
+ const label = subagentInbox.nextLabel(params.subagent_type);
215
+ subagentInbox.observe(pool);
216
+ subagentInbox.start(poolTaskId, label, params.subagent_type);
217
+ try {
218
+ const dispatchResult = await pool.dispatch(params.prompt, {
219
+ forceAgent: params.subagent_type,
220
+ context: "",
221
+ model: dispatchModel,
222
+ provider: ctx.model?.provider,
223
+ sessionFile: forkSessionFile,
224
+ taskId: poolTaskId,
225
+ });
226
+ subagentInbox.finish(poolTaskId, dispatchResult);
227
+ return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
228
+ taskId: poolTaskId,
229
+ label,
230
+ });
231
+ }
232
+ catch (error) {
233
+ const reason = error instanceof Error ? error.message : String(error);
234
+ taskStore.update(task.id, { status: "failed" });
235
+ subagentInbox.fail(poolTaskId, reason);
236
+ // A background dispatch reports failure as a compact notification, not a
237
+ // thrown tool error — the call was already answered by a placeholder.
238
+ return {
239
+ content: [{ type: "text", text: `${label} failed ✗ — ${reason}` }],
240
+ details: {
241
+ subagent_type: params.subagent_type,
242
+ ok: false,
243
+ error: reason,
244
+ taskId: task.id,
245
+ poolTaskId,
246
+ background: true,
247
+ },
248
+ };
249
+ }
250
+ }
251
+ // Foreground: block the turn and return the subagent's full answer inline.
226
252
  try {
227
253
  const dispatchResult = await pool.dispatch(params.prompt, {
228
254
  forceAgent: params.subagent_type,
229
255
  context: "",
230
- model: ctx.model?.id,
256
+ model: dispatchModel,
231
257
  provider: ctx.model?.provider,
232
258
  sessionFile: forkSessionFile,
233
259
  });
@@ -292,18 +318,28 @@ function collectBackgroundAgentNames(cwd) {
292
318
  function mergeChildTaskTree(nodes, parentTaskId) {
293
319
  if (!nodes)
294
320
  return;
295
- for (const node of nodes) {
296
- const created = taskStore.create(node.title, {
297
- source: node.source,
298
- subagentMode: node.subagentMode,
299
- parentTaskId,
300
- });
301
- taskStore.update(created.id, { status: node.status, usage: node.usage });
302
- mergeChildTaskTree(node.children, created.id);
303
- }
321
+ taskStore.batch(() => {
322
+ for (const node of nodes) {
323
+ const created = taskStore.create(node.title, {
324
+ source: node.source,
325
+ subagentMode: node.subagentMode,
326
+ parentTaskId,
327
+ });
328
+ taskStore.update(created.id, { status: node.status, usage: node.usage });
329
+ mergeChildTaskTree(node.children, created.id);
330
+ }
331
+ });
304
332
  }
305
- /** Extract the final answer from a finished dispatch, updating the task panel. */
306
- function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resumeHandle) {
333
+ /**
334
+ * Update the task panel from a finished dispatch and shape the tool result.
335
+ *
336
+ * Foreground calls return the subagent's full answer inline and signal a hard
337
+ * failure by throwing (the agent loop derives a tool's error state from a thrown
338
+ * error). A background call passes `background`: the body already lives in the
339
+ * inbox, so it returns a compact, self-contained notification (success or
340
+ * failure) and never throws — the call was already answered by a placeholder.
341
+ */
342
+ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resumeHandle, background) {
307
343
  const result = dispatchResult.result;
308
344
  const resultData = result?.result_data;
309
345
  const usage = resultData?.usage;
@@ -316,12 +352,23 @@ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resum
316
352
  taskStore.addAgentStats(subagentType, { input: usage.input, output: usage.output, cost: usage.cost });
317
353
  }
318
354
  if (!result || !result.ok) {
319
- // Signal failure by throwing: the agent loop derives a tool's error state
320
- // from a thrown error, not from a returned flag.
321
355
  const failNote = result?.usedInheritedModelFallback ? "inherited-model retry failed" : undefined;
322
356
  taskStore.update(taskStoreId, { status: "failed", usage, note: failNote });
323
357
  taskStore.patchAgent(subagentType, { state: "failed" });
324
358
  const reason = result?.error ?? (result?.status ? `subagent ${result.status}` : "unknown error");
359
+ if (background) {
360
+ return {
361
+ content: [{ type: "text", text: `${background.label} failed ✗ — ${reason}` }],
362
+ details: {
363
+ subagent_type: subagentType,
364
+ ok: false,
365
+ error: reason,
366
+ taskId: taskStoreId,
367
+ poolTaskId: background.taskId,
368
+ background: true,
369
+ },
370
+ };
371
+ }
325
372
  const stderr = result?.stderr?.trim();
326
373
  throw new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\nstderr: ${stderr.slice(-500)}` : ""}`);
327
374
  }
@@ -341,73 +388,171 @@ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resum
341
388
  if (result.status === "partial" && resumeHandle) {
342
389
  answer += `\n\n[Partial result. To continue this subagent, call Task again with resume_task_id="${resumeHandle}".]`;
343
390
  }
391
+ if (background) {
392
+ // Compact notification: the body is retained in the inbox; the model pulls it
393
+ // with TaskOutput. Keeps a wide swarm from flooding the parent's context.
394
+ const partial = result.status === "partial" ? " (partial — resume to continue)" : "";
395
+ const outstanding = subagentInbox.outstanding().length;
396
+ const tail = outstanding > 0 ? ` ${outstanding} still running.` : "";
397
+ const text = `${background.label} finished ✓${partial} — ${summarize(answer)}.${tail}\n` +
398
+ `Read the full result with TaskOutput("${background.label}").`;
399
+ return {
400
+ content: [{ type: "text", text }],
401
+ details: {
402
+ subagent_type: subagentType,
403
+ ok: true,
404
+ taskId: taskStoreId,
405
+ poolTaskId: background.taskId,
406
+ background: true,
407
+ },
408
+ };
409
+ }
344
410
  return {
345
411
  content: [{ type: "text", text: answer }],
346
412
  details: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },
347
413
  };
348
414
  }
349
415
  const taskOutputParams = Type.Object({
350
- task_id: Type.String({
351
- description: "The task_id of a background (or previously dispatched) subagent, as returned by the Task tool.",
352
- }),
416
+ task_id: Type.Optional(Type.String({
417
+ 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.',
418
+ })),
419
+ list: Type.Optional(Type.Boolean({
420
+ description: "List all background subagents with their status (running/done/failed) and current activity. No result bodies are returned.",
421
+ })),
422
+ wait: Type.Optional(Type.Boolean({
423
+ 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.",
424
+ })),
425
+ timeout_ms: Type.Optional(Type.Number({ description: "Maximum time to block in wait mode, in milliseconds (default 120000)." })),
353
426
  });
427
+ const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 120_000;
428
+ /** Whole seconds a record has run (so far, or until it settled). */
429
+ function recordElapsed(rec) {
430
+ const end = rec.endedAt ?? Date.now();
431
+ return `${Math.max(0, Math.round((end - rec.startedAt) / 1000))}s`;
432
+ }
433
+ /** A compact roster of every known background subagent — status + activity, no bodies. */
434
+ function formatTaskRoster() {
435
+ const all = subagentInbox.list();
436
+ const outstanding = subagentInbox.outstanding().length;
437
+ if (all.length === 0) {
438
+ return {
439
+ content: [{ type: "text", text: "No background subagents have been dispatched." }],
440
+ details: { status: "empty", ok: true, outstanding: 0 },
441
+ };
442
+ }
443
+ const lines = all.map((r) => {
444
+ const when = recordElapsed(r);
445
+ switch (r.lifecycle) {
446
+ case "running":
447
+ return `- ${r.label} running ${when}${r.lastActivity ? ` · ${r.lastActivity}` : ""}`;
448
+ case "done":
449
+ return `- ${r.label} done (uncollected) ${when} — ${r.summaryLine ?? ""}`;
450
+ case "collected":
451
+ return `- ${r.label} collected ${when} — ${r.summaryLine ?? ""}`;
452
+ default:
453
+ return `- ${r.label} ${r.lifecycle} ✗ — ${r.error ?? "unknown error"}`;
454
+ }
455
+ });
456
+ const header = `${all.length} background subagent${all.length === 1 ? "" : "s"} (${outstanding} running):`;
457
+ const hint = all.some((r) => r.lifecycle === "done") ? '\nRead a finished one with TaskOutput("<label>").' : "";
458
+ return {
459
+ content: [{ type: "text", text: `${header}\n${lines.join("\n")}${hint}` }],
460
+ details: { status: "list", ok: true, outstanding },
461
+ };
462
+ }
354
463
  /**
355
- * TaskOutput tool: poll a background subagent and collect its final answer.
356
- * Returns the current status while running, or the subagent's final answer once
357
- * complete. Registered alongside the Task tool when subagents are enabled.
464
+ * TaskOutput tool: check on background subagents and pull their results.
465
+ *
466
+ * Background `Task` calls don't push their body into the conversation — they
467
+ * leave it in the inbox and post a compact notification. TaskOutput is how the
468
+ * model pulls a body, checks liveness, or waits. It never throws on a valid
469
+ * handle (an error tool result would only confuse the loop): it reports status
470
+ * instead. Modes: `list` (roster), a `task_id` to read/check one, and `wait` to
471
+ * block until one task — or all outstanding tasks — finish.
358
472
  */
359
473
  export function createTaskOutputToolDefinition() {
360
474
  return defineTool({
361
475
  name: "TaskOutput",
362
476
  label: "TaskOutput",
363
477
  description: [
364
- "Check the status of a background subagent and collect its final answer once it finishes.",
365
- "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.",
478
+ "Check on background subagents dispatched via Task, and pull their results.",
479
+ '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.',
480
+ "Set list:true (or omit task_id) to list every background subagent with its status and current activity.",
481
+ "Set wait:true to block until that task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier).",
482
+ "It never errors on a valid handle: a running task reports status, a finished one returns its result, an already-read one says so.",
366
483
  ].join("\n"),
367
- promptSnippet: "check status / collect the result of a background subagent",
484
+ promptSnippet: "check status / list / collect the results of background subagents",
368
485
  parameters: taskOutputParams,
369
486
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
370
- const pool = getSubagentPool(ctx.cwd);
371
- const status = pool.get_status(params.task_id);
372
- if (status === "running" || status === "queued") {
487
+ // Touch the pool so the inbox is wired to its progress stream for activity.
488
+ subagentInbox.observe(getSubagentPool(ctx.cwd));
489
+ const handle = params.task_id?.trim();
490
+ // Barrier: wait for the target (or all outstanding) to settle first.
491
+ if (params.wait) {
492
+ const timeout = params.timeout_ms ?? TASK_OUTPUT_DEFAULT_TIMEOUT_MS;
493
+ if (handle)
494
+ await subagentInbox.waitFor(handle, timeout);
495
+ else
496
+ await subagentInbox.waitForAll(timeout);
497
+ }
498
+ // Roster when asked, or when no specific task was named.
499
+ if (params.list || !handle) {
500
+ return formatTaskRoster();
501
+ }
502
+ const rec = subagentInbox.get(handle);
503
+ if (!rec) {
373
504
  return {
374
505
  content: [
375
506
  {
376
507
  type: "text",
377
- text: `Subagent task "${params.task_id}" is ${status}. Call TaskOutput again later to collect its result.`,
508
+ text: `No background task "${handle}". Call TaskOutput with list:true to see active tasks.`,
378
509
  },
379
510
  ],
380
- details: { task_id: params.task_id, status, ok: true },
511
+ details: { task_id: handle, status: "unknown", ok: false },
381
512
  };
382
513
  }
383
- if (status === "unknown") {
514
+ if (rec.lifecycle === "running") {
515
+ const activity = rec.lastActivity ? ` (currently: ${rec.lastActivity})` : "";
384
516
  return {
385
517
  content: [
386
518
  {
387
519
  type: "text",
388
- text: `No result available for task "${params.task_id}" (status: unknown). It may not exist or its result was already collected.`,
520
+ text: `${rec.label} is still running ${recordElapsed(rec)} elapsed${activity}. Call TaskOutput again, or with wait:true to block until it finishes.`,
389
521
  },
390
522
  ],
391
- details: { task_id: params.task_id, status, ok: false },
523
+ details: { task_id: handle, status: "running", ok: true },
392
524
  };
393
525
  }
394
- const result = pool.collect(params.task_id);
395
- if (!result) {
396
- throw new Error(`No result available for task "${params.task_id}" (status: ${status}). It may not exist or its result was already collected.`);
526
+ if (rec.lifecycle === "done") {
527
+ const collected = subagentInbox.collect(handle);
528
+ const body = collected?.body ?? rec.summaryLine ?? "(subagent returned no output)";
529
+ return {
530
+ content: [{ type: "text", text: body }],
531
+ details: { task_id: handle, status: "done", ok: true },
532
+ };
397
533
  }
398
- if (!result.ok) {
399
- const reason = result.error ?? (result.status ? `subagent ${result.status}` : status);
400
- throw new Error(`Background subagent "${params.task_id}" failed: ${reason}`);
534
+ if (rec.lifecycle === "collected") {
535
+ return {
536
+ content: [
537
+ {
538
+ type: "text",
539
+ text: `${rec.label} was already delivered — ${rec.summaryLine ?? "(no summary kept)"}.`,
540
+ },
541
+ ],
542
+ details: { task_id: handle, status: "collected", ok: true },
543
+ };
401
544
  }
402
- const resultData = result.result_data;
403
- const answer = resultData?.summary || "(subagent returned no output)";
545
+ // failed / stalled / timeout
404
546
  return {
405
- content: [{ type: "text", text: answer }],
406
- details: { task_id: params.task_id, status: result.status ?? "complete", ok: true },
547
+ content: [
548
+ { type: "text", text: `${rec.label} ${rec.lifecycle} ✗ — ${rec.error ?? "unknown error"}.` },
549
+ ],
550
+ details: { task_id: handle, status: rec.lifecycle, ok: false },
407
551
  };
408
552
  },
409
553
  renderCall(args, theme) {
410
- const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("dim", String(args.task_id ?? ""));
554
+ const target = args.list ? "list" : String(args.task_id ?? "");
555
+ const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("dim", args.wait ? `${target} (wait)` : target);
411
556
  return new Text(text, 0, 0);
412
557
  },
413
558
  });