UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

124 lines • 6.84 kB
import type { SinkWriter, StreamFrame, TokenStreamSink } from './spi/token-stream-sink.js'; import type { LucidDatabaseLike } from './stores/lucid.js'; export interface LucidTokenStreamSinkOptions { /** The frame table. Defaults to `agent_stream_frame` (`AGENT_TABLES.streamFrames`). */ tableName?: string; /** * Gap (ms) between two reads of a run a subscriber is following — the most a frame waits in the * table before a browser sees it. Defaults to 250. */ pollIntervalMs?: number; /** * Gap (ms) between two reads once the run has written nothing for five seconds (parked on a * human, a slow tool), so an open SSE connection on a quiet run costs one query a second rather * than four. The first frame to arrive puts the subscriber back on `pollIntervalMs`. Defaults to * 1000; never below `pollIntervalMs`. */ idlePollIntervalMs?: number; /** * Window (ms) consecutive `text` frames are gathered over and written as ONE row — a model * streams token by token, and a row per token is more writes than a database should take for a * chat answer. Any other frame, and `end()`, write what is gathered first, so order is kept. * Defaults to 50. `0` writes every frame as its own row. */ flushMs?: number; /** * TTL (seconds) of a run's rows, counted from its LAST write — so a long run stays, and a run * that crashed without ending still lapses. {@link LucidTokenStreamSink.purgeExpired} is what * deletes them. Defaults to 3600 (1h). `0` keeps rows until `close()`/manual cleanup. */ ttlSeconds?: number; /** * Purge lapsed runs from this instance as a side effect of ending a run, at most once a minute, * so the table stays bounded in an app that never schedules {@link LucidTokenStreamSink.purgeExpired}. * Default `true`. Set `false` to purge only from your own schedule. */ autoPurge?: boolean; /** * Create the frame table on first use (and at startup, under the agent provider). Default `true`. * Set `false` when the published migration (`createAgentTables`) owns the schema. */ autoCreateTables?: boolean; } /** * A multi-replica {@link TokenStreamSink} over the app's SQL database (through `@adonisjs/lucid`) — * for a deployment with several replicas and NO Redis. The replica running the model turn appends * each frame as a row of `agent_stream_frame`; any replica serves the run's SSE by reading the rows * past its cursor, in order, until it meets the end marker. A subscriber that attaches late, or * after the run ended, replays from the first row — the same guarantee the in-process and Redis * sinks give. * * Two things differ from the Redis sink, and both are the price of having no broker: * * - **Delivery is polled.** There is no publish to wake a subscriber, so a frame waits up to * `pollIntervalMs` (250 ms) before a browser sees it. * - **Text is coalesced on the way in.** Consecutive `text` frames written within `flushMs` * (50 ms) are stored as ONE `text` frame holding their concatenation. The stored rows are the * run's stream: every subscriber, on every replica, reads the same rows in the same order, so * the SSE event ids `AgentSseEncoder` counts from them — and an `after` cursor — stay exact. They * are NOT the ids the in-process sink would have produced for the same run (it numbers every * token), which only matters to a cursor carried across a change of sink. * * Sequence numbers are per run, start at 1 and have no gaps: each append takes `MAX(seq) + 1` for * its run in the same statement that inserts it, and the `(run_id, seq)` primary key turns a * collision into a retry. A run normally has ONE writer, and then no append ever collides; the * retry exists for a parent whose delegated children (each forwarding into its stream, possibly * from another replica) write at once. Nothing depends on ordering across runs. * * Text a writer is still gathering lives in the process for at most `flushMs`; a process that dies * in that window loses it from the LIVE stream (the persisted message is unaffected). * * Wire it with `defineConfig({ sink: tokenSinks.lucid() })`. */ export declare class LucidTokenStreamSink implements TokenStreamSink { private readonly db; private readonly table; private readonly pollIntervalMs; private readonly idlePollIntervalMs; private readonly flushMs; private readonly ttlSeconds; private readonly autoPurge; private readonly autoCreateTables; private readonly writes; private provisioned; private lastAutoPurge; constructor(db: LucidDatabaseLike, options?: LucidTokenStreamSinkOptions); /** * Create the frame table if it is missing. The agent provider calls this once as the app starts; * a sink built by hand falls back to it on first use. A no-op under `autoCreateTables: false`. */ ensureSchema(): Promise<void>; private runWrites; /** Forget a run's write state once nothing is gathered, queued or left to report. */ private release; /** Queue `work` behind every write already queued for the run, so rows land in call order. */ private enqueue; /** Queue the gathered text (if any) as one `text` row. */ private flushText; /** Raise, once, a gathered write that failed with nobody awaiting it. */ private raiseFailure; /** Append one row to the run — a frame, or the end marker (`null`). */ private append; /** * The run's writer. Writers opened for the same run in one process share what they are gathering, * so a delegated run's text (forwarded through a {@link childSinkWriter}) and its parent's next * frame keep the order they were written in. */ open(runId: string): SinkWriter; subscribe(runId: string): AsyncIterable<StreamFrame>; has(runId: string): Promise<boolean>; close(runId: string): Promise<void>; /** * Delete every run whose last write is older than `ttlSeconds` at `now` (epoch ms) — ended runs * past their replay window and runs that crashed without ending alike. Returns how many runs went. * Safe to call from any replica, and from several at once. A no-op under `ttlSeconds: 0`. * * An instance already calls it after a run ends (at most once a minute; `autoPurge`), which keeps * the table bounded on a replica that streams. Call it from a schedule as well if a quiet * deployment must not keep the last hour's streams around indefinitely. */ purgeExpired(now?: number): Promise<number>; /** {@link purgeExpired}, unawaited and at most once a minute. Its failure is not the run's. */ private purgeInBackground; } //# sourceMappingURL=lucid-token-stream-sink.d.ts.map