import type { EventListener, Events, HarnessEvent, HarnessEventType, WatchHandle } from "./agent-harness.ts"; import type { Context } from "./context.ts"; type UntypedEventListener = (event: HarnessEvent, context: Context) => void | Promise; type ResnapshotCapture = (context: Context, markBoundary: () => void) => Promise; /** Passive harness event bus with isolated handler failures. */ export class HarnessEventBus implements Events { private readonly listeners = new Map>(); private readonly watchListeners = new Set(); private deliveryTail: Promise = Promise.resolve(); private closedError: Error | undefined; on( type: TType, listener: EventListener>, ): () => void { if (this.closedError !== undefined) throw this.closedError; const wrapped: UntypedEventListener = (event, context) => listener(event as Extract, context); let listeners = this.listeners.get(type); if (listeners === undefined) { listeners = new Set(); this.listeners.set(type, listeners); } listeners.add(wrapped); return () => listeners?.delete(wrapped); } emit(event: HarnessEvent, context: Context): Promise { return this.emitBatch([event], context); } /** Bind current recipients and append one contiguous batch to the global delivery tail. */ emitBatch(events: readonly HarnessEvent[], context: Context): Promise { if (this.closedError !== undefined || events.length === 0) return Promise.resolve(); const bound = events.map((event) => { const payload = structuredClone(event); return { payload, recipients: this.snapshotRecipients(payload) }; }); const delivery = this.deliveryTail.then(async () => { for (const { payload, recipients } of bound) await this.deliver(payload, recipients, true, context); }); this.deliveryTail = delivery.catch(() => {}); return delivery; } watch( snapshot: T, filter: (event: HarnessEvent) => boolean, _context: Context, resnapshot?: ResnapshotCapture, ): WatchHandle { if (this.closedError !== undefined) throw this.closedError; return this.installWatcher(snapshot, filter, resnapshot); } async watchFromSnapshot( capture: (context: Context) => Promise, filter: (event: HarnessEvent) => boolean, context: Context, ): Promise> { if (this.closedError !== undefined) throw this.closedError; const watcher = this.installWatcher(undefined, filter, async (captureContext, markBoundary) => { const snapshot = await capture(captureContext); markBoundary(); return snapshot; }); try { watcher.setSnapshot(await capture(context)); return watcher; } catch (error) { watcher.unsubscribe(); throw error; } } close(error: Error): void { this.closedError ??= error; void this.deliveryTail.finally(() => { this.listeners.clear(); this.watchListeners.clear(); }); } private installWatcher( snapshot: T | undefined, filter: (event: HarnessEvent) => boolean, resnapshot: ResnapshotCapture | undefined, ): BufferedEventWatcher { let watcher!: BufferedEventWatcher; const capture = resnapshot === undefined ? undefined : async (context: Context) => { let marked = false; const next = await resnapshot(context, () => { if (marked) throw new Error("Resnapshot boundary was already marked"); marked = true; this.enqueueBarrier(() => watcher.markResnapshotBoundary()); }); if (!marked) throw new Error("Resnapshot capture did not mark its boundary"); return next; }; watcher = new BufferedEventWatcher(snapshot, capture, async (error, event, context) => { if (event.type === "handler_error") return; const normalized = error instanceof Error ? error : new Error(String(error)); const lane = "lane" in event && typeof event.lane === "string" ? event.lane : undefined; await this.emit( { type: "handler_error", kind: "event", event: event.type, error: normalized.message, ...(normalized.stack === undefined ? {} : { stack: normalized.stack }), ...(lane === undefined ? {} : { lane }), }, context, ); }); const watchListener: UntypedEventListener = (event, context) => { if (filter(event)) watcher.push(event, context); }; this.watchListeners.add(watchListener); watcher.setUnsubscribe(() => this.watchListeners.delete(watchListener)); return watcher; } private enqueueBarrier(barrier: () => void): void { const delivery = this.deliveryTail.then(barrier); this.deliveryTail = delivery.catch(() => {}); } private snapshotRecipients(event: HarnessEvent): UntypedEventListener[] { return [...(this.listeners.get(event.type) ?? []), ...this.watchListeners]; } private async deliver( event: HarnessEvent, recipients: readonly UntypedEventListener[], reportErrors: boolean, context: Context, ): Promise { for (const listener of recipients) { try { await listener(structuredClone(event), context); } catch (error) { if (!reportErrors || event.type === "handler_error") continue; const normalized = error instanceof Error ? error : new Error(String(error)); const lane = "lane" in event && typeof event.lane === "string" ? event.lane : undefined; const handlerError: HarnessEvent = { type: "handler_error", kind: "event", event: event.type, error: normalized.message, ...(normalized.stack === undefined ? {} : { stack: normalized.stack }), ...(lane === undefined ? {} : { lane }), }; await this.deliver(handlerError, this.snapshotRecipients(handlerError), false, context); } } } } class BufferedEventWatcher implements WatchHandle { snapshot: T; private readonly resnapshotCallback: ((context: Context) => Promise) | undefined; private readonly onError: (error: unknown, event: HarnessEvent, context: Context) => void | Promise; private buffer: Array<{ event: HarnessEvent; context: Context; epoch: number }> = []; private listener: EventListener | undefined; private unsubscribeCallback: (() => void) | undefined; private deliveryTail: Promise = Promise.resolve(); private epoch = 0; private resnapshotState: | { phase: "dropping" | "holding"; held: Array<{ event: HarnessEvent; context: Context }>; reached: Promise; resolveReached: () => void; } | undefined; private state: "buffering" | "started" | "unsubscribed" = "buffering"; constructor( snapshot: T | undefined, resnapshot: ((context: Context) => Promise) | undefined, onError: (error: unknown, event: HarnessEvent, context: Context) => void | Promise, ) { this.snapshot = snapshot as T; this.resnapshotCallback = resnapshot; this.onError = onError; } setSnapshot(snapshot: T): void { this.snapshot = snapshot; } start(listener: EventListener): void { if (this.state !== "buffering") throw new Error("WatchHandle.start() may be called only once"); this.state = "started"; this.listener = listener; const buffered = this.buffer; this.buffer = []; for (const bufferedEvent of buffered) { this.enqueue(bufferedEvent.event, bufferedEvent.context, bufferedEvent.epoch); } } async resnapshot(context: Context): Promise { if (this.state === "unsubscribed") throw new Error("WatchHandle is unsubscribed"); if (this.resnapshotCallback === undefined) throw new Error("WatchHandle does not support resnapshot"); if (this.resnapshotState !== undefined) throw new Error("WatchHandle resnapshot is already in progress"); let resolveReached!: () => void; const reached = new Promise((resolve) => { resolveReached = resolve; }); const resnapshotState = { phase: "dropping" as const, held: [] as Array<{ event: HarnessEvent; context: Context }>, reached, resolveReached, }; this.epoch++; this.resnapshotState = resnapshotState; try { const snapshot = await this.resnapshotCallback(context); await reached; this.snapshot = snapshot; this.resnapshotState = undefined; for (const held of resnapshotState.held) this.push(held.event, held.context); return snapshot; } catch (error) { this.resnapshotState = undefined; for (const held of resnapshotState.held) this.push(held.event, held.context); throw error; } } markResnapshotBoundary(): void { const resnapshot = this.resnapshotState; if (resnapshot === undefined || resnapshot.phase !== "dropping") return; resnapshot.phase = "holding"; resnapshot.resolveReached(); } unsubscribe(): void { if (this.state === "unsubscribed") return; this.state = "unsubscribed"; this.buffer = []; this.listener = undefined; this.unsubscribeCallback?.(); this.unsubscribeCallback = undefined; } push(event: HarnessEvent, context: Context): void { if (this.state === "unsubscribed") return; if (this.resnapshotState?.phase === "dropping") return; if (this.resnapshotState?.phase === "holding") { this.resnapshotState.held.push({ event, context }); return; } if (this.state === "buffering") { this.buffer.push({ event, context, epoch: this.epoch }); return; } this.enqueue(event, context, this.epoch); } setUnsubscribe(callback: () => void): void { this.unsubscribeCallback = callback; } private enqueue(event: HarnessEvent, context: Context, epoch: number): void { const listener = this.listener; if (listener === undefined) return; this.deliveryTail = this.deliveryTail .then(async () => { if (this.state === "started" && epoch === this.epoch) await listener(event, context); }) .catch(async (error) => { try { await this.onError(error, event, context); } catch {} }); } }