Agora

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.

@adonis-agora/agent adds an AI agent to your AdonisJS app: streaming chat, tool-calling, and multi-agent delegation — all behind a governance layer that is fail-closed by default. A tool a user's role can't reach is never even offered to the model; an action tool never runs without a human approving it; the caller's identity is never fabricated. You wire a model, declare a few tools, and get a governed /agent/chat endpoint that streams tokens over SSE.

Pre-1.0

Everything documented here is in the published package — this is a description of the code, not a wishlist. The surface is still moving, though: pin a version and expect refinements before 1.0.

The problem it solves

Bolting an LLM onto a real app is rarely "call the API and stream the reply". You need the model to do things — read a record, purge a cache, book a slot — and the moment it can act, you inherit a governance problem: which tools may this user reach, which actions need a human's sign-off, how do you meter token spend per tenant, and how do you keep a full transcript. @adonis-agora/agent collapses that into a few guarantees:

  • Governed by construction. Tool visibility is a role/persona intersection, action tools require human approval (HITL), quotas are checked before the model runs, and the acting identity comes from your auth — never invented.
  • The loop is one thing. A single provider-agnostic agent loop drives model → tools → approval → model until the turn settles. The same loop body runs in-process or as a durable workflow — one flag, no rewrite.
  • Everything is recorded. Threads, messages, tool calls, and token/cost usage persist to SQL through Lucid, so you have a durable transcript and a governance ledger from day one.

One loop, two runners

The heart of the library is runAgentLoop — a provider-agnostic turn that iterates model and tools. It never talks to HTTP, a database driver, or a specific LLM directly; those arrive as injected dependencies and a small set of hooks (openSink, awaitApproval, step, runAgent). That seam is what lets one loop body run under two runners:

  • Inline runner (the default) — runs the turn in-process. HITL approval resolves a pending promise; delegation runs a nested loop. Single-replica, zero infrastructure.
  • Durable runner (durable: true) — the same loop, with step mapped to a durable checkpoint and awaitApproval mapped to a durable signal, so a turn survives crashes, deploys, and month-long approval waits. See The durable runner.

durable: true runs each turn as a replay-safe @adonis-agora/durable workflow. It requires the durable peer installed and configured; if it can't be wired the provider logs a warning and falls back to the inline runner, so setting it is always safe.

Quickstart

Install and configure:

node ace add @adonis-agora/agent

This registers the provider in adonisrc.ts, publishes config/agent.ts and the Lucid migration, and registers an Assembler init hook that generates the typed app/agent_tools barrel.

Point config/agent.ts at a model, a store, and an actor resolver:

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: { lucid: stores.lucid(), memory: stores.memory() },
  actorResolver: new AuthActorResolver(),
})

Declare a tool under app/agent_tools — a read tool auto-executes:

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) {
    return { city: input.city, tempC: 21 }
  }
}

Chat. POST /agent/chat streams the reply back over SSE:

curl -N http://localhost:3333/agent/chat \
  -H 'content-type: application/json' \
  -d '{"message":"What is the weather in Lisbon?"}'

The response is an SSE stream: an event: meta frame with the run and thread ids, then one data: frame per AgentStreamEvent ({"kind":"text","text":"..."} per token, plus steps, tool calls, …), then event: done. See Streaming & HTTP.

What you get

  • Streaming chat over SSE — the core /agent routes for chat, re-attach, cancel, threads, HITL approve/reject, and quota.
  • Tool-calling with governance — @AiTool classes, defineTool functions, or BaseTool/ReadTool/ActionTool base classes, discovered from app/agent_tools (resolved through the IoC container, so a tool can @inject its dependencies), gated per actor by roles, abilities, feature flags and the persona's allow-list.
  • Object-level ownership — a caller may act only on the runs and threads it owns; a non-owner who guesses a runId/threadId gets 404/403, unless it is governance-privileged (IDOR closed). See Authorization.
  • Human-in-the-loop — action tools pause the run for an explicit approve/reject decision before they execute — or, in independent mode, become proposals decided from the web, an API or a text reply and executed by a background worker.
  • Generative UI on every surface — tools push typed components from a shared catalog; a React chat draws them, an AG-UI client receives them, and a text-only channel gets each component's text. See One output, many surfaces.
  • Multi-agent delegation — declare delegatesTo and an orchestrator gets synthesized ask_<agent> tools that hand work to other named agents.
  • Cost & quota accounting — a per-turn usage ledger, day/month token and USD budgets checked before a send, and a pricing table for cost estimates.
  • Durable persistence — the agent tables on Postgres / MySQL / SQLite via Lucid, plus an in-memory twin for tests.
  • A shipped React layer — <AgentProvider> + useAgentChat() from @adonis-agora/agent/react: history, resume, a message queue, approvals, attachments and threads, headless, with the session cookie and CSRF token already wired. A framework-free SSE client for everything else.
  • An MCP server — the same governed registry exposed to external assistants over Streamable HTTP, with OAuth or API-key auth.
  • Retrieval — memory, pgvector, and Qdrant backends behind one Retriever SPI, with tenant-scoped metadata filters.

Where to go next

Getting Started

Install, configure a model and store, run the migration, define a tool, and stream your first chat.

The agent loop

The turn state machine, the hooks seam, and inline vs durable runners.

Authoring tools

@AiTool classes, defineTool functions, discovery, kinds, schemas, and roles.

Personas & agents

Personas, named agents, and multi-agent delegation.

Authorization

The fail-closed tool authorizer, the double-check, and the actor resolver.

Quota & cost

Pre-model quota checks, the usage ledger, and cost accounting.

The durable runner

Run each turn as a replay-safe durable workflow — memoized steps, signal-based HITL, child delegation.

RAG & retrieval

Retriever/embedding/reranker SPIs, memory & pgvector stores, and always-on inject retrieval.

Governance read-model

Spend, run lifecycle, tool stats, reliability, and the approvals inbox over /agent/governance/*.

State store

The Lucid tables, the migration, and the in-memory store.

Streaming & HTTP

The routes, the SSE envelope, HITL, and re-attaching to a live run.

Generative UI

Push typed components from a catalog — ctx.emitUi, present, and the genui tools.

One output, many surfaces

UI capabilities, text fallbacks, and what React, AG-UI, text channels and MCP receive.

Independent approvals

Actions that wait for a decision while the conversation goes on.

React

AgentProvider + useAgentChat — a Hello chat for Adonis + Inertia, the message queue, approvals, attachments.

Browser client

The framework-free SSE client — streaming, resume, and component frames.

MCP server

Expose the same governed tool registry to Claude, Cursor, and any other MCP client.

Testing

The FakeModelProvider, echoScript, and in-memory doubles.

On this page

Partner

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

Become a partner →