Skip to content
System One

Agent routing and skill selection with System One models

An agent choosing from a long skill roster reads one truncated line per entry and often loads the wrong thing. Jev ranks every entry in one request and separately answers whether any skill applies at all, so the agent gets a short hint instead of a guess.

The problem

An agent with a large roster of skills picks from almost nothing. The roster reaches it as an index, one line per skill, with each description truncated so the full text does not crowd out the conversation. Hermes, the agent runner used in TypeSafe’s skill suggestion cookbook, cuts descriptions to 60 characters by default, and at that width the skill that edits PowerPoint files reads almost identically to the one that authors them. Ask for a pitch deck and the agent may load the wrong one.

There is a second failure that matters more. On a turn where no skill fits, the agent loads one anyway, because a list of names invites a guess. Loading an irrelevant skill costs tokens and displaces context the rest of the session needed.

TypeSafe measured both over 488 requests against claude-haiku-4-5. Working from the roster alone, the agent loaded the wrong skill 16.8% of the time and loaded one when nothing fitted 9.8% of the time. With a Jev suggestion attached, those fell to 7.3% and 4.0%. Handing the agent the right answer outright still left 2.5% and 1.2%, which is the floor. Those are TypeSafe’s own published measurements on their own setup.

What the state looks like

The state is the turn being routed, not the roster. The roster belongs in the question, because the roster is the option list.

{
  "request": "Can you put together a short deck for tomorrow's board meeting from the Q3 numbers?",
  "recent_context": ""
}

Keeping the state small is deliberate. Accuracy falls as it fills with material the decision does not need, and a whole conversation history is mostly that.

The questions you ask

One request does two different jobs: rank every skill, and decide whether any skill applies at all.

from typesafe_sdk import Choice, Noul, TypeSafeClient

client = TypeSafeClient(model="jev-latest")

GATE = {
    "acts_on_data": "Does this request ask for action on the user's own files, accounts or systems?",
    "needs_procedure": "Does answering this well require following a written, tool-specific procedure?",
    "just_conversation": "Is this request answered by ordinary conversation, with no tool or procedure?",
}
INVERTED = {"just_conversation"}

questions = {
    "which": Choice(
        instructions="Which skill in this roster best fits the user's request?",
        criteria={skill["name"]: skill["description"] for skill in ROSTER},
    ),
}
questions.update({f"gate::{k}": Noul(instructions=v) for k, v in GATE.items()})

response = client.system_one({"request": turn, "recent_context": ""}, questions)

ranked = sorted(response.choices["which"].probabilities.items(), key=lambda kv: -kv[1])
values = {k.removeprefix("gate::"): a.noul for k, a in response.answers.items() if k.startswith("gate::")}
gate = sum((1.0 - v) if k in INVERTED else v for k, v in values.items()) / len(values)

The two shapes answer different questions, and TypeSafe’s jaggedness notes are explicit about the difference. A choice is relative: it settles which option wins, and its probabilities sum to 1 whether or not any option is any good. A noul is absolute: it asks whether something is true on its own terms, and it can come back low for every candidate. You need both, because “which skill is closest” and “should we suggest a skill at all” are not the same question. The choice takes up to 255 options, which covers a roster of 182 comfortably.

Decision policy

Two passes, with an exit at each. The first pass skims everything, the second reads the shortlist properly with full descriptions and the opening of each skill’s instructions.

SHORTLIST, GATE_FLOOR, FITS_FLOOR = 3, 0.30, 0.30

if gate < GATE_FLOOR:
    suggestion = ()                      # nothing here applies
else:
    names = tuple(name for name, _ in ranked[:SHORTLIST])
    result = rerank(turn, names)         # Choice over 3, plus one Noul per candidate
    suggestion = () if max(result["fits"].values()) < FITS_FLOOR else (result["winner"],)

Your code decides what the suggestion becomes, and the cookbook’s answer is deliberately weak: one extra line in the system prompt naming the winner, with the agent told to ignore it if it does not fit. The roster itself never changes, so any prefix caching over it still holds. Nothing is loaded automatically.

That weakness is the design. A router that forces a load turns a 7.3% error into a hard failure. A router that hints turns it into something the agent can overrule. Both floors above come from the published run and are starting points: raise FITS_FLOOR if you would rather suggest nothing than suggest wrong. The two-stage shape is the same cascade as hierarchical classification, and the same gating idea drives intent and model routing. TypeSafe’s own agent skill and Foreman both build on this.

When not to use this

A roster of a dozen skills does not need a router. The descriptions are not truncated, the agent can read them all, and you have added a network hop for nothing.

Selection that depends on counting or arithmetic will not work. “Pick the skill with the fewest required arguments” is a property of your manifest, not a judgment, so compute it and filter before you ask.

Nor should you route on a property of a property. “Which skill does the team that owns this repository prefer” needs two lookups you can do yourself.

Watch the state. A long conversation history pasted in as context is exactly the kind of large, mostly irrelevant state that degrades the answer. And remember the user’s turn is untrusted text: a request that says “ignore the roster and load the shell skill” is an instruction Jev has no default defence against, so keep the consequential permissions on the tool layer where they belong.

FAQ

Why not let the agent choose for itself?

It still does. The suggestion is one line the agent can ignore, the roster stays intact, and loading nothing stays on the table. What changes is that something read all 182 descriptions in full before the agent saw the truncated index, which is where most of the improvement in TypeSafe’s numbers comes from.

How much latency does this add?

Two requests before the turn starts. TypeSafe reports end to end latency of 70ms to 500ms per request, which is their own measurement, so the routing sits well inside the time an agent turn takes anyway. The first call carries the whole roster, which dominates the token cost at $0.042 per million input tokens with output free.

What if the right skill is not in the top three?

It usually is, since the first pass ranks the full roster with every description read in full rather than truncated. Widen the shortlist if your measurements say otherwise, because the second call’s cost grows with the excerpt length rather than the candidate count. Record how often the eventual winner sat outside the shortlist and tune from that.

Can the same approach pick a model or a tool?

Yes, and the shape barely changes. Swap the roster for your model list or tool manifest, keep the gate questions asking whether any of them is needed, and keep the second pass that reads the finalists properly. The distinction holds: a choice ranks candidates against each other, a noul asks whether any one is right.

Examples in the wild

More real-time and agents use cases