@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
text/typescript
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