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. Omit it and the endpoints are public — every browser is its own anonymous actor
  // (an HttpOnly cookie). This line requires login instead (reads ctx.auth.user; 401 without).
  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 nine tables (threads, messages, tool calls, token usage, model pricing, the run lifecycle, the chat message queue, spent confirm tokens, and the frame buffer the Lucid token sink uses). Apply the migration:

node ace migration:run

You can also skip this step: the Lucid store creates (and, across upgrades, repairs) its own tables as the app starts, which is why a fresh app works before you have run anything and an upgrade needs no migration of yours. 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 open unless restricted

A tool that declares no roles inherits defaultRoles — [], no restriction, by default: anyone the actor resolver resolved can call it (an anonymous visitor included). Give it a roles list to restrict it, or set defaultRoles: ['ADMIN'] to make that the default. action tools still wait for approval. A tool the actor 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":"..."}

id: 1
data: {"kind":"step-start"}

id: 2
data: {"kind":"text","text":"The "}

id: 3
data: {"kind":"text","text":"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

Partner

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

Become a partner →