Phases & Transitions
Phases are a state machine for your agent's session. They control which tools the model can see at each step — the most aggressive intervention point in the enforcement pipeline.
Why phases matter
Without phases, your agent has access to all tools at all times. A model tasked with fixing a bug could call terraform destroy because it has permissions. A support agent could skip identity verification and go straight to issuing refunds.
With phases, tools are invisible outside their valid phase. The model can't call what it can't see.
How it works
1. Define phases in session.yaml
schema_version: "1.0"
agent: refund-support-bot
phases:
- name: triage
initial: true
- name: customer_identified
- name: eligibility_checked
- name: refund_issued
- name: completed
terminal: true
transitions:
triage: [customer_identified]
customer_identified: [eligibility_checked]
eligibility_checked: [refund_issued]
refund_issued: [completed]
phases is an array, transitions is an objectNote the different formats: phases is an array of objects (each with a name), while transitions is an object map (keys are phase names, values are arrays of target phases). Don't mix them up — using an object map for phases will produce a parse error.
# phases: array of { name, initial?, terminal? }
phases:
- name: triage
initial: true
- name: done
terminal: true
# transitions: object map of from → [to, ...]
transitions:
triage: [done]
Rules:
- Exactly one phase must be
initial: true - At least one phase must be
terminal: true - Every non-terminal phase must be reachable from the initial phase
- The
transitionsmap defines legal phase flows - If
session.yamlhas phases but compilation fails (e.g., missing initial or terminal phase), the session will refuse to start — allcreate()calls throwReplayConfigError
2. Mark tools with phase restrictions
# lookup_customer.yaml
transitions:
valid_in_phases: [triage]
advances_to: customer_identified
This tool is only visible during the triage phase. When it executes successfully, the session advances to customer_identified.
3. Narrowing removes invisible tools
Before every LLM call, the enforcement pipeline filters the tool list:
You pass: [lookup_customer, check_eligibility, issue_refund, send_confirmation]
Phase: triage
LLM sees: [lookup_customer]
Removed: check_eligibility (wrong_phase), issue_refund (wrong_phase), send_confirmation (wrong_phase)
The model literally cannot ask for a tool that isn't in its current phase.
Common patterns
Linear workflow
transitions:
start: [step_a]
step_a: [step_b]
step_b: [step_c]
step_c: [done]
start → step_a → step_b → step_c → done
Use for: Sequential processes like refund flows, onboarding, data pipelines.
Branching with escalation
phases:
- name: triage
initial: true
- name: investigating
- name: resolved
terminal: true
- name: escalated
terminal: true
transitions:
triage: [investigating, escalated]
investigating: [resolved, escalated]
triage → investigating → resolved
↘ ↘
escalated escalated
Use for: Any workflow where any step can bail out to a human.
Hub-and-spoke (research phase)
phases:
- name: start
initial: true
- name: researching
- name: analyzing
- name: done
terminal: true
transitions:
start: [researching]
researching: [researching, analyzing] # Can loop back to itself
analyzing: [done]
start → researching ↻ → analyzing → done
Use for: Agents that need multiple research passes before deciding.
Parallel branches
transitions:
start: [path_a, path_b]
path_a: [merge]
path_b: [merge]
merge: [done]
Both paths converge at merge. The model takes one path based on which tool it calls first.
Multiple tools per phase
Multiple tools can be valid in the same phase:
# search_web.yaml
transitions:
valid_in_phases: [researching]
advances_to: analyzing
# search_database.yaml
transitions:
valid_in_phases: [researching]
advances_to: analyzing
Both tools are visible during researching. The model picks which one to use. Both advance to the same phase.
Different targets from same phase:
# approve.yaml
transitions:
valid_in_phases: [review]
advances_to: approved
# reject.yaml
transitions:
valid_in_phases: [review]
advances_to: rejected
Both tools are visible during review. The model's choice determines which branch the session takes. This is legal branching — the model decides.
Tools without phase restrictions
A tool without transitions is available in all phases:
# logging_tool.yaml
tool: log_event
side_effect: read
# No transitions field — always available
Use for: utility tools like logging, help functions, or status checks that should be available everywhere.
Phase validation rules
The pipeline catches two types of illegal transitions:
Illegal transition
A tool attempts to advance to a phase not in the transitions map:
Current phase: triage
Tool calls: issue_refund (advances_to: refund_issued)
transitions[triage] = [customer_identified]
Result: BLOCKED — "illegal_phase_transition"
This is caught during validation (Stage 3), before the response is released.
Ambiguous transition
Multiple tool calls in one response attempt different phase targets:
Current phase: review
Tool calls: approve (→ approved) + reject (→ rejected)
Result: BLOCKED — "ambiguous_phase_transition"
If both tools advance to the same phase, it's fine. Only different targets in one response are ambiguous.
Phase state in code
const session = replay(client, {
contractsDir: "./contracts",
agent: "refund-support-bot",
mode: "enforce",
});
// Check current phase
const state = session.getState();
console.log(state.currentPhase); // "triage"
// After first tool call...
const state2 = session.getState();
console.log(state2.currentPhase); // "customer_identified"
The onNarrow callback
See exactly what tools were removed and why:
const session = replay(client, {
contractsDir: "./contracts",
agent: "refund-support-bot",
mode: "enforce",
onNarrow: (narrowing) => {
console.log(`Allowed: ${narrowing.allowed.map(t => t.function.name).join(", ")}`);
for (const r of narrowing.removed) {
console.log(` Removed: ${r.tool} — ${r.reason}`);
}
},
});
Removal reasons:
| Reason | Meaning |
|---|---|
wrong_phase | Tool not valid in current phase |
precondition_not_met | Required prior tool not called |
forbidden_in_state | Tool in forbiddenTools set |
no_contract | No contract found (and unmatchedPolicy: "block") |
policy_denied | Principal not authorized |
manual_filter | Excluded by session.narrow() |
Manual narrowing
Temporarily restrict tools beyond what contracts require:
// Only allow lookup_customer (within compiled legal space)
session.narrow(["lookup_customer"]);
// Remove manual restriction (returns to contract-driven narrowing)
session.widen();
Important: narrow() can only restrict further — it cannot add tools that contracts would remove. widen() returns to contract-driven narrowing, not unbounded access.
Cross-phase behavior
Aggregates and envelopes persist across phases
Session aggregates and value envelopes are not phase-scoped. They accumulate across phase transitions by design:
- An aggregate tracking
sumof shares accumulates whether the session is inpre_tradeorpost_trade - An envelope ceiling set in
pre_tradeconstrains values inpost_trade
This prevents salami attacks that span phases — breaking a harmful total across multiple phases doesn't bypass aggregate limits.
Graph analysis detects dead and unreachable phases
The contract graph analyzer (run at compile time) checks for phase-related issues:
- Dead phases — a phase exists but no tools have
valid_in_phasesincluding it - Unreachable phases — a non-initial phase has no incoming transitions
- Circular deadlocks — a set of phases forms a cycle with no exit (error severity)
These diagnostics appear during session creation or when running vesanor validate. They can be suppressed in session.yaml with a required reason:
graph_analysis:
suppress:
- check: dead_phase
phase: pass_through_phase
reason: "Intentional pass-through, no tools needed"
Design tips
-
Start simple. A linear 3-5 phase workflow covers most use cases. Add branching later if needed.
-
Name phases by what's been accomplished, not what's happening.
customer_identified(past tense, clear) beatsidentifying_customer(ongoing, ambiguous about completion). -
Always have an escalation terminal. Real agents need an escape hatch:
escalated,error, ormanual_review. -
Don't over-phase. If two tools are always valid together and advance to the same place, they don't need separate phases. Phases should represent meaningful state boundaries.
-
Test with shadow mode first. Run
mode: "shadow"to see how your phase machine behaves with real traffic before enabling enforcement.
Next steps
- Preconditions & Ordering — add cross-step dependencies within phases
- Session Limits — cap total steps, cost, and tool calls
- Contract Cookbook — every contract field explained