A command-line experiment: given a task in plain text and a catalog of 40 tools described by one-line snippets, can Jev pick the right tool (and a useful top-3) from the snippets alone, how does that compare with a deterministic lexical ranker, and how many context bytes does an agent save by loading only the chosen tools' full schemas instead of all of them?
What it is not. A routing pick is evidence about which tool a task is asking for; it is not authorization to run anything, and nothing here executes a tool. The catalog and tasks are synthetic (fixtures/tool-router/), written for this experiment and not taken from any real repository or service. Mock-mode numbers exercise the harness and say nothing about Jev. This is independent community work in the community playground, not an official TypeSafe product, and the benchmark is 30 tasks, not a capability claim. It is unrelated to the /tool-router workspace, which routes one graph edge at a time; see tool-router.md.
One request per task, one choice question, pinned to jev-1.13.0 (the release jev-latest resolved to on docs.typesafe.ai/models when this was written; the alias is never sent so every result file names the exact model).
{
"model": "jev-1.13.0",
"state": { "task": "Text Jordan that the deploy finished." },
"questions": {
"tool": {
"type": "choice",
"instructions": "Choose the one tool whose description best fits the task in state.task. ... Choose none when no listed tool does what the task asks ... Selecting a tool is routing evidence only; it is not authorization to run it.",
"criteria": {
"read_file": "Read the full text of one file at a workspace path.",
"...": "... 38 more name: snippet pairs ...",
"send_sms": "Send a short text message to a phone number.",
"none": "No listed tool fits this task, or the task is not a tool request."
}
}
}
}Jev sees only name and snippet for each tool; full schemas are never in the payload (a test asserts the serialized request contains no properties). The full instructions are TOOL_INSTRUCTIONS in lib/tool-router/route.ts; docs/tool-router-results.json carries a complete sample request.
- Single stage. Choice accepts up to 255 options, so 40 tools plus
nonefit in one question.planRoutereportssinglefor this catalog. - Two stage. When tools +
nonewould exceed 255, the router asks a category question first (each option lists that category's tool names), then a tool question restricted to the chosen category. Results recordpath: "two-stage"andstageCategory. This path is covered by tests with a 300-tool synthetic catalog and was not needed for the benchmark. - Closed set, no default.
choicemust be one of the offered ids. An invented id, a non-choice answer, a provider error, a timeout, or cancellation yieldsunavailablewithtop1: nulland an emptytop3; the router never falls back to an argmax or a first option. noneis a choice.none: trueonly when Jev selected thenoneoption.
top3 is the three tool options with the highest probabilities from the same choice answer, with Jev's choice kept first and none excluded. No score questions are sent.
Reason: the choice answer already returns the full probability distribution across every option, so a ranking costs zero extra questions. A score question rates one item on a 2 to 10 level ordered scale and returns a weighted mean level; ranking 40 tools that way would mean 40 score questions per task (one per tool), and each would measure absolute fit in isolation rather than preference among the options. That is neither cheap nor the right shape. Observed in the live run below: the provider reports probabilities at 0.01 granularity and concentrated all mass on one option for most tasks, so top3 usually had a single entry. Caveat: probabilities near the floor of the distribution are a weak ordering signal, so read top-3 accuracy as "the expected tool got non-trivial mass", not as a calibrated second and third pick.
rankLexically in lib/tool-router/baseline.ts is a BM25-lite ranker over the same name + snippet text Jev sees: lowercase, split on non-alphanumerics, drop a short stopword list, strip a trailing s, then per-term idf times saturating term frequency (k1 = 1.2, b = 0.75). Ties break by name, so it is fully deterministic. It answers none only when no query term overlaps any tool, which is deliberately naive.
Accuracy is over the 25 tasks whose expected answer is a tool. Top-1 is top1 === expected; Top-1 (acceptable) also counts any tool in the task's acceptable list (two tasks list a second acceptable tool); Top-3 is expected in top3. An unavailable or none result counts as a miss on all three. None precision/recall treat none as the positive class over all 30 tasks; unavailable results are neither a none prediction nor a tool pick. Unavailable counts tasks with no usable answer. Mean Jev ms is wall-clock per task over available results, including transport overhead.
Context bytes use compact JSON byte lengths from the fixtures: "all schemas" is every {name, schema}; "snippets only" is every {name, snippet} (the router's own input); "snippets + top-k" adds the top-k tools' schemas per task (nothing extra for none, all schemas when unavailable, since a failed router leaves the agent loading everything), averaged over the 30 tasks.
pnpm exec tsx scripts/tool-router-bench.ts # mock, writes docs/tool-router-results.json
op run --env-file=.env.1password -- pnpm exec tsx scripts/tool-router-bench.ts --live --output docs/tool-router-results.live.json
pnpm exec tsx --test tests/tool-router-bench.test.tsLive mode needs TYPESAFE_API_KEY in the environment, sends one request per task (30 total, no retries, sequential, 50 s timeout each), and exits with status 1 if any task came back unavailable so a partial run is never mistaken for a complete one. The result JSON records the requested model, every model id the provider reported, token usage, per-task picks with probabilities, and a sample request.
Mock transport (lib/tool-router/mock.ts) turns lexical overlap into a probability distribution with a fixed weight on none. These tables only show the harness working; the mock and the baseline share a vocabulary, so their agreement is expected. Source: docs/tool-router-results.json.
| Category | Tasks | Top-1 | Top-1 (acceptable) | Top-3 | None precision | None recall | Unavailable |
|---|---|---|---|---|---|---|---|
| calendar | 3 | 33.3% (1/3) | 33.3% (1/3) | 33.3% (1/3) | n/a | n/a | 0 |
| code | 3 | 66.7% (2/3) | 66.7% (2/3) | 100.0% (3/3) | n/a | n/a | 0 |
| data | 3 | 33.3% (1/3) | 33.3% (1/3) | 66.7% (2/3) | n/a | n/a | 0 |
| 3 | 33.3% (1/3) | 33.3% (1/3) | 33.3% (1/3) | n/a | n/a | 0 | |
| files | 3 | 33.3% (1/3) | 33.3% (1/3) | 66.7% (2/3) | n/a | n/a | 0 |
| notify | 4 | 75.0% (3/4) | 75.0% (3/4) | 75.0% (3/4) | n/a | n/a | 0 |
| tickets | 3 | 0.0% (0/3) | 0.0% (0/3) | 66.7% (2/3) | n/a | n/a | 0 |
| web | 3 | 0.0% (0/3) | 0.0% (0/3) | 33.3% (1/3) | n/a | n/a | 0 |
| none | 5 | n/a | n/a | n/a | 100.0% (4/4) | 80.0% (4/5) | 0 |
| Total | 30 | 36.0% (9/25) | 36.0% (9/25) | 60.0% (15/25) | 100.0% (4/4) | 80.0% (4/5) | 0 |
| Category | Tasks | Top-1 | Top-1 (acceptable) | Top-3 | None precision | None recall | Unavailable | Mean Jev ms |
|---|---|---|---|---|---|---|---|---|
| calendar | 3 | 33.3% (1/3) | 33.3% (1/3) | 66.7% (2/3) | n/a | n/a | 0 | 33 |
| code | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 0 |
| data | 3 | 66.7% (2/3) | 66.7% (2/3) | 66.7% (2/3) | n/a | n/a | 0 | 4 |
| 3 | 0.0% (0/3) | 0.0% (0/3) | 33.3% (1/3) | n/a | n/a | 0 | 2 | |
| files | 3 | 33.3% (1/3) | 33.3% (1/3) | 100.0% (3/3) | n/a | n/a | 0 | 0 |
| notify | 4 | 50.0% (2/4) | 50.0% (2/4) | 50.0% (2/4) | n/a | n/a | 0 | 0 |
| tickets | 3 | 33.3% (1/3) | 33.3% (1/3) | 66.7% (2/3) | n/a | n/a | 0 | 1 |
| web | 3 | 0.0% (0/3) | 0.0% (0/3) | 33.3% (1/3) | 0.0% (0/1) | n/a | 0 | 0 |
| none | 5 | n/a | n/a | n/a | 100.0% (4/4) | 80.0% (4/5) | 0 | 0 |
| Total | 30 | 40.0% (10/25) | 40.0% (10/25) | 64.0% (16/25) | 80.0% (4/5) | 80.0% (4/5) | 0 | 4 |
| Strategy | Bytes | Of all schemas |
|---|---|---|
| All 40 full schemas | 19000 | 100.0% |
| Snippets only (router input) | 4010 | 21.1% |
| Snippets + baseline top-3 schemas | 5067 | 26.7% |
| Snippets + baseline top-1 schema | 4419 | 23.3% |
| Snippets + Jev top-3 schemas | 5052 | 26.6% |
| Snippets + Jev top-1 schema | 4402 | 23.2% |
One run on 2026-09-22 (started 08:41:52 UTC, finished 08:42:01 UTC) with model jev-1.13.0 requested and jev-1.13.0 reported by the provider on every response. 30 requests, 43,251 input and 10,869 output tokens, 0 unavailable. Source: docs/tool-router-results.live.json, run with op run --env-file=.env.1password -- pnpm exec tsx scripts/tool-router-bench.ts --live --output docs/tool-router-results.live.json.
| Category | Tasks | Top-1 | Top-1 (acceptable) | Top-3 | None precision | None recall | Unavailable |
|---|---|---|---|---|---|---|---|
| calendar | 3 | 33.3% (1/3) | 33.3% (1/3) | 33.3% (1/3) | n/a | n/a | 0 |
| code | 3 | 66.7% (2/3) | 66.7% (2/3) | 100.0% (3/3) | n/a | n/a | 0 |
| data | 3 | 33.3% (1/3) | 33.3% (1/3) | 66.7% (2/3) | n/a | n/a | 0 |
| 3 | 33.3% (1/3) | 33.3% (1/3) | 33.3% (1/3) | n/a | n/a | 0 | |
| files | 3 | 33.3% (1/3) | 33.3% (1/3) | 66.7% (2/3) | n/a | n/a | 0 |
| notify | 4 | 75.0% (3/4) | 75.0% (3/4) | 75.0% (3/4) | n/a | n/a | 0 |
| tickets | 3 | 0.0% (0/3) | 0.0% (0/3) | 66.7% (2/3) | n/a | n/a | 0 |
| web | 3 | 0.0% (0/3) | 0.0% (0/3) | 33.3% (1/3) | n/a | n/a | 0 |
| none | 5 | n/a | n/a | n/a | 100.0% (4/4) | 80.0% (4/5) | 0 |
| Total | 30 | 36.0% (9/25) | 36.0% (9/25) | 60.0% (15/25) | 100.0% (4/4) | 80.0% (4/5) | 0 |
| Category | Tasks | Top-1 | Top-1 (acceptable) | Top-3 | None precision | None recall | Unavailable | Mean Jev ms |
|---|---|---|---|---|---|---|---|---|
| calendar | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 228 |
| code | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 258 |
| data | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 332 |
| 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 246 | |
| files | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 402 |
| notify | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | n/a | n/a | 0 | 235 |
| tickets | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 453 |
| web | 3 | 100.0% (3/3) | 100.0% (3/3) | 100.0% (3/3) | n/a | n/a | 0 | 191 |
| none | 5 | n/a | n/a | n/a | 100.0% (5/5) | 100.0% (5/5) | 0 | 225 |
| Total | 30 | 100.0% (25/25) | 100.0% (25/25) | 100.0% (25/25) | 100.0% (5/5) | 100.0% (5/5) | 0 | 280 |
| Strategy | Bytes | Of all schemas |
|---|---|---|
| All 40 full schemas | 19000 | 100.0% |
| Snippets only (router input) | 4010 | 21.1% |
| Snippets + baseline top-3 schemas | 5067 | 26.7% |
| Snippets + baseline top-1 schema | 4419 | 23.3% |
| Snippets + Jev top-3 schemas | 4511 | 23.7% |
| Snippets + Jev top-1 schema | 4433 | 23.3% |
Reading these tables:
- Jev picked the expected tool on all 25 tool tasks and
noneon all 5 none tasks, including every distractor pair (search_inboxvssearch_tickets,create_eventvscreate_reminder,send_smsvssend_push_notification,export_csvvsimport_csv,format_codevslint_code,fetch_urlvsscreenshot_page) and the prompt-injection-shaped task, which went tonone. The baseline got 9 of 25 and answerednonefor 4 of 5. This is one run over 30 synthetic tasks with an easy, clean catalog; it shows that snippet-only routing works on this fixture, not a general accuracy figure. - Top-3 is effectively top-1 here. The provider reports probabilities at 0.01 granularity and put all mass on a single option for 23 of 30 tasks, so
top3had one entry for 23 tasks, two for 3, three for 1, and none for the 3nonetasks whose distribution wasnone: 1.0. Top-3 accuracy therefore adds no information on this catalog, and "snippets + Jev top-3 schemas" is close to "snippets + Jev top-1 schema" for the same reason. A catalog with closer alternatives, or a task set written to be ambiguous, would be needed to evaluate a real second and third pick. Reportedconfidenceranged from 0.73 (t16,run_aggregation0.74 vsquery_sql0.25, both acceptable) to 1.0, mean 0.97. - Context bytes: loading every schema costs 19,000 bytes per task; the router's own input (name + snippet for all 40 tools) is 4,010 bytes, and snippets plus the routed schema comes to 4,433 bytes on average, 23.3% of loading everything. The router's input is the dominant cost of that 23.3%, so the saving depends on schemas being much larger than snippets, which they are here by design (246 to 662 bytes vs 32 to 88 characters). The 43,251 input tokens across 30 requests (about 1,440 per request) are the price of sending all 41 options each time.
- Mean latency was 280 ms wall-clock per task from this machine, including the playground's server transport and rate-limit bookkeeping; no request retried or timed out.
lib/tool-router/types.ts,catalog.ts,baseline.ts,route.ts,mock.ts,metrics.ts: pure TypeScript, no React, no network; the transport is injected.fixtures/tool-router/catalog.json: 40 tools in 8 categories with schemas of 246 to 662 bytes, 6 near-duplicate distractor pairs.fixtures/tool-router/tasks.json: 30 tasks, 5 expectingnone(including one prompt-injection-shaped task).scripts/tool-router-bench.ts,tests/tool-router-bench.test.ts.
Credits: this experiment lives in the community playground that extends @nickthompson480's original TypeSafe AI playground under the MIT license. It is not affiliated with or endorsed by TypeSafe AI.