UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

226 lines (225 loc) • 10.2 kB
import { terminalProjection } from "../core/runner/terminal-projection.js"; import { releaseSession } from "./session-util.js"; import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js"; import { buildCumulativeStats } from "./cumulative-stats.js"; import { createSafeNotifier } from "../core/safe-notify.js"; import { thrownText } from "../core/thrown-value.js"; const CASCADE_ON_RUNG_SITE = "cascade.onRung"; const CASCADE_CONFIG_ERROR_CODE = "config.cascade_invalid"; const CASCADE_NO_DISPATCH_ERROR_CODE = "cascade.budget_exhausted"; function cascadeConfigError(message) { const e = new Error(message); e.code = CASCADE_CONFIG_ERROR_CODE; return e; } function attemptTerminal(result) { const face = terminalProjection(result.terminal); return { status: face.status, errorCode: face.errorCode }; } function createDefaultGate(spec) { const requiresStructured = spec.outputSchema != null; return (result) => result.terminal.kind === "completed" && (!requiresStructured || result.structuredOutput !== undefined); } export async function runCascade(runner, spec, config, internals) { const ladder = config.ladder; if (!ladder || ladder.length === 0) { throw new Error("runCascade: config.ladder must have at least one rung"); } const maxEscalations = config.maxEscalations ?? ladder.length - 1; if (typeof maxEscalations !== "number" || !Number.isFinite(maxEscalations)) { throw cascadeConfigError(`runCascade: config.maxEscalations must be a finite number (got ${String(config.maxEscalations)}) — an unevaluable escalation ceiling is not a ceiling, and folding it to a default would run the cascade under a bound nobody chose.`); } const maxRungs = Math.min(ladder.length, Math.max(0, maxEscalations) + 1); const gate = config.gate ?? createDefaultGate(spec); const startedAt = Date.now(); const deadlineAt = config.totalTimeoutMs != null ? startedAt + config.totalTimeoutMs : undefined; const notifier = createSafeNotifier(config.onNotifyError !== undefined ? { onError: config.onNotifyError } : undefined); const attempts = []; let totalCost = 0; let ceilingCostKnown = true; let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownCached = 0, ownOutput = 0, ownCostMicro = 0; let ownTotalInput = 0; let ownCacheWrite = 0, ownCacheWriteLong = 0; let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0; let ownLlmRootMicro = 0, ownCompactionMicro = 0; let anyRungUnpriced = false; let hrCount = 0, hrWaitMs = 0; const hrGates = []; const cumulativeStats = (baseStats) => buildCumulativeStats(baseStats, { tokens: ownTokens, turns: ownTurns, promptTokens: ownPrompt, totalInputTokens: ownTotalInput, cachedTokens: ownCached, outputTokens: ownOutput, cacheWriteTokens: ownCacheWrite, cacheWriteTokensLong: ownCacheWriteLong, costMicroUsd: ownCostMicro, anyLegUnpriced: anyRungUnpriced, llmRootMicroUsd: ownLlmRootMicro, compactionMicroUsd: ownCompactionMicro, nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro }, humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates }, }); let lastResult; let passedRung = -1; let ladderPlacementRoot; const { sessionId: _drop, ...specBase } = spec; for (let i = 0; i < maxRungs; i++) { if (deadlineAt !== undefined && Date.now() >= deadlineAt) break; if (i > 0 && config.costCeilingMicroUsd != null && (totalCost >= config.costCeilingMicroUsd || !ceilingCostKnown)) { break; } const rung = ladder[i]; let deadlineFired = false; const rungAbort = new AbortController(); let deadlineTimer; if (deadlineAt !== undefined) { deadlineTimer = setTimeout(() => { deadlineFired = true; rungAbort.abort(new Error(`runCascade: totalTimeoutMs (${config.totalTimeoutMs}ms) reached`)); }, Math.max(0, deadlineAt - Date.now())); deadlineTimer.unref?.(); } const rungSignal = deadlineAt === undefined ? specBase.signal : specBase.signal ? AbortSignal.any([specBase.signal, rungAbort.signal]) : rungAbort.signal; let result; const rungInternals = ladderPlacementRoot === undefined || internals?.placementRoot !== undefined || internals?.rootSessionId !== undefined ? internals : { ...internals, placementRoot: ladderPlacementRoot }; try { result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}), ...(rungSignal ? { signal: rungSignal } : {}) }, rungInternals); } finally { if (deadlineTimer !== undefined) clearTimeout(deadlineTimer); } lastResult = result; ladderPlacementRoot ??= internals?.placementRoot ?? internals?.rootSessionId ?? result.sessionId; const s = result.stats; const rungCostKnown = s.costMicroUsd !== undefined && (s.nested === undefined || s.nested.costMicroUsd !== undefined); const rungCost = rungCostKnown ? (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0) : undefined; totalCost += rungCost ?? 0; if (!rungCostKnown) ceilingCostKnown = false; ownTokens += s.tokens; ownTurns += s.turns; ownPrompt += s.promptTokens ?? 0; ownTotalInput += s.totalInputTokens ?? 0; ownCached += s.cachedTokens ?? 0; ownOutput += s.outputTokens ?? 0; ownCacheWrite += s.cacheWriteTokens ?? 0; ownCacheWriteLong += s.cacheWriteTokensLong ?? 0; ownCostMicro += s.costMicroUsd ?? 0; if (s.costMicroUsd === undefined) anyRungUnpriced = true; if (s.costBreakdown) { ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd; ownCompactionMicro += s.costBreakdown.compactionMicroUsd; } if (s.humanReview) { hrCount += s.humanReview.count; hrWaitMs += s.humanReview.totalWaitMs; hrGates.push(...s.humanReview.gates); } if (s.nested) { nestedTokens += s.nested.tokens; nestedTurns += s.nested.turns; nestedTasks += s.nested.tasks; nestedCostMicro += s.nested.costMicroUsd ?? 0; } if (isDurablePause(result.terminal)) { const mapped = mapNestedSuspend(result); attempts.push({ index: i, model: rung.model, passed: false, costMicroUsd: rungCost, status: attemptTerminal(result).status, errorCode: attemptTerminal(mapped).errorCode }); return { ...mapped, stats: cumulativeStats(result.stats), cascadeOutcome: "exhausted", ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}), escalated: i > 0, finalRung: i, attempts, }; } if (deadlineFired || specBase.signal?.aborted === true) { attempts.push({ index: i, model: rung.model, passed: false, costMicroUsd: rungCost, ...attemptTerminal(result), ...(deadlineFired ? { timedOut: true } : { cancelled: true }), }); notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed: false, result }), CASCADE_ON_RUNG_SITE); await releaseSession(runner, result.sessionId); break; } let passed = false; let gateError; let diagnostics; try { try { const verdict = await gate(result, { index: i, model: rung.model }); if (typeof verdict === "boolean") passed = verdict; else { passed = verdict.pass; diagnostics = verdict.diagnostics; } } catch (e) { passed = false; gateError = thrownText(e); } const statusRejected = passed && config.acceptPartial !== true && result.terminal.kind !== "completed"; if (statusRejected) passed = false; attempts.push({ index: i, model: rung.model, passed, costMicroUsd: rungCost, ...attemptTerminal(result), gateError, diagnostics, ...(statusRejected ? { statusRejected: true } : {}), }); notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed, result }), CASCADE_ON_RUNG_SITE); } finally { await releaseSession(runner, result.sessionId); } if (passed) { passedRung = i; break; } } if (lastResult === undefined) { return { taskId: spec.taskId ?? "", sessionId: "", result: `runCascade: the overall wall-clock budget (totalTimeoutMs ${config.totalTimeoutMs}ms) was already spent when the ladder was entered — no rung dispatched`, terminal: { kind: "failed", code: CASCADE_NO_DISPATCH_ERROR_CODE }, stats: { tokens: 0, turns: 0, costMicroUsd: 0 }, cascadeOutcome: "exhausted", escalated: false, finalRung: -1, attempts, }; } const base = lastResult; const finalRung = attempts.length - 1; return { ...base, stats: cumulativeStats(base.stats), cascadeOutcome: passedRung >= 0 ? "passed" : "exhausted", ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}), escalated: finalRung > 0, finalRung, attempts, }; }