@replayci/replay 0.1.7 → 0.1.9
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 +469 -66
- package/dist/index.d.cts +52 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +469 -66
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3213,6 +3213,34 @@ function redactCapture(input) {
|
|
|
3213
3213
|
pattern_fingerprint: PATTERN_FINGERPRINT
|
|
3214
3214
|
};
|
|
3215
3215
|
}
|
|
3216
|
+
function redactTrace(trace, captureLevel) {
|
|
3217
|
+
if (captureLevel === "full") return trace;
|
|
3218
|
+
return {
|
|
3219
|
+
...trace,
|
|
3220
|
+
entries: trace.entries.map((entry) => redactTraceEntry(entry, captureLevel))
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function redactTraceEntry(entry, captureLevel) {
|
|
3224
|
+
if (captureLevel === "metadata") {
|
|
3225
|
+
return {
|
|
3226
|
+
...entry,
|
|
3227
|
+
checked: redactRecord(entry.checked),
|
|
3228
|
+
found: redactRecord(entry.found)
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
return entry;
|
|
3232
|
+
}
|
|
3233
|
+
function redactRecord(record) {
|
|
3234
|
+
const result = {};
|
|
3235
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3236
|
+
if (typeof value === "string") {
|
|
3237
|
+
result[key] = redactString(value);
|
|
3238
|
+
} else {
|
|
3239
|
+
result[key] = value;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
return result;
|
|
3243
|
+
}
|
|
3216
3244
|
|
|
3217
3245
|
// src/errors/replay.ts
|
|
3218
3246
|
var ReplayContractError = class extends Error {
|
|
@@ -3509,8 +3537,9 @@ function toRecord8(value) {
|
|
|
3509
3537
|
import crypto2 from "crypto";
|
|
3510
3538
|
|
|
3511
3539
|
// src/phases.ts
|
|
3512
|
-
function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
|
|
3540
|
+
function validatePhaseTransition(toolCalls, sessionState, compiledSession, ctx) {
|
|
3513
3541
|
if (!compiledSession.phases) {
|
|
3542
|
+
ctx?.trace.push({ stage: "phase", tool: null, verdict: "skip", reason: "no_phases_configured", checked: {}, found: {} });
|
|
3514
3543
|
return { legal: true, newPhase: sessionState.currentPhase };
|
|
3515
3544
|
}
|
|
3516
3545
|
const attemptedTransitions = [];
|
|
@@ -3521,6 +3550,14 @@ function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
|
|
|
3521
3550
|
sessionState.currentPhase ?? ""
|
|
3522
3551
|
);
|
|
3523
3552
|
if (!allowedTransitions?.includes(contract.transitions.advances_to)) {
|
|
3553
|
+
ctx?.trace.push({
|
|
3554
|
+
stage: "phase",
|
|
3555
|
+
tool: toolCall.name,
|
|
3556
|
+
verdict: "block",
|
|
3557
|
+
reason: "illegal_phase_transition",
|
|
3558
|
+
checked: { advances_to: contract.transitions.advances_to, from: sessionState.currentPhase },
|
|
3559
|
+
found: { allowed_transitions: allowedTransitions ?? [] }
|
|
3560
|
+
});
|
|
3524
3561
|
return {
|
|
3525
3562
|
legal: false,
|
|
3526
3563
|
newPhase: sessionState.currentPhase,
|
|
@@ -3537,6 +3574,14 @@ function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
|
|
|
3537
3574
|
if (attemptedTransitions.length > 1) {
|
|
3538
3575
|
const distinctTargets = new Set(attemptedTransitions.map((t) => t.target));
|
|
3539
3576
|
if (distinctTargets.size > 1) {
|
|
3577
|
+
ctx?.trace.push({
|
|
3578
|
+
stage: "phase",
|
|
3579
|
+
tool: attemptedTransitions.map((t) => t.tool).join(", "),
|
|
3580
|
+
verdict: "block",
|
|
3581
|
+
reason: "ambiguous_phase_transition",
|
|
3582
|
+
checked: { targets: Array.from(distinctTargets) },
|
|
3583
|
+
found: { from: sessionState.currentPhase }
|
|
3584
|
+
});
|
|
3540
3585
|
return {
|
|
3541
3586
|
legal: false,
|
|
3542
3587
|
newPhase: sessionState.currentPhase,
|
|
@@ -3547,7 +3592,17 @@ function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
|
|
|
3547
3592
|
}
|
|
3548
3593
|
}
|
|
3549
3594
|
if (attemptedTransitions.length > 0) {
|
|
3550
|
-
|
|
3595
|
+
const target = attemptedTransitions[0].target;
|
|
3596
|
+
const allowedTransitions = compiledSession.transitions.get(sessionState.currentPhase ?? "") ?? [];
|
|
3597
|
+
ctx?.trace.push({
|
|
3598
|
+
stage: "phase",
|
|
3599
|
+
tool: attemptedTransitions[0].tool,
|
|
3600
|
+
verdict: "allow",
|
|
3601
|
+
reason: "phase_advanced",
|
|
3602
|
+
checked: { advances_to: target, from: sessionState.currentPhase },
|
|
3603
|
+
found: { allowed_transitions: allowedTransitions }
|
|
3604
|
+
});
|
|
3605
|
+
return { legal: true, newPhase: target };
|
|
3551
3606
|
}
|
|
3552
3607
|
return { legal: true, newPhase: sessionState.currentPhase };
|
|
3553
3608
|
}
|
|
@@ -3797,7 +3852,7 @@ function checkCircuitBreaker(state, config) {
|
|
|
3797
3852
|
}
|
|
3798
3853
|
|
|
3799
3854
|
// src/crossStep.ts
|
|
3800
|
-
function validateCrossStep(toolCalls, sessionState, contracts) {
|
|
3855
|
+
function validateCrossStep(toolCalls, sessionState, contracts, ctx) {
|
|
3801
3856
|
const failures = [];
|
|
3802
3857
|
const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
|
|
3803
3858
|
const workingForbidden = new Set(sessionState.forbiddenTools);
|
|
@@ -3825,8 +3880,17 @@ function validateCrossStep(toolCalls, sessionState, contracts) {
|
|
|
3825
3880
|
reason: "forbidden_tool",
|
|
3826
3881
|
detail: resourceValue !== void 0 ? `Tool "${tc.name}" is forbidden in this session for resource ${JSON.stringify(resourceValue)}` : `Tool "${tc.name}" is forbidden in this session`
|
|
3827
3882
|
});
|
|
3883
|
+
ctx?.trace.push({
|
|
3884
|
+
stage: "cross_step",
|
|
3885
|
+
tool: tc.name,
|
|
3886
|
+
verdict: "block",
|
|
3887
|
+
reason: "forbidden_tool",
|
|
3888
|
+
checked: { tool: tc.name },
|
|
3889
|
+
found: { is_resource_scoped: resourceValue !== void 0, resource_value: resourceValue ?? null }
|
|
3890
|
+
});
|
|
3828
3891
|
continue;
|
|
3829
3892
|
}
|
|
3893
|
+
let crossStepPassed = true;
|
|
3830
3894
|
if (contract?.preconditions && contract.preconditions.length > 0) {
|
|
3831
3895
|
const results = evaluatePreconditions(
|
|
3832
3896
|
contract.preconditions,
|
|
@@ -3835,6 +3899,7 @@ function validateCrossStep(toolCalls, sessionState, contracts) {
|
|
|
3835
3899
|
);
|
|
3836
3900
|
for (const result of results) {
|
|
3837
3901
|
if (!result.satisfied) {
|
|
3902
|
+
crossStepPassed = false;
|
|
3838
3903
|
failures.push({
|
|
3839
3904
|
toolName: tc.name,
|
|
3840
3905
|
reason: "precondition_not_met",
|
|
@@ -3843,6 +3908,25 @@ function validateCrossStep(toolCalls, sessionState, contracts) {
|
|
|
3843
3908
|
}
|
|
3844
3909
|
}
|
|
3845
3910
|
}
|
|
3911
|
+
if (crossStepPassed) {
|
|
3912
|
+
ctx?.trace.push({
|
|
3913
|
+
stage: "cross_step",
|
|
3914
|
+
tool: tc.name,
|
|
3915
|
+
verdict: "allow",
|
|
3916
|
+
reason: "preconditions_satisfied",
|
|
3917
|
+
checked: { requires_prior_tool: contract?.preconditions?.[0]?.requires_prior_tool ?? null },
|
|
3918
|
+
found: { resource_value: resourceValue ?? null }
|
|
3919
|
+
});
|
|
3920
|
+
} else {
|
|
3921
|
+
ctx?.trace.push({
|
|
3922
|
+
stage: "cross_step",
|
|
3923
|
+
tool: tc.name,
|
|
3924
|
+
verdict: "block",
|
|
3925
|
+
reason: "precondition_not_met",
|
|
3926
|
+
checked: { requires_prior_tool: contract?.preconditions?.[0]?.requires_prior_tool ?? null },
|
|
3927
|
+
found: { resource_value: resourceValue ?? null, matching_step_index: null }
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3846
3930
|
if (contract?.forbids_after) {
|
|
3847
3931
|
for (const entry of contract.forbids_after) {
|
|
3848
3932
|
if (typeof entry === "string") {
|
|
@@ -4066,20 +4150,23 @@ function extractPath2(obj, path) {
|
|
|
4066
4150
|
}
|
|
4067
4151
|
|
|
4068
4152
|
// src/narrow.ts
|
|
4069
|
-
function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPolicy, manualFilter) {
|
|
4153
|
+
function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPolicy, manualFilter, ctx) {
|
|
4070
4154
|
const allowed = [];
|
|
4071
4155
|
const removed = [];
|
|
4072
4156
|
for (const tool of requestedTools) {
|
|
4073
4157
|
if (manualFilter && !manualFilter.includes(tool.name)) {
|
|
4074
4158
|
removed.push({ tool: tool.name, reason: "manual_filter" });
|
|
4159
|
+
ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "manual_filter", checked: { filter: manualFilter }, found: {} });
|
|
4075
4160
|
continue;
|
|
4076
4161
|
}
|
|
4077
4162
|
const contract = compiledSession.perToolContracts.get(tool.name);
|
|
4078
4163
|
if (!contract) {
|
|
4079
4164
|
if (unmatchedPolicy === "allow") {
|
|
4080
4165
|
allowed.push(tool);
|
|
4166
|
+
ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "allow", reason: "no_contract_passthrough", checked: { unmatched_policy: "allow" }, found: {} });
|
|
4081
4167
|
} else {
|
|
4082
4168
|
removed.push({ tool: tool.name, reason: "no_contract" });
|
|
4169
|
+
ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "no_contract", checked: { unmatched_policy: "block" }, found: {} });
|
|
4083
4170
|
}
|
|
4084
4171
|
continue;
|
|
4085
4172
|
}
|
|
@@ -4092,6 +4179,7 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
|
|
|
4092
4179
|
reason: "wrong_phase",
|
|
4093
4180
|
detail: `Tool valid in [${contract.transitions.valid_in_phases.join(", ")}], current phase: ${sessionState.currentPhase}`
|
|
4094
4181
|
});
|
|
4182
|
+
ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "wrong_phase", checked: { valid_in_phases: contract.transitions.valid_in_phases }, found: { current_phase: sessionState.currentPhase } });
|
|
4095
4183
|
continue;
|
|
4096
4184
|
}
|
|
4097
4185
|
}
|
|
@@ -4102,6 +4190,18 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
|
|
|
4102
4190
|
);
|
|
4103
4191
|
const unsatisfied = results.find((r) => !r.satisfied);
|
|
4104
4192
|
if (unsatisfied) {
|
|
4193
|
+
const firstPre = contract.preconditions[0];
|
|
4194
|
+
ctx?.trace.push({
|
|
4195
|
+
stage: "narrow",
|
|
4196
|
+
tool: tool.name,
|
|
4197
|
+
verdict: "remove",
|
|
4198
|
+
reason: "precondition_not_met",
|
|
4199
|
+
checked: {
|
|
4200
|
+
requires_prior_tool: firstPre.requires_prior_tool ?? null,
|
|
4201
|
+
with_output: firstPre.with_output ?? []
|
|
4202
|
+
},
|
|
4203
|
+
found: { satisfied_precondition_cache_hit: false }
|
|
4204
|
+
});
|
|
4105
4205
|
removed.push({
|
|
4106
4206
|
tool: tool.name,
|
|
4107
4207
|
reason: "precondition_not_met",
|
|
@@ -4115,6 +4215,7 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
|
|
|
4115
4215
|
tool: tool.name,
|
|
4116
4216
|
reason: "forbidden_in_state"
|
|
4117
4217
|
});
|
|
4218
|
+
ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "forbidden_in_state", checked: { tool: tool.name }, found: { is_resource_scoped: false } });
|
|
4118
4219
|
continue;
|
|
4119
4220
|
}
|
|
4120
4221
|
if (compiledSession.policyProgram && compiledSession.principal !== null && compiledSession.principal !== void 0) {
|
|
@@ -4131,9 +4232,24 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
|
|
|
4131
4232
|
reason: "policy_denied",
|
|
4132
4233
|
detail: verdict.reason ?? "Policy deny rule matched"
|
|
4133
4234
|
});
|
|
4235
|
+
ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "policy_denied", checked: { rule_type: "session_deny" }, found: { matched: true } });
|
|
4134
4236
|
continue;
|
|
4135
4237
|
}
|
|
4136
4238
|
}
|
|
4239
|
+
ctx?.trace.push({
|
|
4240
|
+
stage: "narrow",
|
|
4241
|
+
tool: tool.name,
|
|
4242
|
+
verdict: "allow",
|
|
4243
|
+
reason: "all_checks_passed",
|
|
4244
|
+
checked: {
|
|
4245
|
+
has_contract: true,
|
|
4246
|
+
phase_ok: true,
|
|
4247
|
+
preconditions_ok: true,
|
|
4248
|
+
not_forbidden: true,
|
|
4249
|
+
policy_ok: true
|
|
4250
|
+
},
|
|
4251
|
+
found: {}
|
|
4252
|
+
});
|
|
4137
4253
|
allowed.push(tool);
|
|
4138
4254
|
}
|
|
4139
4255
|
return { allowed, removed };
|
|
@@ -4627,7 +4743,7 @@ function replay(client, opts = {}) {
|
|
|
4627
4743
|
const unmatchedPolicy = opts.unmatchedPolicy ?? "block";
|
|
4628
4744
|
const maxRetries = Math.min(Math.max(0, opts.maxRetries ?? 0), MAX_RETRIES);
|
|
4629
4745
|
const compatEnforcement = opts.compatEnforcement ?? "protective";
|
|
4630
|
-
const diagnostics = opts.diagnostics;
|
|
4746
|
+
const diagnostics = opts.diagnostics ?? defaultReplayDiagnosticsHandler;
|
|
4631
4747
|
let provider;
|
|
4632
4748
|
try {
|
|
4633
4749
|
provider = detectProvider(client);
|
|
@@ -4660,6 +4776,12 @@ function replay(client, opts = {}) {
|
|
|
4660
4776
|
emitDiagnostic2(diagnostics, { type: "replay_compile_error", details: detail });
|
|
4661
4777
|
return createBlockingInactiveSession(client, sessionId, detail);
|
|
4662
4778
|
}
|
|
4779
|
+
if (opts.contractsDir && !discoveredSessionYaml && !opts.sessionYamlPath) {
|
|
4780
|
+
emitDiagnostic2(diagnostics, {
|
|
4781
|
+
type: "replay_compile_warning",
|
|
4782
|
+
details: "No session.yaml found in contractsDir \u2014 session-level features (phases, policy, session_limits) are inactive. Per-tool contracts still apply."
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4663
4785
|
let sessionYaml = discoveredSessionYaml;
|
|
4664
4786
|
if (!sessionYaml && opts.providerConstraints) {
|
|
4665
4787
|
sessionYaml = { schema_version: "1.0", agent, provider_constraints: opts.providerConstraints };
|
|
@@ -4819,6 +4941,7 @@ function replay(client, opts = {}) {
|
|
|
4819
4941
|
let bypassDetected = false;
|
|
4820
4942
|
let lastShadowDeltaValue = null;
|
|
4821
4943
|
let lastNarrowResult = null;
|
|
4944
|
+
let lastTrace = null;
|
|
4822
4945
|
let shadowEvaluationCount = 0;
|
|
4823
4946
|
let manualFilter = null;
|
|
4824
4947
|
const deferredReceipts = /* @__PURE__ */ new Map();
|
|
@@ -4826,6 +4949,17 @@ function replay(client, opts = {}) {
|
|
|
4826
4949
|
const compiledLimits = compiledSession?.sessionLimits;
|
|
4827
4950
|
const mergedLimits = { ...contractLimits ?? {}, ...compiledLimits ?? {} };
|
|
4828
4951
|
const resolvedSessionLimits = Object.keys(mergedLimits).length > 0 ? mergedLimits : null;
|
|
4952
|
+
if (resolvedSessionLimits?.max_tool_calls_mode === "narrow" && resolvedSessionLimits.max_calls_per_tool) {
|
|
4953
|
+
const budgetedTools = new Set(Object.keys(resolvedSessionLimits.max_calls_per_tool));
|
|
4954
|
+
const unbudgeted = contracts.map((c) => c.tool).filter((t) => !budgetedTools.has(t));
|
|
4955
|
+
if (unbudgeted.length > 0) {
|
|
4956
|
+
emitDiagnostic2(diagnostics, {
|
|
4957
|
+
type: "replay_narrow_unbudgeted_tools",
|
|
4958
|
+
session_id: sessionId,
|
|
4959
|
+
tools: unbudgeted
|
|
4960
|
+
});
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4829
4963
|
const store = opts.store ?? null;
|
|
4830
4964
|
let storeLoadPromise = null;
|
|
4831
4965
|
let storeLoadDone = false;
|
|
@@ -4893,6 +5027,18 @@ function replay(client, opts = {}) {
|
|
|
4893
5027
|
} catch {
|
|
4894
5028
|
}
|
|
4895
5029
|
}
|
|
5030
|
+
function createTrace(stepIndex) {
|
|
5031
|
+
const entries = [];
|
|
5032
|
+
return {
|
|
5033
|
+
sessionId,
|
|
5034
|
+
stepIndex,
|
|
5035
|
+
complete: false,
|
|
5036
|
+
entries,
|
|
5037
|
+
push(entry) {
|
|
5038
|
+
entries.push(entry);
|
|
5039
|
+
}
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
4896
5042
|
const enforcementCreate = async function replayEnforcementCreate(...args) {
|
|
4897
5043
|
if (killed) {
|
|
4898
5044
|
throw new ReplayKillError(sessionId, killedAt);
|
|
@@ -4935,8 +5081,19 @@ function replay(client, opts = {}) {
|
|
|
4935
5081
|
total_ms: 0,
|
|
4936
5082
|
enforcement_ms: 0
|
|
4937
5083
|
};
|
|
5084
|
+
const trace = createTrace(sessionState.totalStepCount);
|
|
5085
|
+
const traceCtx = { trace };
|
|
5086
|
+
let currentTraceStage = "narrow";
|
|
4938
5087
|
const request = toRecord10(args[0]);
|
|
4939
5088
|
const requestToolNames = extractRequestToolNames(request);
|
|
5089
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
5090
|
+
if (messages.length > 0) {
|
|
5091
|
+
const toolResults = extractToolResults(messages, provider);
|
|
5092
|
+
if (toolResults.length > 0) {
|
|
5093
|
+
const outputUpdates = extractOutputFromToolResults(toolResults, sessionState, contracts);
|
|
5094
|
+
sessionState = applyOutputExtracts(sessionState, outputUpdates);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
4940
5097
|
let narrowResult = null;
|
|
4941
5098
|
let activeArgs = args;
|
|
4942
5099
|
if (compiledSession && Array.isArray(request.tools) && request.tools.length > 0) {
|
|
@@ -4947,7 +5104,8 @@ function replay(client, opts = {}) {
|
|
|
4947
5104
|
sessionState,
|
|
4948
5105
|
compiledSession,
|
|
4949
5106
|
unmatchedPolicy,
|
|
4950
|
-
manualFilter
|
|
5107
|
+
manualFilter,
|
|
5108
|
+
traceCtx
|
|
4951
5109
|
);
|
|
4952
5110
|
lastNarrowResult = narrowResult;
|
|
4953
5111
|
if (narrowResult.removed.length > 0) {
|
|
@@ -4985,55 +5143,96 @@ function replay(client, opts = {}) {
|
|
|
4985
5143
|
timing.narrow_ms = Date.now() - guardStart;
|
|
4986
5144
|
const preCheckStart = Date.now();
|
|
4987
5145
|
try {
|
|
5146
|
+
currentTraceStage = "pre_check";
|
|
4988
5147
|
if (mode === "enforce" && resolvedSessionLimits) {
|
|
4989
5148
|
const limitResult = checkSessionLimits(sessionState, resolvedSessionLimits);
|
|
4990
5149
|
if (limitResult.exceeded) {
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5150
|
+
let narrowedPastLimit = false;
|
|
5151
|
+
if (limitResult.reason?.startsWith("max_tool_calls") && resolvedSessionLimits.max_tool_calls_mode === "narrow" && resolvedSessionLimits.max_calls_per_tool) {
|
|
5152
|
+
const costOk = !(typeof resolvedSessionLimits.max_cost_per_session === "number" && sessionState.actualCost >= resolvedSessionLimits.max_cost_per_session);
|
|
5153
|
+
if (costOk) {
|
|
5154
|
+
const currentRequest = toRecord10(activeArgs[0]);
|
|
5155
|
+
const currentTools = Array.isArray(currentRequest.tools) ? extractToolDefinitions(currentRequest.tools) : [];
|
|
5156
|
+
const budgetedTools = currentTools.filter((tool) => {
|
|
5157
|
+
const max = resolvedSessionLimits.max_calls_per_tool[tool.name];
|
|
5158
|
+
if (typeof max !== "number") return false;
|
|
5159
|
+
return (sessionState.toolCallCounts.get(tool.name) ?? 0) < max;
|
|
5160
|
+
});
|
|
5161
|
+
if (budgetedTools.length > 0) {
|
|
5162
|
+
const modifiedRequest = { ...currentRequest, tools: budgetedTools };
|
|
5163
|
+
activeArgs = [modifiedRequest, ...Array.prototype.slice.call(activeArgs, 1)];
|
|
5164
|
+
narrowedPastLimit = true;
|
|
5165
|
+
trace.push({
|
|
5166
|
+
stage: "pre_check",
|
|
5167
|
+
tool: null,
|
|
5168
|
+
verdict: "narrow",
|
|
5169
|
+
reason: "max_tool_calls_narrow_mode",
|
|
5170
|
+
checked: { max_tool_calls: resolvedSessionLimits.max_tool_calls ?? null, budgeted_tools: budgetedTools.map((t) => t.name) },
|
|
5171
|
+
found: { total_tool_calls: sessionState.totalToolCalls }
|
|
5172
|
+
});
|
|
5173
|
+
}
|
|
5011
5174
|
}
|
|
5012
5175
|
}
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
void 0,
|
|
5026
|
-
timing
|
|
5027
|
-
);
|
|
5028
|
-
if (isCompatAdvisory) {
|
|
5029
|
-
emitDiagnostic2(diagnostics, {
|
|
5030
|
-
type: "replay_compat_advisory",
|
|
5031
|
-
session_id: sessionId,
|
|
5032
|
-
would_block: decision.blocked,
|
|
5033
|
-
details: limitResult.reason ?? "session limit exceeded"
|
|
5176
|
+
if (!narrowedPastLimit) {
|
|
5177
|
+
trace.push({
|
|
5178
|
+
stage: "pre_check",
|
|
5179
|
+
tool: null,
|
|
5180
|
+
verdict: "block",
|
|
5181
|
+
reason: "session_limit_exceeded",
|
|
5182
|
+
checked: {
|
|
5183
|
+
max_steps: resolvedSessionLimits.max_steps ?? null,
|
|
5184
|
+
max_tool_calls: resolvedSessionLimits.max_tool_calls ?? null,
|
|
5185
|
+
max_cost: resolvedSessionLimits.max_cost_per_session ?? null
|
|
5186
|
+
},
|
|
5187
|
+
found: { total_steps: sessionState.totalStepCount, total_tool_calls: sessionState.totalToolCalls, actual_cost: sessionState.actualCost }
|
|
5034
5188
|
});
|
|
5035
|
-
|
|
5036
|
-
|
|
5189
|
+
const decision = {
|
|
5190
|
+
action: "block",
|
|
5191
|
+
tool_calls: [],
|
|
5192
|
+
blocked: [{
|
|
5193
|
+
tool_name: "_session",
|
|
5194
|
+
arguments: "",
|
|
5195
|
+
reason: "session_limit_exceeded",
|
|
5196
|
+
contract_file: "",
|
|
5197
|
+
failures: [{ path: "$", operator: "session_limit", expected: "", found: "", message: limitResult.reason ?? "session limit exceeded" }]
|
|
5198
|
+
}],
|
|
5199
|
+
response_modification: gateMode
|
|
5200
|
+
};
|
|
5201
|
+
sessionState = recordDecisionOutcome(sessionState, "blocked");
|
|
5202
|
+
if (resolvedSessionLimits.circuit_breaker) {
|
|
5203
|
+
const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
|
|
5204
|
+
if (cbResult.triggered) {
|
|
5205
|
+
killed = true;
|
|
5206
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5207
|
+
sessionState = killSession(sessionState);
|
|
5208
|
+
emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
timing.pre_check_ms = Date.now() - preCheckStart;
|
|
5212
|
+
captureDecision(
|
|
5213
|
+
decision,
|
|
5214
|
+
null,
|
|
5215
|
+
request,
|
|
5216
|
+
guardStart,
|
|
5217
|
+
requestToolNames,
|
|
5218
|
+
null,
|
|
5219
|
+
narrowResult,
|
|
5220
|
+
null,
|
|
5221
|
+
null,
|
|
5222
|
+
null,
|
|
5223
|
+
void 0,
|
|
5224
|
+
timing
|
|
5225
|
+
);
|
|
5226
|
+
if (isCompatAdvisory) {
|
|
5227
|
+
emitDiagnostic2(diagnostics, {
|
|
5228
|
+
type: "replay_compat_advisory",
|
|
5229
|
+
session_id: sessionId,
|
|
5230
|
+
would_block: decision.blocked,
|
|
5231
|
+
details: limitResult.reason ?? "session limit exceeded"
|
|
5232
|
+
});
|
|
5233
|
+
} else {
|
|
5234
|
+
throw buildContractError2(decision);
|
|
5235
|
+
}
|
|
5037
5236
|
}
|
|
5038
5237
|
}
|
|
5039
5238
|
if (isAtHardStepCap(sessionState)) {
|
|
@@ -5066,8 +5265,23 @@ function replay(client, opts = {}) {
|
|
|
5066
5265
|
);
|
|
5067
5266
|
throw buildContractError2(decision);
|
|
5068
5267
|
}
|
|
5268
|
+
if (!checkSessionLimits(sessionState, resolvedSessionLimits).exceeded) {
|
|
5269
|
+
trace.push({
|
|
5270
|
+
stage: "pre_check",
|
|
5271
|
+
tool: null,
|
|
5272
|
+
verdict: "allow",
|
|
5273
|
+
reason: "session_limits_ok",
|
|
5274
|
+
checked: {
|
|
5275
|
+
max_steps: resolvedSessionLimits.max_steps ?? null,
|
|
5276
|
+
max_tool_calls: resolvedSessionLimits.max_tool_calls ?? null,
|
|
5277
|
+
max_cost: resolvedSessionLimits.max_cost_per_session ?? null
|
|
5278
|
+
},
|
|
5279
|
+
found: { total_steps: sessionState.totalStepCount, total_tool_calls: sessionState.totalToolCalls, actual_cost: sessionState.actualCost }
|
|
5280
|
+
});
|
|
5281
|
+
}
|
|
5282
|
+
} else if (mode === "enforce") {
|
|
5283
|
+
trace.push({ stage: "pre_check", tool: null, verdict: "skip", reason: "no_session_limits", checked: {}, found: {} });
|
|
5069
5284
|
}
|
|
5070
|
-
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
5071
5285
|
if (messages.length > 0) {
|
|
5072
5286
|
const msgResult = validateToolResultMessages(messages, contracts, provider);
|
|
5073
5287
|
if (!msgResult.passed) {
|
|
@@ -5077,13 +5291,6 @@ function replay(client, opts = {}) {
|
|
|
5077
5291
|
});
|
|
5078
5292
|
}
|
|
5079
5293
|
}
|
|
5080
|
-
if (messages.length > 0) {
|
|
5081
|
-
const toolResults = extractToolResults(messages, provider);
|
|
5082
|
-
if (toolResults.length > 0) {
|
|
5083
|
-
const outputUpdates = extractOutputFromToolResults(toolResults, sessionState, contracts);
|
|
5084
|
-
sessionState = applyOutputExtracts(sessionState, outputUpdates);
|
|
5085
|
-
}
|
|
5086
|
-
}
|
|
5087
5294
|
const inputFailures = evaluateInputInvariants(request, contracts);
|
|
5088
5295
|
if (mode === "enforce" && inputFailures.length > 0) {
|
|
5089
5296
|
if (onError === "block") {
|
|
@@ -5158,6 +5365,10 @@ function replay(client, opts = {}) {
|
|
|
5158
5365
|
sessionState = updateActualCost(sessionState, costDelta);
|
|
5159
5366
|
}
|
|
5160
5367
|
if (mode === "log-only") {
|
|
5368
|
+
trace.push({ stage: "gate", tool: null, verdict: "allow", reason: "log_only_mode", checked: {}, found: {} });
|
|
5369
|
+
trace.complete = true;
|
|
5370
|
+
lastTrace = trace;
|
|
5371
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5161
5372
|
captureDecision(
|
|
5162
5373
|
{ action: "allow", tool_calls: extractToolCalls(response, provider) },
|
|
5163
5374
|
response,
|
|
@@ -5170,13 +5381,26 @@ function replay(client, opts = {}) {
|
|
|
5170
5381
|
null,
|
|
5171
5382
|
null,
|
|
5172
5383
|
void 0,
|
|
5173
|
-
timing
|
|
5384
|
+
timing,
|
|
5385
|
+
trace
|
|
5174
5386
|
);
|
|
5175
5387
|
return response;
|
|
5176
5388
|
}
|
|
5389
|
+
currentTraceStage = "validate";
|
|
5177
5390
|
const toolCalls = extractToolCalls(response, provider);
|
|
5178
5391
|
const validateStart = Date.now();
|
|
5179
5392
|
const validation = validateResponse2(response, toolCalls, contracts, requestToolNames, unmatchedPolicy, provider);
|
|
5393
|
+
for (const f of validation.failures) {
|
|
5394
|
+
const toolName = extractToolNameFromFailure(f, toolCalls);
|
|
5395
|
+
trace.push({
|
|
5396
|
+
stage: "validate",
|
|
5397
|
+
tool: toolName === "_response" ? null : toolName,
|
|
5398
|
+
verdict: "block",
|
|
5399
|
+
reason: f.operator === "response_format" ? "response_format_failed" : "output_invariant_failed",
|
|
5400
|
+
checked: { path: f.path, operator: f.operator, invariant_type: f.operator === "response_format" ? "response_format" : "output" },
|
|
5401
|
+
found: { value: f.found }
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5180
5404
|
timing.validate_ms += Date.now() - validateStart;
|
|
5181
5405
|
if (isActiveGovern && !attemptDegraded && attemptPreparedRequestId) {
|
|
5182
5406
|
const rtProposalStart = Date.now();
|
|
@@ -5211,9 +5435,10 @@ function replay(client, opts = {}) {
|
|
|
5211
5435
|
}
|
|
5212
5436
|
timing.runtime_ms += Date.now() - rtProposalStart;
|
|
5213
5437
|
}
|
|
5438
|
+
currentTraceStage = "cross_step";
|
|
5214
5439
|
const crossStepStart = Date.now();
|
|
5215
5440
|
const crossStepContracts = compiledSession ? Array.from(compiledSession.perToolContracts.values()) : contracts;
|
|
5216
|
-
const crossStepResult = validateCrossStep(toolCalls, sessionState, crossStepContracts);
|
|
5441
|
+
const crossStepResult = validateCrossStep(toolCalls, sessionState, crossStepContracts, traceCtx);
|
|
5217
5442
|
if (!crossStepResult.passed) {
|
|
5218
5443
|
for (const f of crossStepResult.failures) {
|
|
5219
5444
|
validation.failures.push({
|
|
@@ -5227,10 +5452,11 @@ function replay(client, opts = {}) {
|
|
|
5227
5452
|
}
|
|
5228
5453
|
}
|
|
5229
5454
|
timing.cross_step_ms += Date.now() - crossStepStart;
|
|
5455
|
+
currentTraceStage = "phase";
|
|
5230
5456
|
let phaseResult = null;
|
|
5231
5457
|
const phaseStart = Date.now();
|
|
5232
5458
|
if (compiledSession) {
|
|
5233
|
-
phaseResult = validatePhaseTransition(toolCalls, sessionState, compiledSession);
|
|
5459
|
+
phaseResult = validatePhaseTransition(toolCalls, sessionState, compiledSession, traceCtx);
|
|
5234
5460
|
if (!phaseResult.legal) {
|
|
5235
5461
|
validation.failures.push({
|
|
5236
5462
|
path: `$.tool_calls.${phaseResult.blockedTool}`,
|
|
@@ -5260,7 +5486,7 @@ function replay(client, opts = {}) {
|
|
|
5260
5486
|
for (const f of avResult.failures) {
|
|
5261
5487
|
validation.failures.push({
|
|
5262
5488
|
path: f.path,
|
|
5263
|
-
operator:
|
|
5489
|
+
operator: "argument_value_mismatch",
|
|
5264
5490
|
expected: String(f.expected),
|
|
5265
5491
|
found: String(f.actual),
|
|
5266
5492
|
message: f.detail,
|
|
@@ -5271,10 +5497,12 @@ function replay(client, opts = {}) {
|
|
|
5271
5497
|
}
|
|
5272
5498
|
}
|
|
5273
5499
|
}
|
|
5500
|
+
currentTraceStage = "limit";
|
|
5274
5501
|
if (resolvedSessionLimits) {
|
|
5275
5502
|
const workingState = { ...sessionState, toolCallCounts: workingToolCallCounts };
|
|
5276
5503
|
const perToolResult = checkPerToolLimits(workingState, tc.name, resolvedSessionLimits);
|
|
5277
5504
|
if (perToolResult.exceeded) {
|
|
5505
|
+
trace.push({ stage: "limit", tool: tc.name, verdict: "block", reason: "per_tool_limit_exceeded", checked: { max_calls: resolvedSessionLimits.max_calls_per_tool?.[tc.name] ?? null }, found: { current_calls: workingToolCallCounts.get(tc.name) ?? 0 } });
|
|
5278
5506
|
validation.failures.push({
|
|
5279
5507
|
path: `$.tool_calls.${tc.name}`,
|
|
5280
5508
|
operator: "session_limit",
|
|
@@ -5283,6 +5511,8 @@ function replay(client, opts = {}) {
|
|
|
5283
5511
|
message: perToolResult.reason ?? "per-tool limit exceeded",
|
|
5284
5512
|
contract_file: ""
|
|
5285
5513
|
});
|
|
5514
|
+
} else {
|
|
5515
|
+
trace.push({ stage: "limit", tool: tc.name, verdict: "allow", reason: "per_tool_limit_ok", checked: { max_calls: resolvedSessionLimits.max_calls_per_tool?.[tc.name] ?? null }, found: { current_calls: workingToolCallCounts.get(tc.name) ?? 0 } });
|
|
5286
5516
|
}
|
|
5287
5517
|
}
|
|
5288
5518
|
workingToolCallCounts.set(tc.name, (workingToolCallCounts.get(tc.name) ?? 0) + 1);
|
|
@@ -5299,6 +5529,7 @@ function replay(client, opts = {}) {
|
|
|
5299
5529
|
).length;
|
|
5300
5530
|
const totalMatches = loopResult.matchCount + intraMatches;
|
|
5301
5531
|
if (totalMatches >= resolvedSessionLimits.loop_detection.threshold) {
|
|
5532
|
+
trace.push({ stage: "limit", tool: tc.name, verdict: "block", reason: "loop_detected", checked: { window: resolvedSessionLimits.loop_detection.window, threshold: resolvedSessionLimits.loop_detection.threshold }, found: { match_count: totalMatches, arguments_hash: argsHash } });
|
|
5302
5533
|
validation.failures.push({
|
|
5303
5534
|
path: `$.tool_calls.${tc.name}`,
|
|
5304
5535
|
operator: "loop_detected",
|
|
@@ -5312,6 +5543,7 @@ function replay(client, opts = {}) {
|
|
|
5312
5543
|
}
|
|
5313
5544
|
}
|
|
5314
5545
|
timing.argument_values_ms += Date.now() - argValuesStart;
|
|
5546
|
+
currentTraceStage = "policy";
|
|
5315
5547
|
let policyVerdicts = null;
|
|
5316
5548
|
const policyStart = Date.now();
|
|
5317
5549
|
if (compiledSession?.policyProgram && compiledSession.principal !== null && compiledSession.principal !== void 0) {
|
|
@@ -5332,6 +5564,14 @@ function replay(client, opts = {}) {
|
|
|
5332
5564
|
);
|
|
5333
5565
|
policyVerdicts.set(tc.name, verdict);
|
|
5334
5566
|
if (!verdict.allowed) {
|
|
5567
|
+
trace.push({
|
|
5568
|
+
stage: "policy",
|
|
5569
|
+
tool: tc.name,
|
|
5570
|
+
verdict: "block",
|
|
5571
|
+
reason: verdict.reason?.startsWith("Session deny") ? "session_deny_matched" : verdict.reason?.startsWith("default_deny") ? "default_deny_no_allow" : "policy_denied",
|
|
5572
|
+
checked: { has_policy: true, default_deny: compiledSession.policyProgram.defaultDeny },
|
|
5573
|
+
found: { matched: true }
|
|
5574
|
+
});
|
|
5335
5575
|
validation.failures.push({
|
|
5336
5576
|
path: `$.tool_calls.${tc.name}`,
|
|
5337
5577
|
operator: "policy_denied",
|
|
@@ -5340,10 +5580,22 @@ function replay(client, opts = {}) {
|
|
|
5340
5580
|
message: `Policy denied: ${tc.name} \u2014 ${verdict.reason}`,
|
|
5341
5581
|
contract_file: ""
|
|
5342
5582
|
});
|
|
5583
|
+
} else {
|
|
5584
|
+
trace.push({
|
|
5585
|
+
stage: "policy",
|
|
5586
|
+
tool: tc.name,
|
|
5587
|
+
verdict: "allow",
|
|
5588
|
+
reason: "policy_allowed",
|
|
5589
|
+
checked: { has_policy: true, default_deny: compiledSession.policyProgram.defaultDeny },
|
|
5590
|
+
found: { session_deny_matched: false, tool_deny_matched: false }
|
|
5591
|
+
});
|
|
5343
5592
|
}
|
|
5344
5593
|
}
|
|
5594
|
+
} else {
|
|
5595
|
+
trace.push({ stage: "policy", tool: null, verdict: "skip", reason: "no_policy_configured", checked: {}, found: {} });
|
|
5345
5596
|
}
|
|
5346
5597
|
timing.policy_ms += Date.now() - policyStart;
|
|
5598
|
+
currentTraceStage = "gate";
|
|
5347
5599
|
if (mode === "shadow") {
|
|
5348
5600
|
const shadowGateStart = Date.now();
|
|
5349
5601
|
const shadowDecision = validation.failures.length > 0 ? {
|
|
@@ -5352,6 +5604,15 @@ function replay(client, opts = {}) {
|
|
|
5352
5604
|
blocked: buildBlockedCalls(toolCalls, validation.failures, validation.unmatchedBlocked),
|
|
5353
5605
|
response_modification: gateMode
|
|
5354
5606
|
} : { action: "allow", tool_calls: toolCalls };
|
|
5607
|
+
const blockedTools = shadowDecision.action === "block" ? shadowDecision.blocked.map((b) => b.tool_name) : [];
|
|
5608
|
+
trace.push({
|
|
5609
|
+
stage: "gate",
|
|
5610
|
+
tool: null,
|
|
5611
|
+
verdict: blockedTools.length > 0 ? "info" : "allow",
|
|
5612
|
+
reason: blockedTools.length > 0 ? "violations_found" : "no_violations",
|
|
5613
|
+
checked: { gate_mode: gateMode },
|
|
5614
|
+
found: { blocked_count: blockedTools.length, action: shadowDecision.action, ...blockedTools.length > 0 ? { blocked_tools: blockedTools } : {} }
|
|
5615
|
+
});
|
|
5355
5616
|
const shadowDelta = {
|
|
5356
5617
|
would_have_blocked: shadowDecision.action === "block" ? shadowDecision.blocked : [],
|
|
5357
5618
|
would_have_narrowed: narrowResult?.removed ?? [],
|
|
@@ -5361,7 +5622,11 @@ function replay(client, opts = {}) {
|
|
|
5361
5622
|
lastShadowDeltaValue = shadowDelta;
|
|
5362
5623
|
shadowEvaluationCount++;
|
|
5363
5624
|
timing.gate_ms += Date.now() - shadowGateStart;
|
|
5364
|
-
|
|
5625
|
+
trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: sessionState.currentPhase, phase_after: sessionState.currentPhase, tools_committed: [], tools_blocked: blockedTools, killed: false, step_index: sessionState.totalStepCount } });
|
|
5626
|
+
trace.complete = true;
|
|
5627
|
+
lastTrace = trace;
|
|
5628
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5629
|
+
captureDecision(shadowDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, shadowDelta, timing, trace);
|
|
5365
5630
|
return response;
|
|
5366
5631
|
}
|
|
5367
5632
|
if (isCompatAdvisory) {
|
|
@@ -5402,7 +5667,21 @@ function replay(client, opts = {}) {
|
|
|
5402
5667
|
sessionState = recordDecisionOutcome(sessionState, "allowed");
|
|
5403
5668
|
}
|
|
5404
5669
|
timing.finalize_ms += Date.now() - advisoryFinalizeStart;
|
|
5405
|
-
|
|
5670
|
+
const advisoryBlockedTools = advisoryDecision.action === "block" ? advisoryDecision.blocked.map((b) => b.tool_name) : [];
|
|
5671
|
+
trace.push({
|
|
5672
|
+
stage: "gate",
|
|
5673
|
+
tool: null,
|
|
5674
|
+
verdict: advisoryBlockedTools.length > 0 ? "info" : "allow",
|
|
5675
|
+
reason: advisoryBlockedTools.length > 0 ? "violations_found" : "no_violations",
|
|
5676
|
+
checked: { gate_mode: gateMode },
|
|
5677
|
+
found: { blocked_count: advisoryBlockedTools.length, action: advisoryDecision.action, ...advisoryBlockedTools.length > 0 ? { blocked_tools: advisoryBlockedTools } : {} }
|
|
5678
|
+
});
|
|
5679
|
+
const advisoryNewPhase = phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase ? phaseResult.newPhase : sessionState.currentPhase;
|
|
5680
|
+
trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: sessionState.currentPhase, phase_after: advisoryNewPhase, tools_committed: toolCalls.map((tc) => tc.name), tools_blocked: advisoryBlockedTools, killed: false, step_index: sessionState.totalStepCount } });
|
|
5681
|
+
trace.complete = true;
|
|
5682
|
+
lastTrace = trace;
|
|
5683
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5684
|
+
captureDecision(advisoryDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing, trace);
|
|
5406
5685
|
return response;
|
|
5407
5686
|
}
|
|
5408
5687
|
const enforceGateStart = Date.now();
|
|
@@ -5440,7 +5719,20 @@ function replay(client, opts = {}) {
|
|
|
5440
5719
|
});
|
|
5441
5720
|
}
|
|
5442
5721
|
}
|
|
5443
|
-
|
|
5722
|
+
trace.push({
|
|
5723
|
+
stage: "gate",
|
|
5724
|
+
tool: null,
|
|
5725
|
+
verdict: "allow",
|
|
5726
|
+
reason: "no_violations",
|
|
5727
|
+
checked: { gate_mode: gateMode },
|
|
5728
|
+
found: { blocked_count: 0, action: "allow" }
|
|
5729
|
+
});
|
|
5730
|
+
const allowNewPhase = phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase ? phaseResult.newPhase : sessionState.currentPhase;
|
|
5731
|
+
trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: completedStep.phase, phase_after: allowNewPhase, tools_committed: toolCalls.map((tc) => tc.name), tools_blocked: [], killed: false, step_index: sessionState.totalStepCount } });
|
|
5732
|
+
trace.complete = true;
|
|
5733
|
+
lastTrace = trace;
|
|
5734
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5735
|
+
captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing, trace);
|
|
5444
5736
|
return response;
|
|
5445
5737
|
}
|
|
5446
5738
|
sessionState = recordDecisionOutcome(sessionState, "blocked");
|
|
@@ -5505,15 +5797,42 @@ function replay(client, opts = {}) {
|
|
|
5505
5797
|
);
|
|
5506
5798
|
continue;
|
|
5507
5799
|
}
|
|
5508
|
-
|
|
5800
|
+
const blockBlockedTools = decision.action === "block" ? decision.blocked.map((b) => b.tool_name) : [];
|
|
5801
|
+
trace.push({
|
|
5802
|
+
stage: "gate",
|
|
5803
|
+
tool: null,
|
|
5804
|
+
verdict: "block",
|
|
5805
|
+
reason: "violations_found",
|
|
5806
|
+
checked: { gate_mode: gateMode },
|
|
5807
|
+
found: { blocked_count: blockBlockedTools.length, action: "block", blocked_tools: blockBlockedTools }
|
|
5808
|
+
});
|
|
5809
|
+
trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: sessionState.currentPhase, phase_after: sessionState.currentPhase, tools_committed: [], tools_blocked: blockBlockedTools, killed, step_index: sessionState.totalStepCount } });
|
|
5810
|
+
trace.complete = true;
|
|
5811
|
+
lastTrace = trace;
|
|
5812
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5813
|
+
captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing, trace);
|
|
5509
5814
|
return applyGateDecision(decision, response, provider, gateMode, opts.onBlock);
|
|
5510
5815
|
}
|
|
5511
5816
|
if (lastError) throw lastError;
|
|
5512
5817
|
throw new ReplayInternalError("Retry loop exhausted without result", { sessionId });
|
|
5513
5818
|
} catch (err) {
|
|
5514
5819
|
if (err instanceof ReplayContractError || err instanceof ReplayKillError) {
|
|
5820
|
+
if (!trace.complete) {
|
|
5821
|
+
lastTrace = trace;
|
|
5822
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5823
|
+
}
|
|
5515
5824
|
throw err;
|
|
5516
5825
|
}
|
|
5826
|
+
trace.push({
|
|
5827
|
+
stage: currentTraceStage,
|
|
5828
|
+
tool: null,
|
|
5829
|
+
verdict: "error",
|
|
5830
|
+
reason: "stage_threw",
|
|
5831
|
+
checked: {},
|
|
5832
|
+
found: { error: err instanceof Error ? err.message : String(err) }
|
|
5833
|
+
});
|
|
5834
|
+
lastTrace = trace;
|
|
5835
|
+
emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
|
|
5517
5836
|
sessionState = recordDecisionOutcome(sessionState, "error");
|
|
5518
5837
|
if (resolvedSessionLimits?.circuit_breaker) {
|
|
5519
5838
|
const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
|
|
@@ -5639,6 +5958,9 @@ function replay(client, opts = {}) {
|
|
|
5639
5958
|
getLastShadowDelta() {
|
|
5640
5959
|
return lastShadowDeltaValue;
|
|
5641
5960
|
},
|
|
5961
|
+
getLastTrace() {
|
|
5962
|
+
return lastTrace;
|
|
5963
|
+
},
|
|
5642
5964
|
/**
|
|
5643
5965
|
* v3: Manually restrict available tools within compiled legal space.
|
|
5644
5966
|
* @see specs/replay-v3.md § narrow() / widen()
|
|
@@ -5774,7 +6096,7 @@ function replay(client, opts = {}) {
|
|
|
5774
6096
|
}
|
|
5775
6097
|
return wrapped;
|
|
5776
6098
|
}
|
|
5777
|
-
function captureDecision(decision, response, request, guardStart, requestToolNames, crossStep, narrowing = null, phaseResult = null, policyVerdictMap = null, constraintVerdictVal = null, shadowDelta = void 0, timingParam) {
|
|
6099
|
+
function captureDecision(decision, response, request, guardStart, requestToolNames, crossStep, narrowing = null, phaseResult = null, policyVerdictMap = null, constraintVerdictVal = null, shadowDelta = void 0, timingParam, traceParam) {
|
|
5778
6100
|
if (!buffer && !store) return;
|
|
5779
6101
|
if (timingParam) {
|
|
5780
6102
|
timingParam.total_ms = Date.now() - guardStart;
|
|
@@ -5809,6 +6131,7 @@ function replay(client, opts = {}) {
|
|
|
5809
6131
|
phase: sessionState.currentPhase,
|
|
5810
6132
|
phase_transition: phaseTransitionStr,
|
|
5811
6133
|
shadow_delta: shadowDelta,
|
|
6134
|
+
trace: traceParam ? redactTrace(traceParam, opts.captureLevel ?? "full") : void 0,
|
|
5812
6135
|
receipt: null
|
|
5813
6136
|
};
|
|
5814
6137
|
const capturedCall = {
|
|
@@ -6366,6 +6689,7 @@ function resolveSessionLimits(contracts) {
|
|
|
6366
6689
|
const sl = c.session_limits;
|
|
6367
6690
|
if (sl.max_steps !== void 0 && merged.max_steps === void 0) merged.max_steps = sl.max_steps;
|
|
6368
6691
|
if (sl.max_tool_calls !== void 0 && merged.max_tool_calls === void 0) merged.max_tool_calls = sl.max_tool_calls;
|
|
6692
|
+
if (sl.max_tool_calls_mode !== void 0 && merged.max_tool_calls_mode === void 0) merged.max_tool_calls_mode = sl.max_tool_calls_mode;
|
|
6369
6693
|
if (sl.max_cost_per_session !== void 0 && merged.max_cost_per_session === void 0) merged.max_cost_per_session = sl.max_cost_per_session;
|
|
6370
6694
|
if (sl.loop_detection && !merged.loop_detection) merged.loop_detection = sl.loop_detection;
|
|
6371
6695
|
if (sl.circuit_breaker && !merged.circuit_breaker) merged.circuit_breaker = sl.circuit_breaker;
|
|
@@ -6473,6 +6797,7 @@ function createInactiveSession(client, sessionId, reason) {
|
|
|
6473
6797
|
getState: () => EMPTY_STATE_SNAPSHOT,
|
|
6474
6798
|
getLastNarrowing: () => null,
|
|
6475
6799
|
getLastShadowDelta: () => null,
|
|
6800
|
+
getLastTrace: () => null,
|
|
6476
6801
|
narrow() {
|
|
6477
6802
|
},
|
|
6478
6803
|
widen() {
|
|
@@ -6514,6 +6839,7 @@ function createBlockingInactiveSession(client, sessionId, detail, configError) {
|
|
|
6514
6839
|
getState: () => EMPTY_STATE_SNAPSHOT,
|
|
6515
6840
|
getLastNarrowing: () => null,
|
|
6516
6841
|
getLastShadowDelta: () => null,
|
|
6842
|
+
getLastTrace: () => null,
|
|
6517
6843
|
narrow() {
|
|
6518
6844
|
},
|
|
6519
6845
|
widen() {
|
|
@@ -6598,6 +6924,83 @@ function generateSessionId2() {
|
|
|
6598
6924
|
function stripHashPrefix(hash) {
|
|
6599
6925
|
return hash.startsWith("sha256:") ? hash.slice(7) : hash;
|
|
6600
6926
|
}
|
|
6927
|
+
function resolveLogLevel() {
|
|
6928
|
+
const raw = typeof process !== "undefined" ? process.env.REPLAYCI_LOG : void 0;
|
|
6929
|
+
if (!raw) return "warn";
|
|
6930
|
+
const lower = raw.toLowerCase();
|
|
6931
|
+
if (lower === "trace" || lower === "debug") return "trace";
|
|
6932
|
+
if (lower === "silent" || lower === "off" || lower === "none") return "silent";
|
|
6933
|
+
return "warn";
|
|
6934
|
+
}
|
|
6935
|
+
function defaultReplayDiagnosticsHandler(event) {
|
|
6936
|
+
const level = resolveLogLevel();
|
|
6937
|
+
if (level === "silent") return;
|
|
6938
|
+
switch (event.type) {
|
|
6939
|
+
case "replay_inactive":
|
|
6940
|
+
console.warn(`[replayci] replay() inactive: ${event.reason}${event.error_message ? ` \u2014 ${event.error_message}` : ""}`);
|
|
6941
|
+
break;
|
|
6942
|
+
case "replay_compile_error":
|
|
6943
|
+
console.warn(`[replayci] compile error: ${event.details}`);
|
|
6944
|
+
break;
|
|
6945
|
+
case "replay_compile_warning":
|
|
6946
|
+
console.warn(`[replayci] compile warning: ${event.details}`);
|
|
6947
|
+
break;
|
|
6948
|
+
case "replay_bypass_detected":
|
|
6949
|
+
console.warn(`[replayci] bypass detected on session ${event.session_id}`);
|
|
6950
|
+
break;
|
|
6951
|
+
case "replay_kill":
|
|
6952
|
+
console.warn(`[replayci] session ${event.session_id} killed`);
|
|
6953
|
+
break;
|
|
6954
|
+
case "replay_block":
|
|
6955
|
+
console.warn(`[replayci] blocked ${event.tool_name}: ${event.reason}`);
|
|
6956
|
+
break;
|
|
6957
|
+
case "replay_narrow": {
|
|
6958
|
+
for (const r of event.removed) {
|
|
6959
|
+
console.warn(`[replayci] removed ${r.tool} \u2192 ${r.reason}${r.detail ? ` (${r.detail})` : ""}`);
|
|
6960
|
+
}
|
|
6961
|
+
break;
|
|
6962
|
+
}
|
|
6963
|
+
case "replay_trace": {
|
|
6964
|
+
const t = event.trace;
|
|
6965
|
+
if (level === "trace") {
|
|
6966
|
+
for (const entry of t.entries) {
|
|
6967
|
+
const toolStr = entry.tool ? ` ${entry.tool}` : "";
|
|
6968
|
+
const detail = entry.reason !== entry.verdict ? ` \u2014 ${entry.reason}` : "";
|
|
6969
|
+
const checkedStr = Object.keys(entry.checked).length > 0 ? ` checked=${JSON.stringify(entry.checked)}` : "";
|
|
6970
|
+
const foundStr = Object.keys(entry.found).length > 0 ? ` found=${JSON.stringify(entry.found)}` : "";
|
|
6971
|
+
console.warn(`[replayci] ${entry.stage}${toolStr}: ${entry.verdict}${detail}${checkedStr}${foundStr}`);
|
|
6972
|
+
}
|
|
6973
|
+
if (!t.complete) {
|
|
6974
|
+
console.warn(`[replayci] trace INCOMPLETE (fault in pipeline)`);
|
|
6975
|
+
}
|
|
6976
|
+
} else {
|
|
6977
|
+
const blocks = t.entries.filter((e) => e.verdict === "block");
|
|
6978
|
+
for (const b of blocks) {
|
|
6979
|
+
const toolStr = b.tool ?? "session";
|
|
6980
|
+
console.warn(`[replayci] blocked ${toolStr} at ${b.stage} \u2192 ${b.reason}`);
|
|
6981
|
+
}
|
|
6982
|
+
if (!t.complete) {
|
|
6983
|
+
console.warn(`[replayci] enforcement cycle incomplete (fault) \u2014 session.getLastTrace() for partial trace`);
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
break;
|
|
6987
|
+
}
|
|
6988
|
+
case "replay_workflow_error":
|
|
6989
|
+
console.warn(`[replayci] workflow error: ${event.details}`);
|
|
6990
|
+
break;
|
|
6991
|
+
case "replay_state_sync_error":
|
|
6992
|
+
console.warn(`[replayci] state sync error: ${event.details}`);
|
|
6993
|
+
break;
|
|
6994
|
+
case "replay_receipt_error":
|
|
6995
|
+
console.warn(`[replayci] receipt error (${event.tool_name}): ${event.details}`);
|
|
6996
|
+
break;
|
|
6997
|
+
case "replay_capture_error":
|
|
6998
|
+
console.warn(`[replayci] capture error: ${event.details}`);
|
|
6999
|
+
break;
|
|
7000
|
+
default:
|
|
7001
|
+
break;
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
6601
7004
|
function emitDiagnostic2(diagnostics, event) {
|
|
6602
7005
|
try {
|
|
6603
7006
|
diagnostics?.(event);
|