Skip to main content

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

ParameterTypeRequiredDescription
clientOpenAI | AnthropicYesYour LLM provider client
optionsReplayOptionsYesConfiguration 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:

ValueBehavior
"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):

ValueBehavior
"protective" (default)Enforces contracts locally and blocks illegal calls
"advisory"Evaluates contracts and emits diagnostics, but does not block

Mode behavior

ModeEnforcementBlocks callsCaptures
enforceFull pipelineYesYes
shadowFull pipelineNo (records what would happen)Yes
log-onlyNoneNoYes

Gate behavior

GateOn block
reject_all (default)Throw ReplayContractError if any tool call blocked
strip_partialRemove blocked calls; throw if ALL blocked
strip_blockedRemove 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:

conditionCause
policy_without_principalA contract or session.yaml has a policy block but no principal was supplied
constraints_without_wrapperexecution_constraints declared but tool not in tools map
compilation_failedInvalid contract YAML (circular transitions, unreachable phases, observe() already active, another session already attached)
provider_incompatibleProvider 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:

ReasonSource
output_invariant_failedOutput assertion failed
input_invariant_failedInput assertion failed
response_format_invalidWrong finish_reason or unexpected content
argument_value_mismatchArgument value invariant failed
precondition_not_metRequired prior tool not called
forbidden_toolTool in forbids_after set
session_limit_exceededBudget or rate limit hit
loop_detectedSame tool+args repeated
illegal_phase_transitionPhase transition not in transitions map
ambiguous_phase_transitionMultiple tools attempt different phases
unmatched_tool_blockedNo contract and unmatchedPolicy: "block"
policy_deniedPrincipal authorization denied
execution_constraint_violatedPre-execution argument check failed
risk_gate_blockedTool blocked by risk_defaults + side_effect classification
killedsession.kill() was called
binding_not_foundref operator: referenced binding slot hasn't been set yet
ref_mismatchref operator: bound value doesn't match current value
aggregate_limit_exceededSession aggregate bound breached
aggregate_path_missingAggregated field missing from tool arguments (fail-closed)
envelope_not_establishedEnvelope reference value not yet set (sequencing error)
envelope_violationValue violates envelope constraint (directional bound)
checkpoint_deniedHuman checkpoint: approver denied the call
checkpoint_timeoutHuman checkpoint: no response within timeout
checkpoint_budget_exceededMore than 10 checkpoints triggered in session

Note: Bypass detection (replay_bypass_detected) is a diagnostic event, not a block reason. It marks the session as compromised but 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:

FieldTypeRequiredDescription
decision"approve" | "deny"yesApproval decision
decided_bystringyesIdentifier of the approver
reasonstringnoReason 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