diff --git a/apps/website/content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx b/apps/website/content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx new file mode 100644 index 000000000..c25f28a3e --- /dev/null +++ b/apps/website/content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx @@ -0,0 +1,101 @@ +--- +title: 'LangGraph Subgraphs: When to Split a Graph and When Not To' +description: 'When a LangGraph subgraph earns its complexity, how state crosses the boundary, and what your UI sees while a child graph runs.' +date: 2026-08-27 +tags: [langgraph, subgraphs, agents, streaming, angular] +author: brian +featured: false +draft: false +--- + +Most people reach for a LangGraph subgraph expecting a _state_ boundary, and what they actually get is an _observable_ one. + +That's the whole post, so let's start there and then earn it. + +If your question is "single agent, approval loop, or multi-agent?", that's an architecture question and the [decision matrix](/docs/langgraph/concepts/agent-architecture) already answers it. +This post is about the layer underneath: what a subgraph actually changes at runtime, why our own graphs got split, and what the frontend sees while a child is running. + +## What does a subgraph actually give you? + +Nested execution and namespaced stream events. That's the honest list. + +Let's start with the canonical pattern, which is small. Compile a child `StateGraph`, then add the compiled graph as a node in the parent: + +```python +research_builder = StateGraph(MessagesState) +research_builder.add_node("search", search_web) +research_builder.add_edge(START, "search") +research_subgraph = research_builder.compile() + +builder = StateGraph(MessagesState) +builder.add_node("research", research_subgraph) # a compiled graph, used as a node +``` + +Two things genuinely change. The child runs as its own graph, with its own nodes and its own step sequence rather than being flattened into the parent's. And LangGraph emits the child's stream events under a namespace, so a consumer can tell parent output from child output. + +Here's the part I think gets assumed and shouldn't: state isolation is not one of them. + +If parent and child share `MessagesState`, the child appends to the same message list the parent is building. Nothing about `add_node` fenced anything off. Isolation is something you design — give the child its own state schema, then map in at the boundary and map the result back out. That's a decision you make and maintain, not a property `compile()` hands you. + +So if you're splitting purely to keep state tidy, be honest that you're the one who has to keep it tidy. + +The mechanism is worth knowing, because it's how you draw the boundary. LangGraph passes state into and out of a subgraph node through the keys the two schemas **share**. So the boundary isn't a wall you erect; it's the shape of the overlap. Give the child a schema with no `messages` key and it cannot read the transcript or append to it — not because anything blocked it, but because there's no channel for it to travel on. + +## Why do people really split? + +In our own repo, the honest answer is: so the frontend can see the delegation. + +Let's look at the evidence, because we wrote it down. + +The comment sitting above the research subagent in our canonical `examples/chat` graph says the quiet part out loud — running it as an actual subgraph rather than inline logic is what causes LangGraph to emit stream events under a `tools:` namespace for the child run, which is what our `@threadplane/langgraph` `SubagentTracker` keys on to populate `agent.subagents()`. That's not a state argument. That's a visibility argument. + +The design doc for that feature is even more direct. It lists a plain `@tool` returning a synthesized "subagent" payload as an approach considered, and rejects it for exactly one reason: no subgraph runs, so no `tools:` namespace events are emitted, so the card would render empty. Simpler graph code, invisible to the UI, rejected. + +Then there's the conversion. Our `cockpit/chat/subagents` demo originally ran its three specialists as a flat in-process helper. It was rewritten to dispatch a real compiled child graph — and the design doc frames the whole change as a demo-wiring gap: the flat version emitted no namespace events, so `subagents()` stayed empty and no card rendered. A working feature was restructured so a UI card would appear. + +One nuance worth naming, since it cuts against the tidy mental model. In both of those graphs the compiled child is invoked from inside a `@tool` body, not wired in as a plain node. That's deliberate: the tool call is what the tracker registers, and our own docs are blunt that [plain subgraph nodes](/docs/langgraph/guides/subgraphs) don't show up in that map at all. So "subgraph" and "tracked subagent" aren't the same thing — the subgraph is what makes the events observable, and the tool call is what gives them a name. + +The repo now carries the other half of that pair. `cockpit/langgraph/subgraphs` composes a compiled child as a plain node, gives it a schema with no `messages` key, and routes into it conditionally. Its sidebar reads the parent's own state through `agent.value()`, precisely because `subagents()` stays empty for that shape no matter what you configure. Two demos, two mechanisms, and the naming finally lines up with what each one does. + +## What does the frontend see while a child runs? + +Namespaced events — and nearly everything interesting downstream follows from that one fact. + +Let's take it from the wire inward. The event type carries the namespace after a pipe, so the base type is the part before it: + +```text +messages # parent +messages|tools:call-1 # child run dispatched by tool call "call-1" +``` + +Our transport requests those child streams by default: `streamSubgraphs` defaults to `true`, so it's opt-out, not opt-in. Small naming trap if you're coming from the raw SDK docs — the option on our config is `streamSubgraphs`, not `subgraphs: true`. + +Now the hazard. A child graph terminates before the parent does, and a child's terminal event looks an awful lot like the parent's. Without a namespace guard, that child terminal marker gets read as "the run finished" and closes out the parent's still-streaming assistant message. We guard it by refusing namespaced events as top-level terminal evidence, and there's a test that feeds a namespaced terminal marker in and asserts the parent message settles with outcome `interrupted` rather than a clean completion. If you ever write a transport against this stream yourself, that's the bug you'll hit, and it will look like truncation rather than a namespace bug. + +Child text also lands in your main transcript by default. `filterSubagentMessages` is optional and off unless you set it, so a child's tokens flow into `messages()` alongside the parent's. Turning it on is usually what you want once you're rendering the child separately, otherwise the same content shows up twice. + +Read that option's name carefully, though, because it does less than it sounds like. The filter sits inside a branch guarded by the `tools:` namespace check — so for a plain subgraph node, whose namespace looks like `research:`, it never fires at all. Its tokens merge into the transcript and `filterSubagentMessages` will not stop them. The lever for that shape is `transcriptNodeNames`, which whitelists the graph nodes whose messages count as transcript. + +What makes this one nasty is that it's a mid-stream bug with a clean end state. The parent's final `values` event rewrites the message list from authoritative graph state, so the extra bubble disappears on its own once the run settles. Assert on the finished DOM and everything looks right; watch the streaming pass and you'll see the child's internal notes render as their own message and then vanish. A final-state test cannot catch it. + +Attribution, meanwhile, is a heuristic and I'd rather you know that than discover it. The tracker maps a child namespace onto a parent tool call by comparing the child's first human message against the tool call's `description` argument: exact match first, then substring in either direction, then — if nothing matched — a last-resort fallback to any unmapped subagent that's still pending or running. That fallback is doing real work in practice, since a delegation tool doesn't have to take a `description` argument at all. It's good enough for the demos we ship. It is not structural, and a graph that fans out several look-alike children would be leaning on it hard. + +One more limit, stated plainly: only the _first_ `tools:` segment of a namespace is read. A subagent that itself delegates will have its inner events attributed to the outer tool call. Deeper nesting isn't exercised anywhere in this repo, so treat it as untested rather than supported. + +## When should you not split? + +When there's no observable boundary to draw and no genuinely divergent state. + +Let's use the cleanest control group we have. Our AG-UI demo ships the same three-subagent feature as the LangGraph one — same roles, same cards in the UI — with no subgraph anywhere. The specialists are a flat `async` helper, and progress reaches the frontend through a custom `subagent_activity` event dispatched from the tool body. + +Nothing was compromised by staying flat. The AG-UI transport already carries a first-class delegation event, so there was no structural workaround to perform. Same feature, same UI, one fewer graph. + +That's the test I'd apply. If your transport already has a way to say "a child is working right now," or your UI doesn't render per-child progress at all, then a subgraph is a boundary you now have to defend: an extra state schema, mapping at both edges, and one more place to look when a message goes missing. + +And splitting because a region of the graph _feels_ like a separate concern isn't a reason on its own. A node is already a unit. For me, the bar is whether something outside the graph needs to observe the child as a distinct run. + +## Conclusion + +Here's the heuristic. Split when something outside the graph needs to see the child run as its own thing — a card, a progress panel, per-child streaming. Split when the child genuinely needs a different state schema and you're willing to own the mapping at both edges. Don't split for tidiness, and don't assume the split isolated state, because with a shared `MessagesState` it didn't. + +The [architecture matrix](/docs/langgraph/concepts/agent-architecture) covers the tiering question, the [subgraphs guide](/docs/langgraph/guides/subgraphs) has the composition and `subagents()` wiring, and [What injectAgent() Actually Returns](/blog/what-inject-agent-returns) walks the signal surface those child streams land in. diff --git a/docs/superpowers/plans/2026-08-27-langgraph-subgraphs-post.md b/docs/superpowers/plans/2026-08-27-langgraph-subgraphs-post.md new file mode 100644 index 000000000..9b0eddb70 --- /dev/null +++ b/docs/superpowers/plans/2026-08-27-langgraph-subgraphs-post.md @@ -0,0 +1,154 @@ +# LangGraph Subgraphs Blog Post Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish a first-hand essay for the `langgraph subgraphs` query (41 impressions, position 32.3, nothing of ours competing) arguing that the boundary a subgraph really buys you is observability, not state. + +**Architecture:** One new MDX file in `apps/website/content/blog/`. No code changes. At most two short code blocks (Python is fine — audience is Python-first). + +**Tech Stack:** MDX blog content, Next.js website (`apps/website`), vitest for content validation. + +**Spec:** `docs/superpowers/specs/2026-08-27-langgraph-subgraphs-post-design.md` +**Branch:** `blove/langgraph-subgraphs-post` (off main at `d2e5ce73`) + +--- + +## Verified facts (I re-read each of these in source; drafter must still open them) + +**The observability motive, in our own code** — `examples/chat/python/src/graph.py:276-281`, verbatim: + +``` +# Research subagent — a small compiled child graph the parent dispatches +# via the `research` @tool. Running it as an actual subgraph (vs. inline +# logic) is what causes LangGraph to emit stream events under namespace +# prefix `tools:` for the child run, which is what the @threadplane/langgraph +# SubagentTracker keys on to populate `agent.subagents()`. +``` + +**Namespace gating** (`libs/langgraph/src/lib/internals/subagent-tracker.ts`): +- `isSubagentNamespace()` :265-269 — a segment must start with `tools:`. +- `extractToolCallIdFromNamespace()` :271-277 — returns the FIRST `tools:` segment only, so nested delegation attributes inner events to the outer call. +- `matchSubgraphToSubagent()` :130-175 — exact description match, then substring either direction, then the fallback at :165-169: any unmapped subagent whose status is `pending` or `running`. Heuristic, not structural. + +**Streaming defaults:** +- `streamSubgraphs: streamSubgraphs ?? true` — `libs/langgraph/src/lib/transport/fetch-stream.transport.ts:154`. Subgraph streaming is opt-OUT. The SDK option is `streamSubgraphs`, not `subgraphs: true`. +- `filterSubagentMessages?: boolean` — `libs/langgraph/src/lib/agent.types.ts:283`, doc comment "When true, subagent messages are filtered from the main messages signal." Optional, so default OFF: subagent chatter lands in the parent transcript unless you opt in. +- Events are `|`-delimited (`messages|tools:call-1`), parsed in `fetch-stream.transport.ts:191-198` and `stream-manager.bridge.ts:1311-1318`. +- Namespaced terminal events would close out the parent's streaming message without a guard — `stream-manager.bridge.ts:314-330`, with `stream-manager.bridge.spec.ts:474-505` asserting outcome `interrupted`. + +**The cross-transport A/B:** +- LangGraph side: `cockpit/chat/subagents` uses a real subgraph; converted from flat to subgraph in PR #718 (spec `docs/superpowers/specs/2026-06-19-cockpit-subagents-subgraph-design.md`) so a UI card would render. +- AG-UI side: `cockpit/ag-ui/subagents/python/src/graph.py` — flat `async def _run_subagent(...)` at :107 and `adispatch_custom_event("subagent_activity", {...})` at :166-168. No subgraph. Same feature. +- Rejected alternative on record: `docs/superpowers/specs/2026-05-08-*-subagents-design.md:23` — a plain `@tool` returning a synthesized payload was rejected because no `tools:` namespace events are emitted when no subgraph runs, so the card would render empty. + +**Docs to link, not restate:** `apps/website/content/docs/langgraph/concepts/agent-architecture.mdx:626-696` (three-tier breakdown + decision matrix) and `apps/website/content/docs/langgraph/guides/subgraphs.mdx` (note :114 — "Plain subgraph nodes do not appear in this map"). + +**Hard prohibitions** (from the spec): +- Do NOT claim `cockpit/langgraph/subgraphs` demonstrates delegation or populates the sidebar — it does neither (unconditional edge `orchestrate → research → END` at `python/src/graph.py:57-58`; no `subagentToolNames` in `angular/src/app/app.config.ts`). Two separate tasks are correcting that example; the post must not depend on or contradict their outcome. Cite `cockpit/chat/subagents` and `examples/chat` for working delegation instead. +- Do NOT speculate about split performance/latency, parallel subagent fan-out, or child-specific checkpointers/state schemas. Nothing in the repo measures or exercises those. + +--- + +### Task 1: Author the post + +**Files:** +- Create: `apps/website/content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx` + +- [ ] **Step 1: Frontmatter** + +```yaml +--- +title: 'LangGraph Subgraphs: When to Split a Graph and When Not To' +description: 'When a LangGraph subgraph earns its complexity, how state crosses the boundary, and what your UI sees while a child graph runs.' +date: 2026-08-27 +tags: [langgraph, subgraphs, agents, streaming, angular] +author: brian +featured: false +draft: false +--- +``` + +Description is 127 chars (≤155). The drafter may sharpen the title toward the observability thesis but MUST keep "Subgraphs" early for the query, and must re-count the description if changed. No licensing callout. + +- [ ] **Step 2: Body** + +1. **Lede** (no header): restate the searcher's question, then land the thesis early — the boundary a subgraph really draws is usually observability, not state. +2. `## What does a subgraph actually give you?` — first line answers. The canonical pattern (compile a child, add it as a node). What genuinely changes: nested execution and namespaced stream events. What does NOT change automatically: state isolation — if parent and child share a `MessagesState`, the child appends to the parent's list. Link the architecture matrix for the tiering question rather than restating it. At most one short Python block here. +3. `## Why do people really split?` — the thesis, evidenced: quote or closely paraphrase the `examples/chat/python/src/graph.py:276-281` comment (attribute it as our own code), the rejected `@tool` alternative, and the flat→subgraph conversion done so a card would render. +4. `## What does the frontend see while a child runs?` — the differentiated section. Namespaced `|`-delimited events; `streamSubgraphs` defaults on; without namespace guards a child's terminal event closes the parent's message; `filterSubagentMessages` defaults off so child text lands in the parent transcript; attribution is heuristic (`matchSubgraphToSubagent` falls back to any unmapped pending/running subagent) and nested `tools:` collapses to the outer call — say plainly that deeper nesting is untested here. +5. `## When should you not split?` — the honest counterweight. If you don't need the observable boundary and don't have genuinely divergent state, a subgraph adds a boundary you then have to defend. Cite AG-UI doing the same feature flat because its transport has a first-class delegation event. +6. `## Conclusion` — the heuristic in a short paragraph; forward links to `/docs/langgraph/concepts/agent-architecture`, `/docs/langgraph/guides/subgraphs`, and `/blog/what-inject-agent-returns`. No CTA. + +- [ ] **Step 3: Voice pass** + +`docs/gtm/voice.md` with the 2026 technical override. Register references: `apps/website/content/blog/2026-08-26-what-inject-agent-returns.mdx` and `apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx` — this post must read as the same author. Checklist: title-restating lede, no "Introduction" header, contractions, 1–3-line paragraphs, H2-as-question answered in its first line, ≥1 "Let's" per major section, opinions flagged ("I think"/"For me"), no invented anecdotes, no emoji, no hype, no CTAs. Don't copy sentences from our docs pages verbatim. + +- [ ] **Step 4: Accuracy pass** + +- Open every file cited in Verified Facts and confirm the claim before it ships. Cite behavior, not line numbers, in the prose — line numbers rot. +- Confirm the hard prohibitions above are respected. +- Published-release check: `npm pack @threadplane/langgraph@latest @threadplane/chat@latest` in the scratchpad; grep the `.d.ts` for any public member named (`subagents`, `filterSubagentMessages`, `subagentToolNames`, `streamSubgraphs`). Drop main-only members. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx +git commit -m "feat(website): add 'LangGraph Subgraphs' blog post" +``` + +--- + +### Task 2: Validate + +- [ ] **Step 1: Frontmatter + description length** + +```bash +cd apps/website && node -e " +const matter = require('/Users/blove/repos/angular-agent-framework/node_modules/gray-matter'); +const fs = require('fs'); +const f = matter(fs.readFileSync('content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx','utf8')); +console.log('desc length:', f.data.description.length); +if (f.data.description.length > 155) throw new Error('description too long'); +if (!f.data.title || !f.data.date || f.data.author !== 'brian') throw new Error('frontmatter incomplete'); +console.log('OK'); +" +``` + +- [ ] **Step 2: Test suite** (`nx test website` does not exist): + +```bash +cd apps/website && npx vitest run --config vite.config.mts +``` + +`blog.spec.ts` and `sitemap-dates.spec.ts` must pass. Known pre-existing failures, do NOT fix, confirm the count is unchanged at 5: `PostCard.spec.tsx` (1), `Differentiator.spec.tsx` (1), `thanks/page.spec.tsx` (3). + +- [ ] **Step 3: Render check** — `npx next dev -p 3111` from `apps/website` in the background, then curl: + - `/blog/langgraph-subgraphs-when-to-split` → 200, correct ``, meta description matches frontmatter, code blocks carry `data-language` with a theme + - `/blog` lists the post + + Kill the server, confirm port 3111 free, and `git checkout apps/website/next-env.d.ts` if the dev server modified it. + +- [ ] **Step 4: Commit fixes** (skip if none): + +```bash +git add -A apps/website/content/blog/ && git commit -m "fix(website): render fixes for subgraphs post" +``` + +--- + +### Task 3: PR and merge + +- [ ] **Step 1: Push and open PR** + +```bash +git push -u origin HEAD +gh pr create --title "feat(website): add 'LangGraph Subgraphs' blog post" --body "Third post of the GSC-driven blog sequence (spec: docs/superpowers/specs/2026-08-27-langgraph-subgraphs-post-design.md). + +Targets \`langgraph subgraphs\` — 41 impressions at position 32.3, the second-highest-impression query on the site with nothing of ours competing. Argues from first-hand repo evidence that a subgraph's real payoff on LangGraph is an observable boundary rather than a state boundary. + +🤖 Generated with [Claude Code](https://claude.com/claude-code)" +``` + +- [ ] **Step 2: Merge on green, then verify production** + +Arm auto-merge (`gh pr merge <n> --squash --auto`); only `Vercel – threadplane` gates. After merge, confirm `origin/main` carries the final content, then poll `https://threadplane.ai/blog/langgraph-subgraphs-when-to-split` until it returns 200 and spot-check the live title and meta description. Do not report "shipped" before the production URL answers 200 — a merge is not a deploy. diff --git a/docs/superpowers/specs/2026-08-27-langgraph-subgraphs-post-design.md b/docs/superpowers/specs/2026-08-27-langgraph-subgraphs-post-design.md new file mode 100644 index 000000000..109d193c9 --- /dev/null +++ b/docs/superpowers/specs/2026-08-27-langgraph-subgraphs-post-design.md @@ -0,0 +1,59 @@ +# Post #1 design: LangGraph subgraphs — the observability boundary + +**Date:** 2026-08-27 +**Status:** Approved (angle + scope approved in session; third post of the sequence in `docs/superpowers/specs/2026-08-26-blog-sequence-inject-agent-design.md`) + +## Intent and evidence + +`langgraph subgraphs` — 41 impressions, position 32.3, 0 clicks. Second-highest impression query on the site and we're effectively invisible for it. Nothing of ours competes. + +This is the "grow the search surface" play: the searcher is a LangGraph-core engineer, often Python-first, not necessarily looking for an Angular UI framework. Per the candidates doc, serving them well may mean content only incidentally about Threadplane. That's the deliberate choice here. + +**Slug:** `langgraph-subgraphs-when-to-split` +**File:** `apps/website/content/blog/2026-08-27-langgraph-subgraphs-when-to-split.mdx` +**Title:** working title "LangGraph Subgraphs: When to Split a Graph and When Not To" — the drafter may sharpen it toward the observability thesis, but must keep "subgraphs" early in the title for the query. +**Meta description (≤155 chars):** "When a LangGraph subgraph earns its complexity, how state crosses the boundary, and what your UI sees while a child graph runs." +**No licensing callout.** + +## The angle: the observability boundary + +**Rejected:** a generic "when to split" decision essay. `apps/website/content/docs/langgraph/concepts/agent-architecture.mdx:626-696` already carries a three-tier breakdown and a decision matrix; a generic post would co-rank against our own docs and lose the first-hand advantage. Link that section for the architecture question instead of restating it. + +**The thesis:** most people reach for a subgraph expecting a *state* boundary, but in practice — at least on LangGraph — the thing a subgraph actually buys you is an *observable* boundary. That is the claim only we can make, and the repo says it out loud. + +Supporting first-hand material (all verified; cite by path): + +1. **Our own code says the motive is observability.** `examples/chat/python/src/graph.py:277-281` states that running the work as a real subgraph "is what causes LangGraph to emit stream events under namespace prefix `tools:<id>`… which the SubagentTracker keys on." The subgraph exists for the UI. +2. **A controlled A/B across transports.** The same three-subagent feature exists twice: with a subgraph on LangGraph (`cockpit/chat/subagents`) and with no subgraph at all on AG-UI (`cockpit/ag-ui/subagents/python/src/graph.py` — flat `_run_subagent()` at :107-135 plus `adispatch_custom_event("subagent_activity", …)` at :167). Same feature, opposite structural answer, because AG-UI's transport has a first-class delegation event and LangGraph's doesn't. +3. **A rejected alternative on record.** `docs/superpowers/specs/2026-05-08-*-subagents-design.md:23` rejected a plain `@tool` returning a synthesized payload because "no `tools:` namespace events get emitted because no subgraph runs. The card would render empty." +4. **A reverse migration.** `cockpit/chat/subagents` was converted from flat to subgraph (spec `2026-06-19-cockpit-subagents-subgraph-design.md`, PR #718) purely so a UI card would render. +5. **Splitting has real streaming costs the docs don't cover.** Namespaced terminal events would close out the parent's streaming message without a guard (`libs/chat`… see `stream-manager.bridge.ts:314-330`, test at `:474-505` asserting outcome `interrupted`); subagent text lands in the parent transcript unless you opt into `filterSubagentMessages` (default off, `agent.types.ts:282-283`). +6. **Attribution across the boundary is heuristic, not structural.** `matchSubgraphToSubagent()` (`subagent-tracker.ts:130-175`) falls back to "any unmapped pending/running subagent" (:165-169), and `extractToolCallIdFromNamespace` takes only the first `tools:` segment (:271-277) — so a subagent that itself delegates attributes inner events to the outer call. Nested delegation is untested here; say so plainly. + +## Structure + +1. **Lede** (no header): restate the question the searcher has, then land the thesis early — the useful boundary a subgraph draws is usually observability, not state. +2. `## What does a subgraph actually give you?` — the canonical pattern (compiled child as a node), what genuinely changes (nested execution, namespaced events) and what doesn't (state isolation isn't automatic; a shared `MessagesState` means the child appends to the parent's list). Point to the docs' architecture matrix for the tiering question rather than restating it. +3. `## Why do people really split?` — the observability thesis with the evidence above: our code comment, the rejected `@tool` alternative, the reverse migration. +4. `## What does the frontend see while a child runs?` — the differentiated section. Namespaced events (`messages|tools:call-1`), `streamSubgraphs` defaulting on, what breaks without namespace guards, `filterSubagentMessages` defaulting off, heuristic attribution and its fallback. +5. `## When should you not split?` — the honest counterweight: if you don't need the observable boundary and don't need genuinely divergent state, nesting adds a boundary you have to defend. Cite AG-UI doing the same feature flat. +6. `## Conclusion` — the heuristic, plus forward links to `/docs/langgraph/concepts/agent-architecture`, `/docs/langgraph/guides/subgraphs`, and `/blog/what-inject-agent-returns` (the `subagents()` signal is part of that return surface). + +## Voice and register + +Same as posts #11 and #9: `docs/gtm/voice.md` with the 2026 technical override — H2-as-question answered in the first line, contractions, 1–3-line paragraphs, "Let's" transitions, opinions flagged, no anecdotes, no emoji, no hype, no CTAs. Register reference: the two shipped posts. + +Python code is fine here (the audience is Python-first); keep code to at most two short blocks. + +## Accuracy requirements (drafting gate) + +- Every claim cited to a real path/line, verified by reading the file — not from this spec's summary. +- **Do not claim the `cockpit/langgraph/subgraphs` example demonstrates delegation or populates the subagent sidebar.** It does neither (unconditional edge; no `subagentToolNames`; sidebar permanently empty). Two separate tasks are correcting the example and its guide; this post must not depend on or contradict their outcome — prefer citing `cockpit/chat/subagents` and `examples/chat` for working delegation. +- **Do not speculate about:** performance/latency of splitting (nothing measures it); parallel subagent fan-out (explicitly out of scope, e2e dispatches sequentially); child-specific checkpointers or state schemas (nothing exercises a separate checkpointer). +- Verify named public members against published 0.0.58 tarballs; drop main-only members. +- Frontmatter per existing posts; tags along the lines of [langgraph, subgraphs, agents, streaming, angular]. + +## Out of scope + +- Fixing the subgraphs example or its guide (routed as separate tasks). +- Post #12 (own pass later).