@sema-agent/core
Version:
Stateless, task-oriented AI agent core
151 lines (150 loc) • 9.11 kB
JavaScript
import { Value } from "typebox/value";
import { formatToolError, formatZodValidationError } from "./tool-errors.js";
import { readMember } from "./thrown-value.js";
import { attachToolContract } from "../prompt-assembly/tool-catalog.js";
function normalizeContent(ret) {
if (typeof ret === "string") {
return { content: [{ type: "text", text: ret }], details: undefined, terminate: false, isError: false };
}
const raw = readMember(ret, "content");
if (typeof raw !== "string" && !Array.isArray(raw)) {
return {
content: [{ type: "text", text: "Tool error: content must be a string or an array of content blocks." }],
details: undefined,
terminate: false,
isError: true,
};
}
const content = typeof raw === "string" ? [{ type: "text", text: raw }] : raw;
return { content, details: readMember(ret, "details"), terminate: readMember(ret, "terminate") === true, isError: readMember(ret, "isError") === true };
}
function isEmptyToolContent(content) {
if (content.length === 0)
return true;
return content.every((b) => b.type === "text" && (typeof b.text !== "string" || b.text.trim() === ""));
}
export function errorResult(text, details) {
return details === undefined ? { content: text, isError: true } : { content: text, isError: true, details };
}
const DEFINE_TOOL_BRAND = Symbol("sema.core.defineTool.product");
const DEFINE_TOOL_REBIND = Symbol("sema.core.defineTool.rebind");
export function isDefineToolProduct(x) {
return typeof x === "object" && x !== null && Object.getOwnPropertyDescriptor(x, DEFINE_TOOL_BRAND)?.value === true;
}
export function stampDefineToolBrand(tool, rebind) {
Object.defineProperty(tool, DEFINE_TOOL_BRAND, { value: true, enumerable: false });
Object.defineProperty(tool, DEFINE_TOOL_REBIND, { value: sealedRebind(tool, rebind), enumerable: false });
return tool;
}
function sealedRebind(stamped, rebuild) {
const ownExecute = Object.getOwnPropertyDescriptor(stamped, "execute")?.value;
const stillSealed = () => {
const d = Object.getOwnPropertyDescriptor(stamped, "execute");
return d !== undefined && "value" in d && d.value === ownExecute;
};
return (enrich, mounted) => {
if (mounted !== stamped) {
throw new Error(`defineTool product ${JSON.stringify(stamped.name)}: the object being mounted is not the one this brand was stamped on (a derived object — prototype, proxy or descriptor copy — carrying a borrowed brand), so it cannot be rebound to the run's ctx without dropping its own overrides — wrap the product as a NEW object (stampDefineToolBrand with a rebind that re-wraps the rebound inner product), or author it as a raw ToolSpec`);
}
const refuse = () => {
throw new Error(`defineTool product ${JSON.stringify(stamped.name)}: its execute is not the own data property the factory wrote (replaced, or turned into an accessor, after the factory built it), so it cannot be rebound to the run's ctx without dropping the replacement — wrap the product as a NEW object (stampDefineToolBrand with a rebind that re-wraps the rebound inner product), or author it as a raw ToolSpec`);
};
if (!stillSealed())
refuse();
const rebound = rebuild(enrich, mounted);
if (!stillSealed())
refuse();
return rebound;
};
}
export function rebindDefineToolCtx(product, enrich) {
const rebind = Object.getOwnPropertyDescriptor(product, DEFINE_TOOL_REBIND)?.value;
if (typeof rebind !== "function")
throw new Error(`rebindDefineToolCtx: ${product.name} is not a defineTool product (no rebind seat)`);
return rebind(enrich, product);
}
function stampCallIdentity(enriched, toolCallId, signal) {
const own = (value) => ({ value, enumerable: true, configurable: true, writable: true });
return Object.create(Object.getPrototypeOf(enriched), { ...Object.getOwnPropertyDescriptors(enriched), toolCallId: own(toolCallId), signal: own(signal) });
}
export function defineTool(spec, options) {
const executionMode = spec.executionMode ?? (spec.effect === "read" ? "parallel" : "sequential");
const tool = {
name: spec.name,
...(spec.aliases && spec.aliases.length > 0 ? { aliases: spec.aliases } : {}),
description: spec.description,
...(spec.descriptionClassic !== undefined ? { descriptionClassic: spec.descriptionClassic } : {}),
label: spec.label ?? spec.name,
parameters: spec.parameters,
executionMode,
...(spec.isConcurrencySafe ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
...(spec.effect ? { effect: spec.effect } : {}),
...(spec.contentOrigin !== undefined ? { contentOrigin: spec.contentOrigin } : {}),
...(spec.egress ? { egress: true } : {}),
...(spec.permissionSeat === "passthrough" ? { permissionSeat: "passthrough" } : {}),
...(spec.irreversibility !== undefined ? { irreversibility: spec.irreversibility } : {}),
...(spec.reversibilityProbe ? { reversibilityProbe: spec.reversibilityProbe } : {}),
...(spec.offload !== undefined ? { offload: spec.offload } : {}),
...(spec.offloadThresholdChars !== undefined ? { offloadThresholdChars: spec.offloadThresholdChars } : {}),
...(spec.defer !== undefined ? { defer: spec.defer } : {}),
...(spec.alwaysLoad !== undefined ? { alwaysLoad: spec.alwaysLoad } : {}),
...(spec.prepareArguments ? { prepareArguments: spec.prepareArguments } : {}),
...(spec.family !== undefined ? { family: spec.family } : {}),
...(spec.pathTarget !== undefined ? { pathTarget: spec.pathTarget } : {}),
...(spec.ruleFace !== undefined ? { ruleFace: spec.ruleFace } : {}),
...(spec.renderHints !== undefined ? { renderHints: spec.renderHints } : {}),
...(spec.compactable === true ? { compactable: true } : {}),
...(spec.budgetExempt === true ? { budgetExempt: true } : {}),
...(spec.modelGate !== undefined ? { modelGate: spec.modelGate } : {}),
...(spec.approvalPreview ? { approvalPreview: spec.approvalPreview } : {}),
...(spec.validateInput ? { validateInput: spec.validateInput } : {}),
execute: async (toolCallId, rawParams, signal) => {
const params = spec.prepareArguments ? spec.prepareArguments(rawParams) : rawParams;
if (!Value.Check(spec.parameters, params)) {
const detail = formatZodValidationError(spec.parameters, params);
return {
content: [{ type: "text", text: `${detail} Fix the arguments and call again.` }],
details: { invalidArguments: true },
terminate: false,
isError: true,
};
}
try {
const baseCtx = { toolCallId, signal };
const ctx = options?.enrichCtx ? stampCallIdentity(options.enrichCtx(baseCtx), toolCallId, signal) : baseCtx;
const ret = await spec.execute(params, ctx);
const { content, details, terminate, isError } = normalizeContent(ret);
const guarded = isEmptyToolContent(content)
? [{ type: "text", text: isError ? `(${spec.name} reported an error with no further output)` : `(${spec.name} completed with no output)` }]
: content;
return { content: guarded, details, terminate, ...(isError ? { isError: true } : {}) };
}
catch (err) {
const wrapped = new Error(formatToolError(err));
const marks = readMember(err, "details");
if (marks !== undefined)
wrapped.details = marks;
const errorKind = readMember(err, "errorKind");
if (typeof errorKind === "string")
wrapped.errorKind = errorKind;
throw wrapped;
}
},
};
if (spec.contract) {
attachToolContract(tool, {
contractId: spec.contract.contractId,
implementationRevision: spec.contract.implementationRevision,
origin: spec.contract.contractId.startsWith("core.") ? "core" : "caller",
...(spec.aliases && spec.aliases.length > 0 ? { aliases: spec.aliases } : {}),
});
}
stampDefineToolBrand(tool, (enrich) => {
const composed = options?.enrichCtx === undefined ? enrich : (base) => options.enrichCtx(enrich(base));
const rebuilt = defineTool(spec, { ...options, enrichCtx: composed });
const { [DEFINE_TOOL_BRAND]: _brand, [DEFINE_TOOL_REBIND]: _seat, ...face } = Object.getOwnPropertyDescriptors(tool);
const copy = Object.create(Object.getPrototypeOf(tool), { ...face, execute: { value: rebuilt.execute, enumerable: true, configurable: true, writable: true } });
return stampDefineToolBrand(copy, (again) => rebindDefineToolCtx(rebuilt, again));
});
return tool;
}