UNPKG

@copilotkit/runtime

Version:

<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />

282 lines (280 loc) 9.65 kB
import "reflect-metadata"; import { AgentRunner } from "./agent-runner.mjs"; import { AG_UI_CHANNEL_EVENT, finalizeRunEvents, phoenixExponentialBackoff } from "@copilotkit/shared"; import { EMPTY, Observable, from } from "rxjs"; import { randomUUID as randomUUID$1 } from "node:crypto"; import { EventType } from "@ag-ui/client"; import { catchError as catchError$1, finalize as finalize$1 } from "rxjs/operators"; import { Socket } from "phoenix"; //#region src/v2/runtime/runner/intelligence.ts var IntelligenceAgentRunner = class extends AgentRunner { constructor(options) { super(); this.threads = /* @__PURE__ */ new Map(); this.options = options; } /** * Create a new Phoenix socket with explicit exponential backoff. * * Each run/connect gets its own socket so that: * - A socket failure only affects a single thread, not all threads. * - Cleanup is simple: channel.leave() + socket.disconnect() tears * down everything for that run with no shared-state concerns. * - Each run gets its own independent retry budget. * * reconnectAfterMs — delay before Phoenix reconnects the WebSocket * after an unclean close. 100ms base, doubling up to maxReconnectMs (default 10s). * * rejoinAfterMs — delay before Phoenix re-joins a channel that * entered the "errored" state. 1s base, doubling up to maxRejoinMs (default 30s). * * These are set explicitly because Phoenix's default schedule is a * fixed stepped array (not exponential), and any code that calls * socket.disconnect() in an onError handler will set * closeWasClean = true and reset the reconnect timer — permanently * killing retries. */ createSocket() { const socket = new Socket(this.options.url, { ...this.options.authToken ? { authToken: this.options.authToken } : {}, reconnectAfterMs: phoenixExponentialBackoff(100, this.options.maxReconnectMs ?? 1e4), rejoinAfterMs: phoenixExponentialBackoff(1e3, this.options.maxRejoinMs ?? 3e4) }); socket.connect(); return socket; } createRunnerEventPayload(event, request, state) { const payload = { ...this.stampRunnerMetadata(this.stampCanonicalRunOwnership(event, request), state) }; payload.threadId = request.threadId; payload.runId = request.input.runId; payload.thread_id = request.threadId; payload.run_id = request.input.runId; return payload; } stampCanonicalRunOwnership(event, request) { return { ...event, threadId: request.threadId, runId: request.input.runId }; } stampRunnerMetadata(event, state) { const eventRecord = event; const existingMetadata = eventRecord.metadata ?? {}; const hasEventId = typeof existingMetadata.cpki_event_id === "string"; const hasEventSeq = typeof existingMetadata.cpki_event_seq === "number"; if (hasEventId && hasEventSeq) { const eventSeq = existingMetadata.cpki_event_seq; state.nextEventSeq = Math.max(state.nextEventSeq, eventSeq + 1); return eventRecord; } const eventSeq = state.nextEventSeq++; return { ...eventRecord, metadata: { ...existingMetadata, cpki_event_id: typeof existingMetadata.cpki_event_id === "string" ? existingMetadata.cpki_event_id : randomUUID$1(), cpki_event_seq: eventSeq } }; } run(request) { return this.createRunObservable(request); } runWithStartupBoundary(request) { let resolveStartup; let rejectStartup; const startup = new Promise((resolve, reject) => { resolveStartup = resolve; rejectStartup = reject; }); return { events: this.createRunObservable(request, { resolveStartup: () => resolveStartup?.(), rejectStartup: (error) => rejectStartup?.(error) }), startup }; } createRunObservable(request, startupBoundary) { const { threadId, agent, input } = request; if (this.threads.get(threadId)?.isRunning) throw new Error("Thread already running"); return new Observable((observer) => { const socket = this.createSocket(); const channel = socket.channel(`ingestion:${input.runId}`, { thread_id: threadId, run_id: input.runId }); const state = { socket, channel, isRunning: true, stopRequested: false, agent, currentEvents: [], nextEventSeq: 1, hasRunStarted: false }; this.threads.set(threadId, state); const MAX_CONSECUTIVE_ERRORS = 5; let consecutiveErrors = 0; socket.onError(() => { consecutiveErrors++; if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS && state.agent) try { state.agent.abortRun(); } catch {} }); socket.onOpen(() => { consecutiveErrors = 0; }); channel.on(AG_UI_CHANNEL_EVENT, (payload) => { if (payload.type === EventType.CUSTOM && payload.name === "stop") this.stop({ threadId }); }); channel.join().receive("ok", () => { startupBoundary?.resolveStartup(); this.executeAgentRun(request, state, threadId).subscribe({ complete: () => observer.complete() }); }).receive("error", (resp) => { const error = /* @__PURE__ */ new Error(`Failed to join channel: ${JSON.stringify(resp)}`); const errorEvent = { type: EventType.RUN_ERROR, message: error.message, code: "CHANNEL_JOIN_ERROR" }; observer.next(errorEvent); state.currentEvents.push(errorEvent); this.removeThread(threadId); startupBoundary?.rejectStartup(error); observer.complete(); }).receive("timeout", () => { const error = /* @__PURE__ */ new Error("Timed out joining channel"); const errorEvent = { type: EventType.RUN_ERROR, message: error.message, code: "CHANNEL_JOIN_TIMEOUT" }; observer.next(errorEvent); state.currentEvents.push(errorEvent); this.removeThread(threadId); startupBoundary?.rejectStartup(error); observer.complete(); }); return () => { this.removeThread(threadId); }; }); } connect(request) { const { threadId } = request; return new Observable((observer) => { const socket = this.createSocket(); const channel = socket.channel(`thread:${threadId}`); channel.on("ag_ui_event", (payload) => { observer.next(payload); if (payload.type === EventType.RUN_FINISHED || payload.type === EventType.RUN_ERROR) observer.complete(); }); const cleanup = () => { channel.leave(); socket.disconnect(); }; channel.join().receive("ok", () => void 0).receive("error", (resp) => { observer.error(/* @__PURE__ */ new Error(`Failed to join channel: ${JSON.stringify(resp)}`)); cleanup(); }).receive("timeout", () => { observer.error(/* @__PURE__ */ new Error("Timed out joining channel")); cleanup(); }); return () => { cleanup(); }; }); } isRunning(request) { const state = this.threads.get(request.threadId); return Promise.resolve(state?.isRunning ?? false); } stop(request) { const state = this.threads.get(request.threadId); if (!state || !state.isRunning || state.stopRequested) return Promise.resolve(false); state.stopRequested = true; if (state.agent) try { state.agent.abortRun(); } catch {} return Promise.resolve(true); } executeAgentRun(request, state, threadId) { const { currentEvents, channel } = state; const pushCanonicalEvent = (event) => { const canonicalEvent = this.stampRunnerMetadata(this.stampCanonicalRunOwnership(event, request), state); currentEvents.push(canonicalEvent); if (canonicalEvent.type === EventType.RUN_STARTED) state.hasRunStarted = true; channel.push("event", this.createRunnerEventPayload(canonicalEvent, request, state)); }; const getPersistedInputMessages = () => request.persistedInputMessages ?? request.input.messages; const buildRunStartedEvent = (source) => { const baseInput = source?.input ?? request.input; const persistedInputMessages = getPersistedInputMessages(); return { ...source ?? { type: EventType.RUN_STARTED, threadId: request.threadId, runId: request.input.runId }, threadId: request.threadId, runId: request.input.runId, input: { ...baseInput, threadId: request.threadId, runId: request.input.runId, ...persistedInputMessages !== void 0 ? { messages: persistedInputMessages } : {} } }; }; const ensureRunStarted = () => { if (!state.hasRunStarted) { state.hasRunStarted = true; pushCanonicalEvent(buildRunStartedEvent()); } }; return from(request.agent.runAgent(request.input, { onEvent: ({ event }) => { if (event.type === EventType.RUN_STARTED) { pushCanonicalEvent(buildRunStartedEvent(event)); return; } ensureRunStarted(); pushCanonicalEvent(event); } })).pipe(catchError$1((error) => { ensureRunStarted(); pushCanonicalEvent({ type: EventType.RUN_ERROR, message: error instanceof Error ? error.message : String(error) }); return EMPTY; }), finalize$1(() => { ensureRunStarted(); const appended = finalizeRunEvents(currentEvents, { stopRequested: state.stopRequested }); for (const event of appended) channel.push("event", this.createRunnerEventPayload(event, request, state)); this.removeThread(threadId); })); } /** * Tear down all resources for a thread: leave the channel, * disconnect the per-run socket, and remove the thread state. * * Idempotent — safe to call multiple times for the same threadId * (e.g. from join error handlers, finalize, and Observable teardown). */ removeThread(threadId) { const state = this.threads.get(threadId); if (!state) return; this.threads.delete(threadId); try { state.channel.leave(); } catch {} try { state.socket.disconnect(); } catch {} } }; //#endregion export { IntelligenceAgentRunner }; //# sourceMappingURL=intelligence.mjs.map