UNPKG

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