@replayci/replay 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,2197 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ observe: () => observe,
24
+ prepareContracts: () => prepareContracts,
25
+ validate: () => validate
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/observe.ts
30
+ var import_contracts_core = require("@replayci/contracts-core");
31
+
32
+ // src/buffer.ts
33
+ var DEFAULT_CAPTURE_ENDPOINT = "https://app.replayci.com";
34
+ var CAPTURES_PATH = "/api/v1/captures";
35
+ var DEFAULT_MAX_BUFFER = 100;
36
+ var HARD_MAX_BUFFER = 1e3;
37
+ var MAX_ITEM_SIZE = 1e6;
38
+ var MAX_BATCH_SIZE = 50;
39
+ var DEFAULT_FLUSH_MS = 5e3;
40
+ var DEFAULT_TIMEOUT_MS = 5e3;
41
+ var MAX_SEND_TIMEOUT = 1e4;
42
+ var CIRCUIT_BREAKER_FAILURE_LIMIT = 5;
43
+ var CIRCUIT_BREAKER_MS = 10 * 6e4;
44
+ var beforeExitRegistered = false;
45
+ var activeBuffers = /* @__PURE__ */ new Set();
46
+ var CaptureBuffer = class {
47
+ maxBuffer;
48
+ flushMs;
49
+ timeoutMs;
50
+ apiKey;
51
+ endpoint;
52
+ diagnostics;
53
+ fetchImpl;
54
+ now;
55
+ queue = [];
56
+ timer;
57
+ flushPromise;
58
+ failureCount = 0;
59
+ circuitOpenUntil = 0;
60
+ remoteDisabled = false;
61
+ closed = false;
62
+ constructor(opts) {
63
+ this.apiKey = opts.apiKey;
64
+ this.endpoint = normalizeEndpoint(opts.endpoint);
65
+ this.maxBuffer = clampMaxBuffer(opts.maxBuffer);
66
+ this.flushMs = clampPositiveNumber(opts.flushMs, DEFAULT_FLUSH_MS);
67
+ this.timeoutMs = Math.min(
68
+ clampPositiveNumber(opts.timeoutMs, DEFAULT_TIMEOUT_MS),
69
+ MAX_SEND_TIMEOUT
70
+ );
71
+ this.diagnostics = opts.diagnostics;
72
+ this.fetchImpl = opts.fetchImpl ?? fetch;
73
+ this.now = opts.now ?? Date.now;
74
+ this.scheduleNextDrain();
75
+ }
76
+ get size() {
77
+ return this.queue.length;
78
+ }
79
+ get consecutiveFailures() {
80
+ return this.failureCount;
81
+ }
82
+ get disabledUntil() {
83
+ return this.circuitOpenUntil;
84
+ }
85
+ get isRemoteDisabled() {
86
+ return this.remoteDisabled;
87
+ }
88
+ push(item) {
89
+ if (this.closed || this.remoteDisabled) {
90
+ return;
91
+ }
92
+ if (!fitsWithinItemLimit(item)) {
93
+ return;
94
+ }
95
+ if (this.queue.length >= this.maxBuffer) {
96
+ this.queue.shift();
97
+ emitDiagnostics(this.diagnostics, {
98
+ type: "buffer_overflow",
99
+ dropped: 1
100
+ });
101
+ }
102
+ this.queue.push(item);
103
+ }
104
+ flush() {
105
+ if (this.closed || this.remoteDisabled) {
106
+ return Promise.resolve();
107
+ }
108
+ if (this.flushPromise) {
109
+ return this.flushPromise;
110
+ }
111
+ const flushPromise = this.flushOnce().catch(() => {
112
+ }).finally(() => {
113
+ if (this.flushPromise === flushPromise) {
114
+ this.flushPromise = void 0;
115
+ }
116
+ });
117
+ this.flushPromise = flushPromise;
118
+ return flushPromise;
119
+ }
120
+ close() {
121
+ if (this.closed) {
122
+ return;
123
+ }
124
+ this.closed = true;
125
+ this.queue.length = 0;
126
+ this.clearTimer();
127
+ unregisterBuffer(this);
128
+ }
129
+ async flushOnce() {
130
+ if (this.closed || this.remoteDisabled || this.queue.length === 0) {
131
+ return;
132
+ }
133
+ const now = this.now();
134
+ if (this.circuitOpenUntil > now) {
135
+ return;
136
+ }
137
+ if (this.circuitOpenUntil !== 0) {
138
+ this.circuitOpenUntil = 0;
139
+ this.failureCount = 0;
140
+ }
141
+ const batch = this.queue.splice(0, Math.min(this.queue.length, MAX_BATCH_SIZE));
142
+ if (batch.length === 0) {
143
+ return;
144
+ }
145
+ let payload = "";
146
+ try {
147
+ payload = JSON.stringify({ captures: batch });
148
+ } catch {
149
+ this.handleFailure();
150
+ return;
151
+ }
152
+ const controller = new AbortController();
153
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
154
+ unrefTimer(timeout);
155
+ try {
156
+ const response = await this.fetchImpl(this.endpoint, {
157
+ method: "POST",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ Authorization: `Bearer ${this.apiKey}`
161
+ },
162
+ body: payload,
163
+ signal: controller.signal
164
+ });
165
+ if (isRemoteDisable(response)) {
166
+ this.remoteDisabled = true;
167
+ this.queue.length = 0;
168
+ this.clearTimer();
169
+ this.failureCount = 0;
170
+ this.circuitOpenUntil = Number.MAX_SAFE_INTEGER;
171
+ return;
172
+ }
173
+ if (!response.ok) {
174
+ this.handleFailure();
175
+ return;
176
+ }
177
+ this.failureCount = 0;
178
+ this.circuitOpenUntil = 0;
179
+ } catch {
180
+ this.handleFailure();
181
+ } finally {
182
+ clearTimeout(timeout);
183
+ }
184
+ }
185
+ handleFailure() {
186
+ this.failureCount += 1;
187
+ if (this.failureCount >= CIRCUIT_BREAKER_FAILURE_LIMIT) {
188
+ this.circuitOpenUntil = this.now() + CIRCUIT_BREAKER_MS;
189
+ this.failureCount = 0;
190
+ }
191
+ }
192
+ scheduleNextDrain() {
193
+ if (this.closed || this.remoteDisabled) {
194
+ return;
195
+ }
196
+ const timeout = setTimeout(() => {
197
+ void this.flush().finally(() => {
198
+ this.scheduleNextDrain();
199
+ });
200
+ }, this.flushMs);
201
+ this.timer = timeout;
202
+ unrefTimer(timeout);
203
+ }
204
+ clearTimer() {
205
+ if (this.timer !== void 0) {
206
+ clearTimeout(this.timer);
207
+ this.timer = void 0;
208
+ }
209
+ }
210
+ };
211
+ function registerBeforeExit(buffer) {
212
+ activeBuffers.add(buffer);
213
+ if (beforeExitRegistered || typeof process === "undefined" || typeof process.on !== "function") {
214
+ return;
215
+ }
216
+ beforeExitRegistered = true;
217
+ process.on("beforeExit", async () => {
218
+ const flushes = [...activeBuffers].map((activeBuffer) => {
219
+ try {
220
+ return activeBuffer.flush();
221
+ } catch {
222
+ return Promise.resolve();
223
+ }
224
+ });
225
+ await Promise.allSettled(flushes);
226
+ });
227
+ }
228
+ function unregisterBuffer(buffer) {
229
+ activeBuffers.delete(buffer);
230
+ }
231
+ function clampMaxBuffer(value) {
232
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
233
+ return DEFAULT_MAX_BUFFER;
234
+ }
235
+ return Math.min(Math.floor(value), HARD_MAX_BUFFER);
236
+ }
237
+ function clampPositiveNumber(value, fallback) {
238
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
239
+ return fallback;
240
+ }
241
+ return Math.floor(value);
242
+ }
243
+ function normalizeEndpoint(endpoint) {
244
+ const base = typeof endpoint === "string" && endpoint.length > 0 ? endpoint : DEFAULT_CAPTURE_ENDPOINT;
245
+ const trimmed = base.replace(/\/+$/, "");
246
+ return trimmed.endsWith(CAPTURES_PATH) ? trimmed : `${trimmed}${CAPTURES_PATH}`;
247
+ }
248
+ function fitsWithinItemLimit(item) {
249
+ try {
250
+ const size = Buffer.byteLength(JSON.stringify(item), "utf8");
251
+ return size <= MAX_ITEM_SIZE;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+ function emitDiagnostics(diagnostics, event) {
257
+ try {
258
+ diagnostics?.(event);
259
+ } catch {
260
+ }
261
+ }
262
+ function isRemoteDisable(response) {
263
+ return response.headers.get("x-replayci-disable")?.toLowerCase() === "true";
264
+ }
265
+ function unrefTimer(timer) {
266
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
267
+ const unref = timer.unref;
268
+ try {
269
+ unref?.call(timer);
270
+ } catch {
271
+ }
272
+ }
273
+ }
274
+
275
+ // src/providers/anthropic.ts
276
+ function extractToolCallsAnthropic(response) {
277
+ return getContentBlocks(response).map((block, index) => normalizeAnthropicToolCall(block, index)).filter((toolCall) => toolCall !== null);
278
+ }
279
+ function extractTextBlocksAnthropic(response) {
280
+ return getContentBlocks(response).map((block) => {
281
+ const record = toRecord(block);
282
+ return record.type === "text" && typeof record.text === "string" ? record.text : null;
283
+ }).filter((text) => text !== null);
284
+ }
285
+ function extractUsageAnthropic(response) {
286
+ const usage = toRecord(toRecord(response).usage);
287
+ const promptTokens = toNumber(usage.input_tokens);
288
+ const completionTokens = toNumber(usage.output_tokens);
289
+ if (promptTokens === void 0 || completionTokens === void 0) {
290
+ return void 0;
291
+ }
292
+ return {
293
+ prompt_tokens: promptTokens,
294
+ completion_tokens: completionTokens,
295
+ total_tokens: promptTokens + completionTokens
296
+ };
297
+ }
298
+ function extractModelAnthropic(response) {
299
+ return toString(toRecord(response).model) ?? "";
300
+ }
301
+ function extractContentAnthropic(response) {
302
+ const textBlocks = extractTextBlocksAnthropic(response);
303
+ return textBlocks.length > 0 ? textBlocks.join("\n") : null;
304
+ }
305
+ function getContentBlocks(response) {
306
+ const content = toRecord(response).content;
307
+ return Array.isArray(content) ? content : [];
308
+ }
309
+ function normalizeAnthropicToolCall(block, index) {
310
+ const record = toRecord(block);
311
+ if (record.type !== "tool_use") {
312
+ return null;
313
+ }
314
+ const name = toString(record.name);
315
+ if (!name) {
316
+ return null;
317
+ }
318
+ return {
319
+ id: toString(record.id) ?? `tool_call_${index}`,
320
+ name,
321
+ arguments: serializeArguments(record.input)
322
+ };
323
+ }
324
+ function serializeArguments(argumentsValue) {
325
+ if (typeof argumentsValue === "string") {
326
+ return argumentsValue;
327
+ }
328
+ if (argumentsValue === void 0) {
329
+ return "null";
330
+ }
331
+ try {
332
+ return JSON.stringify(argumentsValue) ?? "null";
333
+ } catch {
334
+ return "null";
335
+ }
336
+ }
337
+ function toRecord(value) {
338
+ return value !== null && typeof value === "object" ? value : {};
339
+ }
340
+ function toString(value) {
341
+ return typeof value === "string" && value.length > 0 ? value : void 0;
342
+ }
343
+ function toNumber(value) {
344
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
345
+ }
346
+
347
+ // src/providers/openai.ts
348
+ function extractToolCallsOpenAI(response) {
349
+ const message = getOpenAIMessage(response);
350
+ if (!Array.isArray(message.tool_calls)) {
351
+ return [];
352
+ }
353
+ return message.tool_calls.map((toolCall, index) => normalizeOpenAIToolCall(toolCall, index)).filter((toolCall) => toolCall !== null);
354
+ }
355
+ function extractUsageOpenAI(response) {
356
+ const usage = toRecord2(toRecord2(response).usage);
357
+ const promptTokens = toNumber2(usage.prompt_tokens);
358
+ const completionTokens = toNumber2(usage.completion_tokens);
359
+ const totalTokens = toNumber2(usage.total_tokens);
360
+ if (promptTokens === void 0 || completionTokens === void 0 || totalTokens === void 0) {
361
+ return void 0;
362
+ }
363
+ return {
364
+ prompt_tokens: promptTokens,
365
+ completion_tokens: completionTokens,
366
+ total_tokens: totalTokens
367
+ };
368
+ }
369
+ function extractModelOpenAI(response) {
370
+ return toString2(toRecord2(response).model) ?? "";
371
+ }
372
+ function extractContentOpenAI(response) {
373
+ const content = getOpenAIMessage(response).content;
374
+ return typeof content === "string" ? content : null;
375
+ }
376
+ function getOpenAIMessage(response) {
377
+ const record = toRecord2(response);
378
+ if (!Array.isArray(record.choices) || record.choices.length === 0) {
379
+ return {};
380
+ }
381
+ const firstChoice = toRecord2(record.choices[0]);
382
+ return toRecord2(firstChoice.message);
383
+ }
384
+ function normalizeOpenAIToolCall(toolCall, index) {
385
+ const record = toRecord2(toolCall);
386
+ const fn = toRecord2(record.function);
387
+ const name = toString2(fn.name);
388
+ if (!name) {
389
+ return null;
390
+ }
391
+ return {
392
+ id: toString2(record.id) ?? `tool_call_${index}`,
393
+ name,
394
+ // Preserve malformed JSON strings so validate() can emit argument_parse.
395
+ arguments: typeof fn.arguments === "string" ? fn.arguments : serializeArguments2(fn.arguments)
396
+ };
397
+ }
398
+ function serializeArguments2(argumentsValue) {
399
+ if (typeof argumentsValue === "string") {
400
+ return argumentsValue;
401
+ }
402
+ if (argumentsValue === void 0) {
403
+ return "null";
404
+ }
405
+ try {
406
+ return JSON.stringify(argumentsValue) ?? "null";
407
+ } catch {
408
+ return "null";
409
+ }
410
+ }
411
+ function toRecord2(value) {
412
+ return value !== null && typeof value === "object" ? value : {};
413
+ }
414
+ function toString2(value) {
415
+ return typeof value === "string" && value.length > 0 ? value : void 0;
416
+ }
417
+ function toNumber2(value) {
418
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
419
+ }
420
+
421
+ // src/providers/extract.ts
422
+ function extractToolCalls(response, provider) {
423
+ switch (provider) {
424
+ case "openai":
425
+ return extractToolCallsOpenAI(response);
426
+ case "anthropic":
427
+ return extractToolCallsAnthropic(response);
428
+ }
429
+ }
430
+ function extractUsage(response, provider) {
431
+ switch (provider) {
432
+ case "openai":
433
+ return extractUsageOpenAI(response);
434
+ case "anthropic":
435
+ return extractUsageAnthropic(response);
436
+ }
437
+ }
438
+ function extractModel(response, provider) {
439
+ switch (provider) {
440
+ case "openai":
441
+ return extractModelOpenAI(response);
442
+ case "anthropic":
443
+ return extractModelAnthropic(response);
444
+ }
445
+ }
446
+ function extractContent(response, provider) {
447
+ switch (provider) {
448
+ case "openai":
449
+ return extractContentOpenAI(response);
450
+ case "anthropic":
451
+ return extractContentAnthropic(response);
452
+ }
453
+ }
454
+ function extractTextBlocks(response, provider) {
455
+ switch (provider) {
456
+ case "openai":
457
+ return void 0;
458
+ case "anthropic": {
459
+ const textBlocks = extractTextBlocksAnthropic(response);
460
+ return textBlocks.length > 0 ? textBlocks : void 0;
461
+ }
462
+ }
463
+ }
464
+
465
+ // src/errors.ts
466
+ var ReplayConfigurationError = class extends Error {
467
+ constructor(message) {
468
+ super(message);
469
+ this.name = "ReplayConfigurationError";
470
+ }
471
+ };
472
+ var ReplayInternalError = class extends Error {
473
+ cause;
474
+ constructor(message, options) {
475
+ super(message);
476
+ this.name = "ReplayInternalError";
477
+ this.cause = options?.cause;
478
+ }
479
+ };
480
+
481
+ // src/providers/detect.ts
482
+ function detectProvider(client) {
483
+ if (hasPath(client, "chat.completions.create")) {
484
+ return "openai";
485
+ }
486
+ if (hasPath(client, "messages.create")) {
487
+ return "anthropic";
488
+ }
489
+ throw new ReplayConfigurationError(
490
+ "Unsupported client. Pass an OpenAI or Anthropic client instance."
491
+ );
492
+ }
493
+ function hasPath(obj, path) {
494
+ if (path.length === 0) {
495
+ return false;
496
+ }
497
+ let current = obj;
498
+ for (const segment of path.split(".")) {
499
+ if (current == null) {
500
+ return false;
501
+ }
502
+ const record = toRecordLike(current);
503
+ if (!(segment in record)) {
504
+ return false;
505
+ }
506
+ current = record[segment];
507
+ }
508
+ return current !== void 0;
509
+ }
510
+ function toRecordLike(value) {
511
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
512
+ return value;
513
+ }
514
+ return {};
515
+ }
516
+
517
+ // src/stream.ts
518
+ function tapAsyncIterable(response, collector, onComplete) {
519
+ if (!isRecordLike(response) || !canPatchProperty(response, Symbol.asyncIterator)) {
520
+ return false;
521
+ }
522
+ const originalIteratorFactory = response[Symbol.asyncIterator];
523
+ if (typeof originalIteratorFactory !== "function") {
524
+ return false;
525
+ }
526
+ let finished = false;
527
+ response[Symbol.asyncIterator] = function patchedAsyncIterator() {
528
+ const iterator = originalIteratorFactory.call(this);
529
+ const complete = () => {
530
+ if (finished) {
531
+ return;
532
+ }
533
+ finished = true;
534
+ scheduleSoon(() => {
535
+ try {
536
+ onComplete(collector.finalize());
537
+ } catch {
538
+ }
539
+ });
540
+ };
541
+ const fail = () => {
542
+ if (finished) {
543
+ return;
544
+ }
545
+ finished = true;
546
+ scheduleSoon(() => {
547
+ try {
548
+ onComplete(null);
549
+ } catch {
550
+ }
551
+ });
552
+ };
553
+ return {
554
+ next(value) {
555
+ return Promise.resolve(iterator.next?.(value)).then((result) => {
556
+ if (result.done) {
557
+ complete();
558
+ return result;
559
+ }
560
+ try {
561
+ collector.add(result.value);
562
+ } catch {
563
+ }
564
+ return result;
565
+ }, (error) => {
566
+ fail();
567
+ throw error;
568
+ });
569
+ },
570
+ return(value) {
571
+ if (typeof iterator.return !== "function") {
572
+ complete();
573
+ return Promise.resolve({ done: true, value });
574
+ }
575
+ return Promise.resolve(iterator.return(value)).then((result) => {
576
+ complete();
577
+ return result;
578
+ }, (error) => {
579
+ fail();
580
+ throw error;
581
+ });
582
+ },
583
+ throw(error) {
584
+ if (typeof iterator.throw !== "function") {
585
+ fail();
586
+ return Promise.reject(error);
587
+ }
588
+ return Promise.resolve(iterator.throw(error)).then((result) => {
589
+ fail();
590
+ return result;
591
+ }, (thrown) => {
592
+ fail();
593
+ throw thrown;
594
+ });
595
+ },
596
+ [Symbol.asyncIterator]() {
597
+ return this;
598
+ }
599
+ };
600
+ };
601
+ return true;
602
+ }
603
+ function createStreamCollector(provider, captureLevel) {
604
+ return provider === "anthropic" ? createAnthropicCollector(captureLevel) : createOpenAiCollector(captureLevel);
605
+ }
606
+ function createOpenAiCollector(captureLevel) {
607
+ const toolCalls = /* @__PURE__ */ new Map();
608
+ const contentParts = [];
609
+ let model = "";
610
+ let usage;
611
+ let byteEstimate = 0;
612
+ let overflow = false;
613
+ return {
614
+ add(chunk) {
615
+ if (overflow) {
616
+ return;
617
+ }
618
+ const record = toRecord3(chunk);
619
+ const modelValue = toString3(record.model);
620
+ if (modelValue) {
621
+ model = modelValue;
622
+ }
623
+ const usageValue = extractOpenAiUsage(record.usage);
624
+ if (usageValue) {
625
+ usage = usageValue;
626
+ }
627
+ const choices = Array.isArray(record.choices) ? record.choices : [];
628
+ for (const choice of choices) {
629
+ const delta = toRecord3(toRecord3(choice).delta);
630
+ if (captureLevel === "full") {
631
+ const content = toString3(delta.content);
632
+ if (content) {
633
+ contentParts.push(content);
634
+ byteEstimate += Buffer.byteLength(content, "utf8");
635
+ }
636
+ }
637
+ const deltaToolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
638
+ for (const [fallbackIndex, rawToolCall] of deltaToolCalls.entries()) {
639
+ const toolCall = toRecord3(rawToolCall);
640
+ const index = toNumber3(toolCall.index) ?? fallbackIndex;
641
+ const fn = toRecord3(toolCall.function);
642
+ const existing = toolCalls.get(index) ?? {
643
+ id: toString3(toolCall.id) ?? `tool_call_${index}`,
644
+ name: "",
645
+ arguments: ""
646
+ };
647
+ const name = toString3(fn.name);
648
+ if (name) {
649
+ existing.name = name;
650
+ byteEstimate += Buffer.byteLength(name, "utf8");
651
+ }
652
+ if (captureLevel !== "metadata") {
653
+ const args = toString3(fn.arguments);
654
+ if (args) {
655
+ existing.arguments += args;
656
+ byteEstimate += Buffer.byteLength(args, "utf8");
657
+ }
658
+ }
659
+ toolCalls.set(index, existing);
660
+ }
661
+ if (byteEstimate > MAX_ITEM_SIZE) {
662
+ overflow = true;
663
+ return;
664
+ }
665
+ }
666
+ },
667
+ finalize() {
668
+ if (overflow) {
669
+ return null;
670
+ }
671
+ const orderedToolCalls = [...toolCalls.entries()].sort((a, b) => a[0] - b[0]).map(([, toolCall]) => ({
672
+ id: toolCall.id,
673
+ name: toolCall.name,
674
+ arguments: captureLevel === "metadata" ? "null" : toolCall.arguments || "null"
675
+ })).filter((toolCall) => toolCall.name.length > 0);
676
+ return {
677
+ model,
678
+ toolCalls: orderedToolCalls,
679
+ content: captureLevel === "full" && contentParts.length > 0 ? contentParts.join("") : null,
680
+ usage
681
+ };
682
+ }
683
+ };
684
+ }
685
+ function createAnthropicCollector(captureLevel) {
686
+ const toolCalls = /* @__PURE__ */ new Map();
687
+ const textBlocks = /* @__PURE__ */ new Map();
688
+ let model = "";
689
+ let inputTokens;
690
+ let outputTokens;
691
+ let byteEstimate = 0;
692
+ let overflow = false;
693
+ return {
694
+ add(chunk) {
695
+ if (overflow) {
696
+ return;
697
+ }
698
+ const record = toRecord3(chunk);
699
+ switch (toString3(record.type)) {
700
+ case "message_start": {
701
+ const message = toRecord3(record.message);
702
+ const modelValue = toString3(message.model);
703
+ if (modelValue) {
704
+ model = modelValue;
705
+ }
706
+ const usage = toRecord3(message.usage);
707
+ const startInputTokens = toNumber3(usage.input_tokens);
708
+ if (startInputTokens !== void 0) {
709
+ inputTokens = startInputTokens;
710
+ }
711
+ break;
712
+ }
713
+ case "message_delta": {
714
+ const usage = toRecord3(record.usage);
715
+ const deltaOutputTokens = toNumber3(usage.output_tokens);
716
+ if (deltaOutputTokens !== void 0) {
717
+ outputTokens = deltaOutputTokens;
718
+ }
719
+ break;
720
+ }
721
+ case "content_block_start": {
722
+ const index = toNumber3(record.index) ?? 0;
723
+ const contentBlock = toRecord3(record.content_block);
724
+ const blockType = toString3(contentBlock.type);
725
+ if (blockType === "text" && captureLevel === "full") {
726
+ const text = toString3(contentBlock.text);
727
+ if (text) {
728
+ textBlocks.set(index, text);
729
+ byteEstimate += Buffer.byteLength(text, "utf8");
730
+ }
731
+ }
732
+ if (blockType === "tool_use") {
733
+ const name = toString3(contentBlock.name);
734
+ if (!name) {
735
+ break;
736
+ }
737
+ const existing = toolCalls.get(index) ?? {
738
+ id: toString3(contentBlock.id) ?? `tool_call_${index}`,
739
+ name,
740
+ arguments: ""
741
+ };
742
+ existing.name = name;
743
+ if (captureLevel !== "metadata" && contentBlock.input !== void 0) {
744
+ existing.arguments = serializeArguments3(contentBlock.input);
745
+ byteEstimate += Buffer.byteLength(existing.arguments, "utf8");
746
+ }
747
+ toolCalls.set(index, existing);
748
+ }
749
+ break;
750
+ }
751
+ case "content_block_delta": {
752
+ const index = toNumber3(record.index) ?? 0;
753
+ const delta = toRecord3(record.delta);
754
+ const deltaType = toString3(delta.type);
755
+ if (deltaType === "text_delta" && captureLevel === "full") {
756
+ const text = toString3(delta.text);
757
+ if (text) {
758
+ textBlocks.set(index, `${textBlocks.get(index) ?? ""}${text}`);
759
+ byteEstimate += Buffer.byteLength(text, "utf8");
760
+ }
761
+ }
762
+ if (deltaType === "input_json_delta" && captureLevel !== "metadata") {
763
+ const partialJson = toString3(delta.partial_json);
764
+ if (partialJson) {
765
+ const existing = toolCalls.get(index) ?? {
766
+ id: `tool_call_${index}`,
767
+ name: "",
768
+ arguments: ""
769
+ };
770
+ existing.arguments += partialJson;
771
+ toolCalls.set(index, existing);
772
+ byteEstimate += Buffer.byteLength(partialJson, "utf8");
773
+ }
774
+ }
775
+ break;
776
+ }
777
+ }
778
+ if (byteEstimate > MAX_ITEM_SIZE) {
779
+ overflow = true;
780
+ }
781
+ },
782
+ finalize() {
783
+ if (overflow) {
784
+ return null;
785
+ }
786
+ const orderedTextBlocks = [...textBlocks.entries()].sort((a, b) => a[0] - b[0]).map(([, text]) => text).filter((text) => text.length > 0);
787
+ const orderedToolCalls = [...toolCalls.entries()].sort((a, b) => a[0] - b[0]).map(([, toolCall]) => ({
788
+ id: toolCall.id,
789
+ name: toolCall.name,
790
+ arguments: captureLevel === "metadata" ? "null" : toolCall.arguments || "null"
791
+ })).filter((toolCall) => toolCall.name.length > 0);
792
+ const usage = inputTokens !== void 0 && outputTokens !== void 0 ? {
793
+ prompt_tokens: inputTokens,
794
+ completion_tokens: outputTokens,
795
+ total_tokens: inputTokens + outputTokens
796
+ } : void 0;
797
+ return {
798
+ model,
799
+ toolCalls: orderedToolCalls,
800
+ content: captureLevel === "full" && orderedTextBlocks.length > 0 ? orderedTextBlocks.join("\n") : null,
801
+ ...captureLevel === "full" && orderedTextBlocks.length > 0 ? { textBlocks: orderedTextBlocks } : {},
802
+ usage
803
+ };
804
+ }
805
+ };
806
+ }
807
+ function canPatchProperty(target, property) {
808
+ if (Object.isFrozen(target)) {
809
+ return false;
810
+ }
811
+ if (!Object.isExtensible(target)) {
812
+ return false;
813
+ }
814
+ let current = target;
815
+ while (current !== null) {
816
+ const descriptor = Object.getOwnPropertyDescriptor(current, property);
817
+ if (!descriptor) {
818
+ current = Object.getPrototypeOf(current);
819
+ continue;
820
+ }
821
+ if ("value" in descriptor) {
822
+ return descriptor.writable !== false;
823
+ }
824
+ return typeof descriptor.set === "function";
825
+ }
826
+ return true;
827
+ }
828
+ function extractOpenAiUsage(value) {
829
+ const usage = toRecord3(value);
830
+ const promptTokens = toNumber3(usage.prompt_tokens);
831
+ const completionTokens = toNumber3(usage.completion_tokens);
832
+ const totalTokens = toNumber3(usage.total_tokens);
833
+ if (promptTokens === void 0 || completionTokens === void 0 || totalTokens === void 0) {
834
+ return void 0;
835
+ }
836
+ return {
837
+ prompt_tokens: promptTokens,
838
+ completion_tokens: completionTokens,
839
+ total_tokens: totalTokens
840
+ };
841
+ }
842
+ function serializeArguments3(value) {
843
+ if (typeof value === "string") {
844
+ return value;
845
+ }
846
+ if (value === void 0) {
847
+ return "null";
848
+ }
849
+ try {
850
+ return JSON.stringify(value) ?? "null";
851
+ } catch {
852
+ return "null";
853
+ }
854
+ }
855
+ function scheduleSoon(fn) {
856
+ try {
857
+ queueMicrotask(fn);
858
+ } catch {
859
+ const timer = setTimeout(fn, 0);
860
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
861
+ try {
862
+ timer.unref?.();
863
+ } catch {
864
+ }
865
+ }
866
+ }
867
+ }
868
+ function isRecordLike(value) {
869
+ return value !== null && (typeof value === "object" || typeof value === "function");
870
+ }
871
+ function toRecord3(value) {
872
+ return value !== null && typeof value === "object" ? value : {};
873
+ }
874
+ function toString3(value) {
875
+ return typeof value === "string" && value.length > 0 ? value : void 0;
876
+ }
877
+ function toNumber3(value) {
878
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
879
+ }
880
+
881
+ // src/runtime.ts
882
+ var runtimeChecked = false;
883
+ function assertSupportedNodeRuntime() {
884
+ if (runtimeChecked) {
885
+ return;
886
+ }
887
+ assertNodeVersionSupported(process.versions?.node);
888
+ runtimeChecked = true;
889
+ }
890
+ function assertNodeVersionSupported(nodeVersion) {
891
+ const major = parseNodeMajorVersion(nodeVersion);
892
+ if (major !== null && major < 18) {
893
+ throw new ReplayConfigurationError(
894
+ `@replayci/replay requires Node.js 18+. Current version: ${nodeVersion}. The SDK uses native fetch, setTimeout().unref(), and process lifecycle hooks that are not available in older versions.`
895
+ );
896
+ }
897
+ }
898
+ function parseNodeMajorVersion(nodeVersion) {
899
+ if (typeof nodeVersion !== "string") {
900
+ return null;
901
+ }
902
+ const match = nodeVersion.match(/^(\d+)/);
903
+ if (!match) {
904
+ return null;
905
+ }
906
+ const major = Number.parseInt(match[1], 10);
907
+ return Number.isFinite(major) ? major : null;
908
+ }
909
+
910
+ // src/captureSchema.ts
911
+ var CAPTURE_SCHEMA_VERSION_LEGACY = "2026-03-04";
912
+ var CAPTURE_SCHEMA_VERSION_CURRENT = "2026-03-06";
913
+ function isRecord(value) {
914
+ return value !== null && typeof value === "object" && !Array.isArray(value);
915
+ }
916
+ function isJsonValue(value) {
917
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
918
+ return true;
919
+ }
920
+ if (Array.isArray(value)) {
921
+ return value.every((item) => isJsonValue(item));
922
+ }
923
+ if (!isRecord(value)) {
924
+ return false;
925
+ }
926
+ return Object.values(value).every((item) => isJsonValue(item));
927
+ }
928
+ function asJsonObject(value, path) {
929
+ if (!isRecord(value) || !isJsonValue(value)) {
930
+ throw new Error(`${path} must be a JSON object`);
931
+ }
932
+ return value;
933
+ }
934
+ function requireString(value, path) {
935
+ if (typeof value !== "string" || value.trim().length === 0) {
936
+ throw new Error(`${path} must be a non-empty string`);
937
+ }
938
+ return value;
939
+ }
940
+ function nullableString(value, path) {
941
+ if (value === null || value === void 0) {
942
+ return null;
943
+ }
944
+ return requireString(value, path);
945
+ }
946
+ function requireStringArray(value, path) {
947
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
948
+ throw new Error(`${path} must be an array of strings`);
949
+ }
950
+ return value;
951
+ }
952
+ function requireNonNegativeInt(value, path) {
953
+ if (!Number.isInteger(value) || Number(value) < 0) {
954
+ throw new Error(`${path} must be a non-negative integer`);
955
+ }
956
+ return Number(value);
957
+ }
958
+ function requireIsoTimestamp(value, path) {
959
+ const timestamp = requireString(value, path);
960
+ if (Number.isNaN(Date.parse(timestamp))) {
961
+ throw new Error(`${path} must be a valid ISO 8601 timestamp`);
962
+ }
963
+ return timestamp;
964
+ }
965
+ function requireProvider(value, path) {
966
+ if (value !== "openai" && value !== "anthropic") {
967
+ throw new Error(`${path} must be one of: openai, anthropic`);
968
+ }
969
+ return value;
970
+ }
971
+ function validateRequest(value, path) {
972
+ const request = asJsonObject(value, path);
973
+ if (!Array.isArray(request.tools)) {
974
+ throw new Error(`${path}.tools must be an array`);
975
+ }
976
+ for (const [index, tool] of request.tools.entries()) {
977
+ if (!isRecord(tool) || typeof tool.name !== "string" || tool.name.length === 0) {
978
+ throw new Error(`${path}.tools[${index}].name must be a non-empty string`);
979
+ }
980
+ if (!isJsonValue(tool)) {
981
+ throw new Error(`${path}.tools[${index}] must be JSON-serializable`);
982
+ }
983
+ }
984
+ if (request.messages !== void 0) {
985
+ if (!Array.isArray(request.messages)) {
986
+ throw new Error(`${path}.messages must be an array when provided`);
987
+ }
988
+ for (const [index, message] of request.messages.entries()) {
989
+ if (!isJsonValue(message)) {
990
+ throw new Error(`${path}.messages[${index}] must be JSON-serializable`);
991
+ }
992
+ }
993
+ }
994
+ if (request.tool_choice !== void 0 && typeof request.tool_choice !== "string") {
995
+ throw new Error(`${path}.tool_choice must be a string when provided`);
996
+ }
997
+ return request;
998
+ }
999
+ function validateResponse(value, path) {
1000
+ const response = asJsonObject(value, path);
1001
+ if (!Array.isArray(response.tool_calls)) {
1002
+ throw new Error(`${path}.tool_calls must be an array`);
1003
+ }
1004
+ for (const [index, toolCall] of response.tool_calls.entries()) {
1005
+ if (!isRecord(toolCall)) {
1006
+ throw new Error(`${path}.tool_calls[${index}] must be an object`);
1007
+ }
1008
+ if (typeof toolCall.name !== "string" || toolCall.name.length === 0) {
1009
+ throw new Error(`${path}.tool_calls[${index}].name must be a non-empty string`);
1010
+ }
1011
+ if (toolCall.arguments !== void 0 && typeof toolCall.arguments !== "string") {
1012
+ throw new Error(`${path}.tool_calls[${index}].arguments must be a string when provided`);
1013
+ }
1014
+ if (!isJsonValue(toolCall)) {
1015
+ throw new Error(`${path}.tool_calls[${index}] must be JSON-serializable`);
1016
+ }
1017
+ }
1018
+ if (response.content !== null && response.content !== void 0 && typeof response.content !== "string") {
1019
+ throw new Error(`${path}.content must be a string or null`);
1020
+ }
1021
+ if (response.text_blocks !== void 0) {
1022
+ if (!Array.isArray(response.text_blocks) || response.text_blocks.some((item) => typeof item !== "string")) {
1023
+ throw new Error(`${path}.text_blocks must be an array of strings when provided`);
1024
+ }
1025
+ }
1026
+ return response;
1027
+ }
1028
+ function validateContractFailure(value, path) {
1029
+ const failure = asJsonObject(value, path);
1030
+ const message = failure.message;
1031
+ const contractFile = failure.contract_file;
1032
+ if (message !== void 0 && typeof message !== "string") {
1033
+ throw new Error(`${path}.message must be a string when provided`);
1034
+ }
1035
+ if (contractFile !== void 0 && typeof contractFile !== "string") {
1036
+ throw new Error(`${path}.contract_file must be a string when provided`);
1037
+ }
1038
+ return {
1039
+ path: requireString(failure.path, `${path}.path`),
1040
+ operator: requireString(failure.operator, `${path}.operator`),
1041
+ expected: failure.expected ?? null,
1042
+ found: failure.found ?? null,
1043
+ ...message !== void 0 ? { message } : {},
1044
+ ...contractFile !== void 0 ? { contract_file: contractFile } : {}
1045
+ };
1046
+ }
1047
+ function validateValidation(value, path) {
1048
+ const validation = asJsonObject(value, path);
1049
+ if (typeof validation.pass !== "boolean") {
1050
+ throw new Error(`${path}.pass must be a boolean`);
1051
+ }
1052
+ if (!Array.isArray(validation.failures)) {
1053
+ throw new Error(`${path}.failures must be an array`);
1054
+ }
1055
+ if (!Array.isArray(validation.contracts_evaluated) || validation.contracts_evaluated.some((item) => typeof item !== "string")) {
1056
+ throw new Error(`${path}.contracts_evaluated must be an array of strings`);
1057
+ }
1058
+ return {
1059
+ pass: validation.pass,
1060
+ failures: validation.failures.map((failure, index) => validateContractFailure(failure, `${path}.failures[${index}]`)),
1061
+ retries_used: requireNonNegativeInt(validation.retries_used, `${path}.retries_used`),
1062
+ contracts_evaluated: validation.contracts_evaluated
1063
+ };
1064
+ }
1065
+ function validateUsage(value, path) {
1066
+ const usage = asJsonObject(value, path);
1067
+ return {
1068
+ prompt_tokens: requireNonNegativeInt(usage.prompt_tokens, `${path}.prompt_tokens`),
1069
+ completion_tokens: requireNonNegativeInt(
1070
+ usage.completion_tokens,
1071
+ `${path}.completion_tokens`
1072
+ ),
1073
+ total_tokens: requireNonNegativeInt(usage.total_tokens, `${path}.total_tokens`)
1074
+ };
1075
+ }
1076
+ function parseCommonCapture(capture, index, modelId, schemaVersion) {
1077
+ const toolNames = requireStringArray(capture.tool_names, `captures[${index}].tool_names`);
1078
+ const primaryToolName = capture.primary_tool_name === void 0 ? toolNames[0] ?? null : nullableString(capture.primary_tool_name, `captures[${index}].primary_tool_name`);
1079
+ return {
1080
+ schema_version: schemaVersion,
1081
+ agent: requireString(capture.agent, `captures[${index}].agent`),
1082
+ timestamp: requireIsoTimestamp(capture.timestamp, `captures[${index}].timestamp`),
1083
+ provider: requireProvider(capture.provider, `captures[${index}].provider`),
1084
+ model_id: modelId,
1085
+ primary_tool_name: primaryToolName,
1086
+ tool_names: toolNames,
1087
+ request: validateRequest(capture.request, `captures[${index}].request`),
1088
+ response: validateResponse(capture.response, `captures[${index}].response`),
1089
+ ...capture.validation !== void 0 ? { validation: validateValidation(capture.validation, `captures[${index}].validation`) } : {},
1090
+ ...capture.usage !== void 0 ? { usage: validateUsage(capture.usage, `captures[${index}].usage`) } : {},
1091
+ latency_ms: requireNonNegativeInt(capture.latency_ms, `captures[${index}].latency_ms`)
1092
+ };
1093
+ }
1094
+ function parseLegacyCapturedCall(capture, index) {
1095
+ return parseCommonCapture(
1096
+ capture,
1097
+ index,
1098
+ requireString(capture.model, `captures[${index}].model`),
1099
+ CAPTURE_SCHEMA_VERSION_LEGACY
1100
+ );
1101
+ }
1102
+ function parseCurrentCapturedCall(capture, index) {
1103
+ return parseCommonCapture(
1104
+ capture,
1105
+ index,
1106
+ requireString(capture.model_id, `captures[${index}].model_id`),
1107
+ CAPTURE_SCHEMA_VERSION_CURRENT
1108
+ );
1109
+ }
1110
+ var CAPTURE_SCHEMA_PARSERS = {
1111
+ [CAPTURE_SCHEMA_VERSION_LEGACY]: parseLegacyCapturedCall,
1112
+ [CAPTURE_SCHEMA_VERSION_CURRENT]: parseCurrentCapturedCall
1113
+ };
1114
+
1115
+ // src/observe.ts
1116
+ var REPLAY_WRAPPED = /* @__PURE__ */ Symbol.for("replayci.wrapped");
1117
+ var DEFAULT_AGENT = "default";
1118
+ function observe(client, opts = {}) {
1119
+ assertSupportedNodeRuntime();
1120
+ try {
1121
+ if (isDisabled(opts)) {
1122
+ return createNoopHandle(client);
1123
+ }
1124
+ const apiKey = resolveApiKey(opts);
1125
+ if (!apiKey) {
1126
+ return createNoopHandle(client);
1127
+ }
1128
+ const provider = detectProviderSafely(client, opts.diagnostics);
1129
+ if (!provider) {
1130
+ return createNoopHandle(client);
1131
+ }
1132
+ const patchTarget = resolvePatchTarget(client, provider);
1133
+ if (!patchTarget) {
1134
+ emitDiagnostic(opts.diagnostics, {
1135
+ type: "unsupported_client",
1136
+ mode: "observe",
1137
+ detail: `Unsupported ${provider} client shape.`
1138
+ });
1139
+ return createNoopHandle(client);
1140
+ }
1141
+ if (isWrapped(client, patchTarget.target)) {
1142
+ emitDiagnostic(opts.diagnostics, {
1143
+ type: "double_wrap",
1144
+ mode: "observe"
1145
+ });
1146
+ return createNoopHandle(client);
1147
+ }
1148
+ const patchabilityError = getPatchabilityError(patchTarget.target, patchTarget.methodName);
1149
+ if (patchabilityError) {
1150
+ emitDiagnostic(opts.diagnostics, {
1151
+ type: "unsupported_client",
1152
+ mode: "observe",
1153
+ detail: patchabilityError
1154
+ });
1155
+ return createNoopHandle(client);
1156
+ }
1157
+ const captureLevel = normalizeCaptureLevel(opts.captureLevel);
1158
+ const agent = typeof opts.agent === "string" && opts.agent.length > 0 ? opts.agent : DEFAULT_AGENT;
1159
+ const buffer = new CaptureBuffer({
1160
+ apiKey,
1161
+ endpoint: opts.endpoint,
1162
+ maxBuffer: opts.maxBuffer,
1163
+ flushMs: opts.flushMs,
1164
+ timeoutMs: opts.timeoutMs,
1165
+ diagnostics: opts.diagnostics
1166
+ });
1167
+ registerBeforeExit(buffer);
1168
+ const wrappedCreate = function observeWrappedCreate(...args) {
1169
+ const requestSnapshot = snapshotRequest(args[0], captureLevel);
1170
+ const startedAt = Date.now();
1171
+ const result = patchTarget.originalCreate.apply(this, args);
1172
+ return Promise.resolve(result).then((response) => {
1173
+ safelyCaptureResponse({
1174
+ agent,
1175
+ buffer,
1176
+ captureLevel,
1177
+ provider,
1178
+ requestSnapshot,
1179
+ response,
1180
+ startedAt
1181
+ });
1182
+ return response;
1183
+ });
1184
+ };
1185
+ patchTarget.target[patchTarget.methodName] = wrappedCreate;
1186
+ setWrapped(client, patchTarget.target);
1187
+ let restored = false;
1188
+ return {
1189
+ client,
1190
+ restore() {
1191
+ if (restored) {
1192
+ return;
1193
+ }
1194
+ restored = true;
1195
+ if (patchTarget.target[patchTarget.methodName] === wrappedCreate) {
1196
+ if (patchTarget.hadOwnMethod) {
1197
+ patchTarget.target[patchTarget.methodName] = patchTarget.originalCreate;
1198
+ } else {
1199
+ delete patchTarget.target[patchTarget.methodName];
1200
+ }
1201
+ }
1202
+ clearWrapped(client, patchTarget.target);
1203
+ buffer.close();
1204
+ }
1205
+ };
1206
+ } catch {
1207
+ return createNoopHandle(client);
1208
+ }
1209
+ }
1210
+ function safelyCaptureResponse(input) {
1211
+ try {
1212
+ if (isAsyncIterable(input.response)) {
1213
+ const collector = createStreamCollector(input.provider, input.captureLevel);
1214
+ const tapped = tapAsyncIterable(input.response, collector, (summary) => {
1215
+ safelyPushStreamCapture({
1216
+ ...input,
1217
+ summary
1218
+ });
1219
+ });
1220
+ if (tapped) {
1221
+ return;
1222
+ }
1223
+ }
1224
+ const capture = buildCapturedCall({
1225
+ agent: input.agent,
1226
+ captureLevel: input.captureLevel,
1227
+ provider: input.provider,
1228
+ requestSnapshot: input.requestSnapshot,
1229
+ responseData: {
1230
+ model: extractModel(input.response, input.provider),
1231
+ toolCalls: extractToolCalls(input.response, input.provider),
1232
+ content: input.captureLevel === "full" ? extractContent(input.response, input.provider) : null,
1233
+ textBlocks: input.captureLevel === "full" ? extractTextBlocks(input.response, input.provider) : void 0,
1234
+ usage: extractUsage(input.response, input.provider)
1235
+ },
1236
+ endedAt: Date.now(),
1237
+ startedAt: input.startedAt
1238
+ });
1239
+ if (capture) {
1240
+ input.buffer.push(capture);
1241
+ }
1242
+ } catch {
1243
+ }
1244
+ }
1245
+ function safelyPushStreamCapture(input) {
1246
+ try {
1247
+ if (input.summary == null) {
1248
+ return;
1249
+ }
1250
+ const capture = buildCapturedCall({
1251
+ agent: input.agent,
1252
+ captureLevel: input.captureLevel,
1253
+ provider: input.provider,
1254
+ requestSnapshot: input.requestSnapshot,
1255
+ responseData: input.summary,
1256
+ startedAt: input.startedAt,
1257
+ endedAt: Date.now()
1258
+ });
1259
+ if (capture) {
1260
+ input.buffer.push(capture);
1261
+ }
1262
+ } catch {
1263
+ }
1264
+ }
1265
+ function buildCapturedCall(input) {
1266
+ try {
1267
+ const toolCalls = input.responseData.toolCalls.map((toolCall) => input.captureLevel === "metadata" ? { name: toolCall.name } : {
1268
+ name: toolCall.name,
1269
+ arguments: toolCall.arguments
1270
+ });
1271
+ return {
1272
+ schema_version: CAPTURE_SCHEMA_VERSION_CURRENT,
1273
+ agent: input.agent,
1274
+ timestamp: new Date(input.endedAt).toISOString(),
1275
+ provider: input.provider,
1276
+ model_id: input.requestSnapshot.model || input.responseData.model,
1277
+ primary_tool_name: toolCalls[0]?.name ?? null,
1278
+ tool_names: toolCalls.map((toolCall) => toolCall.name),
1279
+ request: input.requestSnapshot.request,
1280
+ response: {
1281
+ tool_calls: toolCalls,
1282
+ content: input.captureLevel === "full" ? input.responseData.content : null,
1283
+ ...input.captureLevel === "full" && input.responseData.textBlocks && input.responseData.textBlocks.length > 0 ? { text_blocks: input.responseData.textBlocks } : {}
1284
+ },
1285
+ ...input.responseData.usage ? { usage: input.responseData.usage } : {},
1286
+ latency_ms: Math.max(0, input.endedAt - input.startedAt)
1287
+ };
1288
+ } catch {
1289
+ return null;
1290
+ }
1291
+ }
1292
+ function snapshotRequest(request, captureLevel) {
1293
+ try {
1294
+ const record = toRecord4(request);
1295
+ return {
1296
+ model: toString4(record.model) ?? "",
1297
+ stream: record.stream === true,
1298
+ request: {
1299
+ ...captureLevel === "full" ? {
1300
+ messages: cloneMessages(record.messages)
1301
+ } : {},
1302
+ tools: captureTools(record.tools, captureLevel),
1303
+ ...serializeToolChoice(record.tool_choice) ? { tool_choice: serializeToolChoice(record.tool_choice) } : {}
1304
+ }
1305
+ };
1306
+ } catch {
1307
+ return {
1308
+ model: "",
1309
+ stream: false,
1310
+ request: {
1311
+ tools: []
1312
+ }
1313
+ };
1314
+ }
1315
+ }
1316
+ function cloneMessages(value) {
1317
+ if (!Array.isArray(value)) {
1318
+ return void 0;
1319
+ }
1320
+ try {
1321
+ return structuredClone(value);
1322
+ } catch {
1323
+ return void 0;
1324
+ }
1325
+ }
1326
+ function captureTools(value, captureLevel) {
1327
+ if (!Array.isArray(value)) {
1328
+ return [];
1329
+ }
1330
+ let normalized;
1331
+ try {
1332
+ normalized = (0, import_contracts_core.normalizeToolArray)(value).normalized;
1333
+ } catch {
1334
+ return captureToolsRaw(value, captureLevel);
1335
+ }
1336
+ if (captureLevel === "metadata") {
1337
+ return normalized.map((tool) => ({ name: tool.name }));
1338
+ }
1339
+ return normalized;
1340
+ }
1341
+ function captureToolsRaw(value, captureLevel) {
1342
+ if (captureLevel === "metadata") {
1343
+ return value.map((tool) => {
1344
+ const name = toString4(toRecord4(tool).name);
1345
+ return name ? { name } : null;
1346
+ }).filter((tool) => tool !== null);
1347
+ }
1348
+ return value.map((tool) => cloneTool(tool)).filter((tool) => tool !== null);
1349
+ }
1350
+ function cloneTool(tool) {
1351
+ const record = toRecord4(tool);
1352
+ const name = toString4(record.name);
1353
+ if (!name) {
1354
+ return null;
1355
+ }
1356
+ try {
1357
+ return structuredClone(record);
1358
+ } catch {
1359
+ return { name };
1360
+ }
1361
+ }
1362
+ function serializeToolChoice(value) {
1363
+ if (typeof value === "string" && value.length > 0) {
1364
+ return value;
1365
+ }
1366
+ const record = toRecord4(value);
1367
+ const type = toString4(record.type);
1368
+ if (!type) {
1369
+ return void 0;
1370
+ }
1371
+ if (type === "function") {
1372
+ const name = toString4(toRecord4(record.function).name);
1373
+ return name ? `function:${name}` : type;
1374
+ }
1375
+ if (type === "tool") {
1376
+ const name = toString4(record.name);
1377
+ return name ? `tool:${name}` : type;
1378
+ }
1379
+ return type;
1380
+ }
1381
+ function resolvePatchTarget(client, provider) {
1382
+ if (provider === "openai") {
1383
+ return resolveTarget(client, ["chat", "completions"], "create");
1384
+ }
1385
+ return resolveTarget(client, ["messages"], "create");
1386
+ }
1387
+ function resolveTarget(client, path, methodName) {
1388
+ let current = client;
1389
+ for (const segment of path) {
1390
+ current = toRecord4(current)[segment];
1391
+ }
1392
+ const target = toRecordLike2(current);
1393
+ const originalCreate = target[methodName];
1394
+ if (typeof originalCreate !== "function") {
1395
+ return null;
1396
+ }
1397
+ return {
1398
+ target,
1399
+ methodName,
1400
+ originalCreate,
1401
+ hadOwnMethod: Object.prototype.hasOwnProperty.call(target, methodName)
1402
+ };
1403
+ }
1404
+ function getPatchabilityError(target, methodName) {
1405
+ if (Object.isFrozen(target)) {
1406
+ return `Cannot patch client: the target object for "${methodName}" is frozen.`;
1407
+ }
1408
+ if (!Object.isExtensible(target)) {
1409
+ return `Cannot patch client: the target object for "${methodName}" is not extensible.`;
1410
+ }
1411
+ let current = target;
1412
+ while (current !== null) {
1413
+ const descriptor = Object.getOwnPropertyDescriptor(current, methodName);
1414
+ if (!descriptor) {
1415
+ current = Object.getPrototypeOf(current);
1416
+ continue;
1417
+ }
1418
+ if ("value" in descriptor && descriptor.writable === false) {
1419
+ return `Cannot patch client: "${methodName}" is non-writable.`;
1420
+ }
1421
+ if (!("value" in descriptor) && typeof descriptor.set !== "function") {
1422
+ return `Cannot patch client: "${methodName}" cannot be reassigned.`;
1423
+ }
1424
+ return null;
1425
+ }
1426
+ return null;
1427
+ }
1428
+ function detectProviderSafely(client, diagnostics) {
1429
+ try {
1430
+ return detectProvider(client);
1431
+ } catch (error) {
1432
+ emitDiagnostic(diagnostics, {
1433
+ type: "unsupported_client",
1434
+ mode: "observe",
1435
+ detail: error instanceof Error ? error.message : "Unsupported client."
1436
+ });
1437
+ return null;
1438
+ }
1439
+ }
1440
+ function isWrapped(client, target) {
1441
+ return Boolean(
1442
+ client[REPLAY_WRAPPED] || target[REPLAY_WRAPPED]
1443
+ );
1444
+ }
1445
+ function setWrapped(client, target) {
1446
+ try {
1447
+ client[REPLAY_WRAPPED] = true;
1448
+ } catch {
1449
+ }
1450
+ try {
1451
+ target[REPLAY_WRAPPED] = true;
1452
+ } catch {
1453
+ }
1454
+ }
1455
+ function clearWrapped(client, target) {
1456
+ try {
1457
+ delete client[REPLAY_WRAPPED];
1458
+ } catch {
1459
+ }
1460
+ try {
1461
+ delete target[REPLAY_WRAPPED];
1462
+ } catch {
1463
+ }
1464
+ }
1465
+ function createNoopHandle(client) {
1466
+ return {
1467
+ client,
1468
+ restore() {
1469
+ }
1470
+ };
1471
+ }
1472
+ function resolveApiKey(opts) {
1473
+ return typeof opts.apiKey === "string" && opts.apiKey.length > 0 ? opts.apiKey : toNonEmptyString(process.env.REPLAYCI_API_KEY);
1474
+ }
1475
+ function isDisabled(opts) {
1476
+ return opts.disabled === true || isTruthyEnvFlag(process.env.REPLAYCI_DISABLE);
1477
+ }
1478
+ function isTruthyEnvFlag(value) {
1479
+ if (!value) {
1480
+ return false;
1481
+ }
1482
+ const normalized = value.trim().toLowerCase();
1483
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
1484
+ }
1485
+ function normalizeCaptureLevel(level) {
1486
+ return level === "metadata" || level === "full" ? level : "redacted";
1487
+ }
1488
+ function emitDiagnostic(diagnostics, event) {
1489
+ try {
1490
+ diagnostics?.(event);
1491
+ } catch {
1492
+ }
1493
+ }
1494
+ function isAsyncIterable(value) {
1495
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
1496
+ }
1497
+ function toRecord4(value) {
1498
+ return value !== null && typeof value === "object" ? value : {};
1499
+ }
1500
+ function toRecordLike2(value) {
1501
+ return value !== null && (typeof value === "object" || typeof value === "function") ? value : {};
1502
+ }
1503
+ function toString4(value) {
1504
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1505
+ }
1506
+ function toNonEmptyString(value) {
1507
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1508
+ }
1509
+
1510
+ // src/validate.ts
1511
+ var import_contracts_core3 = require("@replayci/contracts-core");
1512
+
1513
+ // src/contracts.ts
1514
+ var import_contracts_core2 = require("@replayci/contracts-core");
1515
+ var import_node_fs = require("fs");
1516
+ var import_node_path = require("path");
1517
+ var CONTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
1518
+ var MAX_REGEX_BYTES = 1024;
1519
+ var NESTED_QUANTIFIER_RE = /\((?:[^()\\]|\\.)*[+*{](?:[^()\\]|\\.)*\)(?:[+*]|\{\d+(?:,\d*)?\})/;
1520
+ function loadContracts(input) {
1521
+ const contracts = loadContractsUnchecked(input);
1522
+ if (contracts.length === 0) {
1523
+ throw new ReplayConfigurationError("No contracts found. Check your contracts path.");
1524
+ }
1525
+ validateContractSet(contracts);
1526
+ return contracts;
1527
+ }
1528
+ function matchContracts(contracts, toolCalls, requestOrContext) {
1529
+ const prepared = contracts;
1530
+ const calledToolNames = new Set(toolCalls.map((toolCall) => toolCall.name));
1531
+ const schemaHash = deriveToolSchemaHash(requestOrContext);
1532
+ return prepared.filter((contract) => {
1533
+ if (!calledToolNames.has(contract.tool)) {
1534
+ return false;
1535
+ }
1536
+ if (contract.tool_schema_hash === void 0) {
1537
+ return true;
1538
+ }
1539
+ return schemaHash !== void 0 && contract.tool_schema_hash === schemaHash;
1540
+ });
1541
+ }
1542
+ function findUnmatchedTools(toolCalls, matchedContracts) {
1543
+ const matchedToolNames = new Set(matchedContracts.map((contract) => contract.tool));
1544
+ return toolCalls.filter((toolCall) => !matchedToolNames.has(toolCall.name));
1545
+ }
1546
+ function loadContractsUnchecked(input) {
1547
+ if (typeof input === "string") {
1548
+ return loadContractsFromPaths([input]);
1549
+ }
1550
+ if (Array.isArray(input)) {
1551
+ if (input.length === 0) {
1552
+ return [];
1553
+ }
1554
+ if (input.every((entry) => typeof entry === "string")) {
1555
+ return loadContractsFromPaths(input);
1556
+ }
1557
+ return input.map((contract) => normalizeInlineContract(contract));
1558
+ }
1559
+ return [normalizeInlineContract(input)];
1560
+ }
1561
+ function loadContractsFromPaths(inputs) {
1562
+ const repoRoot = process.cwd();
1563
+ const contractFiles = inputs.flatMap((input) => {
1564
+ try {
1565
+ return collectContractFiles((0, import_node_path.resolve)(repoRoot, input));
1566
+ } catch (error) {
1567
+ throw new ReplayConfigurationError(
1568
+ `Failed to load contracts from "${input}": ${formatErrorMessage(error)}`
1569
+ );
1570
+ }
1571
+ });
1572
+ return contractFiles.map((contractFile) => {
1573
+ const contractPath = (0, import_node_path.relative)(repoRoot, contractFile);
1574
+ const contract = (0, import_contracts_core2.loadContractSync)({
1575
+ repoRoot,
1576
+ contractPath
1577
+ });
1578
+ return normalizeInlineContract({
1579
+ ...contract,
1580
+ contract_file: contractFile
1581
+ });
1582
+ });
1583
+ }
1584
+ function collectContractFiles(inputPath) {
1585
+ const stat = (0, import_node_fs.statSync)(inputPath);
1586
+ if (stat.isFile()) {
1587
+ return [inputPath];
1588
+ }
1589
+ if (!stat.isDirectory()) {
1590
+ return [];
1591
+ }
1592
+ return (0, import_node_fs.readdirSync)(inputPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
1593
+ const fullPath = (0, import_node_path.join)(inputPath, entry.name);
1594
+ if (entry.isDirectory()) {
1595
+ return collectContractFiles(fullPath);
1596
+ }
1597
+ if (entry.isFile() && CONTRACT_EXTENSIONS.has((0, import_node_path.extname)(entry.name).toLowerCase())) {
1598
+ return [fullPath];
1599
+ }
1600
+ return [];
1601
+ });
1602
+ }
1603
+ function normalizeInlineContract(input) {
1604
+ const source = toRecord5(structuredClone(input));
1605
+ const tool = toString5(source.tool);
1606
+ if (!tool) {
1607
+ throw new ReplayConfigurationError("Inline contract is missing required field: tool");
1608
+ }
1609
+ const assertions = toRecord5(source.assertions);
1610
+ const expectTools = toStringArray(source.expect_tools);
1611
+ const expectedToolCalls = toExpectedToolCalls(source.expected_tool_calls);
1612
+ const contract = {
1613
+ tool,
1614
+ ...toString5(source.tool_schema_hash) ? { tool_schema_hash: toString5(source.tool_schema_hash) } : {},
1615
+ ...isSideEffect(source.side_effect) ? { side_effect: source.side_effect } : {},
1616
+ ...toString5(source.contract_file) ? { contract_file: toString5(source.contract_file) } : {},
1617
+ timeouts: {
1618
+ total_ms: toNonNegativeNumber(toRecord5(source.timeouts).total_ms, 0)
1619
+ },
1620
+ retries: {
1621
+ max_attempts: Math.max(1, toNonNegativeNumber(toRecord5(source.retries).max_attempts, 1)),
1622
+ retry_on: toStringArray(toRecord5(source.retries).retry_on)
1623
+ },
1624
+ rate_limits: {
1625
+ on_429: {
1626
+ respect_retry_after: toBoolean(toRecord5(toRecord5(source.rate_limits).on_429).respect_retry_after, false),
1627
+ max_sleep_seconds: toNonNegativeNumber(
1628
+ toRecord5(toRecord5(source.rate_limits).on_429).max_sleep_seconds,
1629
+ 0
1630
+ )
1631
+ }
1632
+ },
1633
+ assertions: {
1634
+ input_invariants: toInvariantArray(assertions.input_invariants),
1635
+ output_invariants: toInvariantArray(assertions.output_invariants)
1636
+ },
1637
+ golden_cases: Array.isArray(source.golden_cases) ? source.golden_cases : [],
1638
+ allowed_errors: toStringArray(source.allowed_errors),
1639
+ ...expectTools.length > 0 ? { expect_tools: expectTools } : {},
1640
+ ...toToolOrder(source.tool_order, expectTools.length > 0) ? {
1641
+ tool_order: toToolOrder(source.tool_order, expectTools.length > 0)
1642
+ } : {},
1643
+ ...isThreshold(source.pass_threshold) ? { pass_threshold: source.pass_threshold } : {},
1644
+ ...expectedToolCalls.length > 0 ? { expected_tool_calls: expectedToolCalls } : {},
1645
+ ...isMatchMode(source.tool_call_match_mode) ? {
1646
+ tool_call_match_mode: source.tool_call_match_mode
1647
+ } : {}
1648
+ };
1649
+ validateSafeRegexes(contract);
1650
+ return contract;
1651
+ }
1652
+ function validateContractSet(contracts) {
1653
+ const seenKeys = /* @__PURE__ */ new Set();
1654
+ for (const contract of contracts) {
1655
+ const key = `${contract.tool}::${contract.tool_schema_hash ?? ""}`;
1656
+ if (seenKeys.has(key)) {
1657
+ throw new ReplayConfigurationError(
1658
+ `Duplicate contract for tool "${contract.tool}"${contract.tool_schema_hash ? ` and schema hash "${contract.tool_schema_hash}"` : ""}.`
1659
+ );
1660
+ }
1661
+ seenKeys.add(key);
1662
+ }
1663
+ }
1664
+ function validateSafeRegexes(contract) {
1665
+ const contractLabel = contract.contract_file ?? contract.tool;
1666
+ const invariantGroups = [
1667
+ {
1668
+ label: "assertions.input_invariants",
1669
+ invariants: contract.assertions.input_invariants
1670
+ },
1671
+ {
1672
+ label: "assertions.output_invariants",
1673
+ invariants: contract.assertions.output_invariants
1674
+ }
1675
+ ];
1676
+ for (const [index, expectedToolCall] of (contract.expected_tool_calls ?? []).entries()) {
1677
+ invariantGroups.push({
1678
+ label: `expected_tool_calls[${index}].argument_invariants`,
1679
+ invariants: expectedToolCall.argument_invariants ?? []
1680
+ });
1681
+ }
1682
+ for (const group of invariantGroups) {
1683
+ for (const invariant of group.invariants) {
1684
+ if (typeof invariant.regex !== "string") {
1685
+ continue;
1686
+ }
1687
+ if (Buffer.byteLength(invariant.regex, "utf8") > MAX_REGEX_BYTES) {
1688
+ throw new ReplayConfigurationError(
1689
+ `Regex pattern exceeds ${MAX_REGEX_BYTES} bytes in ${contractLabel} (${group.label}, ${invariant.path}).`
1690
+ );
1691
+ }
1692
+ if (NESTED_QUANTIFIER_RE.test(invariant.regex)) {
1693
+ throw new ReplayConfigurationError(
1694
+ `Nested quantifiers are not allowed in ${contractLabel} (${group.label}, ${invariant.path}).`
1695
+ );
1696
+ }
1697
+ try {
1698
+ void new RegExp(invariant.regex);
1699
+ } catch (error) {
1700
+ throw new ReplayConfigurationError(
1701
+ `Invalid regex in ${contractLabel} (${group.label}, ${invariant.path}): ${formatErrorMessage(error)}`
1702
+ );
1703
+ }
1704
+ }
1705
+ }
1706
+ }
1707
+ function deriveToolSchemaHash(requestOrContext) {
1708
+ const record = toRecord5(requestOrContext);
1709
+ const boundary = toRecord5(record.boundary);
1710
+ if (typeof boundary.tool_schema_hash === "string") {
1711
+ return boundary.tool_schema_hash;
1712
+ }
1713
+ if (typeof record.tool_schema_hash === "string") {
1714
+ return record.tool_schema_hash;
1715
+ }
1716
+ if (Array.isArray(record.tools)) {
1717
+ try {
1718
+ const { normalized } = (0, import_contracts_core2.normalizeToolArray)(record.tools);
1719
+ return (0, import_contracts_core2.hashToolSchema)(normalized);
1720
+ } catch {
1721
+ return (0, import_contracts_core2.hashToolSchema)(record.tools);
1722
+ }
1723
+ }
1724
+ return void 0;
1725
+ }
1726
+ function toRecord5(value) {
1727
+ return value && typeof value === "object" ? value : {};
1728
+ }
1729
+ function toString5(value) {
1730
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1731
+ }
1732
+ function toStringArray(value) {
1733
+ if (!Array.isArray(value)) {
1734
+ return [];
1735
+ }
1736
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
1737
+ }
1738
+ function toBoolean(value, fallback) {
1739
+ return typeof value === "boolean" ? value : fallback;
1740
+ }
1741
+ function toNonNegativeNumber(value, fallback) {
1742
+ if (typeof value === "number" && Number.isFinite(value)) {
1743
+ return Math.max(0, value);
1744
+ }
1745
+ return fallback;
1746
+ }
1747
+ function toInvariantArray(value) {
1748
+ if (!Array.isArray(value)) {
1749
+ return [];
1750
+ }
1751
+ return value.filter((entry) => Boolean(entry) && typeof entry === "object").map((entry) => ({
1752
+ path: typeof entry.path === "string" ? entry.path : "$",
1753
+ ...entry.equals !== void 0 ? { equals: entry.equals } : {},
1754
+ ...typeof entry.exists === "boolean" ? { exists: entry.exists } : {},
1755
+ ...typeof entry.type === "string" ? { type: entry.type } : {},
1756
+ ...typeof entry.contains === "string" ? { contains: entry.contains } : {},
1757
+ ...typeof entry.equals_env === "string" ? { equals_env: entry.equals_env } : {},
1758
+ ...typeof entry.note === "string" ? { note: entry.note } : {},
1759
+ ...Array.isArray(entry.one_of) ? { one_of: entry.one_of } : {},
1760
+ ...typeof entry.regex === "string" ? { regex: entry.regex } : {},
1761
+ ...typeof entry.gte === "number" ? { gte: entry.gte } : {},
1762
+ ...typeof entry.lte === "number" ? { lte: entry.lte } : {},
1763
+ ...typeof entry.length_gte === "number" ? { length_gte: entry.length_gte } : {},
1764
+ ...typeof entry.length_lte === "number" ? { length_lte: entry.length_lte } : {}
1765
+ }));
1766
+ }
1767
+ function toExpectedToolCalls(value) {
1768
+ if (!Array.isArray(value)) {
1769
+ return [];
1770
+ }
1771
+ return value.filter((entry) => Boolean(entry) && typeof entry === "object").map((entry) => ({
1772
+ name: typeof entry.name === "string" ? entry.name : "",
1773
+ ...Array.isArray(entry.argument_invariants) ? { argument_invariants: toInvariantArray(entry.argument_invariants) } : {}
1774
+ })).filter((entry) => entry.name.length > 0);
1775
+ }
1776
+ function isThreshold(value) {
1777
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
1778
+ }
1779
+ function isMatchMode(value) {
1780
+ return value === "any" || value === "strict";
1781
+ }
1782
+ function toToolOrder(value, hasExpectedTools) {
1783
+ if (value === "any" || value === "strict") {
1784
+ return value;
1785
+ }
1786
+ return hasExpectedTools ? "any" : void 0;
1787
+ }
1788
+ function isSideEffect(value) {
1789
+ return value === "read" || value === "write" || value === "destructive";
1790
+ }
1791
+ function formatErrorMessage(error) {
1792
+ return error instanceof Error ? error.message : String(error);
1793
+ }
1794
+
1795
+ // src/validate.ts
1796
+ function prepareContracts(input) {
1797
+ assertSupportedNodeRuntime();
1798
+ return loadContracts(input);
1799
+ }
1800
+ function validate(response, opts) {
1801
+ assertSupportedNodeRuntime();
1802
+ const start = Date.now();
1803
+ try {
1804
+ if (opts == null || opts.contracts == null) {
1805
+ return {
1806
+ pass: false,
1807
+ failures: [{
1808
+ path: "$",
1809
+ operator: "validate_input",
1810
+ expected: "contracts",
1811
+ found: "missing",
1812
+ message: "validate() requires prepared contracts"
1813
+ }],
1814
+ matched_contracts: 0,
1815
+ unmatched_tools: [],
1816
+ evaluation_ms: Date.now() - start
1817
+ };
1818
+ }
1819
+ if (opts.unmatchedPolicy !== void 0 && opts.unmatchedPolicy !== "deny" && opts.unmatchedPolicy !== "allow") {
1820
+ throw new ReplayConfigurationError(
1821
+ `Invalid unmatchedPolicy: "${String(opts.unmatchedPolicy)}". Use "deny" or "allow".`
1822
+ );
1823
+ }
1824
+ const contracts = Array.isArray(opts.contracts) ? opts.contracts : [opts.contracts];
1825
+ const extraction = extractResponse(response);
1826
+ const matchedContracts = matchContracts(contracts, extraction.toolCalls, response);
1827
+ const unmatchedTools = findUnmatchedTools(extraction.toolCalls, matchedContracts);
1828
+ const unmatchedPolicy = opts.unmatchedPolicy ?? "deny";
1829
+ const results = evaluateAllContracts(matchedContracts, extraction);
1830
+ const unmatchedFailures = unmatchedPolicy === "deny" ? unmatchedTools.map((tool) => ({
1831
+ path: "$.tool_calls",
1832
+ operator: "contract_match",
1833
+ expected: "known tool",
1834
+ found: tool.name,
1835
+ message: `No contract for tool "${tool.name}"`
1836
+ })) : [];
1837
+ const failures = [...results.failures, ...unmatchedFailures];
1838
+ return {
1839
+ pass: failures.length === 0,
1840
+ failures,
1841
+ matched_contracts: results.matched.length,
1842
+ unmatched_tools: unmatchedTools.map((tool) => tool.name),
1843
+ evaluation_ms: Date.now() - start
1844
+ };
1845
+ } catch (error) {
1846
+ if (error instanceof ReplayConfigurationError) {
1847
+ throw error;
1848
+ }
1849
+ throw new ReplayInternalError("validate() internal failure", { cause: error });
1850
+ }
1851
+ }
1852
+ function evaluateAllContracts(matchedContracts, extraction) {
1853
+ const start = Date.now();
1854
+ const failures = [...extraction.failures];
1855
+ for (const contract of matchedContracts) {
1856
+ failures.push(...evaluateExpectTools(contract, extraction.toolCalls));
1857
+ failures.push(...evaluateOutputInvariants(contract, extraction.normalizedResponse));
1858
+ failures.push(...evaluateExpectedToolCallMatchers(contract, extraction.toolCalls));
1859
+ }
1860
+ return {
1861
+ pass: failures.length === 0,
1862
+ failures,
1863
+ matched: matchedContracts,
1864
+ unmatched: [],
1865
+ durationMs: Date.now() - start
1866
+ };
1867
+ }
1868
+ function extractResponse(response) {
1869
+ const normalizedResponse = createNormalizedResponse(response);
1870
+ const toolCalls = extractToolCallValues(response);
1871
+ const failures = [];
1872
+ const normalizedToolCalls = toolCalls.map((toolCall, index) => {
1873
+ const normalized = normalizeToolCall(toolCall, index);
1874
+ if (normalized.failure) {
1875
+ failures.push(normalized.failure);
1876
+ }
1877
+ return normalized.toolCall;
1878
+ });
1879
+ return {
1880
+ failures,
1881
+ normalizedResponse: normalizedResponse(normalizedToolCalls),
1882
+ toolCalls: normalizedToolCalls
1883
+ };
1884
+ }
1885
+ function createNormalizedResponse(response) {
1886
+ if (isRecord2(response) && Array.isArray(response.tool_calls)) {
1887
+ return (toolCalls) => ({
1888
+ ...response,
1889
+ tool_calls: toolCalls.map((toolCall) => ({
1890
+ id: toolCall.id,
1891
+ name: toolCall.name,
1892
+ function: { name: toolCall.name },
1893
+ arguments: toolCall.parsedArguments
1894
+ }))
1895
+ });
1896
+ }
1897
+ const provider = detectResponseProvider(response);
1898
+ if (provider === "openai") {
1899
+ return (toolCalls) => ({
1900
+ ...toRecord6(response),
1901
+ content: extractContentOpenAI(response),
1902
+ tool_calls: toolCalls.map((toolCall) => ({
1903
+ id: toolCall.id,
1904
+ name: toolCall.name,
1905
+ function: { name: toolCall.name },
1906
+ arguments: toolCall.parsedArguments
1907
+ }))
1908
+ });
1909
+ }
1910
+ if (provider === "anthropic") {
1911
+ const textBlocks = extractTextBlocksAnthropic(response);
1912
+ return (toolCalls) => ({
1913
+ ...toRecord6(response),
1914
+ ...textBlocks.length > 0 ? { text_blocks: textBlocks } : {},
1915
+ tool_calls: toolCalls.map((toolCall) => ({
1916
+ id: toolCall.id,
1917
+ name: toolCall.name,
1918
+ function: { name: toolCall.name },
1919
+ arguments: toolCall.parsedArguments
1920
+ }))
1921
+ });
1922
+ }
1923
+ return (toolCalls) => ({
1924
+ ...isRecord2(response) ? response : {},
1925
+ tool_calls: toolCalls.map((toolCall) => ({
1926
+ id: toolCall.id,
1927
+ name: toolCall.name,
1928
+ function: { name: toolCall.name },
1929
+ arguments: toolCall.parsedArguments
1930
+ }))
1931
+ });
1932
+ }
1933
+ function extractToolCallValues(response) {
1934
+ if (isRecord2(response) && Array.isArray(response.tool_calls)) {
1935
+ return response.tool_calls.map((toolCall, index) => extractTopLevelToolCall(toolCall, index)).filter((toolCall) => toolCall !== null);
1936
+ }
1937
+ const provider = detectResponseProvider(response);
1938
+ return provider ? extractToolCalls(response, provider) : [];
1939
+ }
1940
+ function evaluateExpectTools(contract, toolCalls) {
1941
+ if (!contract.expect_tools || contract.expect_tools.length === 0) {
1942
+ return [];
1943
+ }
1944
+ const actualNames = toolCalls.map((toolCall) => toolCall.name);
1945
+ const missingPool = [...actualNames];
1946
+ const missingTools = [];
1947
+ for (const expectedTool of contract.expect_tools) {
1948
+ const matchIndex = missingPool.indexOf(expectedTool);
1949
+ if (matchIndex === -1) {
1950
+ missingTools.push(expectedTool);
1951
+ continue;
1952
+ }
1953
+ missingPool.splice(matchIndex, 1);
1954
+ }
1955
+ const matchedCount = contract.expect_tools.length - missingTools.length;
1956
+ const threshold = contract.pass_threshold ?? 1;
1957
+ const matchRatio = contract.expect_tools.length > 0 ? matchedCount / contract.expect_tools.length : 1;
1958
+ const failures = [];
1959
+ if (matchRatio < threshold) {
1960
+ failures.push({
1961
+ path: "$.tool_calls",
1962
+ operator: "expect_tools",
1963
+ expected: contract.expect_tools,
1964
+ found: actualNames,
1965
+ message: `matched ${matchedCount}/${contract.expect_tools.length} (ratio ${matchRatio.toFixed(2)}, threshold ${threshold}), missing: [${missingTools.join(", ")}]`,
1966
+ contract_file: contract.contract_file
1967
+ });
1968
+ }
1969
+ if (contract.tool_order === "strict" && missingTools.length === 0) {
1970
+ const relevantCalls = [];
1971
+ const orderPool = [...contract.expect_tools];
1972
+ for (const actualName of actualNames) {
1973
+ const matchIndex = orderPool.indexOf(actualName);
1974
+ if (matchIndex === -1) {
1975
+ continue;
1976
+ }
1977
+ relevantCalls.push(actualName);
1978
+ orderPool.splice(matchIndex, 1);
1979
+ }
1980
+ const inOrder = contract.expect_tools.every(
1981
+ (expectedTool, index) => relevantCalls[index] === expectedTool
1982
+ );
1983
+ if (!inOrder) {
1984
+ failures.push({
1985
+ path: "$.tool_calls",
1986
+ operator: "tool_order",
1987
+ expected: contract.expect_tools,
1988
+ found: relevantCalls,
1989
+ message: "strict mode: matched calls not in declared order",
1990
+ contract_file: contract.contract_file
1991
+ });
1992
+ }
1993
+ }
1994
+ return failures;
1995
+ }
1996
+ function evaluateOutputInvariants(contract, normalizedResponse) {
1997
+ const invariantFailures = (0, import_contracts_core3.evaluateInvariants)(
1998
+ normalizedResponse,
1999
+ contract.assertions.output_invariants,
2000
+ process.env
2001
+ );
2002
+ return invariantFailures.map(
2003
+ (failure) => mapInvariantFailure(contract, failure, normalizedResponse)
2004
+ );
2005
+ }
2006
+ function evaluateExpectedToolCallMatchers(contract, toolCalls) {
2007
+ if (!contract.expected_tool_calls || contract.expected_tool_calls.length === 0) {
2008
+ return [];
2009
+ }
2010
+ const result = (0, import_contracts_core3.evaluateExpectedToolCalls)(
2011
+ toolCalls,
2012
+ contract.expected_tool_calls,
2013
+ contract.pass_threshold ?? 1,
2014
+ contract.tool_call_match_mode ?? "any",
2015
+ process.env
2016
+ );
2017
+ return result.failures.map((failure) => ({
2018
+ path: failure.path,
2019
+ operator: failure.rule,
2020
+ expected: contract.expected_tool_calls?.map((toolCall) => toolCall.name) ?? [],
2021
+ found: toolCalls.map((toolCall) => toolCall.name),
2022
+ message: failure.detail,
2023
+ contract_file: contract.contract_file
2024
+ }));
2025
+ }
2026
+ function mapInvariantFailure(contract, failure, normalizedResponse) {
2027
+ const invariant = findMatchingInvariant(contract.assertions.output_invariants, failure);
2028
+ const lookup = (0, import_contracts_core3.getPathValue)(normalizedResponse, failure.path);
2029
+ return {
2030
+ path: failure.path,
2031
+ operator: failure.rule,
2032
+ expected: invariant ? getExpectedValueForInvariant(invariant, failure.rule) : "contract invariant",
2033
+ found: getFoundValueForInvariant(lookup, failure.rule),
2034
+ message: failure.detail,
2035
+ contract_file: contract.contract_file
2036
+ };
2037
+ }
2038
+ function findMatchingInvariant(invariants, failure) {
2039
+ return invariants.find((invariant) => {
2040
+ if (invariant.path !== failure.path) {
2041
+ return false;
2042
+ }
2043
+ switch (failure.rule) {
2044
+ case "equals":
2045
+ return invariant.equals !== void 0;
2046
+ case "exists":
2047
+ return invariant.exists !== void 0;
2048
+ case "type":
2049
+ return invariant.type !== void 0;
2050
+ case "contains":
2051
+ return invariant.contains !== void 0;
2052
+ case "equals_env":
2053
+ return invariant.equals_env !== void 0;
2054
+ case "one_of":
2055
+ return invariant.one_of !== void 0;
2056
+ case "regex":
2057
+ return invariant.regex !== void 0;
2058
+ case "gte":
2059
+ return invariant.gte !== void 0;
2060
+ case "lte":
2061
+ return invariant.lte !== void 0;
2062
+ case "length_gte":
2063
+ return invariant.length_gte !== void 0;
2064
+ case "length_lte":
2065
+ return invariant.length_lte !== void 0;
2066
+ case "path_not_found":
2067
+ return hasValueCheck(invariant);
2068
+ default:
2069
+ return false;
2070
+ }
2071
+ });
2072
+ }
2073
+ function getExpectedValueForInvariant(invariant, operator) {
2074
+ switch (operator) {
2075
+ case "equals":
2076
+ return invariant.equals;
2077
+ case "exists":
2078
+ return invariant.exists;
2079
+ case "type":
2080
+ return invariant.type;
2081
+ case "contains":
2082
+ return invariant.contains;
2083
+ case "equals_env":
2084
+ return invariant.equals_env ? `env:${invariant.equals_env}` : void 0;
2085
+ case "one_of":
2086
+ return invariant.one_of;
2087
+ case "regex":
2088
+ return invariant.regex;
2089
+ case "gte":
2090
+ return invariant.gte;
2091
+ case "lte":
2092
+ return invariant.lte;
2093
+ case "length_gte":
2094
+ return invariant.length_gte;
2095
+ case "length_lte":
2096
+ return invariant.length_lte;
2097
+ case "path_not_found":
2098
+ return "path exists";
2099
+ default:
2100
+ return "contract invariant";
2101
+ }
2102
+ }
2103
+ function getFoundValueForInvariant(lookup, operator) {
2104
+ if (!lookup.exists) {
2105
+ return "missing";
2106
+ }
2107
+ switch (operator) {
2108
+ case "type":
2109
+ return Array.isArray(lookup.value) ? "array" : lookup.value === null ? "null" : typeof lookup.value;
2110
+ case "length_gte":
2111
+ case "length_lte":
2112
+ return Array.isArray(lookup.value) || typeof lookup.value === "string" ? lookup.value.length : lookup.value;
2113
+ default:
2114
+ return lookup.value;
2115
+ }
2116
+ }
2117
+ function normalizeToolCall(toolCall, index) {
2118
+ const parsed = parseArguments(toolCall.arguments);
2119
+ if (parsed.ok) {
2120
+ return {
2121
+ toolCall: {
2122
+ ...toolCall,
2123
+ parsedArguments: parsed.value
2124
+ }
2125
+ };
2126
+ }
2127
+ return {
2128
+ failure: {
2129
+ path: `$.tool_calls[${index}].arguments`,
2130
+ operator: "argument_parse",
2131
+ expected: "valid JSON",
2132
+ found: toolCall.arguments,
2133
+ message: `Tool "${toolCall.name}" returned unparseable arguments`
2134
+ },
2135
+ toolCall: {
2136
+ ...toolCall,
2137
+ parsedArguments: null
2138
+ }
2139
+ };
2140
+ }
2141
+ function parseArguments(rawArguments) {
2142
+ try {
2143
+ return { ok: true, value: JSON.parse(rawArguments) };
2144
+ } catch {
2145
+ return { ok: false };
2146
+ }
2147
+ }
2148
+ function extractTopLevelToolCall(toolCall, index) {
2149
+ const record = toRecord6(toolCall);
2150
+ const name = toString6(record.name);
2151
+ if (!name) {
2152
+ return null;
2153
+ }
2154
+ return {
2155
+ id: toString6(record.id) ?? `tool_call_${index}`,
2156
+ name,
2157
+ arguments: serializeArguments4(record.arguments)
2158
+ };
2159
+ }
2160
+ function detectResponseProvider(response) {
2161
+ const record = toRecord6(response);
2162
+ if (Array.isArray(record.content)) {
2163
+ return "anthropic";
2164
+ }
2165
+ if (!Array.isArray(record.choices) || record.choices.length === 0) {
2166
+ return null;
2167
+ }
2168
+ const firstChoice = toRecord6(record.choices[0]);
2169
+ return isRecord2(firstChoice.message) ? "openai" : null;
2170
+ }
2171
+ function serializeArguments4(argumentsValue) {
2172
+ if (typeof argumentsValue === "string") {
2173
+ return argumentsValue;
2174
+ }
2175
+ if (argumentsValue === void 0) {
2176
+ return "null";
2177
+ }
2178
+ return JSON.stringify(argumentsValue) ?? "null";
2179
+ }
2180
+ function hasValueCheck(invariant) {
2181
+ return invariant.equals !== void 0 || invariant.contains !== void 0 || invariant.type !== void 0 || invariant.equals_env !== void 0 || invariant.one_of !== void 0 || invariant.regex !== void 0 || invariant.gte !== void 0 || invariant.lte !== void 0 || invariant.length_gte !== void 0 || invariant.length_lte !== void 0;
2182
+ }
2183
+ function isRecord2(value) {
2184
+ return value !== null && typeof value === "object";
2185
+ }
2186
+ function toRecord6(value) {
2187
+ return isRecord2(value) ? value : {};
2188
+ }
2189
+ function toString6(value) {
2190
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2191
+ }
2192
+ // Annotate the CommonJS export names for ESM import in node:
2193
+ 0 && (module.exports = {
2194
+ observe,
2195
+ prepareContracts,
2196
+ validate
2197
+ });