@basein/runner 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +540 -350
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -10
- package/dist/bin/bir-scenario.js +704 -135
- package/dist/bin/bir.d.ts +14 -8
- package/dist/bin/bir.js +2937 -488
- package/dist/bin/investigate.js +41 -16
- package/dist/bin/scenario-edit.d.ts +43 -10
- package/dist/bin/scenario-edit.js +281 -80
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2803 -306
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/housekeeping.d.ts +71 -0
- package/dist/record/housekeeping.js +417 -0
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +371 -20
- package/dist/replay/controller.js +1185 -111
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1244
- package/docs/calculatedReplayGuide.md +1212 -879
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -271
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- package/scripts/install.sh +104 -7
package/docs/t-bench.md
CHANGED
|
@@ -1,394 +1,406 @@
|
|
|
1
|
-
# τ²-bench — running it against BaseInstRunner, with Claude Code as the agent
|
|
2
|
-
|
|
3
|
-
Companion to [benchmark.md](benchmark.md), which recommends MCPMark first and τ²-bench
|
|
4
|
-
second. This is the second half: what τ²-bench actually is, how to read a trajectory,
|
|
5
|
-
and the three structural facts that decide whether the integration measures our product
|
|
6
|
-
or measures nothing at all.
|
|
7
|
-
|
|
8
|
-
**Read §3 before writing any code.** τ²-bench executes tool calls *inside its own
|
|
9
|
-
orchestrator*. Nothing crosses MCP by default, which means a naive integration produces a
|
|
10
|
-
clean run, a plausible score, and zero recorded steps.
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 1. Why this benchmark, in one paragraph
|
|
15
|
-
|
|
16
|
-
MCPMark proves a replayed scenario survives a **fresh instance of the same task**.
|
|
17
|
-
τ²-bench proves it survives a **different task of the same shape** — "cancel order W1234
|
|
18
|
-
for user A" against "cancel order W5678 for user B". Its domains are built out of
|
|
19
|
-
policy-constrained task families over a shared database, which is exactly the surface that
|
|
20
|
-
per-step parameter derivation exists for. Nothing else public tests the *calculated* half
|
|
21
|
-
of calculated replay this directly. It also carries the name recognition that makes a
|
|
22
|
-
result travel.
|
|
23
|
-
|
|
24
|
-
Domains: `retail` (115 tasks), `airline` (50), `telecom` (114), plus `mock` and
|
|
25
|
-
`banking_knowledge`. Python ≥3.12 <3.14, `uv sync`, LiteLLM-compatible keys in `.env`.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## 2. Reading a trajectory
|
|
30
|
-
|
|
31
|
-
The link that prompted this doc:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
https://taubench.com/trajectory-visualizer?model=qwen3.5-397b-a17b-think_sierra_2026-03-02&domain=retail&task=0
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
It is a client-side viewer over the simulation JSON that `tau2 run` writes to
|
|
38
|
-
`data/simulations/`. Two modes — **Trajectories** and **Tasks** — and the URL pins one
|
|
39
|
-
`(model, domain, task)` triple. The same data is browsable locally with `tau2 view`, which
|
|
40
|
-
is the version to use while building the adapter, because it reads your own runs.
|
|
41
|
-
|
|
42
|
-
An episode is a four-party loop, and the visualizer shows it as one interleaved column:
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
Agent ──message──▶ User ──message──▶ Agent ──tool_call──▶ Environment ──result──▶ Agent
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
| Element | What it is | What we care about |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| **UserMessage** | the *simulated* customer, driven by `--user-llm` | turn 1 carries the intent; later turns supply parameters |
|
|
51
|
-
| **AssistantMessage** | the agent's text, and optionally `tool_calls` | the text is what the COMMUNICATE check reads |
|
|
52
|
-
| **ToolMessage** | the environment's result | executed by the orchestrator, not the agent (§3.1) |
|
|
53
|
-
| **evaluation_criteria.actions** | *one* reference solution, replayed on a fresh env to derive the target DB hash | **not** a required path (§3.2) |
|
|
54
|
-
| reward breakdown | DB / COMMUNICATE / ENV_ASSERTION / NL_ASSERTION | the product of whatever `reward_basis` lists |
|
|
55
|
-
|
|
56
|
-
Episodes end on a stop token, a user-simulator "task complete" transfer, or `max_steps`.
|
|
57
|
-
|
|
58
|
-
**The one habit worth forming:** when a task scores 0, open the trajectory and check
|
|
59
|
-
*which* component was 0. A DB failure and a COMMUNICATE failure look identical in the
|
|
60
|
-
aggregate and mean opposite things for us — see §3.3.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## 3. The three facts that decide the integration
|
|
65
|
-
|
|
66
|
-
### 3.1 The grade comes from the transcript, not from the world
|
|
67
|
-
|
|
68
|
-
*Corrected after the first working run. An earlier version of this section said the
|
|
69
|
-
binding constraint was that the orchestrator executes tool calls. That is true — the agent
|
|
70
|
-
returns an `AssistantMessage` containing `tool_calls` and the orchestrator invokes them
|
|
71
|
-
against the domain environment — but it is not the constraint that decides the design.*
|
|
72
|
-
|
|
73
|
-
The constraint that decides the design is in `evaluator_env.py`: τ² **does not grade the
|
|
74
|
-
environment the episode ran in**. It constructs a *fresh* environment, replays the
|
|
75
|
-
trajectory's `(tool_call, tool_result)` pairs onto it via `set_state`, and hashes that.
|
|
76
|
-
|
|
77
|
-
> **Consequence:** the reward is earned by what appears in the messages. An agent that
|
|
78
|
-
> executes the whole task perfectly against the live environment and reports it in prose
|
|
79
|
-
> scores **zero**, because the graded environment received nothing.
|
|
80
|
-
|
|
81
|
-
We measured exactly this. Claude Code solved retail task 0 with a call sequence matching
|
|
82
|
-
the reference trajectory *exactly* — same tools, same item ids, same payment method — and
|
|
83
|
-
scored `DB 0`, `reward 0`. Nothing in the results table said "your harness is broken"; it
|
|
84
|
-
read like a hard task.
|
|
85
|
-
|
|
86
|
-
τ²-bench also ships no MCP interface: domain tools are Python callables wrapped in `Tool`
|
|
87
|
-
objects and called in-process. So bir sees nothing unless Claude Code makes real MCP calls
|
|
88
|
-
of its own. Those two facts together — the trajectory must carry the calls, *and* Claude
|
|
89
|
-
Code must make them over MCP — are what §4 has to satisfy at once.
|
|
90
|
-
|
|
91
|
-
### 3.2 Reward is outcome-based, so replay is legitimately scoreable
|
|
92
|
-
|
|
93
|
-
`evaluation_criteria.actions` is a reference trajectory, replayed on a fresh environment to
|
|
94
|
-
establish a **target database hash**. The DB check compares hashes. Any sequence of tool
|
|
95
|
-
calls reaching an equivalent end state passes. The docs are emphatic: *actions document one
|
|
96
|
-
working solution, not the only acceptable one.*
|
|
97
|
-
|
|
98
|
-
`ACTION` — exact trajectory match — is a reward component that exists but is **not** in the
|
|
99
|
-
default `reward_basis` for airline, retail or telecom, which is `["DB", "COMMUNICATE"]`.
|
|
100
|
-
|
|
101
|
-
This is the fact that makes the whole exercise honest. A replayed scenario takes the path
|
|
102
|
-
*a previous run* took, which will not be token-identical to the reference actions. Under an
|
|
103
|
-
action-matching benchmark that would be scored as deviation. Under τ²-bench it is scored as
|
|
104
|
-
what it is: the same outcome, reached differently. We are not exploiting a loophole — we
|
|
105
|
-
are using the benchmark's stated philosophy.
|
|
106
|
-
|
|
107
|
-
**Check `reward_basis` per task before running.** If a domain or task set includes `ACTION`,
|
|
108
|
-
exclude it and say so, rather than discovering it in the aggregate.
|
|
109
|
-
|
|
110
|
-
### 3.3 Reward is a *product*, and COMMUNICATE is in the basis
|
|
111
|
-
|
|
112
|
-
`reward = db_reward × communicate_reward`. `communicate_info` is a list of strings the
|
|
113
|
-
agent must have said to the user, matched as substrings.
|
|
114
|
-
|
|
115
|
-
Now read that against what direct-mode replay does. From the README: on a scenario whose
|
|
116
|
-
every step is a wrapped MCP tool, the sequence runs through connections the proxies already
|
|
117
|
-
hold — **zero model tokens** — and the model's only job is one tool call that reads the
|
|
118
|
-
results.
|
|
119
|
-
|
|
120
|
-
> A replay that executes every step perfectly and never *tells the customer the refund
|
|
121
|
-
> amount* scores `1 × 0 = 0`.
|
|
122
|
-
|
|
123
|
-
The design already accounts for this: `steered_full` means every planned step ran with
|
|
124
|
-
scenario-derived inputs **and the response model was produced**. So the machinery should be
|
|
125
|
-
right. But this is the single highest-value assertion to verify early, because its failure
|
|
126
|
-
signature — every task at exactly 0.0 with a healthy step ledger — is easy to misread as a
|
|
127
|
-
plumbing bug and waste a day on.
|
|
128
|
-
|
|
129
|
-
τ-bench is, in this specific sense, a *harder* test of replay than MCPMark: MCPMark grades
|
|
130
|
-
the world, τ-bench grades the world **and** what the agent said about it.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## 4. Architecture — two environments
|
|
135
|
-
|
|
136
|
-
Both constraints in §3.1 have to hold at once: Claude Code must make **real MCP calls**
|
|
137
|
-
(or bir records nothing), and the **trajectory must carry those calls** (or the grade is
|
|
138
|
-
zero). One environment cannot satisfy both — if Claude Code executes a call and the
|
|
139
|
-
adapter also hands it to the orchestrator, the orchestrator executes it a second time,
|
|
140
|
-
the exchange fails as already-exchanged, and the trajectory records an error the evaluator
|
|
141
|
-
cannot replay.
|
|
142
|
-
|
|
143
|
-
Two copies of the domain resolve it. Each executes every call exactly once.
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
tau2 orchestrator ── graded environment ◀── executes the calls the adapter emits
|
|
147
|
-
├── user simulator (--user-llm) (this is what the trajectory records)
|
|
148
|
-
└── ClaudeCodeAgent
|
|
149
|
-
├── ToolBridge ── private copy of the domain ◀── Claude Code acts here
|
|
150
|
-
│ (in-process HTTP, ephemeral port, one per episode)
|
|
151
|
-
└── claude -p / --resume, one call per orchestrator turn
|
|
152
|
-
└── mcp__tau2__* ─▶ bir-proxy ─▶ node mcp-stdio-bridge.mjs ─▶ ToolBridge
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
The copies start identical — taken after `_initialize_environment` has applied the task's
|
|
156
|
-
setup — and replay the same calls in the same order, so they stay in step. Claude Code's
|
|
157
|
-
results are real; the orchestrator's execution is what gets graded.
|
|
158
|
-
|
|
159
|
-
### 4.1 Taking the copy
|
|
160
|
-
|
|
161
|
-
`build_agent` hands the agent `environment.get_tools()`: live `Tool` objects whose `_func`
|
|
162
|
-
is a bound method of the domain's toolkit, which owns the db. Deep-copying **the toolkit**
|
|
163
|
-
yields an independent domain with its own database:
|
|
164
|
-
|
|
165
|
-
```python
|
|
166
|
-
toolkit = self.tools[0]._func.__self__ # RetailTools, etc.
|
|
167
|
-
shadow = copy.deepcopy(toolkit)
|
|
168
|
-
shadow_tools = list(shadow.get_tools().values())
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Copying the toolkit rather than rebuilding the domain from the registry preserves any
|
|
172
|
-
task-specific initialization that would otherwise have to be reconstructed from the task
|
|
173
|
-
object. Verify independence rather than assuming it: write through the copy and confirm
|
|
174
|
-
`environment.get_db_hash()` does not move. `selftest_bridge.py` is that check.
|
|
175
|
-
|
|
176
|
-
### 4.2 One Claude Code turn becomes N+1 messages
|
|
177
|
-
|
|
178
|
-
τ²'s validator (`validate_message_format_default`) rejects a message carrying both text and
|
|
179
|
-
tool calls. So a turn in which Claude Code makes four calls and then replies is *five*
|
|
180
|
-
messages, each separated by the orchestrator executing one call. The adapter queues them
|
|
181
|
-
and hands over one per `generate_next_message`; the tool results the orchestrator returns
|
|
182
|
-
are ignored, because Claude Code already saw the equivalent result from its own copy and
|
|
183
|
-
the run is over by then. Their purpose is the trajectory.
|
|
184
|
-
|
|
185
|
-
> **Stamp each message when you hand it over, not when you build it.** τ² sorts the final
|
|
186
|
-
> trajectory by timestamp. A queue built in one burst carries timestamps *older* than the
|
|
187
|
-
> tool results answering it, so the sort files every assistant message ahead of every tool
|
|
188
|
-
> message and the evaluator refuses to replay it. It is intermittent — when the clock does
|
|
189
|
-
> not tick between batches the timestamps tie and the stable sort happens to be right — so
|
|
190
|
-
> it presents as roughly one run in three dying with an infrastructure error.
|
|
191
|
-
|
|
192
|
-
Two smaller things in the same area:
|
|
193
|
-
|
|
194
|
-
- **Give every assistant message a `cost`, even zero.** `get_cost` discards the whole
|
|
195
|
-
conversation's cost if any single one is `None`, so the tool-call messages carry `0.0`
|
|
196
|
-
and the reply carries the run's total.
|
|
197
|
-
- **Translate the token counts.** τ² sums `prompt_tokens` / `completion_tokens`; Claude
|
|
198
|
-
Code reports `input_tokens`, `output_tokens` and two cache figures. Cached reads are real
|
|
199
|
-
prompt tokens and are most of the prompt on a resumed session, so they belong in the
|
|
200
|
-
total or every turn after the first looks nearly free.
|
|
201
|
-
|
|
202
|
-
### 4.3 Passing the policy
|
|
203
|
-
|
|
204
|
-
The agent constructor receives `domain_policy` as a string. It goes into the session as
|
|
205
|
-
`--append-system-prompt` (or `--append-system-prompt-file` — retail's `policy.md` is long
|
|
206
|
-
enough that a file is the better shape). Do not paste it into the per-turn prompt: it is
|
|
207
|
-
constant across the episode, and repeating it per turn inflates every cost number we are
|
|
208
|
-
trying to measure.
|
|
209
|
-
|
|
210
|
-
### 4.4 Per-turn invocation
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
# turn 1
|
|
214
|
-
claude -p "$USER_MESSAGE" \
|
|
215
|
-
--append-system-prompt-file "$POLICY_MD" \
|
|
216
|
-
--settings "$BIR_SETTINGS_JSON" \
|
|
217
|
-
--mcp-config "$EPISODE_MCP_JSON" \
|
|
218
|
-
--output-format json \
|
|
219
|
-
--permission-mode dontAsk \
|
|
220
|
-
--allowedTools "mcp__tau2__*" \
|
|
221
|
-
--max-turns 30
|
|
222
|
-
|
|
223
|
-
# turns 2..n
|
|
224
|
-
claude -p "$USER_MESSAGE" --resume "$SESSION_ID" --output-format json ...
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
`--allowedTools "mcp__tau2__*"` and nothing else. The agent has no business touching the
|
|
228
|
-
filesystem or shell in a customer-service episode, and an allow list that says so turns a
|
|
229
|
-
whole class of confusing failures into a clean permission denial.
|
|
230
|
-
|
|
231
|
-
Check `system/init`'s `mcp_server_errors` on turn 1 of every episode. A `--mcp-config`
|
|
232
|
-
entry that fails validation is skipped *silently*; the episode then runs with no tools and
|
|
233
|
-
scores 0, which reads exactly like a hard task.
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
## 5. BIR wiring
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
export BIR_AUTH_URL=https://your-basein-service
|
|
241
|
-
export BIR_CONTROL_URL=http://127.0.0.1:53411 # not discovery
|
|
242
|
-
export BIR_CONTROL_TOKEN=...
|
|
243
|
-
export BIR_REPLAY_ALLOW_SERVERS=tau2
|
|
244
|
-
BIR_REPLAY=1 bir-hooks 2>&1 | tee -a results/tau2-audit.log
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`BIR_CONTROL_URL` / `BIR_CONTROL_TOKEN` are mandatory, not convenient: the README calls
|
|
248
|
-
them the only reliable channel for an SDK session, and every episode turn is one. Discovery
|
|
249
|
-
across a few hundred harness-spawned processes is how an arm silently degrades to Tier 2.
|
|
250
|
-
|
|
251
|
-
`BIR_REPLAY_ALLOW_SERVERS=tau2` is the whole allowance. Direct execution dispatches steps on
|
|
252
|
-
connections that are already open and never reaches the permission system, so the list is
|
|
253
|
-
the blast radius.
|
|
254
|
-
|
|
255
|
-
### 5.1 The per-prompt run boundary — the structural mismatch
|
|
256
|
-
|
|
257
|
-
This is the one that will shape the results, and it is worth understanding before the first
|
|
258
|
-
run rather than after.
|
|
259
|
-
|
|
260
|
-
`onPrompt` rolls a **new run on every prompt**. In τ²-bench, every user-simulator turn is a
|
|
261
|
-
prompt. So an episode of six turns is **six runs**, not one, and matching happens per turn
|
|
262
|
-
against a library of per-turn scenarios.
|
|
263
|
-
|
|
264
|
-
What follows from that:
|
|
265
|
-
|
|
266
|
-
- **Turn 1 carries the intent** — "I want to cancel my order" — and is where a match should
|
|
267
|
-
fire. Later turns are mostly parameter supply ("it's W5678") and are short, cheap, and
|
|
268
|
-
poor matching material.
|
|
269
|
-
- **A scenario is a turn's tool chain, not an episode's.** Cross-episode reuse therefore
|
|
270
|
-
happens turn-by-turn, and the savings ledger sums over turns.
|
|
271
|
-
- **Per-run usage watermarking matters more here than anywhere.** Six runs share one
|
|
272
|
-
Claude Code transcript; summing the file gives turn 6 the cost of turns 1–6. The design
|
|
273
|
-
doc's `markTranscriptUsage` / `usageSince` delta is not an optimisation in this setting,
|
|
274
|
-
it is the difference between a real number and a fabricated one. Check `measured: true`.
|
|
275
|
-
|
|
276
|
-
If the results show matching that fires reliably on turn 1 and rarely after, that is not a
|
|
277
|
-
bug — it is the honest shape of the product against a multi-turn benchmark, and it should
|
|
278
|
-
be reported that way.
|
|
279
|
-
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
## 6. Running it
|
|
283
|
-
|
|
284
|
-
```bash
|
|
285
|
-
git clone https://github.com/sierra-research/tau2-bench && cd tau2-bench
|
|
286
|
-
uv sync && cp .env.example .env # user-simulator key goes here
|
|
287
|
-
|
|
288
|
-
# smoke: one task, one trial
|
|
289
|
-
tau2 run --domain retail --agent claude_code --user-llm <model> \
|
|
290
|
-
--num-trials 1 --task-ids 0
|
|
291
|
-
|
|
292
|
-
tau2 view # read the trajectory you just produced
|
|
293
|
-
|
|
294
|
-
# baseline arm, recording only (BIR_REPLAY unset)
|
|
295
|
-
tau2 run --domain retail --agent claude_code --user-llm <model> \
|
|
296
|
-
--num-trials 4 --max-concurrency 4
|
|
297
|
-
|
|
298
|
-
# replay arm, library seeded from the passing runs above
|
|
299
|
-
BIR_REPLAY=1 tau2 run --domain retail --agent claude_code --user-llm <model> \
|
|
300
|
-
--num-trials 4 --max-concurrency 4
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
-
|
|
391
|
-
|
|
392
|
-
-
|
|
393
|
-
|
|
394
|
-
|
|
1
|
+
# τ²-bench — running it against BaseInstRunner, with Claude Code as the agent
|
|
2
|
+
|
|
3
|
+
Companion to [benchmark.md](benchmark.md), which recommends MCPMark first and τ²-bench
|
|
4
|
+
second. This is the second half: what τ²-bench actually is, how to read a trajectory,
|
|
5
|
+
and the three structural facts that decide whether the integration measures our product
|
|
6
|
+
or measures nothing at all.
|
|
7
|
+
|
|
8
|
+
**Read §3 before writing any code.** τ²-bench executes tool calls *inside its own
|
|
9
|
+
orchestrator*. Nothing crosses MCP by default, which means a naive integration produces a
|
|
10
|
+
clean run, a plausible score, and zero recorded steps.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Why this benchmark, in one paragraph
|
|
15
|
+
|
|
16
|
+
MCPMark proves a replayed scenario survives a **fresh instance of the same task**.
|
|
17
|
+
τ²-bench proves it survives a **different task of the same shape** — "cancel order W1234
|
|
18
|
+
for user A" against "cancel order W5678 for user B". Its domains are built out of
|
|
19
|
+
policy-constrained task families over a shared database, which is exactly the surface that
|
|
20
|
+
per-step parameter derivation exists for. Nothing else public tests the *calculated* half
|
|
21
|
+
of calculated replay this directly. It also carries the name recognition that makes a
|
|
22
|
+
result travel.
|
|
23
|
+
|
|
24
|
+
Domains: `retail` (115 tasks), `airline` (50), `telecom` (114), plus `mock` and
|
|
25
|
+
`banking_knowledge`. Python ≥3.12 <3.14, `uv sync`, LiteLLM-compatible keys in `.env`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Reading a trajectory
|
|
30
|
+
|
|
31
|
+
The link that prompted this doc:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
https://taubench.com/trajectory-visualizer?model=qwen3.5-397b-a17b-think_sierra_2026-03-02&domain=retail&task=0
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It is a client-side viewer over the simulation JSON that `tau2 run` writes to
|
|
38
|
+
`data/simulations/`. Two modes — **Trajectories** and **Tasks** — and the URL pins one
|
|
39
|
+
`(model, domain, task)` triple. The same data is browsable locally with `tau2 view`, which
|
|
40
|
+
is the version to use while building the adapter, because it reads your own runs.
|
|
41
|
+
|
|
42
|
+
An episode is a four-party loop, and the visualizer shows it as one interleaved column:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Agent ──message──▶ User ──message──▶ Agent ──tool_call──▶ Environment ──result──▶ Agent
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Element | What it is | What we care about |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| **UserMessage** | the *simulated* customer, driven by `--user-llm` | turn 1 carries the intent; later turns supply parameters |
|
|
51
|
+
| **AssistantMessage** | the agent's text, and optionally `tool_calls` | the text is what the COMMUNICATE check reads |
|
|
52
|
+
| **ToolMessage** | the environment's result | executed by the orchestrator, not the agent (§3.1) |
|
|
53
|
+
| **evaluation_criteria.actions** | *one* reference solution, replayed on a fresh env to derive the target DB hash | **not** a required path (§3.2) |
|
|
54
|
+
| reward breakdown | DB / COMMUNICATE / ENV_ASSERTION / NL_ASSERTION | the product of whatever `reward_basis` lists |
|
|
55
|
+
|
|
56
|
+
Episodes end on a stop token, a user-simulator "task complete" transfer, or `max_steps`.
|
|
57
|
+
|
|
58
|
+
**The one habit worth forming:** when a task scores 0, open the trajectory and check
|
|
59
|
+
*which* component was 0. A DB failure and a COMMUNICATE failure look identical in the
|
|
60
|
+
aggregate and mean opposite things for us — see §3.3.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. The three facts that decide the integration
|
|
65
|
+
|
|
66
|
+
### 3.1 The grade comes from the transcript, not from the world
|
|
67
|
+
|
|
68
|
+
*Corrected after the first working run. An earlier version of this section said the
|
|
69
|
+
binding constraint was that the orchestrator executes tool calls. That is true — the agent
|
|
70
|
+
returns an `AssistantMessage` containing `tool_calls` and the orchestrator invokes them
|
|
71
|
+
against the domain environment — but it is not the constraint that decides the design.*
|
|
72
|
+
|
|
73
|
+
The constraint that decides the design is in `evaluator_env.py`: τ² **does not grade the
|
|
74
|
+
environment the episode ran in**. It constructs a *fresh* environment, replays the
|
|
75
|
+
trajectory's `(tool_call, tool_result)` pairs onto it via `set_state`, and hashes that.
|
|
76
|
+
|
|
77
|
+
> **Consequence:** the reward is earned by what appears in the messages. An agent that
|
|
78
|
+
> executes the whole task perfectly against the live environment and reports it in prose
|
|
79
|
+
> scores **zero**, because the graded environment received nothing.
|
|
80
|
+
|
|
81
|
+
We measured exactly this. Claude Code solved retail task 0 with a call sequence matching
|
|
82
|
+
the reference trajectory *exactly* — same tools, same item ids, same payment method — and
|
|
83
|
+
scored `DB 0`, `reward 0`. Nothing in the results table said "your harness is broken"; it
|
|
84
|
+
read like a hard task.
|
|
85
|
+
|
|
86
|
+
τ²-bench also ships no MCP interface: domain tools are Python callables wrapped in `Tool`
|
|
87
|
+
objects and called in-process. So bir sees nothing unless Claude Code makes real MCP calls
|
|
88
|
+
of its own. Those two facts together — the trajectory must carry the calls, *and* Claude
|
|
89
|
+
Code must make them over MCP — are what §4 has to satisfy at once.
|
|
90
|
+
|
|
91
|
+
### 3.2 Reward is outcome-based, so replay is legitimately scoreable
|
|
92
|
+
|
|
93
|
+
`evaluation_criteria.actions` is a reference trajectory, replayed on a fresh environment to
|
|
94
|
+
establish a **target database hash**. The DB check compares hashes. Any sequence of tool
|
|
95
|
+
calls reaching an equivalent end state passes. The docs are emphatic: *actions document one
|
|
96
|
+
working solution, not the only acceptable one.*
|
|
97
|
+
|
|
98
|
+
`ACTION` — exact trajectory match — is a reward component that exists but is **not** in the
|
|
99
|
+
default `reward_basis` for airline, retail or telecom, which is `["DB", "COMMUNICATE"]`.
|
|
100
|
+
|
|
101
|
+
This is the fact that makes the whole exercise honest. A replayed scenario takes the path
|
|
102
|
+
*a previous run* took, which will not be token-identical to the reference actions. Under an
|
|
103
|
+
action-matching benchmark that would be scored as deviation. Under τ²-bench it is scored as
|
|
104
|
+
what it is: the same outcome, reached differently. We are not exploiting a loophole — we
|
|
105
|
+
are using the benchmark's stated philosophy.
|
|
106
|
+
|
|
107
|
+
**Check `reward_basis` per task before running.** If a domain or task set includes `ACTION`,
|
|
108
|
+
exclude it and say so, rather than discovering it in the aggregate.
|
|
109
|
+
|
|
110
|
+
### 3.3 Reward is a *product*, and COMMUNICATE is in the basis
|
|
111
|
+
|
|
112
|
+
`reward = db_reward × communicate_reward`. `communicate_info` is a list of strings the
|
|
113
|
+
agent must have said to the user, matched as substrings.
|
|
114
|
+
|
|
115
|
+
Now read that against what direct-mode replay does. From the README: on a scenario whose
|
|
116
|
+
every step is a wrapped MCP tool, the sequence runs through connections the proxies already
|
|
117
|
+
hold — **zero model tokens** — and the model's only job is one tool call that reads the
|
|
118
|
+
results.
|
|
119
|
+
|
|
120
|
+
> A replay that executes every step perfectly and never *tells the customer the refund
|
|
121
|
+
> amount* scores `1 × 0 = 0`.
|
|
122
|
+
|
|
123
|
+
The design already accounts for this: `steered_full` means every planned step ran with
|
|
124
|
+
scenario-derived inputs **and the response model was produced**. So the machinery should be
|
|
125
|
+
right. But this is the single highest-value assertion to verify early, because its failure
|
|
126
|
+
signature — every task at exactly 0.0 with a healthy step ledger — is easy to misread as a
|
|
127
|
+
plumbing bug and waste a day on.
|
|
128
|
+
|
|
129
|
+
τ-bench is, in this specific sense, a *harder* test of replay than MCPMark: MCPMark grades
|
|
130
|
+
the world, τ-bench grades the world **and** what the agent said about it.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 4. Architecture — two environments
|
|
135
|
+
|
|
136
|
+
Both constraints in §3.1 have to hold at once: Claude Code must make **real MCP calls**
|
|
137
|
+
(or bir records nothing), and the **trajectory must carry those calls** (or the grade is
|
|
138
|
+
zero). One environment cannot satisfy both — if Claude Code executes a call and the
|
|
139
|
+
adapter also hands it to the orchestrator, the orchestrator executes it a second time,
|
|
140
|
+
the exchange fails as already-exchanged, and the trajectory records an error the evaluator
|
|
141
|
+
cannot replay.
|
|
142
|
+
|
|
143
|
+
Two copies of the domain resolve it. Each executes every call exactly once.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
tau2 orchestrator ── graded environment ◀── executes the calls the adapter emits
|
|
147
|
+
├── user simulator (--user-llm) (this is what the trajectory records)
|
|
148
|
+
└── ClaudeCodeAgent
|
|
149
|
+
├── ToolBridge ── private copy of the domain ◀── Claude Code acts here
|
|
150
|
+
│ (in-process HTTP, ephemeral port, one per episode)
|
|
151
|
+
└── claude -p / --resume, one call per orchestrator turn
|
|
152
|
+
└── mcp__tau2__* ─▶ bir-proxy ─▶ node mcp-stdio-bridge.mjs ─▶ ToolBridge
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The copies start identical — taken after `_initialize_environment` has applied the task's
|
|
156
|
+
setup — and replay the same calls in the same order, so they stay in step. Claude Code's
|
|
157
|
+
results are real; the orchestrator's execution is what gets graded.
|
|
158
|
+
|
|
159
|
+
### 4.1 Taking the copy
|
|
160
|
+
|
|
161
|
+
`build_agent` hands the agent `environment.get_tools()`: live `Tool` objects whose `_func`
|
|
162
|
+
is a bound method of the domain's toolkit, which owns the db. Deep-copying **the toolkit**
|
|
163
|
+
yields an independent domain with its own database:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
toolkit = self.tools[0]._func.__self__ # RetailTools, etc.
|
|
167
|
+
shadow = copy.deepcopy(toolkit)
|
|
168
|
+
shadow_tools = list(shadow.get_tools().values())
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Copying the toolkit rather than rebuilding the domain from the registry preserves any
|
|
172
|
+
task-specific initialization that would otherwise have to be reconstructed from the task
|
|
173
|
+
object. Verify independence rather than assuming it: write through the copy and confirm
|
|
174
|
+
`environment.get_db_hash()` does not move. `selftest_bridge.py` is that check.
|
|
175
|
+
|
|
176
|
+
### 4.2 One Claude Code turn becomes N+1 messages
|
|
177
|
+
|
|
178
|
+
τ²'s validator (`validate_message_format_default`) rejects a message carrying both text and
|
|
179
|
+
tool calls. So a turn in which Claude Code makes four calls and then replies is *five*
|
|
180
|
+
messages, each separated by the orchestrator executing one call. The adapter queues them
|
|
181
|
+
and hands over one per `generate_next_message`; the tool results the orchestrator returns
|
|
182
|
+
are ignored, because Claude Code already saw the equivalent result from its own copy and
|
|
183
|
+
the run is over by then. Their purpose is the trajectory.
|
|
184
|
+
|
|
185
|
+
> **Stamp each message when you hand it over, not when you build it.** τ² sorts the final
|
|
186
|
+
> trajectory by timestamp. A queue built in one burst carries timestamps *older* than the
|
|
187
|
+
> tool results answering it, so the sort files every assistant message ahead of every tool
|
|
188
|
+
> message and the evaluator refuses to replay it. It is intermittent — when the clock does
|
|
189
|
+
> not tick between batches the timestamps tie and the stable sort happens to be right — so
|
|
190
|
+
> it presents as roughly one run in three dying with an infrastructure error.
|
|
191
|
+
|
|
192
|
+
Two smaller things in the same area:
|
|
193
|
+
|
|
194
|
+
- **Give every assistant message a `cost`, even zero.** `get_cost` discards the whole
|
|
195
|
+
conversation's cost if any single one is `None`, so the tool-call messages carry `0.0`
|
|
196
|
+
and the reply carries the run's total.
|
|
197
|
+
- **Translate the token counts.** τ² sums `prompt_tokens` / `completion_tokens`; Claude
|
|
198
|
+
Code reports `input_tokens`, `output_tokens` and two cache figures. Cached reads are real
|
|
199
|
+
prompt tokens and are most of the prompt on a resumed session, so they belong in the
|
|
200
|
+
total or every turn after the first looks nearly free.
|
|
201
|
+
|
|
202
|
+
### 4.3 Passing the policy
|
|
203
|
+
|
|
204
|
+
The agent constructor receives `domain_policy` as a string. It goes into the session as
|
|
205
|
+
`--append-system-prompt` (or `--append-system-prompt-file` — retail's `policy.md` is long
|
|
206
|
+
enough that a file is the better shape). Do not paste it into the per-turn prompt: it is
|
|
207
|
+
constant across the episode, and repeating it per turn inflates every cost number we are
|
|
208
|
+
trying to measure.
|
|
209
|
+
|
|
210
|
+
### 4.4 Per-turn invocation
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# turn 1
|
|
214
|
+
claude -p "$USER_MESSAGE" \
|
|
215
|
+
--append-system-prompt-file "$POLICY_MD" \
|
|
216
|
+
--settings "$BIR_SETTINGS_JSON" \
|
|
217
|
+
--mcp-config "$EPISODE_MCP_JSON" \
|
|
218
|
+
--output-format json \
|
|
219
|
+
--permission-mode dontAsk \
|
|
220
|
+
--allowedTools "mcp__tau2__*" \
|
|
221
|
+
--max-turns 30
|
|
222
|
+
|
|
223
|
+
# turns 2..n
|
|
224
|
+
claude -p "$USER_MESSAGE" --resume "$SESSION_ID" --output-format json ...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`--allowedTools "mcp__tau2__*"` and nothing else. The agent has no business touching the
|
|
228
|
+
filesystem or shell in a customer-service episode, and an allow list that says so turns a
|
|
229
|
+
whole class of confusing failures into a clean permission denial.
|
|
230
|
+
|
|
231
|
+
Check `system/init`'s `mcp_server_errors` on turn 1 of every episode. A `--mcp-config`
|
|
232
|
+
entry that fails validation is skipped *silently*; the episode then runs with no tools and
|
|
233
|
+
scores 0, which reads exactly like a hard task.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 5. BIR wiring
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
export BIR_AUTH_URL=https://your-basein-service
|
|
241
|
+
export BIR_CONTROL_URL=http://127.0.0.1:53411 # not discovery
|
|
242
|
+
export BIR_CONTROL_TOKEN=...
|
|
243
|
+
export BIR_REPLAY_ALLOW_SERVERS=tau2
|
|
244
|
+
BIR_REPLAY=1 bir-hooks 2>&1 | tee -a results/tau2-audit.log
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`BIR_CONTROL_URL` / `BIR_CONTROL_TOKEN` are mandatory, not convenient: the README calls
|
|
248
|
+
them the only reliable channel for an SDK session, and every episode turn is one. Discovery
|
|
249
|
+
across a few hundred harness-spawned processes is how an arm silently degrades to Tier 2.
|
|
250
|
+
|
|
251
|
+
`BIR_REPLAY_ALLOW_SERVERS=tau2` is the whole allowance. Direct execution dispatches steps on
|
|
252
|
+
connections that are already open and never reaches the permission system, so the list is
|
|
253
|
+
the blast radius.
|
|
254
|
+
|
|
255
|
+
### 5.1 The per-prompt run boundary — the structural mismatch
|
|
256
|
+
|
|
257
|
+
This is the one that will shape the results, and it is worth understanding before the first
|
|
258
|
+
run rather than after.
|
|
259
|
+
|
|
260
|
+
`onPrompt` rolls a **new run on every prompt**. In τ²-bench, every user-simulator turn is a
|
|
261
|
+
prompt. So an episode of six turns is **six runs**, not one, and matching happens per turn
|
|
262
|
+
against a library of per-turn scenarios.
|
|
263
|
+
|
|
264
|
+
What follows from that:
|
|
265
|
+
|
|
266
|
+
- **Turn 1 carries the intent** — "I want to cancel my order" — and is where a match should
|
|
267
|
+
fire. Later turns are mostly parameter supply ("it's W5678") and are short, cheap, and
|
|
268
|
+
poor matching material.
|
|
269
|
+
- **A scenario is a turn's tool chain, not an episode's.** Cross-episode reuse therefore
|
|
270
|
+
happens turn-by-turn, and the savings ledger sums over turns.
|
|
271
|
+
- **Per-run usage watermarking matters more here than anywhere.** Six runs share one
|
|
272
|
+
Claude Code transcript; summing the file gives turn 6 the cost of turns 1–6. The design
|
|
273
|
+
doc's `markTranscriptUsage` / `usageSince` delta is not an optimisation in this setting,
|
|
274
|
+
it is the difference between a real number and a fabricated one. Check `measured: true`.
|
|
275
|
+
|
|
276
|
+
If the results show matching that fires reliably on turn 1 and rarely after, that is not a
|
|
277
|
+
bug — it is the honest shape of the product against a multi-turn benchmark, and it should
|
|
278
|
+
be reported that way.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 6. Running it
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
git clone https://github.com/sierra-research/tau2-bench && cd tau2-bench
|
|
286
|
+
uv sync && cp .env.example .env # user-simulator key goes here
|
|
287
|
+
|
|
288
|
+
# smoke: one task, one trial
|
|
289
|
+
tau2 run --domain retail --agent claude_code --user-llm <model> \
|
|
290
|
+
--num-trials 1 --task-ids 0
|
|
291
|
+
|
|
292
|
+
tau2 view # read the trajectory you just produced
|
|
293
|
+
|
|
294
|
+
# baseline arm, recording only (BIR_REPLAY unset)
|
|
295
|
+
tau2 run --domain retail --agent claude_code --user-llm <model> \
|
|
296
|
+
--num-trials 4 --max-concurrency 4
|
|
297
|
+
|
|
298
|
+
# replay arm, library seeded from the passing runs above
|
|
299
|
+
BIR_REPLAY=1 tau2 run --domain retail --agent claude_code --user-llm <model> \
|
|
300
|
+
--num-trials 4 --max-concurrency 4
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
On the replay arm, `--max-concurrency 4` means several Claude Code sessions are live at
|
|
304
|
+
once, and every `claude -p` turn starts a `tau2` proxy of its own. A step bir runs itself
|
|
305
|
+
for a session — a direct replay, or the rest of a plan after a divergence — goes only to a
|
|
306
|
+
proxy known to be that session's (one of its calls was reported and joined to the
|
|
307
|
+
session), or to one not yet tied to any session while no other session is live. Otherwise
|
|
308
|
+
the step is not run (recorded as `skipped`) and the plan hands the turn to the agent
|
|
309
|
+
(a direct replay books `failed`, or `fell_back` once a step ran), rather than act on another
|
|
310
|
+
episode's copy of the domain. A direct replay starts with `run_scenario`, before
|
|
311
|
+
the turn's proxy has reported anything, so while other episodes run it hands over: measure
|
|
312
|
+
direct mode with `--max-concurrency 1`, and read hand-overs under concurrency as the
|
|
313
|
+
harness, not the scenario.
|
|
314
|
+
|
|
315
|
+
Pin the `--user-llm` model and version across all arms. It is the environment; changing it
|
|
316
|
+
changes the task, and a shifted baseline invalidates every comparison.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 7. Verification gates
|
|
321
|
+
|
|
322
|
+
Run these on `--task-ids 0`, in order. Each has a distinct failure signature, and they are
|
|
323
|
+
ordered so that the cheapest and most misleading failures are caught first.
|
|
324
|
+
|
|
325
|
+
| # | Gate | Failure looks like | Means |
|
|
326
|
+
|---|---|---|---|
|
|
327
|
+
| 1 | `mcp_servers` in `system/init` lists `tau2` as connected | score 0, no tool calls | config skipped silently |
|
|
328
|
+
| 2 | `bir doctor` green, `metadata.tier == 1` inside a harness-spawned run | recording exists but thin | Tier 2 — hooks not loading |
|
|
329
|
+
| 3 | The proxy recorded the episode's tool calls | empty step stream on a passing task | traffic bypassed bir |
|
|
330
|
+
| 4 | Reward > 0 on a task the reference solves | *every* task exactly 0.0 | the trajectory carries no tool calls (§3.1) |
|
|
331
|
+
| 5 | No duplicated side effects | reward 0, doubled refund in the env | one environment, executed twice (§4) |
|
|
332
|
+
| 6 | Zero retries across a multi-trial run | ~1 run in 3 dies as "infrastructure error" | the timestamp race (§4.2) |
|
|
333
|
+
| 7 | `Avg Cost/Conversation` is a number, not `n/a` | costs silently absent | an assistant message with `cost=None` |
|
|
334
|
+
| 8 | On the replay arm: DB=1 **and** COMMUNICATE=1 | reward 0, step ledger healthy | **§3.3** — replay produced no response text |
|
|
335
|
+
|
|
336
|
+
**Every one of these fails quietly.** That is the point of the list. Three of them we hit
|
|
337
|
+
for real, and none announced itself:
|
|
338
|
+
|
|
339
|
+
- **Gate 4** produced a clean table reading `reward 0.0000, DB match 0/1` on a task the
|
|
340
|
+
agent had solved *perfectly* — its calls matched the reference exactly. It reads like a
|
|
341
|
+
hard task, not a broken harness.
|
|
342
|
+
- **Gate 6** was absorbed by τ²'s own retry logic, so the run "succeeded" while costing 3×
|
|
343
|
+
and mixing infrastructure errors into what would look like task variance across a sweep.
|
|
344
|
+
- A fourth, not a gate but worth knowing: on Windows, npm's `claude.cmd` shim routes
|
|
345
|
+
arguments through `cmd.exe`, which re-parses them. Any user turn containing `&`, `|`,
|
|
346
|
+
`%`, `^` or a newline arrives truncated, and the agent answers *"your message came
|
|
347
|
+
through empty."* Call `claude.exe` directly and refuse to fall back to the shim.
|
|
348
|
+
|
|
349
|
+
The lesson generalises past this benchmark: a harness bug and a hard task produce the same
|
|
350
|
+
number. Read one transcript per configuration before trusting any aggregate.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 8. What to measure
|
|
355
|
+
|
|
356
|
+
Per arm, per domain: reward (mean), pass^k across trials, `$`/episode and `$`/turn from
|
|
357
|
+
`--output-format json`'s `total_cost_usd`, wall-clock per episode, and turn count.
|
|
358
|
+
|
|
359
|
+
Per replayed turn, from the audit log and the execution ledger: outcome
|
|
360
|
+
(`steered_full` / `diverged` / `fell_back` / `not_steered` / `failed`), match similarity against
|
|
361
|
+
`BIR_MIN_STEER_SIMILARITY` (default 0.92), direct vs steered mode, `deriveCostUsd` against
|
|
362
|
+
`sessionCostUsd`, and the per-step verdicts.
|
|
363
|
+
|
|
364
|
+
Two τ-specific splits worth reporting that MCPMark cannot give us:
|
|
365
|
+
|
|
366
|
+
- **Reward decomposed.** DB and COMMUNICATE separately, always. An arm where DB holds and
|
|
367
|
+
COMMUNICATE slips is a precise, fixable finding about what replay omits; the product hides
|
|
368
|
+
it completely.
|
|
369
|
+
- **Match rate by turn index.** Turn 1 versus turns 2..n (§5.1). This is the number that
|
|
370
|
+
says whether calculated replay generalises across a *task family* or only across
|
|
371
|
+
restatements of one task, and it is the question τ-bench was chosen to answer.
|
|
372
|
+
|
|
373
|
+
Then the control from [benchmark.md](benchmark.md) §7 Phase 3, which matters more here than
|
|
374
|
+
anywhere: τ-bench task families differ by *entity*, so a matcher that ignores which order
|
|
375
|
+
id the customer named will replay cheerfully against the wrong order. Build the near-miss
|
|
376
|
+
set by swapping entities between same-shape tasks — same words, different customer — and
|
|
377
|
+
report the **false replay rate**. In a retail domain, that number has an obvious real-world
|
|
378
|
+
reading, which is exactly why it should be published whatever it says.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 9. Known limits
|
|
383
|
+
|
|
384
|
+
- **The MCP wrapper is ours.** τ²-bench has no MCP interface, so the transport under test is
|
|
385
|
+
a shim we wrote. A sceptic can question it; the answer is to publish it and to run gate 3
|
|
386
|
+
and gate 5 in public. MCPMark has no such caveat, which is the other reason it is primary.
|
|
387
|
+
- **The user simulator is an LLM**, so the environment is stochastic across trials. pass^k
|
|
388
|
+
here mixes agent variance with simulator variance. Pin the simulator model and report it
|
|
389
|
+
as a limit rather than pretending the variance is all ours.
|
|
390
|
+
- **Six runs per episode** (§5.1) makes every per-run number a per-turn number. Say so in
|
|
391
|
+
the table headers; a `$`/run figure that a reader takes for `$`/task is off by ~6×.
|
|
392
|
+
- **`banking_knowledge` stays out.** Same reasoning as [benchmark.md](benchmark.md) §5 — it
|
|
393
|
+
is retrieval over a document set, not a repeatable tool chain, and there is nothing there
|
|
394
|
+
for a scenario to be.
|
|
395
|
+
- **Voice mode is out of scope.** Full-duplex needs `FullDuplexAgent` and a realtime
|
|
396
|
+
provider; Claude Code is half-duplex and so is this integration.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Sources
|
|
401
|
+
|
|
402
|
+
- τ²-bench — [repo](https://github.com/sierra-research/tau2-bench) · [agent developer guide](https://github.com/sierra-research/tau2-bench/blob/main/src/tau2/agent/README.md) · [orchestrator guide](https://github.com/sierra-research/tau2-bench/blob/main/src/tau2/orchestrator/README.md) · [evaluation docs](https://github.com/sierra-research/tau2-bench/blob/main/docs/evaluation.md)
|
|
403
|
+
- Papers — [τ²-bench (arXiv:2506.07982)](https://arxiv.org/pdf/2506.07982) · [τ-bench (arXiv:2406.12045)](https://arxiv.org/pdf/2406.12045)
|
|
404
|
+
- [Trajectory visualizer](https://taubench.com/trajectory-visualizer/?model=qwen3.5-397b-a17b-think_sierra_2026-03-02&domain=retail&task=0) · [leaderboard](https://taubench.com/leaderboard/)
|
|
405
|
+
- [Claude Code headless mode](https://code.claude.com/docs/en/headless)
|
|
406
|
+
- This repo — [benchmark.md](benchmark.md) · [calculatedReplay.md](calculatedReplay.md) · [calculatedReplayGuide.md](calculatedReplayGuide.md)
|