@kolisachint/hoocode-agent 0.4.62 → 0.4.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +44 -1
- package/dist/core/subagent-result.d.ts +31 -0
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +31 -0
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/task-store.d.ts +11 -1
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +3 -0
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +25 -2
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +12 -7
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +90 -57
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +8 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.64] - 2026-06-16
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- bun CI (`bun-check`/`bun-build`) failed on `main` with TS7016/TS7006 errors
|
|
8
|
+
because `bun install --frozen-lockfile` never installed coding-agent's
|
|
9
|
+
`@types/*` devDeps. The workspace `packages/coding-agent` is named
|
|
10
|
+
`@kolisachint/hoocode-agent`, which collided with the root `package.json`
|
|
11
|
+
self-dependency `@kolisachint/hoocode-agent: ^0.2.0`; bun resolved that name
|
|
12
|
+
to the registry package and dropped the local workspace (and its devDeps)
|
|
13
|
+
from `bun.lock`. Removed the unused root self-dependency (root is private and
|
|
14
|
+
`tsconfig.json` already maps the name to the workspace source) and
|
|
15
|
+
regenerated `bun.lock` so the workspace and its `@types/proper-lockfile` /
|
|
16
|
+
`@types/hosted-git-info` devDeps are captured.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `build:bun-binary` script: builds a self-contained standalone executable with
|
|
21
|
+
`bun build --compile` (embeds the Bun runtime; no Node.js/Bun required to run).
|
|
22
|
+
Stages the runtime assets (themes, HTML export templates, docs, examples,
|
|
23
|
+
templates, photon wasm, package.json) next to the executable in
|
|
24
|
+
`dist/bun-binary/`, and supports cross-compilation via `--target` (e.g.
|
|
25
|
+
`bun-linux-x64`, `bun-darwin-arm64`, `bun-windows-x64`).
|
|
26
|
+
|
|
27
|
+
## [0.4.63] - 2026-06-16
|
|
28
|
+
|
|
3
29
|
## [0.4.62] - 2026-06-16
|
|
4
30
|
|
|
5
31
|
### Changed
|
package/README.md
CHANGED
|
@@ -286,7 +286,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
286
286
|
|
|
287
287
|
## Standalone binary (optional)
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
### Bun-compiled binary (recommended)
|
|
290
|
+
|
|
291
|
+
`bun build --compile` produces a self-contained executable that embeds the Bun
|
|
292
|
+
runtime, so end users need neither Node.js nor Bun installed. Requires
|
|
293
|
+
[Bun](https://bun.sh) on the build machine.
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
cd packages/coding-agent
|
|
297
|
+
bun run build:bun-binary
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
This builds the workspace dependencies, compiles the executable, and stages the
|
|
301
|
+
runtime assets next to it. Output goes to `dist/bun-binary/`:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
dist/bun-binary/
|
|
305
|
+
hoocode # the executable
|
|
306
|
+
package.json # read for name/version/config
|
|
307
|
+
README.md, CHANGELOG.md
|
|
308
|
+
photon_rs_bg.wasm # image processing
|
|
309
|
+
theme/ # built-in themes
|
|
310
|
+
export-html/ # HTML export templates
|
|
311
|
+
docs/, examples/, templates/
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Distribute the entire `dist/bun-binary/` directory: the assets must sit next to
|
|
315
|
+
the executable (the runtime resolves them relative to `process.execPath`).
|
|
316
|
+
|
|
317
|
+
Cross-compile for another platform with `--target` (the binary is written to
|
|
318
|
+
`dist/bun-binary/<target>/`):
|
|
319
|
+
|
|
320
|
+
```sh
|
|
321
|
+
bun run build:bun-binary -- --target bun-linux-x64
|
|
322
|
+
bun run build:bun-binary -- --target bun-darwin-arm64
|
|
323
|
+
bun run build:bun-binary -- --target bun-windows-x64
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Supported targets follow Bun's naming: `bun-<os>-<arch>` where `os` is `linux`,
|
|
327
|
+
`darwin`, or `windows` and `arch` is `x64` or `arm64`. Cross-compiled binaries
|
|
328
|
+
are not smoke-tested on the host.
|
|
329
|
+
|
|
330
|
+
### pkg-compiled binary (legacy)
|
|
331
|
+
|
|
332
|
+
Alternatively, install `pkg` to build a Node.js-based self-contained binary:
|
|
290
333
|
|
|
291
334
|
```sh
|
|
292
335
|
npm install -g pkg
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* from the finished session so subagents never have to write it themselves.
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentMessage } from "@kolisachint/hoocode-agent-core";
|
|
10
|
+
import type { Task, TaskSource, TaskStatus } from "./task-store.js";
|
|
10
11
|
export interface SubagentUsage {
|
|
11
12
|
input: number;
|
|
12
13
|
output: number;
|
|
@@ -14,6 +15,23 @@ export interface SubagentUsage {
|
|
|
14
15
|
cacheWrite: number;
|
|
15
16
|
cost: number;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* One node in a subagent's task subtree, propagated to the parent so nested
|
|
20
|
+
* delegation (a subagent that spawns a subagent) is visible above the process
|
|
21
|
+
* boundary. Serialized from the child's task store at session end and merged
|
|
22
|
+
* under the dispatching task by the parent (see finalizeDispatchResult). The
|
|
23
|
+
* `children` are the child's own delegations/MCP calls, recursively.
|
|
24
|
+
*/
|
|
25
|
+
export interface SubagentTaskNode {
|
|
26
|
+
id: number;
|
|
27
|
+
title: string;
|
|
28
|
+
status: TaskStatus;
|
|
29
|
+
/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */
|
|
30
|
+
source?: TaskSource;
|
|
31
|
+
subagentMode?: string;
|
|
32
|
+
usage?: SubagentUsage;
|
|
33
|
+
children: SubagentTaskNode[];
|
|
34
|
+
}
|
|
17
35
|
export interface SubagentResultFile {
|
|
18
36
|
summary: string;
|
|
19
37
|
files_changed: string[];
|
|
@@ -21,7 +39,20 @@ export interface SubagentResultFile {
|
|
|
21
39
|
status: "complete" | "partial" | "failed";
|
|
22
40
|
/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */
|
|
23
41
|
usage?: SubagentUsage;
|
|
42
|
+
/**
|
|
43
|
+
* The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)
|
|
44
|
+
* so the parent can render nested work below the dispatching task. Extra field;
|
|
45
|
+
* ignored by the verifier.
|
|
46
|
+
*/
|
|
47
|
+
task_tree?: SubagentTaskNode[];
|
|
24
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the nested task forest from a flat task list, linking children to
|
|
51
|
+
* parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each
|
|
52
|
+
* node's children preserve store order. Used to serialize a subagent's task
|
|
53
|
+
* store into its `result.json` for the parent to merge.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[];
|
|
25
56
|
/** Extra build options that override the status derived from the transcript. */
|
|
26
57
|
export interface BuildSubagentResultOptions {
|
|
27
58
|
/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-result.d.ts","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"subagent-result.d.ts","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpE,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,oFAAoF;IACpF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC1C,4FAA4F;IAC5F,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,gBAAgB,EAAE,CAsB1E;AAiED,gFAAgF;AAChF,MAAM,WAAW,0BAA0B;IAC1C,iGAAiG;IACjG,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,KAAK,CAAC,EAAE,aAAa,EACrB,OAAO,CAAC,EAAE,0BAA0B,GAClC,kBAAkB,CAkCpB;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAQjG","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\nimport type { Task, TaskSource, TaskStatus } from \"./task-store.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * One node in a subagent's task subtree, propagated to the parent so nested\n * delegation (a subagent that spawns a subagent) is visible above the process\n * boundary. Serialized from the child's task store at session end and merged\n * under the dispatching task by the parent (see finalizeDispatchResult). The\n * `children` are the child's own delegations/MCP calls, recursively.\n */\nexport interface SubagentTaskNode {\n\tid: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tusage?: SubagentUsage;\n\tchildren: SubagentTaskNode[];\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n\t/**\n\t * The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)\n\t * so the parent can render nested work below the dispatching task. Extra field;\n\t * ignored by the verifier.\n\t */\n\ttask_tree?: SubagentTaskNode[];\n}\n\n/**\n * Build the nested task forest from a flat task list, linking children to\n * parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each\n * node's children preserve store order. Used to serialize a subagent's task\n * store into its `result.json` for the parent to merge.\n */\nexport function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[] {\n\tconst childrenByParent = new Map<number, Task[]>();\n\tconst roots: Task[] = [];\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) {\n\t\t\troots.push(task);\n\t\t\tcontinue;\n\t\t}\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst toNode = (task: Task): SubagentTaskNode => ({\n\t\tid: task.id,\n\t\ttitle: task.title,\n\t\tstatus: task.status,\n\t\tsource: task.source,\n\t\tsubagentMode: task.subagentMode,\n\t\tusage: task.usage,\n\t\tchildren: (childrenByParent.get(task.id) ?? []).map(toNode),\n\t});\n\treturn roots.map(toNode);\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}
|
|
@@ -9,6 +9,37 @@
|
|
|
9
9
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { getDispatchTaskDir } from "../config.js";
|
|
12
|
+
/**
|
|
13
|
+
* Build the nested task forest from a flat task list, linking children to
|
|
14
|
+
* parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each
|
|
15
|
+
* node's children preserve store order. Used to serialize a subagent's task
|
|
16
|
+
* store into its `result.json` for the parent to merge.
|
|
17
|
+
*/
|
|
18
|
+
export function buildTaskForest(tasks) {
|
|
19
|
+
const childrenByParent = new Map();
|
|
20
|
+
const roots = [];
|
|
21
|
+
for (const task of tasks) {
|
|
22
|
+
if (task.parentTaskId === undefined) {
|
|
23
|
+
roots.push(task);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const siblings = childrenByParent.get(task.parentTaskId);
|
|
27
|
+
if (siblings)
|
|
28
|
+
siblings.push(task);
|
|
29
|
+
else
|
|
30
|
+
childrenByParent.set(task.parentTaskId, [task]);
|
|
31
|
+
}
|
|
32
|
+
const toNode = (task) => ({
|
|
33
|
+
id: task.id,
|
|
34
|
+
title: task.title,
|
|
35
|
+
status: task.status,
|
|
36
|
+
source: task.source,
|
|
37
|
+
subagentMode: task.subagentMode,
|
|
38
|
+
usage: task.usage,
|
|
39
|
+
children: (childrenByParent.get(task.id) ?? []).map(toNode),
|
|
40
|
+
});
|
|
41
|
+
return roots.map(toNode);
|
|
42
|
+
}
|
|
12
43
|
/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */
|
|
13
44
|
const MUTATING_TOOLS = new Set(["edit", "write"]);
|
|
14
45
|
/** Collect distinct file paths touched by edit/write tool calls across the session. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-result.js","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subagent-result.js","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AA4ClD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAsB,EAAsB;IAC3E,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnD,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,SAAS;QACV,CAAC;QACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACzD,IAAI,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;YAC7B,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,IAAU,EAAoB,EAAE,CAAC,CAAC;QACjD,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;KAC3D,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAAA,CACzB;AAED,yFAAyF;AACzF,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAElD,uFAAuF;AACvF,SAAS,mBAAmB,CAAC,QAAiC,EAAY;IACzE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW;YAAE,SAAS;QAC3C,KAAK,MAAM,OAAO,IAAK,OAA4B,CAAC,OAAO,EAAE,CAAC;YAC7D,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU;gBAAE,SAAS;YAC1C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;gBAAE,SAAS;YAChD,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;YACrC,MAAM,IAAI,GACT,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;YACxG,IAAI,IAAI;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AAAA,CAClB;AAED,oDAAoD;AACpD,SAAS,aAAa,CAAC,QAAiC,EAAU;IACjE,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW;YAAE,SAAS;QAC3C,MAAM,SAAS,GAAG,OAA2B,CAAC;QAC9C,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACzC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;gBAAE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;QACnD,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;IAC7B,CAAC;IACD,OAAO,EAAE,CAAC;AAAA,CACV;AAED,mEAAmE;AACnE,SAAS,YAAY,CAAC,QAAiC,EAAyB;IAC/E,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,IAAI,IAAI,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,IAAwB,CAAC;QAC3C,IAAI,SAAS,CAAC,UAAU,KAAK,OAAO,IAAI,SAAS,CAAC,UAAU,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;IAC7F,CAAC;IACD,OAAO,UAAU,CAAC;AAAA,CAClB;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,QAAiC,EAAsB;IAClF,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,IAAI,IAAI,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,IAAwB,CAAC;QAC3C,IAAI,SAAS,CAAC,UAAU,KAAK,OAAO,IAAI,SAAS,CAAC,YAAY,EAAE,CAAC;YAChE,MAAM,OAAO,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;YAC9C,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC;QAC7B,CAAC;IACF,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAQD;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAClC,QAAiC,EACjC,KAAqB,EACrB,OAAoC,EACf;IACrB,IAAI,OAAO,EAAE,eAAe,EAAE,CAAC;QAC9B,OAAO;YACN,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,uEAAuE;YAC3G,aAAa,EAAE,mBAAmB,CAAC,QAAQ,CAAC;YAC5C,UAAU,EAAE,GAAG;YACf,MAAM,EAAE,SAAS;YACjB,KAAK;SACL,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACzB,yEAAyE;QACzE,2DAA2D;QAC3D,MAAM,YAAY,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,gBAAgB,YAAY,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,cAAc,CAAC;QAC1G,OAAO;YACN,OAAO;YACP,aAAa,EAAE,mBAAmB,CAAC,QAAQ,CAAC;YAC5C,UAAU,EAAE,GAAG;YACf,MAAM;YACN,KAAK;SACL,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,yCAAyC,CAAC;IACrF,OAAO;QACN,OAAO;QACP,aAAa,EAAE,mBAAmB,CAAC,QAAQ,CAAC;QAC5C,UAAU,EAAE,GAAG;QACf,MAAM;QACN,KAAK;KACL,CAAC;AAAA,CACF;AAED,iEAAiE;AACjE,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,MAAc,EAAE,MAA0B,EAAQ;IAClG,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC;IAClE,IAAI,CAAC;QACJ,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACR,4EAA4E;IAC7E,CAAC;AAAA,CACD","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\nimport type { Task, TaskSource, TaskStatus } from \"./task-store.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * One node in a subagent's task subtree, propagated to the parent so nested\n * delegation (a subagent that spawns a subagent) is visible above the process\n * boundary. Serialized from the child's task store at session end and merged\n * under the dispatching task by the parent (see finalizeDispatchResult). The\n * `children` are the child's own delegations/MCP calls, recursively.\n */\nexport interface SubagentTaskNode {\n\tid: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tusage?: SubagentUsage;\n\tchildren: SubagentTaskNode[];\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n\t/**\n\t * The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)\n\t * so the parent can render nested work below the dispatching task. Extra field;\n\t * ignored by the verifier.\n\t */\n\ttask_tree?: SubagentTaskNode[];\n}\n\n/**\n * Build the nested task forest from a flat task list, linking children to\n * parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each\n * node's children preserve store order. Used to serialize a subagent's task\n * store into its `result.json` for the parent to merge.\n */\nexport function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[] {\n\tconst childrenByParent = new Map<number, Task[]>();\n\tconst roots: Task[] = [];\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) {\n\t\t\troots.push(task);\n\t\t\tcontinue;\n\t\t}\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst toNode = (task: Task): SubagentTaskNode => ({\n\t\tid: task.id,\n\t\ttitle: task.title,\n\t\tstatus: task.status,\n\t\tsource: task.source,\n\t\tsubagentMode: task.subagentMode,\n\t\tusage: task.usage,\n\t\tchildren: (childrenByParent.get(task.id) ?? []).map(toNode),\n\t});\n\treturn roots.map(toNode);\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}
|
|
@@ -56,6 +56,15 @@ export interface Task {
|
|
|
56
56
|
subagentMode?: string;
|
|
57
57
|
/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */
|
|
58
58
|
agent?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Id of the task that spawned this one, linking a dispatched subagent (and its
|
|
61
|
+
* own delegations) back to the Task call that created it. Root tasks omit it.
|
|
62
|
+
* Drives the subagents lens's recursive task tree: a node's children are the
|
|
63
|
+
* tasks whose `parentTaskId` is its id, so nesting deeper than one level (a
|
|
64
|
+
* subagent that spawns a subagent) is visible. Set when a child subagent's task
|
|
65
|
+
* subtree is merged into the parent (see finalizeDispatchResult).
|
|
66
|
+
*/
|
|
67
|
+
parentTaskId?: number;
|
|
59
68
|
/**
|
|
60
69
|
* Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent
|
|
61
70
|
* fell back to the inherited model, or was skipped because the provider was
|
|
@@ -77,8 +86,9 @@ export interface CreateTaskOptions {
|
|
|
77
86
|
source?: TaskSource;
|
|
78
87
|
subagentMode?: string;
|
|
79
88
|
agent?: string;
|
|
89
|
+
parentTaskId?: number;
|
|
80
90
|
}
|
|
81
|
-
export type TaskPatch = Partial<Pick<Task, "title" | "status" | "source" | "subagentMode" | "agent" | "usage" | "note">>;
|
|
91
|
+
export type TaskPatch = Partial<Pick<Task, "title" | "status" | "source" | "subagentMode" | "agent" | "usage" | "note" | "parentTaskId">>;
|
|
82
92
|
export type TaskAgentPatch = Partial<Omit<TaskAgent, "id">>;
|
|
83
93
|
/**
|
|
84
94
|
* Owner group for a task when no explicit agent is set: subagent-sourced work
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\" | \"parentTaskId\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
package/dist/core/task-store.js
CHANGED
|
@@ -31,6 +31,7 @@ class TaskStore {
|
|
|
31
31
|
source: options.source,
|
|
32
32
|
subagentMode: options.subagentMode,
|
|
33
33
|
agent: options.agent,
|
|
34
|
+
parentTaskId: options.parentTaskId,
|
|
34
35
|
createdAt: now,
|
|
35
36
|
updatedAt: now,
|
|
36
37
|
};
|
|
@@ -54,6 +55,8 @@ class TaskStore {
|
|
|
54
55
|
task.subagentMode = patch.subagentMode;
|
|
55
56
|
if (patch.agent !== undefined)
|
|
56
57
|
task.agent = patch.agent;
|
|
58
|
+
if (patch.parentTaskId !== undefined)
|
|
59
|
+
task.parentTaskId = patch.parentTaskId;
|
|
57
60
|
if (patch.usage !== undefined)
|
|
58
61
|
task.usage = patch.usage;
|
|
59
62
|
if (patch.note !== undefined)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\" | \"parentTaskId\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAU7D;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAwBrE;AASD;0EAC0E;AAC1E,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAoBvE;AAuBD,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,OAAO,CAAC;CACZ;AAeD,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAsB,GAAG,cAAc,CAqJpF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,GAAG,EAAE,MAAM,GACT,MAAM,GAAG,SAAS,CAQpB;AA8ED;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,cAAc,CAiD/D","sourcesContent":["/**\n * Task tool: delegate a focused task to a specialized subagent.\n *\n * Mirrors the Claude Code `Task` tool. The parent agent decides *when* to\n * delegate based on each agent's `description` (there is no deterministic gate)\n * and selects *which* agent via `subagent_type`. The chosen agent runs in a\n * fresh, isolated child process (SubagentPool) and only its final answer is\n * returned to the parent.\n *\n * It is an optional, opt-in tool (enabled via --enable-subagents or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentDefinition } from \"../agent-frontmatter.js\";\nimport { loadAgentRegistry } from \"../agent-registry.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { getProviderExhaustion } from \"../provider-health.js\";\nimport { SessionManager } from \"../session-manager.js\";\nimport { delegateAllowList, isDelegateAllowed } from \"../subagent-depth.js\";\nimport type { TaskResult } from \"../subagent-pool.js\";\nimport { getSubagentPool } from \"../subagent-pool-instance.js\";\nimport type { SubagentResultFile } from \"../subagent-result.js\";\nimport { taskStore } from \"../task-store.js\";\n\n/**\n * Condense a (possibly multi-line, bulleted) agent description into a single\n * useful one-liner for the agent picker list.\n *\n * Built-in agent descriptions open with a boilerplate header (\"Use this\n * subagent ONLY when:\") followed by \"when to use\" bullets and a \"DO NOT use\"\n * section. Taking the first line alone yields that identical header for every\n * agent, so instead surface the first meaningful bullets (or the first prose\n * line) from the positive \"when to use\" region.\n */\nexport function summarizeAgentDescription(description: string): string {\n\tconst lines = description\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.trim())\n\t\t.filter((line) => line.length > 0);\n\tif (lines.length === 0) return \"\";\n\n\t// Keep only the positive region: everything before a \"DO NOT use\" section.\n\tconst stop = lines.findIndex((line) => /^(do\\s*not|don'?t|avoid)\\b/i.test(line));\n\tconst region = stop === -1 ? lines : lines.slice(0, stop);\n\n\t// Drop a leading header line (e.g. \"Use this subagent ONLY when:\").\n\tconst body = region.length > 1 && region[0]!.endsWith(\":\") ? region.slice(1) : region;\n\n\tconst stripBullet = (line: string) => line.replace(/^[-*\\u2022]\\s+/, \"\").trim();\n\tconst bullets = body\n\t\t.filter((line) => /^[-*\\u2022]\\s+/.test(line))\n\t\t.map(stripBullet)\n\t\t.filter((line) => line.length > 0);\n\n\tconst summary = bullets.length > 0 ? bullets.slice(0, 3).join(\"; \") : (body[0] ?? lines[0] ?? \"\").replace(/:$/, \"\");\n\n\tconst MAX = 200;\n\treturn summary.length > MAX ? `${summary.slice(0, MAX - 1).trimEnd()}\\u2026` : summary;\n}\n\n/** Render the available agents as a \"- name: description\" list for prompts. */\nfunction describeAvailableAgents(cwd: string): string {\n\tconst agents = loadAgentRegistry({ cwd }).list();\n\tif (agents.length === 0) return \"(no agents available)\";\n\treturn agents.map((a) => `- ${a.name}: ${summarizeAgentDescription(a.description)}`).join(\"\\n\");\n}\n\n/** System prompt appendix for the main session when the Task tool is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport function buildTaskMainPrompt(cwd: string = process.cwd()): string {\n\treturn `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer.\n\nAvailable agents (choose one via \\`subagent_type\\`):\n${describeAvailableAgents(cwd)}\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Choose the agent whose description best matches the task.\n- Make every task specific and self-contained. The subagent cannot see this conversation; pass all necessary context (files, constraints, prior findings) in \\`prompt\\`.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.\n- Default to handling small, quick, or single-file work inline; delegate only self-contained units.\n- Some agents are configured to run in the background (non-blocking). For those, 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.\n- To continue a previous subagent (for example one that returned partial results), call Task again with \\`resume_task_id\\` set to its task_id; it resumes with its full prior transcript and \\`prompt\\` is your follow-up.`;\n}\n\nconst taskParams = Type.Object({\n\tdescription: Type.String({\n\t\tdescription: \"A short (3-5 word) description of the task, shown in the task panel.\",\n\t}),\n\tprompt: Type.String({\n\t\tdescription:\n\t\t\t\"The full, self-contained task for the subagent. It cannot see this conversation, so include all needed context, files, and constraints.\",\n\t}),\n\tsubagent_type: Type.String({\n\t\tdescription: \"The name of the specialized agent to delegate to. Must be one of the available agents.\",\n\t}),\n\tresume_task_id: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional. To continue a previous subagent run, pass its task_id (returned by an earlier Task or TaskOutput call). The subagent resumes with its full prior transcript and `prompt` is your follow-up instruction.\",\n\t\t}),\n\t),\n});\n\ntype TaskParams = Static<typeof taskParams>;\n\nexport interface TaskToolDetails {\n\tsubagent_type: string;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** Pool-level task id usable for resume/polling. */\n\tpoolTaskId?: string;\n\t/** True when dispatched as a non-blocking background task. */\n\tbackground?: boolean;\n}\n\nexport interface TaskOutputDetails {\n\ttask_id: string;\n\tstatus: string;\n\tok: boolean;\n}\n\n/**\n * A short, human-readable task name for the task panel: the first line limited\n * to ~8 words so it stays glanceable. A character cap guards a single long word.\n */\nfunction summarize(task: string): string {\n\tconst firstLine = (task.trim().split(\"\\n\")[0] ?? \"\").trim();\n\tif (!firstLine) return \"(task)\";\n\tconst words = firstLine.split(/\\s+/);\n\tlet name = words.length > 8 ? `${words.slice(0, 8).join(\" \")}…` : firstLine;\n\tif (name.length > 60) name = `${name.slice(0, 59)}…`;\n\treturn name;\n}\n\n/** Create the Task tool definition. Registered as a customTool when enabled. */\nexport function createTaskToolDefinition(cwd: string = process.cwd()): ToolDefinition {\n\tconst agentList = describeAvailableAgents(cwd);\n\t// Agents whose definitions opt into background execution. The agent loop reads\n\t// the tool's `background` flag per call and, for these, runs the dispatch\n\t// detached: the parent keeps reasoning and the subagent's answer is injected as\n\t// a follow-up message when it finishes (no polling needed).\n\tconst backgroundAgents = collectBackgroundAgentNames(cwd);\n\treturn defineTool<typeof taskParams, TaskToolDetails>({\n\t\tname: \"Task\",\n\t\tlabel: \"Task\",\n\t\tbackground: (toolCall) => backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? \"\")),\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Select the agent via `subagent_type`; pass everything it needs via `prompt`. The subagent returns only its final answer.\",\n\t\t\t\"Available agents:\",\n\t\t\tagentList,\n\t\t\t\"WHEN TO USE: (1) self-contained work where you only need the final result;\",\n\t\t\t\"(2) parallel investigation/edits without losing your reasoning chain;\",\n\t\t\t\"(3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs);\",\n\t\t\t\"(4) a long command or test suite you want to run without blocking your reasoning.\",\n\t\t\t\"Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.\",\n\t\t\t\"Prefer handling small, quick, or single-file tasks yourself; delegate only self-contained units of work.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"delegate a self-contained task to a specialized subagent (choose via subagent_type)\",\n\t\tparameters: taskParams,\n\n\t\tasync execute(_toolCallId, params: TaskParams, _signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\n\t\t\t// Pre-flight: if the inherited provider recently exhausted its quota (the\n\t\t\t// parent's own turn failed with a usage/rate-limit error that did not\n\t\t\t// recover), skip the spawn. Subagents run on the same provider, so this\n\t\t\t// would only burn another failed attempt. The signal self-expires and is\n\t\t\t// cleared on the next successful response.\n\t\t\tconst provider = ctx.model?.provider;\n\t\t\tconst exhaustion = provider ? getProviderExhaustion(provider) : undefined;\n\t\t\tif (exhaustion) {\n\t\t\t\tconst skipped = taskStore.create(params.description?.trim() || summarize(params.prompt), {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: params.subagent_type,\n\t\t\t\t});\n\t\t\t\ttaskStore.update(skipped.id, { status: \"failed\", note: `${provider} exhausted` });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t`Did not dispatch subagent \"${params.subagent_type}\": the \"${provider}\" provider appears ` +\n\t\t\t\t\t\t\t\t`exhausted or rate-limited (this session just failed with: ${exhaustion.message}). ` +\n\t\t\t\t\t\t\t\t`Subagents run on the same provider, so dispatching would fail too. Wait for the quota to ` +\n\t\t\t\t\t\t\t\t`reset or switch model/provider, then retry — or complete the work directly in this session.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { subagent_type: params.subagent_type, ok: false, taskId: skipped.id },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Scoped delegation: a delegating agent may be restricted to certain subagent\n\t\t\t// types (its `delegate: <types>` frontmatter). The root is unrestricted.\n\t\t\tif (!isDelegateAllowed(params.subagent_type)) {\n\t\t\t\tconst allowed = delegateAllowList()?.join(\", \") ?? \"\";\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`This agent may not delegate to \"${params.subagent_type}\". Allowed subagent types: ${allowed || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Resume path: continue a previously dispatched subagent with a follow-up\n\t\t\t// prompt, reusing its persisted session (full prior transcript).\n\t\t\tconst resumeId = params.resume_task_id?.trim();\n\t\t\tif (resumeId) {\n\t\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: params.subagent_type,\n\t\t\t\t});\n\t\t\t\tregisterSubagentDispatch(params.subagent_type);\n\t\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await pool.resume(resumeId, params.prompt, {\n\t\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t});\n\t\t\t\t\t// The session lives under the original task id; keep it as the resume handle.\n\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, resumeId);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// The model has already decided to delegate and which agent to use; honor\n\t\t\t// it. Validate the requested agent against the registry (no routing gate).\n\t\t\tconst registry = loadAgentRegistry({ cwd: ctx.cwd });\n\t\t\tconst def = registry.get(params.subagent_type);\n\t\t\tif (!def) {\n\t\t\t\tconst available = registry\n\t\t\t\t\t.list()\n\t\t\t\t\t.map((a) => a.name)\n\t\t\t\t\t.join(\", \");\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Unknown subagent_type: \"${params.subagent_type}\". Available agents: ${available || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\tsource: \"subagent\",\n\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\tagent: params.subagent_type,\n\t\t\t});\n\t\t\tregisterSubagentDispatch(params.subagent_type);\n\n\t\t\t// Always dispatch and await the subagent's full result here. Background\n\t\t\t// agents (def.background) are made non-blocking by the agent loop via this\n\t\t\t// tool's `background` flag: the loop runs this execute() detached, answers\n\t\t\t// the call with a placeholder, and injects the answer below as a follow-up\n\t\t\t// message when it resolves. Foreground agents block the turn as usual.\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t// Fork agents inherit the parent's conversation via a forked session.\n\t\t\tconst forkSessionFile = def.fork\n\t\t\t\t? resolveForkSessionFile(def, ctx.sessionManager?.getSessionFile(), ctx.cwd)\n\t\t\t\t: undefined;\n\t\t\ttry {\n\t\t\t\tconst dispatchResult = await pool.dispatch(params.prompt, {\n\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\tcontext: \"\",\n\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\tsessionFile: forkSessionFile,\n\t\t\t\t});\n\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, dispatchResult.task_id);\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst type = args.subagent_type ?? \"agent\";\n\t\t\tconst preview = summarize(args.description ?? args.prompt ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"Agent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${type}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n\n/**\n * For a `fork: true` agent, fork the parent's session so the subagent inherits the\n * full parent conversation (and its prompt cache) instead of starting fresh. Returns\n * the forked session file to dispatch the child with, or undefined to fall back to a\n * fresh session (non-fork agent, no parent session, or an empty/invalid source).\n */\nexport function resolveForkSessionFile(\n\tdef: Pick<AgentDefinition, \"fork\">,\n\tparentSessionPath: string | undefined,\n\tcwd: string,\n): string | undefined {\n\tif (!def.fork || !parentSessionPath) return undefined;\n\ttry {\n\t\treturn SessionManager.forkFrom(parentSessionPath, cwd).getSessionFile();\n\t} catch {\n\t\t// Empty/invalid parent session: fall back to a fresh subagent session.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Register the dispatched agent in the task store's roster so the task pane's\n * grouped views (subagents/teams) can draw a group header for it. Upsert keeps\n * accumulated stats across re-dispatches of the same agent type.\n */\nfunction registerSubagentDispatch(type: string): void {\n\ttaskStore.upsertAgent({ id: type, name: type, role: \"subagent\", kind: \"subagent\", state: \"running\" });\n}\n\n/** Names of agents configured to run in the background (non-blocking). */\nfunction collectBackgroundAgentNames(cwd: string): Set<string> {\n\tconst names = new Set<string>();\n\tfor (const agent of loadAgentRegistry({ cwd }).list()) {\n\t\tif (agent.background) names.add(agent.name);\n\t}\n\treturn names;\n}\n\n/** Extract the final answer from a finished dispatch, updating the task panel. */\nfunction finalizeDispatchResult(\n\tdispatchResult: TaskResult,\n\tsubagentType: string,\n\ttaskStoreId: number,\n\tresumeHandle: string | undefined,\n): { content: Array<{ type: \"text\"; text: string }>; details: TaskToolDetails } {\n\tconst result = dispatchResult.result;\n\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\tconst usage = resultData?.usage;\n\n\t// Roll the agent's per-run usage into its roster stats so the grouped views'\n\t// header carries the agent's own token/cost totals.\n\tif (usage) {\n\t\ttaskStore.addAgentStats(subagentType, { input: usage.input, output: usage.output, cost: usage.cost });\n\t}\n\n\tif (!result || !result.ok) {\n\t\t// Signal failure by throwing: the agent loop derives a tool's error state\n\t\t// from a thrown error, not from a returned flag.\n\t\tconst failNote = result?.usedInheritedModelFallback ? \"inherited-model retry failed\" : undefined;\n\t\ttaskStore.update(taskStoreId, { status: \"failed\", usage, note: failNote });\n\t\ttaskStore.patchAgent(subagentType, { state: \"failed\" });\n\t\tconst reason = result?.error ?? (result?.status ? `subagent ${result.status}` : \"unknown error\");\n\t\tconst stderr = result?.stderr?.trim();\n\t\tthrow new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\\nstderr: ${stderr.slice(-500)}` : \"\"}`);\n\t}\n\n\t// Leave the task in the store with its final status; it stays visible in the\n\t// task panel until the next user message arrives. Surface a ⚠ cue when the run\n\t// fell back to the inherited model rather than emitting a chat message.\n\tconst fallbackNote = dispatchResult.result?.usedInheritedModelFallback ? \"ran on inherited model\" : undefined;\n\ttaskStore.update(taskStoreId, { status: \"done\", usage, note: fallbackNote });\n\t// Parallel dispatches share one roster entry per agent type: stay `running`\n\t// while a sibling task is still live, settle to `done` otherwise.\n\tconst siblingLive = taskStore\n\t\t.list()\n\t\t.some((t) => t.agent === subagentType && (t.status === \"in_progress\" || t.status === \"pending\"));\n\ttaskStore.patchAgent(subagentType, { state: siblingLive ? \"running\" : \"done\" });\n\tlet answer = resultData?.summary || \"(subagent returned no output)\";\n\t// Partial results are resumable; surface the handle so the parent can continue.\n\tif (result.status === \"partial\" && resumeHandle) {\n\t\tanswer += `\\n\\n[Partial result. To continue this subagent, call Task again with resume_task_id=\"${resumeHandle}\".]`;\n\t}\n\treturn {\n\t\tcontent: [{ type: \"text\", text: answer }],\n\t\tdetails: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },\n\t};\n}\n\nconst taskOutputParams = Type.Object({\n\ttask_id: Type.String({\n\t\tdescription: \"The task_id of a background (or previously dispatched) subagent, as returned by the Task tool.\",\n\t}),\n});\n\ntype TaskOutputParams = Static<typeof taskOutputParams>;\n\n/**\n * TaskOutput tool: poll a background subagent and collect its final answer.\n * Returns the current status while running, or the subagent's final answer once\n * complete. Registered alongside the Task tool when subagents are enabled.\n */\nexport function createTaskOutputToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof taskOutputParams, TaskOutputDetails>({\n\t\tname: \"TaskOutput\",\n\t\tlabel: \"TaskOutput\",\n\t\tdescription: [\n\t\t\t\"Check the status of a background subagent and collect its final answer once it finishes.\",\n\t\t\t\"Pass the task_id returned by a background Task call. While the subagent runs this reports its status; once complete it returns only the subagent's final answer.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"check status / collect the result of a background subagent\",\n\t\tparameters: taskOutputParams,\n\n\t\tasync execute(_toolCallId, params: TaskOutputParams, _signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\t\t\tconst status = pool.get_status(params.task_id);\n\t\t\tif (status === \"running\" || status === \"queued\") {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Subagent task \"${params.task_id}\" is ${status}. Call TaskOutput again later to collect its result.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: params.task_id, status, ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = pool.collect(params.task_id);\n\t\t\tif (!result) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No result available for task \"${params.task_id}\" (status: ${status}). It may not exist or its result was already collected.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!result.ok) {\n\t\t\t\tconst reason = result.error ?? (result.status ? `subagent ${result.status}` : status);\n\t\t\t\tthrow new Error(`Background subagent \"${params.task_id}\" failed: ${reason}`);\n\t\t\t}\n\t\t\tconst resultData = result.result_data as SubagentResultFile | undefined;\n\t\t\tconst answer = resultData?.summary || \"(subagent returned no output)\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: answer }],\n\t\t\t\tdetails: { task_id: params.task_id, status: result.status ?? \"complete\", ok: true },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst text = theme.fg(\"toolTitle\", theme.bold(\"TaskOutput \")) + theme.fg(\"dim\", String(args.task_id ?? \"\"));\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAU7D;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAwBrE;AASD;0EAC0E;AAC1E,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAoBvE;AAuBD,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,OAAO,CAAC;CACZ;AAeD,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAsB,GAAG,cAAc,CAqJpF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,GAAG,EAAE,MAAM,GACT,MAAM,GAAG,SAAS,CAQpB;AAsGD;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,cAAc,CAiD/D","sourcesContent":["/**\n * Task tool: delegate a focused task to a specialized subagent.\n *\n * Mirrors the Claude Code `Task` tool. The parent agent decides *when* to\n * delegate based on each agent's `description` (there is no deterministic gate)\n * and selects *which* agent via `subagent_type`. The chosen agent runs in a\n * fresh, isolated child process (SubagentPool) and only its final answer is\n * returned to the parent.\n *\n * It is an optional, opt-in tool (enabled via --enable-subagents or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentDefinition } from \"../agent-frontmatter.js\";\nimport { loadAgentRegistry } from \"../agent-registry.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { getProviderExhaustion } from \"../provider-health.js\";\nimport { SessionManager } from \"../session-manager.js\";\nimport { delegateAllowList, isDelegateAllowed } from \"../subagent-depth.js\";\nimport type { TaskResult } from \"../subagent-pool.js\";\nimport { getSubagentPool } from \"../subagent-pool-instance.js\";\nimport type { SubagentResultFile, SubagentTaskNode } from \"../subagent-result.js\";\nimport { taskStore } from \"../task-store.js\";\n\n/**\n * Condense a (possibly multi-line, bulleted) agent description into a single\n * useful one-liner for the agent picker list.\n *\n * Built-in agent descriptions open with a boilerplate header (\"Use this\n * subagent ONLY when:\") followed by \"when to use\" bullets and a \"DO NOT use\"\n * section. Taking the first line alone yields that identical header for every\n * agent, so instead surface the first meaningful bullets (or the first prose\n * line) from the positive \"when to use\" region.\n */\nexport function summarizeAgentDescription(description: string): string {\n\tconst lines = description\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.trim())\n\t\t.filter((line) => line.length > 0);\n\tif (lines.length === 0) return \"\";\n\n\t// Keep only the positive region: everything before a \"DO NOT use\" section.\n\tconst stop = lines.findIndex((line) => /^(do\\s*not|don'?t|avoid)\\b/i.test(line));\n\tconst region = stop === -1 ? lines : lines.slice(0, stop);\n\n\t// Drop a leading header line (e.g. \"Use this subagent ONLY when:\").\n\tconst body = region.length > 1 && region[0]!.endsWith(\":\") ? region.slice(1) : region;\n\n\tconst stripBullet = (line: string) => line.replace(/^[-*\\u2022]\\s+/, \"\").trim();\n\tconst bullets = body\n\t\t.filter((line) => /^[-*\\u2022]\\s+/.test(line))\n\t\t.map(stripBullet)\n\t\t.filter((line) => line.length > 0);\n\n\tconst summary = bullets.length > 0 ? bullets.slice(0, 3).join(\"; \") : (body[0] ?? lines[0] ?? \"\").replace(/:$/, \"\");\n\n\tconst MAX = 200;\n\treturn summary.length > MAX ? `${summary.slice(0, MAX - 1).trimEnd()}\\u2026` : summary;\n}\n\n/** Render the available agents as a \"- name: description\" list for prompts. */\nfunction describeAvailableAgents(cwd: string): string {\n\tconst agents = loadAgentRegistry({ cwd }).list();\n\tif (agents.length === 0) return \"(no agents available)\";\n\treturn agents.map((a) => `- ${a.name}: ${summarizeAgentDescription(a.description)}`).join(\"\\n\");\n}\n\n/** System prompt appendix for the main session when the Task tool is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport function buildTaskMainPrompt(cwd: string = process.cwd()): string {\n\treturn `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer.\n\nAvailable agents (choose one via \\`subagent_type\\`):\n${describeAvailableAgents(cwd)}\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Choose the agent whose description best matches the task.\n- Make every task specific and self-contained. The subagent cannot see this conversation; pass all necessary context (files, constraints, prior findings) in \\`prompt\\`.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.\n- 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.\n- 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.\n- To continue a previous subagent (for example one that returned partial results), call Task again with \\`resume_task_id\\` set to its task_id; it resumes with its full prior transcript and \\`prompt\\` is your follow-up.`;\n}\n\nconst taskParams = Type.Object({\n\tdescription: Type.String({\n\t\tdescription: \"A short (3-5 word) description of the task, shown in the task panel.\",\n\t}),\n\tprompt: Type.String({\n\t\tdescription:\n\t\t\t\"The full, self-contained task for the subagent. It cannot see this conversation, so include all needed context, files, and constraints.\",\n\t}),\n\tsubagent_type: Type.String({\n\t\tdescription: \"The name of the specialized agent to delegate to. Must be one of the available agents.\",\n\t}),\n\tresume_task_id: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional. To continue a previous subagent run, pass its task_id (returned by an earlier Task or TaskOutput call). The subagent resumes with its full prior transcript and `prompt` is your follow-up instruction.\",\n\t\t}),\n\t),\n});\n\ntype TaskParams = Static<typeof taskParams>;\n\nexport interface TaskToolDetails {\n\tsubagent_type: string;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** Pool-level task id usable for resume/polling. */\n\tpoolTaskId?: string;\n\t/** True when dispatched as a non-blocking background task. */\n\tbackground?: boolean;\n}\n\nexport interface TaskOutputDetails {\n\ttask_id: string;\n\tstatus: string;\n\tok: boolean;\n}\n\n/**\n * A short, human-readable task name for the task panel: the first line limited\n * to ~8 words so it stays glanceable. A character cap guards a single long word.\n */\nfunction summarize(task: string): string {\n\tconst firstLine = (task.trim().split(\"\\n\")[0] ?? \"\").trim();\n\tif (!firstLine) return \"(task)\";\n\tconst words = firstLine.split(/\\s+/);\n\tlet name = words.length > 8 ? `${words.slice(0, 8).join(\" \")}…` : firstLine;\n\tif (name.length > 60) name = `${name.slice(0, 59)}…`;\n\treturn name;\n}\n\n/** Create the Task tool definition. Registered as a customTool when enabled. */\nexport function createTaskToolDefinition(cwd: string = process.cwd()): ToolDefinition {\n\tconst agentList = describeAvailableAgents(cwd);\n\t// Agents whose definitions opt into background execution. The agent loop reads\n\t// the tool's `background` flag per call and, for these, runs the dispatch\n\t// detached: the parent keeps reasoning and the subagent's answer is injected as\n\t// a follow-up message when it finishes (no polling needed).\n\tconst backgroundAgents = collectBackgroundAgentNames(cwd);\n\treturn defineTool<typeof taskParams, TaskToolDetails>({\n\t\tname: \"Task\",\n\t\tlabel: \"Task\",\n\t\tbackground: (toolCall) => backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? \"\")),\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Select the agent via `subagent_type`; pass everything it needs via `prompt`. The subagent returns only its final answer.\",\n\t\t\t\"Available agents:\",\n\t\t\tagentList,\n\t\t\t\"WHEN TO USE: (1) self-contained work where you only need the final result;\",\n\t\t\t\"(2) parallel investigation/edits without losing your reasoning chain;\",\n\t\t\t\"(3) a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs);\",\n\t\t\t\"(4) a long command or test suite you want to run without blocking your reasoning.\",\n\t\t\t\"Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.\",\n\t\t\t\"Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"delegate a self-contained task to a specialized subagent (choose via subagent_type)\",\n\t\tparameters: taskParams,\n\n\t\tasync execute(_toolCallId, params: TaskParams, _signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\n\t\t\t// Pre-flight: if the inherited provider recently exhausted its quota (the\n\t\t\t// parent's own turn failed with a usage/rate-limit error that did not\n\t\t\t// recover), skip the spawn. Subagents run on the same provider, so this\n\t\t\t// would only burn another failed attempt. The signal self-expires and is\n\t\t\t// cleared on the next successful response.\n\t\t\tconst provider = ctx.model?.provider;\n\t\t\tconst exhaustion = provider ? getProviderExhaustion(provider) : undefined;\n\t\t\tif (exhaustion) {\n\t\t\t\tconst skipped = taskStore.create(params.description?.trim() || summarize(params.prompt), {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: params.subagent_type,\n\t\t\t\t});\n\t\t\t\ttaskStore.update(skipped.id, { status: \"failed\", note: `${provider} exhausted` });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t`Did not dispatch subagent \"${params.subagent_type}\": the \"${provider}\" provider appears ` +\n\t\t\t\t\t\t\t\t`exhausted or rate-limited (this session just failed with: ${exhaustion.message}). ` +\n\t\t\t\t\t\t\t\t`Subagents run on the same provider, so dispatching would fail too. Wait for the quota to ` +\n\t\t\t\t\t\t\t\t`reset or switch model/provider, then retry — or complete the work directly in this session.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { subagent_type: params.subagent_type, ok: false, taskId: skipped.id },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Scoped delegation: a delegating agent may be restricted to certain subagent\n\t\t\t// types (its `delegate: <types>` frontmatter). The root is unrestricted.\n\t\t\tif (!isDelegateAllowed(params.subagent_type)) {\n\t\t\t\tconst allowed = delegateAllowList()?.join(\", \") ?? \"\";\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`This agent may not delegate to \"${params.subagent_type}\". Allowed subagent types: ${allowed || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Resume path: continue a previously dispatched subagent with a follow-up\n\t\t\t// prompt, reusing its persisted session (full prior transcript).\n\t\t\tconst resumeId = params.resume_task_id?.trim();\n\t\t\tif (resumeId) {\n\t\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\t\tsource: \"subagent\",\n\t\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\t\tagent: params.subagent_type,\n\t\t\t\t});\n\t\t\t\tregisterSubagentDispatch(params.subagent_type);\n\t\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t\ttry {\n\t\t\t\t\tconst dispatchResult = await pool.resume(resumeId, params.prompt, {\n\t\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\t});\n\t\t\t\t\t// The session lives under the original task id; keep it as the resume handle.\n\t\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, resumeId);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// The model has already decided to delegate and which agent to use; honor\n\t\t\t// it. Validate the requested agent against the registry (no routing gate).\n\t\t\tconst registry = loadAgentRegistry({ cwd: ctx.cwd });\n\t\t\tconst def = registry.get(params.subagent_type);\n\t\t\tif (!def) {\n\t\t\t\tconst available = registry\n\t\t\t\t\t.list()\n\t\t\t\t\t.map((a) => a.name)\n\t\t\t\t\t.join(\", \");\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Unknown subagent_type: \"${params.subagent_type}\". Available agents: ${available || \"(none)\"}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst summary = params.description?.trim() || summarize(params.prompt);\n\t\t\tconst task = taskStore.create(summary, {\n\t\t\t\tsource: \"subagent\",\n\t\t\t\tsubagentMode: params.subagent_type,\n\t\t\t\tagent: params.subagent_type,\n\t\t\t});\n\t\t\tregisterSubagentDispatch(params.subagent_type);\n\n\t\t\t// Always dispatch and await the subagent's full result here. Background\n\t\t\t// agents (def.background) are made non-blocking by the agent loop via this\n\t\t\t// tool's `background` flag: the loop runs this execute() detached, answers\n\t\t\t// the call with a placeholder, and injects the answer below as a follow-up\n\t\t\t// message when it resolves. Foreground agents block the turn as usual.\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\t// Fork agents inherit the parent's conversation via a forked session.\n\t\t\tconst forkSessionFile = def.fork\n\t\t\t\t? resolveForkSessionFile(def, ctx.sessionManager?.getSessionFile(), ctx.cwd)\n\t\t\t\t: undefined;\n\t\t\ttry {\n\t\t\t\tconst dispatchResult = await pool.dispatch(params.prompt, {\n\t\t\t\t\tforceAgent: params.subagent_type,\n\t\t\t\t\tcontext: \"\",\n\t\t\t\t\tmodel: ctx.model?.id,\n\t\t\t\t\tprovider: ctx.model?.provider,\n\t\t\t\t\tsessionFile: forkSessionFile,\n\t\t\t\t});\n\t\t\t\treturn finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, dispatchResult.task_id);\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst type = args.subagent_type ?? \"agent\";\n\t\t\tconst preview = summarize(args.description ?? args.prompt ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"Agent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${type}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n\n/**\n * For a `fork: true` agent, fork the parent's session so the subagent inherits the\n * full parent conversation (and its prompt cache) instead of starting fresh. Returns\n * the forked session file to dispatch the child with, or undefined to fall back to a\n * fresh session (non-fork agent, no parent session, or an empty/invalid source).\n */\nexport function resolveForkSessionFile(\n\tdef: Pick<AgentDefinition, \"fork\">,\n\tparentSessionPath: string | undefined,\n\tcwd: string,\n): string | undefined {\n\tif (!def.fork || !parentSessionPath) return undefined;\n\ttry {\n\t\treturn SessionManager.forkFrom(parentSessionPath, cwd).getSessionFile();\n\t} catch {\n\t\t// Empty/invalid parent session: fall back to a fresh subagent session.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Register the dispatched agent in the task store's roster so the task pane's\n * grouped views (subagents/teams) can draw a group header for it. Upsert keeps\n * accumulated stats across re-dispatches of the same agent type.\n */\nfunction registerSubagentDispatch(type: string): void {\n\ttaskStore.upsertAgent({ id: type, name: type, role: \"subagent\", kind: \"subagent\", state: \"running\" });\n}\n\n/** Names of agents configured to run in the background (non-blocking). */\nfunction collectBackgroundAgentNames(cwd: string): Set<string> {\n\tconst names = new Set<string>();\n\tfor (const agent of loadAgentRegistry({ cwd }).list()) {\n\t\tif (agent.background) names.add(agent.name);\n\t}\n\treturn names;\n}\n\n/**\n * Merge a child subagent's task subtree into the parent's task store, rooting\n * each top-level node under the dispatching task (`parentTaskId`). Recurses so a\n * subagent that itself delegated shows its nested work — the subtree the child\n * could not surface across the process boundary on its own. Each node is its own\n * task (no key-by-type collapse), preserving the order the child created them.\n */\nfunction mergeChildTaskTree(nodes: readonly SubagentTaskNode[] | undefined, parentTaskId: number): void {\n\tif (!nodes) return;\n\tfor (const node of nodes) {\n\t\tconst created = taskStore.create(node.title, {\n\t\t\tsource: node.source,\n\t\t\tsubagentMode: node.subagentMode,\n\t\t\tparentTaskId,\n\t\t});\n\t\ttaskStore.update(created.id, { status: node.status, usage: node.usage });\n\t\tmergeChildTaskTree(node.children, created.id);\n\t}\n}\n\n/** Extract the final answer from a finished dispatch, updating the task panel. */\nfunction finalizeDispatchResult(\n\tdispatchResult: TaskResult,\n\tsubagentType: string,\n\ttaskStoreId: number,\n\tresumeHandle: string | undefined,\n): { content: Array<{ type: \"text\"; text: string }>; details: TaskToolDetails } {\n\tconst result = dispatchResult.result;\n\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\tconst usage = resultData?.usage;\n\n\t// Merge the child's own task subtree under the dispatching task so nested\n\t// delegation (depth >= 2) is visible in the subagents lens's task tree.\n\tmergeChildTaskTree(resultData?.task_tree, taskStoreId);\n\n\t// Roll the agent's per-run usage into its roster stats so the grouped views'\n\t// header carries the agent's own token/cost totals.\n\tif (usage) {\n\t\ttaskStore.addAgentStats(subagentType, { input: usage.input, output: usage.output, cost: usage.cost });\n\t}\n\n\tif (!result || !result.ok) {\n\t\t// Signal failure by throwing: the agent loop derives a tool's error state\n\t\t// from a thrown error, not from a returned flag.\n\t\tconst failNote = result?.usedInheritedModelFallback ? \"inherited-model retry failed\" : undefined;\n\t\ttaskStore.update(taskStoreId, { status: \"failed\", usage, note: failNote });\n\t\ttaskStore.patchAgent(subagentType, { state: \"failed\" });\n\t\tconst reason = result?.error ?? (result?.status ? `subagent ${result.status}` : \"unknown error\");\n\t\tconst stderr = result?.stderr?.trim();\n\t\tthrow new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\\nstderr: ${stderr.slice(-500)}` : \"\"}`);\n\t}\n\n\t// Leave the task in the store with its final status; it stays visible in the\n\t// task panel until the next user message arrives. Surface a ⚠ cue when the run\n\t// fell back to the inherited model rather than emitting a chat message.\n\tconst fallbackNote = dispatchResult.result?.usedInheritedModelFallback ? \"ran on inherited model\" : undefined;\n\ttaskStore.update(taskStoreId, { status: \"done\", usage, note: fallbackNote });\n\t// Parallel dispatches share one roster entry per agent type: stay `running`\n\t// while a sibling task is still live, settle to `done` otherwise.\n\tconst siblingLive = taskStore\n\t\t.list()\n\t\t.some((t) => t.agent === subagentType && (t.status === \"in_progress\" || t.status === \"pending\"));\n\ttaskStore.patchAgent(subagentType, { state: siblingLive ? \"running\" : \"done\" });\n\tlet answer = resultData?.summary || \"(subagent returned no output)\";\n\t// Partial results are resumable; surface the handle so the parent can continue.\n\tif (result.status === \"partial\" && resumeHandle) {\n\t\tanswer += `\\n\\n[Partial result. To continue this subagent, call Task again with resume_task_id=\"${resumeHandle}\".]`;\n\t}\n\treturn {\n\t\tcontent: [{ type: \"text\", text: answer }],\n\t\tdetails: { subagent_type: subagentType, ok: true, taskId: taskStoreId, poolTaskId: resumeHandle },\n\t};\n}\n\nconst taskOutputParams = Type.Object({\n\ttask_id: Type.String({\n\t\tdescription: \"The task_id of a background (or previously dispatched) subagent, as returned by the Task tool.\",\n\t}),\n});\n\ntype TaskOutputParams = Static<typeof taskOutputParams>;\n\n/**\n * TaskOutput tool: poll a background subagent and collect its final answer.\n * Returns the current status while running, or the subagent's final answer once\n * complete. Registered alongside the Task tool when subagents are enabled.\n */\nexport function createTaskOutputToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof taskOutputParams, TaskOutputDetails>({\n\t\tname: \"TaskOutput\",\n\t\tlabel: \"TaskOutput\",\n\t\tdescription: [\n\t\t\t\"Check the status of a background subagent and collect its final answer once it finishes.\",\n\t\t\t\"Pass the task_id returned by a background Task call. While the subagent runs this reports its status; once complete it returns only the subagent's final answer.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"check status / collect the result of a background subagent\",\n\t\tparameters: taskOutputParams,\n\n\t\tasync execute(_toolCallId, params: TaskOutputParams, _signal, _onUpdate, ctx) {\n\t\t\tconst pool = getSubagentPool(ctx.cwd);\n\t\t\tconst status = pool.get_status(params.task_id);\n\t\t\tif (status === \"running\" || status === \"queued\") {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Subagent task \"${params.task_id}\" is ${status}. Call TaskOutput again later to collect its result.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { task_id: params.task_id, status, ok: true },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = pool.collect(params.task_id);\n\t\t\tif (!result) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No result available for task \"${params.task_id}\" (status: ${status}). It may not exist or its result was already collected.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!result.ok) {\n\t\t\t\tconst reason = result.error ?? (result.status ? `subagent ${result.status}` : status);\n\t\t\t\tthrow new Error(`Background subagent \"${params.task_id}\" failed: ${reason}`);\n\t\t\t}\n\t\t\tconst resultData = result.result_data as SubagentResultFile | undefined;\n\t\t\tconst answer = resultData?.summary || \"(subagent returned no output)\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: answer }],\n\t\t\t\tdetails: { task_id: params.task_id, status: result.status ?? \"complete\", ok: true },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst text = theme.fg(\"toolTitle\", theme.bold(\"TaskOutput \")) + theme.fg(\"dim\", String(args.task_id ?? \"\"));\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
|