UNPKG

@adonis-agora/agent

Version:

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

201 lines • 10.3 kB
import type { Actor } from '../types.js'; import type { InboundMessage } from './types.js'; /** * How a channel's inbound messages are processed: one {@link ChannelJob} per message, in order per * conversation, each made of named phases. * * - {@link inlineExecutor}: in this process, after the `200`. A restart loses what was in flight. * - {@link durableExecutor}: as `@adonis-agora/durable` runs — persisted before the `200`, one at a * time per conversation (a singleton key), each phase a checkpointed step (a dispatched one, when * the engine has somewhere to serve it), and resumed by the engine's recovery after a crash. */ /** What a channel processes: a message, the rest of a run after its question, a question timing out. */ export type ChannelJob = { kind: 'message'; channel: string; conversation: string; message: InboundMessage; /** When the webhook took it (epoch ms) — what a turn's queue wait is measured from. */ receivedAt?: number; /** * Recorded in the conversation's burst log when it was taken (`coalesce` on): the job first * looks for a burst to join, or to lead. */ coalesce?: boolean; } | { /** Read a run's stream (again) and deliver what was not delivered yet. */ kind: 'resume'; channel: string; conversation: string; runId: string; threadId: string | null; actor: Actor; } | { /** A question nobody answered in time: skip it, then resume the run. Runs outside the lock. */ kind: 'timeout'; channel: string; conversation: string; /** The run parked on the question (a delegate's, sometimes). */ runId: string; toolCallId: string; /** The run whose stream the person reads. */ streamRunId: string; threadId: string | null; actor: Actor; }; /** * The phases a job is made of. Each one is rebuilt from its (JSON) input — the job and what the * phases before it returned — so it can run in any process that registered the channel. */ export type ChannelPhase = 'gather' | 'prepare' | 'act' | 'read' | 'timeout'; /** What one phase is handed: the job, and what the phases before it came to (all JSON). */ export interface ChannelPhaseInput { job: ChannelJob; /** `gather`: which look at the burst this is (0, 1, …). */ round?: number; /** The messages the turn is made of, when it groups several (a burst). */ batch?: InboundMessage[]; /** What `prepare` came to. */ prepared?: unknown; /** What `act` came to. */ turn?: unknown; } /** Runs one phase: checkpointed (durable — in this process, or dispatched) or just called (inline). */ export type ChannelPhaseRunner = <T>(phase: ChannelPhase, input: ChannelPhaseInput) => Promise<T>; /** How a job runs its phases, and how it waits for a burst to end. */ export interface ChannelJobIo { phase: ChannelPhaseRunner; /** Wait until `at` (epoch ms): the durable engine's sleep, else a timer. */ sleepUntil(at: number): Promise<void>; } /** What a registered channel gives the executors: its job, and each of its phases on its own. */ export interface ChannelRunner { run(job: ChannelJob, io: ChannelJobIo): Promise<void>; phase(phase: ChannelPhase, input: ChannelPhaseInput): Promise<unknown>; /** How long a dispatched phase may go without an answer before the engine gives up on it. */ stepTimeoutMs(phase: ChannelPhase): number; /** `false` → this channel's phases always run as `localStep`s (`durableSteps: false`). */ dispatched?: boolean; } export interface ChannelExecutor { readonly durable: boolean; /** * Take a job: persisted (durable) or queued (inline) when this resolves — what the webhook waits * for before its `200`. Jobs of one conversation run one at a time, in the order they were taken. */ enqueue(job: ChannelJob, id: string): Promise<void>; /** Run a job at `at` (epoch ms), outside the conversation's order. */ later(job: ChannelJob, id: string, at: number): Promise<void>; /** Resolves once every job taken by this executor so far has finished. */ drain(): Promise<void>; } /** Ids a store / engine takes whatever the provider's message id looks like. */ export declare function jobId(...parts: string[]): string; /** Each phase called right here, a wait a timer: what the inline executor runs a job with. */ export declare function inlineIo(runner: Pick<ChannelRunner, 'phase'>): ChannelJobIo; /** In this process: a promise chain per conversation. Nothing survives a restart. */ export declare function inlineExecutor(run: (job: ChannelJob) => Promise<void>): ChannelExecutor; /** What a dispatched step handler is given besides its input — a durable `StepLogger`'s slice. */ export interface ChannelStepLog { /** Tell the engine the step is alive (rearms its timeout, where the transport carries beats). */ heartbeat?(progress?: unknown): void; } /** * Where dispatched steps are served BY NAME — a durable `Transport` with `handle` (every built-in * one has it: in-memory, BullMQ, database). A handler registered here runs in whatever process the * transport hands the step to. */ export interface ChannelStepServer { handle(name: string, fn: (input: unknown, log?: ChannelStepLog) => Promise<unknown>): void; } /** * The slice of an `@adonis-agora/durable` `WorkflowEngine` the channels use — the engine itself * satisfies it (`channels.handle(adapter, { durable: engine })`). */ export interface ChannelWorkflowEngine { register(name: string, version: string, fn: (ctx: ChannelWorkflowCtx, input: unknown) => Promise<unknown>, opts?: { singleton?: { key: (input: unknown) => string; limit?: number; }; /** Package attribution stamped on every run (`WorkflowRun.origin`). */ origin?: string; }): void; start(workflow: string, input: unknown, runId: string, opts?: { startAt?: number | Date; }): Promise<unknown>; waitForRun?(runId: string, opts?: { timeoutMs?: number; terminal?: boolean; }): Promise<unknown>; /** * Serve a dispatched step by name — for an engine that serves its own steps. `@adonis-agora/durable`'s * engine serves them on its transport instead: the channels find it in the app's container (or take * it as `channels.handle(…, { durableSteps: transport })`). Neither → the phases run in the process * that runs the workflow (`ctx.localStep`). */ handleStep?(name: string, fn: (input: unknown, log?: ChannelStepLog) => Promise<unknown>): void; } /** The slice of a durable `WorkflowCtx` a channel job uses. */ export interface ChannelWorkflowCtx { localStep<T>(name: string, fn: () => Promise<T>, options?: { retries?: number; backoff?: 'fixed' | 'exp'; backoffMs?: number; }): Promise<T>; /** * A dispatched step, routed to the handler served under `name` in any process. Absent → the * phases run as `localStep`s. */ step?<T>(name: string, input: unknown, options?: { retries?: number; timeoutMs?: number; pickupTimeoutMs?: number; }): Promise<T>; /** Suspend the run until `when` (durable). Absent → {@link sleep}, else a timer in this process. */ sleepUntil?(when: number | Date): Promise<void>; /** Suspend the run for `duration` ms (durable). */ sleep?(duration: number): Promise<void>; } /** The workflow a message (or a resume) runs as: one at a time per channel conversation. */ export declare const CHANNEL_JOB_WORKFLOW = "agora.channel.job"; /** A question's timeout: runs on its own, so it never waits behind the conversation. */ export declare const CHANNEL_TIMER_WORKFLOW = "agora.channel.timer"; /** The dispatched step a phase runs as: `agora.channel.prepare`, `agora.channel.read`, … */ export declare const channelPhaseStep: (phase: ChannelPhase) => string; /** A control-flow signal a workflow body surfaces (a suspend for a sleep, say) — not a failure. */ export declare const isControlFlowSignal: (error: unknown) => boolean; /** * Register the channel workflows on `engine`, once. Their body finds the job's channel with * `runnerFor` — the handler `channels.handle()` registered under that name IN THIS PROCESS. A * process that resumes channel jobs must have created the same handlers. * * - version `1`: every phase a `ctx.localStep`, in the process running the workflow — kept so runs * started before an upgrade finish on the path they were recorded on; * - version `2` (new runs): each phase a DISPATCHED step (`agora.channel.<phase>`) when `server` (or * `engine.handleStep`) serves them — any process that registered the channel runs it, any * process continues the workflow — else a `localStep`; and a burst (`coalesce`) waited for with * the engine's durable sleep. Which of the two a run uses is recorded in its first step. */ export declare function registerChannelWorkflows(engine: ChannelWorkflowEngine, runnerFor: (channel: string) => ChannelRunner | undefined, server?: ChannelStepServer): void; /** As durable runs on `engine` — see {@link registerChannelWorkflows}. */ export declare function durableExecutor(engine: ChannelWorkflowEngine): ChannelExecutor; export interface ChannelRetryOptions { /** Attempts per phase (the first included). Default 3. */ attempts?: number; /** Delay before the second attempt; doubles each time (with jitter). Default 1 s. */ backoffMs?: number; /** The longest delay between two attempts. Default 30 s. */ maxBackoffMs?: number; /** * Retry after this error? Default: not a refusal that retrying cannot change — a definite 4xx * from the provider (`ChannelDeliveryError.definite`), an error carrying a 4xx `status`, a * `QuotaBlockedError` — everything else yes. */ retryable?(error: unknown): boolean; } /** {@link ChannelRetryOptions.retryable}'s default. */ export declare function retryableByDefault(error: unknown): boolean; /** Run `fn`, again after a growing delay while it fails with a retryable error. */ export declare function withRetries<T>(fn: () => Promise<T>, options?: ChannelRetryOptions): Promise<T>; //# sourceMappingURL=executor.d.ts.map