UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

239 lines (238 loc) • 8.46 kB
import { createHash } from "node:crypto"; export function canonicalize(value) { try { return encode(value); } catch { return UNCANONICALIZABLE; } } export const UNCANONICALIZABLE = "<uncanonicalizable>"; export function boundInputHashOf(value) { return createHash("sha256").update(canonicalize(value), "utf8").digest("hex"); } const MAX_DEPTH = 256; function encode(value, depth = 0) { if (value === null) return "N"; if (value === undefined) return "U"; switch (typeof value) { case "boolean": return value ? "b1" : "b0"; case "number": return `n:${encodeNumber(value)}`; case "bigint": return `i:${value.toString()}`; case "string": return `s${Buffer.byteLength(value, "utf8")}:${value}`; case "function": return "fn"; case "symbol": return `y:${value.description ?? ""}`; case "object": { if (depth >= MAX_DEPTH) return "<max-depth>"; try { if (Array.isArray(value)) { let out = `a${value.length}:[`; for (const el of value) out += encode(el, depth + 1); return `${out}]`; } const branded = encodeBranded(value, depth); if (branded !== undefined) return branded; return encodeOwnKeys(value, depth); } catch { return "<unserializable>"; } } default: return "U"; } } function encodeOwnKeys(source, depth, skipIndicesBelow = 0) { const obj = source; const all = Object.keys(obj); const keys = (skipIndicesBelow > 0 ? all.filter((k) => !isIndexBelow(k, skipIndicesBelow)) : all).sort(); let out = `o${keys.length}:{`; for (const k of keys) { let encoded; try { encoded = encode(obj[k], depth + 1); } catch { encoded = "<unreadable-property>"; } out += `${Buffer.byteLength(k, "utf8")}:${k}=${encoded}`; } return `${out}}`; } function isIndexBelow(key, limit) { const n = Number(key); return Number.isInteger(n) && n >= 0 && n < limit && String(n) === key; } function brandOf(value) { try { const s = Object.prototype.toString.call(value); return s.startsWith("[object ") && s.endsWith("]") ? s.slice(8, -1) : "Object"; } catch { return "Object"; } } function encodeBranded(value, depth) { const brand = brandOf(value); if (brand === "Object") return undefined; if (ArrayBuffer.isView(value)) { try { const { buffer, byteOffset, byteLength, elements, family } = viewRange(value); const bytesToken = digestBytes(new Uint8Array(buffer, byteOffset, byteLength)); return `V:${family}:${brand}:${byteLength}:${bytesToken}${encodeOwnKeys(value, depth, elements)}`; } catch { } } const own = encodeOwnKeys(value, depth); try { switch (brand) { case "Date": { const t = Date.prototype.getTime.call(value); return `D:${Number.isNaN(t) ? "Invalid" : encodeNumber(t)}${own}`; } case "Map": { const size = Number(mapSizeGetter.call(value)); let out = `m${size}:{`; let seen = 0; for (const [k, v] of mapEntries.call(value)) { if (seen++ >= size) break; out += `${encode(k, depth + 1)}=${encode(v, depth + 1)}`; } return `${out}}${own}`; } case "Set": { const size = Number(setSizeGetter.call(value)); let out = `t${size}:[`; let seen = 0; for (const v of setValues.call(value)) { if (seen++ >= size) break; out += encode(v, depth + 1); } return `${out}]${own}`; } case "RegExp": { const source = String(regexpSourceGetter.call(value)); const flags = String(regexpFlagsGetter.call(value)); return `r${Buffer.byteLength(source, "utf8")}:${source}/${flags}${own}`; } case "Error": { const name = String(value.name); const message = String(value.message); return `E${Buffer.byteLength(name, "utf8")}:${name}:${Buffer.byteLength(message, "utf8")}:${message}${own}`; } case "URL": { const href = String(urlHrefGetter.call(value)); return `L${Buffer.byteLength(href, "utf8")}:${href}${own}`; } case "Boolean": case "Number": case "String": case "BigInt": case "Symbol": { const unboxed = boxedValueOf[brand].call(value); return `B:${encode(unboxed, depth + 1)}${own}`; } case "ArrayBuffer": case "SharedArrayBuffer": { const byteLength = Number((brand === "ArrayBuffer" ? arrayBufferByteLengthGetter : sharedArrayBufferByteLengthGetter).call(value)); return `A:${brand}:${byteLength}:${digestBytes(new Uint8Array(value))}${own}`; } default: break; } } catch { } return `x${Buffer.byteLength(brand, "utf8")}:${brand}${own}`; } function primordialGetter(proto, key) { const get = Object.getOwnPropertyDescriptor(proto, key)?.get; if (!get) { return () => { throw new Error(`no primordial getter for ${key}`); }; } return get; } const mapEntries = Map.prototype.entries; const setValues = Set.prototype.values; const mapSizeGetter = primordialGetter(Map.prototype, "size"); const setSizeGetter = primordialGetter(Set.prototype, "size"); const regexpSourceGetter = primordialGetter(RegExp.prototype, "source"); const regexpFlagsGetter = primordialGetter(RegExp.prototype, "flags"); const urlHrefGetter = primordialGetter(URL.prototype, "href"); const arrayBufferByteLengthGetter = primordialGetter(ArrayBuffer.prototype, "byteLength"); const sharedArrayBufferByteLengthGetter = typeof SharedArrayBuffer !== "undefined" ? primordialGetter(SharedArrayBuffer.prototype, "byteLength") : primordialGetter({}, "byteLength"); const boxedValueOf = { Boolean: Boolean.prototype.valueOf, Number: Number.prototype.valueOf, String: String.prototype.valueOf, BigInt: BigInt.prototype.valueOf, Symbol: Symbol.prototype.valueOf, }; const typedArrayProto = Object.getPrototypeOf(Uint8Array.prototype); const typedArrayLengthGetter = primordialGetter(typedArrayProto, "length"); const viewGetters = { typed: { buffer: primordialGetter(typedArrayProto, "buffer"), byteOffset: primordialGetter(typedArrayProto, "byteOffset"), byteLength: primordialGetter(typedArrayProto, "byteLength"), }, dataView: { buffer: primordialGetter(DataView.prototype, "buffer"), byteOffset: primordialGetter(DataView.prototype, "byteOffset"), byteLength: primordialGetter(DataView.prototype, "byteLength"), }, }; function viewRange(view) { for (const family of ["typed", "dataView"]) { const g = viewGetters[family]; try { const range = { buffer: g.buffer.call(view), byteOffset: Number(g.byteOffset.call(view)), byteLength: Number(g.byteLength.call(view)), elements: 0, family, }; if (family === "typed") range.elements = Number(typedArrayLengthGetter.call(view)); return range; } catch { continue; } } throw new Error("not a readable ArrayBuffer view"); } function digestBytes(bytes) { return createHash("sha256").update(bytes).digest("hex"); } function encodeNumber(n) { if (Number.isNaN(n)) return "NaN"; if (n === Infinity) return "Inf"; if (n === -Infinity) return "-Inf"; if (n === 0) return "0"; return String(n); }