Skip to content

fix(middleware)!: server_tools router default; Python emit_custom_event helper - #1052

Merged
blove merged 2 commits into
mainfrom
blove/middleware-tools-node-custom-events
Sep 7, 2026
Merged

fix(middleware)!: server_tools router default; Python emit_custom_event helper#1052
blove merged 2 commits into
mainfrom
blove/middleware-tools-node-custom-events

Conversation

@blove

@blove blove commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Two independent middleware follow-ups.

1. The JS router's default toolsNode collided with the library's own state channel (breaking)

clientToolsChannel() declares tools and client_tools state channels. LangGraph.js shares one namespace between channel names and node names, so on any graph carrying those channels addNode('tools', …) throws "tools is already being used as a state attribute". Yet routeAfterAgent() and clientToolsRouter() defaulted toolsNode to 'tools' — a destination such a graph could never have. Every working consumer was already passing an override.

The default is now 'server_tools'.

BREAKING CHANGE. Rename your server tool node to server_tools and drop the override, or keep the override pointing at whatever name your node uses. There is no shim, by design. The Python package's route_after_agent() keeps tools_node="tools" — Python LangGraph has no such namespace collision.

Docs that were previously written around an explicit { toolsNode: 'server_tools' } override (Quick Start, LangGraph Client Tools) now drop the redundant option and keep the callout explaining why tools cannot be a node name.

2. get_stream_writer output is silently dropped under ag-ui-langgraph

The bridge consumes astream_events, so the only path from a node to the adapter's customEvents() signal is adispatch_custom_event. A get_stream_writer() write with stream_mode="custom" surfaces at most as a raw event and never reaches the client — with no error anywhere.

Adds threadplane.middleware.langgraph.emit_custom_event(name, value, *, config=None), an async wrapper around adispatch_custom_event that uses the node's config when the caller has it and otherwise relies on the ambient run context. Exported from __all__. No pyproject.toml version bump.

Documented on the AG-UI Custom Events guide (its backend snippet now shows the helper), the Python LangGraph guide, the middleware Introduction helper table, and the package README.

Deployment note

deployments/ag-ui-dev/requirements.txt and deployments/ag-ui-dev/deps/client_tools/requirements.txt pin threadplane-middleware==0.0.1 from PyPI, and cockpit/ag-ui/client-tools/python/pyproject.toml declares threadplane-middleware>=0.0.1. No lane installs the package from its git source, so emit_custom_event will not be importable in those images until the package is released. Nothing was re-exported here.

Specs

  • libs/middleware/src/integration.spec.ts — invokes a graph whose ToolNode is named server_tools with no override and asserts the tool actually ran. Failed before the fix with "Branch condition returned unknown or null destination"; a compile-only version of this spec passed vacuously and was replaced.
  • libs/middleware/src/integration.spec.ts — pins the addNode('tools', …) throw so the reason for the default is recorded.
  • libs/middleware/src/langgraph.spec.ts — the three default-destination assertions updated to 'server_tools'.
  • packages/threadplane-middleware/tests/test_custom_events.py — drives a real one-node graph and asserts both emit_custom_event calls (one with config, one without) arrive as on_custom_event through astream_events. Failed on import before the helper existed.

Verification

  • npx nx run-many -t lint,test,build --projects=middleware — green (48 tests, lint clean, build clean).
  • uv run --extra test python -m pytest -q in packages/threadplane-middleware — 43 passed.
  • npx vitest run --root apps/website — 131 files, 1326 tests passed.
  • npm run generate-api-docs run; middleware/api/api-docs.json committed.
  • No touched docs page uses <ExampleCode>, so the GROWTH_FORM_POLICY website build was not required.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 7, 2026 9:26pm UTC

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 Sep 7, 2026

Copy link
Copy Markdown
Contributor

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


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 7, 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 and others added 2 commits September 7, 2026 14:23
…ag-ui-langgraph

The ag-ui-langgraph bridge consumes the graph through astream_events, so the
only path from a node to the adapter's customEvents() signal is
adispatch_custom_event. A get_stream_writer() write with stream_mode="custom"
surfaces at most as a raw event and is silently dropped — the payload never
reaches the client, with no error anywhere.

Adds threadplane.middleware.langgraph.emit_custom_event, an async wrapper
around adispatch_custom_event that accepts the node's config when the caller
has it and otherwise relies on the ambient run context. Exported from the
package's __all__. The pytest drives a real one-node graph and asserts both
calls arrive as on_custom_event through astream_events.

Documents it in the package README, the Python LangGraph guide, and the AG-UI
Custom Events guide, whose backend snippet now shows the helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
clientToolsChannel() declares a `tools` state channel, and LangGraph.js shares
one namespace between channel names and node names — so on exactly the graphs
these helpers are for, addNode('tools', …) throws "tools is already being used
as a state attribute". routeAfterAgent() and clientToolsRouter() nonetheless
defaulted toolsNode to 'tools', a destination no such graph could ever have.
Every working consumer already passed an override.

BREAKING CHANGE: the default is now 'server_tools'. Rename your server tool
node to server_tools and drop the override, or keep the override pointing at
whatever name your node uses. There is no shim. The Python package's
route_after_agent() keeps tools_node="tools"; Python LangGraph has no such
namespace collision.

Two integration specs record the behavior: one invokes a graph whose ToolNode
is named server_tools with no override and asserts the tool actually ran (it
failed with "Branch condition returned unknown or null destination" before the
fix), and one pins the addNode('tools', …) throw so the reason is written down.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blove
blove force-pushed the blove/middleware-tools-node-custom-events branch from 33dee81 to 6ac61c9 Compare September 7, 2026 21:23
@github-actions

github-actions Bot commented Sep 7, 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 3f6d73f into main Sep 7, 2026
32 checks passed
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