Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/website/content/docs/licensing/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
{
"name": "emitNag",
"kind": "function",
"description": "",
"description": "Emit one non-blocking license warning for a package/status pair.\n\n`licensed` and `noncommercial` statuses are silent. Other statuses warn once\nper package and status so repeated initialization does not spam logs.",
"signature": "emitNag(result: Pick<EvaluateResult, \"status\">, options: EmitNagOptions): void",
"params": [
{
Expand All @@ -210,7 +210,7 @@
{
"name": "evaluateLicense",
"kind": "function",
"description": "",
"description": "Convert a verified token result into a license status.\n\n`verifyResult` may be undefined when no token was supplied. In that case a\ncaller-provided `isNoncommercial` hint returns `noncommercial`; otherwise the\nstatus is `missing`. Signature failures return `tampered`, and valid claims\nare evaluated against `exp` plus the optional grace window.",
"signature": "evaluateLicense(verifyResult: VerifyResult | undefined, options: EvaluateOptions): EvaluateResult",
"params": [
{
Expand Down Expand Up @@ -247,7 +247,7 @@
{
"name": "runLicenseCheck",
"kind": "function",
"description": "",
"description": "Run the full package license check.\n\nThe helper verifies an optional token, evaluates the status, emits the\npackage nag warning when appropriate, and memoizes identical package/token\npairs so repeated provider initialization stays quiet.",
"signature": "runLicenseCheck(options: RunLicenseCheckOptions): Promise<LicenseStatus>",
"params": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The higher-level check is built not to block app startup:
- warning output goes through `console.warn` unless a custom `warn` function is supplied;
- no network request is made by the licensing check.

The code returns statuses instead of throwing for normal license states. For me, that's the right default here: you get to decide what a missing or expired license means for your app instead of having the check make that call for you. The tradeoff is that nothing stops a consumer from ignoring the status entirely. `@threadplane/chat` treats it as a warning and visibility mechanism, not an app kill switch.
The code returns statuses instead of throwing for normal license states. That keeps policy decisions in the host package or application: a missing or expired license can be logged, surfaced, or enforced by the caller instead of being decided by initialization code. `@threadplane/chat` treats the status as a warning and visibility mechanism, not an app kill switch.

## Next steps

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# bindClientTools()
# LangGraph.js client-tool helpers

`bindClientTools()` is the main helper from `@threadplane/middleware/langgraph`. It binds your server tools plus the current run's client-tool catalog onto a LangChain chat model.

Expand All @@ -10,7 +10,7 @@ import {
} from '@threadplane/middleware/langgraph';
```

There is no root JavaScript entry point for `@threadplane/middleware`; import from the `/langgraph` subpath.
There is no root JavaScript entry point for `@threadplane/middleware`; import from the `/langgraph` subpath. The Python package exposes equivalent snake_case helpers from `threadplane.middleware.langgraph`.

Related helpers in the same entry point include `clientToolsChannel()`, `clientToolsRouter()`, `clientToolSpecs()`, `clientToolNames()`, `hasClientToolCall()`, `hasServerToolCall()`, `routeAfterAgent()`, and `lastMessage()`.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Introduction

`@threadplane/middleware` is the backend companion for Threadplane client tools. It lets a browser declare tools, lets the model call those tools, and routes client-tool-only turns back to the browser for execution.
Threadplane publishes middleware for backend graphs that need browser-executed client tools. The browser declares tools, the model can call those tools, and the backend routes client-tool-only turns back to the browser for execution.

The package currently publishes one runtime entry point:
There are two package surfaces:

| Runtime | Package | Entry point |
|---------|---------|-------------|
| LangGraph.js | `@threadplane/middleware` | `@threadplane/middleware/langgraph` |
| Python LangGraph | `threadplane-middleware` | `threadplane.middleware.langgraph` |

The TypeScript package currently publishes one runtime entry point:

```ts
import {
Expand All @@ -16,23 +23,23 @@ There is no root `@threadplane/middleware` JavaScript entry point. Import from `

## What it does

The LangGraph entry point reads a client tool catalog from graph state, converts it into OpenAI function-tool objects, binds those tool stubs onto your chat model, and routes client-tool calls to `END` so the browser can execute them.
The LangGraph entry points read a client tool catalog from graph state, convert it into OpenAI function-tool objects, bind those tool stubs onto your chat model, and route client-tool calls to `END` so the browser can execute them.

The catalog is read from `state.tools` first. If that channel is absent or empty, it falls back to `state.client_tools`.
The catalog is read from `state.tools` first. If that channel is absent or empty, both packages fall back to `state.client_tools`.

## Runtime flow

1. The browser sends tool specs with the run request.
2. Your LangGraph node calls `bindClientTools()` inside the run, because the catalog can differ per request.
2. Your LangGraph node calls `bindClientTools()` or `bind_client_tools()` inside the run, because the catalog can differ per request.
3. The model emits a tool call for a browser-declared tool.
4. `clientToolsRouter()` routes client-only tool calls to `END`.
4. The router routes client-only tool calls to `END`.
5. The browser executes the local tool and resumes the graph with a `ToolMessage`.

If a turn mixes server tool calls and client tool calls, server tools win the first route. The server tool node runs first, and the client call can surface on a later turn.

## Public surface
## TypeScript public surface

The entry point exports:
`@threadplane/middleware/langgraph` exports:

| API | Purpose |
|-----|---------|
Expand All @@ -46,14 +53,29 @@ The entry point exports:
| `routeAfterAgent()` | Lower-level routing helper used by `clientToolsRouter()`. |
| `lastMessage()` | Reads the last message from state. |

## Python public surface

`threadplane.middleware.langgraph` exports:

| API | Purpose |
|-----|---------|
| `bind_client_tools()` | Binds server tools plus client-declared tool stubs onto a model. |
| `client_tool_specs()` | Converts state catalog entries into OpenAI function-tool specs. |
| `client_tool_names()` | Returns the set of client-declared tool names for a run. |
| `has_client_tool_call()` | Checks whether the last message calls a client tool. |
| `has_server_tool_call()` | Checks whether the last message calls a server or unknown tool. |
| `route_after_agent()` | Routing helper for conditional edges. |
| `last_message()` | Reads the last message from state. |

## When to use it

Use middleware when you own a LangGraph.js backend and want browser-declared tools from `@threadplane/chat` to participate in model tool calling without executing browser-only code on the server.
Use middleware when you own a LangGraph backend and want browser-declared tools from `@threadplane/chat` to participate in model tool calling without executing browser-only code on the server.

If your backend already speaks AG-UI, use `@threadplane/ag-ui` instead. If your frontend talks directly to LangGraph and does not need browser-executed tools, `@threadplane/langgraph` can run without this middleware.

## Next steps

- [Quick Start](/docs/middleware/getting-started/quickstart) - install and wire the LangGraph helper.
- [LangGraph Client Tools](/docs/middleware/guides/langgraph-client-tools) - routing details and server-tool behavior.
- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API reference for the exported helpers.
- [Python LangGraph Middleware](/docs/middleware/guides/python-langgraph) - the Python package and snake_case helpers.
- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API reference for the TypeScript helpers.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Quick Start

Install the middleware package and its LangGraph peer dependencies:
Install the TypeScript middleware package and its LangGraph.js peer dependencies:

```bash
npm install @threadplane/middleware @langchain/core @langchain/langgraph
```

The package exposes its JavaScript API from `@threadplane/middleware/langgraph`.
The package exposes its JavaScript API from `@threadplane/middleware/langgraph`. For Python LangGraph, install `threadplane-middleware` and follow the [Python guide](/docs/middleware/guides/python-langgraph).

## Add client-tool state channels

Expand Down Expand Up @@ -95,4 +95,5 @@ On the frontend, declare client tools with `@threadplane/chat` and send them thr
## Next steps

- [LangGraph Client Tools](/docs/middleware/guides/langgraph-client-tools) - mixed server/client routing and helper behavior.
- [Python LangGraph Middleware](/docs/middleware/guides/python-langgraph) - the equivalent Python package and helper names.
- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API details.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# LangGraph Client Tools
# LangGraph.js Client Tools

Client tools are frontend-declared tools that the model can call but the browser executes. The backend only exposes tool schemas to the model and decides whether a turn should continue on the server or end so the client can run a local tool.

This page documents the TypeScript `@threadplane/middleware/langgraph` entry point. The Python package uses the same routing model with snake_case helpers; see [Python LangGraph Middleware](/docs/middleware/guides/python-langgraph).

## State shape

The middleware reads this state slice:
Expand Down
95 changes: 95 additions & 0 deletions apps/website/content/docs/middleware/guides/python-langgraph.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Python LangGraph Middleware

The Python package is `threadplane-middleware`. It is the Python LangGraph twin of `@threadplane/middleware/langgraph`: it binds browser-declared client-tool stubs onto a chat model and routes client-tool-only turns to `END`.

## Install

```bash
pip install threadplane-middleware
```

The package depends on `langchain-core>=0.3.0` and `langgraph>=0.3.0`.

Install your model provider package separately, for example `langchain-openai` when using `ChatOpenAI`.

## Bind tools per run

Call `bind_client_tools()` inside your agent node. The browser sends the tool catalog with each run, so the model-visible tool list is request-scoped.

```python
from langchain_openai import ChatOpenAI
from threadplane.middleware.langgraph import bind_client_tools

SERVER_TOOLS = [search_tool]
base_llm = ChatOpenAI(model="gpt-4o-mini")

def agent_node(state):
llm = bind_client_tools(base_llm, SERVER_TOOLS, state)
response = llm.invoke(state["messages"])
return {"messages": [response]}
```

The helper reads `state["tools"]` first and falls back to `state["client_tools"]`. It appends each client tool as an explicit OpenAI function-tool dict:

```python
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Read local weather",
"parameters": {"type": "object"},
},
}
```

## Route after the agent

Use `route_after_agent()` from a LangGraph conditional edge. It returns the server tools node name when the last model message contains a server or unknown tool call. It returns `__end__` when the turn has only client tool calls or no tool calls.

```python
from langgraph.graph import END, StateGraph
from langgraph.prebuilt import ToolNode
from threadplane.middleware.langgraph import route_after_agent

server_tool_names = [tool.name for tool in SERVER_TOOLS]

def router(state):
return route_after_agent(state, server_tool_names)

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})
```

You can override the returned route labels:

```python
route_after_agent(state, server_tool_names, tools_node="server_tools", end="done")
```

## Helper surface

```python
from threadplane.middleware.langgraph import (
bind_client_tools,
client_tool_specs,
client_tool_names,
has_client_tool_call,
has_server_tool_call,
last_message,
route_after_agent,
)
```

| Helper | Purpose |
|--------|---------|
| `client_tool_specs(state)` | Convert the run catalog into OpenAI function-tool dicts. |
| `client_tool_names(state)` | Return the set of client-declared tool names. |
| `has_client_tool_call(state)` | Check whether the last message calls a known client tool. |
| `has_server_tool_call(state, server_tool_names)` | Check whether the last message calls a server or unknown tool. |
| `last_message(state)` | Return the last message from `state["messages"]`, or `None`. |

## Frontend contract

The middleware does not execute browser tools. The frontend still needs to send the catalog, observe the model tool call, execute the local function or UI interaction, and resume the graph with a `ToolMessage` containing the result.
Loading
Loading