UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

345 lines (344 loc) • 17.7 kB
import { uuidv7 } from "../internal/harness.js"; import { summarizeWorkflowRun } from "./workflow-run-store.js"; import { delimitUntrusted } from "./untrusted-text.js"; import { boundedRedactedSummary } from "./untrusted-egress.js"; import { clipWithFilePointer, OUTPUT_BUDGET_MIN_CHARS } from "./tool-errors.js"; import { resolveEnvBudget } from "./env-budget.js"; export function mintCompletionId(target) { if (target.completionId === undefined) target.completionId = uuidv7(); return target.completionId; } export const defaultMonitorTimers = { setInterval: (fn, ms) => { const h = setInterval(() => void fn(), ms); h.unref?.(); return h; }, clearInterval: (h) => clearInterval(h), now: () => Date.now(), }; const BLOCK_POLL_INTERVAL_MS = 400; export const sleepPollStep = (deadline, signal) => sleepMs(deadline === undefined ? BLOCK_POLL_INTERVAL_MS : Math.max(1, Math.min(BLOCK_POLL_INTERVAL_MS, deadline - Date.now())), signal); export const sleepMs = (ms, signal) => new Promise((resolve) => { const t = setTimeout(done, ms); function done() { clearTimeout(t); signal?.removeEventListener("abort", done); resolve(); } signal?.addEventListener("abort", done, { once: true }); }); export const MONITOR_BATCH_WINDOW_MS = 200; export const MONITOR_DEFAULT_TIMEOUT_MS = 300_000; export const MONITOR_MAX_TIMEOUT_MS = 3_600_000; export const MONITOR_MAX_BATCHES_PER_MINUTE = 50; export const MONITOR_STORM_BURST = 10; export const MONITOR_STORM_KILL_AFTER_MS = 30_000; const MONITOR_EVENT_RESULT_CAP = 2_000; const MONITOR_EVENT_LINE_CAP = 500; export const MONITOR_LINE_BUF_CAP = 1_048_576; export function clipMonitorEvent(s) { return s.length <= MONITOR_EVENT_RESULT_CAP ? s : `${s.slice(0, MONITOR_EVENT_RESULT_CAP)}\n…[+${s.length - MONITOR_EVENT_RESULT_CAP} chars truncated]`; } export function clipMonitorLine(s) { return s.length <= MONITOR_EVENT_LINE_CAP ? s : `${s.slice(0, MONITOR_EVENT_LINE_CAP)}...(truncated)`; } export function terminalTaskSummary(kind, label, status, exitCode) { if (kind === "monitor") { if (status === "completed") return `Monitor "${label}" stream ended`; if (status === "failed") return `Monitor "${label}" script failed${exitCode !== undefined ? ` (exit ${exitCode})` : ""}`; return `Monitor "${label}" stopped`; } if (status === "completed") return `Background command "${label}" completed${exitCode !== undefined ? ` (exit code ${exitCode})` : ""}`; if (status === "failed") return `Background command "${label}" failed${exitCode !== undefined ? ` with exit code ${exitCode}` : ""}`; return `Background command "${label}" was stopped`; } const TASK_OUTPUT_DEFAULT_CHARS = 32_000; export const TASK_OUTPUT_MAX_CHARS = 160_000; function taskMaxOutputChars() { return resolveEnvBudget(process.env.TASK_MAX_OUTPUT_LENGTH, { name: "TASK_MAX_OUTPUT_LENGTH", unit: "characters", default: TASK_OUTPUT_DEFAULT_CHARS, min: OUTPUT_BUDGET_MIN_CHARS, max: TASK_OUTPUT_MAX_CHARS }); } export function clipTaskOutput(s, fullOutputPath) { return clipWithFilePointer(s, taskMaxOutputChars(), fullOutputPath); } export function statusFromBackground(status, exitCode) { if (status === "running") return "running"; if (status === "killed") return "killed"; if (status === "failed") return "failed"; if (status === "exited") return exitCode === 0 ? "completed" : "failed"; return "failed"; } export function rollSpoolText(spool, s, cap, onDrop) { if (s.length <= cap) return s; const half = Math.floor(cap / 2); const dropped = s.slice(half, s.length - half); spool.rolledChars += dropped.length; onDrop?.(dropped); return s.slice(0, half) + s.slice(s.length - half); } export const MONITOR_SPILL_CAP_CHARS = 64 * 1024 * 1024; export function accountDroppedBytes(spool, poll) { const d = poll.bytesDroppedBeforeCursor ?? 0; if (d > 0) spool.droppedBytes = (spool.droppedBytes ?? 0) + d; else if (poll.truncated === true) spool.dropUnknown = true; } export function spoolDropNote(acct) { if ((acct.droppedBytes ?? 0) > 0) return `[!] ${acct.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`; if (acct.dropUnknown === true) return "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"; return ""; } export const TASK_OUTPUT_REPLAY_KINDS = Object.freeze(["full", "cursor"]); export const REPLAY_FACE = { full: { stdout: "stdout (full, re-readable)", stdoutWhenGapped: "stdout (re-readable spool — INCOMPLETE, see note above)", stderr: "stderr" }, cursor: { stdout: "new stdout", stdoutWhenGapped: undefined, stderr: "new stderr" }, }; export const SPOOL_REPLAY = "full"; export const INCREMENT_REPLAY = "cursor"; export function stdoutHeadingOf(replay, gapDisclosed) { const face = REPLAY_FACE[replay]; return gapDisclosed ? (face.stdoutWhenGapped ?? face.stdout) : face.stdout; } export function renderSpoolBody(spool, opts) { const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : ""; const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : opts.running ? "running" : opts.handleStatus) + timedOutClause; const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${opts.rolledExtraClause}\n` : ""; const droppedNote = spoolDropNote(spool); const stdoutHeading = stdoutHeadingOf(SPOOL_REPLAY, droppedNote !== "" || opts.drainNote !== ""); return (`status: ${displayStatus}\n${rolledNote}${droppedNote}${opts.drainNote}--- ${stdoutHeading} ---\n${opts.shape(spool.stdout) || "(none)"}\n` + `--- ${REPLAY_FACE[SPOOL_REPLAY].stderr} ---\n${opts.shape(spool.stderr) || "(none)"}`); } export function droppedGapNote(spool) { if ((spool.droppedBytes ?? 0) > 0) return ` [!] ${spool.droppedBytes} earlier byte(s) of output were permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.`; if (spool.dropUnknown === true) return " [!] An unknown amount of earlier output was permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE."; return ""; } export function firstDropNote(spool) { if (spool.dropNotified === true) return ""; const note = droppedGapNote(spool); if (note !== "") spool.dropNotified = true; return note; } export function alreadyTerminalStopNote(id, status) { return `${id} already ${status} — nothing to stop; poll TaskOutput for its final state.`; } export function assertOwnership(input, site) { if (input.owner === undefined || input.scope === undefined) { throw new Error(`TaskRegistry.${site}: owner AND scope are both required (access is default-deny on both axes; use scope "default" for a single-tenant deployment)`); } } export function canAccessWorkflowRun(run, access) { if (access.scope === undefined || run.scope !== access.scope) return false; if (access.owner !== undefined && run.sourceTaskId !== undefined && run.sourceTaskId === access.owner) return true; if (access.sessionId !== undefined && run.originatingSessionId !== undefined && run.originatingSessionId === access.sessionId) return true; return false; } const WORKFLOW_PARTIAL_RESULTS_MAX = 5; const WORKFLOW_AGENT_ROWS_MAX = 60; function workflowAgentRow(a, ordinal) { return { ordinal, label: a.label, ...(a.phase !== undefined ? { phase: a.phase } : {}), status: a.status, ...(a.errorCode !== undefined ? { errorCode: a.errorCode } : {}), ...(a.errorMessage !== undefined ? { error: delimitUntrusted(`workflow agent ${a.label} error`, boundedRedactedSummary(a.errorMessage, 200)) } : {}), ...(a.startedAt !== undefined && a.endedAt !== undefined ? { elapsedMs: a.endedAt - a.startedAt } : {}), ...(a.replayed === true ? { replayed: true } : {}), }; } function budgetOvershootNote(overshoot) { if (overshoot === undefined) return ""; const unsettled = overshoot.unsettledTokens ?? 0; const total = overshoot.spentTokens + unsettled; return (` token budget OVERSHOT: this run spent ${total.toLocaleString()} total tokens against a ${overshoot.budgetTokens.toLocaleString()} ceiling ` + `(over by ${(total - overshoot.budgetTokens).toLocaleString()}` + `${unsettled > 0 ? `, of which ${unsettled.toLocaleString()} was observed on agents still in flight at the terminal and never settled` : ""}).`); } function timeoutInterruptionNote(interruption) { if (interruption === undefined) return ""; return (` INTERRUPTED by the workflow's total timeout (${interruption.timeoutMs.toLocaleString()}ms): when the deadline fired ` + `${interruption.agentsCompleted} agent(s) had completed, ${interruption.agentsFailed} had failed, and ` + `${interruption.agentsInFlight} were still in flight — the failed run status is the deadline's, not a verdict on those agents.` + (interruption.approvalWaitedMs !== undefined ? ` The longest single agent wait on a human approval reported for this run was ${interruption.approvalWaitedMs.toLocaleString()}ms` + (interruption.approvalWaitedMs > interruption.timeoutMs ? ` — MORE than the ${interruption.timeoutMs.toLocaleString()}ms window itself, so the deployment's approval clock is` + ` counting time this run did not (an earlier leg's wait carried forward, or a mis-scaled figure). Read it as` + ` "waiting dominated", not as a slice of this window.` : ` — that much of the ${interruption.timeoutMs.toLocaleString()}ms window was one agent BLOCKED on a person` + ` rather than working (the longest single wait, not a total).`) + ` Raising the timeout only helps if that approval gets answered.` : "")); } export function formatWorkflowRun(run) { const summary = summarizeWorkflowRun(run); const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length; const failed = run.agents.filter((a) => a.status === "failed").length; const allRows = run.agents.map((a, i) => ({ a, i })); const overCap = allRows.length > WORKFLOW_AGENT_ROWS_MAX; const keptRows = overCap ? (() => { const urgent = allRows.filter((r) => r.a.status !== "completed"); const budget = Math.max(0, WORKFLOW_AGENT_ROWS_MAX - urgent.length); const fill = allRows.filter((r) => r.a.status === "completed").slice(0, budget); return [...urgent, ...fill].sort((x, y) => x.i - y.i); })() : allRows; const agentRows = keptRows.map((r) => workflowAgentRow(r.a, r.i)); const finishedAgents = run.status === "failed" ? run.agents.filter((a) => a.status === "completed" && a.output !== undefined && a.output !== "") : []; return { content: JSON.stringify({ task_id: run.id, runId: run.id, type: "workflow", status: run.status, ...(summary.name ? { name: summary.name } : {}), ...(summary.currentPhase ? { currentPhase: summary.currentPhase } : {}), agents: { done, total: run.agents.length, failed }, agent_runs: agentRows, ...(overCap ? { agent_runs_note: `showing ${agentRows.length} of ${run.agents.length} agent rows (every non-completed row kept; completed rows trimmed).` } : {}), startedAt: run.startedAt, ...(run.endedAt !== undefined ? { endedAt: run.endedAt, elapsedMs: run.endedAt - run.startedAt } : {}), ...(run.resume !== undefined ? { resume: { from_run_id: run.resume.fromRunId, journal_entries: run.resume.journalEntries, replayed: run.resume.replayed, ...(run.resume.divergedAtOrdinal !== undefined ? { replay_stopped_at: run.resume.divergedAtOrdinal, replay_stopped_reason: run.resume.divergedReason } : {}), }, } : {}), ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}), ...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}), ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}), ...(run.status === "completed" && run.result !== undefined ? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) } : {}), ...(finishedAgents.length > 0 ? { partial_results: finishedAgents.slice(0, WORKFLOW_PARTIAL_RESULTS_MAX).map((a) => ({ agent: a.label, output: delimitUntrusted(`workflow agent ${a.label} output`, a.output), })), ...(finishedAgents.length > WORKFLOW_PARTIAL_RESULTS_MAX ? { partial_results_note: `showing ${WORKFLOW_PARTIAL_RESULTS_MAX} of ${finishedAgents.length} completed agent outputs; the full set remains retrievable from the workflow run record (getWorkflowRun / the workflow journal).` } : {}), } : {}), note: (run.status === "running" ? "still running — poll again shortly." : run.status === "completed" ? run.result !== undefined ? "completed — `result` above is the workflow's return value." : "completed — this run predates result persistence; its result was delivered on the completion notification." : finishedAgents.length > 0 ? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.` : "failed — see error.") + timeoutInterruptionNote(summary.timeoutInterruption) + budgetOvershootNote(summary.budgetOvershoot), }), details: { task_id: run.id, type: "workflow", status: run.status, retrieval_status: "success", ...(run.completionId !== undefined ? { completionId: run.completionId } : {}), ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}), details: summary, }, }; } export const DURABLE_AGENT_HEARTBEAT_MS = 60_000; export const DURABLE_AGENT_HANDLE_RE = /^a[0-9a-f]{6,32}$/; export const BG_AGENT_REAP_STOP_ERROR = "stopped (parent task ended or TaskStop)"; export function normalizeAgentName(s) { return s .normalize("NFKC") .replace(/[\p{Cc}\p{Cf}]/gu, (t) => (/\s/.test(t) ? t : "")) .trim() .toLowerCase() .replace(/\s+/g, "-"); } export function editDistance(a, b) { if (a === b) return 0; const m = a.length; const n = b.length; const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) { d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1); } } } return d[m][n]; } export function closestName(query, candidates) { let best; let bestDist = 3; const seen = new Set(); for (const c of candidates) { if (seen.has(c)) continue; seen.add(c); if (Math.abs(c.length - query.length) > 2) continue; const dist = editDistance(query, c); if (dist <= 2 && dist < bestDist) { bestDist = dist; best = c; } } return best; } export function canAccess(handle, access) { if (handle.scope === undefined || access.scope === undefined || access.scope !== handle.scope) return false; if (handle.owner === undefined) return false; if (access.owner === handle.owner) return true; if (handle.sessionScoped && access.sessionId !== undefined && access.sessionId === handle.owner) return true; if (handle.type === "workflow" && access.sessionId !== undefined && handle.originatingSessionId === access.sessionId) return true; if (handle.type === "background_agent" && access.sessionId !== undefined && handle.rootSessionId === access.sessionId) return true; return false; } export function commitCompletionIdIfEmpty(target, value) { if (target.completionId === undefined) target.completionId = value; return target.completionId; }