@sema-agent/core
Version:
Stateless, task-oriented AI agent core
305 lines (304 loc) • 11.7 kB
JavaScript
import { thrownText } from "../thrown-value.js";
const STRICT_KEYWORDS = new Set([
"$schema",
"type",
"description",
"title",
"properties",
"required",
"additionalProperties",
"items",
"enum",
"const",
"anyOf",
]);
const ANYOF_SIBLINGS = new Set(["$schema", "description", "title"]);
const STRICT_TYPES = new Set(["object", "array", "string", "integer", "number", "boolean", "null"]);
const STRICT_MAX_DEPTH = 32;
const STRICT_NODE_BUDGET = 100_000;
const GUARD_NODE_BUDGET = 100_000;
const GUARD_MAX_DEPTH = 10_000;
function isPlainObject(e) {
return typeof e === "object" && e !== null && !Array.isArray(e);
}
function isPrimitiveLiteral(e) {
return e === null || typeof e === "string" || (typeof e === "number" && Number.isFinite(e)) || typeof e === "boolean";
}
function deriveNode(e, depth, budget) {
if (depth <= 0)
return { reason: "max_depth" };
if (--budget.remaining < 0)
return { reason: "max_nodes" };
if (!isPlainObject(e))
return { reason: "not_object" };
for (const key of Object.keys(e))
if (!STRICT_KEYWORDS.has(key))
return { reason: "unsupported_keyword" };
const n = {};
if (e.description !== undefined) {
if (typeof e.description !== "string")
return { reason: "unsupported_keyword" };
n.description = e.description;
}
if (e.title !== undefined) {
if (typeof e.title !== "string")
return { reason: "unsupported_keyword" };
n.title = e.title;
}
if (e.anyOf !== undefined) {
for (const key of Object.keys(e))
if (key !== "anyOf" && !ANYOF_SIBLINGS.has(key))
return { reason: "unsupported_keyword" };
if (!Array.isArray(e.anyOf) || e.anyOf.length === 0)
return { reason: "unsupported_keyword" };
const branches = [];
for (const branch of e.anyOf) {
const r = deriveNode(branch, depth - 1, budget);
if ("reason" in r)
return r;
branches.push(r.node);
}
n.anyOf = branches;
return { node: n };
}
if (e.const !== undefined) {
if (!isPrimitiveLiteral(e.const))
return { reason: "unsupported_const" };
n.const = e.const;
}
if (e.enum !== undefined) {
if (!Array.isArray(e.enum) || e.enum.length === 0 || !e.enum.every(isPrimitiveLiteral) || new Set(e.enum).size !== e.enum.length) {
return { reason: "unsupported_enum" };
}
n.enum = e.enum.slice();
}
const type = e.type;
if (type !== undefined) {
if (typeof type === "string") {
if (!STRICT_TYPES.has(type))
return { reason: "unsupported_type" };
n.type = type;
}
else if (Array.isArray(type)) {
if (type.length === 0 ||
!type.every((t) => typeof t === "string" && STRICT_TYPES.has(t) && t !== "object" && t !== "array") ||
new Set(type).size !== type.length) {
return { reason: "unsupported_type" };
}
n.type = type.slice();
}
else {
return { reason: "unsupported_type" };
}
}
if (type !== "object" && (e.properties !== undefined || e.required !== undefined || e.additionalProperties !== undefined)) {
return { reason: "mismatched_keywords" };
}
if (type !== "array" && e.items !== undefined)
return { reason: "mismatched_keywords" };
if (type === "object") {
const props = e.properties;
if (!isPlainObject(props))
return { reason: "no_properties" };
if (e.additionalProperties !== undefined && e.additionalProperties !== false)
return { reason: "additional_properties" };
if (e.required !== undefined) {
if (!Array.isArray(e.required) ||
!e.required.every((r) => typeof r === "string" && Object.hasOwn(props, r)) ||
new Set(e.required).size !== e.required.length) {
return { reason: "invalid_required" };
}
n.required = e.required.slice();
}
const derived = [];
for (const [name, sub] of Object.entries(props)) {
const r = deriveNode(sub, depth - 1, budget);
if ("reason" in r)
return r;
derived.push([name, r.node]);
}
n.properties = Object.fromEntries(derived);
n.additionalProperties = false;
}
else if (type === "array") {
const items = e.items;
if (items === undefined || Array.isArray(items))
return { reason: "unsupported_items" };
const r = deriveNode(items, depth - 1, budget);
if ("reason" in r)
return r;
n.items = r.node;
}
else if (type === undefined && n.enum === undefined && !("const" in n)) {
return { reason: "missing_type" };
}
return { node: n };
}
export function deriveStrictSchema(schema) {
const r = deriveNode(schema, STRICT_MAX_DEPTH, { remaining: STRICT_NODE_BUDGET });
if ("reason" in r)
return { ok: false, reason: r.reason };
if (r.node.type !== "object")
return { ok: false, reason: "root_not_object" };
return { ok: true, schema: { ...r.node, type: "object" } };
}
function exceedsSizeGuard(root) {
let n = GUARD_NODE_BUDGET;
const stack = [{ value: root, depth: 0 }];
while (stack.length > 0) {
const { value, depth } = stack.pop();
if (--n < 0 || depth > GUARD_MAX_DEPTH)
return true;
if (typeof value !== "object" || value === null)
continue;
for (const v of Object.values(value))
stack.push({ value: v, depth: depth + 1 });
}
return false;
}
const NUMBER_KEYWORDS = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"];
const NON_NEGATIVE_INT_KEYWORDS = ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"];
const STRING_KEYWORDS = ["description", "title", "$comment", "$ref", "$id", "$schema", "format", "contentEncoding", "contentMediaType"];
const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "definitions", "$defs"];
const SUBSCHEMA_KEYWORDS = ["additionalProperties", "additionalItems", "not", "if", "then", "else", "contains", "propertyNames"];
const SCHEMA_ARRAY_KEYWORDS = ["anyOf", "oneOf", "allOf"];
function checkSchemaShape(node, path) {
if (typeof node === "boolean")
return null;
if (!isPlainObject(node))
return `${path}: schema must be an object or boolean`;
const e = node;
if (e.type !== undefined) {
if (typeof e.type === "string") {
if (!STRICT_TYPES.has(e.type))
return `${path}/type: "${String(e.type)}" is not a valid JSON Schema type`;
}
else if (Array.isArray(e.type)) {
if (e.type.length === 0 || !e.type.every((t) => typeof t === "string" && STRICT_TYPES.has(t))) {
return `${path}/type: must be a non-empty array of valid JSON Schema types`;
}
if (new Set(e.type).size !== e.type.length)
return `${path}/type: duplicate entries`;
}
else {
return `${path}/type: must be a string or an array of strings`;
}
}
if (e.enum !== undefined && !Array.isArray(e.enum))
return `${path}/enum: must be an array`;
if (e.required !== undefined) {
if (!Array.isArray(e.required) || !e.required.every((r) => typeof r === "string")) {
return `${path}/required: must be an array of strings`;
}
if (new Set(e.required).size !== e.required.length)
return `${path}/required: duplicate entries`;
}
if (e.uniqueItems !== undefined && typeof e.uniqueItems !== "boolean")
return `${path}/uniqueItems: must be a boolean`;
if (e.pattern !== undefined) {
if (typeof e.pattern !== "string")
return `${path}/pattern: must be a string`;
try {
new RegExp(e.pattern, "u");
}
catch {
try {
new RegExp(e.pattern);
}
catch {
return `${path}/pattern: invalid regular expression`;
}
}
}
for (const k of NUMBER_KEYWORDS) {
if (e[k] !== undefined && (typeof e[k] !== "number" || !Number.isFinite(e[k])))
return `${path}/${k}: must be a number`;
}
if (typeof e.multipleOf === "number" && e.multipleOf <= 0)
return `${path}/multipleOf: must be > 0`;
for (const k of NON_NEGATIVE_INT_KEYWORDS) {
const v = e[k];
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 0)) {
return `${path}/${k}: must be a non-negative integer`;
}
}
for (const k of STRING_KEYWORDS) {
if (e[k] !== undefined && typeof e[k] !== "string")
return `${path}/${k}: must be a string`;
}
for (const k of SCHEMA_MAP_KEYWORDS) {
const v = e[k];
if (v === undefined)
continue;
if (!isPlainObject(v))
return `${path}/${k}: must be an object mapping names to schemas`;
for (const [name, sub] of Object.entries(v)) {
const err = checkSchemaShape(sub, `${path}/${k}/${name}`);
if (err)
return err;
}
}
if (e.items !== undefined) {
if (Array.isArray(e.items)) {
for (let i = 0; i < e.items.length; i++) {
const err = checkSchemaShape(e.items[i], `${path}/items/${i}`);
if (err)
return err;
}
}
else {
const err = checkSchemaShape(e.items, `${path}/items`);
if (err)
return err;
}
}
for (const k of SUBSCHEMA_KEYWORDS) {
if (e[k] !== undefined) {
const err = checkSchemaShape(e[k], `${path}/${k}`);
if (err)
return err;
}
}
for (const k of SCHEMA_ARRAY_KEYWORDS) {
const v = e[k];
if (v === undefined)
continue;
if (!Array.isArray(v) || v.length === 0)
return `${path}/${k}: must be a non-empty array of schemas`;
for (let i = 0; i < v.length; i++) {
const err = checkSchemaShape(v[i], `${path}/${k}/${i}`);
if (err)
return err;
}
}
return null;
}
export function validateJsonSchemaShape(schema) {
if (exceedsSizeGuard(schema))
return "schema too large";
return checkSchemaShape(schema, "root");
}
const compileMemo = new WeakMap();
export function compileOutputSchema(schema) {
const cached = typeof schema === "object" && schema !== null ? compileMemo.get(schema) : undefined;
if (cached)
return cached;
const result = compileUncached(schema);
if (typeof schema === "object" && schema !== null)
compileMemo.set(schema, result);
return result;
}
function compileUncached(schema) {
const invalid = validateJsonSchemaShape(schema);
if (invalid !== null)
return { error: invalid };
try {
const derived = deriveStrictSchema(schema);
if (derived.ok)
return { modelSchema: derived.schema, strict: true };
return { modelSchema: schema, strict: false, fallbackReason: derived.reason };
}
catch (err) {
return { modelSchema: schema, strict: false, fallbackReason: `derivation_error: ${thrownText(err)}` };
}
}