Agora

Configuration reference

Every field of AgentConfig — the shape of config/agent.ts — with its type, default, and behavior.

config/agent.ts exports defineConfig(config) where config is an AgentConfig. Only model is required; every other field has a safe, fail-closed default. This page documents all of them.

Full example

config/agent.ts
import { defineConfig, stores, AuthActorResolver } from '@adonis-agora/agent'
import { aiSdkModel } from '@adonis-agora/agent/ai-sdk'
import { openai } from '@ai-sdk/openai'

export default defineConfig({
  model: () => aiSdkModel(openai('gpt-4o-mini')),

  store: 'lucid',
  stores: { memory: stores.memory(), lucid: stores.lucid() },

  actorResolver: new AuthActorResolver(),
  defaultRoles: ['ADMIN'],
  maxSteps: 8,
  path: 'agent',

  governanceAuthorize: (actor) => actor.roles?.includes('ADMIN') ?? false,
  dashboard: { authorize: (actor) => actor.roles?.includes('ADMIN') ?? false },

  defaultAgent: {
    systemPrompt: 'You are a helpful assistant.',
    defaultPersona: 'default',
    personas: [{ id: 'default', label: 'Default', systemPrompt: 'You are a helpful assistant.' }],
  },
})

Fields

model required

Required unless an engine runs the turns.

ModelProvider | () => ModelProvider | Promise<ModelProvider>. The LLM. Pass a ModelProvider directly, or (recommended) a lazy thunk so the provider SDK peer is imported only at boot. Wrap a Vercel AI SDK model with aiSdkModel(...), or implement the tiny ModelProvider SPI yourself.

store

string. The name of a key in stores to use. Omit for the in-memory store (single-process). See State stores.

stores

Record<string, StoreFactory>. Named store factories built with the stores helper — stores.lucid(...) and/or stores.memory(). Each factory is a lazy thunk, so a driver's peer (@adonisjs/lucid) is imported only when its store is selected.

sink

TokenStreamSink | SinkFactory. The live token transport (data plane). Defaults to the in-process sink (InProcessTokenStreamSink), which buffers per run so a client can re-attach, and drops a run's buffer one hour after its last write, ended or not (the replay window the Redis and Lucid sinks keep by default). Use tokenSinks.redis({...}) (aliased streamTransports.redis) for the multi-replica Redis sink so any pod serves any run's SSE stream, or tokenSinks.lucid({...}) for the same over the app's SQL database when there is no Redis (connection, tableName, pollIntervalMs, idlePollIntervalMs, flushMs, ttlSeconds, autoPurge, autoCreateTables). See Multi-replica streaming and Multi-replica streaming without Redis.

quota

{ limits: { day?: { tokens?, usd? }, month?: { tokens?, usd? } }, warnAt?: number, countEstimatedCost?: boolean } | QuotaProvider. The caller's budget. The usd windows count provider-reported cost and the loop's persisted estimate for a provider that reports none; countEstimatedCost: false counts reported cost only; warnAt (0..1) is the soft limit past which GET /agent/quota carries a warning — reported by GET /agent/quota and gating a send (429 quota_exceeded) once a window is exhausted. Omit → reported off the ledger, never enforced. See Budgets.

pricingStore

AgentPricingStore | PricingFactory | false. Prices each turn's tokens into the assistant message's usage.costUsd. A provider-reported (gateway) cost always wins — OpenRouter's providerMetadata.openrouter.usage.cost (the adapter requests usage accounting on every OpenRouter call) and the Vercel AI Gateway's providerMetadata.gateway.cost; otherwise the loop estimates from this store's current price rows, which boot seeds from models.dev for any configured model that has none (see priceCatalog). Omit → it mirrors the main store: with a stores.lucid() store, a Lucid pricing store on the same connection (agent_model_pricing, auto-created); with any other store, pricing is off. false turns it off. With no pricing store costUsd is always null (never a fabricated 0). Use pricingStores.lucid() or pricingStores.memory() to choose one. See Quota & cost.

priceCatalog

PriceCatalogOptions | false. Where boot fills a missing price row. At start, every model the model provider describes (aiSdkModel / aiSdkModels) that the pricing table does not price gets a row — only when it has none, so your prices are never overwritten — from, in order: prices (your own { model, input, output, cacheRead?, cacheWrite?, currency?, unit? } rate card), the built-in AWS GovCloud Bedrock table, then the models.dev list price (commercial: never used for a GovCloud / China Bedrock model). Bedrock ARNs and geo-prefixed profile ids are normalized to the profile and base id. region names the AWS region for a Bedrock id that does not (default AWS_REGION / AWS_DEFAULT_REGION). A model still unpriced afterwards whose provider reports no cost (anything but OpenRouter / the Vercel AI Gateway) is named in one boot warning with the reason and the fix, since it would record cost_usd = NULL. Omit → models.dev, except under NODE_ENV=test (no network from a test suite). { url?, fetch? } points at a mirror; modelsDev: false never fetches; false turns off every source but the built-in table (the warning still runs). See Prices from models.dev and Your own prices, and AWS Bedrock.

governanceQueries

AgentGovernanceQueries | GovernanceQueriesFactory | false. The read-model the optional /agent/governance/* routes serve from — cost/usage rollups, run lifecycle, tool stats, reliability, and the approvals inbox. Omitting it does not disable it: it defaults to mirroring a stores.lucid() store on the same connection. Pass a lazy governanceQueries.lucid() to point it elsewhere, or false to actually turn it off. Mounting the routes needs this and governanceAuthorize — a read-model with no gate mounts nothing. See Governance read-model.

governanceAuthorize

AgentGovernanceAuthorize — (actor, ctx) => boolean | Promise<boolean>. The authorization gate for the cross-actor /agent/governance/* read routes. It runs after the actor is resolved (the caller is authenticated) and decides whether this actor may read the platform-wide governance read-model — every actor's spend, usage, threads, and pending approvals. Return false (or throw — it's fail-closed) → the route replies 403. Omit → the routes are not mounted at all — every /agent/governance/* path answers 404, and the provider logs a boot warning. Typically you set it to an ADMIN check to mount them gated; set governanceAuthorize: () => true to deliberately restore the old behaviour of letting ANY authenticated actor read them. The dashboard console is a pure consumer of these routes, so it refuses to mount without the gate too. It does not gate the per-actor GET /agent/approvals/mine route, which is unaffected — it stays mounted whenever the read-model resolves — and the same predicate feeds the object-level ownership check (passing it makes a caller cross-actor privileged). Mirrors the dashboard's authorize hook. See Governance route authorization.

retriever / retrievalTopK / retrievalFilter

retriever: Retriever | RetrieverFactory. Enables always-on ("inject") RAG — before each turn the loop retrieves passages for the user message and folds them into the system prompt. Use retrievers.pgvector({...}), retrievers.qdrant({...}), or retrievers.memory({...}). Omit → no injection.

retrievalTopK: number — how many passages inject-mode requests per run (default 5).

retrievalFilter: (actor: Actor) => Record<string, unknown>. Derives the metadata filter for each run's retrieval from the acting identity — the control that keeps one tenant's passages out of another tenant's context window:

retrievalFilter: (actor) => ({ tenantRef: actor.tenantRef }),

Omit and inject-mode retrieval is UNSCOPED: every actor retrieves over the whole corpus. That is fine for a single-tenant corpus and a leak in any other. See RAG & retrieval.

attachments

AttachmentStagingStore | AttachmentStagingFactory. Where chat attachments live — attachmentStores.media() (on @adonis-agora/media), attachmentStores.memory() (tests), or your own store. When set, POST /agent/attachments accepts uploads within the store's limits (describe(); default 20 MiB and images/PDF/plain text/CSV) and a send names them as attachments: [{ mediaId }], resolved by the store. Omit → attachments are off. Replaces attachmentStaging / attachmentMaxBytes / attachmentAllowedContentTypes (0.46). See Attachments & multimodal. A store that implements list also serves GET /agent/attachments (the caller's staged files) and AgentService.collectableAttachments (the never-sent ones, for a sweep).

actorDirectory

ActorDirectory | ActorDirectoryFactory. Read-side lookup from opaque actorRefs to human display labels (the read dual of actorResolver). When bound, every governance route returning an actorRef also returns an actorLabel, resolved in one batched lookup per response. Use actorDirectories.memory({ labels }) or your own over the host's user table. Omit → surfaces render raw refs. See Actor labels.

toolTransientRetry

ToolTransientRetryOptions | false. Retries a tool's own invocation in place on a classified-transient error (DB deadlock, lock-wait timeout, serialization failure). Default ON ({ attempts: 2, backoffMs: 150 }); pass { classify } to change the rule, or false to disable. Non-transient failures are never retried. See Transient tool retries.

historyWindow

HistoryWindow. The ceiling on how much of a thread rides into a turn. Omit → the whole thread, every turn, which is unbounded: a long-lived thread eventually exceeds the model's context limit, and pays for the full transcript until it does.

new SlidingWindowHistory({ maxMessages, maxTokens }) keeps the newest messages that fit a count, a token budget, or both — whichever cuts more wins, and the newest message always rides. Adding summarize: summarizeWithModel(model) folds what the window left out into a summary appended to the turn's system prompt (worded by renderSummary, default renderHistorySummary), at the cost of one extra non-streamed model call per run, recorded as a summary usage row. Pass any HistoryWindow of your own for a window the built-in can't express; estimateMessageTokens and DEFAULT_HISTORY_SUMMARY_INSTRUCTION are exported for composing one. See Bounding the history.

messageTimestamps

{ timeZone?: string | ((actor) => string | undefined); format?: (sentAt: Date, ctx) => string }. Show the model when the person sent each message: every user message of the history a turn reads — old ones included — starts with a label, [sent 2026-10-23T11:17-03:00] the feeding started just now by default (ISO 8601 to the minute, in timeZone, default 'UTC'). format returns your own label ('' leaves a message unlabelled); ctx is { actor, threadId, timeZone, message: { role, content } }. Omit → messages reach the model exactly as stored, and the prompt is byte-for-byte what it was before the option existed.

The send time is the message's sentAt — a text channel's provider timestamp, the time a queued send was queued, or sentAt on a send (POST /agent/chat { sentAt }, AgentService.send({ sentAt })) — else its createdAt. Only the model's view changes: the stored content, GET /agent/threads/:id and the UI are untouched. The labels are added while the turn reads its thread, inside load:thread, so a durable replay reads back the same prompt, and a historyWindow selects over the labelled messages (a token budget counts the labels).

messageTimestamps: {
  timeZone: (actor) => actor.attributes?.timeZone ?? 'America/Sao_Paulo',
  format: (sentAt, { timeZone }) =>
    `[enviada ${DateTime.fromJSDate(sentAt).setZone(timeZone).toFormat('dd/MM HH:mm')}]`,
},

Tell the model what the label means in your system prompt ("the label is when the person sent the message; 'now' in a message means that time").

authorizer / rolesPolicy

RolesPolicy. The tool authorization gate; authorizer and rolesPolicy are aliases — pass either. Defaults to DefaultToolAuthorizer (role-set intersection against tool.roles ?? defaultRoles; open when that list is empty). Swap it to plug an ability-aware policy. See Authorization.

defaultRoles

string[]. The roles a tool requires when it declares none. Default [] — no restriction: anyone the actor resolver resolved can call a tool that declares no roles (action tools still wait for approval). ['ADMIN'] makes an ungoverned tool admin-only, the pre-0.46 default.

emptyRoles

'allow' | 'deny'. What an empty roles list means to the default authorizer. Default 'allow' — no restriction. 'deny' makes it reach nobody: a tool needs roles (or a non-empty defaultRoles) to be offered or invoked. Since 0.46 an empty list is open; set 'deny' if your app relied on it denying. Ignored when authorizer / rolesPolicy is set. See Authorization.

actorResolver

ActorResolver. Resolves the acting identity per request — the identity seam. Omit → the endpoints are public and every browser is its own anonymous actor (AnonymousActorResolver: an HttpOnly agent_anon cookie, the actor id anon: + a SHA-256 of it), announced at boot. new AuthActorResolver() requires login (reads ctx.auth.user, 401 without); HeaderActorResolver or your own also fit.

A public deployment should set a quota: model spend is otherwise unbounded (per browser).

path

string. The route prefix the /agent/* routes mount under. Default 'agent' (→ /agent/chat, /agent/threads, …). Leading/trailing slashes are stripped.

models

ModelCatalog | ModelCatalogView. Which models a caller may pick — served by GET /agent/models, checked on a send's model and on PATCH /agent/threads/:id { model }. A literal view for a fixed list, a ModelCatalog (list({ actor, agent })) to decide per caller. Omit → the catalog the model provider carries (aiSdkModels({ … })), else an empty one — and a request naming a model is refused with 400. See Models.

approvalPolicy

ApprovalPolicy | ApprovalRules. Who approves an action call and how long the request stays open. The shorthand { approver?, ttlMs?, tools?: { [name]: { required?, approver?, ttlMs? } } } covers most deployments; a whole ApprovalPolicy (requirementFor, optional canDecide) decides per actor, thread or agent. Omit → the person chatting approves, with no expiry. See Approval policy.

actionApprovalMode / backgroundActorResolver / actionProposalWorker

actionApprovalMode: 'blocking' | 'independent'. Default 'blocking' — an approval-requiring action call parks its turn. 'independent' stores it as a proposal, ends the turn, and executes it in a background worker once approved. Requires a store with the proposal capabilities (Lucid, or the in-memory store for one process) and a backgroundActorResolver; the boot fails otherwise.

backgroundActorResolver: { resolve({ actorRef, tenantRef }): Promise<Actor | null> }. Loads the requester's current identity and roles for a deferred execution. Required in independent mode.

actionProposalWorker: { pollIntervalMs?, leaseMs?, maxConcurrency?, onSettled? } — defaults 1000, 30000, 1; positive integers, with pollIntervalMs below a third of leaseMs. onSettled(proposal) is called by the worker that ran a proposal once it settles — to push the outcome to a channel that is not the web chat; a throw is logged, never retried. See Independent approvals.

actionProposalText

{ vocabulary?, replies? } — the words a chat message must be to decide a proposal (approve, reject, remember) and what the agent answers. Default English (yes/approve/no/reject, …). ptBrActionProposalText (exported) is Brazilian Portuguese. Each field you omit keeps its default. See Text decisions.

onPresentationError

(error, { toolName, toolCallId?, runId?, threadId? }) => void | Promise<void>. Where a tool's presentation failures go — a present hook that throws, a component push the catalog refuses — in turns and in proposal executions. The tool's result is unaffected. Default: a warning on the app logger.

genui

GenuiFactory — genui({ catalog, resolver?, mode?, treeSchema?, streaming?, showTool?, terminal?, sandbox?, … }) from @adonis-agora/agent/genui. mode defaults to 'tree' (one ui__render tool; 'per-component' registers a ui__show_* tool per component), streaming to 'complete' ('partial' draws a tree while the model writes it — Loading states and skeletons). Registers the tools a model uses to push catalog components, binds the catalog in the container (AgentGenui), and makes every component push validated and negotiated against the catalog. sandbox (true or defineSandbox options, off by default) adds the sandbox component: model-written HTML, CSS and JS for a one-off interactive answer, offered as a last resort and run in an isolated iframe. Omit → no catalog tools, and pushes are not validated (a turn carrying uiCapabilities then refuses every push). See Generative UI.

adapters

ProtocolAdapter[]. Other wire protocols served over the same runs, each mounting its own routes under path. agUiAdapter() from @adonis-agora/agent/ag-ui serves AG-UI at POST <path>/ag-ui (agUiAdapter({ a2ui: true }) adds A2UI surfaces to it). a2uiAdapter() from @adonis-agora/agent/a2ui serves A2UI v0.9 as JSON Lines at POST <path>/a2ui. Omit → only the native routes.

inputProcessors / outputProcessors

InputProcessor[] / OutputProcessor[]. Rewrite the prompt before every model call, and rule on each step's answer before anything downstream sees it. createGuardrails (@adonis-agora/agent/guardrails) provides both. See Input and output processors.

mcpServers

McpServerConfig[]. External MCP servers whose tools this deployment imports into its own registry (imported tools default to kind: 'action'). The other direction from config/mcp.ts. See MCP client.

skills / memory

skills: { provider: SkillProvider, scopes?, maxSkills? }. Authored procedures the model can load mid-turn: one catalog line per skill in the system prompt, and a skill tool that returns a body. Served by GET <path>/skills. Omit → no catalog and no skill tool.

memory: { provider: MemoryProvider, scopes?, maxMemories?, maxFactChars? }. Facts about an actor and their organisation carried across turns and threads, with a remember tool. GET <path>/memories lists what is on file about the caller and DELETE <path>/memories/:id forgets one. Omit → no memory block and no remember tool. Both default scopes to the actor's own, their tenant's and the deployment's.

durable

boolean. Run each turn as a replay-safe durable workflow (over @adonis-agora/durable) instead of in-process: LLM/tool steps become memoized checkpoints, HITL approval suspends on a signal (resuming across a restart), and delegation is a tracked child run. Requires @adonis-agora/durable installed and configured (config/durable.ts); if it can't be wired the provider logs a warning and falls back to the inline runner, so setting it is always safe. See The durable runner.

engine

AgentEngine | (() => AgentEngine | Promise<AgentEngine>). Run turns on something other than the library's loop — openCode({ host }) from @adonis-agora/agent/opencode. The routes, store, sink, approvals, questions and queue stay the library's; model becomes optional, and durable: true is refused at boot unless the engine is durable itself (openCodeDurable()). See OpenCode engine.

agents

AgentDefinition[]. Additional named agents an orchestrator can delegate to via delegatesTo — awaited (ask_<target>), or in the background with { agent, detached: true } (start_<target>). See Personas & agents.

defaultAgent

Omit<AgentDefinition, 'name'> & { name?: string }. The implicit single agent's config — its base systemPrompt, personas, defaultPersona, tool allow-list (tools), delegatesTo, modelId, maxSteps, and the question surfaces ask (offer the model the built-in ask tool) and intake (an authored question set asked before the work — see Asking the user). Omit for a bare assistant ('You are a helpful assistant.'). Its name defaults to 'default'.

tools

BrandedFunctionalTool[]. Static defineTool(...) functional tools to register at boot, in addition to app/agent_tools discovery. See Tools.

maxSteps

number. The cap on model↔tool iterations per turn. Default 8. A per-agent AgentDefinition.maxSteps overrides it for that agent.

dashboard

AgentDashboardConfig. The bundled governance console. Defaults to { enabled: true }, mounting the SPA at <path>/dashboard.

KeyTypeDefaultMeaning
enabledbooleantruefalse keeps the console's routes off entirely.
pathstring<path>/dashboardWhere the SPA mounts.
authorize(actor, ctx) => boolean | Promise<boolean>—An extra gate run after the actor resolves. Falsy or throwing → 403. Same shape as governanceAuthorize, so one predicate can gate both.
onUnauthenticated(ctx) => void | Promise<void>—Runs when the actor resolver itself rejects the caller. Set a Location header here (ctx.response.redirect('/login')) and the provider stands down so the redirect goes through, instead of writing its 401 JSON.

The console is not mounted without governanceAuthorize, and refuses to mount when governanceQueries: false — it is a browser client of routes that would not exist. Both refusals log a boot warning naming the responsible knob.

The console is not read-only: its Approvals section decides pending HITL calls and its Pricing section writes model rates. authorize is the switch that decides who can do that.

emitDiagnostics

boolean. Emit agora:agent:* lifecycle events when @adonis-agora/diagnostics is installed. Default true (a no-op when the diagnostics package is absent). See Diagnostics & tracing for the event names and payloads.

On this page

Partner

Your product as the official integration for Agent, right here.

Become a partner →