@copilotkit/runtime 1.66.2 → 1.66.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/converters/aisdk.cjs +17 -9
- package/dist/agent/converters/aisdk.cjs.map +1 -1
- package/dist/agent/converters/aisdk.d.cts +1 -1
- package/dist/agent/converters/aisdk.d.cts.map +1 -1
- package/dist/agent/converters/aisdk.d.mts +1 -1
- package/dist/agent/converters/aisdk.d.mts.map +1 -1
- package/dist/agent/converters/aisdk.mjs +17 -9
- package/dist/agent/converters/aisdk.mjs.map +1 -1
- package/dist/agent/converters/tanstack.cjs +17 -9
- package/dist/agent/converters/tanstack.cjs.map +1 -1
- package/dist/agent/converters/tanstack.d.cts +1 -1
- package/dist/agent/converters/tanstack.d.cts.map +1 -1
- package/dist/agent/converters/tanstack.d.mts +1 -1
- package/dist/agent/converters/tanstack.d.mts.map +1 -1
- package/dist/agent/converters/tanstack.mjs +17 -9
- package/dist/agent/converters/tanstack.mjs.map +1 -1
- package/dist/agent/index.cjs +6 -4
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +6 -4
- package/dist/agent/index.mjs.map +1 -1
- package/dist/agent/state-delta.cjs +322 -0
- package/dist/agent/state-delta.cjs.map +1 -0
- package/dist/agent/state-delta.mjs +321 -0
- package/dist/agent/state-delta.mjs.map +1 -0
- package/dist/package.cjs +1 -1
- package/dist/runtime/package.mjs +1 -1
- package/dist/v2/index.cjs +6 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/runtime/core/channel-manager.cjs +110 -9
- package/dist/v2/runtime/core/channel-manager.cjs.map +1 -1
- package/dist/v2/runtime/core/channel-manager.d.cts +79 -8
- package/dist/v2/runtime/core/channel-manager.d.cts.map +1 -1
- package/dist/v2/runtime/core/channel-manager.d.mts +79 -8
- package/dist/v2/runtime/core/channel-manager.d.mts.map +1 -1
- package/dist/v2/runtime/core/channel-manager.mjs +110 -9
- package/dist/v2/runtime/core/channel-manager.mjs.map +1 -1
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +1 -0
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +1 -0
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs +2 -5
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs +2 -5
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/mcp-apps-servers.cjs +23 -0
- package/dist/v2/runtime/handlers/shared/mcp-apps-servers.cjs.map +1 -0
- package/dist/v2/runtime/handlers/shared/mcp-apps-servers.mjs +22 -0
- package/dist/v2/runtime/handlers/shared/mcp-apps-servers.mjs.map +1 -0
- package/dist/v2/runtime/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -1
- package/dist/v2/runtime/open-generative-ui-middleware.cjs +1 -0
- package/dist/v2/runtime/open-generative-ui-middleware.cjs.map +1 -1
- package/dist/v2/runtime/open-generative-ui-middleware.mjs +1 -0
- package/dist/v2/runtime/open-generative-ui-middleware.mjs.map +1 -1
- package/dist/v2/runtime/runner/in-memory.cjs +335 -101
- package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
- package/dist/v2/runtime/runner/in-memory.d.cts +246 -6
- package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
- package/dist/v2/runtime/runner/in-memory.d.mts +246 -6
- package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
- package/dist/v2/runtime/runner/in-memory.mjs +331 -102
- package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
- package/dist/v2/runtime/runner/index.d.cts +1 -1
- package/dist/v2/runtime/runner/index.d.mts +1 -1
- package/dist/v2/runtime/runner/index.mjs +1 -1
- package/package.json +5 -5
- package/skills/runtime/references/agent-runners-in-memory.md +49 -34
- package/skills/runtime/references/agent-runners.md +47 -21
- package/skills/runtime/references/wiring-mcp-apps-middleware.md +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory.mjs","names":[],"sources":["../../../../src/v2/runtime/runner/in-memory.ts"],"sourcesContent":["import type {\n AgentRunnerConnectRequest,\n AgentRunnerIsRunningRequest,\n AgentRunnerRunRequest,\n} from \"./agent-runner\";\nimport { AgentRunner } from \"./agent-runner\";\nimport type { AgentRunnerStopRequest } from \"./agent-runner\";\nimport type { Observable } from \"rxjs\";\nimport { ReplaySubject } from \"rxjs\";\nimport type {\n AbstractAgent,\n BaseEvent,\n Message,\n RunStartedEvent,\n StateSnapshotEvent,\n} from \"@ag-ui/client\";\nimport { EventType, compactEvents } from \"@ag-ui/client\";\nimport { finalizeRunEvents } from \"@copilotkit/shared\";\n\ninterface HistoricRun {\n threadId: string;\n runId: string;\n /** ID of the agent that executed this run. */\n agentId: string;\n parentRunId: string | null;\n events: BaseEvent[];\n /**\n * Snapshot of all messages (input + generated) at the end of this run.\n * Used by the local thread-messages fallback endpoint.\n */\n messages: Message[];\n createdAt: number;\n}\n\n/**\n * Lightweight thread summary returned by {@link InMemoryAgentRunner.listThreads}.\n * Shape matches the Intelligence platform's ThreadRecord so the same HTTP\n * response envelope can be used for both backends.\n */\nexport interface InMemoryThread {\n id: string;\n name: string | null;\n agentId: string;\n organizationId: \"\"; // always empty in in-memory mode\n createdById: \"\"; // always empty in in-memory mode\n archived: false; // always false in in-memory mode\n createdAt: string;\n updatedAt: string;\n}\n\nclass InMemoryEventStore {\n constructor(public threadId: string) {}\n\n /** The subject that current consumers subscribe to. */\n subject: ReplaySubject<BaseEvent> | null = null;\n\n /** True while a run is actively producing events. */\n isRunning = false;\n\n /** Current run ID */\n currentRunId: string | null = null;\n\n /** Historic completed runs */\n historicRuns: HistoricRun[] = [];\n\n /** Currently running agent instance (if any). */\n agent: AbstractAgent | null = null;\n\n /** Subject returned from run() while the run is active. */\n runSubject: ReplaySubject<BaseEvent> | null = null;\n\n /** True once stop() has been requested but the run has not yet finalized. */\n stopRequested = false;\n\n /** Reference to the events emitted in the current run. */\n currentEvents: BaseEvent[] | null = null;\n}\n\nconst GLOBAL_STORE = new Map<string, InMemoryEventStore>();\n\nexport class InMemoryAgentRunner extends AgentRunner {\n readonly ɵsupportsLocalThreadEndpoints = true;\n\n /**\n * How to handle a `run()` for a thread that already has an in-flight run.\n * `\"throw\"` (default) preserves the historic behavior. `\"supersede\"` aborts\n * the prior run (mirroring `stop()`) and starts the new one — opted into by\n * the hosted-bot listener so a fast follow-up turn on the same thread cleanly\n * replaces a still-running (or wedged) prior turn instead of erroring with\n * \"Thread already running\".\n */\n private readonly onConcurrentRun: \"throw\" | \"supersede\";\n\n constructor(options?: { onConcurrentRun?: \"throw\" | \"supersede\" }) {\n super();\n this.onConcurrentRun = options?.onConcurrentRun ?? \"throw\";\n }\n\n run(request: AgentRunnerRunRequest): Observable<BaseEvent> {\n let existingStore = GLOBAL_STORE.get(request.threadId);\n if (!existingStore) {\n existingStore = new InMemoryEventStore(request.threadId);\n GLOBAL_STORE.set(request.threadId, existingStore);\n }\n const store = existingStore; // Now store is const and non-null\n\n if (store.isRunning) {\n if (this.onConcurrentRun !== \"supersede\") {\n throw new Error(\"Thread already running\");\n }\n // Supersede: abort the prior (possibly wedged) run so this one can start.\n // Mirrors stop(). The prior run's async finalization runs later and is\n // prevented from clobbering this run's state by the run-id guard below.\n const priorAgent = store.agent;\n store.stopRequested = true;\n store.isRunning = false;\n if (priorAgent) {\n try {\n priorAgent.abortRun();\n } catch (error) {\n console.error(\"Failed to abort superseded run\", error);\n }\n }\n store.stopRequested = false;\n }\n store.isRunning = true;\n store.currentRunId = request.input.runId;\n store.agent = request.agent;\n store.stopRequested = false;\n\n // Track seen message IDs and current run events for this run\n const seenMessageIds = new Set<string>();\n const currentRunEvents: BaseEvent[] = [];\n store.currentEvents = currentRunEvents;\n\n // Get all previously seen message IDs from historic runs\n const historicMessageIds = new Set<string>();\n for (const run of store.historicRuns) {\n for (const event of run.events) {\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n historicMessageIds.add(event.messageId);\n }\n if (event.type === EventType.RUN_STARTED) {\n const runStarted = event as RunStartedEvent;\n const messages = runStarted.input?.messages ?? [];\n for (const message of messages) {\n historicMessageIds.add(message.id);\n }\n }\n }\n }\n\n const nextSubject = new ReplaySubject<BaseEvent>(Infinity);\n const prevSubject = store.subject;\n\n // Update the store's subject immediately\n store.subject = nextSubject;\n\n // Create a subject for run() return value\n const runSubject = new ReplaySubject<BaseEvent>(Infinity);\n store.runSubject = runSubject;\n\n // Helper function to run the agent and handle errors\n const runAgent = async () => {\n // Get parent run ID for chaining\n const lastRun = store.historicRuns[store.historicRuns.length - 1];\n const parentRunId = lastRun?.runId ?? null;\n\n try {\n await request.agent.runAgent(request.input, {\n onEvent: ({ event }) => {\n let processedEvent: BaseEvent = event;\n if (event.type === EventType.RUN_STARTED) {\n const runStartedEvent = event as RunStartedEvent;\n if (!runStartedEvent.input) {\n const sanitizedMessages = request.input.messages\n ? request.input.messages.filter(\n (message) => !historicMessageIds.has(message.id),\n )\n : undefined;\n const updatedInput = {\n ...request.input,\n ...(sanitizedMessages !== undefined\n ? { messages: sanitizedMessages }\n : {}),\n };\n runStartedEvent.input = updatedInput;\n processedEvent = runStartedEvent;\n }\n }\n\n runSubject.next(processedEvent); // For run() return - only agent events\n nextSubject.next(processedEvent); // For connect() / store - all events\n currentRunEvents.push(processedEvent); // Accumulate for storage\n },\n onNewMessage: ({ message }) => {\n // Called for each new message\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n },\n onRunStartedEvent: () => {\n // Mark any messages from the input as seen so they aren't emitted twice\n if (request.input.messages) {\n for (const message of request.input.messages) {\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n }\n }\n },\n });\n\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: store.stopRequested,\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Store the completed run in memory with ONLY its events. Guard on the\n // per-run id (not the shared `store.currentRunId`): a superseded run no\n // longer owns the store, so it must not push history — and never under\n // a newer run's id, which would corrupt the thread's history.\n if (store.currentRunId === request.input.runId) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n\n store.historicRuns.push({\n threadId: request.threadId,\n runId: request.input.runId,\n agentId: request.agent.agentId ?? \"default\",\n parentRunId,\n events: compactedEvents,\n // Snapshot all messages (input + generated) for the thread-messages endpoint\n messages: Array.isArray(request.agent.messages)\n ? [...request.agent.messages]\n : [],\n createdAt: Date.now(),\n });\n }\n\n // Complete the run. Guard the shared-store reset: if a newer run has\n // superseded this one (`currentRunId` changed), that run now owns the\n // store — don't clobber its state. Always complete THIS run's subjects.\n if (store.currentRunId === request.input.runId) {\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n }\n runSubject.complete();\n nextSubject.complete();\n } catch (error) {\n const interruptionMessage =\n error instanceof Error ? error.message : String(error);\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: store.stopRequested,\n interruptionMessage,\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Store the run even if it failed (partial events). Same per-run guard:\n // a superseded run's error teardown must not push history under the\n // newer run's id (see success-path note).\n if (\n store.currentRunId === request.input.runId &&\n currentRunEvents.length > 0\n ) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n store.historicRuns.push({\n threadId: request.threadId,\n runId: request.input.runId,\n agentId: request.agent.agentId ?? \"default\",\n parentRunId,\n events: compactedEvents,\n messages: Array.isArray(request.agent.messages)\n ? [...request.agent.messages]\n : [],\n createdAt: Date.now(),\n });\n }\n\n // Complete the run (see success-path note). Same run-id guard so a\n // superseded run's error teardown can't reset the newer run's state.\n if (store.currentRunId === request.input.runId) {\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n }\n runSubject.complete();\n nextSubject.complete();\n }\n };\n\n // Bridge previous events if they exist\n if (prevSubject) {\n prevSubject.subscribe({\n next: (e) => nextSubject.next(e),\n error: (err) => nextSubject.error(err),\n complete: () => {\n // Don't complete nextSubject here - it needs to stay open for new events\n },\n });\n }\n\n // Start the agent execution immediately (not lazily)\n runAgent();\n\n // Return the run subject (only agent events, no injected messages)\n return runSubject.asObservable();\n }\n\n connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {\n const store = GLOBAL_STORE.get(request.threadId);\n const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);\n\n if (!store) {\n // No store means no events\n connectionSubject.complete();\n return connectionSubject.asObservable();\n }\n\n // Collect all historic events from memory\n const allHistoricEvents: BaseEvent[] = [];\n for (const run of store.historicRuns) {\n allHistoricEvents.push(...run.events);\n }\n\n // Apply compaction to all historic events together (like SQLite)\n const compactedEvents = compactEvents(allHistoricEvents);\n\n // Emit compacted events and track message IDs\n const emittedMessageIds = new Set<string>();\n for (const event of compactedEvents) {\n connectionSubject.next(event);\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n emittedMessageIds.add(event.messageId);\n }\n }\n\n // Bridge active run to connection if exists\n if (store.subject && (store.isRunning || store.stopRequested)) {\n store.subject.subscribe({\n next: (event) => {\n // Skip message events that we've already emitted from historic\n if (\n \"messageId\" in event &&\n typeof event.messageId === \"string\" &&\n emittedMessageIds.has(event.messageId)\n ) {\n return;\n }\n connectionSubject.next(event);\n },\n complete: () => connectionSubject.complete(),\n error: (err) => connectionSubject.error(err),\n });\n } else {\n // No active run, complete after historic events\n connectionSubject.complete();\n }\n\n return connectionSubject.asObservable();\n }\n\n isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {\n const store = GLOBAL_STORE.get(request.threadId);\n return Promise.resolve(store?.isRunning ?? false);\n }\n\n stop(request: AgentRunnerStopRequest): Promise<boolean | undefined> {\n const store = GLOBAL_STORE.get(request.threadId);\n if (!store || !store.isRunning) {\n return Promise.resolve(false);\n }\n if (request.runId !== undefined && store.currentRunId !== request.runId) {\n return Promise.resolve(false);\n }\n if (store.stopRequested) {\n return Promise.resolve(false);\n }\n\n store.stopRequested = true;\n store.isRunning = false;\n\n const agent = store.agent;\n if (!agent) {\n store.stopRequested = false;\n store.isRunning = false;\n return Promise.resolve(false);\n }\n\n try {\n agent.abortRun();\n return Promise.resolve(true);\n } catch (error) {\n console.error(\"Failed to abort agent run\", error);\n store.stopRequested = false;\n store.isRunning = true;\n return Promise.resolve(false);\n }\n }\n\n /**\n * Returns a summary of every thread that has been run through this runner.\n *\n * This powers the local-dev fallback for `GET /threads` when the Intelligence\n * platform is not configured. Each entry mirrors the shape of a platform\n * `ThreadRecord` so the HTTP handler can use the same response envelope.\n */\n listThreads(): InMemoryThread[] {\n const threads: InMemoryThread[] = [];\n for (const [threadId, store] of GLOBAL_STORE) {\n if (store.historicRuns.length === 0) continue;\n const firstRun = store.historicRuns[0]!;\n const lastRun = store.historicRuns[store.historicRuns.length - 1]!;\n threads.push({\n id: threadId,\n name: null,\n agentId: lastRun.agentId,\n organizationId: \"\",\n createdById: \"\",\n archived: false,\n createdAt: new Date(firstRun.createdAt).toISOString(),\n updatedAt: new Date(lastRun.createdAt).toISOString(),\n });\n }\n // Most recently updated first\n return threads.sort(\n (a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),\n );\n }\n\n /**\n * Returns all messages for a thread, using the snapshot captured at the end\n * of the most recent run.\n *\n * This powers the local-dev fallback for `GET /threads/:threadId/messages`\n * when the Intelligence platform is not configured. The returned `Message[]`\n * objects come directly from the ag-ui agent, so their shape is compatible\n * with the Intelligence platform's `ThreadMessage` type.\n */\n getThreadMessages(threadId: string): Message[] {\n const store = GLOBAL_STORE.get(threadId);\n if (!store || store.historicRuns.length === 0) return [];\n // The last run's snapshot has the complete conversation history\n return store.historicRuns[store.historicRuns.length - 1]!.messages;\n }\n\n /**\n * Returns all AG-UI events for a thread, compacted across historic runs.\n *\n * Powers the local-dev fallback for `GET /threads/:threadId/events` when the\n * Intelligence platform is not configured. The compaction logic matches\n * the connection-replay path in {@link connect}, so the stream a\n * late-joining inspector sees matches what this method returns.\n */\n getThreadEvents(threadId: string): BaseEvent[] {\n const store = GLOBAL_STORE.get(threadId);\n if (!store || store.historicRuns.length === 0) return [];\n const all: BaseEvent[] = [];\n for (const run of store.historicRuns) all.push(...run.events);\n return compactEvents(all);\n }\n\n /**\n * Returns the agent state snapshot for a thread.\n *\n * Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The\n * AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces\n * a single trailing STATE_SNAPSHOT when state changes exist, so this is a\n * faithful view of state at the end of the most recent run.\n *\n * Returns `null` when the thread has never emitted a STATE_SNAPSHOT.\n */\n getThreadState(threadId: string): Record<string, unknown> | null {\n const events = this.getThreadEvents(threadId);\n // Walk backwards — the last snapshot wins.\n for (let i = events.length - 1; i >= 0; i--) {\n const event = events[i]!;\n if (event.type === EventType.STATE_SNAPSHOT) {\n const snapshot = (event as StateSnapshotEvent).snapshot;\n if (snapshot && typeof snapshot === \"object\") {\n return snapshot as Record<string, unknown>;\n }\n return null;\n }\n }\n return null;\n }\n\n /**\n * Clears all in-memory thread history.\n *\n * Powers the local-dev fallback for `POST /threads/clear`, letting consumers\n * (e.g. the demo's Clear button) reset to an empty thread list without\n * restarting the runtime. Intentionally not exposed on the Intelligence\n * platform path: there, thread history lives in a real database and must\n * not be wiped this way.\n */\n clearThreads(): void {\n GLOBAL_STORE.clear();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"in-memory.mjs","names":[],"sources":["../../../../src/v2/runtime/runner/in-memory.ts"],"sourcesContent":["import type {\n AgentRunnerConnectRequest,\n AgentRunnerIsRunningRequest,\n AgentRunnerRunRequest,\n} from \"./agent-runner\";\nimport { AgentRunner } from \"./agent-runner\";\nimport type { AgentRunnerStopRequest } from \"./agent-runner\";\nimport type { Observable } from \"rxjs\";\nimport { ReplaySubject } from \"rxjs\";\nimport type {\n AbstractAgent,\n BaseEvent,\n Message,\n RunStartedEvent,\n StateSnapshotEvent,\n} from \"@ag-ui/client\";\nimport { EventType, compactEvents } from \"@ag-ui/client\";\nimport { finalizeRunEvents } from \"@copilotkit/shared\";\n\nexport interface InMemoryLimits {\n /** LRU cap on distinct threads. */\n maxThreads?: number;\n /** FIFO cap on runs kept per thread. `Infinity` or `0` disables the cap. */\n maxRunsPerThread?: number;\n /**\n * Approximate byte ceiling on RETAINED thread/run history. Enforced at run\n * completion (in `appendRun`), where LRU non-running threads are evicted to\n * keep the total under this limit.\n *\n * Limitation: this bounds only history that has already been committed. A\n * single in-flight run's buffered events (`currentRunEvents` and the two\n * `ReplaySubject<BaseEvent>(Infinity)` buffers in `run()`) are NOT counted\n * until that run completes, so `maxBytes` does not bound a single runaway\n * run mid-stream.\n *\n * Limitation: byte eviction drops only other LRU non-running threads and\n * never self-evicts the active/just-appended thread, so a single dominant\n * thread's own retained history is not byte-trimmed (bounded only by\n * `maxRunsPerThread`). `maxBytes` is thus a cross-thread ceiling enforced by\n * evicting OTHER threads, not a per-thread cap.\n */\n maxBytes?: number;\n}\n\n/**\n * Constructor options for {@link InMemoryAgentRunner}.\n *\n * Extends {@link InMemoryLimits} so bounds can be passed inline alongside the\n * per-runner behavior flags. Be aware of the scope difference: the limits\n * reconfigure the process-global store shared by every runner, whereas\n * `onConcurrentRun` applies only to the runner instance it is passed to.\n */\nexport interface InMemoryAgentRunnerOptions extends InMemoryLimits {\n /**\n * How to handle a `run()` for a thread that already has an in-flight run.\n * `\"throw\"` (default) rejects with \"Thread already running\". `\"supersede\"`\n * aborts the prior run and starts the new one.\n */\n onConcurrentRun?: \"throw\" | \"supersede\";\n}\n\nexport const ɵINMEMORY_DEFAULTS: Required<InMemoryLimits> = {\n maxThreads: 1000,\n maxRunsPerThread: 100,\n maxBytes: 512 * 1024 ** 2,\n};\n\n/**\n * A limit value is well-formed iff it is a non-negative integer OR `+Infinity`.\n * `+Infinity` is the documented \"disabled/unbounded\" sentinel and `0` is the\n * documented run-cap disable sentinel; both are non-negative and pass. Every\n * enforcement site (`evictThreadsIfNeeded`, `enforceRunCap`,\n * `evictByBytesIfNeeded`) compares its counter against the limit with `>` in a\n * `while`/`if` guard, so only these shapes keep those loops finite and correct.\n * Rejected: negatives (drive `count > -1` true on an empty collection, so\n * `enforceRunCap` `shift()!`s `undefined` and throws), `-Infinity` (loops never\n * terminate their intent — always \"over\"), `NaN` (every `>` is false, silently\n * disabling the bound), and non-integer finites (fractional caps are nonsense).\n */\nfunction ɵisValidLimit(value: number): boolean {\n return value === Infinity || (Number.isInteger(value) && value >= 0);\n}\n\n/**\n * Normalize a fully-resolved limits bag so every field is well-formed before it\n * can reach an enforcement loop. Each field is validated independently against\n * {@link ɵisValidLimit}; an invalid value is CLAMPED to its\n * {@link ɵINMEMORY_DEFAULTS} floor and a single `console.warn` naming the field\n * and the received value is emitted.\n *\n * Clamp-and-warn (rather than throw) is deliberate and matches this file's\n * established posture toward bad input: `ɵestimateBytes` swallows serialization\n * failures and returns 0, the limits-clobber path warns rather than throwing,\n * and both the eviction and clobber logs are wrapped so \"logging must never\n * break construction/a run\". Constructing a bounded in-memory runner is a\n * best-effort, non-durable convenience; a typo'd bound must degrade to a safe\n * default, never abort construction or (worse) surface later as an unhandled\n * rejection from the fire-and-forget finalize path.\n */\nexport function ɵnormalizeLimits(\n limits: Required<InMemoryLimits>,\n): Required<InMemoryLimits> {\n const normalized = { ...limits };\n for (const field of Object.keys(\n ɵINMEMORY_DEFAULTS,\n ) as (keyof InMemoryLimits)[]) {\n const value = limits[field];\n if (!ɵisValidLimit(value)) {\n const fallback = ɵINMEMORY_DEFAULTS[field];\n normalized[field] = fallback;\n try {\n console.warn(\n `[CopilotKit] InMemoryAgentRunner: invalid ${field} value ` +\n `${String(value)} (expected a non-negative integer or Infinity); ` +\n `falling back to ${String(fallback)}.`,\n );\n } catch {\n // best-effort: logging must never break construction\n }\n }\n }\n return normalized;\n}\n\nconst EVICTION_GUIDANCE =\n \"[CopilotKit] InMemoryAgentRunner evicted in-memory thread history to stay \" +\n \"under memory limits. This runner is bounded and non-durable by design. For \" +\n \"durable or production threads, configure an Intelligence backend.\";\n\nconst LIMITS_CLOBBER_GUIDANCE =\n \"[CopilotKit] InMemoryAgentRunner was constructed with in-memory limits that \" +\n \"differ from the already-configured process-global store; the last-constructed \" +\n \"runner's limits apply to ALL in-memory threads (the store is shared per-process). \" +\n \"Configure a single consistent set of limits, or use an Intelligence backend for \" +\n \"isolated bounds.\";\n\n/**\n * Best-effort approximate byte size of a value, via serialized length.\n * Never throws — returns 0 when the value cannot be serialized. This is an\n * approximation (UTF-16 length, not exact heap bytes), used only for relative\n * accounting against `maxBytes`.\n */\nexport function ɵestimateBytes(value: unknown): number {\n try {\n return JSON.stringify(value)?.length ?? 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Per-run finalize intent, captured once when a run starts and mutated (only)\n * by whoever aborts THAT run — `stop()` or a superseding `run()`. The run's own\n * teardown reads this captured holder instead of the shared, mutable\n * `store.stopRequested`, so a later run that resets store state can never cause\n * an intentionally-stopped run to be finalized as an error (or vice versa).\n */\ninterface RunFinalizeControl {\n /** True once THIS run has been asked to stop (clean stop, not an error). */\n stopRequested: boolean;\n}\n\ninterface HistoricRun {\n threadId: string;\n runId: string;\n /** ID of the agent that executed this run. */\n agentId: string;\n parentRunId: string | null;\n events: BaseEvent[];\n /**\n * Snapshot of all messages (input + generated) at the end of this run, as\n * passed in by the caller. NOTE: `BoundedThreadStore.appendRun` moves this\n * snapshot to the THREAD level (`InMemoryEventStore.messagesSnapshot`) and\n * clears this field to `[]`, so a stored HistoricRun never carries messages.\n * The thread-messages fallback reads the thread-level snapshot, not this.\n */\n messages: Message[];\n createdAt: number;\n /** Approximate retained byte size of `events`; set by BoundedThreadStore at append. */\n approxEventBytes?: number;\n /**\n * Legacy field retained for shape compatibility. `appendRun` always zeroes it\n * because message bytes are accounted at the thread level, not per run.\n */\n approxMessageBytes?: number;\n}\n\n/**\n * Lightweight thread summary returned by {@link InMemoryAgentRunner.listThreads}.\n * Shape matches the Intelligence platform's ThreadRecord so the same HTTP\n * response envelope can be used for both backends.\n */\nexport interface InMemoryThread {\n id: string;\n name: string | null;\n agentId: string;\n organizationId: \"\"; // always empty in in-memory mode\n createdById: \"\"; // always empty in in-memory mode\n archived: false; // always false in in-memory mode\n createdAt: string;\n updatedAt: string;\n}\n\nclass InMemoryEventStore {\n constructor(public threadId: string) {}\n\n /** The subject that current consumers subscribe to. */\n subject: ReplaySubject<BaseEvent> | null = null;\n\n /** True while a run is actively producing events. */\n isRunning = false;\n\n /** Current run ID */\n currentRunId: string | null = null;\n\n /** Historic completed runs */\n historicRuns: HistoricRun[] = [];\n\n /** Currently running agent instance (if any). */\n agent: AbstractAgent | null = null;\n\n /** Subject returned from run() while the run is active. */\n runSubject: ReplaySubject<BaseEvent> | null = null;\n\n /**\n * Thread-level lifecycle flag: true once a stop/supersede has been requested\n * for the currently-owning run but that run has not yet finalized. Drives\n * eviction protection, the connect() bridge, and stop() de-dup. This is NOT\n * the finalize intent read by a run's teardown — that lives per-run on\n * {@link activeFinalize}, so a superseding run resetting this field cannot\n * mislabel the run it replaced. A new run resets this to false when it takes\n * ownership.\n */\n stopRequested = false;\n\n /**\n * Finalize control of the currently-owning run. `stop()` and a superseding\n * `run()` flip the owning run's flag through this reference; each run also\n * captures the SAME object in its closure, so its teardown finalizes against\n * its own intent regardless of what a later run does to the store.\n */\n activeFinalize: RunFinalizeControl | null = null;\n\n /** Reference to the events emitted in the current run. */\n currentEvents: BaseEvent[] | null = null;\n\n /**\n * The thread's single latest NON-EMPTY message snapshot, held at the THREAD\n * level (independent of `historicRuns` lifecycle). Decoupling the snapshot\n * from per-run storage means run-cap FIFO eviction and interleaved\n * empty-snapshot runs can never drop or pin the thread's message history.\n */\n messagesSnapshot: Message[] = [];\n\n /** Approximate retained byte size of `messagesSnapshot`. */\n approxMessagesSnapshotBytes = 0;\n\n /**\n * The thread's true creation timestamp (epoch ms), captured from the FIRST\n * run ever appended and held at the THREAD level (independent of\n * `historicRuns` lifecycle). Decoupling it from per-run storage means run-cap\n * FIFO eviction — which shifts the oldest entries off `historicRuns` — can\n * never move the reported creation time forward. `null` until the first run\n * lands. Mirrors the `messagesSnapshot` thread-level decoupling.\n */\n createdAt: number | null = null;\n}\n\nexport class ɵBoundedThreadStore {\n private readonly map = new Map<string, InMemoryEventStore>();\n private totalBytes = 0;\n private warned = false;\n /** True once limits have been EXPLICITLY set (via setLimits), not just the constructor default. */\n private limitsExplicitlySet = false;\n /** Warn-once latch for the clobber warning, kept distinct from the eviction `warned` latch. */\n private clobberWarned = false;\n\n private limits: Required<InMemoryLimits>;\n\n constructor(limits: Required<InMemoryLimits>) {\n // Normalize once at construction so `this.limits` is ALWAYS well-formed,\n // regardless of entry point (direct construction or a later `setLimits`).\n this.limits = ɵnormalizeLimits(limits);\n }\n\n get byteTotal(): number {\n return this.totalBytes;\n }\n\n /**\n * The store's CURRENT effective bounds. Exposed (with the `ɵ` internal-API\n * prefix) so a partial `setLimits` can coalesce unspecified fields against the\n * live config rather than the hardcoded {@link ɵINMEMORY_DEFAULTS} — a partial\n * update must be a partial update, never a silent reset of the fields the\n * caller did not mention. Returns a copy so callers cannot mutate the store's\n * bounds through it.\n */\n get ɵlimits(): Required<InMemoryLimits> {\n return { ...this.limits };\n }\n\n /**\n * Reconfigure the process-global store's bounds. Called by the\n * {@link InMemoryAgentRunner} constructor when limits are passed. Because the\n * store is a per-process singleton, this replaces the bounds for ALL in-memory\n * threads. Emits {@link LIMITS_CLOBBER_GUIDANCE} at most ONCE per store when a\n * SECOND (or later) explicit set arrives whose resolved values differ from the\n * prior explicit set — i.e. a genuine clobber of an already-customized config.\n * The first explicit customization (defaults → custom) is the intended\n * override and never warns; identical re-sets never warn.\n */\n setLimits(limits: Required<InMemoryLimits>): void {\n // Normalize FIRST so invalid fields can never reach an enforcement loop and\n // so the clobber comparison below is against the EFFECTIVE (clamped) values,\n // not the raw ones — a typo'd bound that clamps to the current default is not\n // a genuine clobber and must not warn.\n const normalized = ɵnormalizeLimits(limits);\n if (\n this.limitsExplicitlySet &&\n !this.clobberWarned &&\n (normalized.maxThreads !== this.limits.maxThreads ||\n normalized.maxRunsPerThread !== this.limits.maxRunsPerThread ||\n normalized.maxBytes !== this.limits.maxBytes)\n ) {\n this.clobberWarned = true;\n try {\n console.warn(LIMITS_CLOBBER_GUIDANCE);\n } catch {\n // best-effort: logging must never break construction\n }\n }\n this.limitsExplicitlySet = true;\n this.limits = normalized;\n }\n\n get size(): number {\n return this.map.size;\n }\n\n /** Re-insert at the tail so Map iteration order stays LRU-first. */\n private touchOrder(threadId: string, store: InMemoryEventStore): void {\n this.map.delete(threadId);\n this.map.set(threadId, store);\n }\n\n getOrCreate(threadId: string): InMemoryEventStore {\n const existing = this.map.get(threadId);\n if (existing) {\n this.touchOrder(threadId, existing);\n return existing;\n }\n const store = new InMemoryEventStore(threadId);\n this.map.set(threadId, store);\n this.evictThreadsIfNeeded(threadId);\n return store;\n }\n\n get(\n threadId: string,\n opts: { touch: boolean },\n ): InMemoryEventStore | undefined {\n const store = this.map.get(threadId);\n if (store && opts.touch) this.touchOrder(threadId, store);\n return store;\n }\n\n peek(threadId: string): InMemoryEventStore | undefined {\n return this.map.get(threadId);\n }\n\n /**\n * Evict the least-recently-used thread that is neither running NOR\n * mid-finalization. Returns false if none evictable. The `protect` thread\n * (typically the one just created) is never evicted, so a fresh thread is not\n * immediately dropped when it is the only non-running candidate.\n *\n * A thread is skipped while `isRunning` OR `stopRequested` is set.\n * `stop()` flips `isRunning` to false the moment it aborts the agent, but the\n * run keeps finalizing asynchronously (the abort trips the `catch` in\n * `runAgent`, which later calls `appendRun`). During that window\n * `stopRequested` stays true; evicting the thread then would make the pending\n * `appendRun` hit `if (!store) return` and silently drop the aborted run's\n * history. Guarding on `stopRequested` keeps the thread alive until\n * finalization completes.\n */\n private evictOneLru(protect?: string): boolean {\n for (const [threadId, store] of this.map) {\n if (threadId === protect) continue; // never evict the just-created thread\n // never evict a running or still-finalizing (stop-requested) thread\n if (store.isRunning || store.stopRequested) continue;\n this.removeThread(threadId, store);\n this.noteEviction();\n return true;\n }\n return false;\n }\n\n appendRun(threadId: string, run: HistoricRun): void {\n const store = this.map.get(threadId);\n if (!store) return; // best-effort: nothing to append to\n\n // Thread-level creation timestamp: capture the FIRST run's createdAt once\n // and never overwrite it. Held on the store (not derived from\n // `historicRuns[0]`) so run-cap FIFO eviction of the oldest runs cannot\n // drift the thread's reported creation time forward. Mirrors the\n // thread-level `messagesSnapshot` decoupling below.\n if (store.createdAt === null) {\n store.createdAt = run.createdAt;\n }\n\n // Thread-level message snapshot: keep the single latest NON-EMPTY snapshot\n // on the store, decoupled from `historicRuns`. When the incoming run\n // carries a non-empty snapshot, replace the thread's snapshot (adjusting\n // byte accounting). When it's empty (non-array `agent.messages` or an\n // error-path run), leave the existing thread snapshot untouched so history\n // is never lost. The snapshot never lives on a HistoricRun, so run-cap FIFO\n // eviction can never drop it and an interleaved empty run can never pin it.\n if (run.messages.length > 0) {\n this.totalBytes -= store.approxMessagesSnapshotBytes;\n // Store the incoming array directly (SHALLOW, array-level copy). `run.messages`\n // is already a fresh `[...agent.messages]` array created in run(), so we own the\n // array and it is decoupled from `agent.messages` at the array level (push/splice\n // on the agent's array cannot mutate our snapshot). We deliberately do NOT deep-copy\n // here: `structuredClone` throws DataCloneError on a non-cloneable message field,\n // which would wedge the thread and hang SSE — inconsistent with `ɵestimateBytes`,\n // which tolerates the same bad-payload class. The tradeoff is that the inner\n // `Message` objects remain shared by reference with `agent.messages`, so an agent\n // that mutates its own message objects IN PLACE after the run can still be observed\n // through this snapshot. That inner-object isolation is a known limitation tracked as\n // follow-up; callers must treat returned messages as read-only. Estimate bytes on the\n // same value so accounting matches exactly what is retained.\n store.messagesSnapshot = run.messages;\n store.approxMessagesSnapshotBytes = ɵestimateBytes(run.messages);\n this.totalBytes += store.approxMessagesSnapshotBytes;\n }\n\n // Do not carry message bytes on the HistoricRun: the snapshot is now tracked\n // at the thread level, so historicRuns must never account message bytes.\n run.messages = [];\n run.approxMessageBytes = 0;\n\n // Compute this run's approximate event size once, at append time.\n run.approxEventBytes = ɵestimateBytes(run.events);\n store.historicRuns.push(run);\n this.totalBytes += run.approxEventBytes;\n this.touchOrder(threadId, store);\n\n this.enforceRunCap(store);\n this.evictByBytesIfNeeded(threadId);\n }\n\n private enforceRunCap(store: InMemoryEventStore): void {\n const cap = this.limits.maxRunsPerThread;\n if (!cap || cap === Infinity) return; // 0 or Infinity → disabled\n while (store.historicRuns.length > cap) {\n const dropped = store.historicRuns.shift()!;\n // Only event bytes live on a HistoricRun; the message snapshot is tracked\n // at the thread level and survives run-cap eviction.\n this.totalBytes -= dropped.approxEventBytes ?? 0;\n // Per-thread run-cap trimming is also eviction — history is being dropped.\n // Route it through the SAME warn-once latch as whole-thread LRU eviction so\n // this shows up in logs rather than as silent data loss. `noteEviction` is\n // latched (one warning per store, reset by `clear()`), so a hot thread that\n // trims on every subsequent append warns once, never per dropped run. The\n // loop only runs when a run is ACTUALLY over the cap, so a disabled or\n // under-cap `enforceRunCap` stays silent (it returned / never entered here).\n this.noteEviction();\n }\n }\n\n /**\n * Trim the store back under the byte ceiling by evicting LRU non-running\n * threads. `protect` (the just-appended thread) is never self-evicted, so a\n * fresh run pushes OTHER threads out rather than dropping itself.\n */\n private evictByBytesIfNeeded(protect?: string): void {\n while (this.totalBytes > this.limits.maxBytes) {\n if (!this.evictOneLru(protect)) break; // only protected/running threads left → accept overage\n }\n }\n\n private removeThread(threadId: string, store: InMemoryEventStore): void {\n for (const run of store.historicRuns) {\n this.totalBytes -= run.approxEventBytes ?? 0;\n }\n // The thread's message snapshot is tracked at the store level, so it must\n // be reclaimed here in addition to the per-run event bytes.\n this.totalBytes -= store.approxMessagesSnapshotBytes;\n this.map.delete(threadId);\n }\n\n private evictThreadsIfNeeded(protect?: string): void {\n while (this.map.size > this.limits.maxThreads) {\n if (!this.evictOneLru(protect)) break; // everything evictable is running → accept overage\n }\n }\n\n private noteEviction(): void {\n if (this.warned) return;\n this.warned = true;\n try {\n console.warn(EVICTION_GUIDANCE);\n } catch {\n // best-effort: logging must never break a run\n }\n }\n\n listThreads(): InMemoryThread[] {\n const threads: InMemoryThread[] = [];\n for (const [threadId, store] of this.map) {\n if (store.historicRuns.length === 0) continue;\n const lastRun = store.historicRuns[store.historicRuns.length - 1]!;\n // Creation time comes from the thread-level `store.createdAt` (the first\n // run ever appended), NOT `historicRuns[0]` (the oldest RETAINED run):\n // run-cap FIFO eviction drops the oldest retained runs, so deriving it\n // from `historicRuns[0]` would silently drift the timestamp forward over\n // a thread's lifetime. `updatedAt` stays on `lastRun` because FIFO\n // eviction removes from the FRONT, so the newest run is never evicted.\n // The `?? lastRun.createdAt` fallback is defensive only: any thread with\n // runs has had `store.createdAt` set by `appendRun`.\n threads.push({\n id: threadId,\n name: null,\n agentId: lastRun.agentId,\n organizationId: \"\",\n createdById: \"\",\n archived: false,\n createdAt: new Date(store.createdAt ?? lastRun.createdAt).toISOString(),\n updatedAt: new Date(lastRun.createdAt).toISOString(),\n });\n }\n return threads.sort(\n (a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),\n );\n }\n\n clear(): void {\n this.map.clear();\n this.totalBytes = 0;\n this.warned = false;\n }\n}\n\n/**\n * Process-wide singleton backing every {@link InMemoryAgentRunner}. Exported\n * (with the `ɵ` internal-API prefix) so tests can inspect the exact store the\n * runner writes to; not part of the public API.\n */\nexport const ɵGLOBAL_STORE = new ɵBoundedThreadStore(ɵINMEMORY_DEFAULTS);\nconst sharedStore = ɵGLOBAL_STORE;\n\nexport class InMemoryAgentRunner extends AgentRunner {\n readonly ɵsupportsLocalThreadEndpoints = true;\n\n /**\n * How to handle a `run()` for a thread that already has an in-flight run.\n * `\"throw\"` (default) preserves the historic behavior. `\"supersede\"` aborts\n * the prior run (mirroring `stop()`) and starts the new one — opted into by\n * the hosted-bot listener so a fast follow-up turn on the same thread cleanly\n * replaces a still-running (or wedged) prior turn instead of erroring with\n * \"Thread already running\".\n */\n private readonly onConcurrentRun: \"throw\" | \"supersede\";\n\n /**\n * @param options Per-runner behavior (`onConcurrentRun`) plus optional bounds\n * for the in-memory store ({@link InMemoryLimits}).\n *\n * Note the differing scopes: `onConcurrentRun` is per-runner instance, while\n * the limits reconfigure the PROCESS-GLOBAL store shared by every\n * `InMemoryAgentRunner`. Omit the limits for safe defaults\n * ({@link ɵINMEMORY_DEFAULTS}); passing none leaves the store untouched. When\n * multiple runners are constructed with differing limits, the last-constructed\n * wins — in practice the OSS/SSE default construction passes nothing. If a\n * second (or later) runner is constructed with limits that DIFFER from an\n * already-customized store, a one-time `console.warn` is emitted to signal that\n * the shared store's bounds are being clobbered for ALL in-memory threads.\n */\n constructor(options?: InMemoryAgentRunnerOptions) {\n super();\n const { onConcurrentRun, ...limits } = options ?? {};\n this.onConcurrentRun = onConcurrentRun ?? \"throw\";\n\n // Only reconfigure the shared store when a bound was actually supplied.\n // `new InMemoryAgentRunner({ onConcurrentRun: \"supersede\" })` must stay\n // inert with respect to limits.\n if (\n limits.maxThreads !== undefined ||\n limits.maxRunsPerThread !== undefined ||\n limits.maxBytes !== undefined\n ) {\n // Coalesce each unspecified field against the store's CURRENT effective\n // limits, NOT ɵINMEMORY_DEFAULTS. The store is process-global, so tuning\n // one bound must leave every previously-customized sibling bound intact —\n // a partial update stays a partial update instead of silently resetting the\n // fields the caller never mentioned. Passing all three (e.g.\n // ɵINMEMORY_DEFAULTS) still fully replaces the config, so the defaults-\n // restore path is unaffected.\n const current = sharedStore.ɵlimits;\n sharedStore.setLimits({\n maxThreads: limits.maxThreads ?? current.maxThreads,\n maxRunsPerThread: limits.maxRunsPerThread ?? current.maxRunsPerThread,\n maxBytes: limits.maxBytes ?? current.maxBytes,\n });\n }\n }\n\n run(request: AgentRunnerRunRequest): Observable<BaseEvent> {\n const store = sharedStore.getOrCreate(request.threadId);\n\n // Enter the concurrency branch whenever a prior run still owns the thread —\n // either actively running OR still finalizing after a stop()/supersede.\n // `stop()` flips `isRunning` to false the instant it aborts the agent, but\n // the run keeps finalizing asynchronously (`stopRequested` stays true).\n // Gating on `isRunning` alone let a `run()` slip through that window\n // unhandled: no supersede/throw, no per-run intent capture, and a leaked\n // bridge from the dying run's subject.\n if (store.isRunning || store.stopRequested) {\n if (this.onConcurrentRun !== \"supersede\") {\n throw new Error(\"Thread already running\");\n }\n // Supersede: abort the prior (possibly wedged) run so this one can start.\n // Mirrors stop(). Record the prior run's OWN finalize intent on its\n // captured control BEFORE resetting the shared store flags for the new\n // run: a supersede is a clean stop of the prior run, so its async teardown\n // must finalize as RUN_FINISHED, never a synthetic RUN_ERROR. The prior\n // run's async finalization is prevented from clobbering this run's state\n // by the run-id guard below.\n const priorAgent = store.agent;\n const priorFinalize = store.activeFinalize;\n if (priorFinalize) {\n priorFinalize.stopRequested = true;\n }\n store.isRunning = false;\n if (priorAgent) {\n try {\n priorAgent.abortRun();\n } catch (error) {\n console.error(\"Failed to abort superseded run\", error);\n }\n }\n }\n store.isRunning = true;\n store.currentRunId = request.input.runId;\n store.agent = request.agent;\n store.stopRequested = false;\n\n // Per-run finalize control. This run's teardown reads THIS captured holder\n // (never the shared `store.stopRequested`, which a later run resets), so an\n // aborted run is always finalized against its own stop-intent.\n const finalizeControl: RunFinalizeControl = { stopRequested: false };\n store.activeFinalize = finalizeControl;\n\n // Track seen message IDs and current run events for this run\n const seenMessageIds = new Set<string>();\n const currentRunEvents: BaseEvent[] = [];\n store.currentEvents = currentRunEvents;\n\n // Get all previously seen message IDs from historic runs\n const historicMessageIds = new Set<string>();\n for (const run of store.historicRuns) {\n for (const event of run.events) {\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n historicMessageIds.add(event.messageId);\n }\n if (event.type === EventType.RUN_STARTED) {\n const runStarted = event as RunStartedEvent;\n const messages = runStarted.input?.messages ?? [];\n for (const message of messages) {\n historicMessageIds.add(message.id);\n }\n }\n }\n }\n\n const nextSubject = new ReplaySubject<BaseEvent>(Infinity);\n\n // Update the store's subject immediately. We intentionally do NOT capture\n // and bridge the previous subject: see the note before `runAgent()` below.\n store.subject = nextSubject;\n\n // Create a subject for run() return value\n const runSubject = new ReplaySubject<BaseEvent>(Infinity);\n store.runSubject = runSubject;\n\n // Helper function to run the agent and handle errors\n const runAgent = async () => {\n // Get parent run ID for chaining\n const lastRun = store.historicRuns[store.historicRuns.length - 1];\n const parentRunId = lastRun?.runId ?? null;\n\n // Shared teardown for both the success and error paths. Keeping this one\n // helper means the two paths cannot drift apart (they were near-identical\n // and must stay symmetric). `interruptionMessage` is set only on the error\n // path; its presence is what distinguishes the two.\n const finalizeRun = (opts: { interruptionMessage?: string }) => {\n const isError = opts.interruptionMessage !== undefined;\n\n // Capture the count of REAL (agent-emitted) events BEFORE finalizing.\n // `finalizeRunEvents` mutates `currentRunEvents` IN PLACE — it always\n // pushes a synthetic terminal (and any closers) when the stream ended\n // without one — so after the call `currentRunEvents.length` is never 0.\n // The persistence guard below must gate on this pre-finalize count, or\n // the \"skip an immediate throw that emitted nothing\" check is dead.\n const preFinalizeEventCount = currentRunEvents.length;\n\n // Finalize against THIS run's own captured stop-intent — never the\n // shared `store.stopRequested`, which a superseding run resets. An\n // aborted run is thus finalized as a clean RUN_FINISHED, not a synthetic\n // RUN_ERROR.\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: finalizeControl.stopRequested,\n ...(isError ? { interruptionMessage: opts.interruptionMessage } : {}),\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Does this run still own the thread? A superseding run has changed\n // `currentRunId`, so the run it replaced no longer owns the store.\n const ownsThread = store.currentRunId === request.input.runId;\n\n // Store this run's events. Guard on the per-run id (not the shared\n // `store.currentRunId`): a superseded run no longer owns the store, so\n // it must not push history — and never under a newer run's id, which\n // would corrupt the thread's history. On the error path also require at\n // least one real (pre-finalize) event, so an immediate throw with\n // nothing emitted does not create a phantom historic run holding only\n // the synthetic terminal.\n if (ownsThread && (!isError || preFinalizeEventCount > 0)) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n sharedStore.appendRun(request.threadId, {\n threadId: request.threadId,\n runId: request.input.runId,\n agentId: request.agent.agentId ?? \"default\",\n parentRunId,\n events: compactedEvents,\n // Snapshot all messages (input + generated) for the thread-messages endpoint\n messages: Array.isArray(request.agent.messages)\n ? [...request.agent.messages]\n : [],\n createdAt: Date.now(),\n });\n }\n\n // Complete the run. Guard the shared-store reset: if a newer run has\n // superseded this one (`currentRunId` changed), that run now owns the\n // store — don't clobber its state. Always complete THIS run's subjects.\n if (ownsThread) {\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n store.activeFinalize = null;\n }\n runSubject.complete();\n nextSubject.complete();\n // Time-scoped release: this run's events are now in historicRuns, so its\n // infinite ReplaySubject buffer is pure duplication — drop the store's\n // reference so it becomes collectable. The identity guard is what makes\n // this correct, and it does so differently on each path:\n //\n // - Owning path: no newer run superseded this one, so store.subject is\n // still nextSubject and the guard passes. The `if (ownsThread)` block\n // above just cleared isRunning and stopRequested, so connect() — which\n // bridges store.subject only while isRunning || stopRequested — will\n // not re-subscribe; it rebuilds this run's events from historicRuns\n // instead. Nulling the reference is therefore safe.\n //\n // - Superseded path (`onConcurrentRun: \"supersede\"`): a newer run has\n // already installed ITS subject and run id on the store, so the guard\n // fails and we leave store.subject untouched. Here isRunning/\n // stopRequested describe that live run (isRunning is typically true),\n // so it is precisely the identity guard — not those flags — that\n // prevents us from nulling the live run's subject and cutting\n // connect() off from the in-flight stream. This run's own buffer is no\n // longer referenced by the store and becomes collectable regardless.\n if (store.subject === nextSubject) {\n store.subject = null;\n }\n };\n\n try {\n await request.agent.runAgent(request.input, {\n onEvent: ({ event }) => {\n let processedEvent: BaseEvent = event;\n if (event.type === EventType.RUN_STARTED) {\n const runStartedEvent = event as RunStartedEvent;\n if (!runStartedEvent.input) {\n const sanitizedMessages = request.input.messages\n ? request.input.messages.filter(\n (message) => !historicMessageIds.has(message.id),\n )\n : undefined;\n const updatedInput = {\n ...request.input,\n ...(sanitizedMessages !== undefined\n ? { messages: sanitizedMessages }\n : {}),\n };\n runStartedEvent.input = updatedInput;\n processedEvent = runStartedEvent;\n }\n }\n\n runSubject.next(processedEvent); // For run() return - only agent events\n nextSubject.next(processedEvent); // For connect() / store - all events\n currentRunEvents.push(processedEvent); // Accumulate for storage\n },\n onNewMessage: ({ message }) => {\n // Called for each new message\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n },\n onRunStartedEvent: () => {\n // Mark any messages from the input as seen so they aren't emitted twice\n if (request.input.messages) {\n for (const message of request.input.messages) {\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n }\n }\n },\n });\n\n finalizeRun({});\n } catch (error) {\n const interruptionMessage =\n error instanceof Error ? error.message : String(error);\n finalizeRun({ interruptionMessage });\n }\n };\n\n // NOTE: we deliberately do NOT bridge the previous store subject into\n // `nextSubject`. `store.subject` is nulled the moment a run fully tears down\n // (identity guard in `finalizeRun`), so the previous subject is non-null\n // ONLY when this run is superseding a prior run that is still in flight or\n // finalizing. Forwarding that dying run's subject would replay its buffered\n // RUN_STARTED and push its terminal event (RUN_FINISHED/RUN_ERROR) into THIS\n // live run's stream — an invalid AG-UI sequence on a healthy run. A\n // superseded run's stream must stay isolated: it reaches only its own\n // connect() subscribers via its own (now-detached) subject, never the\n // superseding run's.\n\n // Start the agent execution immediately (not lazily)\n runAgent();\n\n // Return the run subject (only agent events, no injected messages)\n return runSubject.asObservable();\n }\n\n connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {\n const store = sharedStore.get(request.threadId, { touch: true });\n const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);\n\n if (!store) {\n // No store means no events\n connectionSubject.complete();\n return connectionSubject.asObservable();\n }\n\n // Collect all historic events from memory\n const allHistoricEvents: BaseEvent[] = [];\n for (const run of store.historicRuns) {\n allHistoricEvents.push(...run.events);\n }\n\n // Apply compaction to all historic events together (like SQLite)\n const compactedEvents = compactEvents(allHistoricEvents);\n\n // Emit compacted events and track message IDs\n const emittedMessageIds = new Set<string>();\n for (const event of compactedEvents) {\n connectionSubject.next(event);\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n emittedMessageIds.add(event.messageId);\n }\n }\n\n // Bridge active run to connection if exists\n if (store.subject && (store.isRunning || store.stopRequested)) {\n store.subject.subscribe({\n next: (event) => {\n // Skip message events that we've already emitted from historic\n if (\n \"messageId\" in event &&\n typeof event.messageId === \"string\" &&\n emittedMessageIds.has(event.messageId)\n ) {\n return;\n }\n connectionSubject.next(event);\n },\n complete: () => connectionSubject.complete(),\n error: (err) => connectionSubject.error(err),\n });\n } else {\n // No active run, complete after historic events\n connectionSubject.complete();\n }\n\n return connectionSubject.asObservable();\n }\n\n isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {\n const store = sharedStore.peek(request.threadId);\n return Promise.resolve(store?.isRunning ?? false);\n }\n\n stop(request: AgentRunnerStopRequest): Promise<boolean | undefined> {\n const store = sharedStore.peek(request.threadId);\n if (!store || !store.isRunning) {\n return Promise.resolve(false);\n }\n if (request.runId !== undefined && store.currentRunId !== request.runId) {\n return Promise.resolve(false);\n }\n if (store.stopRequested) {\n return Promise.resolve(false);\n }\n\n store.stopRequested = true;\n store.isRunning = false;\n // Record the stop on the running run's OWN finalize control so its async\n // teardown finalizes as a clean RUN_FINISHED. This is the same object that\n // run's closure reads, so a later run cannot mislabel this stop.\n const finalizeControl = store.activeFinalize;\n if (finalizeControl) {\n finalizeControl.stopRequested = true;\n }\n\n const agent = store.agent;\n if (!agent) {\n store.stopRequested = false;\n store.isRunning = false;\n if (finalizeControl) {\n finalizeControl.stopRequested = false;\n }\n return Promise.resolve(false);\n }\n\n try {\n agent.abortRun();\n return Promise.resolve(true);\n } catch (error) {\n console.error(\"Failed to abort agent run\", error);\n store.stopRequested = false;\n store.isRunning = true;\n if (finalizeControl) {\n finalizeControl.stopRequested = false;\n }\n return Promise.resolve(false);\n }\n }\n\n /**\n * Returns a summary of every thread that has been run through this runner.\n *\n * This powers the local-dev fallback for `GET /threads` when the Intelligence\n * platform is not configured. Each entry mirrors the shape of a platform\n * `ThreadRecord` so the HTTP handler can use the same response envelope.\n */\n listThreads(): InMemoryThread[] {\n return sharedStore.listThreads();\n }\n\n /**\n * Returns all messages for a thread, using the snapshot captured at the end\n * of the most recent run.\n *\n * This powers the local-dev fallback for `GET /threads/:threadId/messages`\n * when the Intelligence platform is not configured. The returned `Message[]`\n * objects come directly from the ag-ui agent, so their shape is compatible\n * with the Intelligence platform's `ThreadMessage` type.\n */\n getThreadMessages(threadId: string): Message[] {\n const store = sharedStore.peek(threadId);\n if (!store) return [];\n // The thread's latest non-empty snapshot is held at the store level,\n // independent of `historicRuns` lifecycle, so run-cap eviction and\n // interleaved empty-snapshot runs can never lose it. Return a SHALLOW\n // (array-level) copy: a fresh array so a caller mutating array STRUCTURE\n // (push/splice/reassign elements) cannot affect the stored snapshot. We\n // deliberately do NOT deep-copy: `structuredClone` throws DataCloneError on a\n // non-cloneable message field, which would wedge the thread and hang SSE —\n // inconsistent with `ɵestimateBytes`, which tolerates the same bad-payload class.\n // The tradeoff is that the inner `Message` objects remain shared by reference with\n // the stored snapshot, so mutating a returned message's FIELD\n // (e.g. `getThreadMessages(t)[0].content = \"x\"`) is NOT isolated and would corrupt\n // the stored snapshot. That inner-object isolation is a known limitation tracked as\n // follow-up; callers must treat returned messages as read-only.\n return [...store.messagesSnapshot];\n }\n\n /**\n * Returns all AG-UI events for a thread, compacted across historic runs.\n *\n * Powers the local-dev fallback for `GET /threads/:threadId/events` when the\n * Intelligence platform is not configured. The compaction logic matches\n * the connection-replay path in {@link connect}, so the stream a\n * late-joining inspector sees matches what this method returns.\n */\n getThreadEvents(threadId: string): BaseEvent[] {\n const store = sharedStore.peek(threadId);\n if (!store || store.historicRuns.length === 0) return [];\n const all: BaseEvent[] = [];\n for (const run of store.historicRuns) all.push(...run.events);\n return compactEvents(all);\n }\n\n /**\n * Returns the agent state snapshot for a thread.\n *\n * Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The\n * AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces\n * a single trailing STATE_SNAPSHOT when state changes exist, so this is a\n * faithful view of state at the end of the most recent run.\n *\n * Returns `null` when the thread has never emitted a STATE_SNAPSHOT.\n */\n getThreadState(threadId: string): Record<string, unknown> | null {\n const events = this.getThreadEvents(threadId);\n // Walk backwards — the last snapshot wins.\n for (let i = events.length - 1; i >= 0; i--) {\n const event = events[i]!;\n if (event.type === EventType.STATE_SNAPSHOT) {\n const snapshot = (event as StateSnapshotEvent).snapshot;\n // Only plain objects satisfy the Record<string, unknown> contract.\n // `typeof [] === \"object\"` is true, so arrays must be rejected\n // explicitly to avoid returning an array typed as a Record.\n if (\n snapshot &&\n typeof snapshot === \"object\" &&\n !Array.isArray(snapshot)\n ) {\n // Return a defensive shallow copy so callers can't mutate the\n // snapshot object held inside the stored event (matches the\n // getThreadMessages defensive-copy approach).\n return { ...(snapshot as Record<string, unknown>) };\n }\n return null;\n }\n }\n return null;\n }\n\n /**\n * Clears all in-memory thread history.\n *\n * Powers the local-dev fallback for `POST /threads/clear`, letting consumers\n * (e.g. the demo's Clear button) reset to an empty thread list without\n * restarting the runtime. Intentionally not exposed on the Intelligence\n * platform path: there, thread history lives in a real database and must\n * not be wiped this way.\n */\n clearThreads(): void {\n sharedStore.clear();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require("reflect-metadata");
|
|
2
2
|
import { AgentRunner, AgentRunnerConnectRequest, AgentRunnerIsRunningRequest, AgentRunnerRunRequest, AgentRunnerStopRequest, LocalThreadEndpointRecord, LocalThreadEndpointRunner, supportsLocalThreadEndpoints } from "./agent-runner.cjs";
|
|
3
|
-
import { InMemoryAgentRunner, InMemoryThread } from "./in-memory.cjs";
|
|
3
|
+
import { InMemoryAgentRunner, InMemoryAgentRunnerOptions, InMemoryLimits, InMemoryThread, ɵBoundedThreadStore, ɵGLOBAL_STORE, ɵINMEMORY_DEFAULTS, ɵestimateBytes, ɵnormalizeLimits } from "./in-memory.cjs";
|
|
4
4
|
import { IntelligenceAgentRunner, IntelligenceAgentRunnerOptions, RunnerStartupBoundary } from "./intelligence.cjs";
|
|
5
5
|
import { finalizeRunEvents } from "@copilotkit/shared";
|
|
6
6
|
export { finalizeRunEvents };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { AgentRunner, AgentRunnerConnectRequest, AgentRunnerIsRunningRequest, AgentRunnerRunRequest, AgentRunnerStopRequest, LocalThreadEndpointRecord, LocalThreadEndpointRunner, supportsLocalThreadEndpoints } from "./agent-runner.mjs";
|
|
3
|
-
import { InMemoryAgentRunner, InMemoryThread } from "./in-memory.mjs";
|
|
3
|
+
import { InMemoryAgentRunner, InMemoryAgentRunnerOptions, InMemoryLimits, InMemoryThread, ɵBoundedThreadStore, ɵGLOBAL_STORE, ɵINMEMORY_DEFAULTS, ɵestimateBytes, ɵnormalizeLimits } from "./in-memory.mjs";
|
|
4
4
|
import { IntelligenceAgentRunner, IntelligenceAgentRunnerOptions, RunnerStartupBoundary } from "./intelligence.mjs";
|
|
5
5
|
import { finalizeRunEvents as finalizeRunEvents$1 } from "@copilotkit/shared";
|
|
6
6
|
export { finalizeRunEvents$1 as finalizeRunEvents };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { AgentRunner, supportsLocalThreadEndpoints } from "./agent-runner.mjs";
|
|
3
|
-
import { InMemoryAgentRunner } from "./in-memory.mjs";
|
|
3
|
+
import { InMemoryAgentRunner, ɵBoundedThreadStore, ɵGLOBAL_STORE, ɵINMEMORY_DEFAULTS, ɵestimateBytes, ɵnormalizeLimits } from "./in-memory.mjs";
|
|
4
4
|
import { IntelligenceAgentRunner } from "./intelligence.mjs";
|
|
5
5
|
import { finalizeRunEvents as finalizeRunEvents$1 } from "@copilotkit/shared";
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/runtime",
|
|
3
|
-
"version": "1.66.
|
|
3
|
+
"version": "1.66.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -105,9 +105,9 @@
|
|
|
105
105
|
"uuid": "^10.0.0",
|
|
106
106
|
"ws": "^8.18.0",
|
|
107
107
|
"zod": "^3.23.3",
|
|
108
|
-
"@copilotkit/channels-
|
|
109
|
-
"@copilotkit/channels-
|
|
110
|
-
"@copilotkit/shared": "1.66.
|
|
108
|
+
"@copilotkit/channels-core": "^0.8.0",
|
|
109
|
+
"@copilotkit/channels-intelligence": "0.8.0",
|
|
110
|
+
"@copilotkit/shared": "1.66.4"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@copilotkit/aimock": "latest",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"typescript": "^5.2.3",
|
|
131
131
|
"valibot": "^1.2.0",
|
|
132
132
|
"vitest": "^3.2.4",
|
|
133
|
-
"@copilotkit/channels": "0.
|
|
133
|
+
"@copilotkit/channels": "0.8.0",
|
|
134
134
|
"tsconfig": "1.4.12"
|
|
135
135
|
},
|
|
136
136
|
"peerDependencies": {
|
|
@@ -1,64 +1,79 @@
|
|
|
1
|
-
InMemoryAgentRunner — default ephemeral runner.
|
|
1
|
+
InMemoryAgentRunner — default ephemeral runner. Thread state lives in a bounded, process-global store shared by every runner instance in the process.
|
|
2
2
|
|
|
3
3
|
## Store layout
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
6
|
// packages/runtime/src/v2/runtime/runner/in-memory.ts
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
interface GlobalStoreData {
|
|
10
|
-
stores: Map<string, InMemoryEventStore>; // per-threadId
|
|
11
|
-
historicRunsBackup: Map<string, HistoricRun[]>; // restored after HMR
|
|
12
|
-
}
|
|
7
|
+
export const ɵGLOBAL_STORE = new ɵBoundedThreadStore(ɵINMEMORY_DEFAULTS);
|
|
13
8
|
```
|
|
14
9
|
|
|
10
|
+
`ɵBoundedThreadStore` owns the `Map<threadId, InMemoryEventStore>`, LRU ordering, byte accounting, and eviction. The runner keeps all streaming logic and delegates storage to the store. The `ɵ` prefix marks internal API — exported for tests, not part of the public surface.
|
|
11
|
+
|
|
15
12
|
One `InMemoryEventStore` per `threadId`. Each store tracks:
|
|
16
13
|
|
|
17
|
-
- `subject: ReplaySubject<BaseEvent> | null` — current consumers
|
|
14
|
+
- `subject: ReplaySubject<BaseEvent> | null` — current consumers; released on run completion
|
|
18
15
|
- `isRunning: boolean` — gate for the `"Thread already running"` throw
|
|
19
16
|
- `currentRunId: string | null`
|
|
20
|
-
- `historicRuns: HistoricRun[]` — completed runs (
|
|
17
|
+
- `historicRuns: HistoricRun[]` — completed runs (events only; see snapshot note below)
|
|
18
|
+
- `messagesSnapshot: Message[]` — the thread's latest non-empty message snapshot, held at the THREAD level so run-cap eviction can never drop it
|
|
21
19
|
- `agent: AbstractAgent | null` — the instance that owns the active run
|
|
22
20
|
- `runSubject`, `currentEvents`, `stopRequested`
|
|
23
21
|
|
|
22
|
+
## Bounds
|
|
23
|
+
|
|
24
|
+
Three limits, whichever trips first. Defaults in `ɵINMEMORY_DEFAULTS`:
|
|
25
|
+
|
|
26
|
+
| Option | Default | Enforcement |
|
|
27
|
+
| ------------------ | ------- | -------------------------------------------------------------- |
|
|
28
|
+
| `maxThreads` | `1000` | LRU eviction of the least-recently-used thread |
|
|
29
|
+
| `maxRunsPerThread` | `100` | FIFO drop of oldest runs; `Infinity` or `0` disables |
|
|
30
|
+
| `maxBytes` | 512 MiB | Approximate total across all threads; evicts OTHER LRU threads |
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
new InMemoryAgentRunner({
|
|
34
|
+
maxThreads: 200,
|
|
35
|
+
maxRunsPerThread: 50,
|
|
36
|
+
maxBytes: 128 * 1024 ** 2,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Invariants worth knowing before touching this code:
|
|
41
|
+
|
|
42
|
+
- A thread is never evicted while `isRunning` **or** `stopRequested` is set. `stop()` flips `isRunning` false immediately but the run finalizes asynchronously; evicting in that window would make the pending `appendRun` silently drop history.
|
|
43
|
+
- `maxBytes` only bounds **committed** history. A single in-flight run's buffered events are not counted until the run completes, so it does not bound one runaway run mid-stream.
|
|
44
|
+
- `maxBytes` evicts other threads and never self-evicts the just-appended thread — it is a cross-thread ceiling, not a per-thread cap. A single dominant thread is bounded by `maxRunsPerThread`.
|
|
45
|
+
- Byte accounting is a `JSON.stringify().length` estimate, not exact heap bytes.
|
|
46
|
+
- Two eviction forms, both steering heavy users to an Intelligence backend via one shared warn-once latch. Whole-thread eviction (`maxThreads` count or `maxBytes` ceiling) drops the entire LRU thread — it stops appearing in `GET /threads`. Per-thread `maxRunsPerThread` trimming drops only a thread's oldest runs' events, keeping the thread visible with its original `createdAt` and its thread-level `messagesSnapshot`. The latch fires **once per store** (not once per eviction), reset only by `clearThreads()`/`clear()`, so a hot thread trimming on every append logs a single line and every later eviction is silent until a clear.
|
|
47
|
+
|
|
48
|
+
## Concurrency
|
|
49
|
+
|
|
50
|
+
`onConcurrentRun` is per-runner (unlike the limits, which are process-global):
|
|
51
|
+
|
|
52
|
+
- `"throw"` (default) — a second `run()` on a live thread throws `Error("Thread already running")`.
|
|
53
|
+
- `"supersede"` — aborts the in-flight run (same path as `stop()`) and starts the new one. The superseded run's teardown is guarded on `store.currentRunId === request.input.runId` (so it cannot push history under the new run's id or reset the new run's state) and on `store.subject === nextSubject` (so releasing its ReplaySubject cannot null out the live run's subject).
|
|
54
|
+
|
|
24
55
|
## Lifecycle
|
|
25
56
|
|
|
26
|
-
1. `run({ threadId, agent, input })` —
|
|
27
|
-
2. On `
|
|
28
|
-
3. `connect({ threadId })` —
|
|
29
|
-
4. `stop({ threadId })` — sets `stopRequested = true
|
|
57
|
+
1. `run({ threadId, agent, input })` — `sharedStore.getOrCreate(threadId)` (may evict other threads), then throw or supersede per `onConcurrentRun`. Create `ReplaySubject`s, run the agent, push events into the subjects and `currentEvents`, mark `isRunning`.
|
|
58
|
+
2. On completion or error: finalize, `sharedStore.appendRun(...)` with the compacted events (which enforces the run cap and byte ceiling), clear `isRunning` / `currentRunId` / `agent`, and release `store.subject` so the infinite ReplaySubject buffer becomes collectable. History is rebuilt from `historicRuns` afterwards.
|
|
59
|
+
3. `connect({ threadId })` — replays compacted `historicRuns`, then bridges the live subject while `isRunning || stopRequested`.
|
|
60
|
+
4. `stop({ threadId })` — sets `stopRequested = true`, aborts the agent; teardown runs in the run's `catch`.
|
|
30
61
|
|
|
31
|
-
##
|
|
62
|
+
## Config scope gotcha
|
|
32
63
|
|
|
33
|
-
|
|
64
|
+
Limits reconfigure the shared store, so the **last-constructed runner wins for all in-memory threads**. A second runner passing limits that differ from an already-customized store logs a one-time clobber warning. Passing only `onConcurrentRun` leaves the limits untouched.
|
|
34
65
|
|
|
35
66
|
## When NOT to use
|
|
36
67
|
|
|
37
68
|
- Multi-instance production deploys — each process has its own store.
|
|
38
|
-
-
|
|
69
|
+
- Anywhere history loss is unacceptable — eviction is history loss, same as a restart.
|
|
39
70
|
- Load-balanced serverless with cold starts — new workers see empty stores.
|
|
40
71
|
|
|
41
72
|
## When it is OK
|
|
42
73
|
|
|
43
74
|
- Local development.
|
|
44
75
|
- Single-instance preview environments.
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
// test setup
|
|
49
|
-
const storeKey = Symbol.for("@copilotkit/runtime/in-memory-store");
|
|
50
|
-
const data = (globalThis as any)[storeKey] as
|
|
51
|
-
| {
|
|
52
|
-
stores: Map<string, unknown>;
|
|
53
|
-
historicRunsBackup: Map<string, unknown>;
|
|
54
|
-
}
|
|
55
|
-
| undefined;
|
|
56
|
-
if (data) {
|
|
57
|
-
data.stores.clear();
|
|
58
|
-
data.historicRunsBackup.clear();
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The runtime does not yet expose an official reset helper — a `__TEST_ONLY_clearGlobalStore` export would be a reasonable follow-up.
|
|
76
|
+
- Production single-instance deploys where scrollback is best-effort — the bounds make this safe against OOM, not durable.
|
|
77
|
+
- Tests. Every `new InMemoryAgentRunner()` shares the same store, so use a fresh `threadId` per test or call `runner.clearThreads()` (which resets the map, byte total, and eviction warn latch) between tests. Tests that customize limits must restore them: `new InMemoryAgentRunner(ɵINMEMORY_DEFAULTS)` in an `afterEach` — a no-arg construction is inert and will NOT restore defaults.
|
|
63
78
|
|
|
64
79
|
Source: `packages/runtime/src/v2/runtime/runner/in-memory.ts`.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`AgentRunner` is the abstraction that owns thread run state — active runs, the event stream
|
|
4
4
|
replay, and stop semantics. Pick one per `CopilotRuntime` instance.
|
|
5
5
|
|
|
6
|
-
- `InMemoryAgentRunner` — default;
|
|
6
|
+
- `InMemoryAgentRunner` — default; process-global in-memory Map; lost on restart.
|
|
7
7
|
- `SqliteAgentRunner` — file-backed; requires `better-sqlite3` peer.
|
|
8
8
|
- `IntelligenceAgentRunner` — auto-wired by `CopilotIntelligenceRuntime`. You do NOT
|
|
9
9
|
construct this directly and you cannot pass `runner` alongside `intelligence`.
|
|
@@ -80,9 +80,14 @@ class MyRunner extends AgentRunner {
|
|
|
80
80
|
|
|
81
81
|
### Handle double-submit on the client
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
`"Thread already running"` on concurrent `run()` calls for the same `threadId`.
|
|
85
|
-
|
|
83
|
+
By default, both `InMemoryAgentRunner` and `SqliteAgentRunner` throw
|
|
84
|
+
`"Thread already running"` on concurrent `run()` calls for the same `threadId`.
|
|
85
|
+
`"throw"` is the default, but it is not the only option: constructing
|
|
86
|
+
`InMemoryAgentRunner` with `onConcurrentRun: "supersede"` makes it abort the
|
|
87
|
+
in-flight run (the same path `stop()` takes) and start the new one instead of
|
|
88
|
+
throwing — the superseded run's partial output is discarded rather than persisted
|
|
89
|
+
to history. `SqliteAgentRunner` has no such option and always throws. When the
|
|
90
|
+
throw does happen, how it surfaces to the client depends on the runtime mode:
|
|
86
91
|
|
|
87
92
|
- **Intelligence mode** — the Intelligence platform returns HTTP `409` when a lock is
|
|
88
93
|
held. The client core maps this to `CopilotKitCoreErrorCode.AGENT_THREAD_LOCKED`
|
|
@@ -153,10 +158,12 @@ new CopilotRuntime({
|
|
|
153
158
|
// Or upgrade to Intelligence mode for managed durability.
|
|
154
159
|
```
|
|
155
160
|
|
|
156
|
-
The default runner is `new InMemoryAgentRunner()`. It keeps state in a
|
|
157
|
-
|
|
161
|
+
The default runner is `new InMemoryAgentRunner()`. It keeps state in a process-global,
|
|
162
|
+
bounded store — threads are lost on restart, evicted past the memory limits, and
|
|
163
|
+
horizontally-scaled instances see divergent state. See `agent-runners-in-memory.md`
|
|
164
|
+
for the bounds and how to tune them.
|
|
158
165
|
|
|
159
|
-
Source: `packages/runtime/src/v2/runtime/runner/in-memory.ts
|
|
166
|
+
Source: `packages/runtime/src/v2/runtime/runner/in-memory.ts`.
|
|
160
167
|
|
|
161
168
|
### HIGH Setting runner alongside intelligence option
|
|
162
169
|
|
|
@@ -259,11 +266,22 @@ const [busy, setBusy] = useState(false);
|
|
|
259
266
|
</button>;
|
|
260
267
|
```
|
|
261
268
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
By default both runners throw `"Thread already running"` on concurrent runs, so
|
|
270
|
+
debouncing on the client is still the right baseline. In Intelligence mode you can
|
|
271
|
+
additionally handle `code === "agent_thread_locked"` in `<CopilotKit onError>`; SSE
|
|
272
|
+
mode surfaces only a generic 500 with that message.
|
|
273
|
+
|
|
274
|
+
Throwing is the default (`onConcurrentRun: "throw"`), not the only behavior:
|
|
275
|
+
constructing `InMemoryAgentRunner` with `onConcurrentRun: "supersede"` aborts the
|
|
276
|
+
in-flight run (the `stop()` path) and starts the new one instead of throwing,
|
|
277
|
+
discarding the superseded run's partial output rather than persisting it. That
|
|
278
|
+
suits a UX where a fast follow-up should displace a still-running (or wedged) turn.
|
|
279
|
+
Unlike the process-global memory limits, `onConcurrentRun` is per-runner-instance —
|
|
280
|
+
it affects only the runner you pass it to. `SqliteAgentRunner` has no such option
|
|
281
|
+
and always throws.
|
|
282
|
+
|
|
283
|
+
Source: the `throw new Error("Thread already running")` in `InMemoryAgentRunner.run()`,
|
|
284
|
+
`packages/runtime/src/v2/runtime/runner/in-memory.ts`;
|
|
267
285
|
`packages/core/src/intelligence-agent.ts:368-369`.
|
|
268
286
|
|
|
269
287
|
### HIGH In-memory runner + horizontal scaling
|
|
@@ -278,22 +296,30 @@ new CopilotRuntime({ agents });
|
|
|
278
296
|
Correct:
|
|
279
297
|
|
|
280
298
|
```typescript
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
runner: new SqliteAgentRunner({ dbPath: process.env.THREADS_DB! }),
|
|
285
|
-
});
|
|
286
|
-
// Best: Intelligence mode for managed multi-instance durability.
|
|
299
|
+
// Sticky-session one instance per thread (so every run for a thread lands on the
|
|
300
|
+
// same process), OR move to Intelligence mode for managed multi-instance durability.
|
|
301
|
+
new CopilotRuntime({ agents }); // + route by threadId at the load balancer
|
|
287
302
|
```
|
|
288
303
|
|
|
289
|
-
`InMemoryAgentRunner`'s
|
|
304
|
+
`InMemoryAgentRunner`'s store is a process-global singleton — multi-instance deploys see
|
|
290
305
|
totally different thread state per worker, making reconnects and `GET /connect` non-deterministic.
|
|
291
306
|
|
|
292
|
-
Source: `packages/runtime/src/v2/runtime/runner/in-memory.ts
|
|
307
|
+
Source: the exported `ɵGLOBAL_STORE` singleton in `packages/runtime/src/v2/runtime/runner/in-memory.ts`.
|
|
308
|
+
|
|
309
|
+
A shared `dbPath` on `SqliteAgentRunner` is **not** a horizontal-scaling fix on its own.
|
|
310
|
+
Sharing the file gives you durable, persisted history: runs survive process restarts, and
|
|
311
|
+
completed runs are readable from any instance pointed at the same file. But the live-run
|
|
312
|
+
bookkeeping used by the connect-bridge and by `stop()` lives in a process-local
|
|
313
|
+
`ACTIVE_CONNECTIONS` map. A second instance has **no** entry for a run started elsewhere, so
|
|
314
|
+
it can replay stored history but **cannot** reconnect to — or stop — an in-flight run on
|
|
315
|
+
another instance. Use `SqliteAgentRunner` for restart-resilient single-instance durability;
|
|
316
|
+
for managed multi-instance durability, use Intelligence mode.
|
|
317
|
+
|
|
318
|
+
Source: `packages/sqlite-runner/src/sqlite-runner.ts:46` (module-level `ACTIVE_CONNECTIONS`).
|
|
293
319
|
|
|
294
320
|
## References
|
|
295
321
|
|
|
296
|
-
- [InMemoryAgentRunner —
|
|
322
|
+
- [InMemoryAgentRunner — store, bounds, concurrency, and lifecycle](agent-runners-in-memory.md)
|
|
297
323
|
- [SqliteAgentRunner — schema, retention, ops](agent-runners-sqlite.md)
|
|
298
324
|
- [Custom runner — Redis/Postgres skeleton](agent-runners-custom.md)
|
|
299
325
|
|
|
@@ -49,6 +49,11 @@ export default { fetch: handler };
|
|
|
49
49
|
Each server entry accepts an optional `agentId`. When set, the server's tools are only
|
|
50
50
|
exposed to that agent. Omit it to expose to all agents.
|
|
51
51
|
|
|
52
|
+
Per-tool filtering belongs to `@ag-ui/mcp-apps-middleware`. The runtime currently pins
|
|
53
|
+
version `0.0.3`, which does not support `includeTools` or `excludeTools`. Supplying either
|
|
54
|
+
key raises a configuration error instead of silently ignoring it; use a compatible
|
|
55
|
+
middleware release when that upstream policy contract is available.
|
|
56
|
+
|
|
52
57
|
## Gotcha — do NOT put MCP under agents
|
|
53
58
|
|
|
54
59
|
```typescript
|