API Reference
Complete reference for the replay() SDK.
replay(client, options)
Creates a governed session wrapping your LLM client.
import { replay } from "vesanor";
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)
environment?: "development" | "staging" | "production";
// 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 |
environment
Used by zero-config governance to select runtime behavior:
| Value | Behavior |
|---|---|
"development" | Monitor/shadow behavior after approval |
"staging" | Enforce with advisory compatibility behavior |
"production" | Enforce protectively and fail closed when governance is unavailable |
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)
getLastTrace: () => DecisionTrace | null; // Last full enforcement trace
// 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;
governanceAttachment?: "pending" | "no_plan" | "attached" | "artifact_invalid" | "attachment_failed" | "fetch_failed";
unresolvedReceiptCount?: 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";
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";
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. Most applications interact through the SDK, not directly.
POST /api/v1/replay/sessionsGET /api/v1/replay/sessions/:sessionIdPOST /api/v1/replay/sessions/:sessionId/preflightPOST /api/v1/replay/sessions/:sessionId/proposalsPOST /api/v1/replay/sessions/:sessionId/receiptsPOST /api/v1/replay/sessions/:sessionId/labelsPOST /api/v1/replay/sessions/:sessionId/tool-filterPOST /api/v1/replay/sessions/:sessionId/killPOST /api/v1/replay/sessions/:sessionId/recoverGET /api/v1/replay/workflows/:workflowIdandPOST /api/v1/replay/sessions/:sessionId/handoffswhen workflow support is enabled
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)
Checkpoints
Human checkpoints currently use the SDK onCheckpoint callback and the ApprovalRequest / ApprovalResponse types below. The public runtime API in this repo does not expose a .../proposals/:proposalId/approve route.
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.
type ApprovalResponse = {
checkpoint_id: string;
decision: "approve" | "deny";
decided_by: string;
decided_at: string;
reason?: string;
};
Next steps
- Contract field-by-field YAML reference was moved to
docs/legacy/advanced-local/governance/contract-reference.md. - Troubleshooting — common issues
- Quickstart — get started