import type { Message, Provider, ThinkingLevel } from "@kenkaiiii/gg-ai"; import { type AgentDefinition } from "./agents.js"; import { SubAgentStore } from "./subagent-store.js"; import type { AgentNotificationQueue } from "./agent-notifications.js"; import type { AcceptanceCheck } from "./acceptance-checks.js"; import { type SubAgentTokenUsage } from "../tools/subagent-shared.js"; export type SubAgentState = "starting" | "running" | "completed" | "failed" | "interrupted" | "closed" | "reaped"; export interface SubAgentSnapshot { agent_id: string; task_name: string; state: SubAgentState; started_at: number; updated_at: number; elapsed_ms: number; current_activity?: string; turn_count: number; tool_use_count: number; token_usage: SubAgentTokenUsage; output?: string; /** * Engine-built receipt of the last turn's tool calls plus any paths the * report names that the child never opened. Kept apart from `output` so * wait()'s head truncation can never cut it and output parsers (the ideal * reviewer) see the child's prose unchanged. */ receipt?: string; /** * One-line verdict of the parent's acceptance checks for the last turn * (`2/3 passed, 1 unverified`); the per-check lines ride in `receipt`. */ acceptance?: string; error?: string; agent_name?: string; provider?: string; model?: string; child_session_id?: string; child_session_path?: string; /** Folder the child works in, when it isn't the parent's cwd. */ cwd?: string; collected?: boolean; recovered?: boolean; } /** A provider and model for spawned children (GG Goals' "Critters" pick). */ export interface ChildModelPick { provider: Provider; model: string; } export interface SubAgentManagerOptions { cwd: string; /** * Other folders a child may be started in (the parent's extra workspace * roots). A child's `cwd` must be the parent's cwd, one of these, or inside * one of them; anything else is refused. Read at spawn time. */ getWorkspaceRoots?: () => readonly string[]; /** * Start each child with those roots too (less its own folder), so it can * read and change all of them, not just the folder it works in. */ shareWorkspaceRoots?: boolean; /** * The agent a child is when the spawn names none. Unset, such a child runs * with no agent prompt and the full tool set (GG Coder's default). GG Goals * sets its builder, so no worker ever runs outside its team's rules. */ defaultAgent?: string; agents: AgentDefinition[]; getProvider: () => Provider; getModel: () => string; /** * The provider and model children run on when their agent names no model of * its own (GG Goals' "Critters" pick). Undefined: the parent's. Read at * spawn time, so a change applies to the next worker started. */ getChildModel?: () => ChildModelPick | undefined; getThinkingLevel?: () => ThinkingLevel | undefined; getCacheKey?: () => string | undefined; getBaseUrl?: () => string | undefined; /** Optional per-model concurrency cap (subagentMaxPerModel setting). Counted * against the RESOLVED child model — an agent pinned to its own model id * counts under that model, not the parent's. Only ever * reduces concurrency below the global ACTIVE_LIMIT. */ getMaxPerModel?: () => number | undefined; onState?: (snapshot: SubAgentSnapshot) => void; /** * Push queue for child-completion notifications. When set, a finished child * announces itself into the parent's next turn instead of waiting to be * discovered by `wait_agent` or the pre-stop completion gate. */ notifications?: AgentNotificationQueue; workerEntry?: string; idleTimeoutMs?: number; /** Poll interval for adopting turn records of children orphaned mid-turn. * Test-only knob; production default 5s. */ adoptionPollMs?: number; store?: SubAgentStore; sessionRootDir?: string; } /** Children that may be starting or running at once. */ export declare const ACTIVE_LIMIT = 8; /** * Wait budgets, aligned with the child's own `SUB_AGENT_TIMEOUT_MS` (10 min). * The previous 30s default and 5-min ceiling meant a parent waiting on a * legitimately long child gave up ~20x early and finalized on a half-answer. */ export declare const DEFAULT_WAIT_MS = 120000; export declare const MAX_WAIT_MS: number; /** Shared pre-finalization hook used by both AgentSession and the Ink host. */ export declare function buildSubAgentCompletionFollowUp(manager: Pick | undefined): Message[] | null; export declare class SubAgentManager { private readonly options; private readonly workers; private readonly snapshots; private readonly listeners; private shuttingDown; private shutdownPromise?; /** Background retention started by `hydrate`; never rejects. */ private retention; private readonly store; private readonly sessionRootDir; private parentSessionId?; private persistQueue; /** Bounded watcher for turn records of children orphaned mid-turn. */ private adoptionWatcher?; private adoptionDeadline; private persistPending; private persistScheduled; /** Agent definitions `spawn` resolves names against — the delegation roster. */ /** The agent a child is when its spawn names none (see SubAgentManagerOptions.defaultAgent). */ get defaultAgent(): string | undefined; get agents(): readonly AgentDefinition[]; constructor(options: SubAgentManagerOptions); /** Restore bounded history; dead in-flight workers become honestly interrupted. */ hydrate(parentSessionId: string): Promise; /** Resolves once the retention pass the last `hydrate` started has finished. */ waitForRetention(): Promise; /** * Adopt durable turn records for snapshots that were mid-turn at restart. * A record newer than the parent's last observation of that child is the * child's own terminal verdict — it outranks the pessimistic "interrupted". * Still-running orphans get a bounded watcher: the record usually lands * within one turn timeout, and adoption then notifies the parent's next * turn like any other completion. */ private settleAdoptableTurns; /** Fold a durable turn record into the snapshot and announce the adoption. */ private adoptTurnRecord; /** Rebind durable child history after parent compaction creates a continuation. */ rebindParentSession(parentSessionId: string): Promise; /** A genuinely new parent starts with no unrelated child history. */ resetParentSession(parentSessionId: string): Promise; waitForPersistence(): Promise; /** Explicit per-spawn overrides. Used by harness-owned children (the * independent Ideal reviewer): a read-only tool list and/or a forced model — * bypassing agent-definition routing entirely — and a shorter turn time * limit, after which the child answers from what it has gathered. */ spawn(taskName: string, task: string, agentName?: string, overrides?: { model?: string; tools?: readonly string[]; turnTimeoutMs?: number; checks?: readonly AcceptanceCheck[]; /** Folder the child works in; defaults to the parent's cwd. */ cwd?: string; /** Files the parent already read, handed over in the brief (see withContextFiles). */ contextFiles?: readonly string[]; }): Promise; sendMessage(agentId: string, message: string): Promise; followup(agentId: string, task: string, checks?: readonly AcceptanceCheck[]): Promise; interrupt(agentId: string, collectResult?: boolean): Promise; subscribe(listener: (snapshot: SubAgentSnapshot) => void): () => void; list(): SubAgentSnapshot[]; wait(agentIds?: string[], condition?: "any" | "all", timeoutMs?: number): Promise<{ timed_out: boolean; agents: SubAgentSnapshot[]; }>; completionGate(): { active: SubAgentSnapshot[]; uncollected: SubAgentSnapshot[]; unresolved: number; }; completionGateMessage(): string | undefined; interruptAll(): Promise; shutdownAll(): Promise; /** Synchronous process-exit fallback: terminate detached process groups immediately. */ shutdownAllNow(): void; private markCollected; /** * The folder a new child works in: the parent's cwd by default, otherwise a * folder already in the parent's workspace. Never wider than the parent. */ private resolveChildCwd; /** * The parent's extra roots a child also gets (see shareWorkspaceRoots), * less the folder it works in and any root that folder already covers. * Undefined when the parent doesn't share them. */ private sharedRoots; private spawnWorkerProcess; private respawnRecovered; private attach; private handleFrame; /** * Announce a finished child into the parent's next turn. Carries the id, the * state and a short digest — NOT the output, which stays behind `wait_agent` * so a chatty child cannot dump its transcript into the parent's context. */ private notifyTerminal; private request; private waitForChange; private publish; private snapshot; private queuePersist; private elapsed; private activeCount; /** Enforce the optional per-model cap against the resolved child model. */ private assertModelCapacity; private isActive; private isTerminal; private requireWorker; private assertAvailable; private assertChildSessionPath; private createId; private scheduleIdleReap; private reapExcessIdle; private fail; private close; private kill; }