import type { ModelRegistry } from "../config/model-registry.js"; import type { Settings } from "../config/settings.js"; import { type ThreadId } from "./identity.js"; import type { MainThreadLink } from "./protocol.js"; import type { ThreadService, WorkerPeer } from "./services.js"; /** * Concurrent subagent runs below which every run stays on the main thread. A worker costs * ~100–300 MB and a ~0.2–0.5s boot, while subagents mostly wait on model streams, so a * handful of them share the main thread fine; past this, the main thread (which also * drives the TUI) gets its first helper, and one more per doubling. */ export declare const SUBAGENT_WORKER_THRESHOLD = 9; /** * Worker threads the pool may use with `inFlight` subagent runs main placed (counting the * one being placed): 0 below `threshold`, then one per doubling — [9,18) → 1, [18,36) → 2, * [36,72) → 3, … — capped at `maxWorkers`. */ export declare function allowedSubagentWorkers(inFlight: number, maxWorkers: number, threshold?: number): number; /** Options shared by standalone and pooled workers. */ export interface SubagentWorkerSpawnOptions { /** Bound on start-up (ready handshake + boot); default 30s. */ readonly initTimeoutMs?: number; /** Main-side service hooks to install; defaults to `THREAD_SERVICES`. */ readonly services?: readonly ThreadService[]; /** Tests only: module to load instead of the subagent entry, bypassing the CLI worker host. */ readonly entry?: URL; /** * Pay the worker's one-time first-run costs after boot, before {@link SubagentWorker.ready} * resolves. A failure is logged and leaves the worker usable (the first run pays instead). */ readonly warmUp?: (worker: SubagentWorker) => Promise; } /** Options for one {@link SubagentWorker}. */ export interface SubagentWorkerOptions extends SubagentWorkerSpawnOptions { /** Called once after the worker died and its link closed. */ readonly onExit?: (worker: SubagentWorker, reason: string) => void; /** Called after every run or hold starts or ends, and once the worker booted. */ readonly onLoadChange?: (worker: SubagentWorker) => void; } /** One subagent host worker thread and its RPC link. */ export declare class SubagentWorker implements WorkerPeer { #private; /** Pool-unique id, never reused within the process. */ readonly threadId: ThreadId; /** Link to the worker; closed when the worker dies. */ readonly link: MainThreadLink; /** Resolves once the worker booted every service; rejects (and terminates the worker) on failure. */ readonly ready: Promise; constructor(options?: SubagentWorkerOptions); /** Runs in flight here ({@link beginRun}); what pool placement balances. */ get runs(): number; /** Runs plus resident holds ({@link retain}); a worker at 0 is idle. */ get load(): number; /** Whether the worker finished booting and is still alive. */ get booted(): boolean; /** Whether the worker is still running (starting or booted). */ get alive(): boolean; /** Why the worker died, once it has. */ get exitReason(): string | undefined; /** * Hold the worker for a resident (an open session, a revive): keeps it and the process * alive and its idle shutdown off, without counting for placement. The release fn is idempotent. */ retain(): () => void; /** Count one run in flight here (see {@link retain} for what holding implies); the end fn is idempotent. */ beginRun(): () => void; /** Stop the worker; the link closes and exit hooks run with `reason`. Idempotent. */ terminate(reason?: string): void; } /** Where the pool placed one run; call {@link release} when the run ends. */ export interface SubagentPlacement { /** The worker the run goes to; `undefined` runs it on the main thread. */ readonly worker: SubagentWorker | undefined; /** End the run's count; idempotent. */ release(): void; } /** Options for {@link SubagentWorkerPool}. */ export interface SubagentWorkerPoolOptions { /** Live upper bound on workers, read on every placement (0 keeps every run on the main thread). */ readonly maxWorkers: () => number; /** How long a worker without runs or resident sessions lives on before it terminates; default 60s. */ readonly idleMs?: number; /** Concurrency at which the first worker is allowed; default {@link SUBAGENT_WORKER_THRESHOLD}. */ readonly threshold?: number; /** * Whether the main thread hosts runs; default true. Tests only: `false` places every run * on a worker (at least one worker slot), so they can exercise worker routing serially. */ readonly hostsOnMain?: boolean; /** * Options for every spawned worker. Its `warmUp` runs only on workers started ahead of * demand ({@link SubagentWorkerPool.prewarm}); a worker a placement has to start cold * goes straight to its run, which pays those first-run costs itself. */ readonly worker?: SubagentWorkerSpawnOptions; } /** Main-thread placement of subagent runs over the main thread and a pool of host workers. */ export declare class SubagentWorkerPool { #private; constructor(options: SubagentWorkerPoolOptions); /** Runs in flight that main placed: its own counted runs plus the runs it sent to workers. */ get runsInFlight(): number; /** Runs in flight on the main thread that count for placement. */ get mainRuns(): number; /** * Place one run now, synchronously, so concurrent placements spread out and never * over-spawn: among the main thread and the first {@link allowedSubagentWorkers} worker * slots, the one with the fewest runs in flight, starting a worker for an empty slot. * Ties go to an existing worker over the main thread (which also drives the TUI), and to * the main thread over starting a worker. A worker placement's worker may still be * starting; {@link settle} waits for it. */ place(): SubagentPlacement; /** Wait until `placement`'s worker is ready; a failed start releases the placement and rejects. */ settle(placement: SubagentPlacement): Promise; /** {@link place} then {@link settle}. */ acquire(): Promise; /** * Count a run on the main thread that did not go through {@link place} (a follow-up turn * of a main-homed session), so placement sees the main thread's real load. The end fn is * idempotent. */ beginMainRun(): () => void; /** * `expectedSpawns` spawns are imminent (a parent model is streaming the call): when they * would bring the runs in flight into a bracket whose worker slot is empty, start that * worker now, warmed, in the background (next macrotask, unref'd), so the placement finds * it booted; an idle worker those spawns may use gets its idle countdown restarted. */ prewarm(expectedSpawns?: number): void; /** The live worker with this thread id. */ forThread(id: ThreadId): SubagentWorker | undefined; /** Snapshot of live workers (including ones still starting), in slot order. */ workers(): SubagentWorker[]; /** Observe worker deaths (crash, idle shutdown, dispose); returns an unsubscribe fn. */ onWorkerExit(listener: (worker: SubagentWorker, reason: string) => void): () => void; /** Terminate every worker; later placements throw. */ dispose(): void; } /** * The process-wide pool when worker mode is on: main thread, `task.workerThreads > 0`, * and a CLI worker host (or a pool injected by tests). `undefined` means run subagents in-process. */ export declare function subagentWorkerPool(settings: Settings): SubagentWorkerPool | undefined; /** * A main-thread session is about to spawn `expectedSpawns` subagents (its model is streaming * a spawning tool call): when worker mode is on and they will need a worker that is not * there yet, start it now, warmed with a mirror of `modelRegistry` (the registry its * subagents inherit), so the spawn lands on a booted worker; see * {@link SubagentWorkerPool.prewarm}. A no-op when worker mode is off. */ export declare function prewarmSubagentWorker(settings: Settings, modelRegistry: ModelRegistry, expectedSpawns: number): void; /** * The pool that may already host subagents (the test pool, or the process pool once * worker mode created it), without creating one. Main uses it to reach the home worker * of a worker-homed session. */ export declare function existingSubagentWorkerPool(): SubagentWorkerPool | undefined; /** Tests only: make {@link subagentWorkerPool} return `pool` regardless of settings or worker host; `undefined` restores. */ export declare function setSubagentWorkerPoolForTests(pool: SubagentWorkerPool | undefined): void; /** Smoke probe: spawn and boot one worker, ping through it (async + worker→main sync), terminate. */ export declare function smokeTestSubagentWorker(): Promise;