@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;" />
547 lines (545 loc) • 21.7 kB
JavaScript
require("reflect-metadata");
const require_runtime = require('../../../_virtual/_rolldown/runtime.cjs');
const require_agent_runner = require('./agent-runner.cjs');
let _copilotkit_shared = require("@copilotkit/shared");
let rxjs = require("rxjs");
let _ag_ui_client = require("@ag-ui/client");
//#region src/v2/runtime/runner/in-memory.ts
const ɵINMEMORY_DEFAULTS = {
maxThreads: 1e3,
maxRunsPerThread: 100,
maxBytes: 512 * 1024 ** 2
};
/**
* A limit value is well-formed iff it is a non-negative integer OR `+Infinity`.
* `+Infinity` is the documented "disabled/unbounded" sentinel and `0` is the
* documented run-cap disable sentinel; both are non-negative and pass. Every
* enforcement site (`evictThreadsIfNeeded`, `enforceRunCap`,
* `evictByBytesIfNeeded`) compares its counter against the limit with `>` in a
* `while`/`if` guard, so only these shapes keep those loops finite and correct.
* Rejected: negatives (drive `count > -1` true on an empty collection, so
* `enforceRunCap` `shift()!`s `undefined` and throws), `-Infinity` (loops never
* terminate their intent — always "over"), `NaN` (every `>` is false, silently
* disabling the bound), and non-integer finites (fractional caps are nonsense).
*/
function ɵisValidLimit(value) {
return value === Infinity || Number.isInteger(value) && value >= 0;
}
/**
* Normalize a fully-resolved limits bag so every field is well-formed before it
* can reach an enforcement loop. Each field is validated independently against
* {@link ɵisValidLimit}; an invalid value is CLAMPED to its
* {@link ɵINMEMORY_DEFAULTS} floor and a single `console.warn` naming the field
* and the received value is emitted.
*
* Clamp-and-warn (rather than throw) is deliberate and matches this file's
* established posture toward bad input: `ɵestimateBytes` swallows serialization
* failures and returns 0, the limits-clobber path warns rather than throwing,
* and both the eviction and clobber logs are wrapped so "logging must never
* break construction/a run". Constructing a bounded in-memory runner is a
* best-effort, non-durable convenience; a typo'd bound must degrade to a safe
* default, never abort construction or (worse) surface later as an unhandled
* rejection from the fire-and-forget finalize path.
*/
function ɵnormalizeLimits(limits) {
const normalized = { ...limits };
for (const field of Object.keys(ɵINMEMORY_DEFAULTS)) {
const value = limits[field];
if (!ɵisValidLimit(value)) {
const fallback = ɵINMEMORY_DEFAULTS[field];
normalized[field] = fallback;
try {
console.warn(`[CopilotKit] InMemoryAgentRunner: invalid ${field} value ${String(value)} (expected a non-negative integer or Infinity); falling back to ${String(fallback)}.`);
} catch {}
}
}
return normalized;
}
const EVICTION_GUIDANCE = "[CopilotKit] InMemoryAgentRunner evicted in-memory thread history to stay under memory limits. This runner is bounded and non-durable by design. For durable or production threads, configure an Intelligence backend.";
const LIMITS_CLOBBER_GUIDANCE = "[CopilotKit] InMemoryAgentRunner was constructed with in-memory limits that differ from the already-configured process-global store; the last-constructed runner's limits apply to ALL in-memory threads (the store is shared per-process). Configure a single consistent set of limits, or use an Intelligence backend for isolated bounds.";
/**
* Best-effort approximate byte size of a value, via serialized length.
* Never throws — returns 0 when the value cannot be serialized. This is an
* approximation (UTF-16 length, not exact heap bytes), used only for relative
* accounting against `maxBytes`.
*/
function ɵestimateBytes(value) {
try {
return JSON.stringify(value)?.length ?? 0;
} catch {
return 0;
}
}
var InMemoryEventStore = class {
constructor(threadId) {
this.threadId = threadId;
this.subject = null;
this.isRunning = false;
this.currentRunId = null;
this.historicRuns = [];
this.agent = null;
this.runSubject = null;
this.stopRequested = false;
this.activeFinalize = null;
this.currentEvents = null;
this.messagesSnapshot = [];
this.approxMessagesSnapshotBytes = 0;
this.createdAt = null;
}
};
var ɵBoundedThreadStore = class {
constructor(limits) {
this.map = /* @__PURE__ */ new Map();
this.totalBytes = 0;
this.warned = false;
this.limitsExplicitlySet = false;
this.clobberWarned = false;
this.limits = ɵnormalizeLimits(limits);
}
get byteTotal() {
return this.totalBytes;
}
/**
* The store's CURRENT effective bounds. Exposed (with the `ɵ` internal-API
* prefix) so a partial `setLimits` can coalesce unspecified fields against the
* live config rather than the hardcoded {@link ɵINMEMORY_DEFAULTS} — a partial
* update must be a partial update, never a silent reset of the fields the
* caller did not mention. Returns a copy so callers cannot mutate the store's
* bounds through it.
*/
get ɵlimits() {
return { ...this.limits };
}
/**
* Reconfigure the process-global store's bounds. Called by the
* {@link InMemoryAgentRunner} constructor when limits are passed. Because the
* store is a per-process singleton, this replaces the bounds for ALL in-memory
* threads. Emits {@link LIMITS_CLOBBER_GUIDANCE} at most ONCE per store when a
* SECOND (or later) explicit set arrives whose resolved values differ from the
* prior explicit set — i.e. a genuine clobber of an already-customized config.
* The first explicit customization (defaults → custom) is the intended
* override and never warns; identical re-sets never warn.
*/
setLimits(limits) {
const normalized = ɵnormalizeLimits(limits);
if (this.limitsExplicitlySet && !this.clobberWarned && (normalized.maxThreads !== this.limits.maxThreads || normalized.maxRunsPerThread !== this.limits.maxRunsPerThread || normalized.maxBytes !== this.limits.maxBytes)) {
this.clobberWarned = true;
try {
console.warn(LIMITS_CLOBBER_GUIDANCE);
} catch {}
}
this.limitsExplicitlySet = true;
this.limits = normalized;
}
get size() {
return this.map.size;
}
/** Re-insert at the tail so Map iteration order stays LRU-first. */
touchOrder(threadId, store) {
this.map.delete(threadId);
this.map.set(threadId, store);
}
getOrCreate(threadId) {
const existing = this.map.get(threadId);
if (existing) {
this.touchOrder(threadId, existing);
return existing;
}
const store = new InMemoryEventStore(threadId);
this.map.set(threadId, store);
this.evictThreadsIfNeeded(threadId);
return store;
}
get(threadId, opts) {
const store = this.map.get(threadId);
if (store && opts.touch) this.touchOrder(threadId, store);
return store;
}
peek(threadId) {
return this.map.get(threadId);
}
/**
* Evict the least-recently-used thread that is neither running NOR
* mid-finalization. Returns false if none evictable. The `protect` thread
* (typically the one just created) is never evicted, so a fresh thread is not
* immediately dropped when it is the only non-running candidate.
*
* A thread is skipped while `isRunning` OR `stopRequested` is set.
* `stop()` flips `isRunning` to false the moment it aborts the agent, but the
* run keeps finalizing asynchronously (the abort trips the `catch` in
* `runAgent`, which later calls `appendRun`). During that window
* `stopRequested` stays true; evicting the thread then would make the pending
* `appendRun` hit `if (!store) return` and silently drop the aborted run's
* history. Guarding on `stopRequested` keeps the thread alive until
* finalization completes.
*/
evictOneLru(protect) {
for (const [threadId, store] of this.map) {
if (threadId === protect) continue;
if (store.isRunning || store.stopRequested) continue;
this.removeThread(threadId, store);
this.noteEviction();
return true;
}
return false;
}
appendRun(threadId, run) {
const store = this.map.get(threadId);
if (!store) return;
if (store.createdAt === null) store.createdAt = run.createdAt;
if (run.messages.length > 0) {
this.totalBytes -= store.approxMessagesSnapshotBytes;
store.messagesSnapshot = run.messages;
store.approxMessagesSnapshotBytes = ɵestimateBytes(run.messages);
this.totalBytes += store.approxMessagesSnapshotBytes;
}
run.messages = [];
run.approxMessageBytes = 0;
run.approxEventBytes = ɵestimateBytes(run.events);
store.historicRuns.push(run);
this.totalBytes += run.approxEventBytes;
this.touchOrder(threadId, store);
this.enforceRunCap(store);
this.evictByBytesIfNeeded(threadId);
}
enforceRunCap(store) {
const cap = this.limits.maxRunsPerThread;
if (!cap || cap === Infinity) return;
while (store.historicRuns.length > cap) {
const dropped = store.historicRuns.shift();
this.totalBytes -= dropped.approxEventBytes ?? 0;
this.noteEviction();
}
}
/**
* Trim the store back under the byte ceiling by evicting LRU non-running
* threads. `protect` (the just-appended thread) is never self-evicted, so a
* fresh run pushes OTHER threads out rather than dropping itself.
*/
evictByBytesIfNeeded(protect) {
while (this.totalBytes > this.limits.maxBytes) if (!this.evictOneLru(protect)) break;
}
removeThread(threadId, store) {
for (const run of store.historicRuns) this.totalBytes -= run.approxEventBytes ?? 0;
this.totalBytes -= store.approxMessagesSnapshotBytes;
this.map.delete(threadId);
}
evictThreadsIfNeeded(protect) {
while (this.map.size > this.limits.maxThreads) if (!this.evictOneLru(protect)) break;
}
noteEviction() {
if (this.warned) return;
this.warned = true;
try {
console.warn(EVICTION_GUIDANCE);
} catch {}
}
listThreads() {
const threads = [];
for (const [threadId, store] of this.map) {
if (store.historicRuns.length === 0) continue;
const lastRun = store.historicRuns[store.historicRuns.length - 1];
threads.push({
id: threadId,
name: null,
agentId: lastRun.agentId,
organizationId: "",
createdById: "",
archived: false,
createdAt: new Date(store.createdAt ?? lastRun.createdAt).toISOString(),
updatedAt: new Date(lastRun.createdAt).toISOString()
});
}
return threads.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
}
clear() {
this.map.clear();
this.totalBytes = 0;
this.warned = false;
}
};
/**
* Process-wide singleton backing every {@link InMemoryAgentRunner}. Exported
* (with the `ɵ` internal-API prefix) so tests can inspect the exact store the
* runner writes to; not part of the public API.
*/
const ɵGLOBAL_STORE = new ɵBoundedThreadStore(ɵINMEMORY_DEFAULTS);
const sharedStore = ɵGLOBAL_STORE;
var InMemoryAgentRunner = class extends require_agent_runner.AgentRunner {
/**
* @param options Per-runner behavior (`onConcurrentRun`) plus optional bounds
* for the in-memory store ({@link InMemoryLimits}).
*
* Note the differing scopes: `onConcurrentRun` is per-runner instance, while
* the limits reconfigure the PROCESS-GLOBAL store shared by every
* `InMemoryAgentRunner`. Omit the limits for safe defaults
* ({@link ɵINMEMORY_DEFAULTS}); passing none leaves the store untouched. When
* multiple runners are constructed with differing limits, the last-constructed
* wins — in practice the OSS/SSE default construction passes nothing. If a
* second (or later) runner is constructed with limits that DIFFER from an
* already-customized store, a one-time `console.warn` is emitted to signal that
* the shared store's bounds are being clobbered for ALL in-memory threads.
*/
constructor(options) {
super();
this.ɵsupportsLocalThreadEndpoints = true;
const { onConcurrentRun, ...limits } = options ?? {};
this.onConcurrentRun = onConcurrentRun ?? "throw";
if (limits.maxThreads !== void 0 || limits.maxRunsPerThread !== void 0 || limits.maxBytes !== void 0) {
const current = sharedStore.ɵlimits;
sharedStore.setLimits({
maxThreads: limits.maxThreads ?? current.maxThreads,
maxRunsPerThread: limits.maxRunsPerThread ?? current.maxRunsPerThread,
maxBytes: limits.maxBytes ?? current.maxBytes
});
}
}
run(request) {
const store = sharedStore.getOrCreate(request.threadId);
if (store.isRunning || store.stopRequested) {
if (this.onConcurrentRun !== "supersede") throw new Error("Thread already running");
const priorAgent = store.agent;
const priorFinalize = store.activeFinalize;
if (priorFinalize) priorFinalize.stopRequested = true;
store.isRunning = false;
if (priorAgent) try {
priorAgent.abortRun();
} catch (error) {
console.error("Failed to abort superseded run", error);
}
}
store.isRunning = true;
store.currentRunId = request.input.runId;
store.agent = request.agent;
store.stopRequested = false;
const finalizeControl = { stopRequested: false };
store.activeFinalize = finalizeControl;
const seenMessageIds = /* @__PURE__ */ new Set();
const currentRunEvents = [];
store.currentEvents = currentRunEvents;
const historicMessageIds = /* @__PURE__ */ new Set();
for (const run of store.historicRuns) for (const event of run.events) {
if ("messageId" in event && typeof event.messageId === "string") historicMessageIds.add(event.messageId);
if (event.type === _ag_ui_client.EventType.RUN_STARTED) {
const messages = event.input?.messages ?? [];
for (const message of messages) historicMessageIds.add(message.id);
}
}
const nextSubject = new rxjs.ReplaySubject(Infinity);
store.subject = nextSubject;
const runSubject = new rxjs.ReplaySubject(Infinity);
store.runSubject = runSubject;
const runAgent = async () => {
const parentRunId = store.historicRuns[store.historicRuns.length - 1]?.runId ?? null;
const finalizeRun = (opts) => {
const isError = opts.interruptionMessage !== void 0;
const preFinalizeEventCount = currentRunEvents.length;
const appendedEvents = (0, _copilotkit_shared.finalizeRunEvents)(currentRunEvents, {
stopRequested: finalizeControl.stopRequested,
...isError ? { interruptionMessage: opts.interruptionMessage } : {}
});
for (const event of appendedEvents) {
runSubject.next(event);
nextSubject.next(event);
}
const ownsThread = store.currentRunId === request.input.runId;
if (ownsThread && (!isError || preFinalizeEventCount > 0)) {
const compactedEvents = (0, _ag_ui_client.compactEvents)(currentRunEvents);
sharedStore.appendRun(request.threadId, {
threadId: request.threadId,
runId: request.input.runId,
agentId: request.agent.agentId ?? "default",
parentRunId,
events: compactedEvents,
messages: Array.isArray(request.agent.messages) ? [...request.agent.messages] : [],
createdAt: Date.now()
});
}
if (ownsThread) {
store.currentEvents = null;
store.currentRunId = null;
store.agent = null;
store.runSubject = null;
store.stopRequested = false;
store.isRunning = false;
store.activeFinalize = null;
}
runSubject.complete();
nextSubject.complete();
if (store.subject === nextSubject) store.subject = null;
};
try {
await request.agent.runAgent(request.input, {
onEvent: ({ event }) => {
let processedEvent = event;
if (event.type === _ag_ui_client.EventType.RUN_STARTED) {
const runStartedEvent = event;
if (!runStartedEvent.input) {
const sanitizedMessages = request.input.messages ? request.input.messages.filter((message) => !historicMessageIds.has(message.id)) : void 0;
runStartedEvent.input = {
...request.input,
...sanitizedMessages !== void 0 ? { messages: sanitizedMessages } : {}
};
processedEvent = runStartedEvent;
}
}
runSubject.next(processedEvent);
nextSubject.next(processedEvent);
currentRunEvents.push(processedEvent);
},
onNewMessage: ({ message }) => {
if (!seenMessageIds.has(message.id)) seenMessageIds.add(message.id);
},
onRunStartedEvent: () => {
if (request.input.messages) {
for (const message of request.input.messages) if (!seenMessageIds.has(message.id)) seenMessageIds.add(message.id);
}
}
});
finalizeRun({});
} catch (error) {
finalizeRun({ interruptionMessage: error instanceof Error ? error.message : String(error) });
}
};
runAgent();
return runSubject.asObservable();
}
connect(request) {
const store = sharedStore.get(request.threadId, { touch: true });
const connectionSubject = new rxjs.ReplaySubject(Infinity);
if (!store) {
connectionSubject.complete();
return connectionSubject.asObservable();
}
const allHistoricEvents = [];
for (const run of store.historicRuns) allHistoricEvents.push(...run.events);
const compactedEvents = (0, _ag_ui_client.compactEvents)(allHistoricEvents);
const emittedMessageIds = /* @__PURE__ */ new Set();
for (const event of compactedEvents) {
connectionSubject.next(event);
if ("messageId" in event && typeof event.messageId === "string") emittedMessageIds.add(event.messageId);
}
if (store.subject && (store.isRunning || store.stopRequested)) store.subject.subscribe({
next: (event) => {
if ("messageId" in event && typeof event.messageId === "string" && emittedMessageIds.has(event.messageId)) return;
connectionSubject.next(event);
},
complete: () => connectionSubject.complete(),
error: (err) => connectionSubject.error(err)
});
else connectionSubject.complete();
return connectionSubject.asObservable();
}
isRunning(request) {
const store = sharedStore.peek(request.threadId);
return Promise.resolve(store?.isRunning ?? false);
}
stop(request) {
const store = sharedStore.peek(request.threadId);
if (!store || !store.isRunning) return Promise.resolve(false);
if (request.runId !== void 0 && store.currentRunId !== request.runId) return Promise.resolve(false);
if (store.stopRequested) return Promise.resolve(false);
store.stopRequested = true;
store.isRunning = false;
const finalizeControl = store.activeFinalize;
if (finalizeControl) finalizeControl.stopRequested = true;
const agent = store.agent;
if (!agent) {
store.stopRequested = false;
store.isRunning = false;
if (finalizeControl) finalizeControl.stopRequested = false;
return Promise.resolve(false);
}
try {
agent.abortRun();
return Promise.resolve(true);
} catch (error) {
console.error("Failed to abort agent run", error);
store.stopRequested = false;
store.isRunning = true;
if (finalizeControl) finalizeControl.stopRequested = false;
return Promise.resolve(false);
}
}
/**
* Returns a summary of every thread that has been run through this runner.
*
* This powers the local-dev fallback for `GET /threads` when the Intelligence
* platform is not configured. Each entry mirrors the shape of a platform
* `ThreadRecord` so the HTTP handler can use the same response envelope.
*/
listThreads() {
return sharedStore.listThreads();
}
/**
* Returns all messages for a thread, using the snapshot captured at the end
* of the most recent run.
*
* This powers the local-dev fallback for `GET /threads/:threadId/messages`
* when the Intelligence platform is not configured. The returned `Message[]`
* objects come directly from the ag-ui agent, so their shape is compatible
* with the Intelligence platform's `ThreadMessage` type.
*/
getThreadMessages(threadId) {
const store = sharedStore.peek(threadId);
if (!store) return [];
return [...store.messagesSnapshot];
}
/**
* Returns all AG-UI events for a thread, compacted across historic runs.
*
* Powers the local-dev fallback for `GET /threads/:threadId/events` when the
* Intelligence platform is not configured. The compaction logic matches
* the connection-replay path in {@link connect}, so the stream a
* late-joining inspector sees matches what this method returns.
*/
getThreadEvents(threadId) {
const store = sharedStore.peek(threadId);
if (!store || store.historicRuns.length === 0) return [];
const all = [];
for (const run of store.historicRuns) all.push(...run.events);
return (0, _ag_ui_client.compactEvents)(all);
}
/**
* Returns the agent state snapshot for a thread.
*
* Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The
* AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces
* a single trailing STATE_SNAPSHOT when state changes exist, so this is a
* faithful view of state at the end of the most recent run.
*
* Returns `null` when the thread has never emitted a STATE_SNAPSHOT.
*/
getThreadState(threadId) {
const events = this.getThreadEvents(threadId);
for (let i = events.length - 1; i >= 0; i--) {
const event = events[i];
if (event.type === _ag_ui_client.EventType.STATE_SNAPSHOT) {
const snapshot = event.snapshot;
if (snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)) return { ...snapshot };
return null;
}
}
return null;
}
/**
* Clears all in-memory thread history.
*
* Powers the local-dev fallback for `POST /threads/clear`, letting consumers
* (e.g. the demo's Clear button) reset to an empty thread list without
* restarting the runtime. Intentionally not exposed on the Intelligence
* platform path: there, thread history lives in a real database and must
* not be wiped this way.
*/
clearThreads() {
sharedStore.clear();
}
};
//#endregion
exports.InMemoryAgentRunner = InMemoryAgentRunner;
exports.ɵBoundedThreadStore = ɵBoundedThreadStore;
exports.ɵGLOBAL_STORE = ɵGLOBAL_STORE;
exports.ɵINMEMORY_DEFAULTS = ɵINMEMORY_DEFAULTS;
exports.ɵestimateBytes = ɵestimateBytes;
exports.ɵnormalizeLimits = ɵnormalizeLimits;
//# sourceMappingURL=in-memory.cjs.map