import { spawn } from "node:child_process"; import { EventEmitter } from "node:events"; import type { Api, Model } from "@kolisachint/hoocode-ai"; import type { Settings } from "./settings-manager.js"; export interface SubagentPoolTask { task_id: string; agent_type: string; task: string; context?: string; token_budget?: number; cwd?: string; model?: string; provider?: string; /** * Explicit session file for the child to persist/continue. When omitted the * child uses its own dispatch dir (`//session.jsonl`). * Resume reuses the original task's session file to continue the transcript. */ sessionFile?: string; /** Internal: retry using the caller's model when a built-in agent's preferred model fails. */ useInheritedModelFallback?: boolean; } export interface SubagentSlot { pid: number; agent_type: string; task_id: string; spawned_at: number; token_budget: number; process: ReturnType; } export interface SubagentResult { task_id: string; ok: boolean; stdout: string; stderr: string; exit_code: number | null; error?: string; /** True when the task exceeded its token budget and was hard-stopped. */ budget_exceeded?: boolean; /** Terminal status derived from how the task finished. */ status?: "complete" | "partial" | "failed" | "stalled" | "timeout" | "cancelled"; /** Parsed result.json content when available (e.g. on partial completion). */ result_data?: Record; /** True when this run used the inherited-model fallback (preferred model failed first). */ usedInheritedModelFallback?: boolean; } export interface TaskResult { /** True when the evaluator decided the task is simple enough for inline handling. */ handled_inline: boolean; /** Present when the task was delegated. */ task_id?: string; agent_type?: string; reason?: string; /** Subagent result when delegated. */ result?: SubagentResult; /** Duration in milliseconds when delegated. */ duration?: number; } export interface DispatchOptions { /** Skip evaluation and force this agent type (user/explicit override). * Accepts any registry-defined agent name, not just the built-in modes. */ forceAgent?: string; /** Context distilled from the calling agent, passed to the subagent. */ context?: string; /** Model id for the subagent (defaults to the child's configured default). */ model?: string; /** Provider for the subagent. */ provider?: string; /** Explicit session file to persist/continue (used by resume). */ sessionFile?: string; /** Caller-supplied task id. Defaults to a generated `dispatch-…` id. Lets a * caller register liveness/inbox state under the id before dispatch resolves. */ taskId?: string; } export interface SubagentPoolOptions { /** Path to the hoocode executable (or the runtime, e.g. node, when prefixArgs is set). */ executable: string; /** Args inserted before task args (e.g. the CLI entry script for node/tsx). */ prefixArgs?: string[]; /** Maximum concurrent child processes. Defaults to 5. */ maxConcurrency?: number; /** Working directory for spawned processes. Defaults to process.cwd(). */ cwd?: string; /** Environment variables. Defaults to process.env. */ env?: NodeJS.ProcessEnv; /** Default token budget per task. Defaults to 0. */ defaultTokenBudget?: number; /** * Non-default skill paths to forward to every spawned subagent via --skill. * Subagents auto-discover skills from standard locations; only paths that * won't be found by default discovery need to be forwarded here. */ skillPaths?: string[]; /** Settings for model category resolution. */ settings?: Settings; /** * Available/configured models used to derive default model-category mappings * when a tier is not explicitly set in `settings.modelCategories`. Snapshotted * at pool creation, mirroring how `settings` is captured. */ availableModels?: readonly Model[]; } /** * Default hard cap on assistant turns for a spawned subagent when its definition * does not set `maxTurns`. The token budget is advisory (it warns but never * kills), so this turn cap is the guaranteed hard stop for every subagent. */ export declare const DEFAULT_SUBAGENT_MAX_TURNS = 50; /** * AgentSession event `type`s forwarded from a subagent's json event stream as * `task_progress` events. Deliberately coarse; the child now also filters its * stdout to this set plus `message_end` at the source (see * SUBAGENT_STDOUT_EVENT_TYPES), so the per-delta firehose no longer crosses the * pipe. Re-exported from the shared module so the child emitter and this * consumer stay in lockstep. */ export declare const FORWARDED_SUBAGENT_EVENTS: ReadonlySet; /** The action the pool should take for one JSONL line from a subagent's stdout. */ export type SubagentStdoutLine = { kind: "heartbeat"; } | { kind: "progress"; event: Record; } | { kind: "ignore"; }; /** * Classify one JSONL line from a subagent's stdout into the action to take. * Pure (no side effects) so the ping/forward/drop policy is unit-testable without * spawning a child. Line framing — UTF-8-safe reassembly of chunks split mid-line * — is handled upstream by attachJsonlLineReader; this only sees complete lines. */ export declare function classifySubagentLine(line: string): SubagentStdoutLine; /** * Pool for running hoocode subagents as child processes with bounded concurrency, * FIFO queuing with priority support, and automatic slot refill. * * Events: * - "task_done" – task completed successfully and output was verified * - "task_failed" – task failed (spawn error, bad exit code, verification failure) * - "task_stalled" – heartbeat missed past the load-scaled threshold (60s base, * widened under concurrency/event-loop lag), process SIGKILLed * - "task_timeout" – hard timeout exceeded, process was SIGKILLed * - "task_cancelled" – user-initiated cancel (see cancel()); process tree killed * - "budget_warning" – token usage crossed 80% threshold (advisory) * - "budget_exceeded" – token usage crossed 100% threshold (advisory; never kills) * - "task_progress" – coarse lifecycle event (turn_end, tool start/end) parsed * from the child's json event stream, for live UI updates */ export declare class SubagentPool extends EventEmitter { private readonly maxConcurrency; private readonly executable; private readonly prefixArgs; private readonly cwd; private readonly env; private readonly defaultTokenBudget; /** Non-default skill paths forwarded to every spawned subagent via --skill. */ private skillPaths; private slots; private queue; private completed; private waiters; private budgets; private verifier; private lifeguard; private disposed; /** Lazily-loaded agent registry (frontmatter definitions) for this pool's cwd. */ private registry?; /** Tracks why a task was killed (stalled / timeout / user cancel) before exit handler fires. */ private killReasons; /** Persistent terminal status map, survives wait_for consumption. */ private taskStatus; /** Settings for model category resolution. */ private readonly settings?; /** Available models used to derive default model-category mappings (snapshot). */ private readonly availableModels; constructor(options: SubagentPoolOptions); /** Update the non-default skill paths forwarded to new subagents. */ updateSkillPaths(paths: string[]): void; /** * Report external in-process load (e.g. the number of background MCP tools * currently executing in the parent) to the lifeguard. This widens its * heartbeat/timeout tolerance so monitored subagents aren't false-positive * reaped when the parent's event loop is busy with concurrent background work. */ setExternalLoad(count: number): void; /** Lazily load the agent registry for this pool's cwd. */ private getRegistry; /** Priority value: higher numbers run first. */ private priorityOf; /** Queue a task. It will run when a slot is free. */ spawn(task: SubagentPoolTask): void; /** Current status of a task. */ get_status(task_id: string): "running" | "queued" | "done" | "failed" | "stalled" | "timeout" | "cancelled" | "unknown"; /** * Cancel a task on the user's behalf (Esc/abort mid-turn). A queued task is * removed and settles immediately; a running task's whole process tree is * killed and settles with status "cancelled" when its exit is observed * (unless it already wrote a valid result.json — completed work is honored). * Returns false for unknown/settled task ids. */ cancel(task_id: string): boolean; /** Wait for a task to complete and return its result. */ wait_for(task_id: string): Promise; /** Number of currently running subagents. */ running_count(): number; /** Number of tasks waiting in the queue. */ queued_count(): number; /** * Dispatch a task through the evaluator. * * - If `options.forceAgent` is provided, skip evaluation and spawn directly. * - Otherwise evaluate the task. If it should be handled inline, return * `{ handled_inline: true }` immediately. * - If delegating, spawn the subagent, wait for completion, write * `output.json`, and return the result. */ dispatch(task: string, options?: DispatchOptions): Promise; /** * Fire-and-forget dispatch for background agents. Spawns the subagent and * returns its handle immediately; the caller polls get_status()/collect(). */ dispatchDetached(task: string, options?: DispatchOptions): { handled_inline: boolean; task_id?: string; agent_type?: string; reason?: string; }; /** * Evaluate, log, and spawn a task without waiting. Shared by dispatch() * (blocking) and dispatchDetached() (background). */ private beginDispatch; /** * Non-destructively read a completed task's result (for background polling). * Returns undefined while the task is still running/queued, or if its result * was already consumed via wait_for(). */ collect(task_id: string): SubagentResult | undefined; /** Absolute path of the persisted session file for a task. */ getSessionFile(task_id: string, cwd?: string): string; /** * Resume a previously dispatched subagent, continuing its persisted session * with a follow-up prompt. Recovers the original agent type from its dispatch * log. Rejects if no resumable session exists for the task. */ resume(task_id: string, prompt: string, options?: Omit): Promise; /** Recover the agent type a task was dispatched with, from its dispatch log. */ private readDispatchAgentType; private writeDispatchLog; private writeOutputJson; /** * Remove a task's dispatch dir after a clean, verified success. Best-effort: * a cleanup failure must never fail an otherwise successful task. */ private cleanupDispatchDir; /** Kill all running processes, clear the queue, and reject pending waiters. */ dispose(): void; /** Pull tasks from the queue while slots are available. */ private pull; /** Build CLI arguments for a task. */ private buildArgs; /** * Environment for a spawned child. * * Stamps the child's depth (parent depth + 1) so its own guard knows where it * sits in the tree; the tree-wide cap (HOOCODE_SUBAGENT_MAX_DEPTH) is inherited * via the spread, so at the default cap of 1 the child lands at depth 1 and * cannot spawn further subagents. Also flags the child to skip MCP server * connection when its tool allowlist is explicit and MCP-free — connecting * external servers it can never call is pure boot latency. */ private childSpawnEnv; /** Start a task in a child process, with one retry on failure. */ private startTask; /** Whether a failed built-in subagent should be retried with `model: inherit`. */ private shouldRetryWithInheritedModel; /** Detect provider/model failures where inheriting the parent model can recover. */ private isInheritedModelFallbackError; /** Remove failed attempt artifacts before rerunning the same task id. */ private cleanupRetryArtifacts; /** * Best-effort concrete failure reason for a non-zero-exit subagent. Prefers * the child's result.json summary (which carries the provider/model error * message on failure), then the tail of stderr, then the exit code. */ private deriveFailureReason; private tryReadResultJson; private resolveWaiter; } //# sourceMappingURL=subagent-pool.d.ts.map