import { EventEmitter } from 'node:events'; import type { AgentBridge, BridgeMessage } from '../types/agent-bridge.js'; import type { AwaitAnyResult, CoordinatorStatus, MultiAgentConfig, MultiAgentCoordinator, SpawnResult, SubagentConfig, SubagentRunner, TaskResult, TaskSpec } from '../types/multi-agent.js'; import { type BudgetSessionIdSource } from './subagent-budget.js'; export interface MultiAgentCoordinatorOptions { /** * Callback that executes a task on behalf of a subagent. Required for * `assign()` to actually run anything — without it, tasks queue forever. * The coordinator provides per-subagent isolation (own budget, own signal, * own bridge) and enforces timeout + concurrency. */ runner?: SubagentRunner | undefined; /** * Session id for EventBus/FleetBus emissions produced by this coordinator. * Accepts a getter so a long-lived coordinator follows session resume/new. */ sessionId?: BudgetSessionIdSource | undefined; } export declare class DefaultMultiAgentCoordinator extends EventEmitter implements MultiAgentCoordinator { readonly coordinatorId: string; readonly config: MultiAgentConfig; private runner?; private readonly sessionId; private fleetBus?; private readonly subagents; /** * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`). * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers * (parallel/eternal engine, SDD parallel run) don't go through * `Director.spawn()` where nicknames are normally assigned, so the * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role * names) to memorable ones here — that's what surfaces in the fleet monitor. */ private readonly usedNicknames; /** Maps subagentId → nickname key (e.g. 'einstein'). Used to free the slot on remove(). */ private readonly subagentNicknames; private pendingTasks; private completedResults; /** Prevents completedResults from growing unbounded in long-running coordinators. */ private static readonly MAX_COMPLETED_RESULTS; /** Caps each subagent's retained task history (see assign()); bounds RAM + the recordCompletion lookup. */ private static readonly MAX_SUBAGENT_TASK_HISTORY; private totalIterations; private inFlight; /** * Subagents currently being stopped. Set on entry to `stop()`, cleared * once `recordCompletion` lands the terminal TaskResult. Used by * `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch * to a subagent the caller has already asked to terminate — closes the * assign+terminate race where a fresh task could land on a worker that * was about to be killed. */ private readonly terminating; constructor(config: MultiAgentConfig, options?: MultiAgentCoordinatorOptions); private currentSessionId; /** * Replace the runner after construction. Used when the runner depends * on infrastructure (e.g. FleetBus) that isn't available until after * the coordinator's owning Director is built. */ setRunner(runner: SubagentRunner): void; /** * Wire a FleetBus for director-mode event emission. Call after the * FleetManager is constructed so the coordinator can emit lifecycle * events the TUI and monitoring tools subscribe to. */ setFleetBus(fleet: import('./fleet-bus.js').FleetBus): void; /** * Change the in-flight dispatch ceiling at runtime. Lowering does NOT * preempt running tasks — already-dispatched subagents finish their * current task; only future dispatches respect the new cap. Raising * immediately tries to fill the freed slots from the pending queue. */ setMaxConcurrent(n: number): void; /** * Upgrade a placeholder/role-derived name to a memorable scientist nickname * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is * empty, equals the role (case-insensitive), is a generic default * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id. * Explicit, human-chosen names — including nicknames already assigned by * `Director.spawn()` — are left untouched, so this never double-assigns. */ private withNickname; spawn(subagent: SubagentConfig): Promise; assign(task: TaskSpec): Promise; delegate(to: string, msg: BridgeMessage): Promise; /** * Wire up the communication bridge for a subagent. Call after spawn() once * the caller has created the bidirectional connection. */ setSubagentBridge(subagentId: string, bridge: AgentBridge): void; stop(subagentId: string): Promise; stopAll(): Promise; /** * Get current coordinator stats for monitoring/debugging. */ getStats(): { total: number; running: number; idle: number; stopped: number; inFlight: number; pending: number; completed: number; }; /** Emit a reactive coordinator.stats event on FleetBus so the TUI can subscribe. */ private emitCoordinatorStats; getStatus(): CoordinatorStatus; /** Expose snapshot of completed results — useful for callers awaiting all done. */ results(): readonly TaskResult[]; /** Defensive snapshot of the still-queued (not yet dispatched) tasks. */ listPendingTasks(): readonly TaskSpec[]; /** * Re-pin a still-PENDING task to a different subagent (`undefined` = * unpin, any idle worker may take it), then try to dispatch. Returns * `false` when the task is not in the pending queue — already * dispatched, completed, or unknown. Running tasks can never be pulled; * they can only be steered or terminated. The mutation is synchronous * (same tick as the check), so it cannot race a concurrent dispatch: * `tryDispatchNext` runs on this same call stack or a later one. * * The task keeps its id, so waiters, the report-back notifier, and * checkpoint bookkeeping resolve unchanged when it eventually completes. */ retargetPendingTask(taskId: string, subagentId: string | undefined): boolean; /** * Wait for one or more tasks to complete and return their results. * If a task is already done when called, returns immediately. * Resolves to an array in the same order as `taskIds`. */ awaitTasks(taskIds: string[], opts?: { timeoutMs?: number; }): Promise; /** * Wait until AT LEAST ONE of the named tasks completes. Returns every * requested result already available at that moment (drain-what's-done) * plus the ids still outstanding, so callers can loop "handle finishers, * re-await the remainder" instead of blocking on the whole batch. * * Unlike `awaitTasks`, this never rejects and deliberately does NOT * inherit `config.timeoutMs` — a "return whatever is done" call has no * business timing out unless the caller asks for a window explicitly. */ awaitTasksAny(taskIds: string[], opts?: { timeoutMs?: number; }): Promise; /** * Manual completion — for callers that drive subagents without a runner * (e.g. external orchestrators). When a runner is configured the coordinator * calls this itself. */ completeTask(result: TaskResult): void; private tryDispatchNext; private canDispatch; private takeNextDispatchableTask; private findIdleSubagent; private isIdleSubagent; /** * Returns true iff at least one spawned subagent could still * process a task. A "live" subagent is one that is not stopped * AND not mid-termination — `running` workers count because they * will eventually finish and become idle. * * When no subagent has ever been spawned, returns `true` so a * pre-spawn `assign()` simply queues (legacy behaviour). The * dead-end detection only fires after `stop()` has retired every * spawned worker. * * Used by `tryDispatchNext` to detect a dead-end pending queue. */ private hasLiveSubagent; /** * Drain every pending task with a synthetic `aborted_by_parent` * completion event. Same shape as the `stopAll()` drain — we go * around `recordCompletion` because pending tasks were never * counted in `inFlight` and routing them through would trip the * underflow guard on every task after the first. */ private drainPendingAsAborted; /** * Emit a synthetic `stopped`/`aborted_by_parent` completion for a single * PENDING task — one that was never counted in `inFlight`. This MUST bypass * `recordCompletion`: that path does `inFlight--`, which for a pending task * steals a decrement from a genuinely in-flight task and trips the underflow * guard — suppressing that real task's `task.completed` and hanging its * `awaitTasks()` caller. Pushes the result and fires the event directly. */ private emitPendingAborted; private runDispatched; private executeWithTimeout; private recordCompletion; /** * Stop a subagent and remove it from the coordinator. Releases all * associated resources (AbortController, context, budget state). * The subagent entry is deleted so the id can be reused in a future spawn. */ remove(subagentId: string): Promise; private isDone; } /** * Map any raw exception thrown out of a subagent's runner into a * structured `SubagentError`. Delegates to the shared classifier. * Re-exported for backward compatibility. */ export { classifySubagentError } from './coordinator/error-classifier.js'; //# sourceMappingURL=multi-agent-coordinator.d.ts.map