Getting Started
Run your first governed agent in an AdonisJS app — install, configure a model and store, run the migration, define a tool, and stream a chat over SSE.
This guide gets a governed, streaming agent running in an AdonisJS app in a few minutes. You'll wire a model, persist to SQL with Lucid, declare one tool, and stream a reply from POST /agent/chat.
1. Install & configure
node ace add @adonis-agora/agentadd installs the package and then runs the same codemods configure does:
- registers
@adonis-agora/agent/agent_providerand@adonis-agora/agent/dashboard_providerinadonisrc.ts; - registers the Assembler
inithook that generates the typedapp/agent_toolsbarrel; - publishes
config/agent.tsandconfig/mcp.ts; - publishes two migrations: the agent tables, and the pgvector RAG chunk table.
The MCP provider is not registered — it is opt-in, and config/mcp.ts carries the one-line snippet for adonisrc.ts when you want it. The pgvector migration is Postgres-only; delete it if you are not using pgvector RAG.
You'll also need an LLM provider SDK for the AI SDK adapter — for example @ai-sdk/openai:
npm i ai @ai-sdk/openai zod2. Configure a model, store, and identity
Only model is required. Below we also select the lucid store (SQL persistence) and wire AuthActorResolver, which reads your authenticated principal off ctx.auth.user.
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({
// A lazy thunk keeps the provider SDK peer imported only at boot.
model: () => aiSdkModel(openai('gpt-4o-mini')),
store: 'lucid',
stores: {
memory: stores.memory(),
lucid: stores.lucid(),
},
// Identity seam — fail-closed. Without a resolver, every request 401s.
actorResolver: new AuthActorResolver(),
// The implicit single agent. Omit for a bare assistant.
defaultAgent: {
systemPrompt: 'You are a helpful assistant for our app.',
},
})No identity is ever fabricated
The actor resolver defaults to one that throws on every request. The agent will not invent a caller. In development you can use HeaderActorResolver (trusts x-actor-id / x-actor-role headers) behind a trusted gateway, but production should read a verified principal. See Authorization.
aiSdkModel adapts any Vercel AI SDK v7 LanguageModel to the agent's ModelProvider — pass openai(...), anthropic(...), a gateway model, etc. Prefer a different LLM library? Implement the tiny ModelProvider SPI (runTurn) yourself.
3. Run the migration
The lucid store persists to six tables (threads, messages, tool calls, token usage, model pricing, and the run lifecycle). Apply the migration:
node ace migration:runYou can also skip this step: the Lucid store creates its tables on first use by default, which is why a fresh app works before you have run anything. See Auto-created tables for when to turn that off.
Omitting store entirely uses the in-memory store — single-process, no migration — which is perfect for a first spin or for tests. See State stores.
4. Define a tool
Tools live under app/agent_tools and are discovered at boot. A read tool auto-executes; an action tool requires human approval. Declare an @AiTool class implementing execute:
import { AiTool } from '@adonis-agora/agent'
import type { AiToolCtx, ToolHandler } from '@adonis-agora/agent'
import { z } from 'zod'
@AiTool({
name: 'getWeather',
kind: 'read',
description: 'Current weather for a city.',
input: z.object({ city: z.string() }),
roles: ['MEMBER'],
})
export default class GetWeatherTool implements ToolHandler<{ city: string }> {
async execute(input: { city: string }, ctx: AiToolCtx) {
// ctx.actor, ctx.threadId, ctx.runId are all available here.
return { city: input.city, tempC: 21 }
}
}Tools are fail-closed
A tool that declares no roles inherits defaultRoles — ['ADMIN'] by default. Give a tool an explicit roles list (like ['MEMBER'] above) so non-admin actors can reach it. A tool the actor's role can't invoke is never even shown to the model. See Authorization.
Prefer a function to a class? Use defineTool:
import { defineTool } from '@adonis-agora/agent'
import { z } from 'zod'
export const purgeCache = defineTool(
{
name: 'purgeCache',
kind: 'action', // requires HITL approval before it runs
description: 'Purge a cache key.',
input: z.object({ key: z.string() }),
roles: ['ADMIN'],
},
async ({ key }, ctx) => {
// ... perform the purge
return { purged: key }
},
)5. Chat
The provider mounts the /agent routes at boot. Start a run:
curl -N http://localhost:3333/agent/chat \
-H 'content-type: application/json' \
-d '{"message":"What is the weather in Lisbon?"}'The reply streams back as Server-Sent Events:
event: meta
data: {"runId":"...","threadId":"..."}
data: {"delta":"The "}
data: {"delta":"weather "}
...
event: done
data: {}The X-Agent-Run-Id and X-Agent-Thread-Id response headers carry the same ids, so a client can re-attach to a run's stream after a dropped connection. That's it — a governed agent that streams tokens, runs a role-gated tool, and records the whole exchange.
Next steps
- The agent loop — the turn state machine and the runner seam.
- Authoring tools — kinds, schemas, discovery, and roles.
- Personas & agents — personas and multi-agent delegation.
- Streaming & HTTP — every route, the SSE envelope, and HITL.
- Browser client & React — the shipped SSE client and the
useAgentChathook. - Configuration reference — every
AgentConfigfield.
Agent
A governed, durable-ready AI agent for AdonisJS — streaming chat, tool-calling, fail-closed governance, human-in-the-loop approvals, and multi-agent delegation. One agent loop, two runners.
The agent loop
The provider-agnostic agent turn — the model→tools→approval→model state machine, the hooks seam that makes it replay-safe, and the inline vs durable runners that drive it.