Skip to content

fix(cockpit-langgraph): make the subgraphs example demonstrate real nesting - #838

Merged
blove merged 1 commit into
mainfrom
blove/cockpit-subgraphs-delegation-93fe7b
Aug 27, 2026
Merged

fix(cockpit-langgraph): make the subgraphs example demonstrate real nesting#838
blove merged 1 commit into
mainfrom
blove/cockpit-subgraphs-delegation-93fe7b

Conversation

@blove

@blove blove commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The problem

cockpit/langgraph/subgraphs advertised subagent delegation it could never perform.

The sidebar could never populate. app.config.ts set no subagentToolNames, and the graph added a compiled subgraph as a plain node — which emits a research:<uuid> namespace, not tools:<id>. SubagentTracker only registers delegation tool calls (libs/langgraph/src/lib/internals/subagent-tracker.ts:78-112,265-269), so agent.subagents() was permanently empty and the sidebar always rendered "No active subagents". The manual e2e asserted exactly that empty state and never asserted it populated.

No delegation happened. The docstring claimed "the parent orchestrator decides when to delegate," but the edges were unconditional (orchestrate → research → END). Both graphs were bare MessagesState, so the child inherited and appended to the parent's messages.

The decision: honest nesting, not tool-call delegation

  • cockpit/chat/subagents already demonstrates the tool-call path end to end (task tool + subagent_type + ToolNode loop + sidebar). Converting this example would have made it a near-duplicate and deleted the repo's only demo of LangGraph's actual composition primitive.
  • The website docs already state the correct rule — docs/langgraph/guides/subgraphs.mdx:114: "Plain subgraph nodes do not appear in this map." The example was contradicting published docs; it now agrees with them.
  • The example is filed under topic subgraphs, not subagents.

So: keep the plain subgraph-as-node pattern, but make it teach something real.

Changes

Backend (python/src/graph.py)

  • orchestrate makes a structured routing decision; a conditional edge enters the child graph or skips to answer. The parent actually decides.
  • A real state boundary: the child's ResearchState has no messages key. LangGraph wires the subgraph node through the keys the two schemas share (research_topic, research_brief), so the child gets a topic, returns a brief, and can neither read nor append to the transcript.
  • answer is the sole transcript-writing node.

Frontend

  • Sidebar reads the parent's own state through a typed agent.value() instead of the map that can never fill — and the component doc says plainly why subagents() is the wrong signal here, pointing at Chat Subagents for the tool-call path.
  • transcriptNodeNames: ['answer'].

Docspython/docs/guide.md rewritten (the false stream.subagents() claim at :112 is gone; a <Warning> now states the subgraph-vs-subagent distinction up front). Both prompts/subgraphs.md corrected; the Angular one no longer claims a <chat-subagent-card> per subgraph invocation.

Drive-by: angular/src/index.ts codeAssetPaths pointed at a nonexistent src/app.component.ts.

Tests

Three e2e specs replace the single smoke test. No test asserts an empty state as its only assertion:

  1. Research branch routes into the child and the sidebar shows the topic and the returned brief.
  2. The child's brief never reaches the transcript — paired with a positive sidebar assertion on the same marker, so it cannot pass vacuously, plus an exact message count.
  3. Greeting skips the child and still answers.

The manual live-LLM spec now asserts the branch that ran, not an empty sidebar.

Verification

  • nx e2e cockpit-langgraph-subgraphs-angular3/3 green.
  • nx build --configuration=production — green.
  • Live model in Chrome, both branches driven by hand. Research path: sidebar flips to "Nested", real topic populates, brief streams in, transcript stays at [user, assistant] with the brief absent from the live DOM. Direct path: sidebar stays "Direct", answer arrives.
  • Wire-level check of the raw run stream confirmed the namespaces: 64 top-level messages events from orchestrate, 767 under messages|research:<uuid> from the child, 633 from answer.
  • Mutation-tested transcriptNodeNames. Removing it does not fail the aimock e2e — fixture replay is atomic, so the leak is invisible there. Against a live model it is clearly load-bearing: message count transiently reaches 3 (the child's internal brief rendering as its own chat bubble) before the parent's values event collapses it back to 2. With the option, the count never exceeds 2 at any sampled point. The end state self-corrects, which is why no final-state assertion can catch it — recorded in a comment at the call site rather than claimed as test coverage.

Note on guide.md:112

Checked for the separate in-flight correction mentioned in the task: no open PRs, and the only related branch (blove/langgraph-subgraphs-post) touches website content only. Nothing had landed, so this PR corrects the claim as part of the guide rewrite.

🤖 Generated with Claude Code

…esting

The `cockpit/langgraph/subgraphs` example advertised subagent delegation it
could never perform. `app.config.ts` set no `subagentToolNames`, and the graph
added a compiled subgraph as a plain node — which emits a `research:<uuid>`
namespace, not `tools:<id>`. The SubagentTracker only registers delegation
tool calls, so `agent.subagents()` was permanently empty and the sidebar always
rendered "No active subagents". The manual e2e asserted exactly that empty
state and nothing else. The parent's edges were unconditional, so nothing
decided anything, and both graphs were bare `MessagesState` with no boundary.

Kept the plain subgraph-as-node pattern rather than converting to the tool-call
path: `cockpit/chat/subagents` already demonstrates tool-call delegation end to
end, and this is the repo's only example of LangGraph's actual composition
primitive. The website docs already state the rule
(docs/langgraph/guides/subgraphs.mdx:114 — "Plain subgraph nodes do not appear
in this map"); the example now agrees with them instead of contradicting them.

Backend:
- `orchestrate` makes a structured routing decision; a conditional edge enters
  the child graph or skips straight to `answer`.
- The child's state (`ResearchState`) has no `messages` key, so it exchanges
  only `research_topic` / `research_brief` with the parent and can neither read
  nor append to the transcript.
- `answer` is the sole transcript-writing node.

Frontend:
- Sidebar reads the parent's own state via a typed `agent.value()` instead of
  the map that can never fill, and says plainly why.
- `transcriptNodeNames: ['answer']`. Verified live: without it the message list
  transiently grows to 3 as the child's tokens merge in and its internal brief
  renders as a chat bubble, before the parent's `values` event collapses it
  back to 2. Self-correcting end state, so no final-state assertion catches it.

Tests: three e2e specs replacing the smoke test — the research branch populates
the sidebar, the child's brief never reaches the transcript (paired with a
positive sidebar assertion so it can't pass vacuously), and the direct branch
skips the child and still answers. The manual live-LLM spec asserts the branch
that ran rather than an empty state.

Verified: e2e 3/3 green; production build green; both branches driven against a
real model in Chrome, with the transcript boundary checked from the live DOM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview Aug 27, 2026 2:43pm

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit f4c7b80 into main Aug 27, 2026
28 checks passed
blove added a commit that referenced this pull request Aug 27, 2026
…en Not To' blog post (#839)

* docs: design spec for LangGraph subgraphs blog post

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: implementation plan for LangGraph subgraphs post

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(website): add 'LangGraph Subgraphs' blog post

* docs: handoff for the GSC-driven blog sequence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(website): correct attribution + streamSubgraphs claims in subgraphs post

Applies blocking review findings:
- Attribution is structural (namespace segment IS the tool call id), not a
  heuristic; the description ladder is unreachable in both shipped graphs.
- streamSubgraphs is the LangGraph JS SDK's own option name, not a naming
  trap; re-aimed at the Python subgraphs=True kwarg.
- Test asserts outcome 'interrupted', not phase.
- Concede non-observability split reasons, then reframe.
- Quote evidence in fences; add controlled-experiment framing for AG-UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): editorial pass on subgraphs post

- Drop the false 'no independent checkpointing' cost: both subgraph children
  compile bare, while the flat AG-UI graph is the one with MemorySaver.
- Handle the checkpoint objection honestly — child checkpointing rides the
  namespace under a parent checkpointer, which is the thesis again.
- State that the AG-UI control group is itself a LangGraph StateGraph, so
  only one variable differs.
- Answer the 'just emit custom events' objection.
- Quote the rejected design-doc alternative in a fence.
- Trim demonstrative tic (That's 13->7, Here's 7->3), restore Let's transitions,
  cut duplicated state-isolation and heuristic statements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): account for the examples/ag-ui counterexample

Spec review found a fifth graph the post's thesis didn't survive:
examples/ag-ui compiles a child StateGraph on a transport that ALREADY
emits subagent_activity events, so the split isn't buying observability.
It splits because the child has its own ResearchState (topic + iterations)
and its own agent->tools loop with an iteration cap.

- Add 'So when does a split earn itself?' — two demos on one transport,
  one flat and one split, with child shape as the variable.
- Stop claiming no such case exists in the closing.
- Name cockpit/ag-ui/subagents so it isn't read as examples/ag-ui.
- Last-resort rung accepts pending OR running (tracker:165-169).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): sharpen the split discriminator to control flow

Re-review findings:
- A custom child state schema doesn't discriminate: examples/chat and
  cockpit/chat/subagents both define child TypedDicts yet are single-node
  straight lines. Only control flow separates the earned split.
- Drop hand-rolled streaming as a cost of flatness — examples/ag-ui pays it
  too, so it's a cost of the AG-UI transport, not of staying flat.
- Reconcile the two conflicting 'one variable' claims.
- 'neither graph we ship' was stale now that three tool-dispatch a child.
- Closing no longer claims only two justifications exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): re-check subgraphs post against the landed cockpit example

cockpit/langgraph/subgraphs (#838) now ships a conditional plain-node
subgraph whose child state deliberately omits `messages`.

- Fix the ladder-unreachability reason: it no longer holds for every graph
  by way of an empty message list — the plain-node child has no messages
  key at all.
- Cite that example as first-hand evidence for 'isolation is something you
  design', which until now the post asserted without a concrete instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: refresh blog-sequence handoff for shipped post #1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Aug 28, 2026
…odes (#842)

The subgraphs post tells readers that child text lands in the parent
transcript unless `filterSubagentMessages` opts out. That holds for a child
dispatched from a `@tool` body, but not for the graph shape the post spends
its best section on.

`filterSubagentMessages` is checked inside a branch guarded by
`isSubagentNamespace()`, which matches only `tools:`. A plain subgraph node
emits `research:<uuid>`, never enters that branch, and so ignores the option
entirely — its tokens merge into the transcript no matter how it's set. The
lever for that shape is `transcriptNodeNames`.

This matters because the post showcases `cockpit/langgraph/subgraphs`, which
is exactly the plain-node shape. A reader who follows it, sees the child's
brief appear as its own bubble, and reaches for `filterSubagentMessages` will
find it does nothing.

Also records the failure mode, which is the expensive part: the leak is
mid-stream with a clean end state. The parent's final `values` event rewrites
the message list from authoritative graph state, so the stray bubble
disappears once the run settles. A final-state assertion cannot catch it —
measured against a live model in #838, where the message count transiently
hit 3 before collapsing to 2.

Verified: website builds; blog + sitemap specs 26/26.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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