@tanstack/ai-sandbox
Version:
Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.
125 lines (124 loc) • 5.38 kB
JavaScript
import { isSpecTopLevelKey, tanstackMetadata } from "@tanstack/ai/adapter-internals";
//#region src/chunk-identity.ts
/**
* A deterministic id generator scoped to one run.
*
* Passed as the harness translators' `genId`, so translating the same journal
* prefix twice mints the same message ids. The counter is per-generator, so a
* replay must create a fresh one and start from the journal's first byte —
* which is exactly what the alignment step (a later task) assumes.
*
* No clock, no `Math.random`, no crypto: `next` is the only state, and it is
* seeded fresh for every call to this factory.
*/
function createRunScopedIdGen(runId) {
let next = 0;
return () => {
const id = `${runId}-${next}`;
next += 1;
return id;
};
}
/**
* Fields excluded from a fingerprint because they are wall-clock and therefore
* unreproducible. Kept as an explicit set so adding one is a deliberate,
* reviewable act rather than a silent loosening of the comparison.
*/
var VOLATILE_FIELDS = /* @__PURE__ */ new Set(["timestamp"]);
/**
* The conversation id every chunk carries. Excluded from
* {@link chunkFingerprintIgnoringThreadId} — and ONLY from that variant — so
* alignment can tell an id-only mismatch from a real content divergence.
*/
var THREAD_ID_FIELD = "threadId";
var VOLATILE_AND_THREAD_ID = /* @__PURE__ */ new Set([...VOLATILE_FIELDS, THREAD_ID_FIELD]);
/**
* `dropped` applies at the TOP LEVEL only (nested calls pass `undefined`).
* A `threadId` nested inside, say, a tool call's arguments is real content and
* must keep participating in the comparison.
*/
function stableStringify(value, dropped) {
if (value === null) return "null";
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item, void 0)).join(",")}]`;
if (typeof value === "object") {
const record = value;
return `{${Object.keys(record).filter((key) => dropped === void 0 || !dropped.has(key)).sort().map((key) => {
const entry = record[key];
const encoded = entry === void 0 ? "\"__undefined__\"" : stableStringify(entry, void 0);
return `${JSON.stringify(key)}:${encoded}`;
}).join(",")}}`;
}
const encoded = JSON.stringify(value);
return encoded === void 0 ? "null" : encoded;
}
/**
* A stable, order-independent identity for a chunk, excluding wall-clock
* fields. Used to recognize the chunks a previous host already appended.
*
* - **Key-order independent**: object keys are sorted before stringifying, so
* a JSON round trip through the journal (which does not preserve key order)
* cannot spuriously diverge.
* - **Recurses into nested arrays and objects**: tool-call arguments are
* nested, and a shallow fingerprint would miss a changed argument.
* - **Excludes `timestamp` and leftover adapter extras.** Spec keys
* participate, including `undefined` values. `metadata.tanstack` is dropped
* so stored spec chunks match live adapter yields.
* - **Distinguishes present-but-`undefined` from absent**: `undefined` is
* encoded as the sentinel string `"__undefined__"` rather than dropped, so
* `{a: undefined}` and `{}` do not collide. A translator emitting an
* explicit `undefined` is a different chunk shape and must fingerprint
* differently.
*/
function fingerprintableChunk(chunk) {
const out = {};
for (const [key, value] of Object.entries(chunk)) {
if (key === "timestamp") continue;
if (!isSpecTopLevelKey(chunk.type, key)) continue;
if (key === "metadata" && value != null && typeof value === "object") {
const rest = {};
for (const [metaKey, metaValue] of Object.entries(value)) {
if (metaKey === "tanstack") continue;
rest[metaKey] = metaValue;
}
if (Object.keys(rest).length === 0) continue;
out.metadata = rest;
continue;
}
out[key] = value;
}
return out;
}
function chunkFingerprint(chunk) {
return stableStringify(fingerprintableChunk(chunk), VOLATILE_FIELDS);
}
/**
* {@link chunkFingerprint} with the chunk's own `threadId` also excluded.
*
* NOT an alternative identity — never use it to decide that two chunks are the
* same. Its single purpose is DIAGNOSIS: when a replay diverges from the stored
* log, comparing both fingerprints answers "did the agent behave differently, or
* did only the conversation id move?". Two chunks that match here but not under
* {@link chunkFingerprint} differ in `threadId` and nothing else, which is a
* misconfigured attach route rather than a determinism regression (see
* `JournalReplayThreadIdMismatchError` in `align.ts`).
*/
function chunkFingerprintIgnoringThreadId(chunk) {
return stableStringify(fingerprintableChunk(chunk), VOLATILE_AND_THREAD_ID);
}
/**
* A chunk's own `threadId`, or `undefined` when it carries none.
*
* Reads the field structurally rather than narrowing on `chunk.type`: nearly
* every member of the `StreamChunk` union declares `threadId?: string`, and an
* exhaustive switch would have to be revisited for each new member while adding
* nothing — a chunk with no `threadId` is exactly the `undefined` case.
*/
function chunkThreadId(chunk) {
const value = chunk[THREAD_ID_FIELD];
if (typeof value === "string") return value;
const nested = tanstackMetadata(chunk)?.threadId;
return typeof nested === "string" ? nested : void 0;
}
//#endregion
export { chunkFingerprint, chunkFingerprintIgnoringThreadId, chunkThreadId, createRunScopedIdGen };
//# sourceMappingURL=chunk-identity.js.map