API Reference
Complete reference for the replay() SDK.
replay(client, options)
Creates a governed session wrapping your LLM client.
import { replay } from "@vesanor/replay";
const session = replay(client, options);
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
client | OpenAI | Anthropic | Yes | Your LLM provider client |
options | ReplayOptions | Yes | Configuration object (see below) |
Returns
ReplaySession<T> — a governed session object.
ReplayOptions
type ReplayOptions = {
// Contract source (one required)
contracts?: Contract | Contract[]; // Pre-loaded contract(s)
contractsDir?: string; // Path to contracts directory (auto-loads)
sessionYamlPath?: string; // Explicit session.yaml path (auto-detected if omitted)
// Session identity
agent?: string; // Agent name
sessionId?: string; // External session ID (auto-generated if omitted)
// Behavior
mode?: "enforce" | "shadow" | "log-only"; // Default: "enforce"
gate?: "reject_all" | "strip_partial" | "strip_blocked"; // Default: "reject_all"
onError?: "block" | "allow"; // Default: "block"
unmatchedPolicy?: "block" | "allow"; // Tools with no contract. Default: "block"
compatEnforcement?: "protective" | "advisory"; // Default: "protective"
maxRetries?: number; // Retry on contract failure (max 5). Default: 0
maxUnguardedCalls?: number; // Auto-kill after N unguarded calls (default: 3). Only with onError: "allow"
narrowingFeedback?: "silent" | "metadata" | "inject"; // How narrowing is communicated to the LLM. Default: "silent"
providerConstraints?: ProviderConstraints; // Block/warn on incompatible providers
// Authorization (optional)
principal?: unknown; // Caller identity for policy evaluation
// Tool execution (optional, required for Govern mode)
tools?: Record<string, ToolExecutor>; // Wrapped tool executors
// Server connection (optional, enables Govern mode)
apiKey?: string; // Vesanor API key
runtimeUrl?: string; // Server URL (default: https://app.vesanor.com)
// Capture
captureLevel?: CapturePrivacyTier; // Default: "redacted"
store?: Store; // Custom session state + capture store
// Callbacks
onBlock?: (decision: ReplayDecision) => void;
onNarrow?: (narrowing: NarrowResult) => void;
diagnostics?: (event: DiagnosticEvent) => void;
// Labels (optional)
labels?: string[]; // Session labels (taint semantics, append-only)
// Checkpoints (optional, required if contracts have checkpoints)
onCheckpoint?: (request: ApprovalRequest) => Promise<ApprovalResponse>;
// Workflow (optional)
workflow?: WorkflowOptions;
};
maxUnguardedCalls
Maximum calls allowed without server backing before auto-kill (default: 3). Only applies when onError: "allow". Bounds the state-fiction window — if the server is down and you've opted to continue locally, this limits how far state can diverge before the session is killed.
narrowingFeedback
Controls how narrowing decisions are communicated to the LLM:
| Value | Behavior |
|---|---|
"silent" (default) | Tools removed silently, LLM doesn't know why |
"metadata" | Narrowing info exposed via getState()/getLastNarrowing() only |
"inject" | A system message is prepended explaining which tools were removed and why. Policy-denied reasons are redacted to "restricted". |
providerConstraints
Provider-specific compatibility checks declared in session.yaml. When block_incompatible patterns match the current provider/model combination, replay() throws ReplayConfigError with condition provider_incompatible. warn_incompatible patterns emit replay_provider_warning diagnostics without blocking.
compatEnforcement
Controls behavior when the session tier is "compat" (no authoritative server connection):
| Value | Behavior |
|---|---|
"protective" (default) | Enforces contracts locally and blocks illegal calls |
"advisory" | Evaluates contracts and emits diagnostics, but does not block |
Mode behavior
| Mode | Enforcement | Blocks calls | Captures |
|---|---|---|---|
enforce | Full pipeline | Yes | Yes |
shadow | Full pipeline | No (records what would happen) | Yes |
log-only | None | No | Yes |
Gate behavior
| Gate | On block |
|---|---|
reject_all (default) | Throw ReplayContractError if any tool call blocked |
strip_partial | Remove blocked calls; throw if ALL blocked |
strip_blocked | Remove blocked calls; synthesize text-only if ALL blocked |
ReplaySession<T>
type ReplaySession<T> = {
client: T; // Wrapper client (NOT the original)
flush: () => Promise<FlushResult>; // Flush captures to server
restore: () => void; // Release wrapper, end session
kill: () => void; // Emergency stop
addLabel: (label: string) => void; // Add immutable session label (cannot be removed)
getState: () => SessionStateSnapshot; // Current session state (redacted)
getHealth: () => ReplayHealthSnapshot; // Session health
getLastNarrowing: () => NarrowingSnapshot | null; // Last narrowing result
getLastShadowDelta: () => ShadowDelta | null; // Last shadow delta (shadow mode only)
// Tool executors (present when `tools` provided)
tools: Record<string, WrappedToolExecutor>;
// Manual narrowing
narrow: (toolFilter: string[]) => void; // Restrict tools
widen: () => void; // Remove manual restriction
// Workflow
getWorkflowState: () => Promise<WorkflowStateSnapshot | null>;
handoff: (offer: HandoffOfferInput) => Promise<HandoffOfferResult | null>;
};
client
A wrapper around your original provider client. Routes create() calls through the enforcement pipeline. You must use session.client instead of the original client — direct calls to the original trigger bypass detection.
kill()
Immediately stops all future calls. Throws ReplayKillError on any subsequent create() call. Cannot be reversed. In Govern mode, propagates to the server.
restore()
Deactivates the wrapper and releases the original client. Call this when you're done with the session. After restore(), the wrapper is inert — calling session.client.create() throws.
getState()
Returns a redacted, immutable snapshot of the current session state:
type SessionStateSnapshot = {
sessionId: string;
agent: string | null;
principal: null; // Always null (redacted)
startedAt: Date;
stateVersion: number;
controlRevision: number;
currentPhase: string | null;
totalStepCount: number;
totalToolCalls: number;
totalCost: number;
actualCost: number;
toolCallCounts: Record<string, number>;
forbiddenTools: string[];
satisfiedPreconditions: Record<string, unknown>;
lastStep: CompletedStepSnapshot | null;
lastNarrowing: NarrowingSnapshot | null;
killed: boolean;
totalUnguardedCalls: number;
consecutiveBlockCount: number;
totalBlockCount: number;
};
totalCost vs actualCost: totalCost counts only committed steps. actualCost counts all LLM calls including blocked and retried ones. Session limits check actualCost.
getHealth()
type ReplayHealthSnapshot = {
status: "healthy" | "degraded" | "inactive";
authorityState: "active" | "advisory" | "compromised" | "recovering" | "killed" | "inactive";
protectionLevel: "monitor" | "protect" | "govern";
durability: "server" | "degraded-local" | "inactive";
tier: "strong" | "compat";
compatEnforcement: "protective" | "advisory";
cluster_detected: boolean;
bypass_detected: boolean;
totalSteps: number;
totalBlocks: number;
totalErrors: number;
killed: boolean;
shadowEvaluations: number;
};
Error types
ReplayContractError
Thrown when enforcement blocks a tool call (with gate: "reject_all" or full strip with gate: "strip_partial").
import { ReplayContractError } from "@vesanor/replay";
try {
await session.client.chat.completions.create({ ... });
} catch (e) {
if (e instanceof ReplayContractError) {
console.log(e.decision); // Full ReplayDecision (includes blocked calls)
console.log(e.contractFile); // Contract file that triggered the error
console.log(e.failures); // ContractFailure[]
}
}
ReplayKillError
Thrown on any create() call after session.kill().
import { ReplayKillError } from "@vesanor/replay";
try {
await session.client.chat.completions.create({ ... });
} catch (e) {
if (e instanceof ReplayKillError) {
console.log("Session was killed");
}
}
ReplayConfigError
Thrown at replay() init time when configuration is invalid. The condition field identifies the cause:
condition | Cause |
|---|---|
policy_without_principal | A contract or session.yaml has a policy block but no principal was supplied |
constraints_without_wrapper | execution_constraints declared but tool not in tools map |
compilation_failed | Invalid contract YAML (circular transitions, unreachable phases, observe() already active, another session already attached) |
provider_incompatible | Provider constraint blocks the request (e.g., block_incompatible match) |
NarrowResult
Passed to the onNarrow callback:
type NarrowResult = {
allowed: ToolDefinition[];
removed: NarrowedTool[];
};
type NarrowedTool = {
tool: string;
reason: "wrong_phase" | "precondition_not_met" | "forbidden_in_state"
| "no_contract" | "policy_denied" | "manual_filter" | "label_gate";
detail?: string;
contract_file?: string;
};
Block reasons
When a tool call is blocked, the reason indicates why:
| Reason | Source |
|---|---|
output_invariant_failed | Output assertion failed |
input_invariant_failed | Input assertion failed |
response_format_invalid | Wrong finish_reason or unexpected content |
argument_value_mismatch | Argument value invariant failed |
precondition_not_met | Required prior tool not called |
forbidden_tool | Tool in forbids_after set |
session_limit_exceeded | Budget or rate limit hit |
loop_detected | Same tool+args repeated |
illegal_phase_transition | Phase transition not in transitions map |
ambiguous_phase_transition | Multiple tools attempt different phases |
unmatched_tool_blocked | No contract and unmatchedPolicy: "block" |
policy_denied | Principal authorization denied |
execution_constraint_violated | Pre-execution argument check failed |
risk_gate_blocked | Tool blocked by risk_defaults + side_effect classification |
killed | session.kill() was called |
binding_not_found | ref operator: referenced binding slot hasn't been set yet |
ref_mismatch | ref operator: bound value doesn't match current value |
aggregate_limit_exceeded | Session aggregate bound breached |
aggregate_path_missing | Aggregated field missing from tool arguments (fail-closed) |
envelope_not_established | Envelope reference value not yet set (sequencing error) |
envelope_violation | Value violates envelope constraint (directional bound) |
checkpoint_denied | Human checkpoint: approver denied the call |
checkpoint_timeout | Human checkpoint: no response within timeout |
checkpoint_budget_exceeded | More than 10 checkpoints triggered in session |
Note: Bypass detection (
replay_bypass_detected) is a diagnostic event, not a block reason. It marks the session ascompromisedbut cannot prevent the bypassing call itself.
Runtime API endpoints
These endpoints are used by the SDK in Govern mode. They are documented here for reference — most applications interact through the SDK, not directly.
POST /api/v1/replay/sessions/:sessionId/labels
Adds an immutable label to a session. Labels follow taint semantics — they can be added but never removed.
Request:
{ "label": "mnpi" }
Response:
{ "labels": ["mnpi", "restricted-client"] }
Validation:
- Label must be non-empty, max 128 chars, alphanumeric + hyphens + underscores
- Duplicate labels are idempotent (no error)
- No DELETE endpoint (immutability enforced at API level)
POST /api/v1/replay/sessions/:sessionId/proposals/:proposalId/approve
Resolves a pending checkpoint approval in Govern mode. Used by external approval systems (Slack bots, dashboards, webhooks).
Request:
{
"decision": "approve",
"decided_by": "[email protected]",
"reason": "Verified with trading desk"
}
Response:
{ "status": "approved" }
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
decision | "approve" | "deny" | yes | Approval decision |
decided_by | string | yes | Identifier of the approver |
reason | string | no | Reason for the decision |
The proposal must be in pending_approval status. Once resolved, the SDK receives the decision and proceeds or blocks accordingly.
Checkpoint types
ApprovalRequest
Emitted when a checkpoint triggers. Passed to the onCheckpoint callback.
type ApprovalRequest = {
checkpoint_id: string;
session_id: string;
tool_name: string;
arguments: Record<string, unknown>;
context: Array<{ label: string; value: unknown }>;
reason: string;
timeout_seconds: number;
requested_at: string;
};
ApprovalResponse
Returned by the onCheckpoint callback or the server-side approval API.
type ApprovalResponse = {
checkpoint_id: string;
decision: "approve" | "deny";
decided_by: string;
decided_at: string;
reason?: string;
};
Proposal status: pending_approval
When a checkpoint triggers in Govern mode, the server sets the proposal status to pending_approval. The proposal remains in this state until an external system calls the approval endpoint or the timeout expires.
Next steps
- Contract YAML Reference — every contract field
- Troubleshooting — common issues
- Quickstart — get started