Skip to content

4. Venue-scout subagent

Checkpoint 4 of 68 minutes

Venue-scout subagent

Find nearby places, delegate three candidates to a focused scout, and combine its grounded answer.

A subagent is a capability of its parent, not a registered agent. It has no URL, conversation ID, persistent state, or parent context. useSubagent exposes the built-in task tool; the task prompt is the complete briefing, and only the subagent’s final answer returns to the parent.

Create src/tools/wikipedia.ts:

src/tools/wikipedia.ts+83−0

New file from cp/3-api-tools → cp/4-subagent

src/tools/wikipedia.ts
===================================================================
--- /dev/null cp/3-api-tools
+++ b/src/tools/wikipedia.ts cp/4-subagent
@@ -0,0 +1,83 @@
+import { defineTool } from '@flue/runtime';
+import * as v from 'valibot';
+
+// Wikipedia APIs: free, no API key, but a descriptive User-Agent is required.
+// https://www.mediawiki.org/wiki/API:Etiquette
+const HEADERS = {
+ 'User-Agent': 'FieldTripAgent/0.1 (Flue workshop demo; https://github.com/omkarkhair/field-trip-agent)',
+ Accept: 'application/json',
+};
+
+export const findNearbyPlaces = defineTool({
+ name: 'find_nearby_places',
+ description:
+ 'List Wikipedia articles about places near a latitude/longitude (up to 10 km), with a one-line description and distance. Results include noise (stations, offices, events): pick the places that suit a group offsite.',
+ input: v.object({
+ latitude: v.number(),
+ longitude: v.number(),
+ radiusMeters: v.optional(v.pipe(v.number(), v.minValue(100), v.maxValue(10000)), 10000),
+ }),
+ async run({ data, signal }) {
+ const coord = `${data.latitude}|${data.longitude}`;
+ const params = new URLSearchParams({
+ action: 'query',
+ generator: 'geosearch',
+ ggscoord: coord,
+ ggsradius: String(Math.round(data.radiusMeters)),
+ ggslimit: '20',
+ prop: 'description|coordinates',
+ codistancefrompoint: coord,
+ colimit: 'max', // default is 10, which would leave half the results without a distance
+ format: 'json',
+ formatversion: '2',
+ });
+ const res = await fetch(`https://en.wikipedia.org/w/api.php?${params}`, { headers: HEADERS, signal });
+ if (!res.ok) throw new Error(`Wikipedia geosearch failed: HTTP ${res.status}`);
+ const body = (await res.json()) as {
+ query?: {
+ pages: Array<{ title: string; description?: string; coordinates?: Array<{ dist: number }> }>;
+ };
+ };
+ const pages = body.query?.pages ?? [];
+ if (pages.length === 0) throw new Error('No Wikipedia places found here. Try a larger radius or check the coordinates.');
+ return {
+ output: pages
+ .map((p) => ({
+ title: p.title,
+ description: p.description ?? '',
+ distanceM: Math.round(p.coordinates?.[0]?.dist ?? 0),
+ }))
+ .sort((a, b) => a.distanceM - b.distanceM),
+ };
+ },
+});
+
+export const getPlaceSummary = defineTool({
+ name: 'get_place_summary',
+ description:
+ 'Get a short Wikipedia summary and URL for one place, by its exact article title (as returned by find_nearby_places).',
+ input: v.object({
+ title: v.pipe(v.string(), v.minLength(1)),
+ }),
+ async run({ data, signal }) {
+ const slug = encodeURIComponent(data.title.replaceAll(' ', '_'));
+ const res = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${slug}`, { headers: HEADERS, signal });
+ if (res.status === 404) throw new Error(`No Wikipedia article titled "${data.title}". Use a title from find_nearby_places.`);
+ if (!res.ok) throw new Error(`Wikipedia summary failed: HTTP ${res.status}`);
+ const page = (await res.json()) as {
+ title: string;
+ description?: string;
+ extract?: string;
+ content_urls?: { desktop?: { page?: string } };
+ };
+ return {
+ output: {
+ title: page.title,
+ description: page.description ?? '',
+ // Keep tool output small: it goes into the model's context window.
+ summary: (page.extract ?? '').slice(0, 600),
+ url: page.content_urls?.desktop?.page ?? `https://en.wikipedia.org/wiki/${slug}`,
+ },
+ };
+ },
+});

Create src/subagents/venue-scout.ts:

src/subagents/venue-scout.ts+35−0

New file from cp/3-api-tools → cp/4-subagent

src/subagents/venue-scout.ts
===================================================================
--- /dev/null cp/3-api-tools
+++ b/src/subagents/venue-scout.ts cp/4-subagent
@@ -0,0 +1,35 @@
+// An ordinary module, NOT 'use agent': a subagent is a capability of the agent
+// that mounts it, not a registered agent. It has no URL, no conversation id,
+// no persistent state, and no useModel(). defineSubagent selects its model;
+// omit that option to inherit the parent's model.
+import { defineSubagent, useTool } from '@flue/runtime';
+import { getPlaceSummary } from '../tools/wikipedia.ts';
+
+function VenueScout() {
+ // The scout's world is only what it mounts here. The parent's tools,
+ // instructions, conversation and state are NOT inherited.
+ useTool(getPlaceSummary);
+
+ return `You are venue-scout. You assess a few places for a team offsite.
+
+You only see the task prompt, not the user's conversation. The prompt gives you up to 3 places (Wikipedia article titles) and details about the group.
+
+1. Call \`get_place_summary\` for every place, all in one batch.
+2. Then write your final answer. Use facts from the summary; you may add general knowledge, but mark estimates as estimates.
+
+Your final answer is all the parent sees. Always finish with this exact format, one block per place, and nothing else:
+
+**<place name>** (<Wikipedia URL>)
+- What to do: <1–2 activities for a group>
+- Typical visit: <e.g. ~1–2 h> (estimate)
+- Weather: <indoor | outdoor | mixed>; <how rain or heat affects the visit>
+- Group fit: <one sentence about this group size and interests>`;
+}
+
+export const venueScout = defineSubagent({
+ name: 'venue-scout',
+ description:
+ 'Assesses up to 3 places for a team offsite: activities, typical visit length, weather dependency, and group fit. Prompt with the exact Wikipedia titles, the city, the headcount, and the interests.',
+ agent: VenueScout,
+ model: 'cloudflare/@cf/meta/llama-4-scout-17b-16e-instruct',
+});

Update src/agents/field-trip.ts to register nearby-place discovery and the venue-scout subagent, then add the delegation rules:

src/agents/field-trip.ts+16−2

Changes from cp/3-api-tools → cp/4-subagent

src/agents/field-trip.ts
===================================================================
--- a/src/agents/field-trip.ts cp/3-api-tools
+++ b/src/agents/field-trip.ts cp/4-subagent
@@ -1,8 +1,10 @@
'use agent';
-import { useModel, usePersistentState, useTool } from '@flue/runtime';
+import { useModel, usePersistentState, useSubagent, useTool } from '@flue/runtime';
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 = {
@@ -47,6 +49,13 @@
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);
+
// The agent re-renders before every model call, so these instructions
// always reflect the latest saved brief.
const hasBrief = Object.keys(brief).length > 0;
@@ -57,7 +66,12 @@
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. Keep replies short: at most 120 words unless the user asks for more detail.
+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. Keep replies short: at most 120 words unless the user asks for more detail.
Today is ${today}.

Trip dates are filled automatically in the commands and their copy buttons. The guide reuses your workshop dates.

Terminal window
npm run deploy
sleep 5
npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp4-live "Offsite in Lisbon from <START> to <END> for 14 people, budget 400 EUR each. We like food, history and the outdoors."
TIMEOUT_S=200 npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp4-live "Suggest 3 places for our offsite."

Look for geocode_city, find_nearby_places, then one task call for venue-scout. The model may also call get_forecast. The task prompt must name three exact titles plus the city, headcount, and interests.

Save your workers.dev subdomain to open this test ↗https://field-trip-agent.<subdomain>.workers.dev/?id=cp4-live

This opens the same conversation as the smoke test. If you repeat this checkpoint, before copying its commands.

Verification gate

Prove it works

  • The parent finds nearby candidates with Wikipedia geosearch.
  • One task call briefs venue-scout on all three places.
  • The scout result follows the fixed per-place format.
  • The parent combines the result into a short plan with links.
Optional mini quizCheck delegation boundaries3 questions instant feedback
1What context does venue-scout receive?
2Which Wikipedia tool belongs to the parent?
3What returns from the scout to the parent?
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/3-api-tools, rebuild this exercise, then use its Live deploy verification tab. Stop any running dev server with Ctrl+C before switching.

Terminal
git stash push --include-untracked -m "workshop recovery"
git fetch origin
git branch -f workshop-backup HEAD
git switch --no-track -C workshop origin/cp/3-api-tools
npm ci
Option 02

Catch up with the room

Jump to cp/4-subagent, then use this checkpoint's Live deploy verification tab. Stop any running dev server with Ctrl+C before switching.

Terminal
git stash push --include-untracked -m "workshop recovery"
git fetch origin
git branch -f workshop-backup HEAD
git switch --no-track -C workshop origin/cp/4-subagent
npm ci

Next: Add a Cloudflare Sandbox container →