import type { SubagentConfig } from '../types/multi-agent.js'; import type { SessionWriter } from '../types/session.js'; import type { FleetUsage } from './fleet-bus.js'; import { FleetBus, FleetUsageAggregator } from './fleet-bus.js'; import type { IFleetManager } from './ifleet-manager.js'; import type { DefaultMultiAgentCoordinator } from './multi-agent-coordinator.js'; import type { WorktreeTaskStateUpdate } from './worktree-task-runner.js'; /** Options for constructing a FleetManager. */ export interface FleetManagerOptions { manifestPath?: string | undefined; sessionsRoot?: string | undefined; directorRunId?: string | undefined; maxSpawns?: number | undefined; maxSpawnDepth?: number | undefined; spawnDepth?: number | undefined; stateCheckpointPath?: string | undefined; sessionWriter?: SessionWriter | undefined; manifestDebounceMs?: number | undefined; checkpointDebounceMs?: number | undefined; directorBudget?: { maxCostUsd?: number | undefined; maxTokens?: number | undefined; } | undefined; /** * Maximum context load (as a fraction of maxContext) the leader agent * is allowed to reach before a new spawn is rejected. Default: 0.85. * When the leader's context pressure exceeds this threshold, spawning * a new subagent is refused — the leader must compact first. * Set to 1.0 to disable this check. */ maxLeaderContextLoad?: number | undefined; /** * Provider's max context window in tokens. Used with `maxLeaderContextLoad` * to compute the absolute token threshold. Default: 128_000. * * A function may be supplied when the leader can switch models at runtime; * canSpawn() reads it lazily so the spawn threshold follows the active model. */ maxContext?: number | (() => number | undefined) | undefined; } /** * Fleet-level policy container extracted from `Director`. Owns: * - FleetBus + FleetUsageAggregator * - Spawn caps and counters * - Manifest entries and debounced writing * - State checkpointing * * This lets the `Director` focus on orchestration (spawn/assign/await/ask) * while fleet-level decisions live in one place. The class implements * `IFleetManager` so it remains swappable in future. * * @example * ```typescript * const fm = new FleetManager({ manifestPath: '/tmp/fleet.json' }); * const err = fm.canSpawn({ name: 'worker' }); * if (!err) fm.recordSpawn('sub-1', { name: 'worker' }); * await fm.writeManifest(); * ``` */ export declare class FleetManager implements IFleetManager { /** The fleet-wide event bus. */ readonly fleet: FleetBus; /** Usage rollup across all subagents. */ readonly usage: FleetUsageAggregator; private readonly manifestPath?; private readonly directorRunId; /** Spawn cap (lifetime total). Infinity means unlimited. */ readonly maxSpawns: number; /** Nesting cap. */ readonly maxSpawnDepth: number; /** This director's depth in a director chain. Root = 0. */ readonly spawnDepth: number; /** Live spawn counter. */ private spawnCount; private readonly stateCheckpoint; private readonly sessionWriter; private manifestTimer; private manifestWriteChain; private disposed; /** * Teardown freeze. Set by `closeManifest()` after the final manifest is * flushed; makes any subsequent `scheduleManifest`/`writeManifest`/ * `flushManifest` a no-op so a late fire-and-forget write (e.g. the * task-completion flush the Director issues via `void flushManifest()`) * cannot land an `atomicWrite` in the manifest directory while a caller is * deleting it. Cleared by `recordSpawn` so a stop-then-spawn-again host * resumes writing. */ private closing; private readonly manifestDebounceMs; /** Fleet-wide cost cap. Infinity = no cap. Distinct from SubagentBudget limits, * which track per-subagent spend — this field caps the entire fleet total. */ private readonly maxFleetCostUsd; /** Fleet-wide input+output token ceiling. Infinity = no cap. */ private readonly maxFleetTokens; private readonly manifestEntries; /** Pending tasks with their descriptions — populated by `addPendingTask` * and cleared by `removePendingTask`. Replaces the host-side `pending` * Map so task descriptions live in one place (FleetManager). */ private readonly pendingTasks; private readonly subagentMeta; private readonly priceLookups; /** Tracks which nickname keys are already assigned — prevents collisions. */ private readonly _usedNicknames; /** The coordinator (wired via setCoordinator by Director after construction). */ private coordinator; /** Leader agent's current context pressure (full request tokens). */ private leaderContextPressure; /** Maximum context load fraction before spawn is refused. */ private readonly maxLeaderContextLoad; /** Provider's max context window in tokens, or a live resolver for runtime model switches. */ private readonly maxContext; constructor(opts?: FleetManagerOptions); get fleetBus(): FleetBus; /** * Wire the coordinator after Director construction. The coordinator * is not available when FleetManager is constructed standalone. * Once set, `getFleetStats()` delegates to `coordinator.getStats()`. */ setCoordinator(coordinator: DefaultMultiAgentCoordinator): void; snapshot(): FleetUsage; getSubagentMeta(id: string): { provider?: string | undefined; model?: string | undefined; name?: string | undefined; } | undefined; /** * Returns null if the spawn is allowed, or an object describing * which cap was exceeded. Does NOT throw — the caller decides * how to surface the rejection. */ canSpawn(_config: SubagentConfig): { kind: 'max_spawns' | 'max_spawn_depth' | 'max_cost_usd' | 'max_tokens' | 'max_context_load'; limit: number; observed: number; } | null; budgetSnapshot(): { maxSpawns: number; usedSpawns: number; remainingSpawns: number; maxTokens: number; usedTokens: number; remainingTokens: number; maxCostUsd: number; usedCostUsd: number; remainingCostUsd: number; }; setLeaderContextPressure(tokens: number): void; private resolveMaxContext; /** * Assign a memorable nickname (e.g. "Einstein (Bug Hunter)") to the config, * record it so the same name is never reused, then record the spawn. * * Call this INSTEAD of `recordSpawn` when you want automatic nicknames. * The nickname is written back to `config.name` BEFORE the coordinator * sees the config, so the manifest, logs, and fleet UI all show it. * * NOTE: This method ONLY assigns the nickname and marks it used. * The caller MUST call `recordSpawn(subagentId, config, priceLookup)` AFTER * `coordinator.spawn()` returns with the real subagentId. This is because * the subagentId is not known until after the coordinator creates the subagent. */ assignNicknameAndRecord(config: SubagentConfig): string; /** * Returns the set of already-assigned nickname keys — useful for debugging * and testing. */ get usedNicknames(): ReadonlySet; /** * Records a spawn: increments counter, stores metadata, updates state checkpoint, * and schedules a debounced manifest write. Call AFTER the coordinator * has successfully spawned the subagent. * * @param subagentId The subagent's id (from coordinator.spawn result) * @param config The SubagentConfig that was used * @param priceLookup Optional per-subagent pricing data */ recordSpawn(subagentId: string, config: SubagentConfig, priceLookup?: { input?: number | undefined; output?: number | undefined; cacheRead?: number | undefined; cacheWrite?: number | undefined; }): void; writeManifest(): Promise; private writeManifestNow; /** * Attach task ids to an already-spawned subagent. Called by * `Director.assign()` after the coordinator assigns a task. */ addTaskToSubagent(subagentId: string, taskId: string): void; recordTaskWorktree(update: WorktreeTaskStateUpdate): void; /** * Debounced manifest write. Call after any state mutation * (spawn, assign, complete) so a burst collapses into one write. * When `manifestDebounceMs` is 0, writes are synchronous (no debounce). */ scheduleManifest(): void; /** * Bypass the debounce timer and write the manifest immediately. * Clears any pending debounce timer before writing. */ flushManifest(): Promise; /** * Teardown flush for callers that will delete the manifest directory next * (host `stopAll`/`dispose`). Writes the final manifest, then freezes the * writer so no later fire-and-forget write can start, and drains anything * still in flight. After this resolves it is safe to `rm -rf` the manifest * directory: no `atomicWrite` (temp sibling + rename) can race the removal. * * The freeze is lifted by the next `recordSpawn`, so a stop-then-spawn-again * host keeps persisting manifests. */ closeManifest(): Promise; private clearManifestTimer; /** Best-effort session event writer. Swallows failures. */ private appendSessionEvent; addPendingTask(taskId: string, subagentId: string, description: string): void; removePendingTask(taskId: string): void; getFleetStats(): { total: number; running: number; idle: number; stopped: number; inFlight: number; pending: number; completed: number; subagentStatuses: { subagentId: string; taskId: string; status: string; assigned: boolean; }[]; }; getFleetStatus(): { pending: { taskId: string; description: string; subagentId: string; }[]; live: { subagentId: string; status: string; task?: string | undefined; }[]; }; /** * Clean up all fleet-manager state associated with a removed subagent: * - Frees the nickname slot so the same name can be reused * - Removes any pending tasks for this subagent * - Drops the manifest / meta / price-lookup entries (these previously leaked: * they were never deleted, so each grew one entry per subagent — and each * manifest entry's `taskIds[]` one per task — for the whole leader lifetime). */ removeSubagent(subagentId: string): void; /** Release all resources: clear the manifest debounce timer and dispose the usage aggregator. */ dispose(): void; } //# sourceMappingURL=fleet-manager.d.ts.map