import type { EventBus } from '../kernel/events.js'; import type { FleetBus } from './fleet-bus.js'; export interface AgentTimelineEntry { /** Unique entry id (ULID or timestamp-based). */ id: string; /** Subagent id this entry belongs to. */ subagentId: string; /** Human-readable agent name/role. */ agentName: string; /** ISO 8601 timestamp. */ ts: string; /** Content type. */ kind: 'text' | 'thinking' | 'tool_use' | 'tool_result' | 'error' | 'status' | 'system'; /** The message content (text, tool summary, error message, status text). */ content: string; /** Iteration index within the subagent's run. */ iteration: number; /** For tool entries: tool name. */ toolName?: string | undefined; /** For tool entries: whether the tool succeeded. */ toolOk?: boolean | undefined; /** Running cost estimate. */ costUsd?: number | undefined; } export interface AgentVirtualSession { subagentId: string; agentName: string; createdAt: string; status: string; task?: string | undefined; /** Ordered transcript entries (newest last). */ transcript: AgentTimelineEntry[]; } export interface AgentMonitorOptions { /** Parent/host session id for emitted agent timeline/status events. */ sessionId?: string | undefined; /** The FleetBus to listen on for subagent events. Optional — set via `setFleetBus()` before `start()`. */ fleetBus?: FleetBus | undefined; /** Local EventBus for emitting agent.timeline.* and agent.status_changed events. */ events: EventBus; /** Directory where per-subagent JSONL transcripts will be written. */ transcriptsDir: string; /** Maximum in-memory entries per subagent (ring buffer). Default 500. */ maxEntriesPerAgent?: number; /** Whether agent stream is initially enabled. Default false. */ streamEnabled?: boolean; /** Called for each new timeline entry — used by HQ publisher bridge. */ onEntry?: ((entry: AgentTimelineEntry) => void) | undefined; } export declare class AgentMonitorService { private _fleetBus; private readonly _events; private readonly _sessionId; private readonly _transcriptsDir; private readonly _maxEntries; private _streamEnabled; private _onEntry; /** Per-subagent virtual sessions. */ private readonly _sessions; /** * Per-subagent OPEN streaming segment. Provider text/thinking deltas * arrive word-by-word; writing one transcript entry per delta fragments * every surface (ring, JSONL, HQ) into unreadable single-word lines. * Instead, consecutive same-kind/same-iteration deltas append into ONE * entry that is already in the ring (live views see it grow in place); * the JSONL line is written once, when the segment closes. */ private readonly _openStreams; /** * FIFO chain of in-flight transcript appends. Writes stay fire-and-forget so * a slow disk never stalls a subagent, but `close()` must be able to wait for * them: without this the process can exit between `stop()` flushing the open * segments and those appends reaching disk, losing exactly the text that was * on screen. Mirrors FleetManager.closeManifest()'s drain. */ private readonly _writeQueue; private _writeQueueBytes; /** Transcript dirs already created — avoids a mkdir syscall per append. */ private readonly _ensuredDirs; private _writeDrain; private static readonly _MAX_QUEUED_WRITES; private static readonly _MAX_QUEUED_WRITE_BYTES; /** Disposers for FleetBus subscriptions, keyed by subagentId. */ private readonly _subscriptions; /** Generic fleet-wide subscription disposer. */ private _fleetDisposer; /** Track whether service is running. */ private _started; constructor(opts: AgentMonitorOptions); /** Set the FleetBus to listen on. Must be called before `start()`. */ setFleetBus(bus: FleetBus): void; get streamEnabled(): boolean; /** Enable/disable streaming agent conversations to the main chat timeline. */ setStreamEnabled(enabled: boolean): void; /** Get a snapshot of all known agent sessions. */ getAllSessions(): AgentVirtualSession[]; /** * Rebuild the virtual sessions from the on-disk transcripts. * * The ring only holds what THIS process observed, so after a resume it is * empty and `getAllSessions()` reports no subagents even though every * transcript is sitting in `transcriptsDir`. This reads them back so a * resumed surface can show the prior run's subagent work. * * Entries already in the ring win: a live subagent's in-memory segment is * newer than its JSONL, which is only written when a segment closes. */ loadSessionsFromDisk(): Promise; private _readTranscriptFile; /** Get a specific agent's virtual session, or undefined. */ getSession(subagentId: string): AgentVirtualSession | undefined; /** Get transcript entries for a specific agent, newest first. */ getTranscript(subagentId: string, limit?: number): AgentTimelineEntry[]; /** Set a callback for each new timeline entry (HQ bridge). */ setOnEntry(handler: ((entry: AgentTimelineEntry) => void) | undefined): void; /** Start listening to FleetBus events. */ start(): void; /** * Stop listening and wait for every queued transcript write to reach disk. * * `stop()` alone only *starts* the flush of the open segments; the appends are * fire-and-forget, so a caller that exits right after it loses precisely the * text that was streaming. Shutdown paths should await this instead. * * A hard kill (SIGKILL, power loss) still loses the open segments — they live * only in the ring until a segment closes. Bounding that would mean writing * each delta, which fragments every reply into single-word JSONL lines. */ close(): Promise; /** Stop listening and clean up all subscriptions. */ stop(): void; /** Ensure a subagent is being tracked. Called when a subagent spawns. */ trackSubagent(subagentId: string, agentName: string, task?: string): void; /** Mark a subagent as completed/failed/etc. Called on subagent finish. */ completeSubagent(subagentId: string, status: 'completed' | 'failed' | 'timeout' | 'stopped' | 'budget_exhausted', summary?: string): void; /** Drop a retired subagent from every live in-memory monitor surface. */ removeSubagent(subagentId: string): void; private _routeEvent; /** Max characters accumulated into one streaming segment before rolling over. */ private static readonly _MAX_SEGMENT_CHARS; /** * Fold a provider text/thinking delta into the subagent's open streaming * segment, or open a new one. A segment breaks on kind change, iteration * change, size cap, or any non-delta entry (see `_addEntry`). The entry * is pushed to the ring when the segment OPENS (live views poll the ring * and watch it grow in place); the JSONL line AND the timeline/HQ * announcement happen once, on close, with the COMPLETE segment — so * downstream consumers get one whole message instead of a word-per-event * firehose (or, worse, only the first word). */ private _appendStreamDelta; /** * Close the subagent's open streaming segment: persist it to JSONL and * announce the completed entry on the local bus + HQ callback. */ private _closeStream; private _addEntry; /** Announce a transcript entry on the local bus + HQ callback. */ private _emitEntry; /** * Queue a transcript append. Serialized so two entries for the same subagent * cannot interleave into a torn line, and retained on `_writeChain` so * `close()` can wait for them. Never rejects — a transcript write failing must * not take down the agent it is watching. */ private _enqueueWrite; private _startWriteDrain; private _appendToFile; private _uid; private _formatToolUse; private _formatToolResult; private _stringifyForTimeline; private _capTimelineText; } export declare function createAgentMonitorService(opts: AgentMonitorOptions): AgentMonitorService; //# sourceMappingURL=agent-monitor.d.ts.map