On-demand workspace
Bonus assignment: attach the workspace on demand
Section titled “Bonus assignment: attach the workspace on demand”Time: 5–8 minutes · Start after the finish-line verification
Checkpoint 5 attaches Sandbox tools on every agent render. The main workshop keeps that behavior through checkpoint 6. This optional assignment adds a new capability boundary: the model must explicitly request a workspace before it can use file and shell tools.
1. Design the gate
Section titled “1. Design the gate”In src/agents/field-trip.ts:
- Add a persistent
workspaceflag, initiallyfalsefor each conversation. - Register an
open_workspacetool that sets the flag totrue. - Call
useSandbox(...)only when the flag is true. - Update the itinerary instructions to call
open_workspacebefore writing when the file tools are not yet available.
Flue re-renders the agent before the next model call. After the tool updates state, that next render exposes the Sandbox tools. The decision stays scoped to the conversation, independently of the trip brief.
If your agent already has open_workspace, verify the existing implementation
below rather than registering a duplicate tool.
2. Apply the change
Section titled “2. Apply the change”The diff starts from the guide’s completed checkpoint 6, whose agent is still the checkpoint-5 version. Only the workspace gate and itinerary rule change; configuration, migrations, weather tools, and subagent behavior stay in place.
src/agents/field-trip.ts+12−3Changes from Checkpoint 6 (guide) → On-demand workspace bonus
===================================================================--- a/src/agents/field-trip.ts Checkpoint 6 (guide)+++ b/src/agents/field-trip.ts On-demand workspace bonus@@ -59,8 +59,17 @@ // fresh context with its own tools, and only its final answer comes back. useSubagent(venueScout);
- // A Linux container per conversation (adds read/write/edit/bash/grep/glob tools).- useSandbox(cloudflareSandbox(getSandbox(env.Sandbox, id)));+ // Attach file and shell tools only after this conversation requests a workspace.+ const [workspace, setWorkspace] = usePersistentState('workspace', false);+ useTool({+ name: 'open_workspace',+ description: 'Attach the file workspace (read/write/bash tools).',+ async run() {+ setWorkspace(true);+ return 'Workspace attached.';+ },+ });+ if (workspace) useSandbox(cloudflareSandbox(getSandbox(env.Sandbox, id)));
// The agent re-renders before every model call, so these instructions // always reflect the latest saved brief.@@ -77,7 +86,7 @@ b. Pick the 3 places that best fit the brief (skip stations, offices, hospitals, embassies, companies, events). c. Call \`task\` ONCE with agent \`venue-scout\` for all 3 places. The scout cannot see this conversation, so the prompt must be a complete briefing: the exact place titles, the city, the headcount, and the interests. d. Combine the results into a short plan, keeping the links. If you know the forecast, suggest outdoor places for dry days and indoor ones for rainy days.-5. For an itinerary: \`write\` it to itinerary.md (one section per day: places, timing, weather), then \`read\` it to check. Do not repeat the file in your reply (the user sees the read result); reply in one sentence.+5. For an itinerary: if you have no \`write\` tool yet, call \`open_workspace\` first. Then \`write\` it to itinerary.md (one section per day: places, timing, weather), then \`read\` it to check. Do not repeat the file in your reply (the user sees the read result); reply in one sentence. 6. Keep replies short: at most 120 words unless the user asks for more detail.
Today is ${today}.'use agent';
import { type AgentProps, useModel, usePersistentState, useSandbox, useSubagent, useTool } from '@flue/runtime';import { cloudflareSandbox } from '@flue/runtime/cloudflare';import { getSandbox } from '@cloudflare/sandbox';import { env } from 'cloudflare:workers';import * as v from 'valibot';import { geocodeCity, getForecast } from '../tools/weather.ts';import { findNearbyPlaces } from '../tools/wikipedia.ts';import { venueScout } from '../subagents/venue-scout.ts';
// The trip brief the agent remembers for this conversation.type TripBrief = { city?: string; startDate?: string; // YYYY-MM-DD endDate?: string; // YYYY-MM-DD headcount?: number; budget?: string; interests?: string[];};
export function FieldTrip({ id }: AgentProps) { useModel('cloudflare/@cf/google/gemma-4-26b-a4b-it');
// Durable, per-conversation state (stored in this conversation's Durable Object). // Shaped like React's useState, but it survives restarts and redeploys. const [brief, setBrief] = usePersistentState<TripBrief>('brief', {});
// 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.', input: v.object({ city: v.optional(v.string()), startDate: v.optional(v.pipe(v.string(), v.isoDate())), endDate: v.optional(v.pipe(v.string(), v.isoDate())), headcount: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), budget: v.optional(v.string()), interests: v.optional(v.array(v.string())), }), async run({ data }) { const updates = Object.fromEntries( Object.entries(data).filter(([, value]) => value !== undefined), ) as TripBrief; setBrief((previous) => ({ ...previous, ...updates })); return { output: { saved: updates } }; }, });
// Tools that call an external API (Open-Meteo), defined in src/tools/weather.ts. useTool(geocodeCity); useTool(getForecast);
// The parent finds candidate places (Wikipedia geosearch)... useTool(findNearbyPlaces);
// ...and delegates all three candidates in one task. The scout runs in a // fresh context with its own tools, and only its final answer comes back. useSubagent(venueScout);
// Attach file and shell tools only after this conversation requests a workspace. const [workspace, setWorkspace] = usePersistentState('workspace', false); useTool({ name: 'open_workspace', description: 'Attach the file workspace (read/write/bash tools).', async run() { setWorkspace(true); return 'Workspace attached.'; }, }); if (workspace) useSandbox(cloudflareSandbox(getSandbox(env.Sandbox, id)));
// The agent re-renders before every model call, so these instructions // always reflect the latest saved brief. const hasBrief = Object.keys(brief).length > 0; const today = new Date().toISOString().slice(0, 10); 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. For weather questions: call \`geocode_city\` for the city, then \`get_forecast\` with its latitude/longitude and the trip dates (use the saved brief). If there is no end date, use the start date. Summarise the forecast per day in plain words; if a tool returns an error, explain it to the user.4. For venue, activity or place suggestions: a. Call \`geocode_city\`, then \`find_nearby_places\` with its coordinates. b. Pick the 3 places that best fit the brief (skip stations, offices, hospitals, embassies, companies, events). c. Call \`task\` ONCE with agent \`venue-scout\` for all 3 places. The scout cannot see this conversation, so the prompt must be a complete briefing: the exact place titles, the city, the headcount, and the interests. d. Combine the results into a short plan, keeping the links. If you know the forecast, suggest outdoor places for dry days and indoor ones for rainy days.5. For an itinerary: if you have no \`write\` tool yet, call \`open_workspace\` first. Then \`write\` it to itinerary.md (one section per day: places, timing, weather), then \`read\` it to check. Do not repeat the file in your reply (the user sees the read result); reply in one sentence.6. Keep replies short: at most 120 words unless the user asks for more detail.
Today is ${today}.
## Saved trip brief${hasBrief ? JSON.stringify(brief, null, 2) : '(nothing saved yet)'}`;}3. Verify the capability boundary
Section titled “3. Verify the capability boundary”Use fresh conversation IDs so a previously saved workspace: true flag cannot
hide the initial gate. Trip dates are filled automatically.
npm run typechecknpm run deploysleep 5npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev workspace-live-greeting "Hi, who are you? One sentence."TIMEOUT_S=200 npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev workspace-live "Plan a simple Lisbon itinerary from <START> to <END> for 14 people. Write it to itinerary.md and read it back."npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev workspace-live "Read itinerary.md again."Save your workers.dev subdomain to open this test ↗https://field-trip-agent.<subdomain>.workers.dev/?id=workspace-live
This opens the same conversation as the smoke test. If you repeat this checkpoint, before copying its commands.
Restart npm run dev in Terminal A, wait for VITE ... ready, then run in Terminal B:
npm run typechecknpm run smoke -- http://localhost:5173 workspace-local-greeting "Hi, who are you? One sentence."TIMEOUT_S=200 npm run smoke -- http://localhost:5173 workspace-local "Plan a simple Lisbon itinerary from <START> to <END> for 14 people. Write it to itinerary.md and read it back."npm run smoke -- http://localhost:5173 workspace-local "Read itinerary.md again."Open local chat in your browser ↗http://localhost:5173/?id=workspace-local
This opens the same conversation as the smoke test. If you repeat this checkpoint, before copying its commands.
Verification gate
Prove it works
- The fresh greeting completes without calling
open_workspace. - The first itinerary request calls
open_workspacebeforewriteandread. - A later message in that conversation can read the same file without opening the workspace again.
- The trip brief, weather tools, and venue-scout delegation still work.
Replace the workspace state, open_workspace tool, and conditional attachment
with the original checkpoint-5 block:
// A Linux container per conversation (adds read/write/edit/bash/grep/glob tools). useSandbox(cloudflareSandbox(getSandbox(env.Sandbox, id)));Restore the itinerary rule to start directly with write, removing the
open_workspace instruction. Run npm run typecheck, then redeploy from the
Live deploy tab (or restart Vite for Local dev). Use a fresh conversation for
the next experiment.