From 5fdcc01f81fb884f2841063403efa583bed4c1fc Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 16 Jun 2026 14:18:12 -0700 Subject: [PATCH 1/2] feat(cockpit): langgraph client-tools Node backend (uses @threadplane/middleware) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JS/TS twin of cockpit/langgraph/client-tools/python — same graph behavior built on @threadplane/middleware/langgraph (clientToolsChannel + bindClientTools + clientToolsRouter). Served by langgraphjs dev on the same port (5308) and graph id (client-tools) as the Python backend, so the shared Angular frontend connects unchanged. Scaffold complete; install + serve + live smoke land once @threadplane/middleware is published to npm (the backend consumes the published package, mirroring how the Python backend consumes the published PyPI package). Co-Authored-By: Claude Opus 4.8 (1M context) --- cockpit/langgraph/client-tools/node/README.md | 29 ++++++++ .../client-tools/node/langgraph.json | 7 ++ .../langgraph/client-tools/node/package.json | 19 +++++ .../langgraph/client-tools/node/src/graph.ts | 71 +++++++++++++++++++ .../langgraph/client-tools/node/tsconfig.json | 15 ++++ 5 files changed, 141 insertions(+) create mode 100644 cockpit/langgraph/client-tools/node/README.md create mode 100644 cockpit/langgraph/client-tools/node/langgraph.json create mode 100644 cockpit/langgraph/client-tools/node/package.json create mode 100644 cockpit/langgraph/client-tools/node/src/graph.ts create mode 100644 cockpit/langgraph/client-tools/node/tsconfig.json diff --git a/cockpit/langgraph/client-tools/node/README.md b/cockpit/langgraph/client-tools/node/README.md new file mode 100644 index 00000000..de4c6288 --- /dev/null +++ b/cockpit/langgraph/client-tools/node/README.md @@ -0,0 +1,29 @@ +# LangGraph Client Tools — Node backend + +The TypeScript/LangGraph.js twin of [`../python`](../python). Same graph behavior, +built on [`@threadplane/middleware/langgraph`](https://www.npmjs.com/package/@threadplane/middleware) — +the JS twin of the Python `threadplane-middleware`. It binds the browser-declared +client tools onto the model and ends the turn on a client-tool call so the browser +executes it and re-runs with a `ToolMessage`. + +The browser declares the tools and `@threadplane/langgraph` ships the catalog as +`input.client_tools`; the graph's state channels (`clientToolsChannel()`) retain it +across the turn. + +## Run + +```bash +npm install +OPENAI_API_KEY=... npm run dev # langgraphjs dev --port 5308 +``` + +Then serve the **shared** Angular frontend — it already targets this graph +(`assistantId: "client-tools"`, `http://localhost:4308/api` → proxied to `:5308`): + +```bash +npx nx serve cockpit-langgraph-client-tools-angular +``` + +This backend serves the LangGraph Platform API on the same port (`5308`) and graph +id (`client-tools`) as the Python backend, so the Angular app connects unchanged — +only the backend runtime (Node vs Python) differs. diff --git a/cockpit/langgraph/client-tools/node/langgraph.json b/cockpit/langgraph/client-tools/node/langgraph.json new file mode 100644 index 00000000..4bdb1bb6 --- /dev/null +++ b/cockpit/langgraph/client-tools/node/langgraph.json @@ -0,0 +1,7 @@ +{ + "graphs": { + "client-tools": "./src/graph.ts:graph" + }, + "node_version": "20", + "env": ".env" +} diff --git a/cockpit/langgraph/client-tools/node/package.json b/cockpit/langgraph/client-tools/node/package.json new file mode 100644 index 00000000..005f8bbf --- /dev/null +++ b/cockpit/langgraph/client-tools/node/package.json @@ -0,0 +1,19 @@ +{ + "name": "cockpit-langgraph-client-tools-node", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "LangGraph.js client-tools backend — the TypeScript twin of ../python, built on @threadplane/middleware/langgraph.", + "scripts": { + "dev": "langgraphjs dev --port 5308 --no-browser" + }, + "dependencies": { + "@langchain/core": "^1.1.49", + "@langchain/langgraph": "^1.4.2", + "@langchain/openai": "^1.4.7", + "@threadplane/middleware": "^0.0.1" + }, + "devDependencies": { + "@langchain/langgraph-cli": "^1.3.1" + } +} diff --git a/cockpit/langgraph/client-tools/node/src/graph.ts b/cockpit/langgraph/client-tools/node/src/graph.ts new file mode 100644 index 00000000..f5fcef86 --- /dev/null +++ b/cockpit/langgraph/client-tools/node/src/graph.ts @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +/** + * LangGraph client-tools graph (LangGraph.js / TypeScript path). + * + * The JS twin of `../python/src/graph.py`. The browser declares the tools + * (get_weather / weather_card / confirm_booking) and the `@threadplane/langgraph` + * adapter ships the catalog as `input.client_tools`. This graph declares the + * client-tools state channels so the catalog is retained across the turn, binds + * those client stubs onto the model (no server implementation), and ends the + * turn when the model calls one — the browser executes it and re-runs with a + * ToolMessage, which the model then summarizes. + * + * The bind/route logic comes entirely from `@threadplane/middleware/langgraph`. + */ +import { ChatOpenAI } from '@langchain/openai'; +import { Annotation, MessagesAnnotation, StateGraph, END } from '@langchain/langgraph'; +import { SystemMessage } from '@langchain/core/messages'; +import { + bindClientTools, + clientToolsChannel, + clientToolsRouter, +} from '@threadplane/middleware/langgraph'; + +// `clientToolsChannel()` declares the `tools` (primary) and `client_tools` +// (fallback) channels. The @threadplane/langgraph adapter ships the catalog as +// `client_tools`; bindClientTools reads `tools` then falls back to it. +const State = Annotation.Root({ + ...MessagesAnnotation.spec, + ...clientToolsChannel(), +}); + +// Inlined so the graph has no filesystem/ESM-path dependency under langgraphjs dev. +const SYSTEM_PROMPT = `# Client Tools Assistant + +You are a demo assistant showing browser-executed tools over LangGraph. You have +three client tools — call the right one and do not answer in prose first: + +- When the user asks about the weather for a place, call \`get_weather\` with the + location. After it returns, give a one-sentence summary using the data. +- When the user asks to *show* or *display* a weather card, call \`weather_card\` + with the location and plausible readings (temperatureF, conditions, humidity, + windMph). After it renders, briefly confirm. +- When the user asks to book or reserve something, call \`confirm_booking\` with a + one-line \`summary\` of what they're booking. After the user responds, confirm + if they accepted or acknowledge if they cancelled. + +Keep replies to one short sentence; the components carry the detail.`; + +const baseLlm = new ChatOpenAI({ model: 'gpt-5-mini', streaming: true }); + +function buildClientToolsGraph() { + const agent = async (state: typeof State.State) => { + // bindClientTools reads state.tools then falls back to state.client_tools. + const llm = bindClientTools(baseLlm, [], state); + const response = await llm.invoke([new SystemMessage(SYSTEM_PROMPT), ...state.messages]); + return { messages: [response] }; + }; + + // No server tools: a client tool call ends the run (the browser executes it), + // and so does a final text turn. clientToolsRouter([]) routes both to END. + return new StateGraph(State) + .addNode('agent', agent) + .addEdge('__start__', 'agent') + .addConditionalEdges('agent', clientToolsRouter([]), [END]) + .compile(); +} + +// The graph instance referenced by langgraph.json. For `langgraphjs dev` the +// platform runtime provides the checkpointer, so we compile without one +// (mirrors the Python graph). +export const graph = buildClientToolsGraph(); diff --git a/cockpit/langgraph/client-tools/node/tsconfig.json b/cockpit/langgraph/client-tools/node/tsconfig.json new file mode 100644 index 00000000..a1a51362 --- /dev/null +++ b/cockpit/langgraph/client-tools/node/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"], + "noEmit": true + }, + "include": ["src/**/*.ts"] +} From fdd3cd8a096bd17f8d4ae878990757294bf6dfa1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 16 Jun 2026 14:35:29 -0700 Subject: [PATCH 2/2] chore(cockpit): gitignore install + langgraph artifacts in node demo --- cockpit/langgraph/client-tools/node/.gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 cockpit/langgraph/client-tools/node/.gitignore diff --git a/cockpit/langgraph/client-tools/node/.gitignore b/cockpit/langgraph/client-tools/node/.gitignore new file mode 100644 index 00000000..d4aaabfb --- /dev/null +++ b/cockpit/langgraph/client-tools/node/.gitignore @@ -0,0 +1,6 @@ +node_modules +.env +package-lock.json + +# LangGraph API +.langgraph_api