Error Handling
Production patterns for handling enforcement errors, kills, and degraded state.
The three error types
| Error | When | Recoverable? |
|---|---|---|
ReplayContractError | A tool call was blocked by enforcement | Yes — catch and retry, or handle gracefully |
ReplayKillError | Session was killed (manual or circuit breaker) | No — session is permanently dead |
ReplayConfigError | Invalid configuration at replay() init time | Fix config and restart |
Handling blocked calls
Basic: catch and inform the user
import { ReplayContractError } from "@vesanor/replay";
try {
const response = await session.client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});
// Process response normally
} catch (e) {
if (e instanceof ReplayContractError) {
// Tell the user what happened
console.log("Blocked tool calls:");
for (const f of e.failures) {
console.log(` ${f.path}: ${f.operator} check failed`);
}
// Continue the conversation without the blocked tool call
} else {
throw e; // Re-throw non-replay errors
}
}
With strip_partial: allow valid calls through
If you'd rather keep valid tool calls and only block the bad ones:
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
gate: "strip_partial", // Remove blocked, keep valid
});
// Only throws if ALL tool calls are blocked
const response = await session.client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});
// response.choices[0].message.tool_calls may have fewer calls than proposed
With strip_blocked: never throw on blocks
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
gate: "strip_blocked", // Remove blocked, synthesize text if all blocked
});
// Never throws ReplayContractError
const response = await session.client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});
// If all tool calls were blocked, response is a text-only message
Handling kills
A killed session cannot be resumed. You need a new session.
import { ReplayKillError } from "@vesanor/replay";
try {
const response = await session.client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});
} catch (e) {
if (e instanceof ReplayKillError) {
// Session is dead — clean up
session.restore();
// Option 1: Start a new session
const newSession = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
});
// Option 2: Start a recovery session in log-only mode
const recovery = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "log-only", // No enforcement — just capture what recovery does
});
}
}
Detecting auto-kill before it happens
The circuit breaker kills the session after too many consecutive failures. Monitor the counters:
const state = session.getState();
if (state.consecutiveBlockCount >= 3) {
// Getting close to circuit breaker threshold (default: 5)
// Consider changing strategy or killing gracefully
session.kill();
}
Handling configuration errors
ReplayConfigError is thrown synchronously when replay() is called — not during LLM calls.
import { ReplayConfigError } from "@vesanor/replay";
try {
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
});
} catch (e) {
if (e instanceof ReplayConfigError) {
switch (e.condition) {
case "policy_without_principal":
// A contract has policy rules but no principal was provided
break;
case "constraints_without_wrapper":
// execution_constraints exist but tool not in tools map
break;
case "compilation_failed":
// Invalid YAML, circular transitions, unreachable phases, etc.
break;
case "provider_incompatible":
// Provider constraint blocks the request
break;
}
}
}
Using the onBlock callback
For logging or metrics without try/catch. The callback receives a ReplayDecision object — not a simple { tool, reason } pair.
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
gate: "strip_blocked", // Don't throw — handle via callback
onBlock: (decision) => {
// decision is a ReplayDecision, NOT { tool, reason }
// Access blocked calls via decision.blocked[]
for (const blocked of decision.blocked) {
metrics.increment("replay.block", {
tool: blocked.tool_name, // NOT decision.tool
reason: blocked.reason, // NOT decision.reason
arguments: blocked.arguments, // JSON string of blocked arguments
});
}
// Other fields on decision:
// decision.action — "block"
// decision.tool_calls — all tool calls in the response
// decision.blocked — array of BlockedToolCall
// decision.response_modification — gate mode applied
},
});
onBlock callback shapeThe decision parameter is the full ReplayDecision object, the same type thrown inside ReplayContractError.decision. Individual blocked calls are in decision.blocked[], each with tool_name, reason, arguments, contract_file, and failures[]. Do not access decision.tool or decision.reason directly — those fields don't exist.
Graceful degradation (Govern mode)
When using server-backed enforcement and the server becomes unreachable:
Default: fail closed
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
apiKey: process.env.VESANOR_API_KEY,
// onError: "block" is the default — server failure blocks calls
});
Opt-in: fall back to local enforcement
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
apiKey: process.env.VESANOR_API_KEY,
onError: "allow", // Fall back to local enforcement on server failure
});
// Monitor degradation
const state = session.getState();
if (state.totalUnguardedCalls > 0) {
console.warn(`${state.totalUnguardedCalls} calls ran without server backing`);
}
const health = session.getHealth();
if (health.durability === "degraded-local") {
console.warn("Server unreachable — running locally");
}
Diagnostics for debugging
The diagnostics callback fires for every significant event. Each event has a type field:
| Type | When | Severity |
|---|---|---|
replay_activated | Session initialized successfully | Info |
replay_compile_error | Contract or session.yaml compilation failed | Error — session is blocked |
replay_compile_warning | Compilation succeeded with warnings (e.g., circular transitions) | Warning |
replay_validation_warning | Per-turn runtime warning (e.g., message validation) | Warning |
replay_block | A tool call was blocked | Info |
replay_narrow | Tools were narrowed before LLM call | Info |
replay_bypass_detected | Original client used directly (not session.client) | Error |
replay_kill | Session was killed | Error |
replay_inactive | Session could not activate (unsupported client, degraded runtime) | Warning |
replay_compat_advisory | Advisory-mode would-block decision | Info |
const session = replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
diagnostics: (event) => {
switch (event.type) {
case "replay_compile_error":
// Session.yaml or contract failed to compile — session is BLOCKED
console.error("Compile error:", event.details);
break;
case "replay_compile_warning":
// Compilation succeeded but with warnings
console.warn("Compile warning:", event.details);
break;
case "replay_validation_warning":
// Per-turn validation warning (not blocking)
console.warn("Validation:", event.details);
break;
case "replay_block":
console.log(`Blocked: ${event.tool_name} — ${event.reason}`);
break;
case "replay_bypass_detected":
console.error("BYPASS: original client used directly!");
break;
case "replay_kill":
console.log("Session killed");
break;
}
},
});
replay_compile_error on startupThis means your session.yaml or contracts failed to compile. The session is blocked — all create() calls will throw ReplayConfigError. Check the details field for the specific compilation failure (missing initial phase, undeclared transition target, etc.).
Previous versions emitted replay_contract_error for all errors. This type still exists for backward compatibility, but new code should use the specific types above.
Recommended production pattern
import { replay, ReplayContractError, ReplayKillError } from "@vesanor/replay";
function createGovernedSession(client: OpenAI) {
return replay(client, {
contractsDir: "./contracts",
agent: "my-agent",
mode: "enforce",
gate: "reject_all",
apiKey: process.env.VESANOR_API_KEY,
onError: "block",
onBlock: (d) => metrics.increment("replay.block"),
diagnostics: (e) => logger.debug("replay", e),
});
}
async function agentLoop(session: ReplaySession<OpenAI>) {
while (true) {
try {
const response = await session.client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});
// Process tool calls...
if (isDone(response)) break;
} catch (e) {
if (e instanceof ReplayKillError) {
logger.error("Session killed — stopping agent");
break;
}
if (e instanceof ReplayContractError) {
logger.warn("Blocked", { failures: e.failures });
// Add the block reason to messages so the model can adjust
messages.push({
role: "user",
content: "Your last tool call was blocked. Try a different approach.",
});
continue;
}
throw e;
}
}
session.restore();
}
Next steps
- Kill Switch — manual and automatic kill behavior
- Session Limits — configure circuit breaker thresholds
- API Reference — error type details
- Troubleshooting — common issues