UNPKG

@adonis-agora/agent

Version:

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

99 lines • 4.62 kB
import { ToolPreflightDeniedError } from './tool-registry.js'; /** * Transient tool-error classification + the retry loop that wraps a tool's own invocation. A * classified-transient error (a DB deadlock, a lock-wait timeout, a serialization failure) means the * server rolled the tool's work back — retrying THAT class is safe, unlike a tool's general business * failure, which stays a one-shot outcome (no durable step retries: a tool may not be idempotent). * * `runAgentLoop` wraps `registry.invoke(...)` with {@link invokeWithTransientRetry} INSIDE the * `tool:<call.id>` step body — so under durable replay the whole step is memoized on its successful * result and a retry never becomes a new checkpoint; history still shows exactly one step per tool * call and side effects run once. */ /** Narrow, structural check for a MySQL/Postgres/SQLite transient error shape — no casts. */ function hasTransientShape(error) { if (typeof error !== 'object' || error === null) { return false; } const code = 'code' in error ? error.code : undefined; const errno = 'errno' in error ? error.errno : undefined; const sqlState = 'sqlState' in error ? error.sqlState : undefined; if (code === 1213 || code === 1205 || errno === 1213 || errno === 1205) { return true; // MySQL ER_LOCK_DEADLOCK / ER_LOCK_WAIT_TIMEOUT numeric codes } if (code === 'ER_LOCK_DEADLOCK' || code === 'ER_LOCK_WAIT_TIMEOUT') { return true; // MySQL driver string codes } if (code === '40001' || code === '40P01' || sqlState === '40001' || sqlState === '40P01') { return true; // Postgres serialization_failure / deadlock_detected SQLSTATEs } if (code === 'SQLITE_BUSY') { return true; } const message = 'message' in error ? error.message : undefined; return (typeof message === 'string' && /deadlock|lock wait timeout|serialization failure/i.test(message)); } /** * Default transient-tool-error classifier: true for a recognized MySQL/Postgres/SQLite * lock-contention shape (by driver `code`/`errno`/`sqlState`, or a matching message), checked on the * error itself and one level of `cause` (drivers commonly wrap the original error). A plain `Error` * with none of these markers — any other business failure — is `false`. */ export function isTransientToolError(error) { if (error instanceof ToolPreflightDeniedError) return false; if (hasTransientShape(error)) { return true; } if (typeof error === 'object' && error !== null && 'cause' in error) { const cause = error.cause; if (cause !== undefined && cause !== error && hasTransientShape(cause)) { return true; } } return false; } /** Total attempts (initial try + retries) when `toolTransientRetry` doesn't set `attempts`. */ export const DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS = 2; /** Backoff base in ms — the wait between attempt N and N+1 is `backoffMs * N`. */ export const DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS = 150; function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } /** * Retries `fn` in place — never a new durable step/checkpoint, just repeated attempts inside * whichever step body already wraps this call. `setting: false` runs `fn` once, unwrapped (no * classify/backoff bookkeeping at all). Otherwise: try; on a thrown error, rethrow immediately if * it's a recognized control-flow signal, else if the (possibly custom) classifier calls it transient * AND attempts remain, wait `backoffMs * attemptNumber` and retry; otherwise rethrow the error as-is. */ export async function invokeWithTransientRetry(fn, setting, options) { if (setting === false) { return fn(); } const attempts = setting.attempts ?? DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS; const backoffMs = setting.backoffMs ?? DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS; const classify = setting.classify ?? isTransientToolError; let attempt = 1; for (;;) { try { return await fn(); } catch (error) { if (error instanceof ToolPreflightDeniedError || options?.isControlFlowError?.(error) === true) { throw error; } const attemptsRemain = attempt < attempts; if (!attemptsRemain || !classify(error)) { throw error; } options?.onRetry?.(attempt, error); await delay(backoffMs * attempt); attempt += 1; } } } //# sourceMappingURL=tool-retry.js.map