@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
100 lines • 4.29 kB
JavaScript
import { createHash } from 'node:crypto';
import { InMemoryOpenCodeSessionStore, } from './host.js';
import { openCodeLog } from './log.js';
import { OpenCodeMcpEndpoint, OpenCodeToolsTokens } from './mcp.js';
import { OpenCodeAgentRunner } from './runner.js';
import { OpenCodeTurns } from './turns.js';
const DEFAULT_TOOLS_TTL_MS = 7 * 24 * 60 * 60_000;
async function resolve(value, context, what) {
if (typeof value !== 'function')
return value;
if (context.make === undefined) {
throw new Error(`[@adonis-agora/agent/opencode] the ${what} is a class, and there is no container to build it with: pass an instance.`);
}
return context.make(value);
}
/**
* The OpenCode engine — `engine: openCode({ host })` in `config/agent.ts`. The routes, threads,
* stream protocol, approvals, questions and queue stay the library's; OpenCode runs the model, the
* tools, skills and the context, on the server and sessions the host provides.
*/
export class OpenCodeEngine {
options;
name = 'opencode';
durable = false;
#turns;
#endpoint;
constructor(options) {
this.options = options;
}
/**
* The engine's turn steps, once the provider built the runner: `pushToSession` (trusted pushes
* into a session's run) and `liveRuns()` for the host's own use.
*/
get turns() {
if (this.#turns === undefined) {
throw new Error('[@adonis-agora/agent/opencode] the engine has not been started yet.');
}
return this.#turns;
}
/** The tools endpoint, when `tools` is set and the engine has started. */
get endpoint() {
return this.#endpoint;
}
async createRunner(context) {
const turns = await this.buildTurns(context);
return new OpenCodeAgentRunner(turns, context.store);
}
adapters() {
return this.#endpoint !== undefined ? [this.#endpoint.adapter()] : [];
}
shutdown() {
this.#turns?.close();
}
/** The host, the session store, the tools endpoint and the turn steps every OpenCode runner needs. */
async buildTurns(context) {
const { host: hostOption, sessions: sessionsOption, ...settings } = this.options;
const host = await resolve(hostOption, context, 'host');
const sessions = sessionsOption === undefined
? new InMemoryOpenCodeSessionStore()
: await resolve(sessionsOption, context, 'session store');
let tokens;
if (settings.tools !== undefined) {
const secret = settings.tools.secret ??
(context.appKey !== undefined
? createHash('sha256').update(`opencode-tools:${context.appKey}`).digest('hex')
: undefined);
if (secret === undefined) {
openCodeLog.warn('`tools` has no `secret` and the app has no APP_KEY: the endpoint signs its tokens with a per-process secret, so it only works with one process.');
}
tokens = new OpenCodeToolsTokens(secret, settings.tools.ttlMs ?? DEFAULT_TOOLS_TTL_MS);
}
const turns = new OpenCodeTurns({
host,
sessions,
settings,
factory: context.factory,
store: context.store,
sink: context.sink,
registry: context.registry,
queue: context.queue,
...(context.pricingStore !== undefined ? { pricingStore: context.pricingStore } : {}),
...(context.priceCatalog !== undefined ? { priceCatalog: context.priceCatalog } : {}),
...(tokens !== undefined
? { mintToolsToken: (actor, serverKey) => tokens.mint(actor, serverKey) }
: {}),
});
this.#turns = turns;
this.#endpoint = tokens !== undefined ? new OpenCodeMcpEndpoint(turns, tokens) : undefined;
return turns;
}
}
/**
* Run the agent's turns on OpenCode 2 — `engine: openCode({ host })` in `config/agent.ts`. In
* memory: a turn parked on a person lives in this process (single replica); see
* `openCodeDurable()` (`@adonis-agora/agent/opencode/durable`) for turns that survive restarts.
*/
export function openCode(options) {
return new OpenCodeEngine(options);
}
//# sourceMappingURL=engine.js.map