UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

185 lines (184 loc) • 7.38 kB
import { Value } from "typebox/value"; const MAX_ERROR_CHARS = 10_000; const HALF = MAX_ERROR_CHARS / 2; import { isHighSurrogate, isLowSurrogate, sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js"; import { isErrorLike, readMember, thrownName, thrownText } from "./thrown-value.js"; export function truncateError(s) { if (s.length <= MAX_ERROR_CHARS) return s; const head = sliceHeadSafe(s, HALF); const tail = sliceTailSafe(s, HALF); const removed = s.length - head.length - tail.length; return `${head}\n\n... [${removed} characters truncated] ...\n\n${tail}`; } export const OUTPUT_BUDGET_MIN_CHARS = 512; export function clipWithFilePointer(s, limit, fullOutputPath) { if (s.length <= limit) return s; if (fullOutputPath !== undefined) { const header = `[Truncated. Full output: ${fullOutputPath}]\n\n`; const room = limit - header.length; return room > 0 ? header + s.slice(-room) : header.trimEnd().slice(0, Math.min(limit, s.length)); } const omittedCount = (kept) => s.length - kept; const markerFor = (kept) => `\n…[${s.length} chars total, ${omittedCount(kept)} omitted from the middle]…\n`; const budget = limit - markerFor(0).length; if (budget < 2) return s.slice(0, limit); const half = Math.floor(budget / 2); const headEnd = half > 0 && isHighSurrogate(s.charCodeAt(half - 1)) ? half - 1 : half; const tailStart = half > 0 && isLowSurrogate(s.charCodeAt(s.length - half)) ? s.length - half + 1 : s.length - half; const kept = headEnd + (s.length - tailStart); return `${s.slice(0, headEnd)}${markerFor(kept)}${s.slice(tailStart)}`; } function isShellLike(e) { try { return typeof e === "object" && e !== null && ("exitCode" in e || "stderr" in e || "stdout" in e); } catch { return false; } } export function formatToolError(error) { const parts = []; const name = thrownName(error); if (name === "AbortError") return "Interrupted"; const message = readMember(error, "message"); const messageText = typeof message === "string" ? message : undefined; if (isShellLike(error)) { const exitCode = readMember(error, "exitCode"); const stderr = readMember(error, "stderr"); const stdout = readMember(error, "stdout"); if (typeof exitCode === "number") parts.push(`Exit code ${exitCode}`); if (isErrorLike(error) && messageText !== undefined && messageText !== "") parts.push(messageText); if (typeof stderr === "string" && stderr.trim()) parts.push(stderr); if (typeof stdout === "string" && stdout.trim()) parts.push(stdout); } else if (isErrorLike(error)) { parts.push((messageText ?? "") || (name ?? "") || "Error"); } else { parts.push(thrownText(error)); } if (parts.length === 0) parts.push(thrownText(error)); return truncateError(parts.join("\n").trim() || "Error"); } const BRAIN_FLAT_CODES = new Set([ "auth", "rate_limit", "invalid_request", "server", "network", "http", "stream_torn", "refusal", "length_empty", ]); const EXACT_CODE_CLASS = new Map([ ["limits.max_tokens_exceeded", "budget"], ["limits.max_cost_exceeded", "budget"], ["limits.max_turns_exceeded", "limit"], ["limits.max_walltime_exceeded", "limit"], ["memory.admission_required", "config"], ["memory.admission_denied", "config"], ["memory.capture_optout_denied", "config"], ["env.lifetime_expired", "config"], ]); const DOTTED_PREFIX_CLASS = [ ["limits.", "limit"], ["output.", "output"], ["suspend.", "suspend"], ["review.", "review"], ["unexpected.", "unexpected"], ["oracle.", "oracle"], ["resume.", "resume"], ["config.", "config"], ["usage.", "config"], ]; export function errorClassOf(errorCode) { if (!errorCode) return "unknown"; const exact = EXACT_CODE_CLASS.get(errorCode); if (exact !== undefined) return exact; for (const [prefix, cls] of DOTTED_PREFIX_CLASS) { if (errorCode.startsWith(prefix)) return cls; } if (errorCode === "conflict") return "conflict"; if (errorCode === "aborted") return "aborted"; if (BRAIN_FLAT_CODES.has(errorCode)) return "brain"; return "unknown"; } export function formatValidationPath(path) { const segs = path.split("/").filter((s) => s.length > 0); let out = ""; for (const seg of segs) { if (/^\d+$/.test(seg)) out += `[${seg}]`; else out += out === "" ? seg : `.${seg}`; } return out; } function locateInvalidParam(schema, value, prefix) { const s = schema; if (s.type === "object" && s.properties && value !== null && typeof value === "object" && !Array.isArray(value)) { const obj = value; for (const name of s.required ?? []) { if (!(name in obj)) return { accessor: prefix ? `${prefix}.${name}` : name, missing: true }; } for (const [name, sub] of Object.entries(s.properties)) { if (name in obj && !Value.Check(sub, obj[name])) { return locateInvalidParam(sub, obj[name], prefix ? `${prefix}.${name}` : name); } } } else if (s.type === "array" && s.items && Array.isArray(value)) { for (let i = 0; i < value.length; i++) { if (!Value.Check(s.items, value[i])) return locateInvalidParam(s.items, value[i], `${prefix}[${i}]`); } const a = s; if (typeof a.minItems === "number" && value.length < a.minItems) { const bound = typeof a.maxItems === "number" ? `between ${a.minItems} and ${a.maxItems}` : `at least ${a.minItems}`; return { accessor: prefix, type: `an array with ${bound} items (got ${value.length})` }; } if (typeof a.maxItems === "number" && value.length > a.maxItems) { const bound = typeof a.minItems === "number" ? `between ${a.minItems} and ${a.maxItems}` : `at most ${a.maxItems}`; return { accessor: prefix, type: `an array with ${bound} items (got ${value.length})` }; } } return { accessor: prefix, type: s.type }; } export function formatZodValidationError(schema, value) { const s = schema; if (s.type === "object" && s.properties && typeof value === "object" && value !== null) { const obj = value; for (const name of s.required ?? []) { if (!(name in obj)) return `The required parameter \`${name}\` is missing.`; } for (const [name, sub] of Object.entries(s.properties)) { if (name in obj && !Value.Check(sub, obj[name])) { const loc = locateInvalidParam(sub, obj[name], name); if (loc.missing) return `The required parameter \`${loc.accessor}\` is missing.`; return `The parameter \`${loc.accessor}\` is invalid${loc.type ? `: expected ${loc.type}` : ""}.`; } } } const errs = [...Value.Errors(schema, value)]; const msg = errs[0]?.message?.trim(); return msg ? `Invalid arguments: ${msg}.` : "The arguments do not match the tool's parameter schema."; }