Chat history is for the model; persistent state is data your code can read.
usePersistentState(name, initial) resembles React state, but every write is
stored in the current conversation’s Durable Object. The agent re-renders before
each model call, so its instructions receive the latest state snapshot.
Only src/agents/field-trip.ts changes from cp/1-hello-agent. Add persistent
trip state, the save_trip_brief tool, and instructions that include the saved brief:
- return `You are FieldTrip, a helpful team-offsite planner. You help groups plan memorable offsites by understanding their destination, dates, headcount, budget, and interests.`;
+
+ // Durable, per-conversation state (stored in this conversation's Durable Object).
+ // Shaped like React's useState, but it survives restarts and redeploys.
+ // Tools can write state. The write commits together with the tool call.
+ useTool({
+ name: 'save_trip_brief',
+ description:
+ 'Save or update the offsite trip brief. Call this whenever the user states or changes the city, dates, headcount, budget, or interests. Only include fields the user mentioned; they are merged into the saved brief.',
+ // The agent re-renders before every model call, so these instructions
+ // always reflect the latest saved brief.
+ const hasBrief = Object.keys(brief).length > 0;
+ return `You are FieldTrip, a helpful team-offsite planner. You help groups plan memorable offsites by understanding their destination, dates, headcount, budget, and interests.
+
+Rules:
+1. If the user's message contains ANY trip detail (city, dates, headcount, budget, interests), your FIRST action is to call \`save_trip_brief\` with those fields. Do this before writing any reply.
+2. Answer questions about the trip from the saved brief below. If a detail is missing, ask for it.
+3. Keep replies short: at most 120 words unless the user asks for more detail.
// Tools can write state. The write commits together with the tool call.
useTool({
name: 'save_trip_brief',
description:
'Save or update the offsite trip brief. Call this whenever the user states or changes the city, dates, headcount, budget, or interests. Only include fields the user mentioned; they are merged into the saved brief.',
// The agent re-renders before every model call, so these instructions
// always reflect the latest saved brief.
const hasBrief = Object.keys(brief).length > 0;
return`You are FieldTrip, a helpful team-offsite planner. You help groups plan memorable offsites by understanding their destination, dates, headcount, budget, and interests.
Rules:
1. If the user's message contains ANY trip detail (city, dates, headcount, budget, interests), your FIRST action is to call \`save_trip_brief\` with those fields. Do this before writing any reply.
2. Answer questions about the trip from the saved brief below. If a detail is missing, ask for it.
3. Keep replies short: at most 120 words unless the user asks for more detail.
npmrunsmoke--https://field-trip-agent.<subdomain>.workers.devcp2-live"We're planning an offsite in Lisbon from <START> to <END> for 14 people, budget around 400 EUR per person. We like food, history and something outdoorsy."
npmrunsmoke--https://field-trip-agent.<subdomain>.workers.devcp2-live"Update: two more people are joining, so 16 now."
npmrunsmoke--https://field-trip-agent.<subdomain>.workers.devcp2-live"What's my headcount, city and budget? One line."
Look for a full save_trip_brief call, then a second call containing only
{"headcount":16}. The final answer should retain Lisbon and the budget.
Optional depth check: prove isolation with a different conversation:
Terminal window
npmrunsmoke--https://field-trip-agent.<subdomain>.workers.devcp2-live-other"What's my saved headcount?"
The fresh conversation must not inherit cp2-live state.
This opens the same conversation as the smoke test. If you repeat this checkpoint, before copying its commands.
Start npm run dev in Terminal A and wait for VITE ... ready. Run in Terminal B:
Terminal window
npmrunsmoke--http://localhost:5173cp2-local"We're planning an offsite in Lisbon from <START> to <END> for 14 people, budget around 400 EUR per person. We like food, history and something outdoorsy."
npmrunsmoke--http://localhost:5173cp2-local"Update: two more people are joining, so 16 now."
npmrunsmoke--http://localhost:5173cp2-local"What's my headcount, city and budget? One line."
Look for a full save_trip_brief call, then a second call containing only
{"headcount":16}. The final answer should retain Lisbon and the budget.
Optional depth check: prove isolation with a different conversation:
Terminal window
npmrunsmoke--http://localhost:5173cp2-local-other"What's my saved headcount?"
The fresh conversation must not inherit cp2-local state.
This opens the same conversation as the smoke test. If you repeat this checkpoint, before copying its commands.
Verification gate
Prove it works
The first message calls save_trip_brief with the supplied details.
The update saves only headcount: 16 without erasing other fields.
The final query returns 16, Lisbon, and 400 EUR per person.
Optional: a different conversation ID has no saved brief.
Optional mini quizCheck durable state3 questions / instant feedback
Recovery lane
Get back on track in under a minute
If a dev server is running, stop it first. Uncommitted work is stashed and the current commit gets a backup branch before anything moves.
Option 01
Restart this exercise
Return to cp/1-hello-agent, rebuild this exercise, then use its Live deploy verification tab. Stop any running dev server with Ctrl+C before switching.