@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
473 lines • 24.1 kB
TypeScript
import type { ApplicationService } from '@adonisjs/core/types';
import type { MediaAttachmentsOptions } from '../media/media-attachment-staging.js';
import type { HybridRetrieverOptions } from '../rag/hybrid-retriever.js';
import type { IngestDocument } from '../rag/ingest.js';
import type { PgFullTextOptions, PgVectorColumns, PgVectorMetric, PgVectorStoreOptions } from '../rag/pg-vector-store.js';
import type { QdrantClientLike, QdrantMetric } from '../rag/qdrant-store.js';
import type { RedisStreamClient } from '../redis-stream-client.js';
import type { ActorDirectory } from '../spi/actor-directory.js';
import type { AgentStore } from '../spi/agent-store.js';
import type { AttachmentStagingDescription, AttachmentStagingStore } from '../spi/attachment-staging.js';
import type { EmbeddingProvider } from '../spi/embedding-provider.js';
import type { AgentGovernanceQueries } from '../spi/governance-queries.js';
import type { AgentPricingStore } from '../spi/pricing-store.js';
import type { Retriever } from '../spi/retriever.js';
import type { TokenStreamSink } from '../spi/token-stream-sink.js';
import type { LucidDatabaseLike } from './lucid.js';
/**
* Runtime context a {@link StoreFactory} thunk receives when the agent provider builds the configured
* store at boot. Carries the booted application so a driver can resolve a peer's service if it needs to.
*/
export interface StoreContext {
app: ApplicationService;
}
/**
* A configured agent store: a thunk the agent provider calls at boot to build the {@link AgentStore}.
* Each factory lazily imports its peer dependency (`@adonisjs/lucid`) inside the thunk, so the driver
* is only loaded when it is actually selected — keeping that package optional.
*/
export type StoreFactory = (ctx: StoreContext) => Promise<AgentStore>;
/**
* If `factory` is a `stores.lucid()` store factory, returns its connection (`null` = the app's default
* connection); otherwise `undefined`. The provider uses this to mirror the main store's Lucid
* connection when `pricingStore`/`governanceQueries` are left at their defaults.
*/
export declare function lucidStoreConnection(factory: unknown): string | null | undefined;
/** Options for the Lucid-backed persistent store. */
export interface LucidStoreConfig {
/** Lucid connection name to use. Defaults to the `Database` default connection. */
connection?: string;
/**
* Provision the agent tables on first use — the lib manages its own schema (the ecosystem
* convention, mirroring durable/authz). Default `true`. Set `false` to run the published migration
* instead. Governs the pricing store and governance read-model too, which share these tables.
*/
autoCreateTables?: boolean;
}
/** Options for the in-memory store (single-process, non-durable). */
export type MemoryStoreConfig = Record<string, never>;
/**
* The store factory namespace used in `config/agent.ts`:
*
* ```ts
* import { defineConfig, stores } from '@adonis-agora/agent'
*
* export default defineConfig({
* store: 'lucid',
* stores: {
* memory: stores.memory(),
* lucid: stores.lucid({ connection: 'pg' }),
* },
* })
* ```
*
* Each factory returns a {@link StoreFactory} — a lazy thunk. Calling it in the config file costs
* nothing; the peer dependency is only imported when the provider builds the selected store at boot.
*/
export declare const stores: {
/** In-memory store — single-process, non-durable. Handy for tests and scratch/dev apps. */
memory(_config?: MemoryStoreConfig): StoreFactory;
/** Persist threads/messages/tool-calls/usage in SQL via `@adonisjs/lucid`. */
lucid(config?: LucidStoreConfig): StoreFactory;
};
/** Runtime context a {@link PricingFactory} thunk receives — the booted application. */
export type PricingContext = StoreContext;
/** A configured pricing store: a lazy thunk the agent provider calls at boot. */
export type PricingFactory = (ctx: PricingContext) => AgentPricingStore | Promise<AgentPricingStore>;
/** Options for the Lucid-backed pricing store. */
export interface LucidPricingConfig {
/** Lucid connection name to use. Defaults to the `Database` default connection. */
connection?: string;
/**
* Provision the shared agent tables on first use. Default `true` (the ecosystem convention) — this
* is what lets a pricing seed run before the first agent run. Set `false` to run the migration.
*/
autoCreateTables?: boolean;
}
/**
* The pricing factory namespace used in `config/agent.ts`, mirroring {@link stores}:
*
* ```ts
* export default defineConfig({
* pricingStore: pricingStores.lucid(),
* })
* ```
*
* Seed model prices once (e.g. in a command) with `seedModelPrices(store, [...])`.
*/
export declare const pricingStores: {
/** In-memory pricing table — for tests and the offline demo. */
memory(): PricingFactory;
/** Persist per-model prices in SQL (the `agent_model_pricing` table) via `@adonisjs/lucid`. */
lucid(config?: LucidPricingConfig): PricingFactory;
};
/**
* Runtime context a {@link GovernanceQueriesFactory} thunk receives — the {@link StoreContext} plus the
* already-resolved {@link AgentPricingStore} (when the app configured one), so the read-model prices its
* cost rollups against the SAME live prices the loop's cost fold uses. The provider resolves pricing
* first, then the governance queries, then passes it here (the way a store-reading factory is handed the
* built store). `pricingStore` is absent when the app configured none → the read-model reports 0 cost.
*/
export interface GovernanceQueriesContext extends StoreContext {
pricingStore?: AgentPricingStore;
}
/**
* A configured governance read-model: a lazy thunk the agent provider calls at boot to build the
* {@link AgentGovernanceQueries} the optional `/agent/governance/*` read routes serve from. Each factory
* lazily imports its peer inside the thunk (like {@link stores}), so nothing loads until governance is
* actually selected.
*/
export type GovernanceQueriesFactory = (ctx: GovernanceQueriesContext) => AgentGovernanceQueries | Promise<AgentGovernanceQueries>;
/** Options for the Lucid-backed governance read-model. */
export interface LucidGovernanceConfig {
/** Lucid connection name to use. Defaults to the `Database` default connection. */
connection?: string;
/**
* Provision the shared agent tables on first use. Default `true` (the ecosystem convention) — lets
* the governance routes / dashboard answer on a fresh deploy before the first agent run. Set
* `false` to run the migration.
*/
autoCreateTables?: boolean;
}
/**
* The governance-queries factory namespace used in `config/agent.ts`, mirroring {@link pricingStores}:
*
* ```ts
* export default defineConfig({
* pricingStore: pricingStores.lucid(),
* governanceQueries: governanceQueries.lucid(),
* })
* ```
*
* The Lucid read-model aggregates the same five agent tables the store writes and prices cost against
* the configured `pricingStore`; wiring it mounts the `/agent/governance/*` read routes.
*/
export declare const governanceQueries: {
/** Aggregate the five agent tables in SQL via `@adonisjs/lucid`, priced against the pricing store. */
lucid(config?: LucidGovernanceConfig): GovernanceQueriesFactory;
};
/** Runtime context a {@link RetrieverFactory} thunk receives — the booted application. */
export type RetrieverContext = StoreContext;
/**
* A configured retriever: a lazy thunk the agent provider calls at boot to build the {@link Retriever}
* wired into inject-mode retrieval. Each factory imports the RAG stack inside the thunk, so nothing is
* loaded until a retriever is actually selected — mirroring {@link stores}.
*/
export type RetrieverFactory = (ctx: RetrieverContext) => Retriever | Promise<Retriever>;
/** A lazy {@link EmbeddingProvider} factory, so an embedding SDK peer imports only when the config loads. */
export type EmbeddingFactory = () => EmbeddingProvider | Promise<EmbeddingProvider>;
/** Options for the in-memory embedding retriever (cosine over a Map, no infra). */
export interface MemoryRetrieverConfig {
/** The embedding provider (query + ingestion), or a lazy factory so its SDK peer loads lazily. */
embedder: EmbeddingProvider | EmbeddingFactory;
/** Documents to ingest at boot (chunk → embed → index). Omit to ingest separately at runtime. */
documents?: IngestDocument[];
/** Target max characters per chunk. Default 800. */
chunkSize?: number;
/** Characters of overlap between chunks. Default 100. */
overlap?: number;
}
/** Options for the pgvector-backed retriever (production RAG over `@adonisjs/lucid` + pgvector). */
export interface PgVectorRetrieverConfig {
/** The embedding provider (query + ingestion), or a lazy factory so its SDK peer loads lazily. */
embedder: EmbeddingProvider | EmbeddingFactory;
/**
* A structural Lucid DB handle (bring-your-own). Omit to resolve `@adonisjs/lucid`'s default `db`
* service lazily inside the thunk (optionally on `connection`), keeping the peer optional.
*/
db?: LucidDatabaseLike;
/** Lucid connection name (used only when `db` is omitted). Defaults to the `Database` default. */
connection?: string;
/** Chunk table name. Default `agent_rag_chunks`. Validated against a strict identifier regex. */
table?: string;
/**
* Embedding width — must match the model (e.g. 1536 for text-embedding-3-small). Default 1536. A list
* makes a mixed-dimension table (one partial HNSW index per width); see `PgVectorStoreOptions`.
*/
dimension?: number | readonly number[];
/** Similarity metric / distance operator (`cosine` default, `l2`, `inner`). */
metric?: PgVectorMetric;
/** Override the physical column names (each validated). */
columns?: PgVectorColumns;
/** Allow chunks without an embedding (`embedding: []` → `NULL`). Default `false`. */
nullableEmbeddings?: boolean;
/** pgvector ≥ 0.8 iterative index scan for filtered searches. Default off. */
iterativeScan?: PgVectorStoreOptions['iterativeScan'];
/** `hnsw.ef_search` for searches. */
efSearch?: number;
/** Rows per multi-row upsert statement. Default 100. */
upsertBatchSize?: number;
/**
* Add a Postgres full-text leg: the store becomes a `PgLexicalVectorStore` and the retriever a
* `HybridRetriever` fusing vector + full-text search with reciprocal rank fusion (`{}` for the
* defaults: `simple` config over `to_tsvector(text)`). Needs the GIN index — `ensureSchema` or the
* migration creates it. Default off (vector search only). `minScore` then applies to neither leg: a
* fused rank is not a similarity.
*/
fullText?: PgFullTextOptions & {
hybrid?: HybridRetrieverOptions;
};
/**
* Provision the `vector` extension, table, and index at boot (idempotent DDL). Handy for tests/scripts;
* production should run the bundled migration instead. Default `false`.
*/
ensureSchema?: boolean;
/** Documents to ingest at boot (chunk → embed → upsert). Omit to ingest separately at runtime. */
documents?: IngestDocument[];
/** Target max characters per chunk. Default 800. */
chunkSize?: number;
/** Characters of overlap between chunks. Default 100. */
overlap?: number;
}
/** Options for the Qdrant-backed retriever (production RAG over a managed vector DB, e.g. GuaraCloud). */
export interface QdrantRetrieverConfig {
/** Embedder (or a lazy factory) — the same shape as `pgvector`/`memory`. */
embedder: EmbeddingProvider | (() => Promise<EmbeddingProvider>);
/** Qdrant URL, used when no `client` is passed. Default `http://localhost:6333`. */
url?: string;
/** Qdrant API key (optional). */
apiKey?: string;
/** A ready structural client — for programmatic use and tests; absent → built from `url`/`apiKey`. */
client?: QdrantClientLike;
/** Collection name. Default `agent_rag_chunks`. */
collection?: string;
/** Embedding width — must match the model. Default 1536. */
dimension?: number;
/** Similarity metric. Default `cosine`. */
metric?: QdrantMetric;
/** Provision the collection (dimension + metric) at boot — handy for tests and ingestion. */
ensureCollection?: boolean;
/** Documents to ingest at boot (optional, same shape as `pgvector`). */
documents?: IngestDocument[];
/** Target max characters per chunk. Default 800. */
chunkSize?: number;
/** Characters of overlap between chunks. Default 100. */
overlap?: number;
}
/**
* The retriever factory namespace used in `config/agent.ts`, mirroring {@link stores}:
*
* ```ts
* export default defineConfig({
* model: () => aiSdkModel(openai('gpt-4o-mini')),
* retriever: retrievers.memory({ embedder: myEmbedder, documents: [...] }),
* retrievalTopK: 5,
* })
* ```
*
* Use `retrievers.pgvector({...})` for the production pgvector/Lucid store or `retrievers.qdrant({...})`
* for a managed Qdrant collection; `retrievers.memory` covers tests and small/embedded corpora.
*/
export declare const retrievers: {
/** In-memory embedding retriever — cosine over a Map, single-process. Handy for tests and dev apps. */
memory(config: MemoryRetrieverConfig): RetrieverFactory;
/**
* Production pgvector retriever — cosine/L2/inner similarity over a `vector(N)` column via
* `@adonisjs/lucid` (raw SQL for the pgvector operators; the embedding is a `?::vector` binding). The
* Lucid peer is imported lazily inside the thunk (unless a structural `db` is passed), so it stays
* optional. Pass `ensureSchema: true` to provision the extension/table/index at boot for tests.
*/
pgvector(config: PgVectorRetrieverConfig): RetrieverFactory;
/**
* Production Qdrant retriever — cosine/dot/euclid via `@qdrant/js-client-rest`. The client is
* imported LAZILY inside the thunk (unless a structural `client` is passed), so the package stays an
* OPTIONAL peer. Pass `ensureCollection: true` to provision the collection at boot.
*/
qdrant(config: QdrantRetrieverConfig): RetrieverFactory;
};
/**
* Runtime context an {@link AttachmentStagingFactory} thunk receives — the booted application, and
* the agent store the provider built (what `attachmentStores.media()` asks which media a live
* message of the actor carries).
*/
export interface AttachmentStagingContext extends StoreContext {
agentStore?: AgentStore;
}
/**
* A configured attachment-staging store: a lazy thunk the agent provider calls at boot to build the
* {@link AttachmentStagingStore} the optional `POST /agent/attachments` upload route stages through.
* Each factory imports its impl inside the thunk (like {@link stores}), so nothing is loaded until a
* staging store is actually selected.
*/
export type AttachmentStagingFactory = (ctx: AttachmentStagingContext) => AttachmentStagingStore | Promise<AttachmentStagingStore>;
/**
* The attachment-store factory namespace used in `config/agent.ts`:
*
* ```ts
* export default defineConfig({
* model,
* attachments: attachmentStores.media(), // uploads on @adonis-agora/media, any Drive disk
* })
* ```
*
* Bring your own storage by passing an {@link AttachmentStagingStore} instance instead.
*/
export declare const attachmentStores: {
/**
* Uploads stored through `@adonis-agora/media` (register `@adonis-agora/media/media_provider`
* and configure `config/media.ts`): media rows in their own collection, one owner per actor, on
* any Drive disk. The url the model fetches is minted per request — signed and short-lived by
* default — so it is never taken from the client. See `MediaAttachmentsOptions`.
*/
media(options?: MediaAttachmentsOptions): AttachmentStagingFactory;
/**
* In memory — the uploaded bytes become a `data:` url the model reads inline. Single-process and
* lost on restart: tests and demos. `limits` sets the size cap / content types it declares.
*/
memory(limits?: AttachmentStagingDescription): AttachmentStagingFactory;
};
/**
* A configured {@link TokenStreamSink}: a lazy thunk the agent provider calls at boot to build the live
* token transport (the "data plane"). Each factory imports its impl — and, for Redis, its optional
* driver — inside the thunk, so nothing loads until the sink is actually selected. Assignable to the
* `sink` field's `SinkFactory`.
*/
export type TokenSinkFactory = (ctx: StoreContext) => TokenStreamSink | Promise<TokenStreamSink>;
/** Options for the Redis multi-replica token-stream sink. */
export interface RedisTokenSinkConfig {
/** `@adonisjs/redis` connection name. Omit for the default connection. */
connection?: string;
/** Key/channel namespace. Defaults to `agent:stream`. */
keyPrefix?: string;
/**
* TTL (seconds) for a run's replay keys so they self-expire — the framework never calls the sink's
* `close()`, so without a TTL the buffer would accumulate in Redis forever. Sliding window refreshed
* on every write. Defaults to 3600 (1h). Set `0` to disable (retain until manual cleanup).
*/
ttlSeconds?: number;
/**
* Bring-your-own {@link RedisStreamClient} adapter (over `ioredis`, `node-redis`, ...). When set, the
* `@adonisjs/redis` peer is NOT imported — the factory uses this client directly. Handy for tests and
* non-Adonis Redis drivers.
*/
client?: RedisStreamClient;
}
/** Options for the Lucid (SQL) multi-replica token-stream sink — several replicas, no Redis. */
export interface LucidTokenSinkConfig {
/** Lucid connection name to use. Defaults to the `Database` default connection. */
connection?: string;
/** The frame table. Defaults to `agent_stream_frame`. */
tableName?: string;
/**
* Gap (ms) between two reads of a run a subscriber is following — the most a frame waits before a
* browser sees it. Defaults to 250.
*/
pollIntervalMs?: number;
/**
* Gap (ms) between two reads once the run has been quiet for five seconds (parked on a human, a
* slow tool). Defaults to 1000.
*/
idlePollIntervalMs?: number;
/**
* Window (ms) consecutive `text` frames are gathered over and written as one row, so a streamed
* answer is a handful of inserts rather than one per token. Defaults to 50; `0` writes every frame.
*/
flushMs?: number;
/**
* TTL (seconds) of a run's rows, counted from its last write; `purgeExpired()` deletes what has
* lapsed. Defaults to 3600 (1h). Set `0` to retain until manual cleanup.
*/
ttlSeconds?: number;
/**
* Purge lapsed runs as a side effect of ending a run, at most once a minute per replica. Default
* `true`. Set `false` to purge only from your own schedule (`sink.purgeExpired()`).
*/
autoPurge?: boolean;
/**
* Create the frame table when the app starts. Default `true`. Set `false` to run the published
* migration (`createAgentTables`, which creates it too) instead.
*/
autoCreateTables?: boolean;
/**
* Bring-your-own Lucid database (or any {@link LucidDatabaseLike}). When set, `'lucid.db'` is NOT
* resolved from the container and `connection` is ignored. Handy for tests and scripts.
*/
db?: LucidDatabaseLike;
}
/**
* The token-sink factory namespace used in `config/agent.ts`, mirroring {@link stores}. Also exported as
* `streamTransports` (an alias — pick whichever name reads better):
*
* ```ts
* import { defineConfig, tokenSinks } from '@adonis-agora/agent'
*
* export default defineConfig({
* model: () => aiSdkModel(openai('gpt-4o-mini')),
* sink: tokenSinks.redis({ connection: 'main' }), // multi-replica; any pod serves any run's SSE
* })
* ```
*
* Omitting `sink` entirely keeps the in-process sink (single replica); `tokenSinks.memory()` selects it
* explicitly. `tokenSinks.redis()` fans a run's tokens across replicas over Redis pub/sub + a replayable
* list, keeping the SSE envelope byte-identical. The `@adonisjs/redis` peer is imported ONLY inside the
* `redis` thunk, so it stays fully optional. `tokenSinks.lucid()` is the multi-replica sink for a
* deployment WITHOUT Redis: frames are rows in the app's SQL database, read by polling.
*/
export declare const tokenSinks: {
/** The in-process sink — per-run in-memory buffers, single replica. The default when `sink` is omitted. */
memory(): TokenSinkFactory;
/**
* The Redis multi-replica sink: any pod can serve any run's SSE stream. Builds a
* {@link RedisStreamClient} over `@adonisjs/redis` (imported lazily here), or uses `config.client` when
* provided. Requires `@adonisjs/redis` installed + configured (`config/redis.ts`) unless a `client` is
* passed.
*/
redis(config?: RedisTokenSinkConfig): TokenSinkFactory;
/**
* The Lucid (SQL) multi-replica sink: any pod can serve any run's SSE stream, with no Redis — a
* run's frames are rows of `agent_stream_frame` on the app's database, which subscribers poll.
* Slower to deliver than Redis (up to `pollIntervalMs`) and it writes to your database on every
* streamed answer; pick {@link tokenSinks.redis} when Redis is already there. Requires
* `@adonisjs/lucid` installed + configured unless a `db` is passed.
*/
lucid(config?: LucidTokenSinkConfig): TokenSinkFactory;
};
/** Alias of {@link tokenSinks}. */
export declare const streamTransports: {
/** The in-process sink — per-run in-memory buffers, single replica. The default when `sink` is omitted. */
memory(): TokenSinkFactory;
/**
* The Redis multi-replica sink: any pod can serve any run's SSE stream. Builds a
* {@link RedisStreamClient} over `@adonisjs/redis` (imported lazily here), or uses `config.client` when
* provided. Requires `@adonisjs/redis` installed + configured (`config/redis.ts`) unless a `client` is
* passed.
*/
redis(config?: RedisTokenSinkConfig): TokenSinkFactory;
/**
* The Lucid (SQL) multi-replica sink: any pod can serve any run's SSE stream, with no Redis — a
* run's frames are rows of `agent_stream_frame` on the app's database, which subscribers poll.
* Slower to deliver than Redis (up to `pollIntervalMs`) and it writes to your database on every
* streamed answer; pick {@link tokenSinks.redis} when Redis is already there. Requires
* `@adonisjs/lucid` installed + configured unless a `db` is passed.
*/
lucid(config?: LucidTokenSinkConfig): TokenSinkFactory;
};
/**
* A configured {@link ActorDirectory}: a lazy thunk the agent provider calls at boot to build the
* read-side `actorRef → label` lookup governance/dashboard surfaces resolve display names through. Each
* factory imports its impl inside the thunk (like {@link stores}).
*/
export type ActorDirectoryFactory = (ctx: StoreContext) => ActorDirectory | Promise<ActorDirectory>;
/** Options for the in-memory actor directory. */
export interface MemoryActorDirectoryConfig {
/** Seed `actorRef → display label` mappings. */
labels?: Record<string, string>;
}
/**
* The actor-directory factory namespace used in `config/agent.ts`, mirroring {@link stores}:
*
* ```ts
* export default defineConfig({
* actorDirectory: actorDirectories.memory({ labels: { u_1: 'Ada Lovelace' } }),
* })
* ```
*
* Omit `actorDirectory` → governance surfaces render raw opaque refs. A real deployment binds its own
* directory over the host's user table by passing an instance; `actorDirectories.memory` covers tests,
* the offline demo, and small fixed label tables.
*/
export declare const actorDirectories: {
/** In-memory `actorRef → label` map — single-process. Handy for tests and dev apps. */
memory(config?: MemoryActorDirectoryConfig): ActorDirectoryFactory;
};
//# sourceMappingURL=factory.d.ts.map