@graphorin/core 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +8 -5
  3. package/dist/channels/index.d.ts +2 -2
  4. package/dist/channels/index.js +2 -2
  5. package/dist/channels/pause.d.ts +47 -2
  6. package/dist/channels/pause.d.ts.map +1 -1
  7. package/dist/channels/pause.js +62 -2
  8. package/dist/channels/pause.js.map +1 -1
  9. package/dist/contracts/checkpoint-store.d.ts +97 -1
  10. package/dist/contracts/checkpoint-store.d.ts.map +1 -1
  11. package/dist/contracts/checkpoint-store.js.map +1 -1
  12. package/dist/contracts/index.d.ts +5 -5
  13. package/dist/contracts/index.js +2 -1
  14. package/dist/contracts/memory-store.d.ts +59 -2
  15. package/dist/contracts/memory-store.d.ts.map +1 -1
  16. package/dist/contracts/provider.d.ts +20 -6
  17. package/dist/contracts/provider.d.ts.map +1 -1
  18. package/dist/contracts/session-store.d.ts +10 -2
  19. package/dist/contracts/session-store.d.ts.map +1 -1
  20. package/dist/contracts/tool.d.ts +75 -1
  21. package/dist/contracts/tool.d.ts.map +1 -1
  22. package/dist/contracts/tool.js +57 -0
  23. package/dist/contracts/tool.js.map +1 -0
  24. package/dist/contracts/tracer.d.ts +53 -5
  25. package/dist/contracts/tracer.d.ts.map +1 -1
  26. package/dist/contracts/tracer.js.map +1 -1
  27. package/dist/index.d.ts +9 -7
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -5
  30. package/dist/package.js +1 -1
  31. package/dist/package.js.map +1 -1
  32. package/dist/types/agent-event-wire.d.ts +74 -0
  33. package/dist/types/agent-event-wire.d.ts.map +1 -0
  34. package/dist/types/agent-event-wire.js +130 -0
  35. package/dist/types/agent-event-wire.js.map +1 -0
  36. package/dist/types/agent-event.d.ts +48 -6
  37. package/dist/types/agent-event.d.ts.map +1 -1
  38. package/dist/types/index.d.ts +3 -2
  39. package/dist/types/index.js +2 -1
  40. package/dist/types/memory.d.ts +11 -0
  41. package/dist/types/memory.d.ts.map +1 -1
  42. package/dist/types/run.d.ts +86 -4
  43. package/dist/types/run.d.ts.map +1 -1
  44. package/dist/types/run.js.map +1 -1
  45. package/dist/types/tool.d.ts +10 -0
  46. package/dist/types/tool.d.ts.map +1 -1
  47. package/dist/types/usage.d.ts +11 -1
  48. package/dist/types/usage.d.ts.map +1 -1
  49. package/dist/types/usage.js.map +1 -1
  50. package/dist/utils/binary-json.d.ts +165 -0
  51. package/dist/utils/binary-json.d.ts.map +1 -0
  52. package/dist/utils/binary-json.js +240 -0
  53. package/dist/utils/binary-json.js.map +1 -0
  54. package/dist/utils/index.d.ts +2 -1
  55. package/dist/utils/index.js +2 -1
  56. package/dist/utils/validation.d.ts +10 -1
  57. package/dist/utils/validation.d.ts.map +1 -1
  58. package/dist/utils/validation.js +1 -1
  59. package/dist/utils/validation.js.map +1 -1
  60. package/package.json +9 -7
  61. package/src/channels/channels.ts +206 -0
  62. package/src/channels/directive.ts +41 -0
  63. package/src/channels/dispatch.ts +37 -0
  64. package/src/channels/durable.ts +151 -0
  65. package/src/channels/index.ts +62 -0
  66. package/src/channels/pause.ts +216 -0
  67. package/src/contracts/auth-token-store.ts +42 -0
  68. package/src/contracts/checkpoint-store.ts +256 -0
  69. package/src/contracts/embedder.ts +42 -0
  70. package/src/contracts/eval-scorer.ts +44 -0
  71. package/src/contracts/index.ts +112 -0
  72. package/src/contracts/local-provider-trust.ts +33 -0
  73. package/src/contracts/logger.ts +61 -0
  74. package/src/contracts/memory-store.ts +187 -0
  75. package/src/contracts/oauth-server-store.ts +78 -0
  76. package/src/contracts/preferred-model.ts +56 -0
  77. package/src/contracts/provider.ts +316 -0
  78. package/src/contracts/reasoning-retention.ts +52 -0
  79. package/src/contracts/redaction-validator.ts +56 -0
  80. package/src/contracts/sandbox.ts +70 -0
  81. package/src/contracts/secret-ref.ts +22 -0
  82. package/src/contracts/secret-value.ts +117 -0
  83. package/src/contracts/secrets-store.ts +90 -0
  84. package/src/contracts/session-store.ts +163 -0
  85. package/src/contracts/token-counter.ts +23 -0
  86. package/src/contracts/tool.ts +397 -0
  87. package/src/contracts/tracer.ts +219 -0
  88. package/src/contracts/trigger-store.ts +40 -0
  89. package/src/index.ts +23 -0
  90. package/src/types/agent-event-wire.ts +193 -0
  91. package/src/types/agent-event.ts +579 -0
  92. package/src/types/handoff.ts +111 -0
  93. package/src/types/index.ts +148 -0
  94. package/src/types/memory.ts +427 -0
  95. package/src/types/message.ts +174 -0
  96. package/src/types/run.ts +312 -0
  97. package/src/types/sensitivity.ts +35 -0
  98. package/src/types/session-scope.ts +18 -0
  99. package/src/types/stop-condition.ts +108 -0
  100. package/src/types/tool-call.ts +24 -0
  101. package/src/types/tool.ts +324 -0
  102. package/src/types/usage.ts +120 -0
  103. package/src/types/workflow-event.ts +132 -0
  104. package/src/utils/assert-never.ts +24 -0
  105. package/src/utils/async-context.ts +55 -0
  106. package/src/utils/binary-json.ts +425 -0
  107. package/src/utils/hash.ts +122 -0
  108. package/src/utils/index.ts +57 -0
  109. package/src/utils/streams.ts +233 -0
  110. package/src/utils/validation.ts +82 -0
@@ -0,0 +1,425 @@
1
+ /**
2
+ * JSON-safe projection of binary-bearing message content (W-004).
3
+ *
4
+ * `Message` and `ToolResult.contentParts` carry `Uint8Array | URL`
5
+ * payloads that a naive `JSON.stringify` silently corrupts: a
6
+ * `Uint8Array` turns into an object with numeric keys and a `URL`
7
+ * into `{}`. The wire types below replace those fields with explicit
8
+ * {@link EncodedBytes} / {@link EncodedUrl} envelopes so a run state
9
+ * (or any message transcript) survives `JSON.parse(JSON.stringify(x))`
10
+ * byte-for-byte.
11
+ *
12
+ * The codec is runtime-neutral by design: base64 is hand-rolled (no
13
+ * `Buffer`, no `btoa`) so the module works in browsers, workers and
14
+ * edge runtimes exactly as it does under Node.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ import type {
20
+ AssistantMessage,
21
+ AudioContent,
22
+ FileContent,
23
+ ImageContent,
24
+ Message,
25
+ MessageContent,
26
+ SystemMessage,
27
+ ToolMessage,
28
+ UserMessage,
29
+ } from '../types/message.js';
30
+ import type { PendingSubRun, RunState, RunStep } from '../types/run.js';
31
+ import type { CompletedToolCall, ToolError, ToolResult } from '../types/tool.js';
32
+
33
+ /**
34
+ * Base64-encoded binary payload as it appears on the wire.
35
+ *
36
+ * @stable
37
+ */
38
+ export interface EncodedBytes {
39
+ readonly enc: 'base64';
40
+ readonly data: string;
41
+ }
42
+
43
+ /**
44
+ * URL reference as it appears on the wire (`URL` instances do not
45
+ * survive `JSON.stringify`).
46
+ *
47
+ * @stable
48
+ */
49
+ export interface EncodedUrl {
50
+ readonly enc: 'url';
51
+ readonly href: string;
52
+ }
53
+
54
+ /**
55
+ * Wire form of a `Uint8Array | URL` binary field.
56
+ *
57
+ * @stable
58
+ */
59
+ export type EncodedBinary = EncodedBytes | EncodedUrl;
60
+
61
+ /** Wire twin of {@link ImageContent}. @stable */
62
+ export interface WireImageContent extends Omit<ImageContent, 'image'> {
63
+ readonly image: EncodedBinary;
64
+ }
65
+
66
+ /** Wire twin of {@link AudioContent}. @stable */
67
+ export interface WireAudioContent extends Omit<AudioContent, 'audio'> {
68
+ readonly audio: EncodedBinary;
69
+ }
70
+
71
+ /** Wire twin of {@link FileContent}. @stable */
72
+ export interface WireFileContent extends Omit<FileContent, 'file'> {
73
+ readonly file: EncodedBinary;
74
+ }
75
+
76
+ /**
77
+ * JSON-safe twin of {@link MessageContent}: binary-bearing variants
78
+ * carry {@link EncodedBinary} envelopes, text/reasoning variants pass
79
+ * through untouched.
80
+ *
81
+ * @stable
82
+ */
83
+ export type WireMessageContent =
84
+ | Exclude<MessageContent, ImageContent | AudioContent | FileContent>
85
+ | WireImageContent
86
+ | WireAudioContent
87
+ | WireFileContent;
88
+
89
+ /** Wire twin of {@link UserMessage}. @stable */
90
+ export interface WireUserMessage extends Omit<UserMessage, 'content'> {
91
+ readonly content: string | readonly WireMessageContent[];
92
+ }
93
+
94
+ /** Wire twin of {@link AssistantMessage}. @stable */
95
+ export interface WireAssistantMessage extends Omit<AssistantMessage, 'content'> {
96
+ readonly content: string | readonly WireMessageContent[];
97
+ }
98
+
99
+ /** Wire twin of {@link ToolMessage}. @stable */
100
+ export interface WireToolMessage extends Omit<ToolMessage, 'content'> {
101
+ readonly content: string | readonly WireMessageContent[];
102
+ }
103
+
104
+ /**
105
+ * JSON-safe twin of {@link Message}. System messages are plain strings
106
+ * and pass through unchanged.
107
+ *
108
+ * @stable
109
+ */
110
+ export type WireMessage = SystemMessage | WireUserMessage | WireAssistantMessage | WireToolMessage;
111
+
112
+ /** Wire twin of {@link ToolResult}: `contentParts` are encoded. @stable */
113
+ export type WireToolResult<TOutput = unknown> = Omit<ToolResult<TOutput>, 'contentParts'> & {
114
+ readonly contentParts?: readonly WireMessageContent[];
115
+ };
116
+
117
+ /** Wire twin of `ToolOutcome`. @stable */
118
+ export type WireToolOutcome<TOutput = unknown> = WireToolResult<TOutput> | ToolError;
119
+
120
+ /** Wire twin of {@link CompletedToolCall}. @stable */
121
+ export type WireCompletedToolCall<TOutput = unknown> = Omit<
122
+ CompletedToolCall<TOutput>,
123
+ 'outcome'
124
+ > & {
125
+ readonly outcome: WireToolOutcome<TOutput>;
126
+ };
127
+
128
+ /** Wire twin of {@link RunStep}. @stable */
129
+ export type WireRunStep = Omit<RunStep, 'toolCalls'> & {
130
+ readonly toolCalls: readonly WireCompletedToolCall[];
131
+ };
132
+
133
+ /** Wire twin of {@link PendingSubRun}: the child state recurses. @stable */
134
+ export type WirePendingSubRun = Omit<PendingSubRun, 'state'> & {
135
+ readonly state: WireRunState;
136
+ };
137
+
138
+ /**
139
+ * JSON-safe twin of {@link RunState}: `messages`, every
140
+ * `steps[].toolCalls[].outcome.contentParts`, and each parked
141
+ * `pendingSubRuns[].state` (recursively, W-001) are projected through
142
+ * the binary codec. Everything else is structurally identical.
143
+ *
144
+ * `pendingApprovals[].args` and `ToolResult.output` are model-produced
145
+ * JSON and are assumed JSON-safe already - the projection does not
146
+ * walk them.
147
+ *
148
+ * @stable
149
+ */
150
+ export type WireRunState = Omit<RunState, 'messages' | 'steps' | 'pendingSubRuns'> & {
151
+ readonly messages: readonly WireMessage[];
152
+ readonly steps: readonly WireRunStep[];
153
+ readonly pendingSubRuns?: readonly WirePendingSubRun[];
154
+ };
155
+
156
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
157
+
158
+ const B64_LOOKUP: ReadonlyMap<string, number> = new Map(
159
+ [...B64_ALPHABET].map((ch, i) => [ch, i] as const),
160
+ );
161
+
162
+ /**
163
+ * Encode bytes as standard (padded) base64 without relying on `Buffer`
164
+ * or `btoa`.
165
+ *
166
+ * @stable
167
+ */
168
+ export function bytesToBase64(bytes: Uint8Array): string {
169
+ let out = '';
170
+ for (let i = 0; i < bytes.length; i += 3) {
171
+ const b0 = bytes[i] as number;
172
+ const b1 = i + 1 < bytes.length ? (bytes[i + 1] as number) : undefined;
173
+ const b2 = i + 2 < bytes.length ? (bytes[i + 2] as number) : undefined;
174
+ out += B64_ALPHABET[b0 >> 2];
175
+ out += B64_ALPHABET[((b0 & 0x03) << 4) | ((b1 ?? 0) >> 4)];
176
+ out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2) | ((b2 ?? 0) >> 6)];
177
+ out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
178
+ }
179
+ return out;
180
+ }
181
+
182
+ /**
183
+ * Decode standard base64 (padding optional). Throws on characters
184
+ * outside the base64 alphabet.
185
+ *
186
+ * @stable
187
+ */
188
+ export function base64ToBytes(data: string): Uint8Array {
189
+ // Strip trailing padding with a loop: /=+$/ backtracks polynomially
190
+ // on '='-heavy adversarial input (CodeQL js/polynomial-redos).
191
+ let end = data.length;
192
+ while (end > 0 && data.charCodeAt(end - 1) === 0x3d /* '=' */) end -= 1;
193
+ const stripped = data.slice(0, end);
194
+ const out = new Uint8Array(Math.floor((stripped.length * 6) / 8));
195
+ let offset = 0;
196
+ let buffer = 0;
197
+ let bits = 0;
198
+ for (const ch of stripped) {
199
+ const value = B64_LOOKUP.get(ch);
200
+ if (value === undefined) {
201
+ throw new Error(`base64ToBytes: invalid base64 character ${JSON.stringify(ch)}`);
202
+ }
203
+ buffer = (buffer << 6) | value;
204
+ bits += 6;
205
+ if (bits >= 8) {
206
+ bits -= 8;
207
+ out[offset] = (buffer >> bits) & 0xff;
208
+ offset += 1;
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+
214
+ function isRecord(v: unknown): v is Record<string, unknown> {
215
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
216
+ }
217
+
218
+ function encodeBinary(value: Uint8Array | URL | EncodedBinary): EncodedBinary {
219
+ if (value instanceof Uint8Array) return { enc: 'base64', data: bytesToBase64(value) };
220
+ if (value instanceof URL) return { enc: 'url', href: value.href };
221
+ // Already wire-encoded (idempotent re-projection).
222
+ return value;
223
+ }
224
+
225
+ /**
226
+ * Repair the exact corruption `JSON.stringify(Uint8Array)` produces: an
227
+ * object whose keys are the dense indices `0..n-1` with byte values.
228
+ * Applied ONLY to known binary fields of legacy (schema <= 1.1)
229
+ * payloads; anything else is returned as-is.
230
+ */
231
+ function repairNumericKeyBytes(value: Record<string, unknown>): Uint8Array | undefined {
232
+ const keys = Object.keys(value);
233
+ if (keys.length === 0) return undefined;
234
+ const out = new Uint8Array(keys.length);
235
+ for (let i = 0; i < keys.length; i += 1) {
236
+ const raw = value[String(i)];
237
+ if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 0 || raw > 255) {
238
+ return undefined;
239
+ }
240
+ out[i] = raw;
241
+ }
242
+ return out;
243
+ }
244
+
245
+ function decodeBinary(value: unknown): Uint8Array | URL | unknown {
246
+ if (value instanceof Uint8Array || value instanceof URL) return value;
247
+ if (!isRecord(value)) return value;
248
+ if (value.enc === 'base64' && typeof value.data === 'string') {
249
+ return base64ToBytes(value.data);
250
+ }
251
+ if (value.enc === 'url' && typeof value.href === 'string') {
252
+ return new URL(value.href);
253
+ }
254
+ // Legacy repair path: schema 1.0/1.1 checkpoints stringified the raw
255
+ // Uint8Array into a numeric-key object. A corrupted URL serialized to
256
+ // `{}` and is unrecoverable - it stays as-is.
257
+ const repaired = repairNumericKeyBytes(value);
258
+ return repaired ?? value;
259
+ }
260
+
261
+ function encodeContentPart(part: MessageContent | WireMessageContent): WireMessageContent {
262
+ switch (part.type) {
263
+ case 'image':
264
+ return { ...part, image: encodeBinary(part.image) };
265
+ case 'audio':
266
+ return { ...part, audio: encodeBinary(part.audio) };
267
+ case 'file':
268
+ return { ...part, file: encodeBinary(part.file) };
269
+ default:
270
+ return part;
271
+ }
272
+ }
273
+
274
+ function decodeContentPart(part: WireMessageContent | MessageContent): MessageContent {
275
+ switch (part.type) {
276
+ case 'image':
277
+ return { ...part, image: decodeBinary(part.image) as Uint8Array | URL };
278
+ case 'audio':
279
+ return { ...part, audio: decodeBinary(part.audio) as Uint8Array | URL };
280
+ case 'file':
281
+ return { ...part, file: decodeBinary(part.file) as Uint8Array | URL };
282
+ default:
283
+ return part;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Project multimodal content parts into their JSON-safe wire form.
289
+ * Text and reasoning parts pass through untouched.
290
+ *
291
+ * @stable
292
+ */
293
+ export function toJsonSafeContentParts(
294
+ parts: readonly MessageContent[],
295
+ ): readonly WireMessageContent[] {
296
+ return parts.map(encodeContentPart);
297
+ }
298
+
299
+ /**
300
+ * Inverse of {@link toJsonSafeContentParts}. Accepts legacy corrupted
301
+ * payloads (numeric-key byte objects) and repairs them best-effort.
302
+ *
303
+ * @stable
304
+ */
305
+ export function fromJsonSafeContentParts(
306
+ parts: readonly WireMessageContent[],
307
+ ): readonly MessageContent[] {
308
+ return parts.map(decodeContentPart);
309
+ }
310
+
311
+ /**
312
+ * Project a {@link Message} into its JSON-safe wire form. Idempotent:
313
+ * projecting an already-wire message returns an equivalent value.
314
+ *
315
+ * @stable
316
+ */
317
+ export function toJsonSafeMessage(message: Message | WireMessage): WireMessage {
318
+ if (message.role === 'system' || typeof message.content === 'string') {
319
+ return message as WireMessage;
320
+ }
321
+ return { ...message, content: message.content.map(encodeContentPart) } as WireMessage;
322
+ }
323
+
324
+ /**
325
+ * Inverse of {@link toJsonSafeMessage}.
326
+ *
327
+ * @stable
328
+ */
329
+ export function fromJsonSafeMessage(message: WireMessage | Message): Message {
330
+ if (message.role === 'system' || typeof message.content === 'string') {
331
+ return message as Message;
332
+ }
333
+ return { ...message, content: message.content.map(decodeContentPart) } as Message;
334
+ }
335
+
336
+ function encodeOutcome(outcome: CompletedToolCall['outcome']): WireToolOutcome {
337
+ // Defensive: tolerate loose historical/fixture shapes where the
338
+ // outcome is missing or not an object.
339
+ if (isRecord(outcome) && Array.isArray(outcome.contentParts)) {
340
+ return {
341
+ ...outcome,
342
+ contentParts: toJsonSafeContentParts(outcome.contentParts as readonly MessageContent[]),
343
+ } as WireToolOutcome;
344
+ }
345
+ return outcome as WireToolOutcome;
346
+ }
347
+
348
+ function decodeOutcome(outcome: WireToolOutcome): CompletedToolCall['outcome'] {
349
+ if (isRecord(outcome) && Array.isArray(outcome.contentParts)) {
350
+ return {
351
+ ...outcome,
352
+ contentParts: fromJsonSafeContentParts(outcome.contentParts as readonly WireMessageContent[]),
353
+ } as CompletedToolCall['outcome'];
354
+ }
355
+ return outcome as CompletedToolCall['outcome'];
356
+ }
357
+
358
+ function encodeStep(step: RunStep | WireRunStep): WireRunStep {
359
+ if (step.toolCalls.length === 0) return step as WireRunStep;
360
+ return {
361
+ ...step,
362
+ toolCalls: step.toolCalls.map((tc) => ({
363
+ ...tc,
364
+ outcome: encodeOutcome(tc.outcome as CompletedToolCall['outcome']),
365
+ })),
366
+ };
367
+ }
368
+
369
+ function decodeStep(step: WireRunStep): RunStep {
370
+ if (step.toolCalls.length === 0) return step as unknown as RunStep;
371
+ return {
372
+ ...step,
373
+ toolCalls: step.toolCalls.map((tc) => ({ ...tc, outcome: decodeOutcome(tc.outcome) })),
374
+ } as unknown as RunStep;
375
+ }
376
+
377
+ /**
378
+ * Project a full {@link RunState} into its JSON-safe {@link WireRunState}
379
+ * twin: `messages` and `steps[].toolCalls[].outcome.contentParts` go
380
+ * through the binary codec, everything else is copied structurally.
381
+ *
382
+ * @stable
383
+ */
384
+ export function toJsonSafeRunState(state: RunState | WireRunState): WireRunState {
385
+ const subRuns = state.pendingSubRuns;
386
+ return {
387
+ ...state,
388
+ messages: state.messages.map(toJsonSafeMessage),
389
+ steps: state.steps.map(encodeStep),
390
+ // W-001: parked child states carry binary-bearing messages of their
391
+ // own - project them recursively (idempotent, like every codec leg).
392
+ ...(subRuns !== undefined && subRuns.length > 0
393
+ ? {
394
+ pendingSubRuns: subRuns.map((sub) => ({
395
+ ...sub,
396
+ state: toJsonSafeRunState(sub.state),
397
+ })),
398
+ }
399
+ : {}),
400
+ } as WireRunState;
401
+ }
402
+
403
+ /**
404
+ * Inverse of {@link toJsonSafeRunState}. Best-effort: legacy corrupted
405
+ * binary fields (numeric-key byte objects from schema <= 1.1 payloads)
406
+ * are repaired to `Uint8Array`; unrecoverable shapes are left as-is.
407
+ *
408
+ * @stable
409
+ */
410
+ export function fromJsonSafeRunState(state: WireRunState): RunState {
411
+ const subRuns = state.pendingSubRuns;
412
+ return {
413
+ ...state,
414
+ messages: state.messages.map(fromJsonSafeMessage),
415
+ steps: state.steps.map(decodeStep),
416
+ ...(subRuns !== undefined && subRuns.length > 0
417
+ ? {
418
+ pendingSubRuns: subRuns.map((sub) => ({
419
+ ...sub,
420
+ state: fromJsonSafeRunState(sub.state),
421
+ })),
422
+ }
423
+ : {}),
424
+ } as RunState;
425
+ }
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /**
4
+ * MD5 hex digest. Used by the memory layer for content deduplication
5
+ * (`MD5(content)` is the dedup key for incoming facts / messages).
6
+ *
7
+ * MD5 is **not** collision-resistant (practical chosen-prefix
8
+ * collisions exist) and is **not** used as a cryptographic primitive
9
+ * here - it is a fast content-addressing convenience where an
10
+ * adversarial collision merely suppresses a duplicate memory write.
11
+ * Do not use this helper for password hashing, MAC, tamper evidence,
12
+ * or any other security-sensitive use case (use `@graphorin/security`
13
+ * for those; the audit log uses SHA-256).
14
+ *
15
+ * @stable
16
+ */
17
+ export function md5(content: string | Uint8Array): string {
18
+ const h = createHash('md5');
19
+ if (typeof content === 'string') {
20
+ h.update(content, 'utf8');
21
+ } else {
22
+ h.update(content);
23
+ }
24
+ return h.digest('hex');
25
+ }
26
+
27
+ /**
28
+ * Pure-JS XXH32 implementation. Used by the memory-modification guard
29
+ * - fast, non-cryptographic content fingerprinting (`xxhash(content)`
30
+ * tracks whether a tool's view of memory has shifted while the LLM was
31
+ * thinking).
32
+ *
33
+ * Not security-sensitive - never use for tampering detection of an
34
+ * untrusted attacker; for that the audit log uses SHA-256 (in
35
+ * `@graphorin/security`).
36
+ *
37
+ * @stable
38
+ */
39
+ export function xxhash(input: string | Uint8Array, seed = 0): string {
40
+ const bytes = typeof input === 'string' ? utf8Encode(input) : input;
41
+ const h = xxh32(bytes, seed >>> 0);
42
+ return h.toString(16).padStart(8, '0');
43
+ }
44
+
45
+ const PRIME32_1 = 0x9e3779b1 >>> 0;
46
+ const PRIME32_2 = 0x85ebca77 >>> 0;
47
+ const PRIME32_3 = 0xc2b2ae3d >>> 0;
48
+ const PRIME32_4 = 0x27d4eb2f >>> 0;
49
+ const PRIME32_5 = 0x165667b1 >>> 0;
50
+
51
+ const utf8 = new TextEncoder();
52
+ function utf8Encode(s: string): Uint8Array {
53
+ return utf8.encode(s);
54
+ }
55
+
56
+ function rotl32(x: number, r: number): number {
57
+ return ((x << r) | (x >>> (32 - r))) >>> 0;
58
+ }
59
+
60
+ function mul32(a: number, b: number): number {
61
+ // Multiply two 32-bit unsigned ints with overflow truncation.
62
+ const aLo = a & 0xffff;
63
+ const aHi = a >>> 16;
64
+ const bLo = b & 0xffff;
65
+ const bHi = b >>> 16;
66
+ return (aLo * bLo + (((aLo * bHi + aHi * bLo) << 16) >>> 0)) >>> 0;
67
+ }
68
+
69
+ function read32LE(buf: Uint8Array, offset: number): number {
70
+ const b0 = buf[offset] ?? 0;
71
+ const b1 = buf[offset + 1] ?? 0;
72
+ const b2 = buf[offset + 2] ?? 0;
73
+ const b3 = buf[offset + 3] ?? 0;
74
+ return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
75
+ }
76
+
77
+ function xxh32(input: Uint8Array, seed: number): number {
78
+ const len = input.length;
79
+ let h32: number;
80
+ let i = 0;
81
+
82
+ if (len >= 16) {
83
+ let v1 = (seed + PRIME32_1 + PRIME32_2) >>> 0;
84
+ let v2 = (seed + PRIME32_2) >>> 0;
85
+ let v3 = seed >>> 0;
86
+ let v4 = (seed - PRIME32_1) >>> 0;
87
+
88
+ while (i + 16 <= len) {
89
+ v1 = mul32(rotl32((v1 + mul32(read32LE(input, i), PRIME32_2)) >>> 0, 13), PRIME32_1);
90
+ i += 4;
91
+ v2 = mul32(rotl32((v2 + mul32(read32LE(input, i), PRIME32_2)) >>> 0, 13), PRIME32_1);
92
+ i += 4;
93
+ v3 = mul32(rotl32((v3 + mul32(read32LE(input, i), PRIME32_2)) >>> 0, 13), PRIME32_1);
94
+ i += 4;
95
+ v4 = mul32(rotl32((v4 + mul32(read32LE(input, i), PRIME32_2)) >>> 0, 13), PRIME32_1);
96
+ i += 4;
97
+ }
98
+
99
+ h32 = (rotl32(v1, 1) + rotl32(v2, 7) + rotl32(v3, 12) + rotl32(v4, 18)) >>> 0;
100
+ } else {
101
+ h32 = (seed + PRIME32_5) >>> 0;
102
+ }
103
+
104
+ h32 = (h32 + len) >>> 0;
105
+
106
+ while (i + 4 <= len) {
107
+ h32 = mul32(rotl32((h32 + mul32(read32LE(input, i), PRIME32_3)) >>> 0, 17), PRIME32_4);
108
+ i += 4;
109
+ }
110
+
111
+ while (i < len) {
112
+ const byte = input[i] ?? 0;
113
+ h32 = mul32(rotl32((h32 + mul32(byte >>> 0, PRIME32_5)) >>> 0, 11), PRIME32_1);
114
+ i++;
115
+ }
116
+
117
+ h32 = mul32(h32 ^ (h32 >>> 15), PRIME32_2);
118
+ h32 = mul32(h32 ^ (h32 >>> 13), PRIME32_3);
119
+ h32 = (h32 ^ (h32 >>> 16)) >>> 0;
120
+
121
+ return h32;
122
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Tiny dependency-free helpers used across the @graphorin/* packages.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export { assertNever } from './assert-never.js';
8
+ export type { AsyncContext } from './async-context.js';
9
+ export { createAsyncContext } from './async-context.js';
10
+ export type {
11
+ EncodedBinary,
12
+ EncodedBytes,
13
+ EncodedUrl,
14
+ WireAssistantMessage,
15
+ WireAudioContent,
16
+ WireCompletedToolCall,
17
+ WireFileContent,
18
+ WireImageContent,
19
+ WireMessage,
20
+ WireMessageContent,
21
+ WireRunState,
22
+ WireRunStep,
23
+ WireToolMessage,
24
+ WireToolOutcome,
25
+ WireToolResult,
26
+ WireUserMessage,
27
+ } from './binary-json.js';
28
+ export {
29
+ base64ToBytes,
30
+ bytesToBase64,
31
+ fromJsonSafeContentParts,
32
+ fromJsonSafeMessage,
33
+ fromJsonSafeRunState,
34
+ toJsonSafeContentParts,
35
+ toJsonSafeMessage,
36
+ toJsonSafeRunState,
37
+ } from './binary-json.js';
38
+ export { md5, xxhash } from './hash.js';
39
+ export {
40
+ collect,
41
+ filter,
42
+ mapStream,
43
+ merge,
44
+ take,
45
+ takeWhile,
46
+ withSignal,
47
+ } from './streams.js';
48
+ export type {
49
+ ValidationResult,
50
+ ZodLikeError,
51
+ ZodLikeSafeParseResult,
52
+ ZodLikeSchema,
53
+ } from './validation.js';
54
+ export {
55
+ validate,
56
+ validateOrThrow,
57
+ } from './validation.js';