@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
TypeScript
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