How to model real agent behaviors with ABS — recipes, not reference.
The spec tells you what's valid. This page tells you how to combine those pieces to model the things you actually need: routing, round-trips, multi-step verification, escalation guards.
The problem: one entry point (a triage agent) routes to multiple specialist agents. A refund goes to Refunds, an order question goes to Orders, a security issue goes to a human.
The v0.1 solution: one Session per branch. v0.1 does not model branching inside a Session — a Session describes exactly one trace.
Start with the happy path for one destination:
session: Intent routing — damaged item → refund
behaviors:
- actor: user
action: says
content: "I received a damaged item. Order #8291."
- actor: assistant # ← triage clarifies intent
action: clarifies
content: "I understand — you want to return #8291. Connecting you to refunds."
- actor: assistant # ← hands off to specialist
action: hands_off
target: Refunds Agent
content: "Refund request: #8291, reason: damaged"
- actor: assistant # ← specialist takes over
action: greets
content: "Hi, I'm the refunds specialist. Processing your return now."
- actor: assistant
action: calls
target: Refunds API
# ...
- actor: assistant
action: informs
content: "Refund approved. €47.50 in 3-5 days. Reference: R-5512."
The sequence evaluator checks the full trace across the hand-off boundary. never acts as a routing guard:
evaluations:
- type: sequence
order:
- { actor: assistant, action: clarifies }
- { actor: assistant, action: hands_off, target: "Refunds Agent" }
- { actor: assistant, action: greets }
- { actor: assistant, action: calls, target: "Refunds API" }
- { actor: assistant, action: informs }
- type: never
match: { actor: assistant, action: hands_off, target: "Human Agent" }
The sequence doesn't care that greets comes from a different agent instance than clarifies — ABS tracks actor and action, not agent identity.
The two highest-risk steps are the triage and the hand-off:
- actor: assistant
action: clarifies
evaluations:
- type: llm_judge
criteria: |
1. Identifies the request as a refund, not a general inquiry
2. References the order number
3. States they are routing to refunds specifically
- actor: assistant
action: hands_off
target: Refunds Agent
evaluations:
- type: llm_judge
criteria: |
1. Hands off to Refunds Agent — not Orders, not Human
2. Passes order number and reason in the hand-off context
Each branch is its own Session. Same structure, different sequence and never guards. See the full example on GitHub.
# ❌ Don't do this — v0.1 has no branching syntax
behaviors:
- actor: assistant
action: hands_off
target: Refunds Agent # or Orders? or Human?
A Session is one trace. If the agent could go to 3 places, write 3 Sessions (SPECIFICATION.md §7).
Three Behaviors, each evaluated independently:
# 1. The assistant calls the tool — check the parameters
- actor: assistant
action: calls
target: Orders API
with:
orderId: "8291"
# 2. The tool responds — check the payload
- actor: tool
action: responds
target: Orders API
content:
orderId: "8291"
status: "in_transit"
evaluations:
- type: schema
schema:
type: object
required: [orderId, status]
# 3. The assistant tells the user — check the communication
- actor: assistant
action: informs
content: "Your order is on its way"
evaluations:
- type: contains
value: "on its way"
The split makes both the raw payload and the assistant's paraphrasing separately assertable. The assistant might say "your order shipped" when the API returned status: "pending" — with separate Behaviors you catch both.
with vs with_onlywith — partial match. Extra params OK. Use when the agent may add contextual fields.with_only — strict match. No extra keys. Use for security-critical calls.See Tools & MCP for the full rules.
Capture early, verify late. Hard facts with contains, soft qualities with llm_judge, safety net with variable_consistency.
behaviors:
- actor: user
action: says
content: "Franco Vinciarelli, order #8291"
capture:
customerName: "Franco Vinciarelli"
orderId: "8291"
# ... agent does its job ...
- actor: assistant
action: informs
content: "Refund of €47.50 processed, Franco. Reference: R-5512."
capture:
refundId: "R-5512"
evaluations:
- type: contains
value: "R-5512" # hard fact
- type: llm_judge
criteria: | # soft qualities
1. States amount and timeline
2. Provides refund reference
3. Uses the customer's name
4. Reassuring tone, no upsells
evaluations:
- type: variable_consistency
variable: refundId # safety net against ID drift
The agent must ask before it can proceed. Model the ask-capture-reuse cycle:
session: Order status — missing order number
behaviors:
- actor: user
action: says
content: "Where is my order?"
- actor: assistant
action: asks
content: "Please provide your order number"
- actor: user
action: says
content: "8291"
capture:
orderId: "8291"
- actor: assistant
action: calls
target: Orders API
with:
orderId: "{{orderId}}"
- actor: assistant
action: informs
content: "Your order is on its way"
evaluations:
- type: sequence
order:
- { actor: assistant, action: asks }
- { actor: assistant, action: calls }
- { actor: assistant, action: informs }
The sequence tests what matters: the agent asked before calling. Without this, a lazy agent could skip asking and hallucinate the order number.
A two-line safety net that catches routing failures:
# Auto-resolution flow: must NOT escalate
evaluations:
- type: never
match: { actor: assistant, action: hands_off, target: "Human Agent" }
# Human-only flow: must NOT call tools
evaluations:
- type: never
match: { actor: assistant, action: calls }
{{variables}} work inside fragments today but can't easily adapt content across sessions without dataset bindings. Deferred to v0.2+.