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;" />

331 lines (330 loc) 15.8 kB
import "reflect-metadata"; import { AgentRunner, AgentRunnerConnectRequest, AgentRunnerIsRunningRequest, AgentRunnerRunRequest, AgentRunnerStopRequest } from "./agent-runner.mjs"; import { Observable, ReplaySubject } from "rxjs"; import { AbstractAgent, BaseEvent, Message } from "@ag-ui/client"; //#region src/v2/runtime/runner/in-memory.d.ts interface InMemoryLimits { /** LRU cap on distinct threads. */ maxThreads?: number; /** FIFO cap on runs kept per thread. `Infinity` or `0` disables the cap. */ maxRunsPerThread?: number; /** * Approximate byte ceiling on RETAINED thread/run history. Enforced at run * completion (in `appendRun`), where LRU non-running threads are evicted to * keep the total under this limit. * * Limitation: this bounds only history that has already been committed. A * single in-flight run's buffered events (`currentRunEvents` and the two * `ReplaySubject<BaseEvent>(Infinity)` buffers in `run()`) are NOT counted * until that run completes, so `maxBytes` does not bound a single runaway * run mid-stream. * * Limitation: byte eviction drops only other LRU non-running threads and * never self-evicts the active/just-appended thread, so a single dominant * thread's own retained history is not byte-trimmed (bounded only by * `maxRunsPerThread`). `maxBytes` is thus a cross-thread ceiling enforced by * evicting OTHER threads, not a per-thread cap. */ maxBytes?: number; } /** * Constructor options for {@link InMemoryAgentRunner}. * * Extends {@link InMemoryLimits} so bounds can be passed inline alongside the * per-runner behavior flags. Be aware of the scope difference: the limits * reconfigure the process-global store shared by every runner, whereas * `onConcurrentRun` applies only to the runner instance it is passed to. */ interface InMemoryAgentRunnerOptions extends InMemoryLimits { /** * How to handle a `run()` for a thread that already has an in-flight run. * `"throw"` (default) rejects with "Thread already running". `"supersede"` * aborts the prior run and starts the new one. */ onConcurrentRun?: "throw" | "supersede"; } declare const ɵINMEMORY_DEFAULTS: Required<InMemoryLimits>; /** * Normalize a fully-resolved limits bag so every field is well-formed before it * can reach an enforcement loop. Each field is validated independently against * {@link ɵisValidLimit}; an invalid value is CLAMPED to its * {@link ɵINMEMORY_DEFAULTS} floor and a single `console.warn` naming the field * and the received value is emitted. * * Clamp-and-warn (rather than throw) is deliberate and matches this file's * established posture toward bad input: `ɵestimateBytes` swallows serialization * failures and returns 0, the limits-clobber path warns rather than throwing, * and both the eviction and clobber logs are wrapped so "logging must never * break construction/a run". Constructing a bounded in-memory runner is a * best-effort, non-durable convenience; a typo'd bound must degrade to a safe * default, never abort construction or (worse) surface later as an unhandled * rejection from the fire-and-forget finalize path. */ declare function ɵnormalizeLimits(limits: Required<InMemoryLimits>): Required<InMemoryLimits>; /** * Best-effort approximate byte size of a value, via serialized length. * Never throws — returns 0 when the value cannot be serialized. This is an * approximation (UTF-16 length, not exact heap bytes), used only for relative * accounting against `maxBytes`. */ declare function ɵestimateBytes(value: unknown): number; /** * Per-run finalize intent, captured once when a run starts and mutated (only) * by whoever aborts THAT run — `stop()` or a superseding `run()`. The run's own * teardown reads this captured holder instead of the shared, mutable * `store.stopRequested`, so a later run that resets store state can never cause * an intentionally-stopped run to be finalized as an error (or vice versa). */ interface RunFinalizeControl { /** True once THIS run has been asked to stop (clean stop, not an error). */ stopRequested: boolean; } interface HistoricRun { threadId: string; runId: string; /** ID of the agent that executed this run. */ agentId: string; parentRunId: string | null; events: BaseEvent[]; /** * Snapshot of all messages (input + generated) at the end of this run, as * passed in by the caller. NOTE: `BoundedThreadStore.appendRun` moves this * snapshot to the THREAD level (`InMemoryEventStore.messagesSnapshot`) and * clears this field to `[]`, so a stored HistoricRun never carries messages. * The thread-messages fallback reads the thread-level snapshot, not this. */ messages: Message[]; createdAt: number; /** Approximate retained byte size of `events`; set by BoundedThreadStore at append. */ approxEventBytes?: number; /** * Legacy field retained for shape compatibility. `appendRun` always zeroes it * because message bytes are accounted at the thread level, not per run. */ approxMessageBytes?: number; } /** * Lightweight thread summary returned by {@link InMemoryAgentRunner.listThreads}. * Shape matches the Intelligence platform's ThreadRecord so the same HTTP * response envelope can be used for both backends. */ interface InMemoryThread { id: string; name: string | null; agentId: string; organizationId: ""; createdById: ""; archived: false; createdAt: string; updatedAt: string; } declare class InMemoryEventStore { threadId: string; constructor(threadId: string); /** The subject that current consumers subscribe to. */ subject: ReplaySubject<BaseEvent> | null; /** True while a run is actively producing events. */ isRunning: boolean; /** Current run ID */ currentRunId: string | null; /** Historic completed runs */ historicRuns: HistoricRun[]; /** Currently running agent instance (if any). */ agent: AbstractAgent | null; /** Subject returned from run() while the run is active. */ runSubject: ReplaySubject<BaseEvent> | null; /** * Thread-level lifecycle flag: true once a stop/supersede has been requested * for the currently-owning run but that run has not yet finalized. Drives * eviction protection, the connect() bridge, and stop() de-dup. This is NOT * the finalize intent read by a run's teardown — that lives per-run on * {@link activeFinalize}, so a superseding run resetting this field cannot * mislabel the run it replaced. A new run resets this to false when it takes * ownership. */ stopRequested: boolean; /** * Finalize control of the currently-owning run. `stop()` and a superseding * `run()` flip the owning run's flag through this reference; each run also * captures the SAME object in its closure, so its teardown finalizes against * its own intent regardless of what a later run does to the store. */ activeFinalize: RunFinalizeControl | null; /** Reference to the events emitted in the current run. */ currentEvents: BaseEvent[] | null; /** * The thread's single latest NON-EMPTY message snapshot, held at the THREAD * level (independent of `historicRuns` lifecycle). Decoupling the snapshot * from per-run storage means run-cap FIFO eviction and interleaved * empty-snapshot runs can never drop or pin the thread's message history. */ messagesSnapshot: Message[]; /** Approximate retained byte size of `messagesSnapshot`. */ approxMessagesSnapshotBytes: number; /** * The thread's true creation timestamp (epoch ms), captured from the FIRST * run ever appended and held at the THREAD level (independent of * `historicRuns` lifecycle). Decoupling it from per-run storage means run-cap * FIFO eviction — which shifts the oldest entries off `historicRuns` — can * never move the reported creation time forward. `null` until the first run * lands. Mirrors the `messagesSnapshot` thread-level decoupling. */ createdAt: number | null; } declare class ɵBoundedThreadStore { private readonly map; private totalBytes; private warned; /** True once limits have been EXPLICITLY set (via setLimits), not just the constructor default. */ private limitsExplicitlySet; /** Warn-once latch for the clobber warning, kept distinct from the eviction `warned` latch. */ private clobberWarned; private limits; constructor(limits: Required<InMemoryLimits>); get byteTotal(): number; /** * The store's CURRENT effective bounds. Exposed (with the `ɵ` internal-API * prefix) so a partial `setLimits` can coalesce unspecified fields against the * live config rather than the hardcoded {@link ɵINMEMORY_DEFAULTS} — a partial * update must be a partial update, never a silent reset of the fields the * caller did not mention. Returns a copy so callers cannot mutate the store's * bounds through it. */ get ɵlimits(): Required<InMemoryLimits>; /** * Reconfigure the process-global store's bounds. Called by the * {@link InMemoryAgentRunner} constructor when limits are passed. Because the * store is a per-process singleton, this replaces the bounds for ALL in-memory * threads. Emits {@link LIMITS_CLOBBER_GUIDANCE} at most ONCE per store when a * SECOND (or later) explicit set arrives whose resolved values differ from the * prior explicit set — i.e. a genuine clobber of an already-customized config. * The first explicit customization (defaults → custom) is the intended * override and never warns; identical re-sets never warn. */ setLimits(limits: Required<InMemoryLimits>): void; get size(): number; /** Re-insert at the tail so Map iteration order stays LRU-first. */ private touchOrder; getOrCreate(threadId: string): InMemoryEventStore; get(threadId: string, opts: { touch: boolean; }): InMemoryEventStore | undefined; peek(threadId: string): InMemoryEventStore | undefined; /** * Evict the least-recently-used thread that is neither running NOR * mid-finalization. Returns false if none evictable. The `protect` thread * (typically the one just created) is never evicted, so a fresh thread is not * immediately dropped when it is the only non-running candidate. * * A thread is skipped while `isRunning` OR `stopRequested` is set. * `stop()` flips `isRunning` to false the moment it aborts the agent, but the * run keeps finalizing asynchronously (the abort trips the `catch` in * `runAgent`, which later calls `appendRun`). During that window * `stopRequested` stays true; evicting the thread then would make the pending * `appendRun` hit `if (!store) return` and silently drop the aborted run's * history. Guarding on `stopRequested` keeps the thread alive until * finalization completes. */ private evictOneLru; appendRun(threadId: string, run: HistoricRun): void; private enforceRunCap; /** * Trim the store back under the byte ceiling by evicting LRU non-running * threads. `protect` (the just-appended thread) is never self-evicted, so a * fresh run pushes OTHER threads out rather than dropping itself. */ private evictByBytesIfNeeded; private removeThread; private evictThreadsIfNeeded; private noteEviction; listThreads(): InMemoryThread[]; clear(): void; } /** * Process-wide singleton backing every {@link InMemoryAgentRunner}. Exported * (with the `ɵ` internal-API prefix) so tests can inspect the exact store the * runner writes to; not part of the public API. */ declare const ɵGLOBAL_STORE: ɵBoundedThreadStore; declare class InMemoryAgentRunner extends AgentRunner { readonly ɵsupportsLocalThreadEndpoints = true; /** * How to handle a `run()` for a thread that already has an in-flight run. * `"throw"` (default) preserves the historic behavior. `"supersede"` aborts * the prior run (mirroring `stop()`) and starts the new one — opted into by * the hosted-bot listener so a fast follow-up turn on the same thread cleanly * replaces a still-running (or wedged) prior turn instead of erroring with * "Thread already running". */ private readonly onConcurrentRun; /** * @param options Per-runner behavior (`onConcurrentRun`) plus optional bounds * for the in-memory store ({@link InMemoryLimits}). * * Note the differing scopes: `onConcurrentRun` is per-runner instance, while * the limits reconfigure the PROCESS-GLOBAL store shared by every * `InMemoryAgentRunner`. Omit the limits for safe defaults * ({@link ɵINMEMORY_DEFAULTS}); passing none leaves the store untouched. When * multiple runners are constructed with differing limits, the last-constructed * wins — in practice the OSS/SSE default construction passes nothing. If a * second (or later) runner is constructed with limits that DIFFER from an * already-customized store, a one-time `console.warn` is emitted to signal that * the shared store's bounds are being clobbered for ALL in-memory threads. */ constructor(options?: InMemoryAgentRunnerOptions); run(request: AgentRunnerRunRequest): Observable<BaseEvent>; connect(request: AgentRunnerConnectRequest): Observable<BaseEvent>; isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean>; stop(request: AgentRunnerStopRequest): Promise<boolean | undefined>; /** * Returns a summary of every thread that has been run through this runner. * * This powers the local-dev fallback for `GET /threads` when the Intelligence * platform is not configured. Each entry mirrors the shape of a platform * `ThreadRecord` so the HTTP handler can use the same response envelope. */ listThreads(): InMemoryThread[]; /** * Returns all messages for a thread, using the snapshot captured at the end * of the most recent run. * * This powers the local-dev fallback for `GET /threads/:threadId/messages` * when the Intelligence platform is not configured. The returned `Message[]` * objects come directly from the ag-ui agent, so their shape is compatible * with the Intelligence platform's `ThreadMessage` type. */ getThreadMessages(threadId: string): Message[]; /** * Returns all AG-UI events for a thread, compacted across historic runs. * * Powers the local-dev fallback for `GET /threads/:threadId/events` when the * Intelligence platform is not configured. The compaction logic matches * the connection-replay path in {@link connect}, so the stream a * late-joining inspector sees matches what this method returns. */ getThreadEvents(threadId: string): BaseEvent[]; /** * Returns the agent state snapshot for a thread. * * Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The * AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces * a single trailing STATE_SNAPSHOT when state changes exist, so this is a * faithful view of state at the end of the most recent run. * * Returns `null` when the thread has never emitted a STATE_SNAPSHOT. */ getThreadState(threadId: string): Record<string, unknown> | null; /** * Clears all in-memory thread history. * * Powers the local-dev fallback for `POST /threads/clear`, letting consumers * (e.g. the demo's Clear button) reset to an empty thread list without * restarting the runtime. Intentionally not exposed on the Intelligence * platform path: there, thread history lives in a real database and must * not be wiped this way. */ clearThreads(): void; } //#endregion export { InMemoryAgentRunner, InMemoryAgentRunnerOptions, InMemoryLimits, InMemoryThread, ɵBoundedThreadStore, ɵGLOBAL_STORE, ɵINMEMORY_DEFAULTS, ɵestimateBytes, ɵnormalizeLimits }; //# sourceMappingURL=in-memory.d.mts.map