UNPKG

@copilotkit/runtime

Version:

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

1 lines 55.4 kB
{"version":3,"file":"in-memory.cjs","names":["AgentRunner","EventType","ReplaySubject"],"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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