@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
184 lines (168 loc) • 5.89 kB
text/typescript
// Dependency-free SHA-256 (FIPS 180-4). Sync and isomorphic (Node + browser)
// so interrupt hashing needs no crypto library. Users can replace the whole
// algorithm through the `interrupts.hash` option; this is only the default.
// ponytail: bundled ~60-line hash, swap via interrupts.hash if you need more.
const SHA256_K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
])
function rotr(x: number, n: number): number {
return (x >>> n) | (x << (32 - n))
}
function sha256Hex(input: string): string {
const msg = new TextEncoder().encode(input)
const bitLen = msg.length * 8
const withOne = msg.length + 1
const total = withOne + ((56 - (withOne % 64) + 64) % 64) + 8
const bytes = new Uint8Array(total)
bytes.set(msg)
bytes[msg.length] = 0x80
const view = new DataView(bytes.buffer)
view.setUint32(total - 8, Math.floor(bitLen / 0x100000000))
view.setUint32(total - 4, bitLen >>> 0)
let h0 = 0x6a09e667
let h1 = 0xbb67ae85
let h2 = 0x3c6ef372
let h3 = 0xa54ff53a
let h4 = 0x510e527f
let h5 = 0x9b05688c
let h6 = 0x1f83d9ab
let h7 = 0x5be0cd19
const w = new Uint32Array(64)
for (let offset = 0; offset < total; offset += 64) {
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4)
for (let i = 16; i < 64; i++) {
const x15 = w[i - 15] ?? 0
const x2 = w[i - 2] ?? 0
const s0 = rotr(x15, 7) ^ rotr(x15, 18) ^ (x15 >>> 3)
const s1 = rotr(x2, 17) ^ rotr(x2, 19) ^ (x2 >>> 10)
w[i] = ((w[i - 16] ?? 0) + s0 + (w[i - 7] ?? 0) + s1) >>> 0
}
let a = h0
let b = h1
let c = h2
let d = h3
let e = h4
let f = h5
let g = h6
let hh = h7
for (let i = 0; i < 64; i++) {
const bigS1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
const ch = (e & f) ^ (~e & g)
const t1 = (hh + bigS1 + ch + (SHA256_K[i] ?? 0) + (w[i] ?? 0)) >>> 0
const bigS0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
const maj = (a & b) ^ (a & c) ^ (b & c)
const t2 = (bigS0 + maj) >>> 0
hh = g
g = f
f = e
e = (d + t1) >>> 0
d = c
c = b
b = a
a = (t1 + t2) >>> 0
}
h0 = (h0 + a) >>> 0
h1 = (h1 + b) >>> 0
h2 = (h2 + c) >>> 0
h3 = (h3 + d) >>> 0
h4 = (h4 + e) >>> 0
h5 = (h5 + f) >>> 0
h6 = (h6 + g) >>> 0
h7 = (h7 + hh) >>> 0
}
const hex = (n: number): string => (n >>> 0).toString(16).padStart(8, '0')
return (
hex(h0) +
hex(h1) +
hex(h2) +
hex(h3) +
hex(h4) +
hex(h5) +
hex(h6) +
hex(h7)
)
}
/**
* A hash function for interrupt bindings and resolution fingerprints. Receives
* canonical JSON text and returns an opaque string. Must be deterministic; the
* server compares hashes it computed against the ones in a binding, so the same
* function has to run wherever a given binding is produced and checked.
*/
export type InterruptHash = (canonicalJson: string) => string
/** The built-in default: SHA-256, prefixed so the algorithm is self-describing. */
export function defaultInterruptHash(canonicalJson: string): string {
return `sha256:${sha256Hex(canonicalJson)}`
}
function canonical(value: unknown, active: WeakSet<object>): string {
if (value === null) return 'null'
if (typeof value === 'string' || typeof value === 'boolean') {
return JSON.stringify(value)
}
if (typeof value === 'number' && Number.isFinite(value)) {
return JSON.stringify(value)
}
if (typeof value !== 'object') {
throw new TypeError('Interrupt values must be JSON-compatible.')
}
if (active.has(value)) {
throw new TypeError('Interrupt values must not cycle.')
}
if (
!Array.isArray(value) &&
Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null
) {
throw new TypeError('Interrupt values must use plain JSON objects.')
}
active.add(value)
let encoded: string
if (Array.isArray(value)) {
const items: Array<string> = []
for (let index = 0; index < value.length; index++) {
items.push(canonical(value[index], active))
}
encoded = `[${items.join(',')}]`
} else {
const record = value as Record<string, unknown>
encoded = `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonical(record[key], active)}`)
.join(',')}}`
}
active.delete(value)
return encoded
}
export function canonicalInterruptJson(value: unknown): string {
return canonical(value, new WeakSet<object>())
}
export function digestInterruptJson(
canonicalJson: string,
hash: InterruptHash = defaultInterruptHash,
): string {
if (typeof canonicalJson !== 'string') {
throw new TypeError('Interrupt digests require canonical JSON text.')
}
return hash(canonicalJson)
}
function freezeTree(value: unknown): void {
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) {
return
}
Object.values(value).forEach(freezeTree)
Object.freeze(value)
}
export function cloneAndDeepFreezeJson<T>(value: T): T {
const clone: T = JSON.parse(canonicalInterruptJson(value))
freezeTree(clone)
return clone
}