@kolisachint/hoocode-agent 0.4.78 → 0.4.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts +5 -7
- package/dist/core/agent-frontmatter.d.ts.map +1 -1
- package/dist/core/agent-frontmatter.js +2 -4
- package/dist/core/agent-frontmatter.js.map +1 -1
- package/dist/core/agent-registry.d.ts +12 -1
- package/dist/core/agent-registry.d.ts.map +1 -1
- package/dist/core/agent-registry.js +38 -3
- package/dist/core/agent-registry.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/dispatch-evaluator.d.ts +1 -1
- package/dist/core/dispatch-evaluator.d.ts.map +1 -1
- package/dist/core/dispatch-evaluator.js +1 -1
- package/dist/core/dispatch-evaluator.js.map +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +24 -6
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-categories.d.ts +14 -8
- package/dist/core/model-categories.d.ts.map +1 -1
- package/dist/core/model-categories.js +13 -24
- package/dist/core/model-categories.js.map +1 -1
- package/dist/core/provider-health.d.ts +1 -1
- package/dist/core/provider-health.d.ts.map +1 -1
- package/dist/core/provider-health.js +1 -1
- package/dist/core/provider-health.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +9 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-depth.d.ts +16 -0
- package/dist/core/subagent-depth.d.ts.map +1 -1
- package/dist/core/subagent-depth.js +22 -0
- package/dist/core/subagent-depth.js.map +1 -1
- package/dist/core/subagent-inbox.d.ts +92 -0
- package/dist/core/subagent-inbox.d.ts.map +1 -0
- package/dist/core/subagent-inbox.js +245 -0
- package/dist/core/subagent-inbox.js.map +1 -0
- package/dist/core/subagent-pool.d.ts +14 -7
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +35 -71
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -8
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/task-store.d.ts +6 -5
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +4 -3
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +23 -20
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +357 -163
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/tools/todo.d.ts.map +1 -1
- package/dist/core/tools/todo.js +10 -6
- package/dist/core/tools/todo.js.map +1 -1
- package/dist/core/warm-subagent-pool-instance.d.ts +23 -0
- package/dist/core/warm-subagent-pool-instance.d.ts.map +1 -0
- package/dist/core/warm-subagent-pool-instance.js +53 -0
- package/dist/core/warm-subagent-pool-instance.js.map +1 -0
- package/dist/core/warm-subagent-pool.d.ts +135 -0
- package/dist/core/warm-subagent-pool.d.ts.map +1 -0
- package/dist/core/warm-subagent-pool.js +383 -0
- package/dist/core/warm-subagent-pool.js.map +1 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +7 -0
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +2 -2
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +23 -8
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +26 -3
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +16 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +74 -9
- package/dist/modes/rpc/rpc-client.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 +4 -4
- package/templates/agents/general-purpose.md +1 -1
- package/templates/agents/plan.md +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warm subagent worker pool (experimental, opt-in via `--warm-subagents` /
|
|
3
|
+
* settings.warmSubagents; default off).
|
|
4
|
+
*
|
|
5
|
+
* The cold path (SubagentPool) re-execs the whole CLI for every dispatch, paying
|
|
6
|
+
* a full module-load + resource-graph boot each time. A warm worker instead keeps
|
|
7
|
+
* a long-lived child running in RPC mode and hands it one task at a time over the
|
|
8
|
+
* existing JSON-line protocol: `new_session` resets its conversation between
|
|
9
|
+
* tasks, `prompt` runs the task, and `agent_end` on the event stream signals
|
|
10
|
+
* completion, after which the answer + usage are pulled inline (no result.json
|
|
11
|
+
* disk round-trip, no OutputVerifier). The first task per worker still pays the
|
|
12
|
+
* boot; every reuse after that skips it.
|
|
13
|
+
*
|
|
14
|
+
* Scope/limits (deliberately conservative — see the dispatch integration):
|
|
15
|
+
* - Workers are pinned per agent type: RPC has no per-prompt system-prompt/tools
|
|
16
|
+
* swap, so each (agentType, model, provider) is its own worker config.
|
|
17
|
+
* - Only non-resume, non-fork dispatches are eligible; resume/fork need a
|
|
18
|
+
* persisted/forked session the warm path does not own.
|
|
19
|
+
* - Any worker/infra failure falls back to the cold pool, so enabling this can
|
|
20
|
+
* only change latency, never whether a task can run.
|
|
21
|
+
*/
|
|
22
|
+
import { type AgentRegistry } from "./agent-registry.js";
|
|
23
|
+
import type { Settings } from "./settings-manager.js";
|
|
24
|
+
/** Usage totals pulled from a worker after a task, shaped like SubagentResultFile.usage. */
|
|
25
|
+
export interface WarmUsage {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
cacheWrite: number;
|
|
30
|
+
cost: number;
|
|
31
|
+
}
|
|
32
|
+
/** Outcome of running one task on a warm worker. */
|
|
33
|
+
export interface WarmRunResult {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
status: "complete" | "failed";
|
|
36
|
+
/** The subagent's final assistant text (its answer to the caller). */
|
|
37
|
+
summary: string;
|
|
38
|
+
usage?: WarmUsage;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Per-dispatch inputs that select and configure a worker. */
|
|
42
|
+
export interface WarmDispatchOptions {
|
|
43
|
+
agentType: string;
|
|
44
|
+
cwd: string;
|
|
45
|
+
/** Model id or category (fast/standard/capable); resolved to a concrete id. */
|
|
46
|
+
model?: string;
|
|
47
|
+
provider?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Reports the tool a warm worker is currently running ("" = idle between tools). */
|
|
50
|
+
export type WarmProgressCallback = (activity: string) => void;
|
|
51
|
+
/** A run failed at the infrastructure level (worker crash, timeout, protocol error). */
|
|
52
|
+
export declare class WarmWorkerError extends Error {
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One long-lived RPC child pinned to a single agent-type configuration. Reused
|
|
56
|
+
* across tasks via reset(); a crash makes it not-alive so the pool discards it.
|
|
57
|
+
*/
|
|
58
|
+
export declare class WarmSubagentWorker {
|
|
59
|
+
readonly key: string;
|
|
60
|
+
private readonly env;
|
|
61
|
+
private readonly client;
|
|
62
|
+
private alive;
|
|
63
|
+
constructor(key: string, options: WarmDispatchOptions, env: NodeJS.ProcessEnv, registry: AgentRegistry, skillPaths: readonly string[], settings: Settings | undefined,
|
|
64
|
+
/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */
|
|
65
|
+
spawnCommand?: {
|
|
66
|
+
executable: string;
|
|
67
|
+
prefixArgs: string[];
|
|
68
|
+
});
|
|
69
|
+
/** Boot the child. Throws (as WarmWorkerError) if it fails to come up. */
|
|
70
|
+
start(): Promise<void>;
|
|
71
|
+
isAlive(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Run one task to completion and return its answer + usage. Throws
|
|
74
|
+
* WarmWorkerError on an infra failure (crash/timeout/protocol) so the caller
|
|
75
|
+
* can fall back to the cold pool; a task that ran but reported failure returns
|
|
76
|
+
* `{ ok: false }` instead (no fall back — the work was actually done).
|
|
77
|
+
*/
|
|
78
|
+
run(prompt: string, onActivity?: WarmProgressCallback, timeoutMs?: number): Promise<WarmRunResult>;
|
|
79
|
+
/** Forward the child's coarse tool-lifecycle events to an activity callback. */
|
|
80
|
+
private tapProgress;
|
|
81
|
+
/** Reset the worker's conversation so it can take the next task cleanly. */
|
|
82
|
+
reset(): Promise<void>;
|
|
83
|
+
dispose(): Promise<void>;
|
|
84
|
+
private readUsage;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pool of warm workers keyed by agent-type configuration. Hands out an idle
|
|
88
|
+
* worker (or boots a new one up to a per-key cap), and on release resets the
|
|
89
|
+
* worker and returns it to the idle set with an idle-TTL reclaim timer.
|
|
90
|
+
*/
|
|
91
|
+
export declare class WarmSubagentPool {
|
|
92
|
+
private readonly cwd;
|
|
93
|
+
private readonly settings;
|
|
94
|
+
private skillPaths;
|
|
95
|
+
private readonly maxPerKey;
|
|
96
|
+
private readonly idleTtlMs;
|
|
97
|
+
/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */
|
|
98
|
+
private readonly spawnCommand?;
|
|
99
|
+
private idle;
|
|
100
|
+
private reclaimTimers;
|
|
101
|
+
private liveCount;
|
|
102
|
+
private disposed;
|
|
103
|
+
private registry?;
|
|
104
|
+
constructor(cwd: string, settings: Settings | undefined, skillPaths?: string[], maxPerKey?: number, idleTtlMs?: number,
|
|
105
|
+
/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */
|
|
106
|
+
spawnCommand?: {
|
|
107
|
+
executable: string;
|
|
108
|
+
prefixArgs: string[];
|
|
109
|
+
} | undefined);
|
|
110
|
+
updateSkillPaths(paths: string[]): void;
|
|
111
|
+
private getRegistry;
|
|
112
|
+
/** A registry agent is poolable when it is not a fork agent (fork needs a forked session). */
|
|
113
|
+
isPoolable(agentType: string): boolean;
|
|
114
|
+
/** Stable key for one worker configuration. */
|
|
115
|
+
private keyFor;
|
|
116
|
+
/** Environment for a warm child: depth stamp + MCP skip, mirroring SubagentPool.childSpawnEnv. */
|
|
117
|
+
private childEnv;
|
|
118
|
+
/**
|
|
119
|
+
* Run a task on a warm worker end to end: acquire (reuse or boot), run, then
|
|
120
|
+
* release back to the pool. Throws WarmWorkerError on infra failure so the
|
|
121
|
+
* caller can fall back to the cold pool.
|
|
122
|
+
*/
|
|
123
|
+
dispatch(prompt: string, options: WarmDispatchOptions, onActivity?: WarmProgressCallback): Promise<WarmRunResult>;
|
|
124
|
+
private acquire;
|
|
125
|
+
private release;
|
|
126
|
+
private discard;
|
|
127
|
+
private armReclaim;
|
|
128
|
+
private clearReclaim;
|
|
129
|
+
private incLive;
|
|
130
|
+
private decLive;
|
|
131
|
+
/** Number of currently idle (parked) workers — exposed for tests/diagnostics. */
|
|
132
|
+
idleCount(): number;
|
|
133
|
+
dispose(): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=warm-subagent-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warm-subagent-pool.d.ts","sourceRoot":"","sources":["../../src/core/warm-subagent-pool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Warm subagent worker pool (experimental, opt-in via `--warm-subagents` /\n * settings.warmSubagents; default off).\n *\n * The cold path (SubagentPool) re-execs the whole CLI for every dispatch, paying\n * a full module-load + resource-graph boot each time. A warm worker instead keeps\n * a long-lived child running in RPC mode and hands it one task at a time over the\n * existing JSON-line protocol: `new_session` resets its conversation between\n * tasks, `prompt` runs the task, and `agent_end` on the event stream signals\n * completion, after which the answer + usage are pulled inline (no result.json\n * disk round-trip, no OutputVerifier). The first task per worker still pays the\n * boot; every reuse after that skips it.\n *\n * Scope/limits (deliberately conservative — see the dispatch integration):\n * - Workers are pinned per agent type: RPC has no per-prompt system-prompt/tools\n * swap, so each (agentType, model, provider) is its own worker config.\n * - Only non-resume, non-fork dispatches are eligible; resume/fork need a\n * persisted/forked session the warm path does not own.\n * - Any worker/infra failure falls back to the cold pool, so enabling this can\n * only change latency, never whether a task can run.\n */\n\nimport type { AgentEvent } from \"@kolisachint/hoocode-agent-core\";\nimport { getSubagentSpawnCommand } from \"../config.js\";\nimport { RpcClient } from \"../modes/rpc/rpc-client.js\";\nimport { MODEL_INHERIT } from \"./agent-frontmatter.js\";\nimport { type AgentRegistry, loadAgentRegistry } from \"./agent-registry.js\";\nimport { resolveModelReference } from \"./model-categories.js\";\nimport type { Settings } from \"./settings-manager.js\";\nimport {\n\tcurrentSubagentDepth,\n\tresolveMaxSubagentDepth,\n\tSUBAGENT_DEPTH_ENV,\n\tSUBAGENT_SKIP_MCP_ENV,\n\ttoolAllowlistNeedsMcp,\n} from \"./subagent-depth.js\";\nimport { DEFAULT_SUBAGENT_MAX_TURNS } from \"./subagent-pool.js\";\n\n/** Usage totals pulled from a worker after a task, shaped like SubagentResultFile.usage. */\nexport interface WarmUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/** Outcome of running one task on a warm worker. */\nexport interface WarmRunResult {\n\tok: boolean;\n\tstatus: \"complete\" | \"failed\";\n\t/** The subagent's final assistant text (its answer to the caller). */\n\tsummary: string;\n\tusage?: WarmUsage;\n\terror?: string;\n}\n\n/** Per-dispatch inputs that select and configure a worker. */\nexport interface WarmDispatchOptions {\n\tagentType: string;\n\tcwd: string;\n\t/** Model id or category (fast/standard/capable); resolved to a concrete id. */\n\tmodel?: string;\n\tprovider?: string;\n}\n\n/** Reports the tool a warm worker is currently running (\"\" = idle between tools). */\nexport type WarmProgressCallback = (activity: string) => void;\n\n/** A run failed at the infrastructure level (worker crash, timeout, protocol error). */\nexport class WarmWorkerError extends Error {}\n\n/** Default per-task wait before declaring a warm run stalled and failing over to cold. */\nconst WARM_RUN_TIMEOUT_MS = 180_000;\n\n/**\n * One long-lived RPC child pinned to a single agent-type configuration. Reused\n * across tasks via reset(); a crash makes it not-alive so the pool discards it.\n */\nexport class WarmSubagentWorker {\n\tprivate readonly client: RpcClient;\n\tprivate alive = true;\n\n\tconstructor(\n\t\treadonly key: string,\n\t\toptions: WarmDispatchOptions,\n\t\tprivate readonly env: NodeJS.ProcessEnv,\n\t\tregistry: AgentRegistry,\n\t\tskillPaths: readonly string[],\n\t\tsettings: Settings | undefined,\n\t\t/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */\n\t\tspawnCommand?: { executable: string; prefixArgs: string[] },\n\t) {\n\t\tconst { executable, prefixArgs } = spawnCommand ?? getSubagentSpawnCommand();\n\t\tthis.client = new RpcClient({\n\t\t\texecutable,\n\t\t\tprefixArgs,\n\t\t\tcwd: options.cwd,\n\t\t\tenv: this.env as Record<string, string>,\n\t\t\targs: buildWorkerArgs(options, registry, skillPaths, settings),\n\t\t});\n\t}\n\n\t/** Boot the child. Throws (as WarmWorkerError) if it fails to come up. */\n\tasync start(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.start();\n\t\t} catch (error) {\n\t\t\tthis.alive = false;\n\t\t\tthrow new WarmWorkerError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tisAlive(): boolean {\n\t\treturn this.alive;\n\t}\n\n\t/**\n\t * Run one task to completion and return its answer + usage. Throws\n\t * WarmWorkerError on an infra failure (crash/timeout/protocol) so the caller\n\t * can fall back to the cold pool; a task that ran but reported failure returns\n\t * `{ ok: false }` instead (no fall back — the work was actually done).\n\t */\n\tasync run(\n\t\tprompt: string,\n\t\tonActivity?: WarmProgressCallback,\n\t\ttimeoutMs = WARM_RUN_TIMEOUT_MS,\n\t): Promise<WarmRunResult> {\n\t\tif (!this.alive) throw new WarmWorkerError(\"worker is not alive\");\n\t\t// Mirror the cold pool's coarse progress: report the tool the child is\n\t\t// currently running so a warm dispatch's task row reads \"⋯ grep\" rather than a\n\t\t// static \"running…\". Cleared between tools and at turn end.\n\t\tconst detachProgress = onActivity ? this.tapProgress(onActivity) : undefined;\n\t\ttry {\n\t\t\tconst events = await this.client.promptAndWait(prompt, undefined, timeoutMs);\n\t\t\tconst failure = firstTurnError(events);\n\t\t\tconst summary = (await this.client.getLastAssistantText()) ?? \"\";\n\t\t\tconst usage = await this.readUsage();\n\t\t\tif (failure) {\n\t\t\t\treturn { ok: false, status: \"failed\", summary, usage, error: failure };\n\t\t\t}\n\t\t\treturn { ok: true, status: \"complete\", summary, usage };\n\t\t} catch (error) {\n\t\t\t// A prompt/await failure means the child is no longer trustworthy: mark it\n\t\t\t// dead so the pool discards it, and signal infra failure for fallback.\n\t\t\tthis.alive = false;\n\t\t\tthrow new WarmWorkerError(error instanceof Error ? error.message : String(error));\n\t\t} finally {\n\t\t\tdetachProgress?.();\n\t\t\tonActivity?.(\"\");\n\t\t}\n\t}\n\n\t/** Forward the child's coarse tool-lifecycle events to an activity callback. */\n\tprivate tapProgress(onActivity: WarmProgressCallback): () => void {\n\t\treturn this.client.onEvent((event) => {\n\t\t\tconst e = event as { type?: string; toolName?: string };\n\t\t\tif (e.type === \"tool_execution_start\") onActivity(typeof e.toolName === \"string\" ? e.toolName : \"\");\n\t\t\telse if (e.type === \"tool_execution_end\" || e.type === \"turn_end\") onActivity(\"\");\n\t\t});\n\t}\n\n\t/** Reset the worker's conversation so it can take the next task cleanly. */\n\tasync reset(): Promise<void> {\n\t\tif (!this.alive) return;\n\t\ttry {\n\t\t\tawait this.client.newSession();\n\t\t} catch (error) {\n\t\t\t// A reset failure leaves the worker in an unknown state — retire it.\n\t\t\tthis.alive = false;\n\t\t\tthrow new WarmWorkerError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tthis.alive = false;\n\t\tawait this.client.stop().catch(() => {});\n\t}\n\n\tprivate async readUsage(): Promise<WarmUsage | undefined> {\n\t\ttry {\n\t\t\tconst stats = await this.client.getSessionStats();\n\t\t\treturn {\n\t\t\t\tinput: stats.tokens.input,\n\t\t\t\toutput: stats.tokens.output,\n\t\t\t\tcacheRead: stats.tokens.cacheRead,\n\t\t\t\tcacheWrite: stats.tokens.cacheWrite,\n\t\t\t\tcost: stats.cost,\n\t\t\t};\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n\n/**\n * Pool of warm workers keyed by agent-type configuration. Hands out an idle\n * worker (or boots a new one up to a per-key cap), and on release resets the\n * worker and returns it to the idle set with an idle-TTL reclaim timer.\n */\nexport class WarmSubagentPool {\n\tprivate idle = new Map<string, WarmSubagentWorker[]>();\n\tprivate reclaimTimers = new Map<WarmSubagentWorker, ReturnType<typeof setTimeout>>();\n\tprivate liveCount = new Map<string, number>();\n\tprivate disposed = false;\n\tprivate registry?: AgentRegistry;\n\n\tconstructor(\n\t\tprivate readonly cwd: string,\n\t\tprivate readonly settings: Settings | undefined,\n\t\tprivate skillPaths: string[] = [],\n\t\tprivate readonly maxPerKey = 2,\n\t\tprivate readonly idleTtlMs = 30_000,\n\t\t/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */\n\t\tprivate readonly spawnCommand?: { executable: string; prefixArgs: string[] },\n\t) {}\n\n\tupdateSkillPaths(paths: string[]): void {\n\t\tthis.skillPaths = [...paths];\n\t}\n\n\tprivate getRegistry(): AgentRegistry {\n\t\tif (!this.registry) this.registry = loadAgentRegistry({ cwd: this.cwd });\n\t\treturn this.registry;\n\t}\n\n\t/** A registry agent is poolable when it is not a fork agent (fork needs a forked session). */\n\tisPoolable(agentType: string): boolean {\n\t\tconst def = this.getRegistry().get(agentType);\n\t\treturn def !== undefined && def.fork !== true;\n\t}\n\n\t/** Stable key for one worker configuration. */\n\tprivate keyFor(options: WarmDispatchOptions): string {\n\t\tconst resolved = options.model ? resolveModelReference(options.model, this.settings) : undefined;\n\t\treturn `${options.agentType}::${resolved ?? \"default\"}::${options.provider ?? \"default\"}`;\n\t}\n\n\t/** Environment for a warm child: depth stamp + MCP skip, mirroring SubagentPool.childSpawnEnv. */\n\tprivate childEnv(agentType: string): NodeJS.ProcessEnv {\n\t\tconst env: NodeJS.ProcessEnv = {\n\t\t\t...process.env,\n\t\t\t[SUBAGENT_DEPTH_ENV]: String(currentSubagentDepth(process.env) + 1),\n\t\t};\n\t\tconst def = this.getRegistry().get(agentType);\n\t\tif (!toolAllowlistNeedsMcp(def?.tools)) env[SUBAGENT_SKIP_MCP_ENV] = \"1\";\n\t\treturn env;\n\t}\n\n\t/**\n\t * Run a task on a warm worker end to end: acquire (reuse or boot), run, then\n\t * release back to the pool. Throws WarmWorkerError on infra failure so the\n\t * caller can fall back to the cold pool.\n\t */\n\tasync dispatch(\n\t\tprompt: string,\n\t\toptions: WarmDispatchOptions,\n\t\tonActivity?: WarmProgressCallback,\n\t): Promise<WarmRunResult> {\n\t\tif (this.disposed) throw new WarmWorkerError(\"warm pool disposed\");\n\t\tconst worker = await this.acquire(options);\n\t\ttry {\n\t\t\tconst result = await worker.run(prompt, onActivity);\n\t\t\tawait this.release(worker);\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\t// Infra failure: the worker already marked itself dead; drop it entirely.\n\t\t\tawait this.discard(worker);\n\t\t\tthrow error instanceof WarmWorkerError ? error : new WarmWorkerError(String(error));\n\t\t}\n\t}\n\n\tprivate async acquire(options: WarmDispatchOptions): Promise<WarmSubagentWorker> {\n\t\tconst key = this.keyFor(options);\n\t\tconst pool = this.idle.get(key);\n\t\twhile (pool && pool.length > 0) {\n\t\t\tconst worker = pool.pop()!;\n\t\t\tthis.clearReclaim(worker);\n\t\t\tif (worker.isAlive()) return worker;\n\t\t\t// Dead idle worker (child exited while parked): drop and try the next.\n\t\t\tthis.decLive(key);\n\t\t\tawait worker.dispose();\n\t\t}\n\n\t\tconst worker = new WarmSubagentWorker(\n\t\t\tkey,\n\t\t\toptions,\n\t\t\tthis.childEnv(options.agentType),\n\t\t\tthis.getRegistry(),\n\t\t\tthis.skillPaths,\n\t\t\tthis.settings,\n\t\t\tthis.spawnCommand,\n\t\t);\n\t\tthis.incLive(key);\n\t\ttry {\n\t\t\tawait worker.start();\n\t\t} catch (error) {\n\t\t\tthis.decLive(key);\n\t\t\tthrow error;\n\t\t}\n\t\treturn worker;\n\t}\n\n\tprivate async release(worker: WarmSubagentWorker): Promise<void> {\n\t\tif (this.disposed || !worker.isAlive()) {\n\t\t\tawait this.discard(worker);\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait worker.reset();\n\t\t} catch {\n\t\t\tawait this.discard(worker);\n\t\t\treturn;\n\t\t}\n\t\tconst key = worker.key;\n\t\tconst pool = this.idle.get(key) ?? [];\n\t\tif (pool.length >= this.maxPerKey) {\n\t\t\tawait this.discard(worker);\n\t\t\treturn;\n\t\t}\n\t\tpool.push(worker);\n\t\tthis.idle.set(key, pool);\n\t\tthis.armReclaim(worker);\n\t}\n\n\tprivate async discard(worker: WarmSubagentWorker): Promise<void> {\n\t\tthis.clearReclaim(worker);\n\t\tthis.decLive(worker.key);\n\t\tawait worker.dispose();\n\t}\n\n\tprivate armReclaim(worker: WarmSubagentWorker): void {\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst pool = this.idle.get(worker.key);\n\t\t\tif (pool) {\n\t\t\t\tconst idx = pool.indexOf(worker);\n\t\t\t\tif (idx !== -1) pool.splice(idx, 1);\n\t\t\t}\n\t\t\tvoid this.discard(worker);\n\t\t}, this.idleTtlMs);\n\t\ttimer.unref?.();\n\t\tthis.reclaimTimers.set(worker, timer);\n\t}\n\n\tprivate clearReclaim(worker: WarmSubagentWorker): void {\n\t\tconst timer = this.reclaimTimers.get(worker);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.reclaimTimers.delete(worker);\n\t\t}\n\t}\n\n\tprivate incLive(key: string): void {\n\t\tthis.liveCount.set(key, (this.liveCount.get(key) ?? 0) + 1);\n\t}\n\n\tprivate decLive(key: string): void {\n\t\tconst n = (this.liveCount.get(key) ?? 1) - 1;\n\t\tif (n <= 0) this.liveCount.delete(key);\n\t\telse this.liveCount.set(key, n);\n\t}\n\n\t/** Number of currently idle (parked) workers — exposed for tests/diagnostics. */\n\tidleCount(): number {\n\t\tlet total = 0;\n\t\tfor (const pool of this.idle.values()) total += pool.length;\n\t\treturn total;\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tconst all: WarmSubagentWorker[] = [];\n\t\tfor (const pool of this.idle.values()) all.push(...pool);\n\t\tthis.idle.clear();\n\t\tfor (const timer of this.reclaimTimers.values()) clearTimeout(timer);\n\t\tthis.reclaimTimers.clear();\n\t\tthis.liveCount.clear();\n\t\tawait Promise.all(all.map((w) => w.dispose()));\n\t}\n}\n\n/**\n * Build the RPC child's CLI args for a warm worker. Mirrors the agent-config\n * portion of SubagentPool.buildArgs (system prompt, tools, model/provider, turn\n * cap, skills) but omits the one-shot `--mode json` / `--session` / `--task-id`\n * bits, since the worker runs persistently in RPC mode and resets via new_session.\n */\nfunction buildWorkerArgs(\n\toptions: WarmDispatchOptions,\n\tregistry: AgentRegistry,\n\tskillPaths: readonly string[],\n\tsettings: Settings | undefined,\n): string[] {\n\tconst args: string[] = [];\n\tconst def = registry.get(options.agentType);\n\n\tif (def?.prompt) args.push(\"--system-prompt\", def.prompt);\n\n\tconst childDepth = currentSubagentDepth(process.env) + 1;\n\tconst canChildDelegate = def?.delegate === true && childDepth < resolveMaxSubagentDepth(undefined, process.env);\n\n\tconst tools = def?.tools ? [...def.tools] : undefined;\n\tif (canChildDelegate && tools) {\n\t\tfor (const t of [\"Task\", \"TaskOutput\"]) if (!tools.includes(t)) tools.push(t);\n\t}\n\tif (tools && tools.length > 0) args.push(\"--tools\", tools.join(\",\"));\n\tif (def?.disallowedTools && def.disallowedTools.length > 0) {\n\t\targs.push(\"--disallowed-tools\", def.disallowedTools.join(\",\"));\n\t}\n\tif (canChildDelegate) {\n\t\targs.push(\"--enable-subagents\");\n\t\tif (def?.delegateTo && def.delegateTo.length > 0) args.push(\"--delegate-allow\", def.delegateTo.join(\",\"));\n\t}\n\n\t// Model precedence matches the cold path: a pinned (non-inherit) agent model\n\t// wins, else the requested model/category, resolved to a concrete id.\n\tconst explicitModel = def?.model && def.model !== MODEL_INHERIT ? def.model : undefined;\n\tconst rawModel = explicitModel ?? options.model;\n\tconst modelToUse = rawModel ? resolveModelReference(rawModel, settings) : undefined;\n\tif (modelToUse) args.push(\"--model\", modelToUse);\n\tif (options.provider) args.push(\"--provider\", options.provider);\n\n\tconst maxTurns = def?.maxTurns && def.maxTurns > 0 ? def.maxTurns : DEFAULT_SUBAGENT_MAX_TURNS;\n\targs.push(\"--max-turns\", String(maxTurns));\n\n\tfor (const skillPath of skillPaths) args.push(\"--skill\", skillPath);\n\n\treturn args;\n}\n\n/**\n * Scan a completed run's events for a turn that ended in error/abort, returning\n * its message (or a generic marker) so the caller can report a task failure\n * without re-reading the transcript. Returns undefined for a clean run.\n */\nfunction firstTurnError(events: readonly AgentEvent[]): string | undefined {\n\tfor (const event of events) {\n\t\tconst e = event as { type?: string; message?: { stopReason?: string; errorMessage?: string } };\n\t\tif (e.type === \"turn_end\" && (e.message?.stopReason === \"error\" || e.message?.stopReason === \"aborted\")) {\n\t\t\treturn e.message.errorMessage || `turn ${e.message.stopReason}`;\n\t\t}\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warm subagent worker pool (experimental, opt-in via `--warm-subagents` /
|
|
3
|
+
* settings.warmSubagents; default off).
|
|
4
|
+
*
|
|
5
|
+
* The cold path (SubagentPool) re-execs the whole CLI for every dispatch, paying
|
|
6
|
+
* a full module-load + resource-graph boot each time. A warm worker instead keeps
|
|
7
|
+
* a long-lived child running in RPC mode and hands it one task at a time over the
|
|
8
|
+
* existing JSON-line protocol: `new_session` resets its conversation between
|
|
9
|
+
* tasks, `prompt` runs the task, and `agent_end` on the event stream signals
|
|
10
|
+
* completion, after which the answer + usage are pulled inline (no result.json
|
|
11
|
+
* disk round-trip, no OutputVerifier). The first task per worker still pays the
|
|
12
|
+
* boot; every reuse after that skips it.
|
|
13
|
+
*
|
|
14
|
+
* Scope/limits (deliberately conservative — see the dispatch integration):
|
|
15
|
+
* - Workers are pinned per agent type: RPC has no per-prompt system-prompt/tools
|
|
16
|
+
* swap, so each (agentType, model, provider) is its own worker config.
|
|
17
|
+
* - Only non-resume, non-fork dispatches are eligible; resume/fork need a
|
|
18
|
+
* persisted/forked session the warm path does not own.
|
|
19
|
+
* - Any worker/infra failure falls back to the cold pool, so enabling this can
|
|
20
|
+
* only change latency, never whether a task can run.
|
|
21
|
+
*/
|
|
22
|
+
import { getSubagentSpawnCommand } from "../config.js";
|
|
23
|
+
import { RpcClient } from "../modes/rpc/rpc-client.js";
|
|
24
|
+
import { MODEL_INHERIT } from "./agent-frontmatter.js";
|
|
25
|
+
import { loadAgentRegistry } from "./agent-registry.js";
|
|
26
|
+
import { resolveModelReference } from "./model-categories.js";
|
|
27
|
+
import { currentSubagentDepth, resolveMaxSubagentDepth, SUBAGENT_DEPTH_ENV, SUBAGENT_SKIP_MCP_ENV, toolAllowlistNeedsMcp, } from "./subagent-depth.js";
|
|
28
|
+
import { DEFAULT_SUBAGENT_MAX_TURNS } from "./subagent-pool.js";
|
|
29
|
+
/** A run failed at the infrastructure level (worker crash, timeout, protocol error). */
|
|
30
|
+
export class WarmWorkerError extends Error {
|
|
31
|
+
}
|
|
32
|
+
/** Default per-task wait before declaring a warm run stalled and failing over to cold. */
|
|
33
|
+
const WARM_RUN_TIMEOUT_MS = 180_000;
|
|
34
|
+
/**
|
|
35
|
+
* One long-lived RPC child pinned to a single agent-type configuration. Reused
|
|
36
|
+
* across tasks via reset(); a crash makes it not-alive so the pool discards it.
|
|
37
|
+
*/
|
|
38
|
+
export class WarmSubagentWorker {
|
|
39
|
+
key;
|
|
40
|
+
env;
|
|
41
|
+
client;
|
|
42
|
+
alive = true;
|
|
43
|
+
constructor(key, options, env, registry, skillPaths, settings,
|
|
44
|
+
/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */
|
|
45
|
+
spawnCommand) {
|
|
46
|
+
this.key = key;
|
|
47
|
+
this.env = env;
|
|
48
|
+
const { executable, prefixArgs } = spawnCommand ?? getSubagentSpawnCommand();
|
|
49
|
+
this.client = new RpcClient({
|
|
50
|
+
executable,
|
|
51
|
+
prefixArgs,
|
|
52
|
+
cwd: options.cwd,
|
|
53
|
+
env: this.env,
|
|
54
|
+
args: buildWorkerArgs(options, registry, skillPaths, settings),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** Boot the child. Throws (as WarmWorkerError) if it fails to come up. */
|
|
58
|
+
async start() {
|
|
59
|
+
try {
|
|
60
|
+
await this.client.start();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.alive = false;
|
|
64
|
+
throw new WarmWorkerError(error instanceof Error ? error.message : String(error));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
isAlive() {
|
|
68
|
+
return this.alive;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run one task to completion and return its answer + usage. Throws
|
|
72
|
+
* WarmWorkerError on an infra failure (crash/timeout/protocol) so the caller
|
|
73
|
+
* can fall back to the cold pool; a task that ran but reported failure returns
|
|
74
|
+
* `{ ok: false }` instead (no fall back — the work was actually done).
|
|
75
|
+
*/
|
|
76
|
+
async run(prompt, onActivity, timeoutMs = WARM_RUN_TIMEOUT_MS) {
|
|
77
|
+
if (!this.alive)
|
|
78
|
+
throw new WarmWorkerError("worker is not alive");
|
|
79
|
+
// Mirror the cold pool's coarse progress: report the tool the child is
|
|
80
|
+
// currently running so a warm dispatch's task row reads "⋯ grep" rather than a
|
|
81
|
+
// static "running…". Cleared between tools and at turn end.
|
|
82
|
+
const detachProgress = onActivity ? this.tapProgress(onActivity) : undefined;
|
|
83
|
+
try {
|
|
84
|
+
const events = await this.client.promptAndWait(prompt, undefined, timeoutMs);
|
|
85
|
+
const failure = firstTurnError(events);
|
|
86
|
+
const summary = (await this.client.getLastAssistantText()) ?? "";
|
|
87
|
+
const usage = await this.readUsage();
|
|
88
|
+
if (failure) {
|
|
89
|
+
return { ok: false, status: "failed", summary, usage, error: failure };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, status: "complete", summary, usage };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// A prompt/await failure means the child is no longer trustworthy: mark it
|
|
95
|
+
// dead so the pool discards it, and signal infra failure for fallback.
|
|
96
|
+
this.alive = false;
|
|
97
|
+
throw new WarmWorkerError(error instanceof Error ? error.message : String(error));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
detachProgress?.();
|
|
101
|
+
onActivity?.("");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Forward the child's coarse tool-lifecycle events to an activity callback. */
|
|
105
|
+
tapProgress(onActivity) {
|
|
106
|
+
return this.client.onEvent((event) => {
|
|
107
|
+
const e = event;
|
|
108
|
+
if (e.type === "tool_execution_start")
|
|
109
|
+
onActivity(typeof e.toolName === "string" ? e.toolName : "");
|
|
110
|
+
else if (e.type === "tool_execution_end" || e.type === "turn_end")
|
|
111
|
+
onActivity("");
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Reset the worker's conversation so it can take the next task cleanly. */
|
|
115
|
+
async reset() {
|
|
116
|
+
if (!this.alive)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
await this.client.newSession();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
// A reset failure leaves the worker in an unknown state — retire it.
|
|
123
|
+
this.alive = false;
|
|
124
|
+
throw new WarmWorkerError(error instanceof Error ? error.message : String(error));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async dispose() {
|
|
128
|
+
this.alive = false;
|
|
129
|
+
await this.client.stop().catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
async readUsage() {
|
|
132
|
+
try {
|
|
133
|
+
const stats = await this.client.getSessionStats();
|
|
134
|
+
return {
|
|
135
|
+
input: stats.tokens.input,
|
|
136
|
+
output: stats.tokens.output,
|
|
137
|
+
cacheRead: stats.tokens.cacheRead,
|
|
138
|
+
cacheWrite: stats.tokens.cacheWrite,
|
|
139
|
+
cost: stats.cost,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Pool of warm workers keyed by agent-type configuration. Hands out an idle
|
|
149
|
+
* worker (or boots a new one up to a per-key cap), and on release resets the
|
|
150
|
+
* worker and returns it to the idle set with an idle-TTL reclaim timer.
|
|
151
|
+
*/
|
|
152
|
+
export class WarmSubagentPool {
|
|
153
|
+
cwd;
|
|
154
|
+
settings;
|
|
155
|
+
skillPaths;
|
|
156
|
+
maxPerKey;
|
|
157
|
+
idleTtlMs;
|
|
158
|
+
spawnCommand;
|
|
159
|
+
idle = new Map();
|
|
160
|
+
reclaimTimers = new Map();
|
|
161
|
+
liveCount = new Map();
|
|
162
|
+
disposed = false;
|
|
163
|
+
registry;
|
|
164
|
+
constructor(cwd, settings, skillPaths = [], maxPerKey = 2, idleTtlMs = 30_000,
|
|
165
|
+
/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */
|
|
166
|
+
spawnCommand) {
|
|
167
|
+
this.cwd = cwd;
|
|
168
|
+
this.settings = settings;
|
|
169
|
+
this.skillPaths = skillPaths;
|
|
170
|
+
this.maxPerKey = maxPerKey;
|
|
171
|
+
this.idleTtlMs = idleTtlMs;
|
|
172
|
+
this.spawnCommand = spawnCommand;
|
|
173
|
+
}
|
|
174
|
+
updateSkillPaths(paths) {
|
|
175
|
+
this.skillPaths = [...paths];
|
|
176
|
+
}
|
|
177
|
+
getRegistry() {
|
|
178
|
+
if (!this.registry)
|
|
179
|
+
this.registry = loadAgentRegistry({ cwd: this.cwd });
|
|
180
|
+
return this.registry;
|
|
181
|
+
}
|
|
182
|
+
/** A registry agent is poolable when it is not a fork agent (fork needs a forked session). */
|
|
183
|
+
isPoolable(agentType) {
|
|
184
|
+
const def = this.getRegistry().get(agentType);
|
|
185
|
+
return def !== undefined && def.fork !== true;
|
|
186
|
+
}
|
|
187
|
+
/** Stable key for one worker configuration. */
|
|
188
|
+
keyFor(options) {
|
|
189
|
+
const resolved = options.model ? resolveModelReference(options.model, this.settings) : undefined;
|
|
190
|
+
return `${options.agentType}::${resolved ?? "default"}::${options.provider ?? "default"}`;
|
|
191
|
+
}
|
|
192
|
+
/** Environment for a warm child: depth stamp + MCP skip, mirroring SubagentPool.childSpawnEnv. */
|
|
193
|
+
childEnv(agentType) {
|
|
194
|
+
const env = {
|
|
195
|
+
...process.env,
|
|
196
|
+
[SUBAGENT_DEPTH_ENV]: String(currentSubagentDepth(process.env) + 1),
|
|
197
|
+
};
|
|
198
|
+
const def = this.getRegistry().get(agentType);
|
|
199
|
+
if (!toolAllowlistNeedsMcp(def?.tools))
|
|
200
|
+
env[SUBAGENT_SKIP_MCP_ENV] = "1";
|
|
201
|
+
return env;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Run a task on a warm worker end to end: acquire (reuse or boot), run, then
|
|
205
|
+
* release back to the pool. Throws WarmWorkerError on infra failure so the
|
|
206
|
+
* caller can fall back to the cold pool.
|
|
207
|
+
*/
|
|
208
|
+
async dispatch(prompt, options, onActivity) {
|
|
209
|
+
if (this.disposed)
|
|
210
|
+
throw new WarmWorkerError("warm pool disposed");
|
|
211
|
+
const worker = await this.acquire(options);
|
|
212
|
+
try {
|
|
213
|
+
const result = await worker.run(prompt, onActivity);
|
|
214
|
+
await this.release(worker);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
// Infra failure: the worker already marked itself dead; drop it entirely.
|
|
219
|
+
await this.discard(worker);
|
|
220
|
+
throw error instanceof WarmWorkerError ? error : new WarmWorkerError(String(error));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async acquire(options) {
|
|
224
|
+
const key = this.keyFor(options);
|
|
225
|
+
const pool = this.idle.get(key);
|
|
226
|
+
while (pool && pool.length > 0) {
|
|
227
|
+
const worker = pool.pop();
|
|
228
|
+
this.clearReclaim(worker);
|
|
229
|
+
if (worker.isAlive())
|
|
230
|
+
return worker;
|
|
231
|
+
// Dead idle worker (child exited while parked): drop and try the next.
|
|
232
|
+
this.decLive(key);
|
|
233
|
+
await worker.dispose();
|
|
234
|
+
}
|
|
235
|
+
const worker = new WarmSubagentWorker(key, options, this.childEnv(options.agentType), this.getRegistry(), this.skillPaths, this.settings, this.spawnCommand);
|
|
236
|
+
this.incLive(key);
|
|
237
|
+
try {
|
|
238
|
+
await worker.start();
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.decLive(key);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
return worker;
|
|
245
|
+
}
|
|
246
|
+
async release(worker) {
|
|
247
|
+
if (this.disposed || !worker.isAlive()) {
|
|
248
|
+
await this.discard(worker);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
await worker.reset();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
await this.discard(worker);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const key = worker.key;
|
|
259
|
+
const pool = this.idle.get(key) ?? [];
|
|
260
|
+
if (pool.length >= this.maxPerKey) {
|
|
261
|
+
await this.discard(worker);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
pool.push(worker);
|
|
265
|
+
this.idle.set(key, pool);
|
|
266
|
+
this.armReclaim(worker);
|
|
267
|
+
}
|
|
268
|
+
async discard(worker) {
|
|
269
|
+
this.clearReclaim(worker);
|
|
270
|
+
this.decLive(worker.key);
|
|
271
|
+
await worker.dispose();
|
|
272
|
+
}
|
|
273
|
+
armReclaim(worker) {
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
const pool = this.idle.get(worker.key);
|
|
276
|
+
if (pool) {
|
|
277
|
+
const idx = pool.indexOf(worker);
|
|
278
|
+
if (idx !== -1)
|
|
279
|
+
pool.splice(idx, 1);
|
|
280
|
+
}
|
|
281
|
+
void this.discard(worker);
|
|
282
|
+
}, this.idleTtlMs);
|
|
283
|
+
timer.unref?.();
|
|
284
|
+
this.reclaimTimers.set(worker, timer);
|
|
285
|
+
}
|
|
286
|
+
clearReclaim(worker) {
|
|
287
|
+
const timer = this.reclaimTimers.get(worker);
|
|
288
|
+
if (timer) {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
this.reclaimTimers.delete(worker);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
incLive(key) {
|
|
294
|
+
this.liveCount.set(key, (this.liveCount.get(key) ?? 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
decLive(key) {
|
|
297
|
+
const n = (this.liveCount.get(key) ?? 1) - 1;
|
|
298
|
+
if (n <= 0)
|
|
299
|
+
this.liveCount.delete(key);
|
|
300
|
+
else
|
|
301
|
+
this.liveCount.set(key, n);
|
|
302
|
+
}
|
|
303
|
+
/** Number of currently idle (parked) workers — exposed for tests/diagnostics. */
|
|
304
|
+
idleCount() {
|
|
305
|
+
let total = 0;
|
|
306
|
+
for (const pool of this.idle.values())
|
|
307
|
+
total += pool.length;
|
|
308
|
+
return total;
|
|
309
|
+
}
|
|
310
|
+
async dispose() {
|
|
311
|
+
if (this.disposed)
|
|
312
|
+
return;
|
|
313
|
+
this.disposed = true;
|
|
314
|
+
const all = [];
|
|
315
|
+
for (const pool of this.idle.values())
|
|
316
|
+
all.push(...pool);
|
|
317
|
+
this.idle.clear();
|
|
318
|
+
for (const timer of this.reclaimTimers.values())
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
this.reclaimTimers.clear();
|
|
321
|
+
this.liveCount.clear();
|
|
322
|
+
await Promise.all(all.map((w) => w.dispose()));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Build the RPC child's CLI args for a warm worker. Mirrors the agent-config
|
|
327
|
+
* portion of SubagentPool.buildArgs (system prompt, tools, model/provider, turn
|
|
328
|
+
* cap, skills) but omits the one-shot `--mode json` / `--session` / `--task-id`
|
|
329
|
+
* bits, since the worker runs persistently in RPC mode and resets via new_session.
|
|
330
|
+
*/
|
|
331
|
+
function buildWorkerArgs(options, registry, skillPaths, settings) {
|
|
332
|
+
const args = [];
|
|
333
|
+
const def = registry.get(options.agentType);
|
|
334
|
+
if (def?.prompt)
|
|
335
|
+
args.push("--system-prompt", def.prompt);
|
|
336
|
+
const childDepth = currentSubagentDepth(process.env) + 1;
|
|
337
|
+
const canChildDelegate = def?.delegate === true && childDepth < resolveMaxSubagentDepth(undefined, process.env);
|
|
338
|
+
const tools = def?.tools ? [...def.tools] : undefined;
|
|
339
|
+
if (canChildDelegate && tools) {
|
|
340
|
+
for (const t of ["Task", "TaskOutput"])
|
|
341
|
+
if (!tools.includes(t))
|
|
342
|
+
tools.push(t);
|
|
343
|
+
}
|
|
344
|
+
if (tools && tools.length > 0)
|
|
345
|
+
args.push("--tools", tools.join(","));
|
|
346
|
+
if (def?.disallowedTools && def.disallowedTools.length > 0) {
|
|
347
|
+
args.push("--disallowed-tools", def.disallowedTools.join(","));
|
|
348
|
+
}
|
|
349
|
+
if (canChildDelegate) {
|
|
350
|
+
args.push("--enable-subagents");
|
|
351
|
+
if (def?.delegateTo && def.delegateTo.length > 0)
|
|
352
|
+
args.push("--delegate-allow", def.delegateTo.join(","));
|
|
353
|
+
}
|
|
354
|
+
// Model precedence matches the cold path: a pinned (non-inherit) agent model
|
|
355
|
+
// wins, else the requested model/category, resolved to a concrete id.
|
|
356
|
+
const explicitModel = def?.model && def.model !== MODEL_INHERIT ? def.model : undefined;
|
|
357
|
+
const rawModel = explicitModel ?? options.model;
|
|
358
|
+
const modelToUse = rawModel ? resolveModelReference(rawModel, settings) : undefined;
|
|
359
|
+
if (modelToUse)
|
|
360
|
+
args.push("--model", modelToUse);
|
|
361
|
+
if (options.provider)
|
|
362
|
+
args.push("--provider", options.provider);
|
|
363
|
+
const maxTurns = def?.maxTurns && def.maxTurns > 0 ? def.maxTurns : DEFAULT_SUBAGENT_MAX_TURNS;
|
|
364
|
+
args.push("--max-turns", String(maxTurns));
|
|
365
|
+
for (const skillPath of skillPaths)
|
|
366
|
+
args.push("--skill", skillPath);
|
|
367
|
+
return args;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Scan a completed run's events for a turn that ended in error/abort, returning
|
|
371
|
+
* its message (or a generic marker) so the caller can report a task failure
|
|
372
|
+
* without re-reading the transcript. Returns undefined for a clean run.
|
|
373
|
+
*/
|
|
374
|
+
function firstTurnError(events) {
|
|
375
|
+
for (const event of events) {
|
|
376
|
+
const e = event;
|
|
377
|
+
if (e.type === "turn_end" && (e.message?.stopReason === "error" || e.message?.stopReason === "aborted")) {
|
|
378
|
+
return e.message.errorMessage || `turn ${e.message.stopReason}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=warm-subagent-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warm-subagent-pool.js","sourceRoot":"","sources":["../../src/core/warm-subagent-pool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Warm subagent worker pool (experimental, opt-in via `--warm-subagents` /\n * settings.warmSubagents; default off).\n *\n * The cold path (SubagentPool) re-execs the whole CLI for every dispatch, paying\n * a full module-load + resource-graph boot each time. A warm worker instead keeps\n * a long-lived child running in RPC mode and hands it one task at a time over the\n * existing JSON-line protocol: `new_session` resets its conversation between\n * tasks, `prompt` runs the task, and `agent_end` on the event stream signals\n * completion, after which the answer + usage are pulled inline (no result.json\n * disk round-trip, no OutputVerifier). The first task per worker still pays the\n * boot; every reuse after that skips it.\n *\n * Scope/limits (deliberately conservative — see the dispatch integration):\n * - Workers are pinned per agent type: RPC has no per-prompt system-prompt/tools\n * swap, so each (agentType, model, provider) is its own worker config.\n * - Only non-resume, non-fork dispatches are eligible; resume/fork need a\n * persisted/forked session the warm path does not own.\n * - Any worker/infra failure falls back to the cold pool, so enabling this can\n * only change latency, never whether a task can run.\n */\n\nimport type { AgentEvent } from \"@kolisachint/hoocode-agent-core\";\nimport { getSubagentSpawnCommand } from \"../config.js\";\nimport { RpcClient } from \"../modes/rpc/rpc-client.js\";\nimport { MODEL_INHERIT } from \"./agent-frontmatter.js\";\nimport { type AgentRegistry, loadAgentRegistry } from \"./agent-registry.js\";\nimport { resolveModelReference } from \"./model-categories.js\";\nimport type { Settings } from \"./settings-manager.js\";\nimport {\n\tcurrentSubagentDepth,\n\tresolveMaxSubagentDepth,\n\tSUBAGENT_DEPTH_ENV,\n\tSUBAGENT_SKIP_MCP_ENV,\n\ttoolAllowlistNeedsMcp,\n} from \"./subagent-depth.js\";\nimport { DEFAULT_SUBAGENT_MAX_TURNS } from \"./subagent-pool.js\";\n\n/** Usage totals pulled from a worker after a task, shaped like SubagentResultFile.usage. */\nexport interface WarmUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/** Outcome of running one task on a warm worker. */\nexport interface WarmRunResult {\n\tok: boolean;\n\tstatus: \"complete\" | \"failed\";\n\t/** The subagent's final assistant text (its answer to the caller). */\n\tsummary: string;\n\tusage?: WarmUsage;\n\terror?: string;\n}\n\n/** Per-dispatch inputs that select and configure a worker. */\nexport interface WarmDispatchOptions {\n\tagentType: string;\n\tcwd: string;\n\t/** Model id or category (fast/standard/capable); resolved to a concrete id. */\n\tmodel?: string;\n\tprovider?: string;\n}\n\n/** Reports the tool a warm worker is currently running (\"\" = idle between tools). */\nexport type WarmProgressCallback = (activity: string) => void;\n\n/** A run failed at the infrastructure level (worker crash, timeout, protocol error). */\nexport class WarmWorkerError extends Error {}\n\n/** Default per-task wait before declaring a warm run stalled and failing over to cold. */\nconst WARM_RUN_TIMEOUT_MS = 180_000;\n\n/**\n * One long-lived RPC child pinned to a single agent-type configuration. Reused\n * across tasks via reset(); a crash makes it not-alive so the pool discards it.\n */\nexport class WarmSubagentWorker {\n\tprivate readonly client: RpcClient;\n\tprivate alive = true;\n\n\tconstructor(\n\t\treadonly key: string,\n\t\toptions: WarmDispatchOptions,\n\t\tprivate readonly env: NodeJS.ProcessEnv,\n\t\tregistry: AgentRegistry,\n\t\tskillPaths: readonly string[],\n\t\tsettings: Settings | undefined,\n\t\t/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */\n\t\tspawnCommand?: { executable: string; prefixArgs: string[] },\n\t) {\n\t\tconst { executable, prefixArgs } = spawnCommand ?? getSubagentSpawnCommand();\n\t\tthis.client = new RpcClient({\n\t\t\texecutable,\n\t\t\tprefixArgs,\n\t\t\tcwd: options.cwd,\n\t\t\tenv: this.env as Record<string, string>,\n\t\t\targs: buildWorkerArgs(options, registry, skillPaths, settings),\n\t\t});\n\t}\n\n\t/** Boot the child. Throws (as WarmWorkerError) if it fails to come up. */\n\tasync start(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.start();\n\t\t} catch (error) {\n\t\t\tthis.alive = false;\n\t\t\tthrow new WarmWorkerError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tisAlive(): boolean {\n\t\treturn this.alive;\n\t}\n\n\t/**\n\t * Run one task to completion and return its answer + usage. Throws\n\t * WarmWorkerError on an infra failure (crash/timeout/protocol) so the caller\n\t * can fall back to the cold pool; a task that ran but reported failure returns\n\t * `{ ok: false }` instead (no fall back — the work was actually done).\n\t */\n\tasync run(\n\t\tprompt: string,\n\t\tonActivity?: WarmProgressCallback,\n\t\ttimeoutMs = WARM_RUN_TIMEOUT_MS,\n\t): Promise<WarmRunResult> {\n\t\tif (!this.alive) throw new WarmWorkerError(\"worker is not alive\");\n\t\t// Mirror the cold pool's coarse progress: report the tool the child is\n\t\t// currently running so a warm dispatch's task row reads \"⋯ grep\" rather than a\n\t\t// static \"running…\". Cleared between tools and at turn end.\n\t\tconst detachProgress = onActivity ? this.tapProgress(onActivity) : undefined;\n\t\ttry {\n\t\t\tconst events = await this.client.promptAndWait(prompt, undefined, timeoutMs);\n\t\t\tconst failure = firstTurnError(events);\n\t\t\tconst summary = (await this.client.getLastAssistantText()) ?? \"\";\n\t\t\tconst usage = await this.readUsage();\n\t\t\tif (failure) {\n\t\t\t\treturn { ok: false, status: \"failed\", summary, usage, error: failure };\n\t\t\t}\n\t\t\treturn { ok: true, status: \"complete\", summary, usage };\n\t\t} catch (error) {\n\t\t\t// A prompt/await failure means the child is no longer trustworthy: mark it\n\t\t\t// dead so the pool discards it, and signal infra failure for fallback.\n\t\t\tthis.alive = false;\n\t\t\tthrow new WarmWorkerError(error instanceof Error ? error.message : String(error));\n\t\t} finally {\n\t\t\tdetachProgress?.();\n\t\t\tonActivity?.(\"\");\n\t\t}\n\t}\n\n\t/** Forward the child's coarse tool-lifecycle events to an activity callback. */\n\tprivate tapProgress(onActivity: WarmProgressCallback): () => void {\n\t\treturn this.client.onEvent((event) => {\n\t\t\tconst e = event as { type?: string; toolName?: string };\n\t\t\tif (e.type === \"tool_execution_start\") onActivity(typeof e.toolName === \"string\" ? e.toolName : \"\");\n\t\t\telse if (e.type === \"tool_execution_end\" || e.type === \"turn_end\") onActivity(\"\");\n\t\t});\n\t}\n\n\t/** Reset the worker's conversation so it can take the next task cleanly. */\n\tasync reset(): Promise<void> {\n\t\tif (!this.alive) return;\n\t\ttry {\n\t\t\tawait this.client.newSession();\n\t\t} catch (error) {\n\t\t\t// A reset failure leaves the worker in an unknown state — retire it.\n\t\t\tthis.alive = false;\n\t\t\tthrow new WarmWorkerError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tthis.alive = false;\n\t\tawait this.client.stop().catch(() => {});\n\t}\n\n\tprivate async readUsage(): Promise<WarmUsage | undefined> {\n\t\ttry {\n\t\t\tconst stats = await this.client.getSessionStats();\n\t\t\treturn {\n\t\t\t\tinput: stats.tokens.input,\n\t\t\t\toutput: stats.tokens.output,\n\t\t\t\tcacheRead: stats.tokens.cacheRead,\n\t\t\t\tcacheWrite: stats.tokens.cacheWrite,\n\t\t\t\tcost: stats.cost,\n\t\t\t};\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n\n/**\n * Pool of warm workers keyed by agent-type configuration. Hands out an idle\n * worker (or boots a new one up to a per-key cap), and on release resets the\n * worker and returns it to the idle set with an idle-TTL reclaim timer.\n */\nexport class WarmSubagentPool {\n\tprivate idle = new Map<string, WarmSubagentWorker[]>();\n\tprivate reclaimTimers = new Map<WarmSubagentWorker, ReturnType<typeof setTimeout>>();\n\tprivate liveCount = new Map<string, number>();\n\tprivate disposed = false;\n\tprivate registry?: AgentRegistry;\n\n\tconstructor(\n\t\tprivate readonly cwd: string,\n\t\tprivate readonly settings: Settings | undefined,\n\t\tprivate skillPaths: string[] = [],\n\t\tprivate readonly maxPerKey = 2,\n\t\tprivate readonly idleTtlMs = 30_000,\n\t\t/** Spawn command override (tests inject a fake RPC child); defaults to the real spawn command. */\n\t\tprivate readonly spawnCommand?: { executable: string; prefixArgs: string[] },\n\t) {}\n\n\tupdateSkillPaths(paths: string[]): void {\n\t\tthis.skillPaths = [...paths];\n\t}\n\n\tprivate getRegistry(): AgentRegistry {\n\t\tif (!this.registry) this.registry = loadAgentRegistry({ cwd: this.cwd });\n\t\treturn this.registry;\n\t}\n\n\t/** A registry agent is poolable when it is not a fork agent (fork needs a forked session). */\n\tisPoolable(agentType: string): boolean {\n\t\tconst def = this.getRegistry().get(agentType);\n\t\treturn def !== undefined && def.fork !== true;\n\t}\n\n\t/** Stable key for one worker configuration. */\n\tprivate keyFor(options: WarmDispatchOptions): string {\n\t\tconst resolved = options.model ? resolveModelReference(options.model, this.settings) : undefined;\n\t\treturn `${options.agentType}::${resolved ?? \"default\"}::${options.provider ?? \"default\"}`;\n\t}\n\n\t/** Environment for a warm child: depth stamp + MCP skip, mirroring SubagentPool.childSpawnEnv. */\n\tprivate childEnv(agentType: string): NodeJS.ProcessEnv {\n\t\tconst env: NodeJS.ProcessEnv = {\n\t\t\t...process.env,\n\t\t\t[SUBAGENT_DEPTH_ENV]: String(currentSubagentDepth(process.env) + 1),\n\t\t};\n\t\tconst def = this.getRegistry().get(agentType);\n\t\tif (!toolAllowlistNeedsMcp(def?.tools)) env[SUBAGENT_SKIP_MCP_ENV] = \"1\";\n\t\treturn env;\n\t}\n\n\t/**\n\t * Run a task on a warm worker end to end: acquire (reuse or boot), run, then\n\t * release back to the pool. Throws WarmWorkerError on infra failure so the\n\t * caller can fall back to the cold pool.\n\t */\n\tasync dispatch(\n\t\tprompt: string,\n\t\toptions: WarmDispatchOptions,\n\t\tonActivity?: WarmProgressCallback,\n\t): Promise<WarmRunResult> {\n\t\tif (this.disposed) throw new WarmWorkerError(\"warm pool disposed\");\n\t\tconst worker = await this.acquire(options);\n\t\ttry {\n\t\t\tconst result = await worker.run(prompt, onActivity);\n\t\t\tawait this.release(worker);\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\t// Infra failure: the worker already marked itself dead; drop it entirely.\n\t\t\tawait this.discard(worker);\n\t\t\tthrow error instanceof WarmWorkerError ? error : new WarmWorkerError(String(error));\n\t\t}\n\t}\n\n\tprivate async acquire(options: WarmDispatchOptions): Promise<WarmSubagentWorker> {\n\t\tconst key = this.keyFor(options);\n\t\tconst pool = this.idle.get(key);\n\t\twhile (pool && pool.length > 0) {\n\t\t\tconst worker = pool.pop()!;\n\t\t\tthis.clearReclaim(worker);\n\t\t\tif (worker.isAlive()) return worker;\n\t\t\t// Dead idle worker (child exited while parked): drop and try the next.\n\t\t\tthis.decLive(key);\n\t\t\tawait worker.dispose();\n\t\t}\n\n\t\tconst worker = new WarmSubagentWorker(\n\t\t\tkey,\n\t\t\toptions,\n\t\t\tthis.childEnv(options.agentType),\n\t\t\tthis.getRegistry(),\n\t\t\tthis.skillPaths,\n\t\t\tthis.settings,\n\t\t\tthis.spawnCommand,\n\t\t);\n\t\tthis.incLive(key);\n\t\ttry {\n\t\t\tawait worker.start();\n\t\t} catch (error) {\n\t\t\tthis.decLive(key);\n\t\t\tthrow error;\n\t\t}\n\t\treturn worker;\n\t}\n\n\tprivate async release(worker: WarmSubagentWorker): Promise<void> {\n\t\tif (this.disposed || !worker.isAlive()) {\n\t\t\tawait this.discard(worker);\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait worker.reset();\n\t\t} catch {\n\t\t\tawait this.discard(worker);\n\t\t\treturn;\n\t\t}\n\t\tconst key = worker.key;\n\t\tconst pool = this.idle.get(key) ?? [];\n\t\tif (pool.length >= this.maxPerKey) {\n\t\t\tawait this.discard(worker);\n\t\t\treturn;\n\t\t}\n\t\tpool.push(worker);\n\t\tthis.idle.set(key, pool);\n\t\tthis.armReclaim(worker);\n\t}\n\n\tprivate async discard(worker: WarmSubagentWorker): Promise<void> {\n\t\tthis.clearReclaim(worker);\n\t\tthis.decLive(worker.key);\n\t\tawait worker.dispose();\n\t}\n\n\tprivate armReclaim(worker: WarmSubagentWorker): void {\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst pool = this.idle.get(worker.key);\n\t\t\tif (pool) {\n\t\t\t\tconst idx = pool.indexOf(worker);\n\t\t\t\tif (idx !== -1) pool.splice(idx, 1);\n\t\t\t}\n\t\t\tvoid this.discard(worker);\n\t\t}, this.idleTtlMs);\n\t\ttimer.unref?.();\n\t\tthis.reclaimTimers.set(worker, timer);\n\t}\n\n\tprivate clearReclaim(worker: WarmSubagentWorker): void {\n\t\tconst timer = this.reclaimTimers.get(worker);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.reclaimTimers.delete(worker);\n\t\t}\n\t}\n\n\tprivate incLive(key: string): void {\n\t\tthis.liveCount.set(key, (this.liveCount.get(key) ?? 0) + 1);\n\t}\n\n\tprivate decLive(key: string): void {\n\t\tconst n = (this.liveCount.get(key) ?? 1) - 1;\n\t\tif (n <= 0) this.liveCount.delete(key);\n\t\telse this.liveCount.set(key, n);\n\t}\n\n\t/** Number of currently idle (parked) workers — exposed for tests/diagnostics. */\n\tidleCount(): number {\n\t\tlet total = 0;\n\t\tfor (const pool of this.idle.values()) total += pool.length;\n\t\treturn total;\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tconst all: WarmSubagentWorker[] = [];\n\t\tfor (const pool of this.idle.values()) all.push(...pool);\n\t\tthis.idle.clear();\n\t\tfor (const timer of this.reclaimTimers.values()) clearTimeout(timer);\n\t\tthis.reclaimTimers.clear();\n\t\tthis.liveCount.clear();\n\t\tawait Promise.all(all.map((w) => w.dispose()));\n\t}\n}\n\n/**\n * Build the RPC child's CLI args for a warm worker. Mirrors the agent-config\n * portion of SubagentPool.buildArgs (system prompt, tools, model/provider, turn\n * cap, skills) but omits the one-shot `--mode json` / `--session` / `--task-id`\n * bits, since the worker runs persistently in RPC mode and resets via new_session.\n */\nfunction buildWorkerArgs(\n\toptions: WarmDispatchOptions,\n\tregistry: AgentRegistry,\n\tskillPaths: readonly string[],\n\tsettings: Settings | undefined,\n): string[] {\n\tconst args: string[] = [];\n\tconst def = registry.get(options.agentType);\n\n\tif (def?.prompt) args.push(\"--system-prompt\", def.prompt);\n\n\tconst childDepth = currentSubagentDepth(process.env) + 1;\n\tconst canChildDelegate = def?.delegate === true && childDepth < resolveMaxSubagentDepth(undefined, process.env);\n\n\tconst tools = def?.tools ? [...def.tools] : undefined;\n\tif (canChildDelegate && tools) {\n\t\tfor (const t of [\"Task\", \"TaskOutput\"]) if (!tools.includes(t)) tools.push(t);\n\t}\n\tif (tools && tools.length > 0) args.push(\"--tools\", tools.join(\",\"));\n\tif (def?.disallowedTools && def.disallowedTools.length > 0) {\n\t\targs.push(\"--disallowed-tools\", def.disallowedTools.join(\",\"));\n\t}\n\tif (canChildDelegate) {\n\t\targs.push(\"--enable-subagents\");\n\t\tif (def?.delegateTo && def.delegateTo.length > 0) args.push(\"--delegate-allow\", def.delegateTo.join(\",\"));\n\t}\n\n\t// Model precedence matches the cold path: a pinned (non-inherit) agent model\n\t// wins, else the requested model/category, resolved to a concrete id.\n\tconst explicitModel = def?.model && def.model !== MODEL_INHERIT ? def.model : undefined;\n\tconst rawModel = explicitModel ?? options.model;\n\tconst modelToUse = rawModel ? resolveModelReference(rawModel, settings) : undefined;\n\tif (modelToUse) args.push(\"--model\", modelToUse);\n\tif (options.provider) args.push(\"--provider\", options.provider);\n\n\tconst maxTurns = def?.maxTurns && def.maxTurns > 0 ? def.maxTurns : DEFAULT_SUBAGENT_MAX_TURNS;\n\targs.push(\"--max-turns\", String(maxTurns));\n\n\tfor (const skillPath of skillPaths) args.push(\"--skill\", skillPath);\n\n\treturn args;\n}\n\n/**\n * Scan a completed run's events for a turn that ended in error/abort, returning\n * its message (or a generic marker) so the caller can report a task failure\n * without re-reading the transcript. Returns undefined for a clean run.\n */\nfunction firstTurnError(events: readonly AgentEvent[]): string | undefined {\n\tfor (const event of events) {\n\t\tconst e = event as { type?: string; message?: { stopReason?: string; errorMessage?: string } };\n\t\tif (e.type === \"turn_end\" && (e.message?.stopReason === \"error\" || e.message?.stopReason === \"aborted\")) {\n\t\t\treturn e.message.errorMessage || `turn ${e.message.stopReason}`;\n\t\t}\n\t}\n\treturn undefined;\n}\n"]}
|