UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

193 lines (192 loc) • 10.6 kB
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js"; import { extractErrorCode, isProviderEchoShape, refusedTheRequest, stripErrorCodePrefix } from "../../brain/errors.js"; import { terminalProjection } from "./terminal-projection.js"; import { haltConsumed } from "./halt-attribution.js"; import { providerReportedModel } from "../message-utils.js"; import { isErrorLike, readMember } from "../thrown-value.js"; import { describeRejectedValue } from "../tool-search-setting.js"; const SALVAGE_ELIGIBLE_TERMINALS = new Set([ "output.degenerate", "limits.max_tokens_exceeded", "limits.max_cost_exceeded", "limits.max_turns_exceeded", "limits.max_walltime_exceeded", "env.lifetime_expired", "usage.window_exhausted", ]); function thrownMessage(threw) { if (isErrorLike(threw)) { const m = readMember(threw, "message"); if (typeof m === "string") return m; } return describeRejectedValue(threw); } export function errorCodeOf(err) { let cur = err; let fallback; for (let depth = 0; cur !== null && cur !== undefined && typeof cur === "object" && depth < 8; depth++) { const code = readMember(cur, "code"); if (typeof code === "string" && code) { if (code !== "unknown") { return code; } fallback ??= code; } cur = readMember(cur, "cause"); } return fallback; } function stripThinkTags(text) { let t = text.replace(/<think>[\s\S]*?<\/think>/gi, ""); const lower = t.toLowerCase(); const close = lower.lastIndexOf("</think>"); if (close !== -1 && !lower.slice(0, close).includes("<think>")) { t = t.slice(close + "</think>".length); } return t.replace(/<\/?think>/gi, "").trim(); } function assistantText(msg) { const text = msg.content .filter((c) => c.type === "text") .map((c) => c.text) .join(""); return stripThinkTags(text); } export function answerSettled(final, continuationCut = false) { return final?.stopReason === "stop" && final.partialFinalized !== true && !continuationCut; } export function terminalCauseOf(final, stats, flags) { let terminal; let apiFailure; const haltOwnsAbortedFinal = flags.haltConsumption?.turnCut === "halt" && final?.stopReason === "aborted" && !flags.abortedLive && flags.abortedForTimeout !== true && flags.abortedForTurns !== true; const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final); if (flags.outputInvalid) { terminal = { kind: "failed", code: "output.invalid", message: "model did not produce a valid structured output within the retry limit" }; } else if (isDegenerate) { terminal = { kind: "failed", code: "output.degenerate", ...(final?.errorMessage !== undefined ? { message: final.errorMessage } : {}) }; } else if (flags.budgetHit) { const axisName = flags.budgetAxis === "tokens" ? "limits.maxTokens" : "limits.maxCostUsd"; terminal = { kind: "failed", code: flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded", message: flags.budgetHit === "precall" ? `the estimated cost of the first call already exceeds ${axisName}; the task was not started` : `cumulative usage exceeded ${axisName}`, }; } else if (flags.suspendLoop) { let message = "task suspended too many times (resume/restart loop) — exceeded the suspend limit"; const gates = stats.humanReview?.gates ?? []; if (gates.length > 0 && gates.every((g) => g.decision === "allow")) { message += `. Diagnosis: all ${gates.length} recorded gate decision(s) on this run were ALLOW — a gate that keeps ` + `asking combined with an approver that keeps approving consumes the suspend allowance on legitimate ` + `work. Remedies: answer asks at a LIVE onAsk (a synchronous allow parks nothing and consumes no ` + `suspend), keep provably-benign commands inside the read boundary so the classifier auto-allows them, ` + `or raise maxSuspends for genuinely approval-heavy tasks.`; } terminal = { kind: "failed", code: "suspend.loop", message }; } else if (flags.threw) { const message = thrownMessage(flags.threw); const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : flags.conflict ? "conflict" : errorCodeOf(flags.threw); terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message }; } else if (flags.blockedReason) { terminal = { kind: "blocked", reason: flags.blockedReason, ...(flags.blockedDetail !== undefined ? { detail: flags.blockedDetail } : {}) }; } else if (flags.paused) { terminal = flags.paused; } else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal && flags.answerSettled !== true) { const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined; terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") }; } else if (haltConsumed(flags.haltConsumption) && (!final || haltOwnsAbortedFinal)) { terminal = { kind: "completed" }; } else if (!final) { terminal = { kind: "failed", message: "no assistant message produced" }; } else if (final.stopReason === "error") { let message = final.errorMessage ?? "model error"; let code; const apiError = readMember(final, "isApiErrorMessage") === true; const apiStatus = readMember(final, "apiErrorStatus"); const apiRequestId = readMember(final, "requestId"); const apiCode = readMember(final, "apiErrorCode"); if (apiError) { apiFailure = { ...(apiStatus !== undefined ? { status: apiStatus } : {}), ...(typeof apiRequestId === "string" && isProviderEchoShape(apiRequestId) ? { requestId: apiRequestId } : {}), }; } let providerNamed = true; const lifted = extractErrorCode(message); if (lifted) { code = lifted; message = stripErrorCodePrefix(message); } else if (flags.gitCoreOverBudget) { code = "irreducible_core_over_budget"; providerNamed = false; } else if (flags.conflict) { code = "conflict"; providerNamed = false; } else { code = "provider.error"; } const detail = apiError && providerNamed && typeof apiStatus === "number" && refusedTheRequest(apiStatus) ? { kind: "provider_rejected", status: apiStatus, ...(typeof apiCode === "string" && isProviderEchoShape(apiCode) ? { code: apiCode } : {}) } : undefined; terminal = { kind: "failed", code, message, ...(detail !== undefined ? { detail } : {}) }; } else { terminal = { kind: "completed" }; } return { terminal, apiFailure }; } export function assembleResult(spec, sessionId, final, stats, flags) { const taskId = spec.taskId ?? sessionId; const text = final ? assistantText(final) : ""; const responseModel = final === undefined ? undefined : providerReportedModel(final); if (!flags.unpricedSpend) { const compactionMicroUsd = stats.compactionMicroUsd ?? 0; const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0; stats.costBreakdown = { llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd), nestedSubagentMicroUsd, memoryConsolidationMicroUsd: 0, compactionMicroUsd, }; } const result = text; let salvagedOutput; const { terminal, apiFailure } = terminalCauseOf(final, stats, flags); const { errorCode } = terminalProjection(terminal); if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) { salvagedOutput = text.trim() || undefined; } const retryAfterMs = errorCode === "usage.window_exhausted" || errorCode === "memory.admission_required" ? flags.retryAfterMs : undefined; const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats; void _internalCompaction; if (flags.unpricedSpend) delete publicStats.costMicroUsd; const stampHaltedByUser = haltConsumed(flags.haltConsumption); return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), ...(responseModel !== undefined ? { responseModel } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), ...(flags.compaction !== undefined ? { compaction: flags.compaction } : {}), stats: publicStats }; }