Exercise
Intermediate
Agents and tools60 min

Write a tool definition a model can use

Start from a vague tool nobody calls correctly and rewrite it until selection is reliable.

What it costs to run

A model with tool use. Roughly 50 short calls, which is cents on any provider.

The task

Here is a tool nobody calls correctly:

const tool = {
  name: 'lookup',
  description: 'Queries the orders service.',
  input_schema: {
    type: 'object',
    properties: {
      id: { type: 'string' },
      mode: { type: 'string' },
    },
    required: ['id'],
  },
}

Rewrite it until a model selects it for the right requests, skips it for the wrong ones, and fills its arguments without inventing them. Then prove it.

Starter material

Give the model three tools at once, because selection between one tool and nothing is not selection:

  • lookup, above.
  • search_help_articles, which searches documentation.
  • escalate_to_human, which opens a support ticket.

And ten requests to run against them. Five should be order-status questions, three should be documentation questions, and two should be neither — a refund request and a complaint about a delivery driver.

Record which tool the model picks for each, and what arguments it supplies. This is your baseline, and it is meant to be bad.

What to check

  1. Selection. For each of the ten, was the right tool chosen? Count the wrong picks in both directions: lookup called for a documentation question, and lookup skipped for an order question.
  2. Arguments. When lookup was called, was id taken from the request, or produced from nothing? Run a request that mentions no order number at all and see what arrives.
  3. Boundaries. The two out-of-scope requests should reach escalate_to_human or no tool. If they reach lookup, the description is claiming ground it does not cover.

Hints

What is the model actually reading when it chooses?

The name and the description, and nothing else. Not the implementation, not the comments, not what the endpoint is called internally. "Queries the orders service" describes the code to a maintainer; it never says what question the tool answers or when to reach for it.

Write the description as an answer to the question the model is asking: given this request, is this the thing to call?

`mode` gets filled with plausible nonsense.

Of course it does — { type: 'string' } is an invitation. If the acceptable values are knowable, enumerate them, and the model can no longer invent around the set. If they are not knowable, the parameter probably should not exist.

A closed set is the strongest constraint available at the schema level, and it costs one line.

The model calls `lookup` for a refund request.

Say what the tool does not cover. A description that names its own boundary — "does not cover refunds or returns" — does more for selection accuracy than another sentence about what it does. You are drawing the edge of the tool, and the edge is where the wrong calls happen.

It asks for an order number sometimes and invents one other times.

That is documented behaviour rather than a bug you can prompt away: a model may ask when a required parameter is missing, and it may also infer a reasonable value. Adding strict schema conformance guarantees the call matches your schema exactly and does nothing about a fabricated value of the right type.

So the fix is not in the tool definition at all. It is a check in your code that the order exists before anything acts on the call.

Solution

One rewrite that holds up
const tool = {
  name: 'get_order_status',
  description:
    'Get the current status and delivery estimate for one order by its ID. ' +
    'Use when a customer asks where an order is or when it will arrive. ' +
    'Does not cover refunds, returns or billing questions.',
  input_schema: {
    type: 'object',
    properties: {
      order_id: {
        type: 'string',
        description: 'Internal order ID, format ORD-00000. Ask if not supplied.',
      },
      include: {
        type: 'string',
        enum: ['status', 'status_and_history'],
        description: 'Whether to include the full tracking history.',
      },
    },
    required: ['order_id'],
    additionalProperties: false,
  },
}

Four changes, each doing one job. The name says what it answers. The description says when to use it and what it excludes. The format of order_id is stated, so the model knows what shape to look for in the conversation. mode became a closed set with a name that means something.

It costs more tokens than the original. Tool definitions are billed on every request whether or not anything is called, so that is a real cost — and it is the right trade, because a wrong tool call costs a round trip and an apology.

Going further

  • Re-run the ten requests and compare against your baseline. If selection did not improve, the description is still describing the implementation.
  • Add a fourth tool that overlaps deliberately, like get_delivery_estimate, and make the two descriptions draw the line between them.