Lab: redesign a tool
By the end: Rewrite a poorly designed tool declaration so a model can use it correctly first time.
Everything in this module was preparation for this. Below is a real shape of tool that shows up constantly: it mirrors an internal service, returns whatever the database returned, and fails silently in the one case that matters.
{
"name": "orderQueryV2",
"description": "Queries the order service.",
"input_schema": {
"type": "object",
"properties": {
"q": { "type": "string" },
"mode": { "type": "string" },
"limit":{ "type": "integer" }
},
"required": ["q", "mode"]
}
}
// Returns: the raw ORM row — 40 fields, internal enum codes,
// nested nulls. On no match it returns [] with no explanation.
// "mode" must be one of: id, email, name_postcode. Nobody told the model that.Your turn
You own the Meridian triage agent. It keeps calling this tool with mode values that do not exist, passing ticket numbers as order ids, and giving up when the result is empty. Redesign the declaration and the result so those three failures become impossible or self-correcting.
Produce
- A rewritten tool declaration: name, description, and a full input schema with a description on every field.
- A sketch of the result shape the tool should return on success — the fields, not the values.
- The exact text of the error returned when nothing matches.
Judge your own work against this
- Does the name say what job it does, rather than which service it calls?
- Does the description say when NOT to use it, and name the tool to use instead?
- Can the model produce an invalid mode? If the schema still allows a free-text mode, the mistake is still available.
- Does the result carry only what the next decision needs, with names a stranger could read?
- Does the empty-result message name the failure, give the fact that makes it actionable, and suggest a next move — without a stack trace or an internal field name?
Stuck? One hint
Three of the failures disappear if the mode stops being a string the model has to remember. What replaces it?
Show one good answer — after you have written yours
{
"name": "search_orders",
"description": "Find a customer's orders when you do not have an order id. Search by email, or by name together with postcode. Returns at most 20 matches, newest first. If you already have an eight-digit order id, use get_order instead — it is exact and cheaper.",
"input_schema": {
"type": "object",
"properties": {
"email": { "type": "string", "description": "Customer email. Exact match, case-insensitive." },
"name": { "type": "string", "description": "Full name. Must be given together with postcode." },
"postcode": { "type": "string", "description": "Delivery postcode. Spaces ignored." },
"since": { "type": "string", "description": "ISO 8601 date. Omit for all time." }
},
"required": []
}
}
// The mode is gone. The model supplies what it HAS, and the tool works out
// how to search. An invalid mode is no longer expressible.
// Success returns, per order:
// { id, status, placed_on, shipped_on, carrier, tracking_url,
// customer_email, items: [{ sku, name, qty }] }
// No match returns:
// "NO_MATCH: no orders for that search. Check the email is the one on the
// order rather than the one that sent the ticket. If you have an
// eight-digit order id, use get_order."End of module 3

