Skip to content

feat(tour-plan): always render the tour map (deterministic map_locations)#117

Open
ArtyETH06 wants to merge 14 commits into
mainfrom
ArtyETH06/3779-tour-plan-map-locations
Open

feat(tour-plan): always render the tour map (deterministic map_locations)#117
ArtyETH06 wants to merge 14 commits into
mainfrom
ArtyETH06/3779-tour-plan-map-locations

Conversation

@ArtyETH06

@ArtyETH06 ArtyETH06 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What & why

Field-sales tour planning rendered the map inconsistently and plain tour prompts often didn't reach the tour tool (#3779). This makes it deterministic: recognize the tour, build it, and render the map automatically — with the leads pre-shaped server-side so the agent never hand-builds the widget payload.

Behavior

  • Routing — "I'm in NYC, who's worth meeting?" / "plan a prospecting tour in <city>" routes to leadbay_tour_plan (no longer leaks to leadbay_pull_leads).
  • Auto-render — builds the mixed tour (known accounts + fresh prospects) and renders the map on its own; the user never has to ask. Falls back to per-lead place-card blocks on hosts without places_map_display_v0.
  • Badges — each stop tagged ★ Customer / ★ Qualified / ✦ New, derived from the real history fields (epilogue_status / last_prospecting_action_at / last_monitor_action_at).

Implementation

  • packages/core/src/composite/tour-plan.ts — server-shaped map_locations[] ({name, address, latitude, longitude, notes}, badge in notes) + map_summary; "null"-string contact-name guard.
  • Templates: tour-plan.md.tmpl, leadbay_plan_tour_in_city.md.tmpl; routing redirect in pull-leads.md.tmpl. Generated files via pnpm prompts:build.

Tests & gates

New tests: map-locations shaping, badge-from-real-fields, null-name guard. pnpm -r build / -r test (core 432, mcp 457, all green) / -r typecheck pass; routing / workflows / budget audits pass.

Eval

WORKFLOWS #35 (tour auto-renders the map) + #36 (no-fabrication). Live /eval (blind judge, leadbay_plan_tour_in_city, New York): WF35 5/5/5/5, WF36 5/5/5/5 — recognized the tour, called leadbay_tour_plan, rendered the map, passed map_locations faithfully, no fabrication. Harness note: the claude -p eval host doesn't expose places_map_display_v0, so the widget itself is verifiable only on a real Claude host.

Closes https://github.com/leadbay/product/issues/3779

…rministically

leadbay_tour_plan returned raw lead objects and left the
places_map_display_v0 payload for the LLM to hand-build from
location.pos + per-lead mode badges, so the map was rendered
inconsistently (#3779).

Pre-shape a ready-to-pass map_locations[] server-side (mirroring
campaign_call_sheet), with the mode badge (Customer/Qualified/New)
baked into each notes string, plus a deterministic map_summary for
the 'N leads without coordinates' footer. Description + prompt now
say ALWAYS render the map first and pass map_locations verbatim.

Co-Authored-By: Claude <noreply@anthropic.com>
@ArtyETH06 ArtyETH06 self-assigned this Jun 22, 2026
ArtyETH06 and others added 13 commits June 22, 2026 15:06
…3779)

WF35 (underdeliver guard): planning a city tour must ALWAYS surface
the geographic itinerary (map or per-lead place cards with mode
badges), never collapse it to a flat prose list.

WF36 (overdeliver guard): the agent must pass the server's
map_locations through verbatim — no invented coordinates/pins for
coordinate-less leads, no fabricated addresses, no competing raw
lat/lng table alongside the place cards.

Both target leadbay_plan_tour_in_city; workflows audit green.

Co-Authored-By: Claude <noreply@anthropic.com>
Reinforce PHASE 2: a field-sales tour is inherently geographic, so the
agent renders the map on its own and must NOT ask 'want a map?' — the
user never has to say 'map'. Rewrite eval WF35 to test exactly this
(map-free scenario prompt + must_not_match on 'would you like ... map')
so a run only passes if the map appears automatically.

Co-Authored-By: Claude <noreply@anthropic.com>
…egex

JS RegExp rejects inline flags; workflow-contract-schema audit compiles
every render_checks regex and CI failed on the (?i). Rewrite with
[Ww]-style character classes, scope the alternation to a nearby 'map',
and stop at sentence boundaries so benign offers don't false-positive.

Co-Authored-By: Claude <noreply@anthropic.com>
Live eval (#3779) on a populated NY tour produced a map pin reading
'Reach null null' — the contacts API sends the literal string 'null'
for an empty name part. Harden toMapLocation to drop 'null'/blank
first/last/title parts (same coercion bug pull-leads guards). New test
file covers the all-null and partial-null cases.

Co-Authored-By: Claude <noreply@anthropic.com>
WF35 improve loop: on a host without places_map_display_v0, the mode
badges (Customer/Qualified/New) lived only in the unrendered widget
notes, so the prose fallback dropped them (eval MM/IA 4/5). Require the
badge per-lead in the chat-prose summary too. Re-eval (NY, blind judge)
now 5/5/5/5: map auto-renders, every lead badged in prose, no map-ask.

Co-Authored-By: Claude <noreply@anthropic.com>
PR-review catch (#3779): the Customer/Qualified split keyed off
`last_monitor_action`, which is NOT part of the pull_followups payload
contract — so the check was always false and every monitored account
was mislabeled ★ Qualified, even ones with prior customer/follow-up
history. Since the prompt tells agents not to re-derive badges, the
wrong server-built badge reached the map and prose.

Key off the fields the follow-up rendering contract actually documents:
epilogue_status, last_prospecting_action_at, last_monitor_action_at (any
present → engaged → ★ Customer). New regression test locks the field
names; updated the in-PR test + templates to match.

Co-Authored-By: Claude <noreply@anthropic.com>
…, not pull_leads

Live dogfood (#3779): 'I'm in San Francisco next Tuesday. Who's worth
meeting?' fired leadbay_pull_leads and rendered prose — no map — because
the map guarantee lives in leadbay_tour_plan, which was never called.
Root cause was routing, not rendering:
- pull_leads redirected geo intent to followups_map (Monitor-only, no
  map_locations) and listed 'who should I meet' as a POSITIVE example.
- tour_plan's triggers required 'tour'/'visiting in N days'; bare
  'I'm in <city>, who's worth meeting' matched nothing.

Redirect visit/who-to-meet intent from pull_leads → tour_plan, broaden
tour_plan triggers + positive examples to the bare phrasing, and
front-load the tour prompt's short_description with the trigger phrases
so Claude Desktop invokes it.

Co-Authored-By: Claude <noreply@anthropic.com>
…ap fallback

Live dogfood (#3779): routing now correctly calls leadbay_tour_plan, but
on Claude Desktop (no places_map_display_v0 in the tool set) the agent
fell back to a flat narrative ('Brooklyn Brewery — Broadway, 10018
(Midtown). Contact: …') — which the host's address auto-detector does
NOT parse into place cards, so no map of any kind rendered.

The fallback path said 'render place-card blocks' but never pinned the
exact shape. Prescribe it explicitly in both the prompt PHASE 2 and the
tool-description fallback: one '### Company · City, State' heading per
lead (badge + one-sentence + linked contact + bare phone), grouped by
mode — the heading-with-city is what triggers the Google-Place-card
carousel. Flag the flat-paragraph shape as WRONG.

Co-Authored-By: Claude <noreply@anthropic.com>
… of auto-rendering

Per Arty: a tour should present the leads, then PROACTIVELY offer to
plot them on a map ('Want me to put these on a map?'), and render only
on acceptance — for both 'plan a prospecting tour in <city>' and
'who's worth meeting in <city>'. Previously the prompt forced an
immediate auto-render and forbade asking; invert to propose-then-render.

- Prompt: PHASE 2 = present leads + offer map (route via ask_user_input_v0
  when available); PHASE 3 = render on yes (widget or place-card carousel);
  if the user asked for the map up front, skip the offer and render.
- Tool description + rendering_hint: present → offer → render-on-yes.
- WORKFLOWS WF35 inverted: asserts the proactive map OFFER (must_match map)
  instead of the old 'must NOT ask' guard.

Co-Authored-By: Claude <noreply@anthropic.com>
… for a street map

Dogfood (#3779): the map rendered as a schematic neighborhood scatter
(dots, no streets) instead of a real Google-style street map. Root cause:
the builtin-widgets gate told the agent to pass a city-only address and
omit latitude/longitude, so the host had nothing to street-geocode and
fell back to relative scatter. tour_plan already returns real pos[] +
location.full in map_locations — the guidance was throwing it away.

- gates/builtin-widgets: pass real latitude/longitude + full street
  address (city-only + no coords → scatter, not a street map).
- tour-plan RENDER: explicitly forbid dropping coords / shortening
  address to city when passing map_locations to the widget.
- trim prepare-outreach body to stay under the 17000-char budget.

Co-Authored-By: Claude <noreply@anthropic.com>
…real street map

Dogfood (#3779): the map rendered as a schematic neighborhood scatter,
not a real street map. The host's actual flow (per Claude's own widget
guidance) is TWO steps: places_search resolves each location to a real
place_id + coords/ratings FIRST, then places_map_display_v0 renders the
resolved places. We were skipping places_search and pushing raw lat/lng
straight to display — which is exactly what degrades to the scatter.

- gates/builtin-widgets: name the places_search + places_map_display_v0
  two-step (Itinerary mode for a tour).
- tour-plan RENDER + prompt PHASE 3: spell out the two-step — places_search
  per lead (company + full address) → display in Itinerary mode, badges in
  notes; never push raw coords straight to display.
- trim prepare-outreach body to stay under the 17000-char budget.

Co-Authored-By: Claude <noreply@anthropic.com>
…ng the map

Per Arty: before planning the tour + map, ask the user the scope via the
ask_user_input_v0 / AskUserQuestion tap-to-answer widget — (1) who to
include: mix / only known accounts / only fresh prospects, (2) enrich
contacts with no channel? New PHASE 1 fires the question widget BEFORE
leadbay_tour_plan and maps answers to real args (only-known →
discover_count:0, only-new → followups_count:0, enrich → run
leadbay_enrich_titles / prepare_outreach(enrich) on the top stops).
Skips the question if the user already stated scope. Phases renumbered
(clarify=1, build=2, present+offer=3, render=4, outreach=5, campaign=6).

Co-Authored-By: Claude <noreply@anthropic.com>
…t just the prompt

Dogfood (#3779): 'I'm going to NYC in 2 days, plan a prospecting tour'
went straight to building + mapping with no scope question. Cause: the
clarify-scope step lived only in the leadbay_plan_tour_in_city PROMPT,
but the agent called leadbay_tour_plan directly (bypassing the prompt),
so it never ran — same prompt-vs-tool gap as the routing fix.

Add a 'BEFORE YOU CALL — clarify scope' directive to the tour_plan tool
description itself (ask who-to-include + enrich via ask_user_input_v0,
map answers to followups_count/discover_count, skip only if the request
already states scope). Now fires whether or not the prompt is invoked.

Co-Authored-By: Claude <noreply@anthropic.com>
@ArtyETH06 ArtyETH06 force-pushed the ArtyETH06/3779-tour-plan-map-locations branch 3 times, most recently from 3aeee7a to 558c8b2 Compare June 23, 2026 23:15
@ArtyETH06 ArtyETH06 marked this pull request as ready for review June 23, 2026 23:28
@ArtyETH06 ArtyETH06 requested a review from milstan June 23, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant