@sema-agent/core
Version:
Stateless, task-oriented AI agent core
578 lines (575 loc) • 26.8 kB
JavaScript
import { randomUUID } from "node:crypto";
import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_PROBLEM_BYTES, MAX_STRATEGY_TEXT_BYTES } from "../core/strategy-store.js";
import { createSafeNotifier, observeThenableRejection } from "../core/safe-notify.js";
import { childClampSeats } from "../core/child-clamp-seats.js";
import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
import { isDefineToolProduct, rebindDefineToolCtx, stampDefineToolBrand } from "../core/tools.js";
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
import { buildCumulativeStats } from "./cumulative-stats.js";
import { thrownLabel, thrownText } from "../core/thrown-value.js";
export const TEACHER_PROMPT = `You are an expert advisor to a less-capable "student" agent that got stuck.
You receive the task and the student's recent failed attempts. Your job is to help the student RECOVER,
not to do the work for it.
Return ONLY one valid JSON object (no markdown, no prose outside it):
{
"strategy": "<general approach for this CLASS of problem, 1-2 sentences>",
"correction": "<specifically what the student did wrong>",
"nextStep": "<one concrete action the student should take now>",
"takeover": <true ONLY if the student is hopelessly stuck and you must do it yourself>,
"confidence": <0=low .. 3=high, how sure you are this guidance is correct>
}
Be terse. Do NOT solve the whole task unless takeover=true. If you are unsure, set confidence=0 and takeover=false.`;
const MONITOR_PROMPT = `You judge whether a stuck-looking agent is making real progress.
Compare its two most recent turns below. Reply with ONLY one word: "stuck" or "progressing".`;
const VERIFIER_INSTRUCTION = `You verify an agent's output against its task. Be lenient: accept anything that SUBSTANTIALLY satisfies the task.`;
function verifierPrompt(objective, output, instruction = VERIFIER_INSTRUCTION) {
return (`${instruction}\n\nTASK:\n${objective}\n\n${delimitUntrusted("AGENT OUTPUT", output)}\n\n` +
`Does the output substantially satisfy the task? Reply with ONLY "pass" or "fail: <one concrete reason>".`);
}
export function parseTeacherAdvice(text) {
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start !== -1 && end > start) {
try {
const oU = JSON.parse(text.slice(start, end + 1));
if (typeof oU !== "object" || oU === null)
throw new Error("not an object");
const o = oU;
return {
strategy: typeof o.strategy === "string" ? o.strategy : undefined,
correction: typeof o.correction === "string" ? o.correction : undefined,
nextStep: typeof o.nextStep === "string" ? o.nextStep : undefined,
takeover: o.takeover === true,
confidence: typeof o.confidence === "number" ? o.confidence : 0,
};
}
catch {
}
}
return { takeover: false, confidence: 0, correction: text.trim(), raw: text };
}
export async function runWithTeacher(runner, studentSpec, teacher = {}) {
const store = teacher.strategyStore;
const scope = teacher.scope;
const incidentNotifier = createSafeNotifier();
const warnedIncidentOps = new Set();
const discloseStoreIncident = (i) => {
const seat = teacher.onStrategyStoreIncident;
if (seat) {
const site = `teacher.onStrategyStoreIncident.${i.op}`;
incidentNotifier.notify(() => observeThenableRejection(seat(i), incidentNotifier, site), site);
}
else if (!warnedIncidentOps.has(i.op)) {
warnedIncidentOps.add(i.op);
console.warn(`runWithTeacher: strategy store ${i.op} failed (contained — the run continues): ${i.error}`);
}
};
const isConfigCoded = (e) => typeof e?.code === "string" && (e.code).startsWith("config.");
let spec = studentSpec;
let injected;
if (store && scope && teacher.injectStrategies !== false) {
let found = [];
try {
const raw = await store.find(scope, studentSpec.objective, teacher.retrieveK ?? 3);
if (!Array.isArray(raw)) {
discloseStoreIncident({ op: "find", error: "store.find returned a non-array result — injecting nothing" });
}
else {
let dropped = 0;
for (const e of raw) {
const c = e;
const id = c?.id;
const ts = c?.ts;
const strategy = c?.strategy;
if (typeof id !== "string" || typeof ts !== "string" || typeof strategy !== "string") {
dropped++;
continue;
}
const origin = c?.origin;
found.push({ id, ts, strategy, ...(origin === "earned" || origin === "seeded" ? { origin } : {}) });
}
if (dropped > 0) {
discloseStoreIncident({ op: "find", error: `${dropped} malformed retrieval entries dropped (id/ts/strategy must be strings)` });
}
}
}
catch (e) {
if (isConfigCoded(e))
throw e;
discloseStoreIncident({ op: "find", error: thrownText(e) });
found = [];
}
let budget = 0;
for (let i = 0; i < found.length; i++) {
budget += Buffer.byteLength(found[i].strategy, "utf8");
if (budget > MAX_STRATEGY_INJECTION_TOTAL_BYTES) {
discloseStoreIncident({
op: "find",
error: `injection truncated to ${i} of ${found.length} retrieved strategies — combined text exceeded ${MAX_STRATEGY_INJECTION_TOTAL_BYTES} bytes`,
});
found = found.slice(0, i);
break;
}
}
if (found.length > 0) {
const block = `[STRATEGIES from PAST, DIFFERENT tasks — they may NOT apply. If unsure, IGNORE them and proceed normally.]\n` +
delimitUntrusted("PAST STRATEGIES (from DIFFERENT tasks; may NOT apply — verify before use)", found.map((s) => `- ${s.strategy}`).join("\n"));
spec = { ...studentSpec, objective: `${block}\n\n${studentSpec.objective}` };
injected = found.map((s) => ({ id: s.id, ts: s.ts, ...(s.origin !== undefined ? { origin: s.origin } : {}) }));
}
}
const result = await runTeacherCore(runner, spec, teacher);
if (store && scope && result.terminal.kind === "completed" && result.escalations.length > 0) {
const advice = result.escalations[result.escalations.length - 1].teacher;
if (advice.strategy && advice.confidence >= (teacher.minConfidenceToStore ?? 2)) {
try {
const clip = (text, maxBytes) => {
let out = text;
while (Buffer.byteLength(out) > maxBytes)
out = out.slice(0, -1);
return out;
};
await store.save({
id: randomUUID(),
problem: clip(studentSpec.objective, MAX_STRATEGY_PROBLEM_BYTES),
strategy: clip(advice.strategy, MAX_STRATEGY_TEXT_BYTES),
confidence: Math.min(3, Math.max(0, advice.confidence)),
scope,
ts: new Date().toISOString(),
teacherModel: typeof teacher.model === "string" ? teacher.model : teacher.model?.id,
});
}
catch (e) {
if (isConfigCoded(e))
throw e;
discloseStoreIncident({ op: "save", error: thrownText(e) });
}
}
}
return injected !== undefined && injected.length > 0 ? { ...result, strategiesInjected: injected } : result;
}
async function runTeacherCore(runner, studentSpec, teacher) {
const maxEscalations = teacher.maxEscalations ?? 3;
const spendCap = teacher.teacherSpendRatioCap ?? 0.4;
const stuckThreshold = teacher.stuckThreshold ?? 3;
const stuckHardOverride = teacher.stuckHardOverride ?? 5;
const useStuckMonitor = teacher.useStuckMonitor !== false;
const verifyOutput = teacher.verifyOutput !== false;
const takeoverAfter = teacher.takeoverAfter ?? 2;
const extSignal = studentSpec.signal;
const escalations = [];
let studentTokens = 0;
let studentTurns = 0;
let studentCostMicro = 0;
let anyStudentLegUnpriced = false;
let studentPrompt = 0;
let studentTotalInput = 0;
let studentCached = 0;
let studentOutput = 0;
let studentCacheWrite = 0;
let studentCacheWriteLong = 0;
let studentLlmRootMicro = 0;
let studentCompactionMicro = 0;
let hrCount = 0;
let hrWaitMs = 0;
const hrGates = [];
let studentNestedTokens = 0;
let studentNestedTurns = 0;
let studentNestedTasks = 0;
let studentNestedCostMicro = 0;
let teacherTokens = 0;
let teacherTasks = 0;
let teacherCostMicro = 0;
let teacherHrCount = 0;
let teacherHrWaitMs = 0;
const teacherHrGates = [];
let correctionFailures = 0;
let sessionId = studentSpec.sessionId;
let objective = studentSpec.objective;
const toolLog = [];
const logTool = (name, args, result) => {
toolLog.push({ tool: name, args: JSON.stringify(args).slice(0, 200), result: result.slice(0, 600) });
while (toolLog.length > 6)
toolLog.shift();
};
let errorStreak = 0;
let lastErrSig = "";
const onToolFail = (name, errText) => {
const sig = `${name}|${errText.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 80)}`;
errorStreak = sig === lastErrSig ? errorStreak + 1 : 1;
lastErrSig = sig;
};
const wrap = (t) => {
if (isDefineToolProduct(t)) {
const product = t;
return stampDefineToolBrand({
...t,
execute: async (toolCallId, rawParams, signal, onUpdate) => {
try {
const out = await product.execute(toolCallId, rawParams, signal, onUpdate);
const blocks = out && typeof out === "object" && "content" in out ? out.content : out;
const text = Array.isArray(blocks)
? blocks.map((b) => (b && typeof b === "object" && "text" in b ? String(b.text) : "")).join("\n")
: String(blocks ?? "");
logTool(t.name, rawParams, text);
errorStreak = 0;
lastErrSig = "";
return out;
}
catch (e) {
const errText = thrownLabel(e);
logTool(t.name, rawParams, `ERROR: ${errText}`);
onToolFail(t.name, errText);
throw e;
}
},
}, (enrich, _mounted) => wrap(rebindDefineToolCtx(product, enrich)));
}
return {
...t,
execute: async (args, ctx) => {
try {
const out = await t.execute(args, ctx);
const text = typeof out === "string"
? out
: out && typeof out === "object" && "content" in out
? String(out.content)
: (JSON.stringify(out) ?? "");
logTool(t.name, args, text);
errorStreak = 0;
lastErrSig = "";
return out;
}
catch (e) {
const errText = thrownLabel(e);
logTool(t.name, args, `ERROR: ${errText}`);
onToolFail(t.name, errText);
throw e;
}
},
};
};
const tools = studentSpec.tools?.map(wrap);
const inheritedDeploymentConfig = {
...(studentSpec.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: studentSpec.getApiKeyAndHeaders } : {}),
...(studentSpec.principal !== undefined ? { principal: studentSpec.principal } : {}),
...(studentSpec.clientContext !== undefined ? { clientContext: studentSpec.clientContext } : {}),
...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
...childClampSeats({ handsReadOnly: studentSpec.handsReadOnly, interactiveTools: studentSpec.interactiveTools, readFace: studentSpec.readFace, writeFace: studentSpec.writeFace, readDenyPatterns: studentSpec.readDenyPatterns }),
...(studentSpec.excludeTools !== undefined
? { excludeTools: Array.isArray(studentSpec.excludeTools) ? [...studentSpec.excludeTools] : studentSpec.excludeTools }
: {}),
...(studentSpec.deferTools !== undefined
? { deferTools: Array.isArray(studentSpec.deferTools) ? [...studentSpec.deferTools] : studentSpec.deferTools }
: {}),
...(studentSpec.alwaysLoadTools !== undefined
? { alwaysLoadTools: Array.isArray(studentSpec.alwaysLoadTools) ? [...studentSpec.alwaysLoadTools] : studentSpec.alwaysLoadTools }
: {}),
...(studentSpec.restoreGatedTools !== undefined
? { restoreGatedTools: Array.isArray(studentSpec.restoreGatedTools) ? [...studentSpec.restoreGatedTools] : studentSpec.restoreGatedTools }
: {}),
...(studentSpec.checkpointStore === "disabled" ? { checkpointStore: "disabled" } : {}),
};
const helperBase = () => teacher.helperModel
? { ...inheritedDeploymentConfig, model: teacher.helperModel }
: { ...inheritedDeploymentConfig, model: studentSpec.model, modelRole: studentSpec.modelRole, roles: studentSpec.roles };
const teacherModelFields = () => {
if (teacher.model) {
return { model: teacher.model };
}
const roles = {
...(studentSpec.model !== undefined ? { default: studentSpec.model } : {}),
...studentSpec.roles,
};
return { modelRole: "advisor", roles: Object.keys(roles).length ? roles : undefined };
};
const runHelper = async (helperObjective) => {
const r = await runner.runTask({
...helperBase(),
objective: helperObjective,
enableBlockedReport: false,
limits: { maxTurns: 1, ...(studentSpec.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: studentSpec.limits.maxWalltimeMs } : {}) },
signal: extSignal,
});
await runner.sessions.release(r.sessionId).catch(() => undefined);
addStudent(r);
return r.result;
};
const addStudent = (r) => {
const s = r.stats;
studentTokens += s.tokens;
studentTurns += s.turns;
studentCostMicro += s.costMicroUsd ?? 0;
if (s.costMicroUsd === undefined)
anyStudentLegUnpriced = true;
studentPrompt += s.promptTokens ?? 0;
studentTotalInput += s.totalInputTokens ?? 0;
studentCached += s.cachedTokens ?? 0;
studentOutput += s.outputTokens ?? 0;
studentCacheWrite += s.cacheWriteTokens ?? 0;
studentCacheWriteLong += s.cacheWriteTokensLong ?? 0;
if (s.costBreakdown) {
studentLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
studentCompactionMicro += s.costBreakdown.compactionMicroUsd;
}
if (s.humanReview) {
hrCount += s.humanReview.count;
hrWaitMs += s.humanReview.totalWaitMs;
hrGates.push(...s.humanReview.gates);
}
if (s.nested) {
studentNestedTokens += s.nested.tokens;
studentNestedTurns += s.nested.turns;
studentNestedTasks += s.nested.tasks;
studentNestedCostMicro += s.nested.costMicroUsd ?? 0;
}
};
const addTeacher = (r) => {
teacherTokens += r.stats.tokens + (r.stats.nested?.tokens ?? 0);
teacherTasks += 1;
teacherCostMicro += (r.stats.costMicroUsd ?? 0) + (r.stats.nested?.costMicroUsd ?? 0);
if (r.stats.humanReview) {
teacherHrCount += r.stats.humanReview.count;
teacherHrWaitMs += r.stats.humanReview.totalWaitMs;
teacherHrGates.push(...r.stats.humanReview.gates);
}
};
const runStudent = async () => {
const ac = new AbortController();
const onExt = () => ac.abort();
if (extSignal) {
if (extSignal.aborted)
ac.abort();
else
extSignal.addEventListener("abort", onExt, { once: true });
}
const stream = runner.runTaskStream({ ...studentSpec, tools, objective, sessionId, signal: ac.signal });
errorStreak = 0;
lastErrSig = "";
let grace = 0;
let stuck = false;
const recent = [];
let turnText = "";
let turnTools = [];
try {
for await (const ev of stream) {
if (ev.type === "text_delta") {
turnText += ev.delta;
}
else if (ev.type === "tool_start") {
turnTools.push(`${ev.toolName}(${JSON.stringify(ev.args).slice(0, 120)})`);
}
else if (ev.type === "turn_end") {
recent.push(`${turnText} ${turnTools.join(" ")}`.trim());
turnText = "";
turnTools = [];
if (errorStreak >= stuckHardOverride) {
stuck = true;
}
else if (grace > 0) {
grace--;
}
else if (errorStreak >= stuckThreshold) {
if (!useStuckMonitor || (await monitorStuck(recent))) {
stuck = true;
}
else {
grace = 2;
}
}
if (stuck) {
ac.abort();
break;
}
}
}
}
finally {
if (extSignal)
extSignal.removeEventListener("abort", onExt);
}
const settled = stream.result().catch(() => syntheticAborted(sessionId));
let result;
if (stuck) {
const fb = abortFallback(sessionId);
result = await Promise.race([settled, fb.promise]);
fb.cancel();
}
else {
result = await settled;
}
addStudent(result);
return { result, stuck, recent };
};
const syntheticAborted = (sid) => ({
taskId: sid ?? "aborted",
sessionId: sid ?? "aborted",
terminal: { kind: "failed", message: "student run did not settle promptly after stuck-abort" },
result: "",
stats: { turns: 0, tokens: 0, usageMissing: true },
});
const abortFallback = (sid) => {
let timer;
const promise = new Promise((res) => {
timer = setTimeout(() => res(syntheticAborted(sid)), 10_000);
});
return { promise, cancel: () => { if (timer)
clearTimeout(timer); } };
};
const monitorStuck = async (recent) => {
const turns = delimitUntrusted("RECENT TURNS", recent.slice(-2).join("\n---\n"));
const out = await runHelper(`${teacher.prompts?.monitor ?? MONITOR_PROMPT}\n\n${turns}`);
return /^\W*stuck\b/i.test(out.trim());
};
const verify = async (taskObjective, output) => {
const k = Math.max(1, Math.floor(teacher.verifierSamples ?? 1));
const votes = await Promise.all(Array.from({ length: k }, () => runHelper(verifierPrompt(taskObjective, output, teacher.prompts?.verifier))));
const passes = votes.filter((v) => /^\s*pass\b/i.test(v)).length;
const pass = passes * 2 > k;
const reason = pass ? "" : (votes.find((v) => !/^\s*pass\b/i.test(v)) ?? "").replace(/^\s*fail\s*:?\s*/i, "").trim();
return { pass, reason };
};
const askTeacher = async (trace) => {
const r = await runner.runTask({
objective: trace,
...inheritedDeploymentConfig,
...teacherModelFields(),
systemPrompt: teacher.prompts?.teacher ?? TEACHER_PROMPT,
enableBlockedReport: false,
limits: { maxTurns: 4, ...(studentSpec.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: studentSpec.limits.maxWalltimeMs } : {}) },
signal: extSignal,
});
await runner.sessions.release(r.sessionId).catch(() => undefined);
addTeacher(r);
return parseTeacherAdvice(r.result);
};
for (;;) {
toolLog.length = 0;
const { result, stuck, recent } = await runStudent();
sessionId = result.sessionId;
if (isDurablePause(result.terminal)) {
return finalize(mapNestedSuspend(result));
}
let trigger;
let verifyReason = "";
if (stuck) {
trigger = "stuck";
}
else if (result.terminal.kind === "blocked") {
trigger = "blocked";
}
else if (result.terminal.kind === "failed") {
trigger = "failed";
}
else if (!verifyOutput) {
trigger = "success";
}
else {
const v = await verify(studentSpec.objective, result.result);
if (v.pass) {
trigger = "success";
}
else {
trigger = "verify-fail";
verifyReason = v.reason;
}
}
if (trigger === "success") {
return finalize(result);
}
if (trigger === "failed" &&
result.terminal.kind === "failed" &&
/truncat|max[ _-]?tokens?|maximum tokens?|token (limit|budget)|context (length|window)|no answer text|(consumed|exceeded).{0,12}budget/i.test(result.terminal.message ?? "")) {
return finalize(result);
}
if (escalations.length >= maxEscalations) {
return finalize(result);
}
const studentTotal = studentTokens + studentNestedTokens;
if (escalations.length > 0 && studentTotal > 0 && teacherTokens / studentTotal > spendCap) {
return finalize(result);
}
if (teacher.escalationPolicy) {
let go = true;
try {
go = await teacher.escalationPolicy({ trigger, attempt: escalations.length + 1, result, recent, verifyReason: verifyReason || undefined });
}
catch {
go = true;
}
if (!go) {
return finalize(result);
}
}
const observations = toolLog.slice(-4).map((e) => `${e.tool}(${e.args}) → ${e.result}`).join("\n");
const studentTurnsText = delimitUntrusted("STUDENT RECENT TURNS", recent.slice(-4).join("\n---\n") || result.result || "(no output)");
const toolResults = delimitUntrusted("RECENT TOOL RESULTS", observations || "(no tool calls)");
const trace = `The student is stuck on this task and needs guidance.\n\nORIGINAL TASK:\n${studentSpec.objective}\n\n` +
`TRIGGER: ${trigger}${verifyReason ? ` (${inlineUntrusted(verifyReason, 300)})` : ""}\n\n` +
`STUDENT'S RECENT TURNS (most recent last):\n${studentTurnsText}\n\n` +
`RECENT TOOL RESULTS (what the tools actually returned):\n${toolResults}\n\n` +
`Give your guidance as the required JSON.`;
const advice = await askTeacher(trace);
const tookOver = advice.takeover || correctionFailures >= takeoverAfter;
const record = { attempt: escalations.length + 1, trigger, teacher: advice, tookOver };
escalations.push(record);
teacher.onEscalation?.(record);
if (tookOver) {
const r = await runner.runTask({
...studentSpec,
sessionId,
objective: `Take over and complete the original task. ${advice.nextStep ?? advice.strategy ?? ""}`.trim(),
...teacherModelFields(),
tools,
enableBlockedReport: false,
signal: extSignal,
});
addTeacher(r);
if (isDurablePause(r.terminal)) {
return finalize(mapNestedSuspend(r));
}
return finalize(r);
}
const verifyClause = studentSpec.tools && studentSpec.tools.length > 0
? " First verify this guidance against the actual tool output, then continue completing the original task."
: " Then continue completing the original task.";
objective =
`[Teacher guidance] Correction: ${advice.correction ?? "(none)"}. Next step: ${advice.nextStep ?? "(none)"}.` +
(advice.strategy ? ` Strategy: ${advice.strategy}.` : "") +
verifyClause;
correctionFailures++;
}
function finalize(result) {
return {
...result,
stats: buildCumulativeStats(result.stats, {
tokens: studentTokens,
turns: studentTurns,
costMicroUsd: studentCostMicro || undefined,
anyLegUnpriced: anyStudentLegUnpriced,
promptTokens: studentPrompt,
totalInputTokens: studentTotalInput,
cachedTokens: studentCached,
outputTokens: studentOutput,
cacheWriteTokens: studentCacheWrite,
cacheWriteTokensLong: studentCacheWriteLong,
llmRootMicroUsd: studentLlmRootMicro,
compactionMicroUsd: studentCompactionMicro,
nested: {
tokens: studentNestedTokens,
turns: studentNestedTurns,
tasks: studentNestedTasks,
costMicroUsd: studentNestedCostMicro,
},
humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates },
}),
escalations,
teacherStats: {
tokens: teacherTokens,
tasks: teacherTasks,
costMicroUsd: teacherCostMicro || undefined,
humanReview: teacherHrCount > 0 ? { count: teacherHrCount, totalWaitMs: teacherHrWaitMs, gates: teacherHrGates } : undefined,
},
};
}
}