UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

99 lines • 4.07 kB
import { openCodeLog } from './log.js'; /** The session an event belongs to. Form events carry it inside the form. */ export function sessionOf(event) { const id = event.data?.sessionID ?? event.data?.form?.sessionID; return typeof id === 'string' ? id : undefined; } /** * One event subscription per OpenCode server, fanned out to the turns listening on its sessions. A * server's stream closes when its last listener leaves; a stream that drops reconnects with backoff. */ export class OpenCodeEventHub { streams = new Map(); /** Listen to one session's events on a server. Resolves once the server's stream is open. */ async listen(serverKey, client, sessionId, listener) { const stream = this.streamFor(serverKey, client); let set = stream.listeners.get(sessionId); if (set === undefined) { set = new Set(); stream.listeners.set(sessionId, set); } set.add(listener); await stream.ready; return () => { // The server's stream now — it may have moved to a new client since this listener joined. const live = this.streams.get(serverKey) ?? stream; const current = live.listeners.get(sessionId); current?.delete(listener); if (current?.size === 0) live.listeners.delete(sessionId); if (live.listeners.size === 0 && this.streams.get(serverKey) === live) { live.controller.abort(); this.streams.delete(serverKey); } }; } close() { for (const stream of this.streams.values()) stream.controller.abort(); this.streams.clear(); } /** * The server's stream, opened on `client`. A different client for a known key (the host replaced * the connection, e.g. a new sandbox under the same key) moves every listener to a stream on the * new one: the old connection may be dead, and its sessions would never hear back. */ streamFor(serverKey, client) { const existing = this.streams.get(serverKey); if (existing !== undefined && existing.client === client) return existing; existing?.controller.abort(); const controller = new AbortController(); let opened; const ready = new Promise((resolve) => { opened = resolve; }); const stream = { client, controller, listeners: existing?.listeners ?? new Map(), ready, }; this.streams.set(serverKey, stream); void this.pump(serverKey, stream, client, opened); return stream; } async pump(serverKey, stream, client, opened) { let backoff = 500; while (!stream.controller.signal.aborted) { try { const events = client.event.subscribe({ signal: stream.controller.signal }); opened(); for await (const event of events) { backoff = 500; const sessionId = sessionOf(event); if (sessionId === undefined) continue; for (const listener of stream.listeners.get(sessionId) ?? []) { try { listener(event); } catch (error) { openCodeLog.error(`an event listener failed: ${error.message}`); } } } } catch (error) { if (stream.controller.signal.aborted) return; openCodeLog.warn(`the event stream of ${serverKey} dropped: ${error.message}`); } if (stream.controller.signal.aborted) return; await new Promise((resolve) => setTimeout(resolve, backoff).unref?.()); backoff = Math.min(backoff * 2, 10_000); } } } //# sourceMappingURL=event-hub.js.map