@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
159 lines • 7.79 kB
JavaScript
import { AgentService } from '../src/agent-service.js';
import { globalPoppyAuthenticate } from '../src/poppy/auth.js';
import { PoppyConversations } from '../src/poppy/conversations.js';
import { announcePoppyEndpoints, poppyConversationsUrl } from '../src/poppy/endpoints.js';
import { adonisExchange, createPoppyHandler } from '../src/poppy/handler.js';
import { setPoppyHandler } from '../src/poppy/runtime.js';
import { InMemoryPoppyStore, LucidPoppyStore } from '../src/poppy/store.js';
import { ToolRegistry } from '../src/tool-registry.js';
/**
* Exposes a registered agent as the Company Agent of Personal Agent Protocol ("Poppy", Draft 0.1)
* conversations (§7). Reads `config/poppy.ts` and answers, under its `path`:
*
* - `POST {path}` — start a conversation (or a Direct Conversation) with its first message;
* - `POST {path}/{id}/messages` — another message;
* - `GET {path}/{id}/events` — read events (cursor, `wait`), or stream them as SSE;
* - `POST {path}/{id}/handoff`, `POST {path}/{id}/close`.
*
* Mounted as SERVER middleware, ahead of the router, like the A2A surface: requests are
* authenticated by their Session Token before the body is read, and never reach the bodyparser,
* session or CSRF. `PoppyConversations` is bound in the container — the app drives the Company
* side through it (a person joining, `user_requested`).
*
* @experimental Tracks Personal Agent Protocol Draft 0.1 (https://personalagentprotocol.org/docs/spec),
* a spec still in development: this API WILL change as the spec evolves — possibly in breaking
* ways, outside semver majors while it is a draft.
*/
function normalizePath(path) {
return (path ?? 'poppy/conversations').replace(/^\/+|\/+$/g, '');
}
/** The config, or `undefined` when the surface is off (a module whose default is `undefined`). */
function poppyConfigOf(app) {
const config = app.config.get('poppy', undefined);
if (!config || typeof config !== 'object')
return undefined;
// A feature-flagged-off `config/poppy.ts` reaches the store as its module namespace.
if ('default' in config && Object.keys(config).length === 1)
return undefined;
return config;
}
export default class PoppyProvider {
app;
#conversations = null;
constructor(app) {
this.app = app;
}
register() {
const config = poppyConfigOf(this.app);
if (!config)
return;
this.app.container.singleton(PoppyConversations, () => this.#conversationsFor(config));
}
async boot() {
const config = poppyConfigOf(this.app);
if (!config)
return;
const server = await this.app.container.make('server');
server.use([() => import('../src/poppy/server_middleware.js')]);
const path = normalizePath(config.path);
const prefix = `/${path}`;
// Tell an authorization server in this process (authkit) where conversations are, for
// `poppy.json`. Only with `baseUrl`: an absolute URL cannot be known before a request.
if (config.baseUrl !== undefined) {
announcePoppyEndpoints({ conversations: poppyConversationsUrl(config.baseUrl, path) });
}
const isPoppy = (url) => url === prefix || url.startsWith(`${prefix}/`);
// Built on the first Poppy request: the agent provider binds `AgentService` in its own boot.
// A failed build is retried on the next one; only Poppy traffic ever touches it.
let built = null;
setPoppyHandler(async (ctx) => {
if (!isPoppy((ctx.request.url() ?? '').split('?')[0]?.replace(/\/+$/, '') ?? '')) {
return false;
}
if (built === null) {
built = this.#build(config, path);
built.catch(() => {
built = null;
});
}
return (await built)(ctx);
});
}
/** Warn, once the app is up, when no one can verify a Session Token: every request is a 501. */
async ready() {
const config = poppyConfigOf(this.app);
if (!config || config.authenticate !== undefined || globalPoppyAuthenticate())
return;
const logger = await this.app.container.make('logger');
logger.warn('poppy: no Session Token resolver — install @adonis-agora/authkit-server with Poppy enabled, or set `authenticate` in config/poppy.ts. Until then the endpoint answers 501.');
}
async shutdown() {
setPoppyHandler(null);
if (this.#conversations !== null)
(await this.#conversations.catch(() => null))?.shutdown();
}
#conversationsFor(config) {
if (this.#conversations === null) {
this.#conversations = this.#createConversations(config);
this.#conversations.catch(() => {
this.#conversations = null;
});
}
return this.#conversations;
}
async #createConversations(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 logger = await this.app.container.make('logger');
return new PoppyConversations({
store: await this.#store(config),
service,
toolRoles: (name) => service.toolRoles?.(name) ?? registry.spec(name)?.roles,
...(config.agent !== undefined ? { agentName: config.agent } : {}),
actions: config.actions ?? 'approve-delegated',
timeoutMs: config.timeoutMs ?? 120_000,
drive: config.drive ?? true,
...(config.actorFor !== undefined ? { actorFor: config.actorFor } : {}),
...(config.handoff !== undefined ? { handoff: config.handoff } : {}),
...(config.direct !== undefined ? { direct: config.direct } : {}),
...(config.texts !== undefined ? { texts: config.texts } : {}),
...(config.formatAction !== undefined ? { formatAction: config.formatAction } : {}),
...(config.retentionMs !== undefined ? { retentionMs: config.retentionMs } : {}),
onError: (error, context) => logger.error({ err: error }, context),
});
}
async #build(config, path) {
const conversations = await this.app.container.make(PoppyConversations);
const logger = await this.app.container.make('logger');
const handle = createPoppyHandler({
path,
conversations,
...(config.authenticate !== undefined ? { authenticate: config.authenticate } : {}),
maxBodyBytes: config.maxBodyBytes ?? 1024 * 1024,
maxWaitSeconds: config.maxWaitSeconds ?? 30,
streamMaxMs: config.streamMaxMs ?? 300_000,
onError: (error) => logger.error({ err: error }, 'poppy: request failed'),
});
return (ctx) => handle(adonisExchange(ctx, config.baseUrl !== undefined ? { baseUrl: config.baseUrl } : {}));
}
async #store(config) {
const store = config.store ?? 'lucid';
if (typeof store === 'object')
return store;
if (store === 'memory')
return new InMemoryPoppyStore();
const db = (await this.app.container.make('lucid.db'));
const lucid = new LucidPoppyStore(db.connection(config.connection), {
...(config.autoCreateTables !== undefined
? { autoCreateTables: config.autoCreateTables }
: {}),
});
await lucid.ready();
return lucid;
}
}
//# sourceMappingURL=poppy_provider.js.map