// Subagent status supervision: the periodic parent-side observation of running // Subagents that projects Native agent observation into a lifecycle, detects // transitions, and reports them as status messages. Presentation refresh and // UI rendering are not exclusive to it. See CONTEXT.md // "Subagent status supervision". import type { RunningSubagent } from "./running-registry.ts"; import type { NestedLifecycleSnapshot } from "./nested-lifecycle.ts"; import { ensureLifecycle } from "./running-registry.ts"; import { formatLifecycleTransitionLine, lifecycleTransition, projectLifecycle, type LifecycleProjection, } from "./lifecycle.ts"; import { capStatusLines, DEFAULT_STATUS_LINE_LIMIT, formatNestedLifecycleDrainDetail, formatStatusAggregate, normalizeStatusName, } from "./status.ts"; import { formatElapsedDuration } from "./format.ts"; /** Steer payload emitted for one batch of lifecycle transitions. */ export interface StatusTransitionPayload { customType: "subagent_status"; content: string; display: true; details: { lines: string[]; overflow: number; source?: "subagent" | "parent" }; } /** Injectable clock so the supervision loop is deterministic in tests. */ export interface StatusSupervisorScheduler { setInterval(callback: () => void, intervalMs: number): unknown; clearInterval(timer: unknown): void; } export interface SubagentStatusSupervisorOptions { /** Current running Subagents; recomputed each tick. */ snapshot: () => Iterable; /** Reads activity and mutates lifecycle (e.g. observeRunningSubagent). */ observe: (running: RunningSubagent, now: number) => void; /** Emits one status steer for a batch of transitions. */ onTransition: (payload: StatusTransitionPayload) => void; /** Reads the current parent ownership/drain state across extension reload. */ parentSnapshot?: () => NestedLifecycleSnapshot; /** Requests a presentation refresh after any lifecycle-kind change. */ onRefresh: () => void; intervalMs?: number; now?: () => number; scheduler?: StatusSupervisorScheduler; } export interface SubagentStatusSupervisor { /** Start the loop. Idempotent while running. */ start(): void; /** Stop the loop and reset transition bookkeeping. */ stop(): void; } const DEFAULT_INTERVAL_MS = 1_000; const productionScheduler: StatusSupervisorScheduler = { setInterval: (callback, intervalMs) => setInterval(callback, intervalMs), clearInterval: (timer) => clearInterval(timer as ReturnType), }; export function createSubagentStatusSupervisor( options: SubagentStatusSupervisorOptions, ): SubagentStatusSupervisor { const snapshot = options.snapshot; const observe = options.observe; const onTransition = options.onTransition; const onRefresh = options.onRefresh; const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS; const now = options.now ?? Date.now; const scheduler = options.scheduler ?? productionScheduler; let timer: unknown = null; const lastProjectedKinds = new Map(); let lastParentStatus: "draining" | "cancelling" | undefined; function clearTimer(): void { if (timer != null) scheduler.clearInterval(timer); timer = null; lastProjectedKinds.clear(); lastParentStatus = undefined; } function parentStatus(snapshot: NestedLifecycleSnapshot | undefined): "draining" | "cancelling" | undefined { if (!snapshot || snapshot.state !== "draining") return undefined; return snapshot.cancellationRequested ? "cancelling" : "draining"; } function formatParentStatusLine(snapshot: NestedLifecycleSnapshot, status: "draining" | "cancelling"): string { const detail = formatNestedLifecycleDrainDetail(snapshot, "launch reservation"); if (status === "cancelling") return `Parent cancellation draining${detail}.`; return snapshot.selfSettled ? `Parent self-settled; draining${detail}.` : `Parent draining${detail}.`; } function tick(): void { const agents = Array.from(snapshot()); const currentParentSnapshot = options.parentSnapshot?.(); const currentParentStatus = parentStatus(currentParentSnapshot); if (agents.length === 0 && currentParentStatus === undefined) { clearTimer(); return; } const current = now(); const transitionLines: string[] = []; let shouldRefresh = false; let hasAgentTransition = false; for (const running of agents) { observe(running, current); const projection = projectLifecycle(ensureLifecycle(running), current); const previousKind = lastProjectedKinds.get(running.id); const transition = lifecycleTransition(previousKind, projection.kind); if (previousKind !== projection.kind) shouldRefresh = true; lastProjectedKinds.set(running.id, projection.kind); if (transition) { hasAgentTransition = true; transitionLines.push( formatLifecycleTransitionLine( normalizeStatusName(running.name), projection, transition, current, running.startTime, formatElapsedDuration, ), ); } } if (currentParentStatus !== lastParentStatus) { shouldRefresh = true; if (currentParentStatus && currentParentSnapshot) { transitionLines.push(formatParentStatusLine(currentParentSnapshot, currentParentStatus)); } lastParentStatus = currentParentStatus; } if (shouldRefresh) onRefresh(); if (transitionLines.length > 0) { const capped = capStatusLines(transitionLines, DEFAULT_STATUS_LINE_LIMIT); onTransition({ customType: "subagent_status", content: formatStatusAggregate(transitionLines, DEFAULT_STATUS_LINE_LIMIT), display: true, details: { lines: capped.visibleLines, overflow: capped.overflow, source: hasAgentTransition ? "subagent" : "parent", }, }); } } return { start(): void { // The supervisor lives on the shared parent runtime, so one timer // spans /reload; there is no prior module's timer to clear. if (timer != null) return; timer = scheduler.setInterval(tick, intervalMs); }, stop(): void { clearTimer(); }, }; }