UNPKG

@adonis-agora/agent

Version:

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

136 lines • 6.2 kB
import { resolveA2aAuth } from '../src/a2a/auth.js'; import { createA2aHandler } from '../src/a2a/handler.js'; import { registerRequestPermissionTool } from '../src/a2a/permission-tool.js'; import { setA2aHandler } from '../src/a2a/runtime.js'; import { ensureA2aTables, InMemoryA2aStore, LucidA2aStore, } from '../src/a2a/store.js'; import { AgentService } from '../src/agent-service.js'; import { ToolRegistry } from '../src/tool-registry.js'; /** * Exposes registered agents to personal agents over A2A 1.0 (HTTP+JSON) — the transport of PACT. * Reads `config/a2a.ts` and answers, for each configured agent: * * - `GET /{path}/{agent}/.well-known/agent-card.json` — the Agent Card (public). * - `POST /{path}/{agent}/message:send` — a synchronous turn (identity, and delegation when sent). * - `GET /{path}/{agent}/tasks`, `GET …/tasks/{id}`, `POST …/tasks/{id}:cancel` — as PACT §2.2 fixes. * * Mounted as SERVER middleware, ahead of the router: A2A requests never reach the bodyparser, * session or CSRF (see `src/a2a/server_middleware.ts`). Everything is resolved on the first A2A * request, so this provider can sit anywhere in `adonisrc.ts`. */ function normalizePath(path) { return (path ?? 'a2a').replace(/^\/+|\/+$/g, ''); } /** * The A2A config, or `undefined` when the surface is off. A `config/a2a.ts` whose default export * is `undefined` (feature-flagged off) reaches the config store as its module namespace * (`{ default: undefined }`), not as `undefined`. */ function a2aConfigOf(app) { const config = app.config.get('a2a', undefined); return config && typeof config === 'object' && config.agents && config.auth ? config : undefined; } export default class A2aProvider { app; constructor(app) { this.app = app; } async boot() { const config = a2aConfigOf(this.app); if (!config) return; const server = await this.app.container.make('server'); server.use([() => import('../src/a2a/server_middleware.js')]); // Only A2A traffic ever touches the A2A runtime: a broken auth driver or database must not // turn every route of the app into a 500. const prefix = `/${normalizePath(config.path)}/`; const isA2a = (url) => url.startsWith(prefix) || (config.rootCard !== undefined && url === '/.well-known/agent-card.json'); // Built on the first A2A request: the agent provider binds `AgentService` in its own boot, and // the auth driver may need a fully booted app. A failed build is retried on the next one. let built = null; setA2aHandler(async (ctx) => { if (!isA2a((ctx.request.url() ?? '').split('?')[0] ?? '')) return false; if (built === null) { built = this.#build(config); built.catch(() => { built = null; }); } return (await built)(ctx); }); } /** * Register `request_permission` in every process — a queue or durable worker that replays a turn * which called it needs the tool as much as the web process that started it. */ async ready() { const config = a2aConfigOf(this.app); if (!config) return; try { await this.#registerPermissionTool(config); } catch (error) { const logger = await this.app.container.make('logger'); logger.warn({ err: error }, 'a2a: could not register request_permission yet'); } } async shutdown() { setA2aHandler(null); } #permissionToolRegistered = false; async #registerPermissionTool(config) { // With its own runtime the app registers `request_permission` where ITS tools live // (`registerRequestPermissionTool`) — the shared registry may be another surface's. if (this.#permissionToolRegistered || config.service !== undefined) return; const auth = await resolveA2aAuth(config.auth, { app: this.app }); const scopes = await auth.delegableScopes(); if (Object.keys(scopes).length > 0) { registerRequestPermissionTool(await this.app.container.make(ToolRegistry), scopes); } this.#permissionToolRegistered = true; } async #build(config) { const registry = await this.app.container.make(ToolRegistry); const service = config.service === undefined ? await this.app.container.make(AgentService) : typeof config.service === 'function' ? await config.service({ app: this.app }) : config.service; const auth = await resolveA2aAuth(config.auth, { app: this.app }); await this.#registerPermissionTool(config); const brands = new Map(Object.entries(config.agents).map(([id, agent]) => [ id, { id, agentName: agent.agent ?? id, card: agent.card }, ])); return createA2aHandler({ path: normalizePath(config.path), ...(config.baseUrl !== undefined ? { baseUrl: config.baseUrl } : {}), brands, ...(config.rootCard !== undefined ? { rootCardBrand: config.rootCard } : {}), auth, store: await this.#store(config), service, registry, actions: config.actions ?? 'approve-delegated', timeoutMs: config.timeoutMs ?? 120_000, maxBodyBytes: config.maxBodyBytes ?? 1024 * 1024, ...(config.roles !== undefined ? { roles: config.roles } : {}), ...(config.actor !== undefined ? { actor: config.actor } : {}), }); } async #store(config) { const store = config.store ?? 'lucid'; if (typeof store === 'object') return store; if (store === 'memory') return new InMemoryA2aStore(); const db = (await this.app.container.make('lucid.db')); const connection = db.connection(config.connection); await ensureA2aTables(connection); return new LucidA2aStore(connection); } } //# sourceMappingURL=a2a_provider.js.map