@sema-agent/core
Version:
Stateless, task-oriented AI agent core
226 lines (225 loc) • 10.2 kB
JavaScript
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,
};
}