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 data: {"delta":"..."} frames per token, 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 by a fail-closed role/persona policy.
  • 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-loopaction tools pause the run for an explicit approve/reject decision before they execute.
  • 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, a daily token quota checked before the model runs, 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 browser client — a framework-free SSE client that resumes across a dropped connection, and a useAgentChat React hook over it.
  • 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

On this page