UNPKG

@adonis-agora/agent

Version:

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

186 lines • 8.32 kB
import { onActionProposalAdmitted } from '../src/action-proposal-listeners.js'; import { AgentService } from '../src/agent-service.js'; import { createPoppyConnectionsHandler, } from '../src/poppy-connections/http.js'; import { continueAfterConnect, resumeAfterConnect } from '../src/poppy-connections/resume.js'; import { setPoppyConnectionsHandler } from '../src/poppy-connections/runtime.js'; import { PoppyConnections } from '../src/poppy-connections/service.js'; import { InMemoryPoppyConnectionStore, LucidPoppyConnectionStore, } from '../src/poppy-connections/store.js'; import { registerPoppyConnectionTools } from '../src/poppy-connections/tools.js'; import { ToolRegistry } from '../src/tool-registry.js'; function configOf(app) { const config = app.config.get('poppy_connections', undefined); if (!config || typeof config !== 'object') return undefined; if ('default' in config && Object.keys(config).length === 1) return undefined; return config; } /** * This app's agent working with OTHER Poppy Companies for its users (`config/poppy_connections.ts`): * the agent identity routes, the connect pages and callback, the `poppy_*` tools, and — when the * config has `api` — `GET/DELETE {path}/api/connections` for the app's own settings UI. * * @experimental Tracks Personal Agent Protocol Draft 0.1 — will change as the spec evolves. */ export default class PoppyConnectionsProvider { app; #service = null; #unsubscribe = null; constructor(app) { this.app = app; } register() { const config = configOf(this.app); if (!config) return; this.app.container.singleton(PoppyConnections, () => this.#serviceFor(config)); } async boot() { const config = configOf(this.app); if (!config) return; const connections = await this.app.container.make(PoppyConnections); // The agent provider's registry when there is one; an app with its own loop registers the tools // in its own registry (`tools.register: false` + `registerPoppyConnectionTools`). if (config.tools?.register !== false && this.app.container.hasBinding(ToolRegistry)) { const registry = await this.app.container.make(ToolRegistry); registerPoppyConnectionTools(registry, connections, config.tools ?? {}); } const logger = await this.app.container.make('logger'); // Independent approvals: once the decided `connect` ran and its outcome is in the thread, go on // with the person's request (a new turn), on the channel it came from. if (config.tools?.connect !== 'link') { this.#unsubscribe = onActionProposalAdmitted(async (outcome) => { if (!this.app.container.hasBinding(AgentService)) return; const service = await this.app.container.make(AgentService); if (service.actionApprovalMode() !== 'independent') return; const { channels } = await import('../src/channels/handler.js'); await continueAfterConnect(service, connections, outcome, { ...(config.tools?.prefix !== undefined ? { prefix: config.tools.prefix } : {}), follow: channels.follow, }).catch((error) => logger.error({ err: error }, 'poppy-connections: continuing after connect failed')); }); } const handle = createPoppyConnectionsHandler(connections, { ...(config.render !== undefined ? { render: config.render } : {}), onError: (error) => logger.error({ err: error }, 'poppy-connections: request failed'), }); const server = await this.app.container.make('server'); server.use([() => import('../src/poppy-connections/server_middleware.js')]); setPoppyConnectionsHandler((ctx) => handle(exchange(ctx))); const api = config.api; if (api) { const router = await this.app.container.make('router'); const base = `/${connections.path}/api/connections`; const who = async (ctx) => { const actor = await api.actor(ctx); if (!actor) ctx.response.unauthorized({ error: 'unauthorized' }); return actor; }; router .get(base, async (ctx) => { const actor = await who(ctx); if (!actor) return; return { connections: await connections.list(connections.ownerOf(actor)) }; }) .as('poppy_connections.list'); router .delete(`${base}/:domain`, async (ctx) => { const actor = await who(ctx); if (!actor) return; const removed = await connections.disconnect(connections.ownerOf(actor), String(ctx.params.domain)); return { disconnected: removed }; }) .as('poppy_connections.disconnect'); } } async shutdown() { this.#unsubscribe?.(); this.#unsubscribe = null; setPoppyConnectionsHandler(null); } #serviceFor(config) { if (this.#service === null) { this.#service = this.#create(config); this.#service.catch(() => { this.#service = null; }); } return this.#service; } async #create(config) { const logger = await this.app.container.make('logger'); const appKey = this.app.config.get('app.appKey'); const secret = config.secret ?? (typeof appKey?.release === 'function' ? appKey.release() : String(appKey ?? '')); return new PoppyConnections({ config, store: await this.#store(config), secret, production: this.app.inProduction, onConnected: (pending) => this.#resume(pending), log: (line) => logger.warn(line), }); } async #store(config) { const store = config.store ?? 'lucid'; if (typeof store === 'object') return store; if (store === 'memory') return new InMemoryPoppyConnectionStore(); const db = (await this.app.container.make('lucid.db')); return new LucidPoppyConnectionStore(db.connection(config.connection), { ...(config.autoCreateTables !== undefined ? { autoCreateTables: config.autoCreateTables } : {}), }); } async #resume(pending) { // No AgentService (an app running its own loop): nothing parked to resume. if (!this.app.container.hasBinding(AgentService)) return; const { channels } = await import('../src/channels/handler.js'); // A sign-in that finished after its card expired still continues the conversation. await resumeAfterConnect(await this.app.container.make(AgentService), pending, { connections: await this.app.container.make(PoppyConnections), follow: channels.follow, }); } } function exchange(ctx) { const original = ctx.request.url(true) ?? ctx.request.url() ?? '/'; const target = new URL(original, 'http://local/'); return { method: ctx.request.method(), path: target.pathname, query: target.searchParams, async readBody(limit) { const raw = ctx.request.raw?.(); if (typeof raw === 'string') return raw; const chunks = []; let size = 0; for await (const chunk of ctx.request.request) { size += chunk.length; if (size > limit) throw new Error('body too large'); chunks.push(chunk); } return Buffer.concat(chunks).toString('utf8'); }, send(status, headers, body) { ctx.response.status(status); for (const [name, value] of Object.entries(headers)) ctx.response.header(name, value); ctx.response.send(body); }, }; } //# sourceMappingURL=poppy_connections_provider.js.map