{"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 type { Api, Model } from \"@kolisachint/hoocode-ai\";\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\tDEFER_MCP_SCHEMAS_ENV,\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\tavailableModels: readonly Model<Api>[],\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, availableModels),\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 \"⋯ search\" 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\t/**\n\t\t * Available models used to derive default model-category mappings when a tier\n\t\t * is not explicitly set in `settings.modelCategories` (snapshot at creation).\n\t\t */\n\t\tprivate readonly availableModels: readonly Model<Api>[] = [],\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\n\t\t\t? resolveModelReference(options.model, this.settings, this.availableModels)\n\t\t\t: 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\t// A child never defers MCP schemas: if it needs MCP it eager-registers its\n\t\t// allowlisted tools at dispatch so they are immediately callable (spec §2).\n\t\tdelete env[DEFER_MCP_SCHEMAS_ENV];\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.availableModels,\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\tavailableModels: readonly Model<Api>[],\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, availableModels) : undefined;\n\tif (modelToUse) args.push(\"--model\", modelToUse);\n\t// Only pass --provider when the model doesn't already encode the provider\n\t// (e.g. \"anthropic/claude-sonnet-4-5\"). When both --model and --provider are\n\t// present, --provider wins and filters candidates to that provider, which\n\t// breaks models whose IDs contain a provider prefix from a different provider.\n\tif (options.provider && (!modelToUse || !modelToUse.includes(\"/\"))) 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"]}