@sema-agent/core
Version:
Stateless, task-oriented AI agent core
238 lines (237 loc) • 8.41 kB
JavaScript
import { createHash } from "node:crypto";
export function canonicalize(value) {
try {
return encode(value);
}
catch {
return "<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);
}