import type { AgentRuntimeDiagnosisV1, MessageProvenanceV1 } from "../../shared/types.ts"; export type ObservationAction = "status" | "diagnose" | "wait" | "watch"; export type ObservationDetail = "summary" | "tail" | "full"; export type ObservationView = "live" | "turns" | "session" | "todos"; export type ObservationWaitMode = "all" | "any" | "count"; export type ObservationPhase = "pending" | "active" | "settled" | "unknown"; export type ObservationOutcome = "success" | "failure" | "stalled" | "aborted"; export type ObservationWaitStatus = | "completed" | "failed" | "terminated" | "result-ready" | "stalled" | "timeout" | "aborted" | "not-found"; export interface ObservationTarget { kind: string; id: string; /** Opaque provider cursor for incremental views such as workspace session activity. */ cursor?: string; } export interface ObservationCapabilities { inspect: boolean; wait: boolean; cancel?: boolean; message?: boolean; supervise?: boolean; } export interface ObservationPage { /** Provider-specific page kind. */ kind: string; /** Cursor to continue after this page. */ nextCursor?: string; /** True when events before this page were evicted from a bounded source. */ gap?: boolean; /** Provider-specific structured items; text rendering remains in detail. */ items: unknown[]; } export interface ObservationSnapshot { target: ObservationTarget; found: boolean; nativeStatus: string; phase: ObservationPhase; outcome?: ObservationOutcome; waitStatus?: ObservationWaitStatus; summary: string; detail?: string[]; updatedAt: number; capabilities?: ObservationCapabilities; error?: string; /** Canonical terminal outcome when the target has settled (completed|failed|terminated). */ terminalStatus?: string; /** Last captured result text of the settled agent. */ lastResult?: string; /** Schema-valid structured output of a settled schema task (detail=full). */ structuredOutput?: unknown; /** Provider content revision used by watch to detect changes without a status transition. */ revision?: string; /** Optional structured page for cursor-based observation views. */ page?: ObservationPage; /** Canonical orthogonal teammate diagnosis; present for supported diagnose snapshots. */ diagnosis?: AgentRuntimeDiagnosisV1; } export interface ObservationReadOptions { detail: ObservationDetail; lines: number; /** Request a canonical runtime diagnosis in addition to the ordinary snapshot. */ diagnose?: boolean; /** "turns" lists target history; "session" shows sanitized workspace root-session activity; "todos" shows worker root-session Todo projections. */ view?: ObservationView; /** Opaque provider cursor copied from the selected target. */ cursor?: string; /** 1-based turn index to expand when view="turns"; omitted lists all turns. */ turn?: number; } export interface ObservationWaitOptions extends ObservationReadOptions { deadline: number; signal: AbortSignal; /** When the wait settles: "result-ready" (default) or "completed" (terminal lifecycle). */ until?: "result-ready" | "completed"; } export interface ObservationProvider { kind: string; capabilities: ObservationCapabilities; snapshot(id: string, options: ObservationReadOptions): ObservationSnapshot | Promise; wait(id: string, options: ObservationWaitOptions): Promise; } export interface ObserveParams { action: ObservationAction; targets: ObservationTarget[]; detail?: ObservationDetail; lines?: number; waitMode?: ObservationWaitMode; waitCount?: number; timeoutMs?: number; /** Block until "result-ready" (default) or "completed" (terminal lifecycle). */ until?: "result-ready" | "completed"; /** "turns" lists history; "session" shows sanitized root-session activity; "todos" shows workspace Todo projections. */ view?: ObservationView; /** 1-based turn index to expand when view="turns"; omitted lists all turns. */ turn?: number; } export interface ObserveResult { action: ObservationAction; reason: "snapshot" | "all" | "any" | "count" | "timeout" | "aborted" | "watch"; observations: ObservationSnapshot[]; durationMs: number; /** Watch history can be evicted; these remain the latest canonical snapshots, in target order. */ latestObservations?: ObservationSnapshot[]; evictedTransitions?: number; } export const WATCH_MAX_TRANSITIONS = 128; export const WATCH_MAX_TRANSITION_BYTES = 256 * 1024; export const OBSERVATION_TEXT_MAX_BYTES = 32 * 1024; const DISPLAY_TRUNCATED = "[observation text truncated; canonical result unchanged]"; function utf8Head(text: string, bytes: number): string { const encoded = Buffer.from(text.slice(0, bytes), "utf8"); let end = Math.min(bytes, encoded.length); // A cut inside a multibyte sequence retreats to its leading byte. while (end > 0 && end < encoded.length && (encoded[end]! & 0xc0) === 0x80) end--; return encoded.subarray(0, end).toString("utf8"); } /** Display-only bound, including separators and the truncation notice. */ export function boundObservationText(lines: Iterable): string[] { const maxBytes = OBSERVATION_TEXT_MAX_BYTES; const kept: string[] = []; let bytes = 0; for (const line of lines) { const nextBytes = bytes + (kept.length ? 1 : 0) + Buffer.byteLength(line, "utf8"); if (nextBytes > maxBytes) { const prefix = utf8Head([...kept, line.slice(0, maxBytes)].join("\n"), Math.max(0, maxBytes - Buffer.byteLength(DISPLAY_TRUNCATED) - 1)); return `${prefix}\n${DISPLAY_TRUNCATED}`.split("\n"); } kept.push(line); bytes = nextBytes; } return kept; } /** Compact, bounded display serialization; never alters the canonical value. */ export function observationJson(value: unknown, maxBytes = OBSERVATION_TEXT_MAX_BYTES): string | undefined { const limit = {}; let budget = maxBytes; let nodes = 0; const ancestors: unknown[] = []; try { const text = JSON.stringify(value, function (key, item) { while (ancestors.length && ancestors.at(-1) !== this) ancestors.pop(); if (++nodes > maxBytes || ancestors.length > 64) throw limit; budget -= Buffer.byteLength(key, "utf8") + 4; budget -= typeof item === "string" ? Buffer.byteLength(item, "utf8") : 8; if (budget < 0) throw limit; if (item !== null && typeof item === "object") ancestors.push(item); return item; }); return text !== undefined && Buffer.byteLength(text, "utf8") <= maxBytes ? text : undefined; } catch (error) { return error === limit ? undefined : "(not JSON-serializable)"; } } const REGISTRY_KEY = Symbol.for("pi-maestro.observation-providers.v1"); const globals = globalThis as typeof globalThis & Record; function registry(): Map { const existing = globals[REGISTRY_KEY]; if (existing instanceof Map) return existing as Map; const created = new Map(); globals[REGISTRY_KEY] = created; return created; } export function registerObservationProvider(provider: ObservationProvider): () => void { if (!provider.kind.trim()) throw new Error("Observation provider kind must not be empty."); registry().set(provider.kind, provider); return () => { if (registry().get(provider.kind) === provider) registry().delete(provider.kind); }; } export function getObservationProvider(kind: string): ObservationProvider | undefined { return registry().get(kind); } export function listObservationProviders(): ObservationProvider[] { return [...registry().values()]; } function unavailable(target: ObservationTarget, waitStatus: ObservationWaitStatus, message: string): ObservationSnapshot { return { target, found: false, nativeStatus: waitStatus, phase: "unknown", outcome: waitStatus === "aborted" ? "aborted" : "failure", waitStatus, summary: message, updatedAt: Date.now(), error: message, }; } function failedObservation(target: ObservationTarget, error: unknown): ObservationSnapshot { const message = error instanceof Error ? error.message : String(error); return unavailable(target, "failed", message); } function pendingObservation(target: ObservationTarget, waitStatus?: "timeout" | "aborted"): ObservationSnapshot { return { target, found: true, nativeStatus: "pending", phase: "active", ...(waitStatus ? { waitStatus, outcome: waitStatus === "aborted" ? "aborted" as const : undefined } : {}), summary: waitStatus ? `Observation ${waitStatus}.` : "Still active.", updatedAt: Date.now(), }; } function normalizedParams(params: ObserveParams): Required> { return { detail: params.detail ?? "summary", lines: params.lines ?? 20, waitMode: params.waitMode ?? "all", waitCount: params.waitCount ?? 1, timeoutMs: params.timeoutMs ?? 10 * 60_000, until: params.until ?? "result-ready", }; } function validate(params: ObserveParams): void { if (params.targets.length === 0) throw new Error("Observe requires at least one target."); if (params.targets.some((target) => !target.kind.trim() || !target.id.trim())) { throw new Error("Every observation target requires non-empty kind and id."); } if (params.targets.some((target) => target.cursor !== undefined && (typeof target.cursor !== "string" || target.cursor.length < 1 || target.cursor.length > 2_048))) { throw new Error("Observe target cursor must be a string between 1 and 2048 characters."); } if (params.targets.some((target) => target.cursor !== undefined) && params.view !== "session") { throw new Error('Observe target cursor requires view="session".'); } if (params.lines !== undefined && (!Number.isInteger(params.lines) || params.lines < 1 || params.lines > 500)) { throw new Error("Observe lines must be an integer between 1 and 500."); } if (params.timeoutMs !== undefined && (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1)) { throw new Error("Observe timeoutMs must be a positive integer."); } if ((params.action === "status" || params.action === "diagnose") && params.timeoutMs !== undefined) { throw new Error("Observe timeoutMs is supported only for wait and watch actions."); } if ( params.action !== "wait" && (params.waitMode !== undefined || params.waitCount !== undefined || params.until !== undefined) ) { throw new Error("Observe waitMode, waitCount, and until are supported only for the wait action."); } if (params.waitCount !== undefined && params.waitMode !== "count") { throw new Error('Observe waitCount requires waitMode="count".'); } if (params.waitMode === "count") { if (!Number.isInteger(params.waitCount) || (params.waitCount ?? 0) < 1 || (params.waitCount ?? 0) > params.targets.length) { throw new Error("Observe waitCount must be between 1 and the number of targets."); } } if (params.view !== undefined && params.view !== "live" && params.view !== "turns" && params.view !== "session" && params.view !== "todos") { throw new Error('Observe view must be "live", "turns", "session", or "todos".'); } if (params.view === "turns" && params.action !== "status") { throw new Error('Observe view="turns" is supported only for the status action.'); } if (params.view === "session" && params.action !== "status" && params.action !== "watch") { throw new Error('Observe view="session" is supported only for status and watch actions.'); } if (params.view === "todos" && params.action !== "status" && params.action !== "watch") { throw new Error('Observe view="todos" is supported only for status and watch actions.'); } if (params.view === "todos" && params.targets.some((target) => target.kind !== "workspace")) { throw new Error('Observe view="todos" requires workspace targets.'); } if (params.turn !== undefined) { if (!Number.isInteger(params.turn) || params.turn < 1) throw new Error("Observe turn must be a positive integer."); if (params.view !== "turns") throw new Error('Observe turn requires view="turns".'); } } export async function observeTargets(params: ObserveParams, signal?: AbortSignal): Promise { validate(params); const startedAt = Date.now(); const options = normalizedParams(params); if (params.action === "status" || params.action === "diagnose") { const observations = await Promise.all(params.targets.map(async (target) => { const provider = getObservationProvider(target.kind); if (!provider) return unavailable(target, "not-found", `No observation provider for kind \"${target.kind}\".`); try { return await provider.snapshot(target.id, { detail: options.detail, lines: options.lines, ...(params.action === "diagnose" ? { diagnose: true } : {}), ...(params.view ? { view: params.view } : {}), ...(target.cursor ? { cursor: target.cursor } : {}), ...(params.turn !== undefined ? { turn: params.turn } : {}), }); } catch (error) { return failedObservation(target, error); } })); return { action: params.action, reason: "snapshot", observations, durationMs: Date.now() - startedAt }; } if (params.action === "watch") { // Persistent observation: poll every target until the deadline, recording // every status transition within a bounded history (with latest snapshots // retained separately), which is richer than a one-shot status and // does not require a barrier condition like wait. return watchTargets(params, options, startedAt, signal); } const controller = new AbortController(); const deadline = startedAt + options.timeoutMs; const observations = new Array(params.targets.length); const pending = new Set(params.targets.map((_target, index) => index)); const required = options.waitMode === "all" ? params.targets.length : options.waitMode === "any" ? 1 : options.waitCount; return new Promise((resolve) => { let finished = false; let settledCount = 0; let timeout: ReturnType | undefined; const finish = (reason: ObserveResult["reason"]): void => { if (finished) return; finished = true; if (timeout) clearTimeout(timeout); signal?.removeEventListener("abort", onAbort); controller.abort(); const pendingStatus = reason === "timeout" ? "timeout" : reason === "aborted" ? "aborted" : undefined; const completed = params.targets.map((target, index) => observations[index] ?? pendingObservation(target, pendingStatus)); resolve({ action: "wait", reason, observations: completed, durationMs: Date.now() - startedAt }); }; const onAbort = (): void => finish("aborted"); // Arm the outer deadline before any provider wait starts. A provider cannot // strand later targets by delaying their timeout registration. timeout = setTimeout(() => finish("timeout"), options.timeoutMs); if (signal?.aborted) { finish("aborted"); return; } signal?.addEventListener("abort", onAbort, { once: true }); params.targets.forEach((target, index) => { const provider = getObservationProvider(target.kind); const wait = provider ? Promise.resolve().then(() => { if (controller.signal.aborted) return pendingObservation(target); return provider.wait(target.id, { detail: options.detail, lines: options.lines, deadline, until: options.until, signal: controller.signal, }); }).catch((error) => failedObservation(target, error)) : Promise.resolve(unavailable(target, "not-found", `No observation provider for kind \"${target.kind}\".`)); void wait.then((observation) => { if (finished || !pending.delete(index)) return; observations[index] = observation; settledCount += 1; if (settledCount < required) return; finish(options.waitMode); }); }); }); } /** * Persistent observation: poll every target until the deadline, recording each * status transition within a count/byte-bounded timeline. Latest snapshots are * retained separately even when history is evicted. No lifecycle barrier is required. */ async function watchTargets( params: ObserveParams, options: ReturnType, startedAt: number, signal?: AbortSignal, ): Promise { const pollMs = Math.min(1_000, Math.max(100, Math.round(options.timeoutMs / 10))); const deadline = startedAt + options.timeoutMs; const providers = params.targets.map((target) => getObservationProvider(target.kind)); const lastSeen = new Map(); const transitions: Array<{ observation: ObservationSnapshot; bytes: number }> = []; const latest = new Array(params.targets.length); let transitionBytes = 2; // JSON array brackets; each entry also reserves a comma. let evictedTransitions = 0; const record = (index: number, observation: ObservationSnapshot, key: string): void => { latest[index] = observation; if (lastSeen.get(index) === key) return; lastSeen.set(index, key); let bytes: number; try { bytes = Buffer.byteLength(JSON.stringify(observation), "utf8") + 1; } catch { bytes = WATCH_MAX_TRANSITION_BYTES + 1; } if (bytes + 2 > WATCH_MAX_TRANSITION_BYTES) { evictedTransitions++; return; } transitions.push({ observation, bytes }); transitionBytes += bytes; while (transitions.length > WATCH_MAX_TRANSITIONS || transitionBytes > WATCH_MAX_TRANSITION_BYTES) { transitionBytes -= transitions.shift()!.bytes; evictedTransitions++; } }; const snapshotAll = async (): Promise => { await Promise.all(params.targets.map(async (target, index) => { const provider = providers[index]; if (!provider) { if (!lastSeen.has(index)) { record(index, unavailable(target, "not-found", `No observation provider for kind \"${target.kind}\".`), "not-found"); } return; } try { const observation = await provider.snapshot(target.id, { detail: options.detail, lines: options.lines, ...(params.view ? { view: params.view } : {}), ...(target.cursor ? { cursor: target.cursor } : {}), }); const key = `${observation.nativeStatus}|${observation.phase}|${observation.revision ?? ""}`; record(index, observation, key); } catch (error) { if (!lastSeen.has(index)) { record(index, failedObservation(target, error), "error"); } } })); }; await snapshotAll(); while (Date.now() < deadline) { if (signal?.aborted) break; await new Promise((resolve) => setTimeout(resolve, pollMs)); await snapshotAll(); } return { action: "watch", reason: signal?.aborted ? "aborted" : "watch", observations: transitions.map((entry) => entry.observation), latestObservations: latest, evictedTransitions, durationMs: Date.now() - startedAt, }; } export function diagnosisProvenanceLine( label: "trigger" | "last-message", provenance: MessageProvenanceV1, ): string { const sender = provenance.sender.kind === "teammate-agent" ? `teammate-agent:${provenance.sender.correlationId}` : provenance.confidence === "unknown" ? `unknown${provenance.legacyLabel ? ` (legacy=${provenance.legacyLabel})` : ""}` : `${provenance.sender.kind}:${provenance.sender.ownerId}`; const identity = provenance.messageId ? ` message=${provenance.messageId}` : ""; const semantics = `${provenance.messageKind ? ` kind=${provenance.messageKind}` : ""}${provenance.deliveryMode ? ` mode=${provenance.deliveryMode}` : ""}`; return `${label}: source=${provenance.source} confidence=${provenance.confidence} sender=${sender}${identity}${semantics}`; } export function diagnosisDetail(diagnosis: AgentRuntimeDiagnosisV1): string[] { return [ `lifecycle=${diagnosis.lifecycle}${diagnosis.phase ? ` phase=${diagnosis.phase}` : ""} health=${diagnosis.health} activity=${diagnosis.activity} tool=${diagnosis.toolActivity} resultReady=${diagnosis.resultReady}`, `reason=${diagnosis.reasonCode} fallback=${diagnosis.fallbackDisposition}`, diagnosisProvenanceLine("trigger", diagnosis.trigger), ...(diagnosis.lastMessage ? [`last-message-role=${diagnosis.lastMessage.role} timestamp=${diagnosis.lastMessage.timestamp}`, diagnosisProvenanceLine("last-message", diagnosis.lastMessage.provenance)] : ["last-message: unavailable"]), ...(diagnosis.previousOutcome ? [`previous-outcome=${diagnosis.previousOutcome.status} settledAt=${diagnosis.previousOutcome.settledAt}${diagnosis.previousOutcome.message ? ` message=${diagnosis.previousOutcome.message}` : ""}`] : []), ]; } /** * Longest `lastResult` excerpt rendered without `verbose`. * * The excerpt exists so an observer learns what a target finished saying * without having to ask for detail — that is the reading a heartbeat needs to * tell "done what I asked" apart from "not started". The full text is available * under `verbose`, so this is a display bound and not a data bound. */ const LAST_RESULT_EXCERPT_CHARS = 240; /** One line of a result, whitespace flattened and cut to the excerpt bound. */ function lastResultExcerpt(text: string): string { const flattened = text.replaceAll(/\s+/gu, " ").trim(); return flattened.length <= LAST_RESULT_EXCERPT_CHARS ? flattened : `${flattened.slice(0, LAST_RESULT_EXCERPT_CHARS)}…`; } export function formatObserveResult(result: ObserveResult, verbose = false): string[] { const observations = result.evictedTransitions ? result.latestObservations ?? result.observations : result.observations; const targetCount = result.latestObservations?.length ?? result.observations.length; function* render(): Generator { yield `${targetCount} targets: ${result.reason} (${result.durationMs}ms)`; if (result.evictedTransitions) { yield `[watch history truncated: ${result.evictedTransitions} transitions evicted; showing latest snapshots]`; } for (const observation of observations) { const label = `${observation.target.kind}:${observation.target.id}`; yield `${label}\t${observation.nativeStatus}\t${observation.summary}`.trimEnd(); if (observation.diagnosis) { yield " --- diagnosis ---"; for (const line of diagnosisDetail(observation.diagnosis)) yield ` ${line}`; } if (verbose && observation.detail) { for (const detail of observation.detail) yield ` ${detail}`; } const detailHasLastResult = observation.detail?.includes("--- last result ---") === true; // Render the target's own account, not just inferred status/idle counts. if (observation.lastResult && !detailHasLastResult) { if (verbose) { yield " --- last result ---"; for (const line of observation.lastResult.split("\n")) yield ` ${line}`; } else { yield ` result: ${lastResultExcerpt(observation.lastResult)}`; } } const detailHasStructuredOutput = observation.detail?.includes("--- structured output ---") === true; if (verbose && observation.structuredOutput !== undefined && !detailHasStructuredOutput) { yield " --- structured output ---"; yield ` ${observationJson(observation.structuredOutput) ?? "[structured output omitted: display budget exceeded; canonical value unchanged]"}`; } } } return boundObservationText(render()); }