@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
JavaScript
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