Skip to content

20 · Routing a conversation, and what the router does not claim

Part II is for a team that puts the MCP tool selector into its own application: your code receives a person's turn, asks for a plan, and runs the plan's steps itself. Part I's agent does all of this for you; here you hold each piece. This chapter is what the selector decides on every turn, where it runs, and the limits every chapter of this part is read against. The selector's routing surface -- the part that reads a turn into a plan -- is called the router from here on.

Language support

The selector reads turns written in English and in Brazilian Portuguese (pt-BR). The language is chosen, never guessed from the turn: the console's Settings page (Language the selector reads), language on a plan request, or the settings.language of your vocabulary file; with none of them, English. A Portuguese turn may be typed with its accents or without them. European Portuguese is not a grammar the selector has, and a turn in any other language is unread.

What the router decides

For every turn, against your catalogue (chapter 21), the router decides four things:

  • which tool, with which arguments, and where each value came from: the turn, an answer to a question, your application's context, or an earlier call's result;
  • what to ask, where a value is missing or two tools fit equally: one fixed question, never a guess;
  • what to compute, where the turn asks for a calculation no tool answers (chapter 23);
  • what to decline: a turn that would move data somewhere no tool takes it, and a calculation that would write.

A turn is read into a plan of steps, each with the rule that decided it. A turn the router reads is decided with no model call: the rule is a rule over what your tools declare, not a judgement, and the same turn over the same catalogue and vocabulary gives the same plan, byte for byte.

Reading a turn

The shop's support desk from Part I, read turn by turn:

def show(turn, plan):
    print(f"{turn!r}: {plan.outcome}")
    for step in plan.steps:
        print(f"  {step.kind} | {step.decided_by} | {step.selection.tool or '-'} | "
              f"{dict(step.selection.arguments)} | from {dict(step.selection.sources)}")
    if plan.declined_because:
        print("  declined because:", plan.declined_because)
    if plan.guidance and plan.outcome != "run":
        print("  says:", plan.guidance.render)


with Client() as client:
    print("answered by:", client.runtime_status()["side"])
    for turn in ["show me order 4411", "charge order 4411 40.00", "charge 40.00 to order 4411",
                 "charge order 4411", "delete order 4411", "zork the frobnicator"]:
        show(turn, client.plan(turn, agent="desk", catalogue=catalogue))
answered by: local
'show me order 4411': run
  tool | tool: admission | get_order | {'order_id': '4411'} | from {'order_id': 'turn'}
'charge order 4411 40.00': run
  tool | tool: provenance | charge_card | {'amount': '40.00', 'order_id': '4411'} | from {'amount': 'turn', 'order_id': 'turn'}
'charge 40.00 to order 4411': run
  tool | tool: provenance | charge_card | {'amount': '40.00', 'order_id': '4411'} | from {'amount': 'turn', 'order_id': 'turn'}
'charge order 4411': ask
  ask | ask: incomplete | charge_card | {'order_id': '4411'} | from {'order_id': 'turn'}
  says: I can charge card -- I need the amount.
'delete order 4411': unread
  unread | unread: no tool | - | {} | from {}
  says: I can get orders, but nothing here can delete them.
'zork the frobnicator': unread
  unread | unread: no tool | - | {} | from {}
  says: I didn't recognise anything in that. I can charge card, get order, hand off, read inbox or send receipt. Which do you mean?
  • show me order 4411 is get_order, its order_id taken from the turn;
  • charge order 4411 40.00 and charge 40.00 to order 4411 are the same call: the amount and the order are told apart by what each parameter is, whatever the order of the words;
  • charge order 4411 is a question, because the charge has no amount. The plan is ask, and it says what is missing;
  • delete order 4411 is no tool's: nothing here deletes. The plan says so in a sentence you can show;
  • zork the frobnicator reads as nothing at all. The router offers what it can do instead.

Each step's decided_by names the rule. A plan that surprises you can be read back to the rule that made it, and argued with in those terms.

The router reads English and Brazilian Portuguese (pt-BR), in the one language you choose for it -- it never guesses a turn's language. With Portuguese chosen, and pedido in your vocabulary as a word for order, me mostra o pedido 4411, cancela o pedido 4411 and vc pode cancelar o pedido 4411? are read with the same rules, and answered with the same steps, as their English. The verbs need nothing from you: mostrar is show and cancelar is cancel. The nouns of an English-named catalogue do: pedido is an order in a shop and a request at a service desk, so the words your users say for your tools' nouns go in a vocabulary file for that language, which you publish or the miner learns (chapter 25). A turn in another language is unread.

A bot made in the console also reads with its own words. The bot builder derives them, in each language, from the whole dictionary for the tools you picked, and writes them beside agent.yaml in a file that vocabulary: names (Agent(vocabulary=...) for a bot declared in Python). They reach a tool only where none of your words decides; a word two tools share is asked about, never guessed; and every plan names the words it read with by their digest, derived_digest. A file changed after the builder wrote it is refused when the bot loads. The file also holds your tools' adjectives and adverbs and their kin -- owing for an unpaid invoice, for good for a permanent deletion -- which choose between the tools a turn reached and fill the parameter they name, and never reach a tool themselves.

What a turn asks, before any tool

Each answer carries form, the turn's logical form: what it asks, read from its grammar before any tool is matched. It is how a plan that surprises you can be traced back past the tools, to what the router took the turn to mean.

  • act -- what the turn asks for: inform (to be told something), act (something done), change (a thing changed), howto (how a thing is done), meaning (what a word means), give (to be given something, as can I get a latte), reply (an answer to the question the bot asked), or none;
  • predicate -- the verb it asks with;
  • roles -- what its words play, each with its text: the thing it is about, a pointed one (this, those), a qualifier (papers that share an author), a destination (to ana@example.com), a quantity and a time;
  • polarity -- affirmative, or negated where the request is negated;
  • tense -- present, past or future;
  • frame -- question, howto, ordering (a request put as one: can I get, quero), or none.

A turn that asks what was done -- did tom send me an email?, a question in the past -- is never a write: the router calls no tool that changes anything for it.

Where it runs

The router runs where your turns are, never on our servers: in the licensed runtime the stalwart-agents package carries, on your machine, under your licence (chapter 38). Client() uses it wherever a licence is active.

client.runtime_status() says whether the runtime answers (local, or none with what to set up). The hosted API compiles your rules, holds your vocabulary, and counts what you use: it never sees a turn, an argument or a result. Where the runtime cannot answer, a plan raises SelectorUnavailableError, naming what to set up, and nothing is decided in its place.

The SDK carries the router's surface, not its logic. Client.plan, Session.read and the rest hand the turn to the runtime and return its answer as data: no Python in the package reads a word of a turn. What you hold is a TurnPlan (chapter 22).

A turn the router cannot read

Every plan carries guidance: the outcome, the evidence for it, and the one move that would change it. The outcomes:

Outcome What it means
resolved one tool, every value it needs
incomplete one tool, a value missing: the plan asks for it
ambiguous two or more tools fit equally: the plan asks which
unmet_object the action is one a tool does, on nothing this catalogue has
unmet_action the thing is one a tool acts on, by no action it does
unread nothing in the turn is read

And the next move, next: Supply (give the missing value), Choose (pick between the tools named), Rephrase (say it again, once), Defer (the host's setting decides) or None (nothing changes it: the catalogue cannot). render is the sentence to show the person.

A turn nothing reads is first met with one request to rephrase it. After that, your setting for unread turns, on_unread, decides: refuse it, hand it to your model, ask the person whether the model may have it, or answer it from your documents (chapter 44). Where you set nothing, each conversation takes the setting on your console's Settings page at its first turn, or refuse.

What the router does not claim

These are the limits every chapter of Part II is read against.

  • A declined plan is never handed to a model, whatever on_unread says. A turn that asks to send data somewhere no tool takes it is declined whole, and no step of it runs.
  • A turn handed back to you is outside what the router decided. Where your setting gives an unread turn to your model, the model chooses what to do with it. Every tool call it then makes is still checked (chapter 24), but which tool the model picked is not a decision of the router's.
  • It decides by what your tools declare. A tool whose name and description say something other than what it does is chosen by what they say. The router does not read your tools' code.
  • It does not prove that a plan is the right one. It reads the turn by fixed rules, and a rule can read a turn differently from the person who typed it. That is why every step names its rule, and why the bug hunter (chapter 29) checks that your guardrails route every tool through it, not that it chose well.
  • The vocabulary moves the plans. A word your users' feedback taught the router (chapter 25) changes how the next turn is read. Every plan names the vocabulary it was read with (vocabulary_digest), and a version can be reverted.