Skip to main content
Agents choose skills from an index that puts every skill, with its description truncated, into the system message. This degrades skill selection and induces context rot for the rest of the session. We add two TypeSafe requests per turn, one to rank skills and one to verify the choice. They cut wrong skill loads by half and needless ones by nearly two thirds. An agent with a large skill roster makes its choice on almost no information. The roster reaches it as an index: one line per skill, with the description truncated so the full text doesn’t crowd out the conversation. Hermes, the agent harness used here, cuts it to 60 characters by default. At that width, the Office skill that edits .pptx files and the finance skill that builds decks both read as PowerPoint tools. Ask for changes to an existing deck and the agent may load the one that builds new ones. On a turn where no skill fits at all, it may still load one anyway, because a list of names invites a guess. We use progressive disclosure, reading all 194 skills cheaply and then reading three of them in detail. Two TypeSafe requests go in front of the decision on which skill to load, if any. The first ranks every skill in the roster against the user’s turn and answers whether the turn needs a skill at all. The second re-reads only the top three, now with each skill’s full description and the opening of its instructions, and is free to reject all of them. The winner’s name goes into one extra line of the agent’s system prompt for that turn:
The agent keeps its full index and its own judgement, and that one line only tells it which entry to look at first. Over 452 requests against claude-haiku-4-5-20251001, using skills from the Hermes roster: The third row is the floor: an agent handed the right skill still does not always load it, and no selection method gets below that. You end up with a suggest() function that returns at most one skill name, a suggestion_block() that wraps it for the system prompt, and the harness that produced the table above, ready to point at your own roster.

Setup

  • Install the TypeSafe client, the Anthropic client, and the shared cookbook helpers.
  • Set a TypeSafe API key, and an Anthropic key for the agent being measured.
Note: this page’s code blocks form one script. To run it, put them in one file in the order they appear.

Caching results

hermes_roster.json, requests.json and the answer cache json_cache.json are in typesafe-public-examples. Put them in the folder you run the script from. We cache each call’s result in json_cache.json, keyed on its inputs, so re-running replays the numbers below instead of calling either API. Delete json_cache.json to run live. The published run used jev-1.14.0 and claude-haiku-4-5-20251001, rendered 2026-10-30.

Step 1: load the roster

hermes_roster.json holds the 194 skills of NousResearch/hermes-agent at one pinned commit. Each record holds a skill’s name and category, the description as the index shows it, the full description, and the opening of its SKILL.md. The skills’ licences are in DATA_LICENSE.md. The index below, and the instructions above it in the prompt, are copied from Hermes.

Step 2: score the agent on its own

requests.json holds 452 single-turn requests, 280 of them covered by exactly one skill and the other 172 covered by nothing. The covered requests were written by Claude Sonnet 5 from each skill’s own SKILL.md, so the labels are trustworthy and the requests are easier than the ones users send. The 172 uncovered ones were all written to punish guessing: 84 everyday requests, 42 technical questions no skill serves (explain what a monad is), and 46 that ask for something specific the roster has no skill for, like post this to Mastodon on a roster that covers X and nothing else. Scoring reads the agent’s first response only. Both numbers are error rates, so lower is better on each:
  • wrong load: of the covered requests, the share where the first skill_view call was not the covering skill. A turn that loaded nothing at all counts as a miss.
  • needless load: of the uncovered requests, the share where the agent called skill_view at all.
The suggestion goes in its own block of the system prompt, after the roster rather than inside it, so the roster text is identical on every turn to maintain prefix caching. The agent has a minimal set of tools, including skill_view to load a skill using a free-text name. The name must match the skill exactly for a correct load.
The agent runs first with nothing but its roster, the way it works today. Its two error rates are the baseline.
Wrong loads land in the right skill’s own category far more often than chance would put them there, so the hard part is telling a few lookalikes apart. The agent is already looking in roughly the right place.

Step 3: rank the whole roster

One request carries two kinds of question:
  • which is a Choice question over all 194 skill names, with the index description as each option’s criteria (the same text the agent itself gets). Its probabilities are the ranking.
  • three Noul questions about the request, each asking in a different way whether the request wants an action taken rather than an explanation given. We average the three, flipping prose_suffices because a yes there means no skill is needed.
  • one Noul question, named: does the request ask for a specific named tool, format, template, or method?
Both kinds go out in one request, so the ranking and the check cost one round trip. Write the three action questions to ask whether an action is wanted. A question about subject matter will not separate explain what a monad is from a request that needs a skill, since both are software. A turn can need a skill for either of two reasons: it wants an action taken, or it names a method a skill owns, such as a meme template or a Suno prompt. The three action questions measure the first reason, so we average them. named measures the second, so it opens the gate on its own instead of joining the average, where the three action answers would outvote it. We suggest nothing only when both reasons come back low. One Choice question holds a roster this size comfortably. A few times larger, you would split it into chunks, rank each chunk, and then rerank the chunk winners as in step 4.
The Notes.app request is unambiguous, and its top option is the right one. Nothing a ranking can do will save the Mastodon request. The three action questions say a skill is wanted, because posting to an account is an action. With a skill for posting to X and none for Mastodon, the closest skill wins anyway. The meme request is the second reason at work. Making a meme acts on nothing of the user’s, so the action questions come back low, but the request names a meme template and named opens the gate. Its top option is the meme skill. That leaves the deck. Both leaders are .pptx skills, and with only 60 characters of description each, the Choice question over all 194 skills puts the deck-building skill ahead of the Office skill, for a request to edit an existing deck.

Step 4: rerank the top three

Three options leave room for the full description plus the opening of each skill’s own SKILL.md, so the second request puts the same question to better evidence:
  • which is a Choice question over the shortlist, with that longer text as each option’s criteria.
  • fits::{name} is one Noul question per candidate: does this skill do the specific thing the request asks for? Each is answered on its own, so they can all come back low, and when they do the shortlist is dropped.
The Re-ranking cookbook uses the same shortlist-then-rescore step on legal passages, with BM25 as the cheap first pass.
The two .pptx skills separate once each one brings its own text: the deck request flips to the Office skill, which edits existing files. The fits nouls score both .pptx skills almost the same, because both can do something with a deck. They decide a different thing from the Choice question, which settles which skill, while the nouls settle whether to say anything at all. The Line-by-line search cookbook pairs a Choice question and a Noul question the same way, to tell whether a document answers a query at all. The Mastodon request stops at the second pass. None of its three near-misses does what it asks, so even its best fits noul comes back under the threshold and nothing is suggested. The whole recipe is two requests and two thresholds, and it returns at most one skill name. To point it at your own roster, replace hermes_roster.json. Every question above reads name, description, description_full, and body out of that file, and nothing else in suggest() is specific to Hermes.

Step 5: measure the suggestion

Each of the 452 requests goes to the agent three times, one measured turn each. The runs differ only in what the agent is told: No real system knows the covering skill in advance, so the third arm sets the floor the other two are measured against. The wording of that suggestion is doing two jobs. It says the suggestion can be ignored, because pushing harder wins compliance on wrong suggestions too, and a wrong one is worse than none. And a turn with nothing to suggest still sends a sentence saying so; sending nothing at all would leave the roster’s own “err on the side of loading” instruction unopposed.
The suggestion fixes many more requests than it breaks, but it does break some the agent had right on its own: a confident wrong suggestion pulls the agent off its own right answer. That is the price of putting one in front of the turn.
output

What the results show

  • Wrong loads fell from 15.7% to 7.9%, closing more than half the gap between guessing from a truncated index and being handed the answer. Needless loads fell from 14.0% to 5.2%, closing nearly all of theirs.
  • Some requests the agent had right on its own come back wrong once a suggestion is attached: of the 280 covered requests, the suggestion broke 7 and fixed 29.

Open it in the playground

Build a playground link for the deck request from step 4, using each candidate’s full description and body excerpt as its criteria.
Open the shortlist + questions in the TypeSafe playground →

What’s next

The same shape shows up elsewhere: Intent Routing for routing to a handler rather than a skill, and Confidence for picking the two thresholds.