import { QxReject, canonicalJson, type Json } from "@quixos/camino-replica-core"; export function boundedText(value: string, limit = 65536, allowEmpty = true): string { if (typeof value !== "string" || (!allowEmpty && !value.trim()) || new TextEncoder().encode(value).length > limit) throw new QxReject("AGENT_INVALID_TEXT"); return value; } export function json(value: string, limit = 65536): Json { boundedText(value, limit, false); let parsed: Json; try { parsed = JSON.parse(value); } catch { throw new QxReject("AGENT_INVALID_JSON"); } let nodes = 0; function walk(v: Json, depth: number) { if (++nodes > 8192 || depth > 24) throw new QxReject("AGENT_JSON_LIMIT"); if (Array.isArray(v)) for (const child of v) walk(child, depth + 1); else if (v && typeof v === "object") for (const child of Object.values(v)) walk(child, depth + 1); } walk(parsed, 0); return parsed; } export function object(v: Json): Record { if (!v || Array.isArray(v) || typeof v !== "object") throw new QxReject("AGENT_EXPECTED_OBJECT"); return v as Record; } export function timestamp(v: string) { if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(v) || !Number.isFinite(Date.parse(v))) throw new QxReject("AGENT_INVALID_TIME"); } const schemaKeys = new Set([ "type", "properties", "required", "items", "enum", "additionalProperties", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "description", "title", ]); export function schema(v: Json, depth = 0, budget = { nodes: 0 }): void { if (++budget.nodes > 256 || depth > 12) throw new QxReject("AGENT_SCHEMA_LIMIT"); const s = object(v); for (const k of Object.keys(s)) if (!schemaKeys.has(k)) throw new QxReject("AGENT_SCHEMA_UNSUPPORTED"); if (!["string", "number", "integer", "boolean", "null", "array", "object"].includes(String(s.type))) throw new QxReject("AGENT_SCHEMA_TYPE"); if (s.enum !== undefined && (!Array.isArray(s.enum) || !s.enum.length || s.enum.length > 128)) throw new QxReject("AGENT_SCHEMA_ENUM"); for (const k of ["minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems"]) { if (s[k] !== undefined && (typeof s[k] !== "number" || !Number.isFinite(s[k]))) throw new QxReject("AGENT_SCHEMA_BOUND"); if ( k !== "minimum" && k !== "maximum" && s[k] !== undefined && (Number(s[k]) < 0 || !Number.isInteger(s[k]) || Number(s[k]) > 65536) ) throw new QxReject("AGENT_SCHEMA_BOUND"); } for (const [min, max] of [ ["minimum", "maximum"], ["minLength", "maxLength"], ["minItems", "maxItems"], ]) if (s[min] !== undefined && s[max] !== undefined && Number(s[min]) > Number(s[max])) throw new QxReject("AGENT_SCHEMA_BOUND"); if (s.type === "object") { const p = object(s.properties ?? {}); if (Object.keys(p).length > 64) throw new QxReject("AGENT_SCHEMA_LIMIT"); if (s.additionalProperties !== undefined && typeof s.additionalProperties !== "boolean") throw new QxReject("AGENT_SCHEMA_UNSUPPORTED"); if ( s.required !== undefined && (!Array.isArray(s.required) || s.required.some((k) => typeof k !== "string" || !Object.hasOwn(p, k)) || new Set(s.required).size !== s.required.length) ) throw new QxReject("AGENT_SCHEMA_REQUIRED"); for (const child of Object.values(p)) schema(child, depth + 1, budget); } if (s.type === "array") schema(s.items ?? null, depth + 1, budget); } export function validateResult(supplied: Json, value: Json): void { schema(supplied); let nodes = 0; function check(shape: Json, v: Json, depth: number) { if (++nodes > 8192 || depth > 24) throw new QxReject("AGENT_RESULT_LIMIT"); const s = object(shape), type = s.type; const actual = v === null ? "null" : Array.isArray(v) ? "array" : typeof v; if (!(type === actual || (type === "integer" && typeof v === "number" && Number.isInteger(v)))) throw new QxReject("AGENT_RESULT_TYPE"); if (s.enum && !(s.enum as Json[]).some((e) => canonicalJson(e) === canonicalJson(v))) throw new QxReject("AGENT_RESULT_ENUM"); if ( typeof v === "number" && ((s.minimum !== undefined && v < Number(s.minimum)) || (s.maximum !== undefined && v > Number(s.maximum))) ) throw new QxReject("AGENT_RESULT_BOUND"); if (typeof v === "string") { const n = Array.from(v).length; if (n < Number(s.minLength ?? 0) || n > Number(s.maxLength ?? 65536)) throw new QxReject("AGENT_RESULT_BOUND"); } if (Array.isArray(v)) { if (v.length < Number(s.minItems ?? 0) || v.length > Number(s.maxItems ?? 8192)) throw new QxReject("AGENT_RESULT_BOUND"); for (const e of v) check(s.items, e, depth + 1); } if (type === "object") { const p = object(s.properties ?? {}), o = object(v); for (const k of (s.required ?? []) as string[]) if (!Object.hasOwn(o, k)) throw new QxReject("AGENT_RESULT_REQUIRED"); for (const [k, e] of Object.entries(o)) { if (Object.hasOwn(p, k)) check(p[k], e, depth + 1); else if (s.additionalProperties === false) throw new QxReject("AGENT_RESULT_PROPERTY"); } } } check(supplied, value, 0); } export function settings(value: string): Record { const s = object(json(value)); const keys = new Set([ "profile", "response", "capabilities", "notifications", "retry", "limits", "interactive", "systemInstructions", ]); for (const k of Object.keys(s)) if (!keys.has(k)) throw new QxReject("AGENT_SETTINGS_UNKNOWN"); if (s.systemInstructions !== undefined) boundedText(s.systemInstructions as string); if (s.profile !== undefined) { if (typeof s.profile !== "string") throw new QxReject("AGENT_SETTINGS_TYPE"); boundedText(s.profile, 80, false); } const response = object(s.response ?? { kind: "text" }); for (const k of Object.keys(response)) if (!["kind", "schema"].includes(k)) throw new QxReject("AGENT_RESPONSE_UNKNOWN"); if (response.kind !== "json" && response.schema !== undefined) throw new QxReject("AGENT_UNEXPECTED_SCHEMA"); if (!["text", "json", "unit"].includes(String(response.kind))) throw new QxReject("AGENT_RESPONSE_MODE"); if (response.kind === "json") schema(response.schema ?? null); if ( s.capabilities !== undefined && (!Array.isArray(s.capabilities) || s.capabilities.length > 6 || new Set(s.capabilities).size !== s.capabilities.length || s.capabilities.some( (c) => !["web-search", "graph-read", "graph-write", "eval", "source-read", "source-write"].includes(String(c)), )) ) throw new QxReject("AGENT_CAPABILITY"); if (s.retry !== undefined && !["never", "inference-only"].includes(String(s.retry))) throw new QxReject("AGENT_RETRY"); if ( s.retry === "inference-only" && Array.isArray(s.capabilities) && s.capabilities.some((c) => ["graph-write", "eval", "source-write"].includes(String(c))) ) throw new QxReject("AGENT_UNSAFE_RETRY"); for (const k of ["interactive", "notifications"]) if (s[k] !== undefined && typeof s[k] !== "boolean") throw new QxReject("AGENT_SETTINGS_TYPE"); if (s.limits !== undefined) { const limits = object(s.limits); for (const [k, v] of Object.entries(limits)) if ( !Object.hasOwn({ toolSteps: 128, activeMs: 1800000, outputBytes: 262144, retries: 1 }, k) || !Number.isInteger(v) || Number(v) < 0 || Number(v) > ({ toolSteps: 128, activeMs: 1800000, outputBytes: 262144, retries: 1 } as Record)[k] ) throw new QxReject("AGENT_LIMIT"); } return { profile: "balanced", response: { kind: "text" }, capabilities: [], notifications: false, retry: "never", interactive: false, ...s, }; }