UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

183 lines (182 loc) • 10.9 kB
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js"; import { extractErrorCode, isProviderEchoShape, refusedTheRequest, stripErrorCodePrefix } from "../../brain/errors.js"; import { completedTerminalOf, modelOutputInvalidOfMessage, providerVerdictOf, requestRouteOfMessage, thrownFailureOf } from "./terminal-account.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); } 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 thrown = thrownFailureOf(flags.threw, thrownMessage(flags.threw)); const axisCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : flags.conflict ? "conflict" : undefined; const code = axisCode ?? thrown.code; const detail = axisCode === undefined ? thrown.detail : undefined; terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: thrown.message, ...(detail !== undefined ? { detail } : {}) }; } 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 messageNamed = true; const lifted = extractErrorCode(message); if (lifted) { code = lifted; message = stripErrorCodePrefix(message); } else if (flags.gitCoreOverBudget) { code = "irreducible_core_over_budget"; messageNamed = false; } else if (flags.conflict) { code = "conflict"; messageNamed = false; } else { code = "provider.error"; } const outputInvalid = apiError ? undefined : modelOutputInvalidOfMessage(final); let detail; if (messageNamed && apiError && typeof apiStatus === "number" && refusedTheRequest(apiStatus)) { const route = requestRouteOfMessage(final); detail = { kind: "provider_rejected", status: apiStatus, ...(typeof apiCode === "string" && isProviderEchoShape(apiCode) ? { code: apiCode } : {}), ...providerVerdictOf(final), ...(route !== undefined ? { route } : {}) }; } else if (messageNamed && outputInvalid !== undefined) { detail = { kind: "model_output_invalid", ...outputInvalid }; } terminal = { kind: "failed", code, message, ...(detail !== undefined ? { detail } : {}) }; } else { terminal = completedTerminalOf(final); } 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, ...(stats.sideCallMicroUsd !== undefined ? { sideCallMicroUsd: stats.sideCallMicroUsd } : {}), }; } 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, sideCallMicroUsd: _internalSideCall, ...publicStats } = stats; void _internalCompaction; void _internalSideCall; 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 }; }