@sema-agent/core
Version:
Stateless, task-oriented AI agent core
409 lines (408 loc) • 17.8 kB
JavaScript
import { createHash } from "node:crypto";
import { Type } from "typebox";
import { uuidv7 } from "../engine/session/uuid.js";
import { defineTool, errorResult } from "./tools.js";
import { deliverEngineNotice } from "./types.js";
import { TOOL_SEARCH_NAME } from "./runner/tool-disclosure.js";
import { toolFace } from "./tool-catalog-entries.js";
import { thrownText } from "./thrown-value.js";
export const TOOL_RESULT_REF_CONFLICT_CODE = "tool_result.ref_conflict";
export class ToolResultRefConflictError extends Error {
ref;
stored;
incoming;
code = TOOL_RESULT_REF_CONFLICT_CODE;
constructor(ref, stored, incoming) {
super(`tool-result store: ref ${JSON.stringify(ref)} already belongs to a different origin ` +
`(stored ${describeProvenance(stored)}, offered ${describeProvenance(incoming)}) — refusing to store under it`);
this.ref = ref;
this.stored = stored;
this.incoming = incoming;
this.name = "ToolResultRefConflictError";
}
}
function describeProvenance(p) {
return p.taskId === undefined ? `session=${p.sessionId}` : `session=${p.sessionId} task=${p.taskId}`;
}
export function assertToolResultProvenanceMatch(ref, stored, incoming) {
if (stored === undefined || incoming === undefined)
return;
if (stored.sessionId === incoming.sessionId && stored.taskId === incoming.taskId)
return;
throw new ToolResultRefConflictError(ref, stored, incoming);
}
export function normalizeToolResultProvenance(p) {
return p.taskId === undefined ? { sessionId: p.sessionId } : { sessionId: p.sessionId, taskId: p.taskId };
}
export function assertSafeToolResultRef(ref) {
const bad = ref === "" ||
ref === "." ||
ref === ".." ||
ref.includes("/") ||
ref.includes("\\") ||
[...ref].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f);
if (bad)
throw new Error(`tool-result store: unsafe ref ${JSON.stringify(ref)}`);
}
const NATIVE_REF_CHARSET = /^[A-Za-z0-9_.-]+$/;
const MAX_REF_SEGMENT_CHARS = 128;
const REF_SEGMENT_SEPARATOR = "~";
const FOLDED_SEGMENT_PREFIX = "fold.";
export function buildToolResultRef(sessionId, toolCallId, ...extraSegments) {
return [`tr_${refSegment(sessionId)}`, refSegment(toolCallId), ...extraSegments.map(refSegment)].join(REF_SEGMENT_SEPARATOR);
}
function refSegment(raw) {
if (raw.length <= MAX_REF_SEGMENT_CHARS && NATIVE_REF_CHARSET.test(raw) && !raw.startsWith(FOLDED_SEGMENT_PREFIX))
return raw;
const base = raw.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 32);
return `${FOLDED_SEGMENT_PREFIX}${base || "x"}-${digestOfString(raw)}`;
}
export const MAX_MINTED_TOOL_RESULT_REF_CHARS = 3 + 4 * MAX_REF_SEGMENT_CHARS + 3;
export function toolResultContentSegment(text) {
return `s${digestOfString(text).slice(0, 32)}`;
}
function digestOfString(s) {
return createHash("sha256").update(Buffer.from(s, "utf16le")).digest("hex");
}
export class InMemoryToolResultStore {
opts;
retention = "none";
map = new Map();
totalChars = 0;
constructor(opts) {
this.opts = opts;
const cap = opts?.maxTotalChars;
if (cap !== undefined && Number.isNaN(cap)) {
throw new Error("InMemoryToolResultStore: maxTotalChars must be a number (got NaN) — omit it for an unbounded store");
}
}
put(ref, content, provenance) {
assertSafeToolResultRef(ref);
const existing = this.map.get(ref);
if (existing !== undefined) {
assertToolResultProvenanceMatch(ref, existing.provenance, provenance);
return;
}
this.map.set(ref, provenance === undefined ? { content } : { content, provenance: normalizeToolResultProvenance(provenance) });
this.totalChars += content.length;
const cap = this.opts?.maxTotalChars;
if (cap !== undefined && Number.isFinite(cap)) {
for (const [oldRef, oldEntry] of this.map) {
if (this.totalChars <= cap || oldRef === ref)
break;
this.map.delete(oldRef);
this.totalChars -= oldEntry.content.length;
}
}
}
ownerOf(ref) {
return this.map.get(ref)?.provenance;
}
deleteBySession(sessionId) {
let deleted = 0;
let unattributable = 0;
for (const [ref, entry] of this.map) {
if (entry.provenance === undefined) {
unattributable++;
continue;
}
if (entry.provenance.sessionId !== sessionId)
continue;
this.map.delete(ref);
this.totalChars -= entry.content.length;
deleted++;
}
return { deleted, unattributable };
}
isEmpty() {
return this.map.size === 0;
}
get(ref, opts) {
const full = this.map.get(ref)?.content;
if (full === undefined)
return undefined;
const offset = Math.min(full.length, Math.max(0, intOr(opts?.offset, 0)));
const limit = opts?.limit != null ? Math.max(0, intOr(opts.limit, 0)) : undefined;
const content = limit != null ? full.slice(offset, offset + limit) : full.slice(offset);
return { content, offset, totalChars: full.length };
}
}
export class RunnerSharedToolResultStore extends InMemoryToolResultStore {
}
export class ScopedToolResultStore {
inner;
scope;
localPuts = 0;
volatileBacking;
constructor(inner, scope) {
this.inner = inner;
this.scope = scope;
this.volatileBacking = inner instanceof InMemoryToolResultStore;
}
key(ref) {
const scope = encodeScopeSegment(this.scope);
return `${scope.length}:${scope}:${ref}`;
}
put(ref, content, provenance) {
assertSafeToolResultRef(ref);
this.localPuts++;
return this.inner.put(this.key(ref), content, provenance);
}
get(ref, opts) {
return this.inner.get(this.key(ref), opts);
}
ownerOf(ref) {
return this.inner.ownerOf?.(this.key(ref));
}
isEmpty() {
return this.localPuts === 0;
}
}
function encodeScopeSegment(scope) {
let out = "";
for (let i = 0; i < scope.length; i++) {
const ch = scope[i];
out += /[A-Za-z0-9]/.test(ch) ? ch : `%${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
}
return out;
}
export function isVolatileOffloadStore(store) {
if (store instanceof ScopedToolResultStore)
return store.volatileBacking;
return store instanceof InMemoryToolResultStore;
}
export const OFFLOAD_TOOL_NAME = "ReadToolResult";
export function createOffloadPersist(store, sessionId, onNotice) {
const provenance = toolResultProvenanceOf(sessionId);
return (toolCallId, fullText) => {
const ref = buildToolResultRef(sessionId, toolCallId, toolResultContentSegment(fullText));
const reportFailedWrite = (err) => {
const cause = thrownText(err);
const message = `tool-result offload: persisting ref "${ref}" failed (${cause}) — this write attempt stored nothing; ` +
`unless an earlier attempt already stored this ref, the inline preview is all that survived. ` +
`A later re-clear of the same result retries the write.`;
deliverEngineNotice(onNotice, { code: "tool_result.offload_put_failed", message, detail: { ref, sessionId, cause, occurrenceId: uuidv7() } });
};
try {
void Promise.resolve(store.put(ref, fullText, provenance)).catch(reportFailedWrite);
}
catch (err) {
reportFailedWrite(err);
}
return ref;
};
}
export function toolResultProvenanceOf(sessionId, taskId) {
return taskId === undefined ? { sessionId } : { sessionId, taskId };
}
export function offloadPagebackHint(ref, form, reachableTools) {
const activate = reachableTools !== undefined && !reachableTools.has(OFFLOAD_TOOL_NAME)
? `${OFFLOAD_TOOL_NAME} is not active yet — call ${TOOL_SEARCH_NAME} with {"query":"select:${OFFLOAD_TOOL_NAME}"} to activate it, then `
: "";
const core = `${activate === "" && form === "preview" ? "Call" : `${activate}call`} ${OFFLOAD_TOOL_NAME} with ref`;
return form === "preview" ? `${core}="${ref}" (offset, limit) to read more.` : `full text persisted; ${core} "${ref}" to read it back`;
}
export const PERSISTED_OUTPUT_PREFIX = "<persisted-output ref=";
export const DEFAULT_TOOL_RESULT_THRESHOLD_CHARS = 20_000;
const PREVIEW_HEAD_CHARS = 1_000;
const PREVIEW_TAIL_CHARS = 1_000;
const READ_DEFAULT_LIMIT = 4_000;
const READ_MAX_LIMIT = 20_000;
function intOr(x, fallback) {
return Number.isFinite(x) ? Math.floor(x) : fallback;
}
function totalTextChars(content) {
let n = 0;
for (const b of content)
if (b.type === "text" && typeof b.text === "string")
n += b.text.length;
return n;
}
export function buildPreview(full, ref, sizes, reachableTools) {
const total = full.length;
const headChars = sizes?.head ?? PREVIEW_HEAD_CHARS;
const tailChars = sizes?.tail ?? PREVIEW_TAIL_CHARS;
const head = full.slice(0, headChars);
const tailStart = Math.max(headChars, total - tailChars);
const tail = full.slice(tailStart);
return (`${PERSISTED_OUTPUT_PREFIX}"${ref}" chars="${total}">\n` +
`${head}\n` +
`…[truncated — ${total} chars total, retained in this run's tool-result store; a user-facing read face is a deployment fact. ` +
`${offloadPagebackHint(ref, "preview", reachableTools)}]\n` +
`${tail}\n` +
`</persisted-output>`);
}
export function withToolResultOffload(tool, store, thresholdChars, sessionId, reachableTools) {
if (!(Number.isFinite(thresholdChars) && thresholdChars > 0))
return tool;
const provenance = toolResultProvenanceOf(sessionId);
const wrappedExecute = async (toolCallId, params, signal, onUpdate) => {
const res = await tool.execute(toolCallId, params, signal, onUpdate);
const offloadedDetails = res.details === undefined ? undefined : await offloadOversizedDetailStrings(res.details, store, thresholdChars, sessionId, toolCallId, provenance);
const withDetails = (r) => offloadedDetails === undefined || offloadedDetails.value === res.details ? r : { ...r, details: offloadedDetails.value };
if (totalTextChars(res.content) <= thresholdChars)
return withDetails(res);
const full = res.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("\n");
if (full.length <= PREVIEW_HEAD_CHARS + PREVIEW_TAIL_CHARS)
return withDetails(res);
const ref = buildToolResultRef(sessionId, toolCallId, toolResultContentSegment(full));
try {
await store.put(ref, full, provenance);
}
catch {
return withDetails(res);
}
const images = res.content.filter((b) => b.type !== "text");
return withDetails({ ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] });
};
return { ...tool, execute: wrappedExecute };
}
const OFFLOADED_DETAIL_HEAD_CHARS = 2_000;
const OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS = MAX_MINTED_TOOL_RESULT_REF_CHARS + 200;
export const OFFLOADED_DETAIL_NOTICE_PREFIX = "…[offloaded — ";
export function isOffloadedDetailReplacement(s) {
return s.includes(`\n${OFFLOADED_DETAIL_NOTICE_PREFIX}`);
}
async function offloadOversizedDetailStrings(details, store, thresholdChars, sessionId, toolCallId, provenance) {
const isPlainObject = (v) => {
if (typeof v !== "object" || v === null)
return false;
const p = Object.getPrototypeOf(v);
return p === Object.prototype || p === null;
};
const onStack = new Set();
const pending = [];
const rebuilt = new Set();
const replace = (full, path) => {
const detailRef = buildToolResultRef(sessionId, toolCallId, path, toolResultContentSegment(full));
let done;
try {
done = Promise.resolve(store.put(detailRef, full, provenance));
}
catch (err) {
done = Promise.reject(err);
}
const marker = {};
pending.push({
marker,
done,
render: (stored) => `${full.slice(0, OFFLOADED_DETAIL_HEAD_CHARS)}\n${OFFLOADED_DETAIL_NOTICE_PREFIX}${full.length} chars total; ` +
(stored
? `ref "${detailRef}"; the remainder is retained in this run's tool-result store and reads back through the deployment's tool-results face]`
: `persisting the remainder to this run's tool-result store failed or went unconfirmed, so no ref is offered — the text above is all this field carries]`),
});
return marker;
};
const memo = new Map();
const walk = (v, segs) => {
if (typeof v === "string")
return v.length > thresholdChars && v.length > OFFLOADED_DETAIL_HEAD_CHARS + OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS ? replace(v, JSON.stringify(segs)) : v;
if (Array.isArray(v)) {
if (onStack.has(v))
return v;
const done = memo.get(v);
if (done !== undefined)
return done;
onStack.add(v);
let changed = false;
const len = v.length;
const next = new Array(len);
for (let i = 0; i < len; i++) {
if (!(i in v))
continue;
const item = v[i];
const w = walk(item, [...segs, i]);
if (w !== item)
changed = true;
Object.defineProperty(next, i, { value: w, enumerable: true, writable: true, configurable: true });
}
onStack.delete(v);
const result = changed ? next : v;
if (changed)
rebuilt.add(next);
memo.set(v, result);
return result;
}
if (isPlainObject(v)) {
if (onStack.has(v))
return v;
const done = memo.get(v);
if (done !== undefined)
return done;
onStack.add(v);
let changed = false;
const next = {};
for (const [k, item] of Object.entries(v)) {
const w = walk(item, [...segs, k]);
if (w !== item)
changed = true;
Object.defineProperty(next, k, { value: w, enumerable: true, writable: true, configurable: true });
}
onStack.delete(v);
const result = changed ? next : v;
if (changed)
rebuilt.add(next);
memo.set(v, result);
return result;
}
return v;
};
const value = walk(details, []);
const settled = await Promise.allSettled(pending.map((p) => p.done));
const notices = new Map();
pending.forEach((p, i) => {
notices.set(p.marker, p.render(settled[i].status === "fulfilled"));
});
const seen = new Set();
const finalize = (node) => {
if (typeof node !== "object" || node === null)
return node;
const notice = notices.get(node);
if (notice !== undefined)
return notice;
if (!rebuilt.has(node) || seen.has(node))
return node;
seen.add(node);
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
if (!Object.hasOwn(node, i))
continue;
Object.defineProperty(node, i, { value: finalize(node[i]), enumerable: true, writable: true, configurable: true });
}
return node;
}
const obj = node;
for (const k of Object.keys(obj))
Object.defineProperty(obj, k, { value: finalize(obj[k]), enumerable: true, writable: true, configurable: true });
return node;
};
return { value: finalize(value) };
}
export function createReadToolResultTool(store) {
return defineTool({
name: OFFLOAD_TOOL_NAME,
...toolFace("read-tool-result"),
description: `Read the full content of a tool result that was too large and persisted (you saw it as ` +
`<persisted-output ref="..."> with a preview). Pass the ref; use offset/limit to page through it.`,
parameters: Type.Object({
ref: Type.String({ description: "The ref from the <persisted-output> tag." }),
offset: Type.Optional(Type.Number({ description: "Char offset to start at (default 0)." })),
limit: Type.Optional(Type.Number({ description: `Max chars to return (default ${READ_DEFAULT_LIMIT}, capped ${READ_MAX_LIMIT}).` })),
}),
execute: async (args) => {
const { ref, offset, limit } = args;
const off = Math.max(0, intOr(offset, 0));
const lim = Math.min(READ_MAX_LIMIT, Math.max(1, intOr(limit, READ_DEFAULT_LIMIT)));
const slice = await store.get(ref, { offset: off, limit: lim });
if (!slice) {
return errorResult(`No persisted output for ref "${ref}" — it may have expired, or this process has no durable tool-result store (the preview above is all that remains).`);
}
const end = slice.offset + slice.content.length;
const more = end < slice.totalChars ? `\n…[${slice.totalChars - end} more chars — call again with offset=${end}.]` : "";
return `[ref=${ref} · chars ${slice.offset}–${end} of ${slice.totalChars}]\n${slice.content}${more}`;
},
});
}