@sema-agent/core
Version:
Stateless, task-oriented AI agent core
171 lines (170 loc) • 8.97 kB
JavaScript
import { primaryActivityArg } from "../arg-summary.js";
import { remainingBudgetMicroUsd, remainingTokens } from "../checkpoint-store.js";
import { computeCostMicroUsd, isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
import { resolveModel } from "../roles.js";
import { emitTrace } from "../trace.js";
import { resolveMaxTurns } from "./clock-and-limits.js";
import { resolveOutputRetries } from "./synthetic-tools.js";
import { cacheFamilyOf } from "./usage-accounting.js";
import { asThrownError } from "../thrown-value.js";
export function runTelemetryAndBudgetSeats(input) {
const { spec, prepared, resume, stats, taskIdRef, entryTracer, modelCatalog, runSourceTaskId, rs, runner, sessions } = input;
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
if (resume &&
resume.cp.suspendedAt !== undefined &&
(resume.cp.gate.kind === "human" ||
resume.cp.gate.kind === "irreversible_ask" ||
resume.cp.gate.kind === "needs_review" ||
resume.cp.gate.kind === "plan_review")) {
const waitMs = Math.max(0, prepared.now() - resume.cp.suspendedAt);
const decision = resume.outcome.gate === "wake" ? undefined : resume.outcome.decision;
prepared.humanReviewRef.count += 1;
prepared.humanReviewRef.totalWaitMs += waitMs;
const gateToolName = "toolName" in resume.cp.gate ? resume.cp.gate.toolName : undefined;
const gatedCall = resume.cp.pendingAction.kind === "tool_approval" ? resume.cp.pendingAction : undefined;
const gateToolArg = gatedCall === undefined ? undefined : primaryActivityArg(gatedCall.args);
prepared.humanReviewRef.gates.push({
kind: resume.cp.gate.kind,
waitMs,
...(decision !== undefined ? { decision } : {}),
...(gateToolName !== undefined ? { toolName: gateToolName } : {}),
...(gatedCall !== undefined ? { toolCallId: gatedCall.toolCallId } : {}),
...(gateToolArg !== undefined ? { toolArg: gateToolArg } : {}),
});
}
if (resume !== undefined && resume.outcome.gate === "plan_review" && resume.outcome.decision === "reject") {
void prepared.permissionMode.steer("plan");
}
rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
rs.telemetry.pricing = runner.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
rs.telemetry.pricingConfigured = isModelPriced(prepared.model, runner.deps.pricing);
if (spec.limits?.degrade) {
try {
rs.degrade.degradeToModel = resolveModel(spec.limits.degrade.to, modelCatalog);
}
catch (e) {
runner.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId });
}
}
const famOverride = prepared.model.params?.promptCacheFamily;
if (famOverride && !["input-includes-cached", "input-excludes-cached", "openai", "anthropic"].includes(famOverride)) {
runner.deps.onError?.(new Error(`prompt-cache: unrecognized model.params.promptCacheFamily "${famOverride}" — ignored (fell back to model.api inference). Use "input-includes-cached" | "input-excludes-cached" (aliases "openai" | "anthropic").`), { phase: "prompt-cache", sessionId: prepared.sessionId });
}
rs.limits.turnsExceeded = false;
rs.degrade.outputErrorStreak = 0;
rs.degrade.outputInvalid = false;
rs.telemetry.cacheBreakReported = false;
rs.limits.outputRetryCap = resolveOutputRetries(spec.outputRetries);
rs.limits.effectiveMaxTurns = resolveMaxTurns(spec.limits);
rs.telemetry.tracer = entryTracer !== undefined ? entryTracer.tracer : (spec.tracer ?? runner.deps.tracer);
rs.telemetry.taskId = runSourceTaskId;
rs.telemetry.runId = prepared.runId;
if (taskIdRef) {
taskIdRef.current = rs.telemetry.taskId;
taskIdRef.sessionId = prepared.sessionId;
}
rs.telemetry.taskStart = Date.now();
rs.telemetry.taskStartMonotonic = performance.now();
const discloseNoteTaskRunFailure = (err) => {
try {
runner.deps.onError?.(asThrownError(err), { phase: "config", sessionId: prepared.sessionId });
}
catch {
}
};
try {
void Promise.resolve(sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId, rs.telemetry.runId)).catch(discloseNoteTaskRunFailure);
}
catch (err) {
discloseNoteTaskRunFailure(err);
}
const noteUnevaluablePriceTable = (p) => {
if (prepared.usageGovernance?.governsCost !== true)
return;
if (malformedPricingField(p) !== undefined)
rs.telemetry.unpricedSpend = true;
};
rs.degrade.recordDegraded = (info, toModel) => {
if (rs.degrade.degraded !== undefined)
return;
let m = toModel;
if (!m) {
try {
m = resolveModel(info.to, modelCatalog);
}
catch {
m = undefined;
}
}
if (m) {
rs.telemetry.pricing = runner.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
rs.telemetry.pricingConfigured = isModelPriced(m, runner.deps.pricing);
noteUnevaluablePriceTable(rs.telemetry.pricing);
rs.telemetry.cacheFamily = cacheFamilyOf(m);
}
else {
if (runner.deps.pricing?.[info.to]) {
rs.telemetry.pricing = runner.deps.pricing[info.to];
rs.telemetry.pricingConfigured = true;
noteUnevaluablePriceTable(rs.telemetry.pricing);
}
else {
rs.telemetry.pricingConfigured = false;
}
rs.telemetry.cacheFamily = "input-excludes-cached";
}
rs.degrade.degraded = info;
emitTrace(rs.telemetry.tracer, () => ({ kind: "task.degraded", version: 1, taskId: rs.telemetry.taskId, from: info.from, to: info.to, reason: info.reason, atTurn: info.atTurn, ts: Date.now() }));
try {
runner.deps.onError?.(new Error(`degraded to a cheaper model: ${info.from} → ${info.to} (${info.reason}) at turn ${info.atTurn}`), { phase: "degraded", sessionId: prepared.sessionId });
}
catch {
}
};
const sliceWindowMicroUsd = spec.limits?.maxCostUsd !== undefined ? Math.round(spec.limits.maxCostUsd * 1e6) : undefined;
rs.budget.remainingMicroUsd = prepared.resourceLedger
? remainingBudgetMicroUsd(prepared.resourceLedger)
: spec.resourceSuspend?.totalBudgetUsd !== undefined
? Math.round(spec.resourceSuspend.totalBudgetUsd * 1e6)
: undefined;
rs.budget.maxCostMicroUsd =
sliceWindowMicroUsd !== undefined && rs.budget.remainingMicroUsd !== undefined
? Math.min(sliceWindowMicroUsd, rs.budget.remainingMicroUsd)
: (sliceWindowMicroUsd ?? rs.budget.remainingMicroUsd);
rs.budget.remainingTokens = prepared.resourceLedger
? remainingTokens(prepared.resourceLedger)
: spec.resourceSuspend?.totalTokens;
rs.budget.maxTokensWindow =
spec.limits?.maxTokens !== undefined && rs.budget.remainingTokens !== undefined
? Math.min(spec.limits.maxTokens, rs.budget.remainingTokens)
: (spec.limits?.maxTokens ?? rs.budget.remainingTokens);
rs.budget.overBudget = () => {
if (rs.budget.maxTokensWindow !== undefined && stats.tokens > rs.budget.maxTokensWindow)
return "tokens";
if (rs.budget.maxCostMicroUsd !== undefined && stats.costMicroUsd > rs.budget.maxCostMicroUsd)
return "cost";
return undefined;
};
rs.budget.streamCancel =
(spec.limits?.budgetStreamCancel ?? rs.budget.maxCostMicroUsd !== undefined) && prepared.suspendForResource === undefined;
rs.budget.callOutputChars = 0;
rs.budget.lastStreamBudgetCheck = 0;
rs.budget.projectedOverBudget = () => {
const estOut = Math.ceil(rs.budget.callOutputChars / 4);
if (rs.budget.maxTokensWindow !== undefined && stats.tokens + estOut > rs.budget.maxTokensWindow)
return "tokens";
if (rs.budget.maxCostMicroUsd !== undefined) {
const estOutMicro = computeCostMicroUsd({ totalInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: estOut }, rs.telemetry.pricing);
if (stats.costMicroUsd + estOutMicro > rs.budget.maxCostMicroUsd)
return "cost";
}
return undefined;
};
rs.turn.turnUsageMissing = false;
rs.counters.repetitionCuts = 0;
rs.counters.repetitionSpared = 0;
rs.counters.repetitionEvents = [];
rs.counters.REPETITION_EVENTS_CAP = 20;
rs.counters.preemptIgnoredReported = false;
return { noteUnevaluablePriceTable };
}