UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

305 lines (304 loc) • 11.7 kB
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)}` }; } }