UNPKG

@adonis-agora/agent

Version:

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

95 lines • 4.07 kB
import { randomUUID } from 'node:crypto'; import { drivePendingRuns } from '../../durable/drive-pending-runs.js'; import { openCodeLog } from '../log.js'; import { decisionToken, isControlFlowSignal, OpenCodeRunWorkflow } from './workflow.js'; function isRunInput(value) { return (typeof value === 'object' && value !== null && typeof value.threadId === 'string' && typeof value.actor === 'object'); } const SETTLED = new Set(['completed', 'failed', 'cancelled', 'dead']); /** * Runs OpenCode turns as `@adonis-agora/durable` workflows ({@link OpenCodeRunWorkflow}): a person's * decision is a durable signal, so a turn waiting on one survives restarts and is resumed by * whichever process receives it. Needs a cross-process sink when more than one process serves the * agent (`tokenSinks.redis()` / `tokenSinks.lucid()`). */ export class DurableOpenCodeAgentRunner { engine; turns; host; store; constructor(engine, turns, host, store) { this.engine = engine; this.turns = turns; this.host = host; this.store = store; turns.startNext = (next, runId) => this.start(next, { runId }); } runIdFor(input) { return this.turns.settings.runId?.(input) ?? randomUUID(); } async start(input, options = {}) { const runId = options.runId ?? this.runIdFor(input); const durable = this.turns.settings.durable; const startOptions = { ...((await durable?.start?.(input, runId)) ?? {}), ...((await this.host.startOptions?.(input, runId)) ?? {}), }; // Before the engine starts it: a run the engine drives synchronously could otherwise end (and // clear it) before it was set. await this.store.setActiveStream(input.threadId, runId); try { await this.engine.start(OpenCodeRunWorkflow, input, runId, startOptions); } catch (error) { // A run that suspends on its first step under a driving dispatcher surfaces the runtime's // suspend signal here: the run is persisted and will be resumed, not failed. if (isControlFlowSignal(error)) return { runId }; throw this.host.startError?.(error, input) ?? durable?.startError?.(error, input) ?? error; } return { runId }; } /** * From the engine's run row. A run it does not know yet counts as running (a queued turn is * claimed before its start lands), and so does one it cannot be asked about. */ async isRunActive(runId) { try { const status = (await this.engine.getRun(runId))?.status; return status === undefined || !SETTLED.has(status); } catch { return true; } } async signal(runId, toolCallId, reply) { await this.engine.signal(decisionToken(runId, toolCallId), reply); } /** The runs {@link drive} set going that have not ended yet. */ driving = new Set(); /** Lease and execute here what of `runId` is still pending — see `drivePendingRuns`. */ async drive(runId) { await drivePendingRuns(this.engine, runId, this.driving); } /** * Interrupt OpenCode, cancel the run in the runtime (a run parked on a person never runs its body * again), and settle it here: the queue, the thread, the `cancelled` frame and the run row. */ async cancel(runId) { const run = await this.engine.getRun(runId).catch(() => null); if (run === null || run === undefined || SETTLED.has(run.status)) return; const input = run.input; if (isRunInput(input)) await this.turns.interrupt(input).catch(() => undefined); await this.engine.cancel(runId).catch((error) => { openCodeLog.warn(`could not cancel run ${runId}: ${error.message}`); }); if (isRunInput(input)) await this.turns.settle(runId, input, { status: 'interrupted' }, 0); } } //# sourceMappingURL=runner.js.map