UNPKG

@adonis-agora/agent

Version:

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

114 lines • 5.31 kB
import { BaseWorkflow } from '@adonis-agora/durable'; import { PACKAGE_ORIGIN } from '../../package-origin.js'; import { errorText } from '../log.js'; import { addUsage, emptyUsage } from '../turn.js'; export const OPENCODE_RUN_WORKFLOW = 'agora.agent.opencode.run'; /** The token a person's answer to `toolCallId` is signalled under — the library's own convention. */ export function decisionToken(runId, toolCallId) { return `tool:${runId}:${toolCallId}`; } /** * A suspend / continue-as-new the runtime throws THROUGH the body — control flow, not a failure. * Recognised by name, so a second copy of `@adonis-agora/durable` (another class identity) still is. */ export function isControlFlowSignal(error) { const name = error?.name; return name === 'WorkflowSuspended' || name === 'ContinueAsNew'; } /** The runtime's `SignalTimeoutError`, recognised by name for the same reason. */ function isSignalTimeout(error) { return error instanceof Error && error.name === 'SignalTimeoutError'; } /** * An OpenCode turn as a durable workflow: * * begin → prompt → observe:0 → [ wait for a person (signal `tool:<run>:<call>`) → reply:n → observe:n+1 ]* → finish * * Every step is checkpointed. A turn parked on a person is a suspended run with a signal waiter: an * app restart loses nothing, and the decision resumes it on whichever process takes it — replaying * the steps from their checkpoints and replying to OpenCode from there (`OpenCodeTurns` rebuilds the * live turn, and re-prompts a fresh session when OpenCode itself restarted meanwhile). A process * that dies while OpenCode works re-runs that `observe` step, which catches up on what OpenCode * asked while nobody listened. * * Built by the engine's factory with the turn steps (see `registerOpenCodeWorkflow`), not by the * container. */ export class OpenCodeRunWorkflow extends BaseWorkflow { turns; engine; static workflow = { name: OPENCODE_RUN_WORKFLOW, version: '1', origin: PACKAGE_ORIGIN }; constructor(turns, engine) { super(); this.turns = turns; this.engine = engine; } async run(ctx, input) { const turns = this.turns; // `@adonis-agora/durable` >= 0.43.3 never runs one run twice at once in a process, so a step // that talks to OpenCode (a `reply`, an `observe`) runs once per execution of the run. const step = (name, body) => ctx.localStep(name, body); // What the run spent: every milestone carries its share, journaled with it, so a run resumed in // another process adds up the same figure (a milestone journaled before usage travelled adds 0). let spent = emptyUsage(); try { const begun = await step('begin', async () => ({ handle: await turns.begin(ctx.runId, input, true), startedAt: Date.now(), })); let handle = begun.handle; await step('prompt', async () => { await turns.prompt(ctx.runId, input, handle, spent); return true; }); for (let n = 0;; n += 1) { const before = spent; const milestone = await step(`observe:${n}`, () => turns.observe(ctx.runId, input, handle, before)); spent = addUsage(spent, milestone.usage); const total = spent; if (milestone.kind === 'finished') { await step('finish', async () => { // A cancel settles the run itself (it may be parked where no step runs again). if (await this.cancelled(ctx.runId)) turns.drop(ctx.runId); else { await turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total); } return true; }); return { outcome: milestone.outcome.status }; } let reply; try { reply = await ctx.waitForSignal(decisionToken(ctx.runId, milestone.ask.id), milestone.timeoutMs !== undefined ? { timeoutMs: milestone.timeoutMs } : undefined); } catch (error) { if (!isSignalTimeout(error)) throw error; reply = { approved: false, expired: true }; } const ask = milestone.ask; handle = await step(`reply:${n}`, () => turns.reply(ctx.runId, input, handle, ask, reply, total)); } } catch (error) { if (isControlFlowSignal(error)) throw error; await step('fail', async () => { await turns.settleFailed(ctx.runId, input, errorText(error, 'the turn failed'), 0, spent); return true; }); return { outcome: 'failed' }; } } async cancelled(runId) { try { const status = (await this.engine?.getRun(runId))?.status; return status === 'cancelling' || status === 'cancelled'; } catch { return false; } } } //# sourceMappingURL=workflow.js.map