Skip to content

refactor(backend): split agent into router + subgraphs; ask_location via interrupt; USE_SUBGRAPH toggle#3

Merged
FireTable merged 20 commits into
mainfrom
refactor/ask-location-addresult
Jun 25, 2026
Merged

refactor(backend): split agent into router + subgraphs; ask_location via interrupt; USE_SUBGRAPH toggle#3
FireTable merged 20 commits into
mainfrom
refactor/ask-location-addresult

Conversation

@FireTable

Copy link
Copy Markdown
Owner

What

Splits the monolithic agent graph into a router that dispatches to chat / weather subgraphs, with ask_location driven by LangGraph's interrupt() + frontend sendCommand() for human input. Adds a USE_SUBGRAPH topology toggle so the inlined workaround stays the default until @langchain/core ships a fix for the "Run ID not found in run map" bug that LangGraph JS subgraphs trigger under 1.2.1.

Why

  • One model node was carrying chat + weather with prompts that kept drifting. Splitting by intent lets each sub-agent own its tool surface (chat = search/fetch, weather = location/forecast).
  • ask_location needed a real human-input pause. interrupt() is the right primitive; the prior addResult path handled the same need by overwriting a sentinel ToolMessage, which still works in inlined mode.
  • LangGraph JS subgraphs trigger a known run-map bug under core 1.2.1 (see memory/langgraph-subgraph-run-map-bug.md). The inlined default is the workaround; flipping USE_SUBGRAPH=true restores the compiled-subgraph path when the upstream bug is fixed.

Notable changes

  • backend/agent.ts — two builders (buildSubgraph, buildInlined) gated by USE_SUBGRAPH. Shared router/rename/after nodes; only the model+tool loop differs.
  • backend/state.tsRouterAgentState (parent) + CommonAgentState (subgraph) schemas.
  • backend/agent/{chat-agent,weather-agent}.ts — compiled subgraphs for the USE_SUBGRAPH=true path.
  • backend/prompt/system.tsWEATHER_AGENT_PROMPT rewritten to a four-step linear flow with explicit one-tool-per-turn rules. Step 2 lets the model skip geocode_location when the picker already returned coords.
  • backend/tool/ask-location.ts — description generalised away from weather-specific wording.
  • components/assistant-ui/thread.tsxInterruptUI (gated on NEXT_PUBLIC_USE_SUBGRAPH + last-message), dispatches to toolkit registry by ui.
  • docs/INTERRUPT.md — generic interrupt contract; ask_location is one example.
  • memory/langgraph-subgraph-run-map-bug.md — updated for the toggle.
  • tests/frontend/interrupt-ui.test.tsx — new jsdom project under vitest.config.ts covers InterruptUI rendering, USE_SUBGRAPH gating, and addResult → sendCommand round-trip.
  • tests/backend/agent-topologies.test.ts — smoke test that both builders compile with the expected node set.

Test plan

  • pnpm test — 147 tests passing (added 6 frontend InterruptUI tests + 4 topology smoke tests + 1 router jsonSchema assertion).
  • pnpm lint && pnpm format:fix — clean.
  • Manual: trigger a weather turn end-to-end with both USE_SUBGRAPH=false (default) and USE_SUBGRAPH=true/NEXT_PUBLIC_USE_SUBGRAPH=true. Pick a city in the picker; verify the model gets the coords and produces the forecast widget.

🤖 Generated with Claude Code

FireTable and others added 20 commits June 23, 2026 23:11
Renames the node to `renameThreadAgentNode` (matching the new `-Agent` naming convention used by the router/chat/weather sub-agents) and pulls the inline Chinese system prompt out into `RENAME_THREAD_PROMPT` in `backend/prompt/system.ts` so all four agent prompts live in one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `CHAT_AGENT_PROMPT`, `ROUTER_AGENT_PROMPT`, and `WEATHER_AGENT_PROMPT` to the shared prompts file. These ship ahead of the agent wiring so the prompts are reviewable on their own; the consumer commits land next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewires the graph: `START → routerAgent → (weatherAgent | chatAgent) → afterAgent → END`, with `renameThreadAgent` fanning out from `START` like before. Adds the router and weather nodes, plus `shouldCallTool` (renamed from `routeAfterAgent`) and the deleted-then-replaced test. `chatModelWithoutThink` loses its `think: false` modelKwarg (unused by the active provider).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The three backend tools the weather sub-agent uses. `ask_location` is a UI marker that returns `{status: 'awaiting_user_location'}` for the frontend picker; `geocode_location` and `get_weather` wrap the new `lib/open-meteo.ts` client (geocode with a 3-step Open-Meteo → Nominatim → suffix-strip fallback; forecast via Open-Meteo's `/v1/forecast` endpoint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the assistant-ui toolkit, the two tool-call message parts (`AskLocationCard`, `WeatherCard`), the shared `tool-result` unwrapper, the loading skeleton, the vendored `WeatherWidget` runtime, and wires the toolkit into `useAui({ tools })`. Also flips `ToolGroupRoot` to `defaultOpen` so collapsed tool calls don't lose the weather canvas. Includes CLAUDE.md rule 6 covering the `mx-*` / `shadow-*` rules for components inside `ToolFallbackContent`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swaps `@assistant-ui/react-markdown/styles/dot.css` for an inlined keyframes block in `app/globals.css` with `--aui-content` overridden to `"\258d"` (typewriter caret) and `color: var(--muted-foreground)`. `markdown-text.tsx` keeps a commented-out import with a one-liner pointing to the inlined replacement. `observability-panel.tsx` gets a small oxfmt pass (line wrap + missing newline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…kpointId

The load callback now returns:
- state.tasks[0]?.interrupts so useLangGraphInterruptState() picks
  up a paused run (e.g. ask_location waiting for a location pick) on
  page refresh, not just the live stream path
- state.values.ui so persisted typedUi state is restored on reload

getCheckpointId gains explicit casts on state.values.messages and
the message id comparison: the SDK types DefaultValues too loosely
to let .messages compile, and the .every callback parameter was
implicit any.

Refs #4563 (resolves the reload half of the subgraph-interrupt
design gap; the live path still needs the runtime fix).
…e for subgraph updates

The ask_location tool now uses LangGraph interrupt() instead of
returning a placeholder. On the first call it throws GraphInterrupt
with { awaiting: 'location' }; on the resumed call (after the
frontend sends Command({ resume: <json> })) it parses the payload
and returns it as the tool result. A single ToolMessage is then
written by ToolNode — no more duplicate-message 400 from the old
addResult path.

AskLocationCard calls useLangGraphSendCommand({ resume: ... })
on user pick instead of addResult. The { status: 'awaiting_user_location' }
variant is dropped from AskLocationResult — the tool never returns
that shape anymore.

A pnpm patch on @assistant-ui/react-langgraph@0.14.9 makes
useLangGraphMessages run extractMessagesFromUpdates + setInterrupt
on the namespaced updates branch instead of dropping the event
after firing onSubgraphUpdates. Without this, the AIMessage with
tool_calls and the __interrupt__ payload both reach the browser
but the runtime silently throws them away, so the toolkit cannot
find a tool-call part to render and useLangGraphInterruptState()
returns undefined during a live stream. The reload path was
already covered by load() returning state.tasks[0].interrupts
(see b151521). Refs #4563.
…esult

Drop the interrupt() + sendCommand path. The tool body now returns its sentinel synchronously; the ToolNode writes the ToolMessage that the card keys on, and the user's pick comes back as an overwritten tool result via addResult. The runtime patch, the askLocationResumeNode, and the card's sendCommand toggle are all gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enforce the strict order: ask_location first (only if no place named), then geocode_location with that name, then get_weather, then a one-sentence reply. Each step is one tool call, no batching. The previous prompt listed tools in the wrong visual order and let the model chain ask_location with geocode_location in the same turn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… interrupt

Split the monolithic StateGraph into a router that dispatches to a chat subgraph or a weather subgraph, each owning its own model + ToolNode. ask_location now pauses via LangGraph interrupt() and the frontend's addResult resumes with the pick. Drop two patches no longer needed: @assistant-ui/react-langgraph 0.14.10 ships the subgraph-update fix, @assistant-ui/core 0.2.19 has the part.text?.trim() guard. Bump 7 patch-level deps and addResult-aware tests.
Cosmetic-only reformat from pnpm format:fix. No logic changes:
- package.json: trailing newline stripped by pnpm add
- app/assistant.tsx: useMemo call folded to one line
- backend/agent.ts: blank line + trailing newline
- backend/model.ts: trailing blank line removed
- backend/node/router-agent-node.ts: invoke() chain split across lines

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
USE_SUBGRAPH=true uses compiled weatherAgent/chatAgent subgraphs; the default inlined version flattens their model/tool loops into the parent graph. The inlined path is the workaround for the @langchain/core 1.2.1 EventStreamCallbackHandler "Run ID not found in run map" bug that LangGraph JS subgraphs trigger (see memory/langgraph-subgraph-run-map-bug.md).

Ask-location card now resumes interrupts via useLangGraphSendCommand and InterruptUI is gated on USE_SUBGRAPH — interrupt flow only applies in subgraph mode.
ask_location was hardcoded to weather (description, default message, prompt forced geocode after picker). Generalised the description + default message and let WEATHER_AGENT_PROMPT skip geocode_location when the picker already returned coords.

Frontend env var reads NEXT_PUBLIC_USE_SUBGRAPH (Next.js inlines NEXT_PUBLIC_* vars); InterruptUI tightens a few any-casts and wires addResult to sendCommand for the subgraph-mode resume path.

vitest splits into node + frontend (jsdom) projects; tests/frontend/interrupt-ui.test.tsx covers the isLast/USE_SUBGRAPH gate, toolkit dispatch, and the addResult → sendCommand round-trip.

CLAUDE.md rewritten to reflect the current router + dual-topology architecture; docs/INTERRUPT.md describes the interrupt contract (ask_location is one example, not the only one).

buildSubgraph/buildInlined exported for the agent-topologies smoke test.
@FireTable FireTable merged commit eeb2e4e into main Jun 25, 2026
@FireTable FireTable deleted the refactor/ask-location-addresult branch June 25, 2026 15:00
@FireTable FireTable restored the refactor/ask-location-addresult branch June 30, 2026 09:38
@FireTable FireTable deleted the refactor/ask-location-addresult branch June 30, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant