UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

220 lines (219 loc) • 9.38 kB
import { TOOL_SPEC_MEMBERS, frozenExtra, frozenMember, newCopyState } from "../tool-spec-members.js"; import { isDefineToolProduct, rebindDefineToolCtx } from "../tools.js"; function specArmCanConstruct(t) { if (typeof t !== "object" || t === null) return false; const name = Object.getOwnPropertyDescriptor(t, "name"); const execute = Object.getOwnPropertyDescriptor(t, "execute"); return name !== undefined && "value" in name && typeof name.value === "string" && name.enumerable === true && execute !== undefined && "value" in execute && typeof execute.value === "function" && execute.enumerable === true; } const ORIGIN = new WeakMap(); export function callerEntryOrigin(entry) { return ORIGIN.get(entry) ?? entry; } const PRODUCT_OF = new WeakMap(); const IS_MEMBER = new Set(TOOL_SPEC_MEMBERS); function faceKeys(t) { const keys = new Set(Reflect.ownKeys(t).filter((k) => Object.getOwnPropertyDescriptor(t, k)?.enumerable === true)); for (const name of TOOL_SPEC_MEMBERS) keys.add(name); return [...keys]; } const BUILDABLE = new WeakSet(); export function specArmCanBuild(entry) { return BUILDABLE.has(entry); } const entryName = (t) => { try { const name = t !== null && (typeof t === "object" || typeof t === "function") ? Object.getOwnPropertyDescriptor(t, "name")?.value : undefined; return typeof name === "string" ? JSON.stringify(name) : "(no own name)"; } catch { return "(no readable name)"; } }; function refuseUnreadableEntry(index, entry, cause) { let said = ""; try { said = cause instanceof Error ? cause.message.slice(0, 120) : ""; } catch { } const e = new Error(`spec.tools[${index}] ${entryName(entry)}: its members could not be read${said === "" ? "" : ` (${said})`} — the run fixes its tool roster by reading each entry once, before its first await, and an entry that cannot be read is not a tool. Pass a tool whose declaration reads without throwing.`); e.code = "config.tool_mount_denied"; throw e; } export function rosterFaceOf(t, index) { if ((typeof t !== "object" && typeof t !== "function") || t === null) return t; try { const product = isDefineToolProduct(t) ? t : undefined; const buildable = product === undefined && specArmCanConstruct(t); const source = t; const face = {}; const ledger = newCopyState(); for (const key of faceKeys(source)) { const own = Object.prototype.hasOwnProperty.call(source, key); const value = source[key]; if (value === undefined && !own) continue; let kept; if (typeof key === "string" && IS_MEMBER.has(key)) kept = frozenMember(key, value, ledger); else { const extra = frozenExtra(String(key), value, ledger); if (!extra.kept) continue; kept = extra.value; } Object.defineProperty(face, key, { value: kept, enumerable: true, writable: true, configurable: true }); } ORIGIN.set(face, source); if (product !== undefined) PRODUCT_OF.set(face, product); else if (buildable && specArmCanConstruct(face)) BUILDABLE.add(face); return Object.freeze(face); } catch (cause) { return refuseEntry(index, t, cause); } } function refuseEntry(index, entry, cause) { let refusal; try { if (cause instanceof Error && cause.code === "config.tool_mount_denied") refusal = cause.message; } catch { } if (refusal === undefined) return refuseUnreadableEntry(index, entry, cause); const e = new Error(`spec.tools[${index}] ${entryName(entry)}: ${refusal}`); e.code = "config.tool_mount_denied"; throw e; } function sameMember(was, now, ledger, depth = 0) { if (Object.is(was, now)) return true; if (depth > 64 || typeof was !== "object" || typeof now !== "object" || was === null || now === null) return false; const met = ledger.pairs.get(was); if (met?.has(now) === true) return true; if (met === undefined) ledger.pairs.set(was, new WeakSet([now])); else met.add(now); try { if (Array.isArray(was) !== Array.isArray(now)) return false; const a = Reflect.ownKeys(was), b = Reflect.ownKeys(now); if (a.length !== b.length) return false; ledger.steps += a.length + 1; if (ledger.steps > COMPARE_STEPS) { ledger.over = true; return false; } for (const key of a) { const x = Object.getOwnPropertyDescriptor(was, key), y = Object.getOwnPropertyDescriptor(now, key); if (x === undefined || y === undefined || !("value" in x) || !("value" in y) || !sameMember(x.value, y.value, ledger, depth + 1)) return false; } return true; } catch { return false; } } const COMPARE_STEPS = 20_000; function shown(v) { if (v === undefined || typeof v === "boolean" || typeof v === "number") return String(v); if (typeof v === "string") return JSON.stringify(v.length > 60 ? `${v.slice(0, 60)}…` : v); try { if (Array.isArray(v) && v.length <= 8 && v.every((x) => typeof x === "string")) return `[${v.map((x) => shown(x)).join(", ")}]`; } catch { } return undefined; } function refuseProductDrift(face, rebound) { const was = face; const now = rebound; const keys = new Set([...Reflect.ownKeys(face), ...faceKeys(rebound)]); keys.delete("execute"); const driftProblems = []; const ledger = { pairs: new WeakMap(), steps: 0, over: false }; for (const key of keys) { let after; try { after = now[key]; } catch { driftProblems.push(`${String(key)} (unreadable)`); continue; } if (sameMember(was[key], after, ledger)) continue; const before = shown(was[key]), later = shown(after); driftProblems.push(ledger.over ? `${String(key)} (declaration comparison over budget)` : before !== undefined && later !== undefined ? `${String(key)} (${before} → ${later})` : `${String(key)} (differs)`); if (ledger.over) break; } if (driftProblems.length === 0) return; const e = new Error(`defineTool product ${entryName(face)} changed after the run fixed its tool roster (before its first await): ${driftProblems.join(", ")}. The safety scan judged the product as it stood then, so a product that no longer matches is never mounted — not under its new name, not under the old one. Pass the final product before starting the run.`); e.code = "config.tool_mount_denied"; throw e; } function mountedProduct(face, rebound) { const members = {}; for (const key of Reflect.ownKeys(face)) members[key] = { value: face[key], enumerable: true }; members.execute = { value: rebound.execute.bind(rebound), enumerable: true }; return Object.freeze(Object.create(Object.prototype, members)); } export function rebindProductEntry(entry, enrich) { const product = PRODUCT_OF.get(entry); if (product === undefined) return undefined; const rebound = rebindDefineToolCtx(product, enrich); refuseProductDrift(entry, rebound); return mountedProduct(entry, rebound); } export function snapshotCallerRoster(spec) { const tools = spec.tools; const descriptors = Object.getOwnPropertyDescriptors(spec); const handed = Array.isArray(tools) ? Object.freeze([...tools]) : undefined; const slot = handed !== undefined ? Object.freeze(handed.map((t, i) => rosterFaceOf(t, i))) : tools; descriptors.tools = { value: slot, enumerable: slot !== undefined, writable: false, configurable: false }; const copy = Object.create(Object.getPrototypeOf(spec), descriptors); return { spec: copy, roster: { caller: spec, handed: tools === undefined ? Object.freeze([]) : handed } }; } export function refuseLateRosterChanges(roster) { const { handed } = roster; if (handed === undefined) return; const current = roster.caller.tools; const now = current === undefined ? [] : Array.isArray(current) ? current : throwChanged([`spec.tools (reassigned to a value that is not an array)`]); const changed = []; for (let i = 0; i < Math.max(now.length, handed.length); i++) { if (now[i] === handed[i]) continue; if (i < now.length) changed.push(`${entryName(now[i])} (${i < handed.length ? "replaced" : "added"})`); else changed.push(`${entryName(handed[i])} (removed)`); } if (changed.length > 0) throwChanged(changed); } function throwChanged(changed) { const e = new Error(`spec.tools changed after the run fixed its tool roster: ${changed.join(", ")}. The roster is the one the request carried when the run started (before its first await) — the scan that judges each tool's effect and marks and the mount read that one copy, so a later entry is never mounted. Pass the complete roster before starting the run.`); e.code = "config.tool_mount_denied"; throw e; }