fix(middleware)!: server_tools router default; Python emit_custom_event helper - #1052
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
blove
enabled auto-merge (squash)
September 7, 2026 21:01
blove
force-pushed
the
blove/middleware-tools-node-custom-events
branch
from
September 7, 2026 21:01
9d9c61d to
33dee81
Compare
Contributor
…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
force-pushed
the
blove/middleware-tools-node-custom-events
branch
from
September 7, 2026 21:23
33dee81 to
6ac61c9
Compare
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent middleware follow-ups.
1. The JS router's default
toolsNodecollided with the library's own state channel (breaking)clientToolsChannel()declarestoolsandclient_toolsstate channels. LangGraph.js shares one namespace between channel names and node names, so on any graph carrying those channelsaddNode('tools', …)throws "tools is already being used as a state attribute". YetrouteAfterAgent()andclientToolsRouter()defaultedtoolsNodeto'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_toolsand drop the override, or keep the override pointing at whatever name your node uses. There is no shim, by design. The Python package'sroute_after_agent()keepstools_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 whytoolscannot be a node name.2.
get_stream_writeroutput is silently dropped underag-ui-langgraphThe bridge consumes
astream_events, so the only path from a node to the adapter'scustomEvents()signal isadispatch_custom_event. Aget_stream_writer()write withstream_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 aroundadispatch_custom_eventthat uses the node'sconfigwhen the caller has it and otherwise relies on the ambient run context. Exported from__all__. Nopyproject.tomlversion 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.txtanddeployments/ag-ui-dev/deps/client_tools/requirements.txtpinthreadplane-middleware==0.0.1from PyPI, andcockpit/ag-ui/client-tools/python/pyproject.tomldeclaresthreadplane-middleware>=0.0.1. No lane installs the package from its git source, soemit_custom_eventwill 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 whoseToolNodeis namedserver_toolswith 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 theaddNode('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 bothemit_custom_eventcalls (one withconfig, one without) arrive ason_custom_eventthroughastream_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 -qinpackages/threadplane-middleware— 43 passed.npx vitest run --root apps/website— 131 files, 1326 tests passed.npm run generate-api-docsrun;middleware/api/api-docs.jsoncommitted.<ExampleCode>, so theGROWTH_FORM_POLICYwebsite build was not required.🤖 Generated with Claude Code