Skip to content
bits
Give your model tools · Lesson 9 of 9 · 25 min

Checkpoint: Klippy with tools

Desk: best at a computer, with a terminal

Verified wrangler 4.157.0agents 0.33.0ai 7.0.162zod 4.6.7remix 3.1.0

Start from b-08 on GitHub. Starting here, or behind? Get the code at that tag:

Shell
git clone https://github.com/ryanyogan/klippy
cd klippy
git checkout b-08
Tutor:

Klippy has five tools now: the weather, the time, saving and listing notes, and reading a link. Time to join them up, fix the one bug a full tour finds, ship it, and let bits check it. Then a last exercise on the idea under all of it: descriptions.

Tutor:

1. The bug a tour finds

Ask "Should I take an umbrella in Dublin today?" and the model may call getTime first (what's "today" there?), then getWeather in a second step. With isStepCount(2), Klippy stops right there, after two tool steps and before any answer, and the reply is "answer":"". We hit exactly this while recording. Fix it in apps/agent/src/ask.ts:

// Up to two rounds of tools (say the time, then the weather), then the answer. Then stop, whatever happens.
stopWhen: isStepCount(3),
TypeScriptUTF-8at b-09● lines 1–2: apps/agent/src/ask.ts
Tutor:

Three steps: up to two rounds of tools, then the answer. Still a hard stop. And if a question needs more than that, say so instead of sending an empty string:

// Which model answered: Claude, or the fallback.
// Stopped mid-way (the last step was still a tool call)? Then there's no text: say so instead of sending "".
const answer = text || "Sorry, I ran out of steps before I could answer. Try asking one thing at a time.";
return { answer, model: providerMetadata?.cloudflare?.model };
TypeScriptUTF-8at b-09● lines 2–4: apps/agent/src/ask.ts
Tutor:

text is empty when the last step was still a tool call. An honest "I ran out of steps" beats a blank reply. Course C is about loops and how many steps to allow; for now, three and a clear message.

Tutor:

2. Tell bits where you are

TAG in apps/agent/src/check.ts says which lesson your code is at. Move it to this checkpoint:

// The lesson tag this code is at. bits reads it to see how far your Klippy has come.
export const TAG = 'b-09';
TypeScriptUTF-8at b-09● lines 2–2: apps/agent/src/check.ts
Tutor:

3. Run every test

test/check.spec.ts checks the tag too, so copy apps/agent/test/ from tag b-09 first (or change 'a-05' to 'b-09' in that file). Then:

Shell
pnpm typecheck
pnpm test
recorded output
pnpm test
apps/agent test$ vitest run
apps/agent test: Test Files 9 passed (9)
apps/agent test: Tests 35 passed (35)
apps/agent test: Done
Tutor:

Plus the driver's 24 tests in packages/data-table-d1. If any fail, compare the file with tag b-09 before you deploy.

Tutor:

4. The live database

Your live Worker gets its own D1 database, separate from the local copy. It's empty, so apply the migration there too. In apps/agent:

Shell
pnpm wrangler d1 migrations apply klippy-db --remote
recorded output
pnpm wrangler d1 migrations apply klippy-db --remote
Migrations to be applied:
│ name │
│ 0001_notes.sql │
? About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue?
🤖 Using fallback value in non-interactive context: yes
🌀 Executing on remote database klippy-db (e51fc5ce-ea1d-48b0-afd7-e0af5cd1672f):
🚣 Executed 2 commands in 0.97ms
│ name │ status │
│ 0001_notes.sql │ ✅ │
Tutor:

Type y when it asks. Always migrate before you deploy code that needs the table, so the first request after the deploy finds it there.

Tutor:

5. The tour

Before shipping, try every tool locally. Our tour (with the free fallback model, as all through this course):

recorded output
curl -s -X POST http://localhost:8801/ask -H "Authorization: Bearer demo-token-for-the-recording" -d '{"question":"Should I take an umbrella in Dublin today?"}'
{"answer":"The weather in Dublin is currently clear with no precipitation, so you don't need an umbrella today.","model":"@cf/zai-org/glm-4.7-flash"}
recorded output
curl -s -X POST http://localhost:8801/note -H "Authorization: Bearer demo-token-for-the-recording" -d '{"text":"pick up the parcel from the post office before sat noon, ref 4471"}'
{"saved":{"id":8,"body":"Pick up the parcel from the post office before Saturday noon, ref 4471"}}
recorded output
curl -s 'http://localhost:8801/klippy/check?nonce=checkpoint-b09'
{"nonce":"checkpoint-b09","tag":"b-09","probe":{"answer":"Hello, bits!","model":"@cf/zai-org/glm-4.7-flash"}}
Tutor:

We also asked the time in Sydney (getTime), what we'd noted lately (listNotes) and about https://example.com. On that last one the fallback model answered from memory without calling readLink, though it called it in the last lesson. Free models are less reliable at tool use than Claude; that's the trade.

Tutor:

6. Ship it

In apps/agent (your token is already set if you deployed in Your first Worker; if not, pnpm wrangler secret put KLIPPY_TOKEN first):

Shell
pnpm wrangler deploy

What we recorded, and what we didn't. As in Your first Worker, we haven't deployed the reference Klippy, so there's no recorded live deploy. Our dry run shows what goes up: the new DB binding is there.

recorded output
pnpm wrangler deploy --dry-run
Total Upload: 1535.58 KiB / gzip: 273.53 KiB
Your Worker has access to the following bindings:
Binding Resource
env.DB (klippy-db) D1 Database
env.AI AI
env.LIMITER (10 requests/60s) Rate Limit
--dry-run: exiting now.
Tutor:

7. Let bits check it

Paste your Worker's address. bits calls /klippy/check on it and looks for this checkpoint's tag.

deploy check+60 neurons

Goal Your Klippy is live at this checkpoint: /klippy/check answers with bits' nonce and tag b-09 (or a later one)

Paste the https://….workers.dev address that wrangler deploy printed.

bits calls /klippy/check?nonce=bits-checkpoint-b09 on your Worker, from bits’ server

  • /klippy/check?nonce=bits-checkpoint-b09 answers 200 OK, not yet
  • Answers the check, not yet
  • Echoes bits' nonce, not yet
  • Is at b-09 or later, not yet

Sign in to check your deploy: the check is saved to your account, and a certificate can link it.

Tutor:

A pass proves a Worker at that address answers the way this lesson asks. It doesn't prove who wrote it. Then, on your live Klippy, try a note with your token: it lands in the live database you migrated.

Tutor:

8. Last exercise: descriptions, one more time

Everything in this course rests on the descriptions Claude reads. Test that one last time in the sandbox: write a get_weather description, then have Claude decide tool or no tool for three messages. Only the first should need it.

Your turn

Goal Write a get_weather description, then have Claude decide tool or no tool for three messages. Only the first needs the tool

What good looks likeThe coach checks the reply
  • Names the toolnot checked yet
  • Says what it doesnot checked yet
  • Says what it can't donot checked yet
  • Keeps the three messagesnot checked yet
  • Ran at 1.0not checked yet
  • Decides tool or no toolnot checked yet
  • Message 1, weather in Lima right now: uses the toolnot checked yet
  • Message 2, the capital of Peru: no tool, Claude already knows itnot checked yet
  • Message 3, rain in Cusco tomorrow: no tool, because get_weather can't forecastnot checked yet
  • Says what it would tell the user when the tool can't helpnice to have

Run your prompt and the replies land here.

    ~0 tokens

    Turn on JavaScript to run prompts.

    quick check+20 neurons
    Tutor:

    Which of these is your code's job, not the model's?

      Pick one option

      Pick one, then Check.

      Tutor:

      That's Klippy with tools: five of them, guarded, tested, deployed and checked. Your code matches tag b-09 (changes from b-08). In the next course, Klippy becomes an agent: a loop, a memory, and one Klippy per person.