Agora

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/agent

add installs the package and then runs the same codemods configure does:

  • registers @adonis-agora/agent/agent_provider and @adonis-agora/agent/dashboard_provider in adonisrc.ts;
  • registers the Assembler init hook that generates the typed app/agent_tools barrel;
  • publishes config/agent.ts and config/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 zod

2. 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.

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({
  // 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:run

You 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:

app/agent_tools/get_weather.ts
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:

app/agent_tools/purge_cache.ts
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

On this page