UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

276 lines (275 loc) • 13.9 kB
import { isDurablePause, pauseFamily } from "./suspend-guard.js"; import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js"; import { assertOracleIsolation } from "../core/oracle-isolation.js"; import { buildCumulativeStats } from "./cumulative-stats.js"; export function isolationPermitsAutoAccept(verdict) { return verdict.protected && verdict.isolationClass === "out_of_process"; } export function repairBundleFromCheckpoint(cp) { return cp.state.repairBundle; } export function terminalForTier(oracle) { if (oracle.tier === "none") return "needs_human_oracle"; if (oracle.passed && !oracle.flaky) { return "candidate_only"; } return undefined; } function repairObjective(bundle) { const diag = bundle.diagnostics.length ? bundle.diagnostics.map((d) => `- ${sanitizeUntrustedText(d)}`).join("\n") : "(no prior diagnoses)"; return (`The oracle REJECTED the previous attempt. Diagnose and fix the underlying cause, then stop — do not ` + `add anything else, and do NOT edit or weaken the oracle/tests to make them pass. If the spec and the ` + `oracle genuinely conflict, say so explicitly rather than satisfying the test.\n\n` + `## Prior diagnoses\n${diag}\n\n` + `## Oracle failure trace (untrusted)\n${delimitUntrusted("ORACLE TRACE", bundle.failureTrace || "(no trace)")}`); } function freshBundle() { return { failureTrace: "", diagnostics: [], rejectedHypotheses: [], attemptCount: 0, oracleTier: "none" }; } function outcomeFor(terminal, oracle, rounds) { const passed = terminal === "candidate_only" || terminal === "fixed"; return { verdict: passed ? "PASS" : terminal === "needs_human_oracle" ? "unverified" : "FAIL", ...(passed ? {} : terminal === "needs_human_oracle" ? { unverifiedReason: "no_verdict" } : {}), rounds, findings: oracle?.trace ? [sanitizeUntrustedText(oracle.trace)] : [], }; } export async function runRepairLoop(runner, implSpec, config) { if (config.workerEnv === undefined && config.immutableOraclePaths === undefined) { config.onWarn?.(new Error("repair-loop §5.1: graderEnv was supplied but workerEnv is undefined — the grader/worker env identity " + "check is SKIPPED. Pass config.workerEnv (the env runner.runTask runs the impl in) so an accidental " + "shared-env misconfiguration is caught; omit it only when the envs provably cannot collide.")); } const unprotectedResult = (reason, isolationClass) => { const oracle = { tier: "none", passed: false, flaky: false, retries: 0 }; return { taskId: implSpec.taskId ?? "", result: `oracle isolation check failed: ${reason}`, terminal: { kind: "failed", code: "oracle.unprotected", message: `oracle isolation check failed: ${reason}` }, sessionId: "", stats: { tokens: 0, turns: 0, costMicroUsd: 0 }, repairTerminal: "oracle.unprotected", ...(isolationClass ? { isolationClass } : {}), bundle: { ...freshBundle(), attemptCount: config.resumeBundle?.attemptCount ?? 0 }, oracleCostMicroUsd: 0, verification: outcomeFor("oracle.unprotected", oracle, 0), }; }; let isolationClass; if (config.immutableOraclePaths !== undefined && config.workerEnv === undefined) { return unprotectedResult("immutableOraclePaths requested the structural isolation assertion but workerEnv is undefined — cannot run it (fail-closed)"); } if (config.immutableOraclePaths !== undefined && config.workerEnv !== undefined) { const grader = { env: config.graderEnv, immutableOraclePaths: config.immutableOraclePaths, provenance: "control_plane", }; const verdict = await assertOracleIsolation(config.workerEnv, grader); isolationClass = verdict.isolationClass; if (!verdict.protected) { return unprotectedResult(verdict.reason ?? "grader env failed the structural isolation assertion", isolationClass); } } else if (config.workerEnv !== undefined && config.workerEnv === config.graderEnv) { return unprotectedResult("graderEnv must not be the worker env"); } const maxAttempts = Number.isFinite(config.maxAttempts) ? Math.max(1, Math.floor(config.maxAttempts)) : 1; if (!Number.isFinite(config.maxAttempts)) { config.onWarn?.(new Error(`repair-loop: maxAttempts must be a finite number (got ${config.maxAttempts}) — falling back to 1 attempt`)); } const bundle = config.resumeBundle ? { ...config.resumeBundle, diagnostics: [...config.resumeBundle.diagnostics], rejectedHypotheses: [...config.resumeBundle.rejectedHypotheses] } : freshBundle(); const startedAt = Date.now(); const seededSpend = config.resumeBundle?.spentMicroUsd; const seededActiveMs = config.resumeBundle?.activeElapsedMs; const priorActiveMs = typeof seededActiveMs === "number" && Number.isFinite(seededActiveMs) ? Math.max(0, seededActiveMs) : 0; let spend = typeof seededSpend === "number" && Number.isFinite(seededSpend) ? Math.max(0, seededSpend) : 0; const stampAccount = () => { bundle.spentMicroUsd = spend; bundle.activeElapsedMs = priorActiveMs + (Date.now() - startedAt); }; let lastResult; let lastOracle; let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownTotalInput = 0, ownCached = 0, ownOutput = 0; let ownCacheWrite = 0, ownCacheWriteLong = 0, ownCostMicro = 0; let ownLlmRootMicro = 0, ownCompactionMicro = 0; let anyAttemptUnpriced = false; let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0; let hrCount = 0, hrWaitMs = 0; const hrGates = []; let oracleCostMicroUsd = 0; const accumulate = (s) => { 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) anyAttemptUnpriced = 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; } }; const cumulativeStats = (base) => buildCumulativeStats(base, { tokens: ownTokens, turns: ownTurns, promptTokens: ownPrompt, totalInputTokens: ownTotalInput, cachedTokens: ownCached, outputTokens: ownOutput, cacheWriteTokens: ownCacheWrite, cacheWriteTokensLong: ownCacheWriteLong, costMicroUsd: ownCostMicro, anyLegUnpriced: anyAttemptUnpriced, llmRootMicroUsd: ownLlmRootMicro, compactionMicroUsd: ownCompactionMicro, nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro }, humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates }, }); let restartedOnce = false; let restartUsed = false; const cleanRestartSpec = () => { const { sessionId: _drop, ...rest } = implSpec; return { ...rest, objective: `${implSpec.objective}\n\n${repairObjective(bundle)}` }; }; const exhaustedBeforeDispatch = (message, errorCode) => ({ taskId: implSpec.taskId ?? "", result: message ?? `repair loop: attempt budget already spent (attemptCount ${bundle.attemptCount} >= maxAttempts ${maxAttempts}) — no attempt dispatched`, terminal: { kind: "failed", code: errorCode ?? "repair.attempts_exhausted", message: message ?? `repair loop: attempt budget already spent (attemptCount ${bundle.attemptCount} >= maxAttempts ${maxAttempts}) — no attempt dispatched` }, sessionId: implSpec.sessionId ?? "", stats: { tokens: 0, turns: 0, costMicroUsd: 0 }, repairTerminal: "gave_up", bundle, ...(isolationClass ? { isolationClass } : {}), oracleCostMicroUsd, verification: outcomeFor("gave_up", undefined, bundle.attemptCount), }); while (true) { if (!restartedOnce && bundle.attemptCount >= maxAttempts) return exhaustedBeforeDispatch(); if (config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd) { return exhaustedBeforeDispatch(`repair loop: cost ceiling already spent (carried ${spend} >= ceiling ${config.costCeilingMicroUsd} micro-USD) — no attempt dispatched`, "repair.budget_exhausted"); } if (config.totalTimeoutMs != null && priorActiveMs + (Date.now() - startedAt) >= config.totalTimeoutMs) { return exhaustedBeforeDispatch(`repair loop: active time budget already spent (carried ${priorActiveMs + (Date.now() - startedAt)}ms >= ${config.totalTimeoutMs}ms) — no attempt dispatched`, "repair.budget_exhausted"); } stampAccount(); const isRepair = bundle.attemptCount > 0 && lastResult !== undefined && !restartedOnce; const isSeededResume = !restartedOnce && lastResult === undefined && config.resumeBundle !== undefined && (bundle.failureTrace.trim() !== "" || bundle.diagnostics.length > 0); const spec = restartedOnce ? cleanRestartSpec() : isRepair ? { ...implSpec, sessionId: lastResult.sessionId, objective: repairObjective(bundle) } : isSeededResume ? { ...implSpec, objective: implSpec.sessionId ? repairObjective(bundle) : `${implSpec.objective}\n\n${repairObjective(bundle)}`, } : implSpec; restartedOnce = false; const liveStamp = setInterval(stampAccount, 1_000); liveStamp.unref?.(); let result; try { result = await runner.runTask(spec, { repairBundle: bundle }); } finally { clearInterval(liveStamp); } spend += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0); stampAccount(); accumulate(result.stats); lastResult = result; bundle.attemptCount += 1; if (isDurablePause(result.terminal)) { return { ...result, stats: cumulativeStats(result.stats), repairTerminal: "gave_up", bundle, ...(isolationClass ? { isolationClass } : {}), oracleCostMicroUsd, verification: { verdict: "unverified", unverifiedReason: pauseFamily(result.terminal), rounds: bundle.attemptCount, findings: [] }, }; } if (result.terminal.kind !== "completed") { return { ...result, stats: cumulativeStats(result.stats), repairTerminal: "gave_up", bundle, ...(isolationClass ? { isolationClass } : {}), oracleCostMicroUsd, verification: { verdict: "unverified", unverifiedReason: "impl_incomplete", rounds: bundle.attemptCount, findings: [] }, }; } const oracle = await config.oracle(config.graderEnv, result.result); oracleCostMicroUsd += oracle.costMicroUsd ?? 0; spend += oracle.costMicroUsd ?? 0; stampAccount(); lastOracle = oracle; bundle.oracleTier = oracle.tier; const projected = terminalForTier(oracle); config.onAttempt?.({ attempt: bundle.attemptCount, repairTerminal: projected, oracle }); if (projected !== undefined) { return { ...result, stats: cumulativeStats(result.stats), repairTerminal: projected, bundle, ...(isolationClass ? { isolationClass } : {}), oracleCostMicroUsd, verification: outcomeFor(projected, oracle, bundle.attemptCount), }; } if (oracle.trace && oracle.trace.trim() !== "") { bundle.failureTrace = oracle.trace; if (bundle.diagnostics.length < 3) bundle.diagnostics.push(oracle.trace); } const overTime = config.totalTimeoutMs != null && priorActiveMs + (Date.now() - startedAt) >= config.totalTimeoutMs; const overCost = config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd; if (bundle.attemptCount >= maxAttempts || overTime || overCost) { if (bundle.attemptCount >= maxAttempts && !overTime && !overCost && lastResult !== undefined && !restartUsed) { restartUsed = true; restartedOnce = true; continue; } const terminal = "gave_up"; return { ...lastResult, stats: cumulativeStats(lastResult.stats), repairTerminal: terminal, bundle, ...(isolationClass ? { isolationClass } : {}), oracleCostMicroUsd, verification: outcomeFor(terminal, lastOracle, bundle.attemptCount), }; } } }