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

  1. Ensure every slot's `content` is plain JSON data — strings, numbers, booleans, null, arrays, and plain objects.
  2. JSON.stringify a test of the content (or run your own cycle check) before assembling to find the offending value.
  3. Strip or convert non-JSON values: `String(bigintValue)` for BigInt, omit functions, break cycles by building a plain snapshot.
  4. 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

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


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)