UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

87 lines (86 loc) • 5.06 kB
import { deliverEngineNotice } from "../engine-notice.js"; import { isProviderEchoShape } from "../../brain/errors.js"; import { streamDropsOf } from "../../internal/llm.js"; import { readMember } from "../thrown-value.js"; import { modelOutputInvalidOfMessage } from "./terminal-account.js"; const FORM_SENTENCE = { trailing_text: "a block after an assistant's tool calls was moved ahead of them", late_result: "a result recorded later was moved up behind its call", missing_result: "a call with no result was closed as interrupted (outcome unknown)", duplicate_result: "a second result recorded for one call was folded into the first (an interrupted close beside a recorded result is left out)", duplicate_call: "a second call with the same id in one assistant message was left out (its results answer the first)", orphan_result: "a result that answers no call before it was left out", }; const afterDroppedCalls = (outcome) => outcome === "reissued" ? "; the model was asked to re-issue them" : outcome === "terminal" ? "; the run ended with this turn (model_output_invalid)" : ""; const INCOMPLETE_SENTENCE = { truncated: (i) => `${i.count} tool call(s) the model made were cut off before their arguments were complete, so they did not run${afterDroppedCalls(i.outcome)}`, unnamed: (i) => `${i.count} tool call(s) the model made arrived with no tool name, so they did not run${afterDroppedCalls(i.outcome)}`, bad_frame: (i) => `${i.count} frame(s) of the model's streamed response could not be read and were dropped, so the response may be missing content; the model was told`, missing_result_synthesized: (i) => `${i.count} earlier tool call(s) have no recorded result, so the model was told their outcome is unknown`, }; export function turnCarriesToolCall(content) { try { return Array.isArray(content) && content.some((c) => c?.type === "toolCall"); } catch { return false; } } function outcomeOf(message) { const stop = readMember(message, "stopReason"); if (stop === "error") return modelOutputInvalidOfMessage(message) !== undefined ? "terminal" : undefined; if (stop === "aborted") return undefined; return turnCarriesToolCall(readMember(message, "content")) || stop === "toolUse" || stop === "length" ? "reissued" : undefined; } export function createToolTurnIncompleteSeat(onNotice, ids) { const announced = new Set(); const announce = (item) => { if (announced.has(item.cause)) return; announced.add(item.cause); const shown = "toolCallIds" in item ? item.toolCallIds.filter((id) => isProviderEchoShape(id)) : []; deliverEngineNotice(onNotice, { code: "task.tool_turn_incomplete", message: `${INCOMPLETE_SENTENCE[item.cause](item)}. Announced once per run for this cause.`, detail: { cause: item.cause, count: item.count, ...("outcome" in item && item.outcome !== undefined ? { outcome: item.outcome } : {}), ...(shown.length > 0 ? { toolCallIds: shown } : {}), sessionId: ids.sessionId, runId: ids.runId }, }); }; return { announce, observe(message) { const drops = streamDropsOf(message); if (drops.length === 0) return; const outcome = outcomeOf(message); for (const drop of drops) announce(drop.cause === "bad_frame" ? { cause: "bad_frame", count: drop.count } : { cause: drop.cause, count: drop.count, outcome }); }, }; } export function createToolTurnRepairSeat(onNotice, ids) { const announced = new Set(); const incomplete = createToolTurnIncompleteSeat(onNotice, ids); return (repairs) => { const missing = repairs.filter((repair) => repair.form === "missing_result" && repair.beforeRun !== true).flatMap((repair) => repair.toolCallIds); if (missing.length > 0) incomplete.announce({ cause: "missing_result_synthesized", count: missing.length, toolCallIds: missing }); const fresh = repairs.filter((repair) => { const key = `${repair.form}\u0000${[...repair.toolCallIds].sort().join("\u0000")}`; if (announced.has(key)) return false; announced.add(key); return true; }); if (fresh.length === 0) return; deliverEngineNotice(onNotice, { code: "context.tool_turn_repaired", message: `The session transcript carries ${fresh.length === 1 ? "a tool turn" : `${fresh.length} tool turns`} in a shape a provider lane ` + `rejects, so the request was repaired on its way out: ${[...new Set(fresh.map((r) => FORM_SENTENCE[r.form]))].join("; ")}. ` + `The transcript is unchanged and every later request is repaired the same way; announced once per run.`, detail: { repairs: fresh.map((r) => ({ form: r.form, toolCallIds: [...r.toolCallIds] })), sessionId: ids.sessionId, runId: ids.runId }, }); }; }