UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

192 lines (191 loc) • 9.46 kB
import { createHash, randomUUID } from "node:crypto"; import { Type } from "typebox"; import { delimitUntrusted } from "../core/untrusted-text.js"; import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js"; import { isDurablePause, pauseFamily } from "../agents/suspend-guard.js"; import { toolFace } from "../core/tool-catalog-entries.js"; export const DECLARE_DONE_TOOL_NAME = "DeclareDone"; function createDeclareDoneTool(ref) { return { name: DECLARE_DONE_TOOL_NAME, ...toolFace("declare-done"), description: "Call this ONLY when you believe the goal is FULLY achieved — verified against evidence, not just " + "attempted. Provide a concise summary of what was achieved and how you verified it. Calling this " + "declares completion; a verification check then runs. If it passes, the goal stops and surfaces for " + "human review. If it does NOT pass, you will be asked to keep working with the verification feedback.", parameters: Type.Object({ summary: Type.String() }), execute: (args) => { ref.declared = true; ref.summary = String(args.summary ?? "").trim(); return { content: "Completion declared. Verifying…", terminate: true }; }, }; } const TIMEOUT_FEEDBACK = "Your previous attempt ran out of steps before declaring completion. Keep going."; const NOT_DECLARED_FEEDBACK = "Your previous attempt ended without declaring completion. If the goal is truly done, declare it; otherwise keep working."; function continuationPrompt(goalObjective, feedback, hasOutputSchema) { const feedbackBlock = typeof feedback === "string" ? feedback : `A verification check found the goal is NOT yet complete:\n${delimitUntrusted("VERIFICATION FEEDBACK", feedback.reason)}`; const declareHow = hasOutputSchema ? `call ${OUTPUT_TOOL_NAME} with your final result` : `call the ${DECLARE_DONE_TOOL_NAME} tool`; return `The goal is NOT yet complete. Continue working toward it.\n\n${feedbackBlock}\n\nGoal: ${goalObjective}\n\nWhen you are truly done, ${declareHow} to declare completion.`; } function raceAbortReject(p, signal) { if (signal.aborted) return Promise.reject(new Error("aborted")); return new Promise((resolve, reject) => { let settled = false; const onAbort = () => { if (settled) return; settled = true; reject(new Error("aborted")); }; signal.addEventListener("abort", onAbort, { once: true }); p.then((v) => { if (settled) return; settled = true; signal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { if (settled) return; settled = true; signal.removeEventListener("abort", onAbort); reject(e); }); }); } export async function runGoal(runner, spec) { if (!Number.isFinite(spec.maxIterations) || spec.maxIterations < 1) { throw new Error(`runGoal: maxIterations must be a finite number ≥ 1 (got ${spec.maxIterations})`); } if (spec.totalTimeoutMs !== undefined && (!Number.isFinite(spec.totalTimeoutMs) || spec.totalTimeoutMs < 0 || spec.totalTimeoutMs > 2_147_483_647)) { throw new Error(`runGoal: totalTimeoutMs must be a finite number in [0, 2147483647] (got ${spec.totalTimeoutMs}) — a host timer clamps anything else and fires immediately. Omit it for no deadline.`); } const hasOutputSchema = spec.taskSpec.outputSchema !== undefined; if (!hasOutputSchema && (spec.taskSpec.tools ?? []).some((t) => t.name === DECLARE_DONE_TOOL_NAME)) { const e = new Error(`Tool name "${DECLARE_DONE_TOOL_NAME}" is reserved in goal mode.`); e.code = "config.reserved_tool_name"; throw e; } const sessionId = spec.taskSpec.sessionId ?? randomUUID(); const verificationKind = spec.verificationKind; let cumulativeTokens = 0; let lastVerdict; let lastResult; const startedAt = Date.now(); const objectiveHash = createHash("sha256").update(spec.objective).digest("hex").slice(0, 16); let spentCostMicroUsd = 0; let spentTurns = 0; let doneCheckRuns = 0; let oracleHadRedRun = false; const deadlineCtl = new AbortController(); let timer; if (spec.totalTimeoutMs !== undefined) { timer = setTimeout(() => deadlineCtl.abort(), spec.totalTimeoutMs); timer.unref?.(); } const goalSignal = spec.signal ? AbortSignal.any([spec.signal, deadlineCtl.signal]) : deadlineCtl.signal; const classifyStop = () => spec.signal?.aborted ? { status: "aborted" } : deadlineCtl.signal.aborted ? { status: "budget", budgetCause: "walltime" } : null; const mk = (status, iterations, over) => { const r = { status, iterations, result: over?.result ?? lastResult, lastVerdict, verificationKind, accepted: false, cumulativeTokens, checkpointToken: over?.checkpointToken, errorCode: over?.errorCode, ...(status === "budget" && over?.budgetCause !== undefined ? { budgetCause: over.budgetCause } : {}), }; if (verificationKind === "mechanical" && status !== "suspended" && status !== "needs_review") { runner.emitTaskOutcome?.({ taskSignature: spec.taskSignature ?? `goal:${objectiveHash}`, runId: r.result?.taskId ?? sessionId, model: r.result?.model, verificationKind: "mechanical", status, errorCode: r.errorCode, oracle: { green: status === "achieved", checks: doneCheckRuns }, oracleHadRedRun, signatureInputs: { toolset: (spec.taskSpec.tools ?? []).map((t) => t.name).sort(), objectiveHash, }, tokens: cumulativeTokens, costMicroUsd: spentCostMicroUsd, turns: spentTurns, wallMs: Date.now() - startedAt, }); } return r; }; try { let lastFeedback; for (let iteration = 1; iteration <= spec.maxIterations; iteration++) { const preStop = classifyStop(); if (preStop) return mk(preStop.status, iteration - 1, { budgetCause: preStop.budgetCause }); if (spec.budgetTokens !== undefined && cumulativeTokens >= spec.budgetTokens) return mk("budget", iteration - 1, { budgetCause: "tokens" }); const objective = iteration === 1 ? spec.objective : continuationPrompt(spec.objective, lastFeedback, hasOutputSchema); const doneRef = {}; const tools = hasOutputSchema ? spec.taskSpec.tools : [...(spec.taskSpec.tools ?? []), createDeclareDoneTool(doneRef)]; const result = await runner.runTask({ ...spec.taskSpec, sessionId, objective, tools, signal: goalSignal }, { goalMode: true }); lastResult = result; cumulativeTokens += (result.stats.tokens ?? 0) + (result.stats.nested?.tokens ?? 0); spentCostMicroUsd += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0); spentTurns += (result.stats.turns ?? 0) + (result.stats.nested?.turns ?? 0); if (isDurablePause(result.terminal)) { return mk(pauseFamily(result.terminal), iteration, { result, checkpointToken: result.terminal.token }); } const midStop = classifyStop(); if (midStop) return mk(midStop.status, iteration, { result, budgetCause: midStop.budgetCause }); if (result.terminal.kind === "blocked") return mk("blocked", iteration, { result }); if (result.terminal.kind === "failed") { const code = result.terminal.code; if (code !== undefined && code.startsWith("limits.max_")) { lastFeedback = TIMEOUT_FEEDBACK; continue; } return mk("failed", iteration, { result, errorCode: code }); } const gate1 = hasOutputSchema ? result.structuredOutput !== undefined : doneRef.declared === true; if (!gate1) { lastFeedback = NOT_DECLARED_FEEDBACK; continue; } let verdict; try { verdict = await raceAbortReject(spec.doneCheck({ result, iteration, sessionId, cumulativeTokens }, goalSignal), goalSignal); } catch { const s = classifyStop(); if (s) return mk(s.status, iteration, { result, budgetCause: s.budgetCause }); return mk("failed", iteration, { result, errorCode: "goal.donecheck_error" }); } lastVerdict = verdict; doneCheckRuns++; if (verdict.done) return mk("achieved", iteration, { result }); oracleHadRedRun = true; lastFeedback = { reason: verdict.reason }; } return mk("max_iterations", spec.maxIterations); } finally { if (timer) clearTimeout(timer); } }