{"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,KAAK,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAOvG,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;AAiCD,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\ntype TaskLifecycle = \"running\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" | \"cancelled\" | \"collected\";\n\n/** A non-terminal task is still doing work; the rest have settled. */\nfunction 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\tif (status === \"cancelled\") return \"cancelled\";\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"]}