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