@kolisachint/hoocode-agent 0.4.78 → 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 (79) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/core/agent-frontmatter.d.ts +5 -7
  3. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  4. package/dist/core/agent-frontmatter.js +2 -4
  5. package/dist/core/agent-frontmatter.js.map +1 -1
  6. package/dist/core/agent-registry.d.ts +12 -1
  7. package/dist/core/agent-registry.d.ts.map +1 -1
  8. package/dist/core/agent-registry.js +38 -3
  9. package/dist/core/agent-registry.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -3
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/dispatch-evaluator.d.ts +1 -1
  14. package/dist/core/dispatch-evaluator.d.ts.map +1 -1
  15. package/dist/core/dispatch-evaluator.js +1 -1
  16. package/dist/core/dispatch-evaluator.js.map +1 -1
  17. package/dist/core/messages.d.ts.map +1 -1
  18. package/dist/core/messages.js +24 -6
  19. package/dist/core/messages.js.map +1 -1
  20. package/dist/core/model-categories.d.ts +14 -8
  21. package/dist/core/model-categories.d.ts.map +1 -1
  22. package/dist/core/model-categories.js +13 -24
  23. package/dist/core/model-categories.js.map +1 -1
  24. package/dist/core/provider-health.d.ts +1 -1
  25. package/dist/core/provider-health.d.ts.map +1 -1
  26. package/dist/core/provider-health.js +1 -1
  27. package/dist/core/provider-health.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +3 -2
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/core/subagent-inbox.d.ts +92 -0
  32. package/dist/core/subagent-inbox.d.ts.map +1 -0
  33. package/dist/core/subagent-inbox.js +245 -0
  34. package/dist/core/subagent-inbox.js.map +1 -0
  35. package/dist/core/subagent-pool.d.ts +3 -7
  36. package/dist/core/subagent-pool.d.ts.map +1 -1
  37. package/dist/core/subagent-pool.js +9 -62
  38. package/dist/core/subagent-pool.js.map +1 -1
  39. package/dist/core/system-prompt.d.ts +1 -1
  40. package/dist/core/system-prompt.d.ts.map +1 -1
  41. package/dist/core/system-prompt.js +8 -8
  42. package/dist/core/system-prompt.js.map +1 -1
  43. package/dist/core/task-store.d.ts +6 -5
  44. package/dist/core/task-store.d.ts.map +1 -1
  45. package/dist/core/task-store.js +4 -3
  46. package/dist/core/task-store.js.map +1 -1
  47. package/dist/core/tools/index.d.ts +1 -1
  48. package/dist/core/tools/index.d.ts.map +1 -1
  49. package/dist/core/tools/index.js +1 -1
  50. package/dist/core/tools/index.js.map +1 -1
  51. package/dist/core/tools/subagent.d.ts +23 -20
  52. package/dist/core/tools/subagent.d.ts.map +1 -1
  53. package/dist/core/tools/subagent.js +267 -163
  54. package/dist/core/tools/subagent.js.map +1 -1
  55. package/dist/core/tools/todo.d.ts.map +1 -1
  56. package/dist/core/tools/todo.js +10 -6
  57. package/dist/core/tools/todo.js.map +1 -1
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/init-templates.generated.d.ts.map +1 -1
  63. package/dist/init-templates.generated.js +1 -1
  64. package/dist/init-templates.generated.js.map +1 -1
  65. package/dist/main.d.ts.map +1 -1
  66. package/dist/main.js +5 -5
  67. package/dist/main.js.map +1 -1
  68. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/task-panel.js +11 -1
  70. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  72. package/dist/modes/interactive/interactive-mode.js +1 -1
  73. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  75. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  76. package/examples/extensions/sandbox/package.json +1 -1
  77. package/examples/extensions/with-deps/package.json +1 -1
  78. package/package.json +4 -4
  79. package/templates/agents/general-purpose.md +1 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Subagent inbox: the notify-and-pull bookkeeping behind background `Task`
3
+ * dispatch and the `TaskOutput` tool.
4
+ *
5
+ * Background dispatches don't push their full result into the parent's context.
6
+ * Instead the parent gets a compact notification ("explore#1 finished") and the
7
+ * body is retained here, keyed by task id, until the model explicitly pulls it
8
+ * with TaskOutput. This keeps a wide swarm of subagents from flooding the
9
+ * context with N full summaries, and gives the model a queryable liveness view
10
+ * (running / done / failed + last activity) so it can tell a working subagent
11
+ * from a rogue one. The same surface works at every delegation depth.
12
+ *
13
+ * Lifecycle per task:
14
+ * running ──▶ done (body retained) ──collect──▶ collected (body dropped)
15
+ * ├─▶ failed
16
+ * ├─▶ stalled
17
+ * └─▶ timeout
18
+ */
19
+ import type { SubagentPool, TaskResult } from "./subagent-pool.js";
20
+ export type TaskLifecycle = "running" | "done" | "failed" | "stalled" | "timeout" | "collected";
21
+ /** A non-terminal task is still doing work; the rest have settled. */
22
+ export declare function isOutstanding(lifecycle: TaskLifecycle): boolean;
23
+ export interface InboxRecord {
24
+ taskId: string;
25
+ /** Friendly handle shown to the model, e.g. "explore#1". */
26
+ label: string;
27
+ agentType: string;
28
+ lifecycle: TaskLifecycle;
29
+ startedAt: number;
30
+ endedAt?: number;
31
+ /** Tool the subagent is currently running, from the pool's progress stream. */
32
+ lastActivity?: string;
33
+ /** First line of the result/summary, kept even after the body is collected. */
34
+ summaryLine?: string;
35
+ /** Full subagent summary, retained only while `done` and uncollected. */
36
+ body?: string;
37
+ /** Failure reason when the task did not succeed. */
38
+ error?: string;
39
+ }
40
+ declare class SubagentInbox {
41
+ private records;
42
+ /** Insertion order of task ids, for stable listing and pruning. */
43
+ private order;
44
+ private labelCounters;
45
+ private observedPools;
46
+ /** One-shot callbacks fired after any record settles, for the wait helpers. */
47
+ private settleListeners;
48
+ private notifySettle;
49
+ /**
50
+ * Resolve once the given task settles (or immediately if already settled /
51
+ * unknown), bounded by `timeoutMs`. Backs TaskOutput's per-task wait.
52
+ */
53
+ waitFor(handle: string, timeoutMs: number): Promise<InboxRecord | undefined>;
54
+ /** Resolve once nothing is outstanding (the swarm barrier), bounded by `timeoutMs`. */
55
+ waitForAll(timeoutMs: number): Promise<void>;
56
+ /** Allocate the next friendly label for an agent type (`explore#1`, `explore#2`, …). */
57
+ nextLabel(agentType: string): string;
58
+ /**
59
+ * Track a pool's `task_progress` events so running records carry a live
60
+ * `lastActivity`. Idempotent per pool, so callers can wire it on every dispatch.
61
+ */
62
+ observe(pool: SubagentPool): void;
63
+ /** Register a freshly dispatched background task as running. */
64
+ start(taskId: string, label: string, agentType: string): InboxRecord;
65
+ /** Settle a task from its dispatch result: retain the body on success, the reason on failure. */
66
+ finish(taskId: string, result: TaskResult): InboxRecord | undefined;
67
+ /** Settle a task that never produced a TaskResult (e.g. a thrown dispatch error). */
68
+ fail(taskId: string, reason: string, lifecycle?: TaskLifecycle): InboxRecord | undefined;
69
+ /** Look up a record by task id or by friendly label. */
70
+ get(handle: string): InboxRecord | undefined;
71
+ /**
72
+ * Read a done task's body and mark it collected, dropping the body so it is not
73
+ * re-fed to the model. Returns the record (with `body` still populated for this
74
+ * one read) or undefined for an unknown handle.
75
+ */
76
+ collect(handle: string): {
77
+ record: InboxRecord;
78
+ body: string;
79
+ } | undefined;
80
+ /** All records, oldest first. */
81
+ list(): InboxRecord[];
82
+ /** Records still doing work. */
83
+ outstanding(): InboxRecord[];
84
+ /** Test/teardown helper. */
85
+ clear(): void;
86
+ /** Drop the oldest settled records once past the cap; running records are kept. */
87
+ private prune;
88
+ }
89
+ /** Process-wide subagent inbox shared by the Task and TaskOutput tools. */
90
+ export declare const subagentInbox: SubagentInbox;
91
+ export {};
92
+ //# sourceMappingURL=subagent-inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagent-inbox.d.ts","sourceRoot":"","sources":["../../src/core/subagent-inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEhG,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAE/D;AAED,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,aAAa,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAgCD,cAAM,aAAa;IAClB,OAAO,CAAC,OAAO,CAAkC;IACjD,mEAAmE;IACnE,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,aAAa,CAA+B;IACpD,+EAA+E;IAC/E,OAAO,CAAC,eAAe,CAAyB;IAEhD,OAAO,CAAC,YAAY;IAIpB;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsB3E;IAED,uFAAuF;IACvF,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3C;IAED,0FAAwF;IACxF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAInC;IAED;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAYhC;IAED,gEAAgE;IAChE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAMnE;IAED,iGAAiG;IACjG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAkBlE;IAED,qFAAqF;IACrF,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAE,aAAwB,GAAG,WAAW,GAAG,SAAS,CAUjG;IAED,wDAAwD;IACxD,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAO3C;IAED;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAOzE;IAED,iCAAiC;IACjC,IAAI,IAAI,WAAW,EAAE,CAEpB;IAED,gCAAgC;IAChC,WAAW,IAAI,WAAW,EAAE,CAE3B;IAED,4BAA4B;IAC5B,KAAK,IAAI,IAAI,CAIZ;IAED,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAkBb;AAED,2EAA2E;AAC3E,eAAO,MAAM,aAAa,eAAsB,CAAC","sourcesContent":["/**\n * Subagent inbox: the notify-and-pull bookkeeping behind background `Task`\n * dispatch and the `TaskOutput` tool.\n *\n * Background dispatches don't push their full result into the parent's context.\n * Instead the parent gets a compact notification (\"explore#1 finished\") and the\n * body is retained here, keyed by task id, until the model explicitly pulls it\n * with TaskOutput. This keeps a wide swarm of subagents from flooding the\n * context with N full summaries, and gives the model a queryable liveness view\n * (running / done / failed + last activity) so it can tell a working subagent\n * from a rogue one. The same surface works at every delegation depth.\n *\n * Lifecycle per task:\n * running ──▶ done (body retained) ──collect──▶ collected (body dropped)\n * ├─▶ failed\n * ├─▶ stalled\n * └─▶ timeout\n */\n\nimport type { SubagentPool, TaskResult } from \"./subagent-pool.js\";\nimport type { SubagentResultFile } from \"./subagent-result.js\";\n\nexport type TaskLifecycle = \"running\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" | \"collected\";\n\n/** A non-terminal task is still doing work; the rest have settled. */\nexport function isOutstanding(lifecycle: TaskLifecycle): boolean {\n\treturn lifecycle === \"running\";\n}\n\nexport interface InboxRecord {\n\ttaskId: string;\n\t/** Friendly handle shown to the model, e.g. \"explore#1\". */\n\tlabel: string;\n\tagentType: string;\n\tlifecycle: TaskLifecycle;\n\tstartedAt: number;\n\tendedAt?: number;\n\t/** Tool the subagent is currently running, from the pool's progress stream. */\n\tlastActivity?: string;\n\t/** First line of the result/summary, kept even after the body is collected. */\n\tsummaryLine?: string;\n\t/** Full subagent summary, retained only while `done` and uncollected. */\n\tbody?: string;\n\t/** Failure reason when the task did not succeed. */\n\terror?: string;\n}\n\n/** Cap on settled (terminal) records kept for late pulls; running records are never pruned. */\nconst MAX_SETTLED = 50;\n\n/** First non-empty line of a block of text, length-capped. */\nfunction firstLine(text: string, max = 120): string {\n\tconst line = (\n\t\ttext\n\t\t\t.trim()\n\t\t\t.split(\"\\n\")\n\t\t\t.find((l) => l.trim()) ?? \"\"\n\t).trim();\n\treturn line.length > max ? `${line.slice(0, max - 1)}…` : line;\n}\n\n/** Map a subagent's terminal `status` onto an inbox lifecycle. */\nfunction failLifecycle(status: string | undefined): TaskLifecycle {\n\tif (status === \"stalled\") return \"stalled\";\n\tif (status === \"timeout\") return \"timeout\";\n\treturn \"failed\";\n}\n\n/** Derive the \"currently running X\" activity label from a forwarded progress event. */\nfunction activityFromEvent(event: Record<string, unknown>): string | undefined {\n\tif (event.type === \"tool_execution_start\" && typeof event.toolName === \"string\") {\n\t\treturn event.toolName;\n\t}\n\tif (event.type === \"turn_end\") return \"thinking\";\n\treturn undefined;\n}\n\nclass SubagentInbox {\n\tprivate records = new Map<string, InboxRecord>();\n\t/** Insertion order of task ids, for stable listing and pruning. */\n\tprivate order: string[] = [];\n\tprivate labelCounters = new Map<string, number>();\n\tprivate observedPools = new WeakSet<SubagentPool>();\n\t/** One-shot callbacks fired after any record settles, for the wait helpers. */\n\tprivate settleListeners = new Set<() => void>();\n\n\tprivate notifySettle(): void {\n\t\tfor (const listener of [...this.settleListeners]) listener();\n\t}\n\n\t/**\n\t * Resolve once the given task settles (or immediately if already settled /\n\t * unknown), bounded by `timeoutMs`. Backs TaskOutput's per-task wait.\n\t */\n\twaitFor(handle: string, timeoutMs: number): Promise<InboxRecord | undefined> {\n\t\tconst current = this.get(handle);\n\t\tif (!current || !isOutstanding(current.lifecycle)) return Promise.resolve(current);\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tconst rec = this.get(handle);\n\t\t\t\tif (!rec || !isOutstanding(rec.lifecycle)) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(rec);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(this.get(handle));\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Resolve once nothing is outstanding (the swarm barrier), bounded by `timeoutMs`. */\n\twaitForAll(timeoutMs: number): Promise<void> {\n\t\tif (this.outstanding().length === 0) return Promise.resolve();\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tif (this.outstanding().length === 0) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Allocate the next friendly label for an agent type (`explore#1`, `explore#2`, …). */\n\tnextLabel(agentType: string): string {\n\t\tconst n = (this.labelCounters.get(agentType) ?? 0) + 1;\n\t\tthis.labelCounters.set(agentType, n);\n\t\treturn `${agentType}#${n}`;\n\t}\n\n\t/**\n\t * Track a pool's `task_progress` events so running records carry a live\n\t * `lastActivity`. Idempotent per pool, so callers can wire it on every dispatch.\n\t */\n\tobserve(pool: SubagentPool): void {\n\t\t// Tolerate a non-EventEmitter stand-in (test fakes): without progress events\n\t\t// records simply carry no live activity.\n\t\tif (typeof (pool as { on?: unknown }).on !== \"function\") return;\n\t\tif (this.observedPools.has(pool)) return;\n\t\tthis.observedPools.add(pool);\n\t\tpool.on(\"task_progress\", (data: { task_id: string; event: Record<string, unknown> }) => {\n\t\t\tconst rec = this.records.get(data.task_id);\n\t\t\tif (!rec || rec.lifecycle !== \"running\") return;\n\t\t\tconst activity = activityFromEvent(data.event);\n\t\t\tif (activity) rec.lastActivity = activity;\n\t\t});\n\t}\n\n\t/** Register a freshly dispatched background task as running. */\n\tstart(taskId: string, label: string, agentType: string): InboxRecord {\n\t\tconst rec: InboxRecord = { taskId, label, agentType, lifecycle: \"running\", startedAt: Date.now() };\n\t\tthis.records.set(taskId, rec);\n\t\tthis.order.push(taskId);\n\t\tthis.prune();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task from its dispatch result: retain the body on success, the reason on failure. */\n\tfinish(taskId: string, result: TaskResult): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\tconst r = result.result;\n\t\tconst data = r?.result_data as SubagentResultFile | undefined;\n\t\tif (r?.ok) {\n\t\t\trec.lifecycle = \"done\";\n\t\t\trec.body = data?.summary?.trim() || \"(subagent returned no output)\";\n\t\t\trec.summaryLine = firstLine(rec.body);\n\t\t} else {\n\t\t\trec.lifecycle = failLifecycle(r?.status);\n\t\t\trec.error = r?.error ?? (r?.status ? `subagent ${r.status}` : \"unknown error\");\n\t\t\trec.summaryLine = rec.error;\n\t\t}\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task that never produced a TaskResult (e.g. a thrown dispatch error). */\n\tfail(taskId: string, reason: string, lifecycle: TaskLifecycle = \"failed\"): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\trec.lifecycle = lifecycle;\n\t\trec.error = reason;\n\t\trec.summaryLine = reason;\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Look up a record by task id or by friendly label. */\n\tget(handle: string): InboxRecord | undefined {\n\t\tconst byId = this.records.get(handle);\n\t\tif (byId) return byId;\n\t\tfor (const rec of this.records.values()) {\n\t\t\tif (rec.label === handle) return rec;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Read a done task's body and mark it collected, dropping the body so it is not\n\t * re-fed to the model. Returns the record (with `body` still populated for this\n\t * one read) or undefined for an unknown handle.\n\t */\n\tcollect(handle: string): { record: InboxRecord; body: string } | undefined {\n\t\tconst rec = this.get(handle);\n\t\tif (!rec || rec.lifecycle !== \"done\" || rec.body === undefined) return undefined;\n\t\tconst body = rec.body;\n\t\trec.lifecycle = \"collected\";\n\t\trec.body = undefined;\n\t\treturn { record: rec, body };\n\t}\n\n\t/** All records, oldest first. */\n\tlist(): InboxRecord[] {\n\t\treturn this.order.map((id) => this.records.get(id)).filter((r): r is InboxRecord => r !== undefined);\n\t}\n\n\t/** Records still doing work. */\n\toutstanding(): InboxRecord[] {\n\t\treturn this.list().filter((r) => isOutstanding(r.lifecycle));\n\t}\n\n\t/** Test/teardown helper. */\n\tclear(): void {\n\t\tthis.records.clear();\n\t\tthis.order = [];\n\t\tthis.labelCounters.clear();\n\t}\n\n\t/** Drop the oldest settled records once past the cap; running records are kept. */\n\tprivate prune(): void {\n\t\tlet settled = this.order.filter((id) => {\n\t\t\tconst r = this.records.get(id);\n\t\t\treturn r !== undefined && !isOutstanding(r.lifecycle);\n\t\t}).length;\n\t\tif (settled <= MAX_SETTLED) return;\n\t\tconst kept: string[] = [];\n\t\tfor (const id of this.order) {\n\t\t\tconst r = this.records.get(id);\n\t\t\tif (r && !isOutstanding(r.lifecycle) && settled > MAX_SETTLED) {\n\t\t\t\tthis.records.delete(id);\n\t\t\t\tsettled--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tkept.push(id);\n\t\t}\n\t\tthis.order = kept;\n\t}\n}\n\n/** Process-wide subagent inbox shared by the Task and TaskOutput tools. */\nexport const subagentInbox = new SubagentInbox();\n"]}
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Subagent inbox: the notify-and-pull bookkeeping behind background `Task`
3
+ * dispatch and the `TaskOutput` tool.
4
+ *
5
+ * Background dispatches don't push their full result into the parent's context.
6
+ * Instead the parent gets a compact notification ("explore#1 finished") and the
7
+ * body is retained here, keyed by task id, until the model explicitly pulls it
8
+ * with TaskOutput. This keeps a wide swarm of subagents from flooding the
9
+ * context with N full summaries, and gives the model a queryable liveness view
10
+ * (running / done / failed + last activity) so it can tell a working subagent
11
+ * from a rogue one. The same surface works at every delegation depth.
12
+ *
13
+ * Lifecycle per task:
14
+ * running ──▶ done (body retained) ──collect──▶ collected (body dropped)
15
+ * ├─▶ failed
16
+ * ├─▶ stalled
17
+ * └─▶ timeout
18
+ */
19
+ /** A non-terminal task is still doing work; the rest have settled. */
20
+ export function isOutstanding(lifecycle) {
21
+ return lifecycle === "running";
22
+ }
23
+ /** Cap on settled (terminal) records kept for late pulls; running records are never pruned. */
24
+ const MAX_SETTLED = 50;
25
+ /** First non-empty line of a block of text, length-capped. */
26
+ function firstLine(text, max = 120) {
27
+ const line = (text
28
+ .trim()
29
+ .split("\n")
30
+ .find((l) => l.trim()) ?? "").trim();
31
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
32
+ }
33
+ /** Map a subagent's terminal `status` onto an inbox lifecycle. */
34
+ function failLifecycle(status) {
35
+ if (status === "stalled")
36
+ return "stalled";
37
+ if (status === "timeout")
38
+ return "timeout";
39
+ return "failed";
40
+ }
41
+ /** Derive the "currently running X" activity label from a forwarded progress event. */
42
+ function activityFromEvent(event) {
43
+ if (event.type === "tool_execution_start" && typeof event.toolName === "string") {
44
+ return event.toolName;
45
+ }
46
+ if (event.type === "turn_end")
47
+ return "thinking";
48
+ return undefined;
49
+ }
50
+ class SubagentInbox {
51
+ records = new Map();
52
+ /** Insertion order of task ids, for stable listing and pruning. */
53
+ order = [];
54
+ labelCounters = new Map();
55
+ observedPools = new WeakSet();
56
+ /** One-shot callbacks fired after any record settles, for the wait helpers. */
57
+ settleListeners = new Set();
58
+ notifySettle() {
59
+ for (const listener of [...this.settleListeners])
60
+ listener();
61
+ }
62
+ /**
63
+ * Resolve once the given task settles (or immediately if already settled /
64
+ * unknown), bounded by `timeoutMs`. Backs TaskOutput's per-task wait.
65
+ */
66
+ waitFor(handle, timeoutMs) {
67
+ const current = this.get(handle);
68
+ if (!current || !isOutstanding(current.lifecycle))
69
+ return Promise.resolve(current);
70
+ return new Promise((resolve) => {
71
+ const cleanup = () => {
72
+ this.settleListeners.delete(check);
73
+ clearTimeout(timer);
74
+ };
75
+ const check = () => {
76
+ const rec = this.get(handle);
77
+ if (!rec || !isOutstanding(rec.lifecycle)) {
78
+ cleanup();
79
+ resolve(rec);
80
+ }
81
+ };
82
+ const timer = setTimeout(() => {
83
+ cleanup();
84
+ resolve(this.get(handle));
85
+ }, timeoutMs);
86
+ timer.unref?.();
87
+ this.settleListeners.add(check);
88
+ });
89
+ }
90
+ /** Resolve once nothing is outstanding (the swarm barrier), bounded by `timeoutMs`. */
91
+ waitForAll(timeoutMs) {
92
+ if (this.outstanding().length === 0)
93
+ return Promise.resolve();
94
+ return new Promise((resolve) => {
95
+ const cleanup = () => {
96
+ this.settleListeners.delete(check);
97
+ clearTimeout(timer);
98
+ };
99
+ const check = () => {
100
+ if (this.outstanding().length === 0) {
101
+ cleanup();
102
+ resolve();
103
+ }
104
+ };
105
+ const timer = setTimeout(() => {
106
+ cleanup();
107
+ resolve();
108
+ }, timeoutMs);
109
+ timer.unref?.();
110
+ this.settleListeners.add(check);
111
+ });
112
+ }
113
+ /** Allocate the next friendly label for an agent type (`explore#1`, `explore#2`, …). */
114
+ nextLabel(agentType) {
115
+ const n = (this.labelCounters.get(agentType) ?? 0) + 1;
116
+ this.labelCounters.set(agentType, n);
117
+ return `${agentType}#${n}`;
118
+ }
119
+ /**
120
+ * Track a pool's `task_progress` events so running records carry a live
121
+ * `lastActivity`. Idempotent per pool, so callers can wire it on every dispatch.
122
+ */
123
+ observe(pool) {
124
+ // Tolerate a non-EventEmitter stand-in (test fakes): without progress events
125
+ // records simply carry no live activity.
126
+ if (typeof pool.on !== "function")
127
+ return;
128
+ if (this.observedPools.has(pool))
129
+ return;
130
+ this.observedPools.add(pool);
131
+ pool.on("task_progress", (data) => {
132
+ const rec = this.records.get(data.task_id);
133
+ if (!rec || rec.lifecycle !== "running")
134
+ return;
135
+ const activity = activityFromEvent(data.event);
136
+ if (activity)
137
+ rec.lastActivity = activity;
138
+ });
139
+ }
140
+ /** Register a freshly dispatched background task as running. */
141
+ start(taskId, label, agentType) {
142
+ const rec = { taskId, label, agentType, lifecycle: "running", startedAt: Date.now() };
143
+ this.records.set(taskId, rec);
144
+ this.order.push(taskId);
145
+ this.prune();
146
+ return rec;
147
+ }
148
+ /** Settle a task from its dispatch result: retain the body on success, the reason on failure. */
149
+ finish(taskId, result) {
150
+ const rec = this.records.get(taskId);
151
+ if (!rec)
152
+ return undefined;
153
+ rec.endedAt = Date.now();
154
+ rec.lastActivity = undefined;
155
+ const r = result.result;
156
+ const data = r?.result_data;
157
+ if (r?.ok) {
158
+ rec.lifecycle = "done";
159
+ rec.body = data?.summary?.trim() || "(subagent returned no output)";
160
+ rec.summaryLine = firstLine(rec.body);
161
+ }
162
+ else {
163
+ rec.lifecycle = failLifecycle(r?.status);
164
+ rec.error = r?.error ?? (r?.status ? `subagent ${r.status}` : "unknown error");
165
+ rec.summaryLine = rec.error;
166
+ }
167
+ this.notifySettle();
168
+ return rec;
169
+ }
170
+ /** Settle a task that never produced a TaskResult (e.g. a thrown dispatch error). */
171
+ fail(taskId, reason, lifecycle = "failed") {
172
+ const rec = this.records.get(taskId);
173
+ if (!rec)
174
+ return undefined;
175
+ rec.endedAt = Date.now();
176
+ rec.lastActivity = undefined;
177
+ rec.lifecycle = lifecycle;
178
+ rec.error = reason;
179
+ rec.summaryLine = reason;
180
+ this.notifySettle();
181
+ return rec;
182
+ }
183
+ /** Look up a record by task id or by friendly label. */
184
+ get(handle) {
185
+ const byId = this.records.get(handle);
186
+ if (byId)
187
+ return byId;
188
+ for (const rec of this.records.values()) {
189
+ if (rec.label === handle)
190
+ return rec;
191
+ }
192
+ return undefined;
193
+ }
194
+ /**
195
+ * Read a done task's body and mark it collected, dropping the body so it is not
196
+ * re-fed to the model. Returns the record (with `body` still populated for this
197
+ * one read) or undefined for an unknown handle.
198
+ */
199
+ collect(handle) {
200
+ const rec = this.get(handle);
201
+ if (!rec || rec.lifecycle !== "done" || rec.body === undefined)
202
+ return undefined;
203
+ const body = rec.body;
204
+ rec.lifecycle = "collected";
205
+ rec.body = undefined;
206
+ return { record: rec, body };
207
+ }
208
+ /** All records, oldest first. */
209
+ list() {
210
+ return this.order.map((id) => this.records.get(id)).filter((r) => r !== undefined);
211
+ }
212
+ /** Records still doing work. */
213
+ outstanding() {
214
+ return this.list().filter((r) => isOutstanding(r.lifecycle));
215
+ }
216
+ /** Test/teardown helper. */
217
+ clear() {
218
+ this.records.clear();
219
+ this.order = [];
220
+ this.labelCounters.clear();
221
+ }
222
+ /** Drop the oldest settled records once past the cap; running records are kept. */
223
+ prune() {
224
+ let settled = this.order.filter((id) => {
225
+ const r = this.records.get(id);
226
+ return r !== undefined && !isOutstanding(r.lifecycle);
227
+ }).length;
228
+ if (settled <= MAX_SETTLED)
229
+ return;
230
+ const kept = [];
231
+ for (const id of this.order) {
232
+ const r = this.records.get(id);
233
+ if (r && !isOutstanding(r.lifecycle) && settled > MAX_SETTLED) {
234
+ this.records.delete(id);
235
+ settled--;
236
+ continue;
237
+ }
238
+ kept.push(id);
239
+ }
240
+ this.order = kept;
241
+ }
242
+ }
243
+ /** Process-wide subagent inbox shared by the Task and TaskOutput tools. */
244
+ export const subagentInbox = new SubagentInbox();
245
+ //# sourceMappingURL=subagent-inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagent-inbox.js","sourceRoot":"","sources":["../../src/core/subagent-inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,sEAAsE;AACtE,MAAM,UAAU,aAAa,CAAC,SAAwB,EAAW;IAChE,OAAO,SAAS,KAAK,SAAS,CAAC;AAAA,CAC/B;AAoBD,+FAA+F;AAC/F,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,8DAA8D;AAC9D,SAAS,SAAS,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG,EAAU;IACnD,MAAM,IAAI,GAAG,CACZ,IAAI;SACF,IAAI,EAAE;SACN,KAAK,CAAC,IAAI,CAAC;SACX,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAC7B,CAAC,IAAI,EAAE,CAAC;IACT,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAAA,CAC/D;AAED,kEAAkE;AAClE,SAAS,aAAa,CAAC,MAA0B,EAAiB;IACjE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC3C,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,uFAAuF;AACvF,SAAS,iBAAiB,CAAC,KAA8B,EAAsB;IAC9E,IAAI,KAAK,CAAC,IAAI,KAAK,sBAAsB,IAAI,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjF,OAAO,KAAK,CAAC,QAAQ,CAAC;IACvB,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,UAAU,CAAC;IACjD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,MAAM,aAAa;IACV,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IACjD,mEAAmE;IAC3D,KAAK,GAAa,EAAE,CAAC;IACrB,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,aAAa,GAAG,IAAI,OAAO,EAAgB,CAAC;IACpD,+EAA+E;IACvE,eAAe,GAAG,IAAI,GAAG,EAAc,CAAC;IAExC,YAAY,GAAS;QAC5B,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC;YAAE,QAAQ,EAAE,CAAC;IAAA,CAC7D;IAED;;;OAGG;IACH,OAAO,CAAC,MAAc,EAAE,SAAiB,EAAoC;QAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjC,IAAI,CAAC,OAAO,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,SAAS,CAAC;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACnF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;gBACrB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnC,YAAY,CAAC,KAAK,CAAC,CAAC;YAAA,CACpB,CAAC;YACF,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC;gBACnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC7B,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC3C,OAAO,EAAE,CAAC;oBACV,OAAO,CAAC,GAAG,CAAC,CAAC;gBACd,CAAC;YAAA,CACD,CAAC;YACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;gBAC9B,OAAO,EAAE,CAAC;gBACV,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YAAA,CAC1B,EAAE,SAAS,CAAC,CAAC;YACd,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;YAChB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAAA,CAChC,CAAC,CAAC;IAAA,CACH;IAED,uFAAuF;IACvF,UAAU,CAAC,SAAiB,EAAiB;QAC5C,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;gBACrB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnC,YAAY,CAAC,KAAK,CAAC,CAAC;YAAA,CACpB,CAAC;YACF,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC;gBACnB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACrC,OAAO,EAAE,CAAC;oBACV,OAAO,EAAE,CAAC;gBACX,CAAC;YAAA,CACD,CAAC;YACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;gBAC9B,OAAO,EAAE,CAAC;gBACV,OAAO,EAAE,CAAC;YAAA,CACV,EAAE,SAAS,CAAC,CAAC;YACd,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;YAChB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAAA,CAChC,CAAC,CAAC;IAAA,CACH;IAED,0FAAwF;IACxF,SAAS,CAAC,SAAiB,EAAU;QACpC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACvD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QACrC,OAAO,GAAG,SAAS,IAAI,CAAC,EAAE,CAAC;IAAA,CAC3B;IAED;;;OAGG;IACH,OAAO,CAAC,IAAkB,EAAQ;QACjC,6EAA6E;QAC7E,yCAAyC;QACzC,IAAI,OAAQ,IAAyB,CAAC,EAAE,KAAK,UAAU;YAAE,OAAO;QAChE,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO;QACzC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,IAAyD,EAAE,EAAE,CAAC;YACvF,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3C,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS;gBAAE,OAAO;YAChD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/C,IAAI,QAAQ;gBAAE,GAAG,CAAC,YAAY,GAAG,QAAQ,CAAC;QAAA,CAC1C,CAAC,CAAC;IAAA,CACH;IAED,gEAAgE;IAChE,KAAK,CAAC,MAAc,EAAE,KAAa,EAAE,SAAiB,EAAe;QACpE,MAAM,GAAG,GAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACnG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,GAAG,CAAC;IAAA,CACX;IAED,iGAAiG;IACjG,MAAM,CAAC,MAAc,EAAE,MAAkB,EAA2B;QACnE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,GAAG,CAAC,YAAY,GAAG,SAAS,CAAC;QAC7B,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;QACxB,MAAM,IAAI,GAAG,CAAC,EAAE,WAA6C,CAAC;QAC9D,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC;YACX,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC;YACvB,GAAG,CAAC,IAAI,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,+BAA+B,CAAC;YACpE,GAAG,CAAC,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACP,GAAG,CAAC,SAAS,GAAG,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YACzC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;YAC/E,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,OAAO,GAAG,CAAC;IAAA,CACX;IAED,qFAAqF;IACrF,IAAI,CAAC,MAAc,EAAE,MAAc,EAAE,SAAS,GAAkB,QAAQ,EAA2B;QAClG,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,GAAG,CAAC,YAAY,GAAG,SAAS,CAAC;QAC7B,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;QAC1B,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC;QACnB,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC;QACzB,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,OAAO,GAAG,CAAC;IAAA,CACX;IAED,wDAAwD;IACxD,GAAG,CAAC,MAAc,EAA2B;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QACtB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACzC,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM;gBAAE,OAAO,GAAG,CAAC;QACtC,CAAC;QACD,OAAO,SAAS,CAAC;IAAA,CACjB;IAED;;;;OAIG;IACH,OAAO,CAAC,MAAc,EAAqD;QAC1E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,KAAK,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACjF,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACtB,GAAG,CAAC,SAAS,GAAG,WAAW,CAAC;QAC5B,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IAAA,CAC7B;IAED,iCAAiC;IACjC,IAAI,GAAkB;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IAAA,CACrG;IAED,gCAAgC;IAChC,WAAW,GAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAAA,CAC7D;IAED,4BAA4B;IAC5B,KAAK,GAAS;QACb,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;IAAA,CAC3B;IAED,mFAAmF;IAC3E,KAAK,GAAS;QACrB,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC/B,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAAA,CACtD,CAAC,CAAC,MAAM,CAAC;QACV,IAAI,OAAO,IAAI,WAAW;YAAE,OAAO;QACnC,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,OAAO,GAAG,WAAW,EAAE,CAAC;gBAC/D,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACxB,OAAO,EAAE,CAAC;gBACV,SAAS;YACV,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IAAA,CAClB;CACD;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,aAAa,EAAE,CAAC","sourcesContent":["/**\n * Subagent inbox: the notify-and-pull bookkeeping behind background `Task`\n * dispatch and the `TaskOutput` tool.\n *\n * Background dispatches don't push their full result into the parent's context.\n * Instead the parent gets a compact notification (\"explore#1 finished\") and the\n * body is retained here, keyed by task id, until the model explicitly pulls it\n * with TaskOutput. This keeps a wide swarm of subagents from flooding the\n * context with N full summaries, and gives the model a queryable liveness view\n * (running / done / failed + last activity) so it can tell a working subagent\n * from a rogue one. The same surface works at every delegation depth.\n *\n * Lifecycle per task:\n * running ──▶ done (body retained) ──collect──▶ collected (body dropped)\n * ├─▶ failed\n * ├─▶ stalled\n * └─▶ timeout\n */\n\nimport type { SubagentPool, TaskResult } from \"./subagent-pool.js\";\nimport type { SubagentResultFile } from \"./subagent-result.js\";\n\nexport type TaskLifecycle = \"running\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" | \"collected\";\n\n/** A non-terminal task is still doing work; the rest have settled. */\nexport function isOutstanding(lifecycle: TaskLifecycle): boolean {\n\treturn lifecycle === \"running\";\n}\n\nexport interface InboxRecord {\n\ttaskId: string;\n\t/** Friendly handle shown to the model, e.g. \"explore#1\". */\n\tlabel: string;\n\tagentType: string;\n\tlifecycle: TaskLifecycle;\n\tstartedAt: number;\n\tendedAt?: number;\n\t/** Tool the subagent is currently running, from the pool's progress stream. */\n\tlastActivity?: string;\n\t/** First line of the result/summary, kept even after the body is collected. */\n\tsummaryLine?: string;\n\t/** Full subagent summary, retained only while `done` and uncollected. */\n\tbody?: string;\n\t/** Failure reason when the task did not succeed. */\n\terror?: string;\n}\n\n/** Cap on settled (terminal) records kept for late pulls; running records are never pruned. */\nconst MAX_SETTLED = 50;\n\n/** First non-empty line of a block of text, length-capped. */\nfunction firstLine(text: string, max = 120): string {\n\tconst line = (\n\t\ttext\n\t\t\t.trim()\n\t\t\t.split(\"\\n\")\n\t\t\t.find((l) => l.trim()) ?? \"\"\n\t).trim();\n\treturn line.length > max ? `${line.slice(0, max - 1)}…` : line;\n}\n\n/** Map a subagent's terminal `status` onto an inbox lifecycle. */\nfunction failLifecycle(status: string | undefined): TaskLifecycle {\n\tif (status === \"stalled\") return \"stalled\";\n\tif (status === \"timeout\") return \"timeout\";\n\treturn \"failed\";\n}\n\n/** Derive the \"currently running X\" activity label from a forwarded progress event. */\nfunction activityFromEvent(event: Record<string, unknown>): string | undefined {\n\tif (event.type === \"tool_execution_start\" && typeof event.toolName === \"string\") {\n\t\treturn event.toolName;\n\t}\n\tif (event.type === \"turn_end\") return \"thinking\";\n\treturn undefined;\n}\n\nclass SubagentInbox {\n\tprivate records = new Map<string, InboxRecord>();\n\t/** Insertion order of task ids, for stable listing and pruning. */\n\tprivate order: string[] = [];\n\tprivate labelCounters = new Map<string, number>();\n\tprivate observedPools = new WeakSet<SubagentPool>();\n\t/** One-shot callbacks fired after any record settles, for the wait helpers. */\n\tprivate settleListeners = new Set<() => void>();\n\n\tprivate notifySettle(): void {\n\t\tfor (const listener of [...this.settleListeners]) listener();\n\t}\n\n\t/**\n\t * Resolve once the given task settles (or immediately if already settled /\n\t * unknown), bounded by `timeoutMs`. Backs TaskOutput's per-task wait.\n\t */\n\twaitFor(handle: string, timeoutMs: number): Promise<InboxRecord | undefined> {\n\t\tconst current = this.get(handle);\n\t\tif (!current || !isOutstanding(current.lifecycle)) return Promise.resolve(current);\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tconst rec = this.get(handle);\n\t\t\t\tif (!rec || !isOutstanding(rec.lifecycle)) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(rec);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(this.get(handle));\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Resolve once nothing is outstanding (the swarm barrier), bounded by `timeoutMs`. */\n\twaitForAll(timeoutMs: number): Promise<void> {\n\t\tif (this.outstanding().length === 0) return Promise.resolve();\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tif (this.outstanding().length === 0) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Allocate the next friendly label for an agent type (`explore#1`, `explore#2`, …). */\n\tnextLabel(agentType: string): string {\n\t\tconst n = (this.labelCounters.get(agentType) ?? 0) + 1;\n\t\tthis.labelCounters.set(agentType, n);\n\t\treturn `${agentType}#${n}`;\n\t}\n\n\t/**\n\t * Track a pool's `task_progress` events so running records carry a live\n\t * `lastActivity`. Idempotent per pool, so callers can wire it on every dispatch.\n\t */\n\tobserve(pool: SubagentPool): void {\n\t\t// Tolerate a non-EventEmitter stand-in (test fakes): without progress events\n\t\t// records simply carry no live activity.\n\t\tif (typeof (pool as { on?: unknown }).on !== \"function\") return;\n\t\tif (this.observedPools.has(pool)) return;\n\t\tthis.observedPools.add(pool);\n\t\tpool.on(\"task_progress\", (data: { task_id: string; event: Record<string, unknown> }) => {\n\t\t\tconst rec = this.records.get(data.task_id);\n\t\t\tif (!rec || rec.lifecycle !== \"running\") return;\n\t\t\tconst activity = activityFromEvent(data.event);\n\t\t\tif (activity) rec.lastActivity = activity;\n\t\t});\n\t}\n\n\t/** Register a freshly dispatched background task as running. */\n\tstart(taskId: string, label: string, agentType: string): InboxRecord {\n\t\tconst rec: InboxRecord = { taskId, label, agentType, lifecycle: \"running\", startedAt: Date.now() };\n\t\tthis.records.set(taskId, rec);\n\t\tthis.order.push(taskId);\n\t\tthis.prune();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task from its dispatch result: retain the body on success, the reason on failure. */\n\tfinish(taskId: string, result: TaskResult): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\tconst r = result.result;\n\t\tconst data = r?.result_data as SubagentResultFile | undefined;\n\t\tif (r?.ok) {\n\t\t\trec.lifecycle = \"done\";\n\t\t\trec.body = data?.summary?.trim() || \"(subagent returned no output)\";\n\t\t\trec.summaryLine = firstLine(rec.body);\n\t\t} else {\n\t\t\trec.lifecycle = failLifecycle(r?.status);\n\t\t\trec.error = r?.error ?? (r?.status ? `subagent ${r.status}` : \"unknown error\");\n\t\t\trec.summaryLine = rec.error;\n\t\t}\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task that never produced a TaskResult (e.g. a thrown dispatch error). */\n\tfail(taskId: string, reason: string, lifecycle: TaskLifecycle = \"failed\"): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\trec.lifecycle = lifecycle;\n\t\trec.error = reason;\n\t\trec.summaryLine = reason;\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Look up a record by task id or by friendly label. */\n\tget(handle: string): InboxRecord | undefined {\n\t\tconst byId = this.records.get(handle);\n\t\tif (byId) return byId;\n\t\tfor (const rec of this.records.values()) {\n\t\t\tif (rec.label === handle) return rec;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Read a done task's body and mark it collected, dropping the body so it is not\n\t * re-fed to the model. Returns the record (with `body` still populated for this\n\t * one read) or undefined for an unknown handle.\n\t */\n\tcollect(handle: string): { record: InboxRecord; body: string } | undefined {\n\t\tconst rec = this.get(handle);\n\t\tif (!rec || rec.lifecycle !== \"done\" || rec.body === undefined) return undefined;\n\t\tconst body = rec.body;\n\t\trec.lifecycle = \"collected\";\n\t\trec.body = undefined;\n\t\treturn { record: rec, body };\n\t}\n\n\t/** All records, oldest first. */\n\tlist(): InboxRecord[] {\n\t\treturn this.order.map((id) => this.records.get(id)).filter((r): r is InboxRecord => r !== undefined);\n\t}\n\n\t/** Records still doing work. */\n\toutstanding(): InboxRecord[] {\n\t\treturn this.list().filter((r) => isOutstanding(r.lifecycle));\n\t}\n\n\t/** Test/teardown helper. */\n\tclear(): void {\n\t\tthis.records.clear();\n\t\tthis.order = [];\n\t\tthis.labelCounters.clear();\n\t}\n\n\t/** Drop the oldest settled records once past the cap; running records are kept. */\n\tprivate prune(): void {\n\t\tlet settled = this.order.filter((id) => {\n\t\t\tconst r = this.records.get(id);\n\t\t\treturn r !== undefined && !isOutstanding(r.lifecycle);\n\t\t}).length;\n\t\tif (settled <= MAX_SETTLED) return;\n\t\tconst kept: string[] = [];\n\t\tfor (const id of this.order) {\n\t\t\tconst r = this.records.get(id);\n\t\t\tif (r && !isOutstanding(r.lifecycle) && settled > MAX_SETTLED) {\n\t\t\t\tthis.records.delete(id);\n\t\t\t\tsettled--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tkept.push(id);\n\t\t}\n\t\tthis.order = kept;\n\t}\n}\n\n/** Process-wide subagent inbox shared by the Task and TaskOutput tools. */\nexport const subagentInbox = new SubagentInbox();\n"]}
@@ -67,6 +67,9 @@ export interface DispatchOptions {
67
67
  provider?: string;
68
68
  /** Explicit session file to persist/continue (used by resume). */
69
69
  sessionFile?: string;
70
+ /** Caller-supplied task id. Defaults to a generated `dispatch-…` id. Lets a
71
+ * caller register liveness/inbox state under the id before dispatch resolves. */
72
+ taskId?: string;
70
73
  }
71
74
  export interface SubagentPoolOptions {
72
75
  /** Path to the hoocode executable (or the runtime, e.g. node, when prefixArgs is set). */
@@ -180,13 +183,6 @@ export declare class SubagentPool extends EventEmitter {
180
183
  get_status(task_id: string): "running" | "queued" | "done" | "failed" | "stalled" | "timeout" | "unknown";
181
184
  /** Wait for a task to complete and return its result. */
182
185
  wait_for(task_id: string): Promise<SubagentResult>;
183
- /**
184
- * Wait for a task to finish without consuming its result. Returns the result
185
- * once the task reaches a terminal state, or undefined if the task is not
186
- * found. Unlike wait_for(), the result remains in the completed map so
187
- * collect() can still read it afterward.
188
- */
189
- wait_for_completion(task_id: string): Promise<SubagentResult | undefined>;
190
186
  /** Number of currently running subagents. */
191
187
  running_count(): number;
192
188
  /** Number of tasks waiting in the queue. */