3.3 KiB
InMemoryAgentRunner — default ephemeral runner. Keyed on a globalThis Symbol so thread state survives hot-module reloads during development.
Store layout
// packages/runtime/src/v2/runtime/runner/in-memory.ts
const GLOBAL_STORE_KEY = Symbol.for("@copilotkit/runtime/in-memory-store");
interface GlobalStoreData {
stores: Map<string, InMemoryEventStore>; // per-threadId
historicRunsBackup: Map<string, HistoricRun[]>; // restored after HMR
}
One InMemoryEventStore per threadId. Each store tracks:
subject: ReplaySubject<BaseEvent> | null— current consumersisRunning: boolean— gate for the"Thread already running"throwcurrentRunId: string | nullhistoricRuns: HistoricRun[]— completed runs (backed up across HMR)agent: AbstractAgent | null— the instance that owns the active runrunSubject,currentEvents,stopRequested
Lifecycle
run({ threadId, agent, input })— ifstore.isRunningthrowError("Thread already running"). Otherwise create aReplaySubject, subscribe toagent.run(input), push events into the subject, track them incurrentEvents, mark the storeisRunning.- On
RunFinishedEvent/RunErrorEvent: finalize the run, push its events intohistoricRuns, clearisRunning,currentRunId,agent. connect({ threadId })— returns aReplaySubjectthat replays the active run events or (if no active run) the most recent historic run.stop({ threadId })— setsstopRequested = true; the active subscription checks the flag on each event and tears down.
Hot reload (development)
In dev, bundlers replace modules. GLOBAL_STORE_KEY uses Symbol.for(...) so the same well-known symbol is reused across module instances — globalThis[KEY] survives. On module re-evaluation, if the stores map is empty but historicRunsBackup still has entries, the runner rehydrates historic-only stores from the backup (active runs are lost, historic runs come back).
When NOT to use
- Multi-instance production deploys — each process has its own store.
- Long-lived servers — restart wipes active threads (historic runs are only preserved in the HMR-dev backup, not across process exit).
- Load-balanced serverless with cold starts — new workers see empty stores.
When it is OK
-
Local development.
-
Single-instance preview environments.
-
Tests (each
new InMemoryAgentRunner()still shares the globalThis store — pass a fresh threadId per test, or clear the captured store in place between tests). Do NOTdelete globalThis[Symbol.for("@copilotkit/runtime/in-memory-store")]:in-memory.ts:98capturesGLOBAL_STORE = getGlobalStore()as a module-level const referencing the innerstoresMap, so replacingglobalThis[KEY]creates a new object that the module no longer consults. Mutate the existing maps in place:// test setup const storeKey = Symbol.for("@copilotkit/runtime/in-memory-store"); const data = (globalThis as any)[storeKey] as | { stores: Map<string, unknown>; historicRunsBackup: Map<string, unknown>; } | undefined; if (data) { data.stores.clear(); data.historicRunsBackup.clear(); }The runtime does not yet expose an official reset helper — a
__TEST_ONLY_clearGlobalStoreexport would be a reasonable follow-up.
Source: packages/runtime/src/v2/runtime/runner/in-memory.ts.