UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

327 lines (256 loc) • 14.9 kB
# @adonis-agora/agent [![npm version](https://img.shields.io/npm/v/@adonis-agora/agent)](https://www.npmjs.com/package/@adonis-agora/agent) [![npm downloads](https://img.shields.io/npm/dm/@adonis-agora/agent)](https://www.npmjs.com/package/@adonis-agora/agent) [![CI](https://github.com/DavideCarvalho/adonis-agora-agent/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/DavideCarvalho/adonis-agora-agent/actions/workflows/ci.yml) [![license](https://img.shields.io/npm/l/@adonis-agora/agent)](https://www.npmjs.com/package/@adonis-agora/agent) Governed, durable-backed AI agent (chat + tool-calling + governance) for **AdonisJS** — part of the [Agora](https://github.com/DavideCarvalho) ecosystem. The package is two things layered on top of each other: - A **framework-agnostic agent loop** — `runAgentLoop(deps, input, hooks)` drives one model → tools → model turn against a set of SPIs (model / store / quota / roles / sink / runner / governance). The `hooks` seam (`step` / `awaitApproval` / `openSink` / `runAgent`) lets the same loop body run in-process or as a replay-safe durable workflow (via `@adonis-agora/durable`, optional peer). - An **AdonisJS integration shell** — a service provider, `defineConfig`, HTTP routes (`/agent/*`, `/agent/governance/*`, `/agent/attachments`), Lucid-backed stores, tool discovery from `app/agent_tools`, RAG (pgvector or Qdrant retrievers, plus PageIndex-style tree navigation for long documents), a governed read-only SQL tool, and a React chat hook. `read` tools auto-execute, `action` tools gate on human approval (HITL), and `agent` tools delegate to another named agent. ## Component registries and server rendering Tools support a `present` hook in both functional and decorated class forms. Register custom components per app, share React renderers between web screens and static exports, and generate paginated PNG/PDF tables and charts for your own delivery adapters. See the [component rendering guide](./docs/authoring/component-rendering.mdx) for complete examples, schemas, text fallbacks, and capture configuration. ### Generative UI sandbox in React Native `@adonis-agora/agent/react-native/genui` draws the sandbox component (`ui__sandbox`) in a hardened `react-native-webview` — the web iframe's document and CSP, the same checked `agent.send` bridge — and re-exports the React Native-safe genui provider and components (import them from here in an app, not from `/react/genui`). Install `react-native-webview` (an optional peer), then: ```tsx import { createNativeSandboxRenderer, GenuiProvider } from '@adonis-agora/agent/react-native/genui' const Sandbox = createNativeSandboxRenderer({ theme: { vars: tokens }, baseUrl: 'https://api.example.com/agent' }) <GenuiProvider registry={{ Sandbox }} onAction={chat.sendUiAction}>…</GenuiProvider> ``` See [Sandbox in React Native](./docs/genui-sandbox-react-native.mdx). ## Install ```sh pnpm add @adonis-agora/agent node ace configure @adonis-agora/agent ``` `configure` registers the provider, wires an Assembler `init` hook that generates the typed `app/agent_tools` barrel (falls back to a runtime scan if absent), publishes `config/agent.ts`, and publishes the migrations — the base agent tables, a pgvector RAG-chunk table, and the tree-navigation tables (delete whichever you don't use, e.g. the pgvector migration if you retrieve via Qdrant instead, or the trees migration if you don't navigate long documents; run `node ace migration:run` for the rest). Only `ai` (or a hand-rolled `ModelProvider`) and a model factory are strictly required. Everything else is an **optional peer**, imported lazily only when configured: | Peer | Needed for | |---|---| | `ai` (`^7.0.0`) | `aiSdkModel` — the Vercel AI SDK v7 adapter (`@adonis-agora/agent/ai-sdk`) | | `@adonisjs/lucid` (`^22.4.0`) | `stores.lucid()`, pricing, governance read-model, the data tool's `db` | | `@adonisjs/redis` (`^9.2.0`) | `tokenSinks.redis()` — multi-replica SSE token streaming | | `@adonis-agora/durable` (`^0.8.0`) | `durable: true` — replay-safe durable runs (`@adonis-agora/agent/durable`) | | `@adonis-agora/authz` (`^0.4.0`) | `AuthzActorResolver` / `AuthzToolAuthorizer` (`@adonis-agora/agent/authz`) | | `@adonis-agora/telescope` (`^0.4.0`) | the Telescope watcher extension (`@adonis-agora/agent/telescope`) | | `@qdrant/js-client-rest` (`^1.11.0`) | `retrievers.qdrant({...})` | | `node-sql-parser` (`^5.3.0`) | the governed `dataTool` (`@adonis-agora/agent/data`) | | `react` (`^18` / `^19`), `@ai-sdk/react` and `ai` | `AgentProvider` / `useAgentChat` (`@adonis-agora/agent/react`) | ## Configure Only `model` is required — pick a `store` by name (omit for the in-memory, single-process store): ```ts // 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')), // any Vercel AI SDK v7 `LanguageModel` store: 'lucid', stores: { lucid: stores.lucid(), memory: stores.memory() }, actorResolver: new AuthActorResolver(), // defaults to one that THROWS — an identity is never fabricated }) ``` Pricing (`costUsd` on each turn) and the `/agent/governance/*` read routes both default to mirroring `store` when it's `stores.lucid()` (same connection, table auto-created) — override with `pricingStore` / `governanceQueries`, or `false` to disable. `sink` defaults to the in-process token sink; pass `tokenSinks.redis({...})` so any pod can serve any run's SSE stream. ## Use ### Chat — the provider mounts the HTTP API for you There's no controller to write: once configured, the provider itself mounts `POST /agent/chat` (starts a run and SSE-pipes the token stream), `GET /agent/chat/:runId/stream` (re-attach), thread CRUD, approve/reject for HITL `action` tools, quota, and — when `governanceQueries` is set — the `/agent/governance/*` read routes. Every route resolves the actor itself (401 on failure); a `threadId` passed to `chat` must belong to the caller. Drive it from React with `@adonis-agora/agent/react` — the full headless layer (history, resume, a message queue, approvals, attachments, threads), with the session cookie and shield's CSRF token already wired: ```tsx // npm i @ai-sdk/react ai import { AgentProvider, useAgentChat } from '@adonis-agora/agent/react' export default function ChatPage() { return ( <AgentProvider> <Chat /> </AgentProvider> ) } function Chat() { const { transcript, composer, queue } = useAgentChat() return ( <form onSubmit={(e) => (e.preventDefault(), void composer.submit())}> {transcript.items.map((item) => ( <p key={item.id}>{item.role}: {item.text}</p> ))} {queue.items.map((m) => <p key={m.id}>(waiting) {m.text}</p>)} <input value={composer.text} onChange={(e) => composer.setText(e.target.value)} /> <button disabled={!composer.canSend}>Send</button> </form> ) } ``` The hooks ship inside Agora. Its protocol remains compatible with Aviary, but installation and releases are independent. Without React, use the framework-free stream client: ```ts import { createAgentChatClient } from '@adonis-agora/agent/client' const client = createAgentChatClient({ basePath: '/agent' }) const { parts } = await client.send({ body: { message: 'hi' }, onParts: (parts) => render(parts) }) ``` ### Write a governed tool Tools are auto-discovered from `app/agent_tools`. `ReadTool` auto-executes; `ActionTool` requires human approval before it runs: ```ts // app/agent_tools/allocation_queue.ts import { z } from 'zod' import { ReadTool } from '@adonis-agora/agent' import type { AiToolCtx } from '@adonis-agora/agent' export default class AllocationQueue extends ReadTool<{}, Row[]> { static tool = { name: 'allocation_queue', description: 'Lists pending allocation requests for the current tenant.', input: z.object({}), ability: 'agent.coordinator.queue.read', } async execute(_input: {}, ctx: AiToolCtx): Promise<Row[]> { return db.from('allocations').where('tenant_ref', ctx.actor.tenantRef) } } ``` An `ActionTool` writes somewhere, and that write and the checkpoint recording it are two writes: a worker that dies between them re-runs the tool on recovery. `ctx.idempotencyKey` (`<runId>:<toolCallId>`) is the same for every execution of one call — pass it on as the downstream idempotency key (a provider's `Idempotency-Key`, a unique column, the `id` of a workflow the tool starts) so the second attempt lands on the first. ### Governed read-only SQL `@adonis-agora/agent/data` ships `dataTool` — a fail-closed SQL tool: it requires an explicit, role-based table allowlist, rewrites in an optional per-row tenant scope (off the calling actor's `tenantRef`), injects a row-count `LIMIT`, and truncates oversized results before they hit the model's context. Register it as a static tool in `config/agent.ts` (`defineConfig({ tools: [dataTool({...})] })`) or export it from an `app/agent_tools/` module: ```ts import { dataTool } from '@adonis-agora/agent/data' import db from '@adonisjs/lucid/services/db' export const executeSql = dataTool({ db, tableAccess: { roleGroups: { ADMIN: ['billing'] }, tablesByGroup: { billing: ['orders', 'invoices'] }, }, tenant: { tenantColumn: 'tenant_ref', scopedTables: ['orders', 'invoices'] }, maxRows: 200, }) ``` ### Guardrails — PII, secrets, prompt injection, tool poisoning `@adonis-agora/agent/guardrails` puts detectors and a rule engine on the loop's processor seams, which `config/agent.ts` now takes as `inputProcessors` / `outputProcessors`: ```ts // config/agent.ts import { createGuardrails } from '@adonis-agora/agent/guardrails' const guardrails = createGuardrails({ pii: 'redact', // reversible: the model sees [EMAIL_1], the reader the address secrets: 'block', injection: { threshold: 0.6 }, // the prompt and every tool result; default action: block toolPoisoning: true, // guardrails.screenTool, for `mcpServers[].screen` rules: (ctx) => rulesForTenant(ctx.actor?.tenantRef), // more rules, resolved per scan onEvent: (event) => audit(event), // hits carry fingerprints, never values }) export default defineConfig({ model: () => aiSdkModel(openai('gpt-4o-mini')), inputProcessors: [guardrails.input], outputProcessors: [guardrails.output], mcpServers: [{ name: 'docs', transport: { type: 'http', url }, screen: (t) => guardrails.screenTool(t) }], }) ``` A blocked prompt fails the run (`ProcessorFailedError` caused by a `GuardrailBlockedError`); a blocked tool result is withheld and the turn goes on; a blocked answer or tool call is an `OutputRejectedError`. `guardrails.wrapTool(name, handler)` restores placeholders into a tool's arguments and runs the `tool_args` rules on them. The output processor declares `incremental`, so the answer keeps streaming. The detectors (`detectPii` with Luhn/CPF/CNPJ/IBAN checks, `detectSecrets`, `scoreInjection`, `scoreToolText`) and `scan` work on their own. The guardrail implementation ships locally in Agora; it does not require an Aviary core package. ### Framework-agnostic core (no AdonisJS) `runAgentLoop(deps, input, hooks)` is the shared turn body both the in-process (`InlineAgentRunner`) and the durable runner drive — `hooks` is what tells it which one it's in. Exercised directly (e.g. in a test) with the in-memory doubles from `@adonis-agora/agent/testing`: ```ts import { runAgentLoop, ToolRegistry, DefaultRolesPolicy } from '@adonis-agora/agent' import { FakeModelProvider, echoScript, InMemoryAgentStore, InMemoryTokenStreamSink, } from '@adonis-agora/agent/testing' const sink = new InMemoryTokenStreamSink() const { text } = await runAgentLoop( { model: new FakeModelProvider(echoScript('hi there')), store: new InMemoryAgentStore(), registry: new ToolRegistry(), rolesPolicy: new DefaultRolesPolicy(), day: '2026-07-21', systemPrompt: 'You are a helpful assistant.', }, { threadId: 'thread-1', actor: { id: 'user-1', roles: ['USER'] }, userText: 'hi' }, { runId: crypto.randomUUID(), openSink: () => sink.open('run-1'), awaitApproval: async () => 'approve', step: (_name, fn) => fn(), }, ) ``` ## Dashboard `node ace configure @adonis-agora/agent` also registers an embedded governance console — the `@adonis-agora/agent-dashboard` React SPA, bundled straight into `@adonis-agora/agent`'s own `dist/` at build time. **No separate install, no separate provider registration.** It mounts at `<path>/dashboard` (default `/agent/dashboard`) once `governanceAuthorize` is configured — every panel but Quota reads the cross-actor `/agent/governance/*` routes, and those don't exist without that gate either, so the console refuses to mount (with a boot warning explaining why) rather than load and 404 on every panel: ```ts // config/agent.ts export default defineConfig({ // ... governanceAuthorize: (actor) => actor.roles?.includes('ADMIN') ?? false, // dashboard: { enabled: true, path: '/agent/dashboard', authorize: (actor) => actor.roles?.includes('ADMIN') ?? false }, }) ``` See [Governance dashboard](./docs/governance/dashboard.mdx) for the full config shape. <details> <summary>Standalone install (still supported)</summary> `@adonis-agora/agent-dashboard` also ships as its own installable package with its own provider, for apps that want its independent release cadence or that configured it before the embedded provider existed: ```sh pnpm add @adonis-agora/agent-dashboard node ace add @adonis-agora/agent-dashboard ``` It reads the SAME `config('agent').dashboard` block. Register only ONE of the two providers — mounting both at the same path throws AdonisJS's "duplicate route" error at boot. </details> ## Diagnostics Runtime events flow through `@adonis-agora/agent/telescope`'s watcher when [`@adonis-agora/telescope`](https://github.com/DavideCarvalho/adonis-telescope) is installed, alongside the framework-agnostic `diagnostics` event types exported from the root entry point. ## Testing `@adonis-agora/agent/testing` ships `FakeModelProvider` and in-memory doubles for the store, sink, quota and governance SPIs, so the agent loop can be exercised deterministically without a DB, Redis, or a real model provider. ## Links - Repo: https://github.com/DavideCarvalho/adonis-agent - Changelog: https://github.com/DavideCarvalho/adonis-agent/blob/master/packages/adonis/CHANGELOG.md - Companion package (also embedded automatically, see [Dashboard](#dashboard) above): [`@adonis-agora/agent-dashboard`](https://github.com/DavideCarvalho/adonis-agent/tree/master/packages/dashboard) ## License MIT