UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

171 lines (170 loc) • 8.97 kB
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 }; }