threadplane-middleware
LangGraph middleware for binding client-declared tool stubs and routing
client tool calls to END so the browser executes them.
This is the Python LangGraph package. For LangGraph.js, use
@threadplane/middleware from npm and import @threadplane/middleware/langgraph.
How it works
When a browser client sends a tool catalog ({name, description, parameters}
dicts) along with a run request, the graph can expose those tools to the LLM
and route their calls back to the browser instead of executing them
server-side. The browser then executes the call and re-runs the graph with a
ToolMessage carrying the result.
The catalog is read from state["tools"], falling back to
state["client_tools"] if tools is absent.
Installation
pip install threadplane-middleware
Usage
from langgraph.graph import END, StateGraph
from threadplane.middleware.langgraph import bind_client_tools, route_after_agent
# Server-side tools your graph owns
SERVER_TOOLS = [search_tool, calculator_tool]
base_llm = ChatOpenAI(model="gpt-4o")
def agent_node(state):
# bind_client_tools must be called per-run inside the node because
# the client catalog arrives in state and may differ between runs.
llm = bind_client_tools(base_llm, SERVER_TOOLS, state)
response = llm.invoke(state["messages"])
return {"messages": [response]}
def router(state):
# Returns "tools" for server tool calls, "__end__" otherwise.
# Map "__end__" to LangGraph's END in add_conditional_edges.
return route_after_agent(state, [t.name for t in SERVER_TOOLS])
graph = StateGraph(...)
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode(SERVER_TOOLS))
graph.add_conditional_edges("agent", router, {"tools": "tools", "__end__": END})
What happens with a client tool call
- The LLM emits a tool call whose name matches a client-declared tool.
route_after_agentreturns"__end__"— the graph run ends.- The browser receives the partial output, executes the tool locally, and
re-runs the graph with a
ToolMessagecontaining the result. - The LLM continues from there as if it had called a server tool.
Lower-level helpers
from threadplane.middleware.langgraph import (
client_tool_specs, # → list of OpenAI function-tool dicts
client_tool_names, # → set[str] of client tool names
has_client_tool_call, # → bool
has_server_tool_call, # → bool
last_message, # → last message from state["messages"]
)
Pushing data to the frontend mid-run
from langchain_core.runnables import RunnableConfig
from threadplane.middleware.langgraph import emit_custom_event
async def analysis_node(state, config: RunnableConfig):
await emit_custom_event("analysis_progress", {"pct": 42}, config=config)
return state
emit_custom_event(name, value, *, config=None) wraps LangChain's
adispatch_custom_event. An ag-ui-langgraph backend consumes the graph
through astream_events, and only adispatch_custom_event places an event on
that stream — a get_stream_writer() write with stream_mode="custom" is
silently dropped and never reaches the client. Pass config when the node
already receives one; omit it and the ambient run context is used.
Development
uv venv
uv run --extra test python -m pytest -q
Release files for threadplane-middleware 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| threadplane_middleware-0.1.0.tar.gz | 99.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| threadplane_middleware-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.9 kB
Release files / threadplane_middleware-0.1.0.tar.gz
| Download URL | threadplane_middleware-0.1.0.tar.gz |
|---|---|
| Size | 99.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
66d550d267b38301e24e0e2eab4df1b83af533272e0b276f6757b13fed184ec7
|
|
BLAKE2b-256 checksum How to use checksums |
f7871b8f5bfbc8144847133ff8e30084546b37607d148a8ace9c5a171354a10e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / threadplane_middleware-0.1.0-py3-none-any.whl
| Download URL | threadplane_middleware-0.1.0-py3-none-any.whl |
|---|---|
| Size | 6.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c5682ec2a380a19ec9bfc408f808b031b37acf3a87488988493c0ecbb3f3f602
|
|
BLAKE2b-256 checksum How to use checksums |
dac3e29d9e4d56beb2c8745725b620b7bcb4841469cf7f5014bff3589da5702f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency log