@replayci/replay 0.1.8 → 0.1.10

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 CHANGED
@@ -2370,7 +2370,10 @@ function normalizeInlineContract(input) {
2370
2370
  ...source.transitions != null ? { transitions: source.transitions } : {},
2371
2371
  ...source.gate != null ? { gate: source.gate } : {},
2372
2372
  ...source.evidence_class != null ? { evidence_class: source.evidence_class } : {},
2373
- ...source.commit_requirement != null ? { commit_requirement: source.commit_requirement } : {}
2373
+ ...source.commit_requirement != null ? { commit_requirement: source.commit_requirement } : {},
2374
+ ...typeof source.schema_derived === "boolean" ? { schema_derived: source.schema_derived } : {},
2375
+ ...Array.isArray(source.schema_derived_exclude) ? { schema_derived_exclude: source.schema_derived_exclude } : {},
2376
+ ...Array.isArray(source.binds) ? { binds: source.binds } : {}
2374
2377
  };
2375
2378
  validateSafeRegexes(contract);
2376
2379
  return contract;
@@ -2593,8 +2596,83 @@ function extractPath(obj, path) {
2593
2596
  return current;
2594
2597
  }
2595
2598
 
2599
+ // src/bindings.ts
2600
+ var MAX_BINDINGS = 256;
2601
+ function captureBindings(toolCall, contract, state, outputExtract) {
2602
+ const bindings = new Map(state.bindings);
2603
+ const captured = [];
2604
+ const diagnostics = [];
2605
+ if (!contract.binds || contract.binds.length === 0) {
2606
+ return { bindings, captured, diagnostics };
2607
+ }
2608
+ if (state.killed) {
2609
+ return { bindings, captured, diagnostics };
2610
+ }
2611
+ let parsedArgs;
2612
+ try {
2613
+ parsedArgs = JSON.parse(toolCall.arguments);
2614
+ } catch {
2615
+ parsedArgs = {};
2616
+ }
2617
+ for (const bind of contract.binds) {
2618
+ const source = bind.source ?? "arguments";
2619
+ const sourceObj = source === "output" ? outputExtract ?? {} : parsedArgs ?? {};
2620
+ const value = extractPath(sourceObj, bind.path);
2621
+ const overwritten = bindings.has(bind.name);
2622
+ if (overwritten) {
2623
+ const prev = bindings.get(bind.name);
2624
+ diagnostics.push({
2625
+ type: "binding_overwritten",
2626
+ slot: bind.name,
2627
+ detail: `Slot '${bind.name}' overwritten: ${JSON.stringify(prev.value)} \u2192 ${JSON.stringify(value)}`
2628
+ });
2629
+ }
2630
+ if (bindings.size >= MAX_BINDINGS && !bindings.has(bind.name)) {
2631
+ diagnostics.push({
2632
+ type: "binding_cap_exceeded",
2633
+ slot: bind.name,
2634
+ detail: `Binding cap (${MAX_BINDINGS}) exceeded, slot '${bind.name}' not written`
2635
+ });
2636
+ continue;
2637
+ }
2638
+ bindings.set(bind.name, {
2639
+ value,
2640
+ source_tool: toolCall.name,
2641
+ source_step: state.totalStepCount,
2642
+ bound_at: (/* @__PURE__ */ new Date()).toISOString()
2643
+ });
2644
+ captured.push({
2645
+ slot: bind.name,
2646
+ source,
2647
+ path: bind.path,
2648
+ value,
2649
+ overwritten
2650
+ });
2651
+ }
2652
+ return { bindings, captured, diagnostics };
2653
+ }
2654
+ function evaluateRef(refName, actualValue, tolerance, bindings) {
2655
+ const bound = bindings.get(refName);
2656
+ if (!bound) {
2657
+ return { passed: false, reason: "binding_not_found" };
2658
+ }
2659
+ const expected = bound.value;
2660
+ if (tolerance !== void 0 && typeof expected === "number" && typeof actualValue === "number") {
2661
+ const denom = Math.max(Math.abs(expected), 1);
2662
+ const relDiff = Math.abs(actualValue - expected) / denom;
2663
+ if (relDiff <= tolerance) {
2664
+ return { passed: true, reason: "ref_match", expected, actual: actualValue };
2665
+ }
2666
+ return { passed: false, reason: "ref_mismatch", expected, actual: actualValue };
2667
+ }
2668
+ if (JSON.stringify(actualValue) === JSON.stringify(expected)) {
2669
+ return { passed: true, reason: "ref_match", expected, actual: actualValue };
2670
+ }
2671
+ return { passed: false, reason: "ref_mismatch", expected, actual: actualValue };
2672
+ }
2673
+
2596
2674
  // src/argumentValues.ts
2597
- function evaluateArgumentValueInvariants(parsedArguments, invariants) {
2675
+ function evaluateArgumentValueInvariants(parsedArguments, invariants, bindings) {
2598
2676
  const failures = [];
2599
2677
  for (const inv of invariants) {
2600
2678
  const value = extractPath(parsedArguments, inv.path);
@@ -2684,6 +2762,18 @@ function evaluateArgumentValueInvariants(parsedArguments, invariants) {
2684
2762
  });
2685
2763
  }
2686
2764
  }
2765
+ if (inv.ref !== void 0 && bindings) {
2766
+ const refResult = evaluateRef(inv.ref, value, inv.tolerance, bindings);
2767
+ if (!refResult.passed) {
2768
+ failures.push({
2769
+ path: inv.path,
2770
+ operator: refResult.reason,
2771
+ expected: refResult.expected,
2772
+ actual: refResult.actual ?? value,
2773
+ detail: refResult.reason === "binding_not_found" ? `Binding '${inv.ref}' not found \u2014 producing tool has not been called` : `Ref mismatch on '${inv.ref}': expected ${JSON.stringify(refResult.expected)}, got ${JSON.stringify(refResult.actual)}`
2774
+ });
2775
+ }
2776
+ }
2687
2777
  }
2688
2778
  return {
2689
2779
  passed: failures.length === 0,
@@ -3113,10 +3203,10 @@ function toString6(value) {
3113
3203
  }
3114
3204
 
3115
3205
  // src/replay.ts
3116
- var import_node_crypto5 = __toESM(require("crypto"), 1);
3206
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
3117
3207
  var import_node_fs3 = require("fs");
3118
3208
  var import_node_path3 = require("path");
3119
- var import_contracts_core6 = require("@replayci/contracts-core");
3209
+ var import_contracts_core7 = require("@replayci/contracts-core");
3120
3210
 
3121
3211
  // src/redaction.ts
3122
3212
  var import_node_crypto2 = __toESM(require("crypto"), 1);
@@ -3664,7 +3754,12 @@ function createInitialState(sessionId, options) {
3664
3754
  totalBlockCount: 0,
3665
3755
  totalUnguardedCalls: 0,
3666
3756
  killed: false,
3667
- contractHash: null
3757
+ contractHash: null,
3758
+ bindings: /* @__PURE__ */ new Map(),
3759
+ aggregates: /* @__PURE__ */ new Map(),
3760
+ envelopes: /* @__PURE__ */ new Map(),
3761
+ labels: new Set(options?.labels ?? []),
3762
+ checkpointCount: 0
3668
3763
  };
3669
3764
  }
3670
3765
  function finalizeExecutedStep(state, step, contracts, compiledSession) {
@@ -3964,8 +4059,529 @@ function validateCrossStep(toolCalls, sessionState, contracts, ctx) {
3964
4059
  };
3965
4060
  }
3966
4061
 
3967
- // src/messageValidation.ts
4062
+ // src/aggregates.ts
4063
+ var MAX_DISTINCT_VALUES = 1e4;
4064
+ function initializeAggregates(definitions) {
4065
+ const map = /* @__PURE__ */ new Map();
4066
+ for (const def of definitions) {
4067
+ const state = {
4068
+ name: def.name,
4069
+ metric: def.metric,
4070
+ currentValue: def.metric === "min" ? Infinity : 0
4071
+ };
4072
+ if (def.metric === "count_distinct") {
4073
+ state.distinctValues = /* @__PURE__ */ new Set();
4074
+ }
4075
+ map.set(def.name, state);
4076
+ }
4077
+ return map;
4078
+ }
4079
+ function matchesTool(toolName, filter) {
4080
+ if (filter === "*") return true;
4081
+ if (typeof filter === "string") return filter === toolName;
4082
+ return filter.includes(toolName);
4083
+ }
4084
+ function speculativeCheck(toolName, parsedArgs, aggregates, definitions) {
4085
+ const failures = [];
4086
+ for (const def of definitions) {
4087
+ if (!matchesTool(toolName, def.tool)) continue;
4088
+ const current = aggregates.get(def.name);
4089
+ if (!current) continue;
4090
+ const currentValue = current.currentValue;
4091
+ if (def.metric === "count") {
4092
+ const speculative2 = currentValue + 1;
4093
+ if (def.lte !== void 0 && speculative2 > def.lte) {
4094
+ failures.push({
4095
+ aggregate: def.name,
4096
+ reason: "aggregate_limit_exceeded",
4097
+ detail: `${def.reason} (count ${speculative2} > lte ${def.lte})`,
4098
+ current: currentValue,
4099
+ speculative: speculative2,
4100
+ bound: { lte: def.lte }
4101
+ });
4102
+ }
4103
+ if (def.gte !== void 0 && speculative2 < def.gte) {
4104
+ }
4105
+ continue;
4106
+ }
4107
+ if (!def.path) {
4108
+ failures.push({
4109
+ aggregate: def.name,
4110
+ reason: "aggregate_path_missing",
4111
+ detail: `Aggregate '${def.name}' requires path but none configured`,
4112
+ current: currentValue,
4113
+ speculative: currentValue
4114
+ });
4115
+ continue;
4116
+ }
4117
+ const extracted = extractPath(parsedArgs, def.path);
4118
+ if (extracted === void 0) {
4119
+ failures.push({
4120
+ aggregate: def.name,
4121
+ reason: "aggregate_path_missing",
4122
+ detail: `Tool '${toolName}' missing path '${def.path}' for aggregate '${def.name}'`,
4123
+ current: currentValue,
4124
+ speculative: currentValue
4125
+ });
4126
+ continue;
4127
+ }
4128
+ let speculative;
4129
+ switch (def.metric) {
4130
+ case "sum": {
4131
+ if (typeof extracted !== "number") {
4132
+ failures.push({
4133
+ aggregate: def.name,
4134
+ reason: "aggregate_type_error",
4135
+ detail: `Aggregate '${def.name}' (sum) requires numeric value, got ${typeof extracted}`,
4136
+ current: currentValue,
4137
+ speculative: currentValue
4138
+ });
4139
+ continue;
4140
+ }
4141
+ speculative = currentValue + extracted;
4142
+ break;
4143
+ }
4144
+ case "max": {
4145
+ if (typeof extracted !== "number") {
4146
+ failures.push({
4147
+ aggregate: def.name,
4148
+ reason: "aggregate_type_error",
4149
+ detail: `Aggregate '${def.name}' (max) requires numeric value, got ${typeof extracted}`,
4150
+ current: currentValue,
4151
+ speculative: currentValue
4152
+ });
4153
+ continue;
4154
+ }
4155
+ speculative = Math.max(currentValue, extracted);
4156
+ break;
4157
+ }
4158
+ case "min": {
4159
+ if (typeof extracted !== "number") {
4160
+ failures.push({
4161
+ aggregate: def.name,
4162
+ reason: "aggregate_type_error",
4163
+ detail: `Aggregate '${def.name}' (min) requires numeric value, got ${typeof extracted}`,
4164
+ current: currentValue,
4165
+ speculative: currentValue
4166
+ });
4167
+ continue;
4168
+ }
4169
+ speculative = Math.min(currentValue, extracted);
4170
+ break;
4171
+ }
4172
+ case "count_distinct": {
4173
+ const key = JSON.stringify(extracted);
4174
+ const existing = current.distinctValues ?? /* @__PURE__ */ new Set();
4175
+ if (existing.has(key)) {
4176
+ speculative = currentValue;
4177
+ } else {
4178
+ speculative = currentValue + 1;
4179
+ }
4180
+ break;
4181
+ }
4182
+ default:
4183
+ continue;
4184
+ }
4185
+ if (def.lte !== void 0 && speculative > def.lte) {
4186
+ failures.push({
4187
+ aggregate: def.name,
4188
+ reason: "aggregate_limit_exceeded",
4189
+ detail: `${def.reason} (${def.metric} ${speculative} > lte ${def.lte})`,
4190
+ current: currentValue,
4191
+ speculative,
4192
+ bound: { lte: def.lte }
4193
+ });
4194
+ }
4195
+ if (def.gte !== void 0 && speculative < def.gte) {
4196
+ failures.push({
4197
+ aggregate: def.name,
4198
+ reason: "aggregate_limit_exceeded",
4199
+ detail: `${def.reason} (${def.metric} ${speculative} < gte ${def.gte})`,
4200
+ current: currentValue,
4201
+ speculative,
4202
+ bound: { gte: def.gte }
4203
+ });
4204
+ }
4205
+ }
4206
+ return { passed: failures.length === 0, failures };
4207
+ }
4208
+ function commitAggregate(toolName, parsedArgs, aggregates, definitions) {
4209
+ const updated = new Map(aggregates);
4210
+ for (const def of definitions) {
4211
+ if (!matchesTool(toolName, def.tool)) continue;
4212
+ const current = updated.get(def.name);
4213
+ if (!current) continue;
4214
+ if (def.metric === "count") {
4215
+ updated.set(def.name, { ...current, currentValue: current.currentValue + 1 });
4216
+ continue;
4217
+ }
4218
+ if (!def.path) continue;
4219
+ const extracted = extractPath(parsedArgs, def.path);
4220
+ if (extracted === void 0) continue;
4221
+ switch (def.metric) {
4222
+ case "sum": {
4223
+ if (typeof extracted !== "number") continue;
4224
+ updated.set(def.name, { ...current, currentValue: current.currentValue + extracted });
4225
+ break;
4226
+ }
4227
+ case "max": {
4228
+ if (typeof extracted !== "number") continue;
4229
+ updated.set(def.name, { ...current, currentValue: Math.max(current.currentValue, extracted) });
4230
+ break;
4231
+ }
4232
+ case "min": {
4233
+ if (typeof extracted !== "number") continue;
4234
+ updated.set(def.name, { ...current, currentValue: Math.min(current.currentValue, extracted) });
4235
+ break;
4236
+ }
4237
+ case "count_distinct": {
4238
+ const key = JSON.stringify(extracted);
4239
+ const existing = current.distinctValues ?? /* @__PURE__ */ new Set();
4240
+ if (existing.has(key)) continue;
4241
+ if (existing.size >= MAX_DISTINCT_VALUES) continue;
4242
+ const newSet = new Set(existing);
4243
+ newSet.add(key);
4244
+ updated.set(def.name, { ...current, currentValue: current.currentValue + 1, distinctValues: newSet });
4245
+ break;
4246
+ }
4247
+ }
4248
+ }
4249
+ return updated;
4250
+ }
4251
+
4252
+ // src/envelopes.ts
4253
+ function initializeEnvelopes(definitions) {
4254
+ const map = /* @__PURE__ */ new Map();
4255
+ for (const def of definitions) {
4256
+ map.set(def.name, {
4257
+ name: def.name,
4258
+ constraint: def.constraint,
4259
+ established: false
4260
+ });
4261
+ }
4262
+ return map;
4263
+ }
4264
+ function evaluateEnvelopes(toolName, parsedArgs, envelopes, definitions) {
4265
+ const failures = [];
4266
+ const captures = [];
4267
+ for (const def of definitions) {
4268
+ const current = envelopes.get(def.name);
4269
+ if (!current) continue;
4270
+ for (const stage of def.stages) {
4271
+ if (stage.tool !== toolName) continue;
4272
+ const extracted = extractPath(parsedArgs, stage.path);
4273
+ if (stage.role === "constrained") {
4274
+ if (!current.established) {
4275
+ failures.push({
4276
+ envelope: def.name,
4277
+ reason: "envelope_not_established",
4278
+ detail: `Envelope '${def.name}': constrained tool '${toolName}' called before reference value established`,
4279
+ constraint: def.constraint
4280
+ });
4281
+ continue;
4282
+ }
4283
+ if (typeof extracted !== "number") {
4284
+ failures.push({
4285
+ envelope: def.name,
4286
+ reason: "envelope_type_error",
4287
+ detail: `Envelope '${def.name}': value at '${stage.path}' is ${typeof extracted}, expected number`,
4288
+ constraint: def.constraint
4289
+ });
4290
+ continue;
4291
+ }
4292
+ const violation = checkConstraint(current, def, extracted);
4293
+ if (violation) {
4294
+ failures.push(violation);
4295
+ }
4296
+ captures.push({
4297
+ envelope: def.name,
4298
+ role: "constrained",
4299
+ path: stage.path,
4300
+ value: extracted
4301
+ });
4302
+ } else {
4303
+ if (typeof extracted !== "number") {
4304
+ failures.push({
4305
+ envelope: def.name,
4306
+ reason: "envelope_type_error",
4307
+ detail: `Envelope '${def.name}': reference value at '${stage.path}' is ${typeof extracted}, expected number`,
4308
+ constraint: def.constraint
4309
+ });
4310
+ continue;
4311
+ }
4312
+ captures.push({
4313
+ envelope: def.name,
4314
+ role: stage.role,
4315
+ path: stage.path,
4316
+ value: extracted
4317
+ });
4318
+ }
4319
+ }
4320
+ }
4321
+ return { passed: failures.length === 0, failures, captures };
4322
+ }
4323
+ function commitEnvelope(captures, envelopes) {
4324
+ if (captures.length === 0) return envelopes;
4325
+ const updated = new Map(envelopes);
4326
+ for (const cap of captures) {
4327
+ const current = updated.get(cap.envelope);
4328
+ if (!current) continue;
4329
+ const next = { ...current };
4330
+ switch (cap.role) {
4331
+ case "ceiling":
4332
+ next.ceiling = cap.value;
4333
+ next.established = true;
4334
+ break;
4335
+ case "floor":
4336
+ next.floor = cap.value;
4337
+ next.established = true;
4338
+ break;
4339
+ case "anchor":
4340
+ next.anchor = cap.value;
4341
+ next.established = true;
4342
+ break;
4343
+ case "initial":
4344
+ next.lastValue = cap.value;
4345
+ next.established = true;
4346
+ break;
4347
+ case "constrained":
4348
+ next.lastValue = cap.value;
4349
+ break;
4350
+ }
4351
+ updated.set(cap.envelope, next);
4352
+ }
4353
+ return updated;
4354
+ }
4355
+ function checkConstraint(state, def, value) {
4356
+ switch (def.constraint) {
4357
+ case "lte_ceiling": {
4358
+ if (state.ceiling === void 0) return null;
4359
+ if (value <= state.ceiling) return null;
4360
+ return {
4361
+ envelope: def.name,
4362
+ reason: "envelope_violation",
4363
+ detail: `${def.reason} (${value} > ceiling ${state.ceiling})`,
4364
+ constraint: def.constraint,
4365
+ referenceValue: state.ceiling,
4366
+ actualValue: value
4367
+ };
4368
+ }
4369
+ case "gte_floor": {
4370
+ if (state.floor === void 0) return null;
4371
+ if (value >= state.floor) return null;
4372
+ return {
4373
+ envelope: def.name,
4374
+ reason: "envelope_violation",
4375
+ detail: `${def.reason} (${value} < floor ${state.floor})`,
4376
+ constraint: def.constraint,
4377
+ referenceValue: state.floor,
4378
+ actualValue: value
4379
+ };
4380
+ }
4381
+ case "within_band": {
4382
+ if (state.anchor === void 0) return null;
4383
+ const band = def.band ?? 0;
4384
+ const denom = Math.max(Math.abs(state.anchor), 1);
4385
+ const relDiff = Math.abs(value - state.anchor) / denom;
4386
+ if (relDiff <= band) return null;
4387
+ return {
4388
+ envelope: def.name,
4389
+ reason: "envelope_violation",
4390
+ detail: `${def.reason} (${value} is ${(relDiff * 100).toFixed(1)}% from anchor ${state.anchor}, band ${(band * 100).toFixed(1)}%)`,
4391
+ constraint: def.constraint,
4392
+ referenceValue: state.anchor,
4393
+ actualValue: value
4394
+ };
4395
+ }
4396
+ case "monotonic_decrease": {
4397
+ const ref = state.lastValue;
4398
+ if (ref === void 0) return null;
4399
+ if (value <= ref) return null;
4400
+ return {
4401
+ envelope: def.name,
4402
+ reason: "envelope_violation",
4403
+ detail: `${def.reason} (${value} > previous ${ref})`,
4404
+ constraint: def.constraint,
4405
+ referenceValue: ref,
4406
+ actualValue: value
4407
+ };
4408
+ }
4409
+ case "monotonic_increase": {
4410
+ const ref = state.lastValue;
4411
+ if (ref === void 0) return null;
4412
+ if (value >= ref) return null;
4413
+ return {
4414
+ envelope: def.name,
4415
+ reason: "envelope_violation",
4416
+ detail: `${def.reason} (${value} < previous ${ref})`,
4417
+ constraint: def.constraint,
4418
+ referenceValue: ref,
4419
+ actualValue: value
4420
+ };
4421
+ }
4422
+ case "bounded": {
4423
+ if (state.floor !== void 0 && value < state.floor) {
4424
+ return {
4425
+ envelope: def.name,
4426
+ reason: "envelope_violation",
4427
+ detail: `${def.reason} (${value} < floor ${state.floor})`,
4428
+ constraint: def.constraint,
4429
+ referenceValue: state.floor,
4430
+ actualValue: value
4431
+ };
4432
+ }
4433
+ if (state.ceiling !== void 0 && value > state.ceiling) {
4434
+ return {
4435
+ envelope: def.name,
4436
+ reason: "envelope_violation",
4437
+ detail: `${def.reason} (${value} > ceiling ${state.ceiling})`,
4438
+ constraint: def.constraint,
4439
+ referenceValue: state.ceiling,
4440
+ actualValue: value
4441
+ };
4442
+ }
4443
+ return null;
4444
+ }
4445
+ default:
4446
+ return null;
4447
+ }
4448
+ }
4449
+
4450
+ // src/checkpoints.ts
4451
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
3968
4452
  var import_contracts_core4 = require("@replayci/contracts-core");
4453
+ var MAX_CHECKPOINT_BUDGET = 10;
4454
+ function evaluateCheckpoints(toolCalls, checkpoints, contracts) {
4455
+ if (checkpoints.length === 0) return null;
4456
+ const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
4457
+ for (const tc of toolCalls) {
4458
+ let parsedArgs;
4459
+ try {
4460
+ parsedArgs = JSON.parse(tc.arguments);
4461
+ } catch {
4462
+ parsedArgs = {};
4463
+ }
4464
+ for (const cp of checkpoints) {
4465
+ if (cp.tool !== "*" && cp.tool !== tc.name) continue;
4466
+ if (cp.when_side_effect) {
4467
+ const contract = contractByTool.get(tc.name);
4468
+ if (!contract?.side_effect || contract.side_effect !== cp.when_side_effect) {
4469
+ if (cp.when === "always" || Array.isArray(cp.when) && cp.when.length === 0) {
4470
+ continue;
4471
+ }
4472
+ } else {
4473
+ return { checkpoint: cp, toolCall: tc, matchedConditions: [`when_side_effect:${cp.when_side_effect}`] };
4474
+ }
4475
+ }
4476
+ if (cp.when === "always") {
4477
+ return { checkpoint: cp, toolCall: tc, matchedConditions: ["always"] };
4478
+ }
4479
+ if (Array.isArray(cp.when) && cp.when.length > 0) {
4480
+ const matched = [];
4481
+ for (const cond of cp.when) {
4482
+ if (evaluateCondition(parsedArgs, cond)) {
4483
+ matched.push(cond.path);
4484
+ }
4485
+ }
4486
+ const triggers = cp.match === "all" ? matched.length === cp.when.length : matched.length > 0;
4487
+ if (triggers) {
4488
+ return { checkpoint: cp, toolCall: tc, matchedConditions: matched };
4489
+ }
4490
+ }
4491
+ }
4492
+ }
4493
+ return null;
4494
+ }
4495
+ function evaluateCondition(args, cond) {
4496
+ const { exists, value } = (0, import_contracts_core4.getPathValue)(args, cond.path);
4497
+ if (!exists) return false;
4498
+ if (cond.gte !== void 0) {
4499
+ if (typeof value !== "number" || value < cond.gte) return false;
4500
+ }
4501
+ if (cond.lte !== void 0) {
4502
+ if (typeof value !== "number" || value > cond.lte) return false;
4503
+ }
4504
+ if (cond.equals !== void 0) {
4505
+ if (value !== cond.equals) return false;
4506
+ }
4507
+ if (cond.one_of !== void 0) {
4508
+ if (!cond.one_of.includes(value)) return false;
4509
+ }
4510
+ return true;
4511
+ }
4512
+ function buildApprovalRequest(checkpoint, toolCall, sessionId, matchedConditions) {
4513
+ let parsedArgs;
4514
+ try {
4515
+ parsedArgs = JSON.parse(toolCall.arguments);
4516
+ } catch {
4517
+ parsedArgs = {};
4518
+ }
4519
+ const context = [];
4520
+ for (const ctx of checkpoint.context) {
4521
+ const { exists, value } = (0, import_contracts_core4.getPathValue)(parsedArgs, ctx.path);
4522
+ context.push({ label: ctx.label, value: exists ? value : void 0 });
4523
+ }
4524
+ const reason = matchedConditions.includes("always") ? `Checkpoint '${checkpoint.name}': always requires approval for ${toolCall.name}` : matchedConditions.some((m) => m.startsWith("when_side_effect:")) ? `Checkpoint '${checkpoint.name}': tool ${toolCall.name} has ${matchedConditions[0]}` : `Checkpoint '${checkpoint.name}': conditions met [${matchedConditions.join(", ")}]`;
4525
+ return {
4526
+ checkpoint_id: import_node_crypto4.default.randomUUID(),
4527
+ session_id: sessionId,
4528
+ tool_name: toolCall.name,
4529
+ arguments: parsedArgs,
4530
+ context,
4531
+ reason,
4532
+ timeout_seconds: checkpoint.timeout_seconds,
4533
+ requested_at: (/* @__PURE__ */ new Date()).toISOString()
4534
+ };
4535
+ }
4536
+ async function waitForApproval(request, onCheckpoint, timeoutSeconds, onTimeout, isKilled) {
4537
+ return new Promise((resolve2) => {
4538
+ let settled = false;
4539
+ const timer = setTimeout(() => {
4540
+ if (settled) return;
4541
+ settled = true;
4542
+ resolve2({
4543
+ checkpoint_id: request.checkpoint_id,
4544
+ decision: isKilled() ? "deny" : onTimeout === "allow" ? "approve" : "deny",
4545
+ decided_by: isKilled() ? "system:killed" : "system:timeout",
4546
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4547
+ reason: isKilled() ? "Session killed during checkpoint wait" : `Timeout after ${timeoutSeconds}s`
4548
+ });
4549
+ }, timeoutSeconds * 1e3);
4550
+ onCheckpoint(request).then(
4551
+ (response) => {
4552
+ if (settled) return;
4553
+ settled = true;
4554
+ clearTimeout(timer);
4555
+ if (isKilled()) {
4556
+ resolve2({
4557
+ checkpoint_id: request.checkpoint_id,
4558
+ decision: "deny",
4559
+ decided_by: "system:killed",
4560
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4561
+ reason: "Session killed during checkpoint wait"
4562
+ });
4563
+ } else {
4564
+ resolve2(response);
4565
+ }
4566
+ },
4567
+ (err) => {
4568
+ if (settled) return;
4569
+ settled = true;
4570
+ clearTimeout(timer);
4571
+ resolve2({
4572
+ checkpoint_id: request.checkpoint_id,
4573
+ decision: "deny",
4574
+ decided_by: "system:error",
4575
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4576
+ reason: `Checkpoint callback error: ${err instanceof Error ? err.message : String(err)}`
4577
+ });
4578
+ }
4579
+ );
4580
+ });
4581
+ }
4582
+
4583
+ // src/messageValidation.ts
4584
+ var import_contracts_core5 = require("@replayci/contracts-core");
3969
4585
  function validateToolResultMessages(messages, contracts, provider) {
3970
4586
  const failures = [];
3971
4587
  const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
@@ -3981,7 +4597,7 @@ function validateToolResultMessages(messages, contracts, provider) {
3981
4597
  } catch {
3982
4598
  continue;
3983
4599
  }
3984
- const invariantResult = (0, import_contracts_core4.evaluateInvariants)(parsed, outputInvariants, process.env);
4600
+ const invariantResult = (0, import_contracts_core5.evaluateInvariants)(parsed, outputInvariants, process.env);
3985
4601
  for (const failure of invariantResult) {
3986
4602
  failures.push({
3987
4603
  toolName: result.toolName,
@@ -4245,6 +4861,29 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
4245
4861
  continue;
4246
4862
  }
4247
4863
  }
4864
+ if (compiledSession.labelGates && compiledSession.labelGates.length > 0 && sessionState.labels.size > 0) {
4865
+ let labelBlocked = false;
4866
+ for (const gate of compiledSession.labelGates) {
4867
+ if (sessionState.labels.has(gate.when_label) && gate.deny_tools.includes(tool.name)) {
4868
+ removed.push({
4869
+ tool: tool.name,
4870
+ reason: "label_gate",
4871
+ detail: gate.reason
4872
+ });
4873
+ ctx?.trace.push({
4874
+ stage: "narrow",
4875
+ tool: tool.name,
4876
+ verdict: "remove",
4877
+ reason: "label_gate",
4878
+ checked: { label: gate.when_label, gate_reason: gate.reason },
4879
+ found: { active_labels: [...sessionState.labels] }
4880
+ });
4881
+ labelBlocked = true;
4882
+ break;
4883
+ }
4884
+ }
4885
+ if (labelBlocked) continue;
4886
+ }
4248
4887
  ctx?.trace.push({
4249
4888
  stage: "narrow",
4250
4889
  tool: tool.name,
@@ -4255,7 +4894,8 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
4255
4894
  phase_ok: true,
4256
4895
  preconditions_ok: true,
4257
4896
  not_forbidden: true,
4258
- policy_ok: true
4897
+ policy_ok: true,
4898
+ label_gates_ok: true
4259
4899
  },
4260
4900
  found: {}
4261
4901
  });
@@ -4285,12 +4925,12 @@ function getToolName(tool) {
4285
4925
  }
4286
4926
 
4287
4927
  // src/executionConstraints.ts
4288
- var import_contracts_core5 = require("@replayci/contracts-core");
4928
+ var import_contracts_core6 = require("@replayci/contracts-core");
4289
4929
  function enforceExecutionConstraints(toolName, args, constraints) {
4290
4930
  if (constraints.length === 0) {
4291
4931
  return { passed: true, failures: [] };
4292
4932
  }
4293
- const failures = (0, import_contracts_core5.evaluateInvariants)(args, constraints, process.env);
4933
+ const failures = (0, import_contracts_core6.evaluateInvariants)(args, constraints, process.env);
4294
4934
  const constraintFailures = failures.map((f) => ({
4295
4935
  path: f.path,
4296
4936
  operator: f.rule,
@@ -4342,7 +4982,7 @@ function buildWrappedToolsMap(tools, compiledSession) {
4342
4982
  }
4343
4983
 
4344
4984
  // src/runtimeClient.ts
4345
- var import_node_crypto4 = __toESM(require("crypto"), 1);
4985
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
4346
4986
  var CIRCUIT_BREAKER_FAILURE_LIMIT2 = 5;
4347
4987
  var CIRCUIT_BREAKER_MS2 = 10 * 6e4;
4348
4988
  var DEFAULT_TIMEOUT_MS2 = 3e4;
@@ -4733,7 +5373,7 @@ function normalizeUrl(url) {
4733
5373
  return url.endsWith("/") ? url.slice(0, -1) : url;
4734
5374
  }
4735
5375
  function generateIdempotencyKey() {
4736
- return `sdk_${import_node_crypto4.default.randomUUID().replace(/-/g, "")}`;
5376
+ return `sdk_${import_node_crypto5.default.randomUUID().replace(/-/g, "")}`;
4737
5377
  }
4738
5378
 
4739
5379
  // src/replay.ts
@@ -4799,7 +5439,7 @@ function replay(client, opts = {}) {
4799
5439
  }
4800
5440
  let compiledSession = null;
4801
5441
  try {
4802
- compiledSession = (0, import_contracts_core6.compileSession)(contracts, sessionYaml, {
5442
+ compiledSession = (0, import_contracts_core7.compileSession)(contracts, sessionYaml, {
4803
5443
  principal: opts.principal,
4804
5444
  tools: opts.tools ? new Map(Object.entries(opts.tools)) : void 0
4805
5445
  });
@@ -4889,13 +5529,13 @@ function replay(client, opts = {}) {
4889
5529
  compiledSession: compiledSession ? {
4890
5530
  schemaVersion: "1",
4891
5531
  hash: compiledSession.compiledHash,
4892
- body: (0, import_contracts_core6.serializeCompiledSession)(compiledSession)
5532
+ body: (0, import_contracts_core7.serializeCompiledSession)(compiledSession)
4893
5533
  } : void 0,
4894
5534
  principal: opts.principal ?? null
4895
5535
  };
4896
5536
  if (workflowOpts && workflowId) {
4897
5537
  if (workflowOpts.type === "root" && compiledWorkflow) {
4898
- const serialized = (0, import_contracts_core6.serializeCompiledWorkflow)(compiledWorkflow);
5538
+ const serialized = (0, import_contracts_core7.serializeCompiledWorkflow)(compiledWorkflow);
4899
5539
  sessionInitPayload.workflow = {
4900
5540
  workflowId,
4901
5541
  role: workflowOpts.role,
@@ -4936,7 +5576,7 @@ function replay(client, opts = {}) {
4936
5576
  }
4937
5577
  const initialTier = protectionLevel === "govern" && apiKey ? "strong" : "compat";
4938
5578
  const principalValue = opts.principal != null && typeof opts.principal === "object" && !Array.isArray(opts.principal) ? opts.principal : null;
4939
- let sessionState = createInitialState(sessionId, { tier: initialTier, agent, principal: principalValue });
5579
+ let sessionState = createInitialState(sessionId, { tier: initialTier, agent, principal: principalValue, labels: opts.labels });
4940
5580
  if (compiledSession?.phases) {
4941
5581
  const initial = compiledSession.phases.find((p) => p.initial);
4942
5582
  if (initial) {
@@ -4944,6 +5584,12 @@ function replay(client, opts = {}) {
4944
5584
  }
4945
5585
  }
4946
5586
  sessionState = { ...sessionState, contractHash: compiledSession?.compiledHash ?? null };
5587
+ if (compiledSession?.aggregates && compiledSession.aggregates.length > 0) {
5588
+ sessionState = { ...sessionState, aggregates: initializeAggregates(compiledSession.aggregates) };
5589
+ }
5590
+ if (compiledSession?.envelopes && compiledSession.envelopes.length > 0) {
5591
+ sessionState = { ...sessionState, envelopes: initializeEnvelopes(compiledSession.envelopes) };
5592
+ }
4947
5593
  let killed = false;
4948
5594
  let killedAt = null;
4949
5595
  let restored = false;
@@ -4958,6 +5604,17 @@ function replay(client, opts = {}) {
4958
5604
  const compiledLimits = compiledSession?.sessionLimits;
4959
5605
  const mergedLimits = { ...contractLimits ?? {}, ...compiledLimits ?? {} };
4960
5606
  const resolvedSessionLimits = Object.keys(mergedLimits).length > 0 ? mergedLimits : null;
5607
+ if (resolvedSessionLimits?.max_tool_calls_mode === "narrow" && resolvedSessionLimits.max_calls_per_tool) {
5608
+ const budgetedTools = new Set(Object.keys(resolvedSessionLimits.max_calls_per_tool));
5609
+ const unbudgeted = contracts.map((c) => c.tool).filter((t) => !budgetedTools.has(t));
5610
+ if (unbudgeted.length > 0) {
5611
+ emitDiagnostic2(diagnostics, {
5612
+ type: "replay_narrow_unbudgeted_tools",
5613
+ session_id: sessionId,
5614
+ tools: unbudgeted
5615
+ });
5616
+ }
5617
+ }
4961
5618
  const store = opts.store ?? null;
4962
5619
  let storeLoadPromise = null;
4963
5620
  let storeLoadDone = false;
@@ -5479,7 +6136,7 @@ function replay(client, opts = {}) {
5479
6136
  } catch {
5480
6137
  parsedArgs = {};
5481
6138
  }
5482
- const avResult = evaluateArgumentValueInvariants(parsedArgs, contract.argument_value_invariants);
6139
+ const avResult = evaluateArgumentValueInvariants(parsedArgs, contract.argument_value_invariants, sessionState.bindings);
5483
6140
  if (!avResult.passed) {
5484
6141
  for (const f of avResult.failures) {
5485
6142
  validation.failures.push({
@@ -5541,6 +6198,93 @@ function replay(client, opts = {}) {
5541
6198
  }
5542
6199
  }
5543
6200
  timing.argument_values_ms += Date.now() - argValuesStart;
6201
+ if (compiledSession?.aggregates && compiledSession.aggregates.length > 0) {
6202
+ const workingAggregates = new Map(sessionState.aggregates);
6203
+ for (const tc of toolCalls) {
6204
+ let parsedArgs;
6205
+ try {
6206
+ parsedArgs = JSON.parse(tc.arguments);
6207
+ } catch {
6208
+ parsedArgs = {};
6209
+ }
6210
+ const aggResult = speculativeCheck(tc.name, parsedArgs, workingAggregates, compiledSession.aggregates);
6211
+ if (!aggResult.passed) {
6212
+ for (const f of aggResult.failures) {
6213
+ trace.push({
6214
+ stage: "aggregate",
6215
+ tool: tc.name,
6216
+ verdict: "block",
6217
+ reason: f.reason,
6218
+ checked: { aggregate: f.aggregate, bound_lte: f.bound?.lte ?? null, bound_gte: f.bound?.gte ?? null },
6219
+ found: { current: f.current, speculative: f.speculative }
6220
+ });
6221
+ validation.failures.push({
6222
+ path: `$.tool_calls.${tc.name}`,
6223
+ operator: f.reason,
6224
+ expected: "",
6225
+ found: String(f.speculative),
6226
+ message: f.detail,
6227
+ contract_file: ""
6228
+ });
6229
+ }
6230
+ } else {
6231
+ const committed = commitAggregate(tc.name, parsedArgs, workingAggregates, compiledSession.aggregates);
6232
+ for (const [k, v] of committed) {
6233
+ workingAggregates.set(k, v);
6234
+ }
6235
+ }
6236
+ }
6237
+ }
6238
+ if (compiledSession?.envelopes && compiledSession.envelopes.length > 0) {
6239
+ const workingEnvelopes = new Map(sessionState.envelopes);
6240
+ for (const tc of toolCalls) {
6241
+ let parsedArgs;
6242
+ try {
6243
+ parsedArgs = JSON.parse(tc.arguments);
6244
+ } catch {
6245
+ parsedArgs = {};
6246
+ }
6247
+ const envResult = evaluateEnvelopes(tc.name, parsedArgs, workingEnvelopes, compiledSession.envelopes);
6248
+ if (!envResult.passed) {
6249
+ for (const f of envResult.failures) {
6250
+ trace.push({
6251
+ stage: "envelope",
6252
+ tool: tc.name,
6253
+ verdict: "block",
6254
+ reason: f.reason,
6255
+ checked: { envelope: f.envelope, constraint: f.constraint, reference: f.referenceValue ?? null },
6256
+ found: { value: f.actualValue ?? null }
6257
+ });
6258
+ validation.failures.push({
6259
+ path: `$.tool_calls.${tc.name}`,
6260
+ operator: f.reason,
6261
+ expected: "",
6262
+ found: String(f.actualValue ?? ""),
6263
+ message: f.detail,
6264
+ contract_file: ""
6265
+ });
6266
+ }
6267
+ }
6268
+ if (envResult.captures.length > 0) {
6269
+ const committed = commitEnvelope(envResult.captures, workingEnvelopes);
6270
+ for (const [k, v] of committed) {
6271
+ workingEnvelopes.set(k, v);
6272
+ }
6273
+ for (const cap of envResult.captures) {
6274
+ if (cap.role !== "constrained") {
6275
+ trace.push({
6276
+ stage: "envelope",
6277
+ tool: tc.name,
6278
+ verdict: "allow",
6279
+ reason: "ceiling_established",
6280
+ checked: { envelope: cap.envelope, role: cap.role, path: cap.path },
6281
+ found: { value: cap.value }
6282
+ });
6283
+ }
6284
+ }
6285
+ }
6286
+ }
6287
+ }
5544
6288
  currentTraceStage = "policy";
5545
6289
  let policyVerdicts = null;
5546
6290
  const policyStart = Date.now();
@@ -5593,6 +6337,143 @@ function replay(client, opts = {}) {
5593
6337
  trace.push({ stage: "policy", tool: null, verdict: "skip", reason: "no_policy_configured", checked: {}, found: {} });
5594
6338
  }
5595
6339
  timing.policy_ms += Date.now() - policyStart;
6340
+ currentTraceStage = "checkpoint";
6341
+ if (compiledSession?.checkpoints && compiledSession.checkpoints.length > 0 && validation.failures.length === 0) {
6342
+ const cpResult = evaluateCheckpoints(toolCalls, compiledSession.checkpoints, contracts);
6343
+ if (cpResult) {
6344
+ const { checkpoint: triggeredCp, toolCall: triggeredTc, matchedConditions } = cpResult;
6345
+ if (mode === "shadow") {
6346
+ trace.push({
6347
+ stage: "checkpoint",
6348
+ tool: triggeredTc.name,
6349
+ verdict: "info",
6350
+ reason: "checkpoint_would_trigger",
6351
+ checked: { checkpoint: triggeredCp.name, conditions: matchedConditions },
6352
+ found: { shadow: true }
6353
+ });
6354
+ emitDiagnostic2(diagnostics, {
6355
+ type: "replay_checkpoint_shadow",
6356
+ session_id: sessionId,
6357
+ checkpoint: triggeredCp.name,
6358
+ tool_name: triggeredTc.name
6359
+ });
6360
+ } else {
6361
+ if (sessionState.checkpointCount >= MAX_CHECKPOINT_BUDGET) {
6362
+ trace.push({
6363
+ stage: "checkpoint",
6364
+ tool: triggeredTc.name,
6365
+ verdict: "block",
6366
+ reason: "checkpoint_budget_exceeded",
6367
+ checked: { checkpoint: triggeredCp.name, budget: MAX_CHECKPOINT_BUDGET },
6368
+ found: { count: sessionState.checkpointCount }
6369
+ });
6370
+ validation.failures.push({
6371
+ path: `$.tool_calls.${triggeredTc.name}`,
6372
+ operator: "checkpoint_budget_exceeded",
6373
+ expected: `<= ${MAX_CHECKPOINT_BUDGET} checkpoints`,
6374
+ found: String(sessionState.checkpointCount),
6375
+ message: `Checkpoint budget exceeded: ${sessionState.checkpointCount} of ${MAX_CHECKPOINT_BUDGET}`,
6376
+ contract_file: ""
6377
+ });
6378
+ } else if (!opts.onCheckpoint) {
6379
+ trace.push({
6380
+ stage: "checkpoint",
6381
+ tool: triggeredTc.name,
6382
+ verdict: "block",
6383
+ reason: "checkpoint_no_handler",
6384
+ checked: { checkpoint: triggeredCp.name },
6385
+ found: { has_handler: false }
6386
+ });
6387
+ validation.failures.push({
6388
+ path: `$.tool_calls.${triggeredTc.name}`,
6389
+ operator: "checkpoint_no_handler",
6390
+ expected: "onCheckpoint callback",
6391
+ found: "not provided",
6392
+ message: `Checkpoint '${triggeredCp.name}' triggered but no onCheckpoint handler provided`,
6393
+ contract_file: ""
6394
+ });
6395
+ } else {
6396
+ trace.push({
6397
+ stage: "checkpoint",
6398
+ tool: triggeredTc.name,
6399
+ verdict: "paused",
6400
+ reason: "checkpoint_triggered",
6401
+ checked: { checkpoint: triggeredCp.name, conditions: matchedConditions },
6402
+ found: {}
6403
+ });
6404
+ emitDiagnostic2(diagnostics, {
6405
+ type: "replay_checkpoint_triggered",
6406
+ session_id: sessionId,
6407
+ checkpoint: triggeredCp.name,
6408
+ tool_name: triggeredTc.name
6409
+ });
6410
+ const approvalRequest = buildApprovalRequest(
6411
+ triggeredCp,
6412
+ triggeredTc,
6413
+ sessionId,
6414
+ matchedConditions
6415
+ );
6416
+ const approvalResponse = await waitForApproval(
6417
+ approvalRequest,
6418
+ opts.onCheckpoint,
6419
+ triggeredCp.timeout_seconds,
6420
+ triggeredCp.on_timeout,
6421
+ () => killed
6422
+ );
6423
+ sessionState = { ...sessionState, checkpointCount: sessionState.checkpointCount + 1 };
6424
+ if (approvalResponse.decision === "approve") {
6425
+ trace.push({
6426
+ stage: "checkpoint",
6427
+ tool: triggeredTc.name,
6428
+ verdict: "allow",
6429
+ reason: "checkpoint_approved",
6430
+ checked: { checkpoint: triggeredCp.name, decided_by: approvalResponse.decided_by },
6431
+ found: { decision: "approve", latency_ms: Date.now() - new Date(approvalRequest.requested_at).getTime() }
6432
+ });
6433
+ emitDiagnostic2(diagnostics, {
6434
+ type: "replay_checkpoint_resolved",
6435
+ session_id: sessionId,
6436
+ checkpoint: triggeredCp.name,
6437
+ decision: "approve"
6438
+ });
6439
+ } else {
6440
+ const isTimeout = approvalResponse.decided_by.startsWith("system:timeout");
6441
+ const blockReason = isTimeout ? "checkpoint_timeout" : "checkpoint_denied";
6442
+ trace.push({
6443
+ stage: "checkpoint",
6444
+ tool: triggeredTc.name,
6445
+ verdict: "block",
6446
+ reason: blockReason,
6447
+ checked: { checkpoint: triggeredCp.name, decided_by: approvalResponse.decided_by },
6448
+ found: {
6449
+ decision: "deny",
6450
+ ...approvalResponse.reason ? { reason: approvalResponse.reason } : {},
6451
+ latency_ms: Date.now() - new Date(approvalRequest.requested_at).getTime()
6452
+ }
6453
+ });
6454
+ emitDiagnostic2(diagnostics, {
6455
+ type: "replay_checkpoint_resolved",
6456
+ session_id: sessionId,
6457
+ checkpoint: triggeredCp.name,
6458
+ decision: isTimeout ? "timeout" : "deny"
6459
+ });
6460
+ validation.failures.push({
6461
+ path: `$.tool_calls.${triggeredTc.name}`,
6462
+ operator: blockReason,
6463
+ expected: "approve",
6464
+ found: approvalResponse.decision,
6465
+ message: `Checkpoint '${triggeredCp.name}' ${isTimeout ? "timed out" : "denied"}${approvalResponse.reason ? `: ${approvalResponse.reason}` : ""}`,
6466
+ contract_file: ""
6467
+ });
6468
+ }
6469
+ }
6470
+ }
6471
+ } else {
6472
+ trace.push({ stage: "checkpoint", tool: null, verdict: "skip", reason: "no_checkpoint_triggered", checked: { checkpoint_count: compiledSession.checkpoints.length }, found: {} });
6473
+ }
6474
+ } else if (compiledSession?.checkpoints && compiledSession.checkpoints.length > 0 && validation.failures.length > 0) {
6475
+ trace.push({ stage: "checkpoint", tool: null, verdict: "skip", reason: "skipped_due_to_validation_failures", checked: {}, found: { failure_count: validation.failures.length } });
6476
+ }
5596
6477
  currentTraceStage = "gate";
5597
6478
  if (mode === "shadow") {
5598
6479
  const shadowGateStart = Date.now();
@@ -5649,6 +6530,34 @@ function replay(client, opts = {}) {
5649
6530
  } else {
5650
6531
  completedStep.phase = sessionState.currentPhase;
5651
6532
  }
6533
+ for (const tc of toolCalls) {
6534
+ const contract = contracts.find((c) => c.tool === tc.name);
6535
+ if (contract?.binds && contract.binds.length > 0) {
6536
+ const bindResult = captureBindings(tc, contract, sessionState);
6537
+ sessionState = { ...sessionState, bindings: bindResult.bindings };
6538
+ }
6539
+ if (compiledSession?.aggregates && compiledSession.aggregates.length > 0) {
6540
+ let parsedArgs;
6541
+ try {
6542
+ parsedArgs = JSON.parse(tc.arguments);
6543
+ } catch {
6544
+ parsedArgs = {};
6545
+ }
6546
+ sessionState = { ...sessionState, aggregates: commitAggregate(tc.name, parsedArgs, sessionState.aggregates, compiledSession.aggregates) };
6547
+ }
6548
+ if (compiledSession?.envelopes && compiledSession.envelopes.length > 0) {
6549
+ let parsedArgs;
6550
+ try {
6551
+ parsedArgs = JSON.parse(tc.arguments);
6552
+ } catch {
6553
+ parsedArgs = {};
6554
+ }
6555
+ const envResult = evaluateEnvelopes(tc.name, parsedArgs, sessionState.envelopes, compiledSession.envelopes);
6556
+ if (envResult.captures.length > 0) {
6557
+ sessionState = { ...sessionState, envelopes: commitEnvelope(envResult.captures, sessionState.envelopes) };
6558
+ }
6559
+ }
6560
+ }
5652
6561
  const prevVersion = sessionState.stateVersion;
5653
6562
  sessionState = finalizeExecutedStep(sessionState, completedStep, contracts, compiledSession);
5654
6563
  syncStateToStore(prevVersion, sessionState);
@@ -5703,6 +6612,44 @@ function replay(client, opts = {}) {
5703
6612
  } else {
5704
6613
  completedStep.phase = sessionState.currentPhase;
5705
6614
  }
6615
+ for (const tc of toolCalls) {
6616
+ const contract = contracts.find((c) => c.tool === tc.name);
6617
+ if (contract?.binds && contract.binds.length > 0) {
6618
+ const bindResult = captureBindings(tc, contract, sessionState);
6619
+ sessionState = { ...sessionState, bindings: bindResult.bindings };
6620
+ for (const cap of bindResult.captured) {
6621
+ trace.push({
6622
+ stage: "bind",
6623
+ tool: tc.name,
6624
+ verdict: "captured",
6625
+ reason: cap.overwritten ? "binding_overwritten" : "binding_captured",
6626
+ checked: { slot: cap.slot, source: cap.source, path: cap.path },
6627
+ found: { value: cap.value }
6628
+ });
6629
+ }
6630
+ }
6631
+ if (compiledSession?.aggregates && compiledSession.aggregates.length > 0) {
6632
+ let parsedArgs;
6633
+ try {
6634
+ parsedArgs = JSON.parse(tc.arguments);
6635
+ } catch {
6636
+ parsedArgs = {};
6637
+ }
6638
+ sessionState = { ...sessionState, aggregates: commitAggregate(tc.name, parsedArgs, sessionState.aggregates, compiledSession.aggregates) };
6639
+ }
6640
+ if (compiledSession?.envelopes && compiledSession.envelopes.length > 0) {
6641
+ let parsedArgs;
6642
+ try {
6643
+ parsedArgs = JSON.parse(tc.arguments);
6644
+ } catch {
6645
+ parsedArgs = {};
6646
+ }
6647
+ const envResult = evaluateEnvelopes(tc.name, parsedArgs, sessionState.envelopes, compiledSession.envelopes);
6648
+ if (envResult.captures.length > 0) {
6649
+ sessionState = { ...sessionState, envelopes: commitEnvelope(envResult.captures, sessionState.envelopes) };
6650
+ }
6651
+ }
6652
+ }
5706
6653
  const prevVersionAllow = sessionState.stateVersion;
5707
6654
  sessionState = finalizeExecutedStep(sessionState, completedStep, contracts, compiledSession);
5708
6655
  sessionState = recordDecisionOutcome(sessionState, "allowed");
@@ -5994,6 +6941,22 @@ function replay(client, opts = {}) {
5994
6941
  });
5995
6942
  }
5996
6943
  },
6944
+ addLabel(label) {
6945
+ if (killed || restored) return;
6946
+ if (sessionState.labels.has(label)) return;
6947
+ if (sessionState.labels.size >= 64) {
6948
+ emitDiagnostic2(diagnostics, {
6949
+ type: "replay_label_cap",
6950
+ session_id: sessionId,
6951
+ label,
6952
+ detail: "Label cap (64) reached, label not added"
6953
+ });
6954
+ return;
6955
+ }
6956
+ const newLabels = new Set(sessionState.labels);
6957
+ newLabels.add(label);
6958
+ sessionState = { ...sessionState, labels: newLabels };
6959
+ },
5997
6960
  tools: wrapToolsWithDeferredReceipts(
5998
6961
  buildWrappedToolsMap(opts.tools, compiledSession)
5999
6962
  ),
@@ -6182,7 +7145,7 @@ function discoverSessionYaml(opts) {
6182
7145
  if (opts.sessionYamlPath) {
6183
7146
  const resolved = (0, import_node_path3.resolve)(opts.sessionYamlPath);
6184
7147
  const raw = (0, import_node_fs3.readFileSync)(resolved, "utf8");
6185
- return (0, import_contracts_core6.parseSessionYaml)(raw);
7148
+ return (0, import_contracts_core7.parseSessionYaml)(raw);
6186
7149
  }
6187
7150
  if (opts.contractsDir) {
6188
7151
  const dir = (0, import_node_path3.resolve)(opts.contractsDir);
@@ -6190,7 +7153,7 @@ function discoverSessionYaml(opts) {
6190
7153
  const candidate = (0, import_node_path3.join)(dir, name);
6191
7154
  if ((0, import_node_fs3.existsSync)(candidate)) {
6192
7155
  const raw = (0, import_node_fs3.readFileSync)(candidate, "utf8");
6193
- return (0, import_contracts_core6.parseSessionYaml)(raw);
7156
+ return (0, import_contracts_core7.parseSessionYaml)(raw);
6194
7157
  }
6195
7158
  }
6196
7159
  }
@@ -6214,11 +7177,11 @@ function discoverWorkflowYaml(opts, workflowOpts) {
6214
7177
  }
6215
7178
  }
6216
7179
  if (!raw) return null;
6217
- const parsed = (0, import_contracts_core6.parseWorkflowYaml)(raw);
6218
- return (0, import_contracts_core6.compileWorkflow)(parsed);
7180
+ const parsed = (0, import_contracts_core7.parseWorkflowYaml)(raw);
7181
+ return (0, import_contracts_core7.compileWorkflow)(parsed);
6219
7182
  }
6220
7183
  function generateWorkflowId() {
6221
- return `rw_${import_node_crypto5.default.randomUUID().replace(/-/g, "").slice(0, 24)}`;
7184
+ return `rw_${import_node_crypto6.default.randomUUID().replace(/-/g, "").slice(0, 24)}`;
6222
7185
  }
6223
7186
  function validateConfig(contracts, opts) {
6224
7187
  const hasPolicyBlock = contracts.some((c) => c.policy != null);
@@ -6310,7 +7273,7 @@ function validateResponse2(response, toolCalls, contracts, requestToolNames, unm
6310
7273
  const outputInvariants = contract.assertions.output_invariants;
6311
7274
  if (outputInvariants.length > 0) {
6312
7275
  const normalizedResponse = buildNormalizedResponse(response, toolCalls);
6313
- const result = (0, import_contracts_core6.evaluateInvariants)(normalizedResponse, outputInvariants, process.env);
7276
+ const result = (0, import_contracts_core7.evaluateInvariants)(normalizedResponse, outputInvariants, process.env);
6314
7277
  for (const failure of result) {
6315
7278
  failures.push({
6316
7279
  path: failure.path,
@@ -6323,7 +7286,7 @@ function validateResponse2(response, toolCalls, contracts, requestToolNames, unm
6323
7286
  }
6324
7287
  }
6325
7288
  if (contract.expected_tool_calls && contract.expected_tool_calls.length > 0) {
6326
- const result = (0, import_contracts_core6.evaluateExpectedToolCalls)(
7289
+ const result = (0, import_contracts_core7.evaluateExpectedToolCalls)(
6327
7290
  toolCalls,
6328
7291
  contract.expected_tool_calls,
6329
7292
  contract.pass_threshold ?? 1,
@@ -6476,7 +7439,7 @@ function evaluateInputInvariants(request, contracts) {
6476
7439
  for (const contract of contracts) {
6477
7440
  if (!requestToolSet.has(contract.tool)) continue;
6478
7441
  if (contract.assertions.input_invariants.length === 0) continue;
6479
- const result = (0, import_contracts_core6.evaluateInvariants)(request, contract.assertions.input_invariants, process.env);
7442
+ const result = (0, import_contracts_core7.evaluateInvariants)(request, contract.assertions.input_invariants, process.env);
6480
7443
  for (const failure of result) {
6481
7444
  failures.push({
6482
7445
  path: failure.path,
@@ -6800,6 +7763,8 @@ function createInactiveSession(client, sessionId, reason) {
6800
7763
  },
6801
7764
  widen() {
6802
7765
  },
7766
+ addLabel() {
7767
+ },
6803
7768
  tools: {},
6804
7769
  getWorkflowState: () => Promise.resolve(null),
6805
7770
  handoff: () => Promise.resolve(null)
@@ -6842,6 +7807,8 @@ function createBlockingInactiveSession(client, sessionId, detail, configError) {
6842
7807
  },
6843
7808
  widen() {
6844
7809
  },
7810
+ addLabel() {
7811
+ },
6845
7812
  tools: {},
6846
7813
  getWorkflowState: () => Promise.resolve(null),
6847
7814
  handoff: () => Promise.resolve(null)
@@ -6917,7 +7884,7 @@ function resolveApiKey2(opts) {
6917
7884
  return typeof envKey === "string" && envKey.length > 0 ? envKey : void 0;
6918
7885
  }
6919
7886
  function generateSessionId2() {
6920
- return `rs_${import_node_crypto5.default.randomUUID().replace(/-/g, "").slice(0, 24)}`;
7887
+ return `rs_${import_node_crypto6.default.randomUUID().replace(/-/g, "").slice(0, 24)}`;
6921
7888
  }
6922
7889
  function stripHashPrefix(hash) {
6923
7890
  return hash.startsWith("sha256:") ? hash.slice(7) : hash;