JuliusBrussee/caveman · error
assembly slot content is not JSON-serializable
Error message
assembly slot ${JSON.stringify(slot.id)} content is not JSON-serializable What it means
assemble() serializes each slot's `content` through canonicalJSON() to compute the content hash used for cache-prefix tracking. If serialization throws (circular structures, BigInt values, functions/symbols, or a custom canonicalizer rejecting non-JSON types), the SDK rethrows as this plain Error naming the offending slot.
Solutions
- Ensure every slot's `content` is plain JSON data — strings, numbers, booleans, null, arrays, and plain objects.
- JSON.stringify a test of the content (or run your own cycle check) before assembling to find the offending value.
- Strip or convert non-JSON values: `String(bigintValue)` for BigInt, omit functions, break cycles by building a plain snapshot.
- Wrap the content in a typed serialization boundary (e.g. a toWire() method) so only plain data ever reaches slots.
Example fix
// before
slots.push({ id: "tools", stability: "session", content: { registry: toolRegistry } }); // circular
// after
slots.push({
id: "tools",
stability: "session",
content: { registry: toolRegistry.list().map((t) => ({ name: t.name, schema: t.schema })) }
}); Defensive patterns
Strategy: validation
Validate before calling
function isJsonSafe(value, seen = new WeakSet()) {
if (value === null || typeof value !== "object") {
return typeof value !== "function" && typeof value !== "symbol" && typeof value !== "bigint";
}
if (seen.has(value)) return false;
seen.add(value);
return Object.values(value).every((v) => isJsonSafe(v, seen));
} Type guard
function hasSerializableContent(slot) {
try { JSON.stringify(slot.content); return true; } catch { return false; }
} Try / catch
try {
return cave.assemble(opts);
} catch (e) {
if (e.message.includes("content is not JSON-serializable")) {
const slotId = e.message.match(/assembly slot (".*")/)?.[1];
// replace the offending slot's content with a plain-data snapshot and retry once
}
throw e;
} Prevention
- Keep slot content as plain JSON data; never embed client objects, functions, or class instances.
- Convert BigInt to string/number at the boundary; strip cyclic parent pointers.
- Add a JSON.stringify smoke test over slot content in unit tests.
- Define a toWire()/snapshot() method per content producer so only plain data reaches slots.
When it happens
Trigger: Passing slot content containing non-JSON-serializable values: circular object references, BigInt, functions, class instances with methods that the canonicalizer walks, `undefined` in arrays, or very deep structures blowing the serializer stack.
Common situations: Putting SDK/provider client objects or tool handles directly into slot content instead of plain data; content pulled from structured-clone contexts (DB drivers, WASM boundaries) carrying BigInt; caching parsed objects with parent pointers creating cycles; passing a function as content by mistake.
Understand the failure class
Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.
Related errors
- assembly slot content is not JSON-serializable
- assemble requires model and session_id
- assemble requires model and sessionId
- assembly slot id must be non-empty and unique
- assembly slot id must be non-empty and unique
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/17f031f7eeccdecb.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/typescript/src/index.ts:1837
}
const ids = new Set<string>();
let ledger = assemblyHashLedgers.get(cave);
if (!ledger) {
ledger = new Map<string, string>();
assemblyHashLedgers.set(cave, ledger);
}
const normalized = options.slots.map((slot) => {
if (!slot.id || ids.has(slot.id)) throw new Error(`assembly slot id must be non-empty and unique: ${JSON.stringify(slot.id)}`);
ids.add(slot.id);
if (!["stable", "session", "volatile"].includes(slot.stability)) {
throw new Error(`assembly slot ${JSON.stringify(slot.id)} has unknown stability ${JSON.stringify(slot.stability)}`);
}
let contentJSON: string;
try {
contentJSON = canonicalJSON(slot.content);
} catch {
throw new Error(`assembly slot ${JSON.stringify(slot.id)} content is not JSON-serializable`);
}
const contentHash = sha256Hex(contentJSON);
if (slot.stability !== "volatile") {
const key = `${options.sessionId}\0${slot.id}`;
const previous = ledger.get(key);
if (previous !== undefined && previous !== contentHash) throw new AssemblyStabilityError(slot.id);
ledger.set(key, contentHash);
}
return { ...slot, content: JSON.parse(contentJSON) as unknown };
});
const ordered = (["stable", "session", "volatile"] as const)
.flatMap((stability) => normalized.filter((slot) => slot.stability === stability));
const prefixSlots = ordered.filter((slot) => slot.stability !== "volatile");
const volatileSlots = ordered.filter((slot) => slot.stability === "volatile");
let request: Record<string, unknown>;
let prefixBytes: string;View on GitHub (pinned to 3ee70a1026)