/** * agent-manager.ts — Tracks agents, background execution, resume support. * * Background agents are subject to a configurable concurrency limit (default: 3). * Excess agents are queued and auto-started as running agents complete. * Foreground agents bypass the queue (they block the parent anyway). */ import { AsyncLocalStorage } from "node:async_hooks"; export declare const activeAgentStorage: AsyncLocalStorage; import type { Model } from "@earendil-works/pi-ai"; import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { type ToolActivity } from "./agent-runner.js"; import { type HookRegistry } from "./hooks.js"; import type { AgentInvocation, AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js"; export type OnAgentComplete = (record: AgentRecord) => void; export type OnAgentStart = (record: AgentRecord) => void; export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void; export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number; }; export type BudgetWarningType = "agents_at_80" | "turns_at_80" | "agents_at_90" | "turns_at_90"; export type OnBudgetWarning = (type: BudgetWarningType, usage: { spawnedAgents: number; totalTurns: number; }, limits: { maxAgents: number; maxTurns: number; }) => void; export interface SessionLimits { maxAgentsPerSession?: number; maxTotalTurnsPerSession?: number; } interface SpawnOptions { description: string; model?: Model; maxTurns?: number; isolated?: boolean; inheritContext?: boolean; thinkingLevel?: ThinkingLevel; isBackground?: boolean; /** * Skip the maxConcurrent queue check for this spawn — start immediately even * if the configured concurrency limit would otherwise queue it. Used by the * scheduler so a fired job can't be deferred past its trigger window. */ bypassQueue?: boolean; /** Isolation mode — "worktree" creates a temp git worktree for the agent. */ isolation?: IsolationMode; /** Resolved invocation snapshot captured for UI display. */ invocation?: AgentInvocation; /** Parent abort signal — when aborted, the subagent is also stopped. */ signal?: AbortSignal; /** Called on tool start/end with activity info (for streaming progress to UI). */ onToolActivity?: (activity: ToolActivity) => void; /** Called on streaming text deltas from the assistant response. */ onTextDelta?: (delta: string, fullText: string) => void; /** Called when the agent session is created (for accessing session stats). */ onSessionCreated?: (session: AgentSession) => void; /** Called at the end of each agentic turn with the cumulative count. */ onTurnEnd?: (turnCount: number) => void; /** Called once per assistant message_end with that message's usage delta. */ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number; }) => void; /** Called when the session successfully compacts. */ onCompaction?: (info: CompactionInfo) => void; /** Nesting depth for this spawn (0 = root). Passed through to the agent record and runner. */ currentLevel?: number; /** * Optional override for the auto-generated 8-hex-char correlation id. * When omitted, `AgentManager.spawn` calls `generateCorrelationId()` so * every record is guaranteed to have one. Pass a deterministic value in * tests to make span assertions stable. */ correlationId?: string; } export declare class AgentManager { private agents; private cleanupInterval; private onComplete?; private onStart?; private onCompact?; private maxConcurrent; private sessionLimits; private sessionUsage; private lastTurnCounts; private sessionMaxSpawns; private sessionMaxTurns; hooks?: HookRegistry; onBudgetWarning?: OnBudgetWarning; /** Queue of background agents waiting to start. */ private queue; /** Number of currently running background agents. */ private runningBackground; /** Cleanup TTL: completed agents older than this are pruned periodically. */ private cleanupTtlMs; constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart, onCompact?: OnAgentCompact, /** * Cleanup TTL in milliseconds: completed/stopped/errored agents older than * this are pruned from memory during periodic cleanup cycles. * Default: 60 seconds (down from 120s to reduce memory pressure during long sessions). */ cleanupTtlMs?: number); /** * Set a new cleanup TTL. Completed/stopped/errored agents older than this * value will be pruned on the next periodic cleanup cycle. */ setCleanupTtl(ms: number): void; /** Get the current cleanup TTL in milliseconds. */ getCleanupTtl(): number; /** Update the max concurrent background agents limit. */ setMaxConcurrent(n: number): void; getMaxConcurrent(): number; setSessionLimits(limits: SessionLimits): void; getSessionLimits(): SessionLimits; setSessionMaxSpawns(n: number): void; getSessionMaxSpawns(): number; setSessionMaxTurns(n: number): void; getSessionMaxTurns(): number; getSessionUsage(): { spawnedAgents: number; totalTurns: number; }; resetSessionUsage(): void; setBudgetWarningHandler(handler: OnBudgetWarning): void; /** Get the ID of the agent executing in this async context. */ getActiveAgentId(): string | undefined; /** * Spawn an agent and return its ID immediately (for background use). * If the concurrency limit is reached, the agent is queued. */ spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: SpawnOptions): string; /** Actually start an agent (called immediately or from queue drain). */ private startAgent; /** * Shared cleanup for agent completion (success or error). * Handles status/error assignment, output flush, worktree cleanup, * and background queue drain. Called from both .then() and .catch() after * result-specific logic. */ private finalizeAgent; /** Start queued agents up to the concurrency limit. */ private drainQueue; /** * Spawn an agent and wait for completion (foreground use). * Foreground agents bypass the concurrency queue. */ spawnAndWait(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: Omit): Promise; /** * Resume an existing agent session with a new prompt. */ resume(id: string, prompt: string, signal?: AbortSignal): Promise; getRecord(id: string): AgentRecord | undefined; listAgents(): AgentRecord[]; /** * Get the correlation id of a previously-spawned agent. Returns * `undefined` if the id is unknown or the record was created before * the `correlationId` field was added. Safe to call from the UI, log * helpers, and the `/agents health` report. */ getCorrelationId(id: string): string | undefined; abort(id: string): boolean; /** Callback invoked when a record is removed from the agent map (cleanup). */ onRecordRemoved?: (id: string) => void; /** Dispose a record's session and remove it from the map. */ private removeRecord; private checkBudgetWarning; private cleanup; /** * Remove all completed/stopped/errored records immediately. * Called on session start/switch so tasks from a prior session don't persist. */ clearCompleted(): void; /** Whether any agents are still running or queued. */ hasRunning(): boolean; /** Abort all running and queued agents immediately. */ abortAll(): number; /** Wait for all running and queued agents to complete (including queued ones). */ waitForAll(): Promise; dispose(): void; } export {};