Contract Cookbook
Every replay contract field explained with examples. Grouped by the problem each field solves.
Basic structure
Every tool contract is a YAML file with the tool name matching the function name in your tool definitions.
tool: my_tool_name
side_effect: read # read | write | destructive
evidence_class: local_transaction
commit_requirement: acknowledged
timeouts: { total_ms: 30000 }
retries: { max_attempts: 1, retry_on: [] }
rate_limits: { on_429: { respect_retry_after: true, max_sleep_seconds: 60 } }
assertions:
input_invariants: []
output_invariants: []
golden_cases: []
allowed_errors: []
The fields below are additive — add only what you need.
Classify risk: side_effect
Tells the enforcement pipeline how risky this tool is.
side_effect: read # Safe — just reading data
side_effect: write # Modifies state — creates/updates records
side_effect: destructive # Irreversible — deletes data, sends money
side_effect: admin # Administrative/governance actions
side_effect: financial # Financial transactions (payments, transfers)
Combined with risk_defaults in session.yaml, this controls whether unmatched tools are allowed:
# session.yaml
risk_defaults:
read: allow
write: block # write tools blocked unless their contract has gate: allow
destructive: block
admin: block
financial: block
Override risk defaults: gate
Override the risk_defaults behavior for a specific tool.
tool: archive_records
side_effect: write
gate: allow # Allow this write tool even though risk_defaults blocks writes
tool: read_audit_log
side_effect: read
gate: block # Block this read tool even though risk_defaults allows reads
Control phase visibility: transitions
Restrict when a tool is available and which phase it advances to.
transitions:
valid_in_phases: [eligibility_checked] # Only visible in this phase
advances_to: refund_issued # After execution, move to this phase
Tools without transitions are available in all phases (no phase restriction).
See Phases & Transitions for full phase machine design.
Enforce ordering: preconditions
Require that specific tools were called before this one.
Basic ordering
preconditions:
- requires_prior_tool: lookup_customer
issue_refund is blocked unless lookup_customer was called earlier in the session.
Require specific output
preconditions:
- requires_prior_tool: check_eligibility
with_output:
- path: "$.eligible"
equals: true
Not only must check_eligibility have been called — its output must have eligible: true.
Same-entity enforcement (resource binding)
preconditions:
- requires_prior_tool: check_eligibility
resource:
bind_from: arguments
path: "$.order_id"
with_output:
- path: "$.eligible"
equals: true
The check_eligibility call must have been for the same order_id as this issue_refund call. Prevents "check order A, refund order B" bypasses.
Minimum step count
preconditions:
- requires_step_count:
gte: 2
description: "At least 2 prior steps before this tool"
See Preconditions & Ordering for detailed examples.
Prevent double execution: forbids_after
After this tool executes, block specific tools for the rest of the session.
tool: issue_refund
forbids_after:
- issue_refund # Can't refund twice
- void_order # Can't void after refund
Once issue_refund runs successfully, both issue_refund and void_order are removed from the tool list permanently. The model can't see them or call them.
Validate arguments at runtime: argument_value_invariants
Check the actual values in tool call arguments — not just types.
argument_value_invariants:
- path: "$.amount"
gte: 0
lte: 10000 # Max $10,000 refund
- path: "$.currency"
one_of: ["USD", "EUR", "GBP"]
- path: "$.customer_email"
regex: "^.+@.+"
- path: "$.query"
exact_match: 'status:"active"' # Catches provider quote-stripping
Available operators for argument_value_invariants:
| Operator | What it checks |
|---|---|
exact_match | String exact match |
type | Value type (string, number, boolean, object, array) |
regex | String matches regex pattern |
one_of | Value is in list |
gte | Greater than or equal |
lte | Less than or equal |
Note:
assertions(input/output invariants) support a broader set of operators — see Contract YAML Reference.
Validate response metadata: response_format_invariants
Check the LLM response envelope itself — not just the tool call arguments.
response_format_invariants:
finish_reason: "tool_calls" # Expect tool calls, not text
content_when_tool_calls: "empty" # No text content alongside tool calls
tool_calls_present: true # Actually has tool calls (catches phantom announcements)
When this matters:
- Models sometimes return
finish_reason: "stop"instead of"tool_calls"— 500/500 responses in research - Some models include text content alongside tool calls — underdocumented behavior
- Models sometimes say "I'll call the tool" but produce no
tool_calls— phantom announcements
Define evidence requirements: evidence_class and commit_requirement
Control how the enforcement pipeline treats execution evidence.
evidence_class: ack_only # Evidence pattern
commit_requirement: acknowledged # Minimum evidence for authoritative commit
evidence_class | Meaning |
|---|---|
local_transaction | Tool execution happens locally with observable results |
ack_only | Execution is acknowledged but not independently verified |
unverifiable | No way to verify execution happened |
commit_requirement | When state advances |
|---|---|
acknowledged | After execution receipt is recorded |
none | Tool is governed and recorded but doesn't advance state |
Restriction: ack_only is not allowed with high-risk side effects
Tools with side_effect of destructive, admin, or financial cannot use evidence_class: ack_only. The compiler rejects this combination with error code ACK_ONLY_ON_HIGH_RISK because high-risk operations require stronger evidence than simple acknowledgment.
Use local_transaction (observable results) or unverifiable (fire-and-forget) instead:
# High-risk tool — use local_transaction, not ack_only
tool: process_payment
side_effect: financial
evidence_class: local_transaction
commit_requirement: acknowledged
side_effect | local_transaction | ack_only | unverifiable |
|---|---|---|---|
read | OK | OK | OK |
write | OK | OK | OK |
destructive | OK | Blocked | OK |
admin | OK | Blocked | OK |
financial | OK | Blocked | OK |
Pre-execution argument validation: execution_constraints
Validate tool arguments before the real tool function runs. Requires wrapping your tool executor.
tool: delete_file
side_effect: destructive
execution_constraints:
arguments:
- path: "$.file_path"
regex: "^/tmp/scratch/" # Only allow files in scratch directory
- path: "$.recursive"
equals: false # Never allow recursive delete
Important: These are argument validation rules, not security capabilities. They check values before your function runs, but they don't reduce the function's actual permissions.
Requires passing tool executors to replay():
const session = replay(client, {
tools: {
delete_file: myDeleteFunction, // Required when execution_constraints declared
},
});
Authorization: policy
Control which callers can use this tool based on a principal identity.
tool: process_refund
side_effect: destructive
policy:
allow:
- principal:
path: "$.department"
equals: "finance"
deny:
- principal:
path: "$.type"
equals: "readonly-auditor"
Requires passing a principal to replay():
const session = replay(client, {
principal: {
department: "finance",
type: "agent",
tier: "L2",
},
});
Rules:
- Deny rules are checked first and are absolute
- Session-level policy (in
session.yaml) overrides per-tool policy - If any contract has a
policyblock and noprincipalis supplied,replay()fails with a config error
Provider constraints: provider_constraints (session.yaml)
Block known API incompatibilities or warn about soft issues.
# session.yaml
provider_constraints:
anthropic:
block_incompatible:
- "tool_choice.type=any + thinking" # Hard API error
openai:
warn_incompatible:
- "parallel_tool_calls=false + gpt-4o-mini" # Silently ignored
warn_floating_alias: true # Recommend pinned model version
block_incompatible entries throw before the LLM call. warn_incompatible entries fire a diagnostic callback and proceed.
Complete example
A production-grade contract for a payment tool:
tool: process_payment
side_effect: financial
evidence_class: local_transaction
commit_requirement: acknowledged
gate: block
timeouts: { total_ms: 30000 }
retries: { max_attempts: 1, retry_on: [] }
rate_limits: { on_429: { respect_retry_after: true, max_sleep_seconds: 60 } }
assertions:
input_invariants: []
output_invariants: []
golden_cases: []
allowed_errors: []
transitions:
valid_in_phases: [payment_approved]
advances_to: payment_processed
preconditions:
- requires_prior_tool: verify_identity
- requires_prior_tool: approve_payment
resource:
bind_from: arguments
path: "$.order_id"
with_output:
- path: "$.approved"
equals: true
forbids_after:
- process_payment
- void_order
argument_value_invariants:
- path: "$.amount"
gte: 0.01
lte: 50000
- path: "$.currency"
one_of: ["USD", "EUR", "GBP", "JPY"]
execution_constraints:
arguments:
- path: "$.amount"
lte: 50000
response_format_invariants:
finish_reason: "tool_calls"
tool_calls_present: true
Next steps
- Phases & Transitions — design your state machine
- Preconditions & Ordering — enforce cross-step rules
- Contract YAML Reference — every field, every type, every value