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,
actiontools 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 → modeluntil 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, withstepmapped to a durable checkpoint andawaitApprovalmapped 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/agentThis 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:
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:
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
/agentroutes for chat, re-attach, cancel, threads, HITL approve/reject, and quota. - Tool-calling with governance —
@AiToolclasses,defineToolfunctions, orBaseTool/ReadTool/ActionToolbase classes, discovered fromapp/agent_tools(resolved through the IoC container, so a tool can@injectits 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/threadIdgets404/403, unless it is governance-privileged (IDOR closed). See Authorization. - Human-in-the-loop —
actiontools pause the run for an explicit approve/reject decision before they execute. - Multi-agent delegation — declare
delegatesToand an orchestrator gets synthesizedask_<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
useAgentChatReact 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
RetrieverSPI, 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.
Browser client & React
The shipped SSE client and the useAgentChat hook — 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.