Your First Tool
Ask the dispatcher what a brake bleed costs right now, and you'll get unpredictable results. You might get some follow up questions, or you might get a confident number that is completely made up. The persona told it to "quote in real dollars from the catalog," but there's no catalog it can actually read. The language model fills the gap with a guess.
That's the moment a tool becomes important. A tool is the agent's hands: a typed function it can call to go get a real answer instead of inventing one. What the shop needs is a catalog of services and prices, and a door the model can reach through to read it.
We'll drop in a ready-made catalog (hand-typing a price list teaches nothing), then build the door ourselves. In eve that door is one file, and the file's name is the tool's name.
Outcome
The dispatcher quotes a real price from the catalog by calling a lookup_service tool you wrote, instead of guessing.
Hands-on exercise
Watch the wrong version first. With no tool, ask for a price:
you › what's a hydraulic brake bleed run me?
dispatcher › I'm having a little trouble pulling up the service catalog right now."I'd estimate." "Usually around." That's a guess dressed up as an answer, and the price isn't ours. Now give it the real thing.
First, give the shop a catalog to read from. Instead of hand-typing a price list, drop in a ready-made one. In a new terminal**, **same directory as the project, run:
curl -f --create-dirs -o agent/lib/shop.ts https://raw.githubusercontent.com/vercel-labs/bike-shop-agent/main/agent/lib/shop.tsThat file is deliberately minimal: an in-memory array of services with prices and time estimates, plus a couple of helpers (listServices, formatUsd) to read it. It's the one piece you don't hand-build, because a hard-coded price list isn't the interesting part. Reaching it through a typed tool is. Swap it for a real booking system or warehouse later, and the tool you're about to write won't notice the difference.
Now create agent/tools/lookup_service.ts. Three pieces make a tool:
- A
descriptionthe model reads to decide when to call it. Write it for the model: say what it does and when to reach for it. - An
inputSchema, a Zod object that both validates the call and types the argument you get inexecute. Here, an optionalqueryto filter the catalog. - An
executefunction, your code, running in the app runtime with full access toagent/lib/.
The catalog you just dropped in does the data work. Import listServices and formatUsd from ../lib/shop.js (note the .js extension, that's how Node's module resolution wants the import written even though the file is .ts).
You won't register this tool anywhere. The file lives at agent/tools/lookup_service.ts, so the model sees a tool named lookup_service. Rename the file and you rename the tool. That's the filesystem-first idea in one move: the directory is the configuration.
Map the catalog rows down to the fields the model actually needs, and turn cents into dollars with formatUsd so the agent never has to do math on a price.
Try It
Restart the dev server and ask the same question:
npx eve devyou › what's a hydraulic brake bleed run me?
⚙ lookup_service { query: "brake" }
↳ [{ id: "brake-bleed", name: "Hydraulic Brake Bleed", description: "Flush the old fluid and bleed the brakes back to a firm lever.", price: "$55.00", estMinutes: 40 }]
dispatcher › A Hydraulic Brake Bleed is $55 and takes about 40 minutes. That
flushes the old fluid and bleeds the brakes back to a firm lever, which is
exactly what a spongy lever usually needs. Want me to check for an opening?Watch the loop in the TUI: the model emitted a lookup_service call, eve ran your execute, the real row came back as a tool result, and then the model answered, with the catalog's number, not its own. You wrote the tool; eve drove the loop.
Notice you never told it to search for "brake." The model read the customer's question, decided lookup_service fit, and picked the query argument on its own. Your job was to describe the tool well enough that it knew when and how to use it.
Seeing ⚙ lookup_service with no result, or a type error on the import? Two usual suspects: the import path needs the .js extension (../lib/shop.js), and the file must export default the defineTool(...) call. Run eve info to confirm eve discovered the tool, you should see lookup_service in the tools list.
Done-When
agent/tools/lookup_service.tsdefault-exportsdefineToolwith adescriptionand ZodinputSchema.eve infolistslookup_serviceamong the tools.- Asking for a price triggers a visible
lookup_servicecall in the TUI. - The quoted number matches the catalog in
agent/lib/shop.ts, not an invented estimate.
Solution
import { defineTool } from "eve/tools";
import { z } from "zod";
import { listServices, formatUsd } from "../lib/shop.js";
export default defineTool({
description:
"Look up the repair services the shop offers, with prices and time estimates. " +
"Pass a query to filter (e.g. 'brake', 'wheel'); omit it to list everything.",
inputSchema: z.object({
query: z.string().optional().describe("Optional keyword to filter services."),
}),
async execute({ query }) {
return listServices(query).map((s) => ({
id: s.id,
name: s.name,
description: s.description,
price: formatUsd(s.priceCents),
estMinutes: s.estMinutes,
}));
},
});The inputSchema does double duty: it validates whatever the model passes, and it types the { query } you destructure in execute. That's why Zod 4 matters here, eve uses the schema as the model-facing contract, and a Zod 3 schema won't satisfy it. The catalog in agent/lib/shop.ts is the file you curled in, kept deliberately dumb because a hard-coded list of services isn't the interesting part; reaching it through a typed tool is.
Was this helpful?