Lab GuideBuild your Droid with your agent
14:30 17:00

LAB GUIDE / SECTION 07

Mission 3 — Build your Droid with your agent

Goal: POST /api/chat answers in character, then your page gets a new feature. Platform concepts: Workers AI through a binding, handling errors without breaking a Contract, directing a Coding Agent.

Note

Two models, two jobs. Your Coding Agent's model (on your laptop) writes the code. Your Droid answers questions with a small, cheap Workers AI model on your own free Cloudflare account. A few Rounds use a tiny fraction of the free daily allowance.

You'll do three things:

  1. By hand: write your Droid's Tone Prompt. That's the creative part, and it's yours.
  2. With your agent: build handleChat, the Droid itself.
  3. With your agent: add something visible to your page and watch it go live.

7.1 Mission 3a — Write the tone (by hand)#

Open src/droid.js. At the top:

js
// Mission 3a: describe how your droid talks (write this yourself)
const TONE = "(your tone here)";

Replace TONE with 2–4 sentences describing how your Droid talks. Your Character and its homeworld facts are added automatically by buildSystemPrompt, so focus on style.

js
const TONE = `You talk like an over-excited football commentator.
Every answer is a live broadcast. Short sentences. LOTS OF ENERGY.
Keep it to two sentences. Never break character.`;

Tip

Answers must fit in 280 characters. Saying so in the tone ("keep it to two sentences") works better than relying on the cut-off.

Tip

The 3B model is small, and it sometimes drifts off-character: it forgets the tone, or starts explaining itself. A sharper TONE helps. Be concrete ("end every answer with 'Arr!'"), say what not to do ("never mention being an AI"), and add one example phrase.

7.2 Mission 3b — Direct your agent to build the Droid#

Below TONE and MODEL, handleChat is a stub that returns "Not built yet". The pieces it needs already exist: getCharacter (your Mission 2) and buildSystemPrompt (src/prompt.js). Your agent's job is to wire them to Workers AI.

Write your own prompt. A good prompt for an agent says what you want and how you'll know it works, not every line of code. Your Droid must:

RequirementWhy
Read question from the JSON bodyThat's what the Hub sends
Build the messages: a system prompt from buildSystemPrompt({ character, tone: TONE }), where character comes from getCharacter(env.CHARACTER_ID), then the user's questionThat's the persona, and it reuses your Mission 2
Ask Workers AI on your own account: env.AI.run(MODEL, { messages, max_tokens: 120 }), where MODEL is already defined at the top of src/droid.js. The answer is in result.responseA small, cheap model that runs on Cloudflare's network, next to your Worker
If Workers AI throws, log the error and answer with a short in-character Fallback line instead of crashingA crash is a red card in the Rounds. A silly answer is still a green one
Return { "answer": "..." }, cut to 280 charactersThat's the Contract

Tip

env.AI is a binding: look for "ai": { "binding": "AI" } in wrangler.jsonc. A binding connects your Worker to another Cloudflare product, with no URL, no API key and no network setup. You declare it in config, and it appears on env.

Keep the prompt focused on src/droid.js. A narrow task uses fewer tokens, and free models have small limits.

Show prompt (if you're stuck)
txt
In src/droid.js, implement handleChat. Don't change TONE, MODEL or any other file.
1. Read { question } from the JSON body. If the body isn't JSON or the question is empty, return a 400 with { error }.
2. character = await getCharacter(env.CHARACTER_ID).
3. messages = [system: buildSystemPrompt({ character, tone: TONE }), user: question].
4. result = await env.AI.run(MODEL, { messages, max_tokens: 120 }). The answer is result.response. Stop waiting after 6 seconds (the Hub gives up at 8).
5. Wrap steps 2-4 in try/catch: on error, console.log it and answer with a short in-character fallback line. Also use that line if the answer is empty.
6. Return Response.json({ answer }) with answer cut to 280 characters.
Then show me the diff.

7.3 Review, test, ship#

Read the diff before anything goes live. Your agent shows it, or run git diff. You wrote Phase 1 by hand, so you can check it: does it call env.AI.run with MODEL? Does it use getCharacter and buildSystemPrompt? Is there a try/catch, so an error still returns an answer? Is TONE still yours?

Test it locally:

sh
npm run dev

In a second terminal:

sh
curl -X POST http://localhost:8787/api/chat -H "content-type: application/json" -d '{"round":"intro","question":"Introduce yourself."}'
Windows PowerShell version
powershell
Invoke-RestMethod -Method Post -Uri http://localhost:8787/api/chat `
  -ContentType "application/json" `
  -Body '{"round":"intro","question":"Introduce yourself."}'

There's no key and no Authorization header: your /api/chat is open. Anyone with your URL can call it, and that's how the Hub reaches it.

Note

Test the error path: temporarily change MODEL to a name that doesn't exist, like "@cf/nope". Your Droid should still answer with its Fallback line, and the terminal should log the error. Then change it back.

Happy with it? Tell your agent:

txt
Commit with a short message and push.

Workers Builds picks up the push. Watch it in the dashboard (your Worker → Deployments), then ask your live Droid:

sh
curl -X POST https://<your-droid-name>.<your-subdomain>.workers.dev/api/chat -H "content-type: application/json" -d '{"round":"intro","question":"Introduce yourself."}'

Warning

Ship only with git push. If your agent suggests wrangler deploy, say no: it skips GitHub, so your live site and your repo stop matching, and your next push quietly overwrites it. AGENTS.md tells it this, but agents don't always listen.

Check#

  • curl to your live URL gets an in-character answer
  • With a broken MODEL, it still answers with its Fallback line
  • You read the diff before it went live
Show solution (paste this if your agent can't get there)

Replace the whole of src/droid.js with this, then rewrite TONE with your own. The pirate is just an example.

js
// SOLUTION for Phase 2. Teacher's reference: copy over src/droid.js.
import { getCharacter } from "./character.js";
import { buildSystemPrompt } from "./prompt.js";

// Mission 3a: describe how your droid talks (every student writes their own)
const TONE = "Talk like a cheerful pirate captain. Use sea slang and end with 'Arr!'.";

// A small, cheap Workers AI chat model. It runs on your own free account through env.AI.
const MODEL = "@cf/meta/llama-3.2-3b-instruct";

const MAX_ANSWER = 280;
const FALLBACK = "Beep boop! My circuits are tangled, but the Force is with us. Ask me again!";

// Cut to 280 characters, ending with an ellipsis if we had to cut.
function limit(text) {
  const clean = String(text ?? "").trim();
  return clean.length <= MAX_ANSWER ? clean : `${clean.slice(0, MAX_ANSWER - 1).trimEnd()}…`;
}

// The Hub gives up after 8 s, so stop waiting for the model after 6.
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => setTimeout(() => reject(new Error("AI timeout")), ms)),
  ]);
}

export async function handleChat(request, env) {
  let question = "";
  try {
    const body = await request.json();
    question = String(body?.question ?? "").trim();
  } catch {
    return Response.json({ error: "Send JSON: { round, question }" }, { status: 400 });
  }
  if (!question) return Response.json({ error: "Missing question" }, { status: 400 });

  try {
    const character = await getCharacter(env.CHARACTER_ID);
    const messages = [
      { role: "system", content: buildSystemPrompt({ character, tone: TONE }) },
      { role: "user", content: question },
    ];
    const result = await withTimeout(env.AI.run(MODEL, { messages, max_tokens: 120 }), 6000);
    const answer = limit(result?.response);
    return Response.json({ answer: answer || FALLBACK });
  } catch (err) {
    console.log("Droid failed:", err.message);
    return Response.json({ answer: FALLBACK });
  }
}
Not working?
SymptomCheck
{"answer":"Not built yet: ask your agent to build handleChat"}handleChat is still the stub. Did the agent save src/droid.js?
Always the Fallback lineLook at the logged error. A wrong MODEL name? Daily free Workers AI allowance used up (very unlikely today)?
The Droid says it's "Unknown Droid"getCharacter returned the placeholder: SWAPI failed or CHARACTER_ID is wrong (17 doesn't exist). Check npx wrangler tail
Answers drift off-characterThe 3B model is small. Make TONE sharper: concrete style, one example phrase, what to avoid
Error on env.AI locallyRun npx wrangler login. Workers AI runs on Cloudflare even during wrangler dev. With several Cloudflare accounts, set CLOUDFLARE_ACCOUNT_ID (see Troubleshooting)
400 / "Missing question"Your request had no question. Copy the curl above exactly
The agent changed other filesgit checkout -- <file> puts a file back. Tell the agent to touch only src/droid.js
The agent goes in circlesStop it, paste the error from the terminal into the chat, and ask for one specific fix. Still stuck? Paste the solution above

7.4 Mission 3c — Add a feature to your page#

Your Droid works, so now make it visible. Ask your agent for one new thing on your page, review the diff, push, and refresh your live URL about 30 seconds later. Some ideas:

  • an "Ask my Droid" box under your name that sends a question to your own /api/chat and shows the answer;
  • a films panel that shows more about your Character than the card does;
  • a new line on the ID card, such as your Character's gender or homeworld population (getCharacter already returns them).

The rules for any feature:

  • /api/card and /api/chat must keep working exactly as before. The Hub calls them in the Rounds.
  • Page features go in src/page.js and public/.
  • Your page's security policy blocks inline scripts. Any JavaScript goes in its own file in public/, loaded with <script src="/your-file.js" defer>.
  • Test locally with npm run dev before you push.
Show prompt (if you're stuck)
txt
Add an "Ask my droid" box to my page, below my name: a text input and a button that POSTs
{ "round": "intro", "question": <input> } to /api/chat and shows the answer under the box.
Edit only src/page.js and public/ (add public/ask.js for the JavaScript and load it from page.js with
<script src="/ask.js" defer>: inline scripts are blocked by the page's Content-Security-Policy).
Keep it plain: no frameworks. Show the answer as text (textContent), never as HTML.
Don't change /api/card or /api/chat. Then show me the diff.
  • Your new feature is live on https://<your-droid-name>.<your-subdomain>.workers.dev

Stretch

Ask your agent to use the round field so your Droid behaves differently for "intro" and "scenario". Read the diff, test it with curl, and push it before the next Round.

Next: Rounds

Checks are saved automatically on this browser.

Your project

Pick your Droid Name first. Add the rest when you know it; every field is optional. These details stay in this browser and are never sent anywhere.

A nickname, not your full name. Used for your Student Worker and repo. Lowercase letters, numbers and hyphens; up to 40 characters.

The workers.dev subdomain you choose at signup. Same naming rules as your Droid Name.

Add this later, or paste a GitHub repo URL to fill your username and Droid Name.

My URL is different

Optional. Use your public https://…workers.dev URL. Paths and trailing slashes are removed. Leave blank to use your Droid Name and subdomain.

LIVE PREVIEW

Your droid will live at https://<your-droid-name>.<your-subdomain>.workers.dev

Your repo: github.com/<your-github-user>/<your-droid-name>