Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-langchain-core for 
openSUSE:Factory checked in at 2026-10-23 20:22:29
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-core (Old)
 and      /work/SRC/openSUSE:Factory/.python-langchain-core.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langchain-core"

Fri Oct 23 20:22:29 2026 rev:22 rq:1392016 version:1.7.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-langchain-core/python-langchain-core.changes  
    2026-10-17 21:26:58.864831975 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-langchain-core.new.2252680/python-langchain-core.changes
 2026-10-23 20:23:54.093506399 +0200
@@ -1,0 +2,22 @@
+Fri Oct 23 08:25:40 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.7.0:
+  * Stream tool-call args as `tool-call-delta` slices instead of
+    `block-delta` snapshots of the args so far; `.tool_calls.get()`
+    and `.output` are unchanged (pin langchain-core<1.7 to keep
+    snapshot behavior)
+  * Iterating `ChatModelStream.tool_calls` yields each call's new
+    args slice, not a snapshot of the args so far
+  * Refresh model profile data
+- Raise python-langchain-protocol floor to >= 0.0.20
+
+-------------------------------------------------------------------
+Thu Oct 22 09:55:32 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.6.13:
+  * Stream consumers understand the new `tool-call-delta`
+    block deltas: `.tool_calls` on v3 event streams yields
+    only the new part of each tool call args instead of the
+    accumulated string; the final message is unchanged
+
+-------------------------------------------------------------------

Old:
----
  langchain_core-1.6.12.tar.gz

New:
----
  langchain_core-1.7.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-langchain-core.spec ++++++
--- /var/tmp/diff_new_pack.jeGirn/_old  2026-10-23 20:23:54.841537559 +0200
+++ /var/tmp/diff_new_pack.jeGirn/_new  2026-10-23 20:23:54.843537642 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-langchain-core
-Version:        1.6.12
+Version:        1.7.0
 Release:        0
 Summary:        Building applications with LLMs through composability
 License:        MIT
@@ -32,7 +32,7 @@
 Requires:       python-PyYAML >= 5.3
 Requires:       python-httpx >= 0.23.0
 Requires:       python-jsonpatch >= 1.33
-Requires:       python-langchain-protocol >= 0.0.17
+Requires:       python-langchain-protocol >= 0.0.20
 Requires:       python-langsmith >= 0.14.9
 Requires:       python-packaging >= 23.2
 Requires:       python-pydantic >= 2.7.4
@@ -46,7 +46,7 @@
 BuildRequires:  %{python_module freezegun}
 BuildRequires:  %{python_module httpx >= 0.23.0}
 BuildRequires:  %{python_module jsonpatch >= 1.33}
-BuildRequires:  %{python_module langchain-protocol >= 0.0.17}
+BuildRequires:  %{python_module langchain-protocol >= 0.0.20}
 BuildRequires:  %{python_module langsmith >= 0.14.9}
 BuildRequires:  %{python_module numpy}
 BuildRequires:  %{python_module packaging >= 23.2}

++++++ langchain_core-1.6.12.tar.gz -> langchain_core-1.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.6.12/PKG-INFO 
new/langchain_core-1.7.0/PKG-INFO
--- old/langchain_core-1.6.12/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.7.0/PKG-INFO   2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: langchain-core
-Version: 1.6.12
+Version: 1.7.0
 Summary: Building applications with LLMs through composability
 Project-URL: Homepage, https://docs.langchain.com/
 Project-URL: Documentation, 
https://reference.langchain.com/python/langchain_core/
@@ -26,7 +26,7 @@
 Requires-Python: <4.0.0,>=3.10.0
 Requires-Dist: httpx<1.0.0,>=0.23.0
 Requires-Dist: jsonpatch<2.0.0,>=1.33.0
-Requires-Dist: langchain-protocol>=0.0.17
+Requires-Dist: langchain-protocol>=0.0.20
 Requires-Dist: langsmith<1.0.0,>=0.14.9
 Requires-Dist: packaging>=23.2.0
 Requires-Dist: pydantic<3.0.0,>=2.7.4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.6.12/langchain_core/language_models/_compat_bridge.py 
new/langchain_core-1.7.0/langchain_core/language_models/_compat_bridge.py
--- old/langchain_core-1.6.12/langchain_core/language_models/_compat_bridge.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.7.0/langchain_core/language_models/_compat_bridge.py   
2020-02-02 01:00:00.000000000 +0100
@@ -61,12 +61,11 @@
     from collections.abc import AsyncIterator, Iterator
 
     from langchain_protocol.protocol import (
-        BlockDelta,
-        BlockDeltaFields,
         ContentBlockDelta,
         DataDelta,
         ReasoningDelta,
         TextDelta,
+        ToolCallDelta,
     )
 
     from langchain_core.messages.ai import UsageMetadata
@@ -110,38 +109,6 @@
     return cast("FinalizedContentBlock", block)
 
 
-def _to_block_delta_fields(block: CompatBlock) -> BlockDeltaFields:
-    """Narrow an internal working dict to protocol block-delta fields."""
-    return cast("BlockDeltaFields", block)
-
-
-def _to_content_delta(block: CompatBlock) -> ContentBlockDelta:
-    """Convert a content-block slice/snapshot to an explicit protocol delta."""
-    btype = block.get("type")
-    if btype == "text":
-        return cast("TextDelta", {"type": "text-delta", "text": 
block.get("text", "")})
-    if btype == "reasoning":
-        return cast(
-            "ReasoningDelta",
-            {
-                "type": "reasoning-delta",
-                "reasoning": block.get("reasoning", ""),
-            },
-        )
-    if "data" in block:
-        delta = cast("DataDelta", {"type": "data-delta", "data": 
block.get("data", "")})
-        if block.get("encoding") == "base64":
-            delta["encoding"] = "base64"
-        return delta
-    return cast(
-        "BlockDelta",
-        {
-            "type": "block-delta",
-            "fields": _to_block_delta_fields(block),
-        },
-    )
-
-
 # ---------------------------------------------------------------------------
 # Block iteration
 # ---------------------------------------------------------------------------
@@ -230,13 +197,17 @@
 # of being serialized twice on the wire.
 _HEAVY_FIELDS = frozenset({"args", "data", "output", "transcript", "value"})
 
+_TOOL_CALL_CHUNK_TYPES = frozenset({"tool_call_chunk", 
"server_tool_call_chunk"})
+
 
 def _start_skeleton(block: CompatBlock) -> ContentBlock:
     """Empty-content placeholder for the `content-block-start` event.
 
     Deltaable block types (text, reasoning, the `_chunk` tool variants)
     get an empty payload so the lifecycle's "start" signal is distinct
-    from the first incremental delta.  Self-contained types (image,
+    from the first incremental delta.  The `_chunk` tool variants also
+    carry the call's `id`, `name` and provider `index`, because
+    `tool-call-delta` carries only args.  Self-contained types (image,
     audio, video, file, non_standard, finalized tool calls) drop their
     heavy payload fields; those are carried by `content-block-finish`.
     Correlation fields (id, name, toolCallId) and small metadata
@@ -256,12 +227,15 @@
         r_skel["reasoning"] = ""
         return _to_protocol_block(r_skel)
     if btype == "tool_call_chunk":
-        return ToolCallChunk(
+        skel = ToolCallChunk(
             type="tool_call_chunk",
             id=block.get("id"),
             name=block.get("name"),
             args="",
         )
+        if block.get("index") is not None:
+            skel["index"] = block["index"]
+        return skel
     if btype == "server_tool_call_chunk":
         s_skel = ServerToolCallChunk(
             type="server_tool_call_chunk",
@@ -271,6 +245,8 @@
             s_skel["id"] = block["id"]
         if block.get("name") is not None:
             s_skel["name"] = block["name"]
+        if block.get("index") is not None:
+            s_skel["index"] = block["index"]
         return s_skel
 
     stripped: CompatBlock = {k: v for k, v in block.items() if k not in 
_HEAVY_FIELDS}
@@ -283,8 +259,8 @@
     return _to_protocol_block(stripped)
 
 
-def _should_emit_delta(block: CompatBlock) -> bool:
-    """Whether a per-chunk block carries content worth a delta event.
+def _to_content_delta(block: CompatBlock) -> ContentBlockDelta | None:
+    """Convert a content-block slice to a protocol delta, or `None` if it has 
none.
 
     Deltaable types emit only when they have fresh content.  Self-contained
     / already-finalized types skip the delta entirely — the `finish`
@@ -292,16 +268,29 @@
     """
     btype = block.get("type")
     if btype == "text":
-        return bool(block.get("text"))
+        if not block.get("text"):
+            return None
+        return cast("TextDelta", {"type": "text-delta", "text": block["text"]})
     if btype == "reasoning":
-        return bool(block.get("reasoning"))
-    if btype in {"tool_call_chunk", "server_tool_call_chunk"}:
-        return bool(
-            block.get("args") or block.get("id") or block.get("name"),
+        if not block.get("reasoning"):
+            return None
+        return cast(
+            "ReasoningDelta",
+            {"type": "reasoning-delta", "reasoning": block["reasoning"]},
         )
-    if "data" in block:
-        return bool(block.get("data"))
-    return False
+    if btype in _TOOL_CALL_CHUNK_TYPES:
+        # Only the args: the call's `id`, `name` and `index` went on its
+        # start. `extras`, and an `id` or `name` arriving after the
+        # start, reach consumers on `content-block-finish`.
+        if not block.get("args"):
+            return None
+        return cast("ToolCallDelta", {"type": "tool-call-delta", "args": 
block["args"]})
+    if block.get("data"):
+        delta = cast("DataDelta", {"type": "data-delta", "data": 
block["data"]})
+        if block.get("encoding") == "base64":
+            delta["encoding"] = "base64"
+        return delta
+    return None
 
 
 def _accumulate(state: CompatBlock | None, delta: CompatBlock) -> CompatBlock:
@@ -340,7 +329,7 @@
                 state["extras"] = {**(state.get("extras") or {}), **value}
             else:
                 state[key] = value
-    elif btype in {"tool_call_chunk", "server_tool_call_chunk"} and dtype == 
btype:
+    elif btype in _TOOL_CALL_CHUNK_TYPES and dtype == btype:
         state["args"] = (state.get("args", "") or "") + (delta.get("args") or 
"")
         if delta.get("id") is not None:
             state["id"] = delta["id"]
@@ -438,7 +427,7 @@
     their terminal shape.
     """
     btype = block.get("type")
-    if btype in {"tool_call_chunk", "server_tool_call_chunk"}:
+    if btype in _TOOL_CALL_CHUNK_TYPES:
         # Carry provider-specific fields from the accumulated chunk onto
         # the finalized block. Drop the chunk-only keys we rewrite
         # explicitly. `index` is stripped on client-side
@@ -670,17 +659,12 @@
             else:
                 wire_idx, existing = blocks[key]
                 blocks[key] = (wire_idx, _accumulate(existing, block))
-            if _should_emit_delta(block):
-                wire_idx, current = blocks[key]
-                is_block_delta = block.get("type") in {
-                    "tool_call_chunk",
-                    "server_tool_call_chunk",
-                }
-                delta_source = current if is_block_delta else block
+            delta = _to_content_delta(block)
+            if delta is not None:
                 yield ContentBlockDeltaData(
                     event="content-block-delta",
                     index=wire_idx,
-                    delta=_to_content_delta(delta_source or block),
+                    delta=delta,
                 )
 
         if msg.usage_metadata:
@@ -750,17 +734,12 @@
             else:
                 wire_idx, existing = blocks[key]
                 blocks[key] = (wire_idx, _accumulate(existing, block))
-            if _should_emit_delta(block):
-                wire_idx, current = blocks[key]
-                is_block_delta = block.get("type") in {
-                    "tool_call_chunk",
-                    "server_tool_call_chunk",
-                }
-                delta_source = current if is_block_delta else block
+            delta = _to_content_delta(block)
+            if delta is not None:
                 yield ContentBlockDeltaData(
                     event="content-block-delta",
                     index=wire_idx,
-                    delta=_to_content_delta(delta_source or block),
+                    delta=delta,
                 )
 
         if msg.usage_metadata:
@@ -792,9 +771,10 @@
     downstream see a uniform event shape regardless of source.
 
     Text and reasoning blocks emit a single `content-block-delta` with
-    the full accumulated content.  Already-finalized blocks (tool_call,
-    server_tool_call, image, etc.) skip the delta and rely on the
-    `content-block-finish` event alone.
+    the full accumulated content, and tool-call chunk blocks a single
+    `tool-call-delta` with their full args.  Already-finalized blocks
+    (tool_call, server_tool_call, image, etc.) skip the delta and rely
+    on the `content-block-finish` event alone.
 
     Args:
         msg: The finalized message — typically an `AIMessage`.
@@ -812,11 +792,12 @@
             index=wire_idx,
             content=_start_skeleton(block),
         )
-        if _should_emit_delta(block):
+        delta = _to_content_delta(block)
+        if delta is not None:
             yield ContentBlockDeltaData(
                 event="content-block-delta",
                 index=wire_idx,
-                delta=_to_content_delta(block),
+                delta=delta,
             )
         yield ContentBlockFinishData(
             event="content-block-finish",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.6.12/langchain_core/language_models/chat_model_stream.py 
new/langchain_core-1.7.0/langchain_core/language_models/chat_model_stream.py
--- 
old/langchain_core-1.6.12/langchain_core/language_models/chat_model_stream.py   
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.7.0/langchain_core/language_models/chat_model_stream.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -29,6 +29,7 @@
     from langchain_protocol.protocol import (
         ContentBlockDeltaData,
         ContentBlockFinishData,
+        ContentBlockStartData,
         FinalizedContentBlock,
         InvalidToolCall,
         MessageFinishData,
@@ -591,6 +592,8 @@
         self._text_per_block: dict[int, str] = {}
         self._reasoning_per_block: dict[int, str] = {}
         self._tool_call_chunks: dict[int, dict[str, Any]] = {}
+        # Calls whose args stopped extending previous deltas; they yield no 
more deltas.
+        self._quiet_tool_calls: set[int] = set()
         self._tool_calls_acc: list[ToolCall] = []
         self._invalid_tool_calls_acc: list[InvalidToolCall] = []
         self._server_tool_call_chunks: dict[int, dict[str, Any]] = {}
@@ -672,6 +675,8 @@
         event_type = event.get("event")
         if event_type == "message-start":
             self._push_message_start(cast("MessageStartData", event))
+        elif event_type == "content-block-start":
+            self._push_content_block_start(cast("ContentBlockStartData", 
event))
         elif event_type == "content-block-delta":
             self._push_content_block_delta(cast("ContentBlockDeltaData", 
event))
         elif event_type == "content-block-finish":
@@ -680,7 +685,6 @@
             self._finish(cast("MessageFinishData", event))
         elif event_type == "error":
             self.fail(RuntimeError(event.get("message", "Unknown error")))
-        # content-block-start is informational — no accumulation needed
 
     # -- Internal push API (called by dispatch) ----------------------------
 
@@ -695,6 +699,53 @@
         if message_id:
             self._message_id = message_id
 
+    def _push_content_block_start(self, data: ContentBlockStartData) -> None:
+        """Process a `content-block-start` event.
+
+        Seeds the chunk stores for tool-call blocks. `tool-call-delta`
+        events don't repeat the call's `id` / `name`, or whether it is a
+        client or server call, so the start is the only place to learn
+        them. Other blocks need nothing from their start.
+        """
+        block = _event_content_block(data)
+        idx = data.get("index")
+        if block is None or idx is None:
+            return
+        btype = block.get("type")
+        if btype == "tool_call_chunk":
+            _merge_block_delta_into_store(self._tool_call_chunks, idx, 
dict(block))
+            args = block.get("args")
+            if isinstance(args, str) and args:
+                self._push_tool_call_chunk(idx, args)
+        elif btype == "server_tool_call_chunk":
+            _merge_block_delta_into_store(
+                self._server_tool_call_chunks, idx, dict(block)
+            )
+
+    def _push_tool_call_chunk(
+        self, idx: int, args: str, *, provider_index: int | str | None = None
+    ) -> None:
+        """Push a `ToolCallChunk` delta for the client tool call at `idx`.
+
+        Deltas don't always repeat `id` / `name`, so each item takes them
+        from the call's store, letting consumers tell parallel calls apart.
+        `provider_index` is the delta's own provider `index`, for deltas
+        that carry one without it being kept in the store.
+        """
+        if idx in self._quiet_tool_calls:
+            return
+        chunk = self._tool_call_chunks[idx]
+        item: ToolCallChunk = {
+            "type": "tool_call_chunk",
+            "id": chunk.get("id"),
+            "name": chunk.get("name"),
+            "args": args,
+        }
+        index = chunk.get("index", provider_index)
+        if index is not None:
+            item["index"] = index
+        self._tool_calls_proj.push(item)
+
     def _push_content_block_delta(self, data: ContentBlockDeltaData) -> None:
         """Process a `content-block-delta` event."""
         delta = _event_delta(data)
@@ -731,16 +782,16 @@
                 idx = data.get("index")
                 if idx is None:
                     idx = tcc.get("index", len(self._tool_call_chunks))  # 
type: ignore[unreachable]
+                held = self._tool_call_chunks.get(idx, {}).get("args") or ""
                 _merge_block_delta_into_store(self._tool_call_chunks, idx, 
dict(tcc))
-                chunk_block: ToolCallChunk = {
-                    "type": "tool_call_chunk",
-                    "id": tcc.get("id"),
-                    "name": tcc.get("name"),
-                    "args": tcc.get("args"),
-                }
-                if "index" in tcc:
-                    chunk_block["index"] = tcc["index"]
-                self._tool_calls_proj.push(chunk_block)
+                # `block-delta` replaces `args` with a snapshot (older
+                # cores); `.tool_calls` yields only the part it adds.
+                args = tcc.get("args")
+                if isinstance(args, str):
+                    if not args.startswith(held):
+                        self._quiet_tool_calls.add(idx)
+                    elif len(args) > len(held):
+                        self._push_tool_call_chunk(idx, args[len(held) :])
             elif btype == "server_tool_call_chunk":
                 stcc = cast("ServerToolCallChunk", fields)
                 idx = data.get("index")
@@ -751,6 +802,23 @@
                     idx,
                     dict(stcc),
                 )
+        elif dtype == "tool-call-delta":
+            # Appends to the call's args. The call's start says
+            # whether it is a client or server call; a delta whose start
+            # this stream never saw (say, trimmed from a replay window)
+            # is left to `content-block-finish`, which carries the call.
+            delta_args = delta.get("args")
+            if event_idx is None or not isinstance(delta_args, str) or not 
delta_args:
+                return
+            if event_idx in self._tool_call_chunks:
+                _merge_chunk_into_store(
+                    self._tool_call_chunks, event_idx, {"args": delta_args}
+                )
+                self._push_tool_call_chunk(event_idx, delta_args)
+            elif event_idx in self._server_tool_call_chunks:
+                _merge_chunk_into_store(
+                    self._server_tool_call_chunks, event_idx, {"args": 
delta_args}
+                )
         elif dtype == "legacy-block-delta":
             fields = delta.get("fields")
             if not isinstance(fields, dict):
@@ -762,15 +830,11 @@
                 if idx is None:
                     idx = tcc.get("index", len(self._tool_call_chunks))  # 
type: ignore[unreachable]
                 _merge_chunk_into_store(self._tool_call_chunks, idx, dict(tcc))
-                legacy_chunk_block: ToolCallChunk = {
-                    "type": "tool_call_chunk",
-                    "id": tcc.get("id"),
-                    "name": tcc.get("name"),
-                    "args": tcc.get("args"),
-                }
-                if "index" in tcc:
-                    legacy_chunk_block["index"] = tcc["index"]
-                self._tool_calls_proj.push(legacy_chunk_block)
+                legacy_args = tcc.get("args")
+                if legacy_args:
+                    self._push_tool_call_chunk(
+                        idx, legacy_args, provider_index=tcc.get("index")
+                    )
             elif btype == "server_tool_call_chunk":
                 stcc = cast("ServerToolCallChunk", fields)
                 idx = data.get("index")
@@ -786,9 +850,13 @@
             # content-block finish event; there is no dedicated projection.
             return
         else:
-            # Transitional legacy path for old `content_block` deltas that
-            # should not be reachable after `_event_delta` conversion, kept
-            # here for custom in-tree test fixtures or third-party emitters.
+            # Unknown delta variants land here and are ignored, as the
+            # protocol requires; `content-block-finish` still carries the
+            # complete block. The one exception is an event that also
+            # carries a legacy `content_block`: a transitional path for
+            # old deltas that should not be reachable after `_event_delta`
+            # conversion, kept for custom test fixtures or third-party
+            # emitters.
             block = data.get("content_block")
             if not isinstance(block, dict):
                 return
@@ -800,15 +868,11 @@
             if idx is None:
                 idx = tcc.get("index", len(self._tool_call_chunks))  # type: 
ignore[unreachable]
             _merge_chunk_into_store(self._tool_call_chunks, idx, dict(tcc))
-            fallback_chunk_block: ToolCallChunk = {
-                "type": "tool_call_chunk",
-                "id": tcc.get("id"),
-                "name": tcc.get("name"),
-                "args": tcc.get("args"),
-            }
-            if "index" in tcc:
-                fallback_chunk_block["index"] = tcc["index"]
-            self._tool_calls_proj.push(fallback_chunk_block)
+            fallback_args = tcc.get("args")
+            if fallback_args:
+                self._push_tool_call_chunk(
+                    idx, fallback_args, provider_index=tcc.get("index")
+                )
 
     def _resolve_block_text(self, idx: int | None, full_text: str) -> str:
         """Return authoritative text for a single text block at `idx`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.6.12/langchain_core/version.py 
new/langchain_core-1.7.0/langchain_core/version.py
--- old/langchain_core-1.6.12/langchain_core/version.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.7.0/langchain_core/version.py  2020-02-02 
01:00:00.000000000 +0100
@@ -1,3 +1,3 @@
 """Version information for `langchain-core`."""
 
-VERSION = "1.6.12"
+VERSION = "1.7.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.6.12/pyproject.toml 
new/langchain_core-1.7.0/pyproject.toml
--- old/langchain_core-1.6.12/pyproject.toml    2020-02-02 01:00:00.000000000 
+0100
+++ new/langchain_core-1.7.0/pyproject.toml     2020-02-02 01:00:00.000000000 
+0100
@@ -21,7 +21,7 @@
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 
-version = "1.6.12"
+version = "1.7.0"
 requires-python = ">=3.10.0,<4.0.0"
 dependencies = [
     "langsmith>=0.14.9,<1.0.0",
@@ -33,7 +33,7 @@
     "packaging>=23.2.0",
     "pydantic>=2.7.4,<3.0.0",
     "uuid-utils>=0.12.0,<1.0",
-    "langchain-protocol>=0.0.17",
+    "langchain-protocol>=0.0.20",
 ]
 
 [project.urls]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.6.12/tests/unit_tests/language_models/test_chat_model_stream.py
 
new/langchain_core-1.7.0/tests/unit_tests/language_models/test_chat_model_stream.py
--- 
old/langchain_core-1.6.12/tests/unit_tests/language_models/test_chat_model_stream.py
        2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.7.0/tests/unit_tests/language_models/test_chat_model_stream.py
 2020-02-02 01:00:00.000000000 +0100
@@ -953,3 +953,329 @@
             await stream.text
         with pytest.raises(RuntimeError, match="async fail"):
             await stream
+
+
+# ---------------------------------------------------------------------------
+# Tool-call args: `tool-call-delta` slices and `block-delta` snapshots
+# ---------------------------------------------------------------------------
+
+_ARGS_PARTS = ['{"path": ', '"a.py", ', '"content": ', '"hi"}']
+
+
+def _start(index: int, block: dict[str, Any]) -> dict[str, Any]:
+    return {"event": "content-block-start", "index": index, "content": block}
+
+
+def _tool_call_start(
+    index: int, *, id_: str | None = "c1", name: str | None = "write_file"
+) -> dict[str, Any]:
+    return _start(
+        index, {"type": "tool_call_chunk", "id": id_, "name": name, "args": ""}
+    )
+
+
+def _tool_call_delta(index: int, args: str) -> dict[str, Any]:
+    return {
+        "event": "content-block-delta",
+        "index": index,
+        "delta": {"type": "tool-call-delta", "args": args},
+    }
+
+
+def _tool_call_finish(
+    index: int, args: dict[str, Any], *, id_: str = "c1", name: str = 
"write_file"
+) -> dict[str, Any]:
+    return {
+        "event": "content-block-finish",
+        "index": index,
+        "content": {"type": "tool_call", "id": id_, "name": name, "args": 
args},
+    }
+
+
+def _dispatch_all(stream: ChatModelStream, events: list[dict[str, Any]]) -> 
None:
+    for event in events:
+        stream.dispatch(event)
+
+
+def _block_delta(index: int, **fields: Any) -> dict[str, Any]:
+    return {
+        "event": "content-block-delta",
+        "index": index,
+        "delta": {
+            "type": "block-delta",
+            "fields": {"type": "tool_call_chunk", **fields},
+        },
+    }
+
+
+def _slice_events() -> list[dict[str, Any]]:
+    """One tool call streamed as `tool-call-delta` slices (emitting cores)."""
+    return [
+        {"event": "message-start", "role": "ai"},
+        _tool_call_start(0),
+        *[_tool_call_delta(0, part) for part in _ARGS_PARTS],
+        _tool_call_finish(0, {"path": "a.py", "content": "hi"}),
+        {"event": "message-finish"},
+    ]
+
+
+def _snapshot_events() -> list[dict[str, Any]]:
+    """The same tool call streamed as `block-delta` snapshots (older cores)."""
+    snapshots = ["".join(_ARGS_PARTS[: i + 1]) for i in 
range(len(_ARGS_PARTS))]
+    return [
+        {"event": "message-start", "role": "ai"},
+        _tool_call_start(0),
+        *[
+            _block_delta(0, id="c1", name="write_file", args=snapshot)
+            for snapshot in snapshots
+        ],
+        _tool_call_finish(0, {"path": "a.py", "content": "hi"}),
+        {"event": "message-finish"},
+    ]
+
+
+_EXPECTED_DELTAS = [
+    {"type": "tool_call_chunk", "id": "c1", "name": "write_file", "args": 
'{"path": '},
+    {"type": "tool_call_chunk", "id": "c1", "name": "write_file", "args": 
'"a.py", '},
+    {
+        "type": "tool_call_chunk",
+        "id": "c1",
+        "name": "write_file",
+        "args": '"content": ',
+    },
+    {"type": "tool_call_chunk", "id": "c1", "name": "write_file", "args": 
'"hi"}'},
+]
+
+
+class TestToolCallArgs:
+    """`.tool_calls` yields deltas whatever form the args arrive in."""
+
+    def test_slice_stream_yields_deltas_carrying_id_and_name(self) -> None:
+        stream = ChatModelStream()
+        _dispatch_all(stream, _slice_events())
+
+        assert list(stream.tool_calls) == _EXPECTED_DELTAS
+
+    def test_snapshot_stream_yields_the_same_deltas(self) -> None:
+        stream = ChatModelStream()
+        _dispatch_all(stream, _snapshot_events())
+
+        assert list(stream.tool_calls) == _EXPECTED_DELTAS
+
+    def test_both_forms_assemble_the_same_tool_calls(self) -> None:
+        from_slices = ChatModelStream()
+        _dispatch_all(from_slices, _slice_events())
+        from_snapshots = ChatModelStream()
+        _dispatch_all(from_snapshots, _snapshot_events())
+
+        expected = [
+            {
+                "type": "tool_call",
+                "id": "c1",
+                "name": "write_file",
+                "args": {"path": "a.py", "content": "hi"},
+            }
+        ]
+        assert from_slices.tool_calls.get() == expected
+        assert from_snapshots.tool_calls.get() == expected
+        assert from_slices.output == from_snapshots.output
+
+    def test_call_whose_args_stop_extending_goes_quiet_until_finish(self) -> 
None:
+        stream = ChatModelStream()
+        _dispatch_all(
+            stream,
+            [
+                {"event": "message-start", "role": "ai"},
+                _tool_call_start(0),
+                _block_delta(0, args='{"path": '),
+                # Doesn't extend '{"path": ', so no delta can express it.
+                _block_delta(0, args="{"),
+                # Lines up again, but the call stays quiet.
+                _block_delta(0, args='{"path": "a.py", '),
+                _tool_call_delta(0, '"content": "hi"}'),
+                _tool_call_finish(0, {"path": "a.py", "content": "hi"}),
+                {"event": "message-finish"},
+            ],
+        )
+
+        assert list(stream.tool_calls) == [
+            {
+                "type": "tool_call_chunk",
+                "id": "c1",
+                "name": "write_file",
+                "args": '{"path": ',
+            },
+        ]
+        assert stream.output.tool_calls == [
+            {
+                "type": "tool_call",
+                "id": "c1",
+                "name": "write_file",
+                "args": {"path": "a.py", "content": "hi"},
+            }
+        ]
+
+    def test_block_delta_without_args_yields_nothing_and_updates_id_and_name(
+        self,
+    ) -> None:
+        stream = ChatModelStream()
+        _dispatch_all(
+            stream,
+            [
+                {"event": "message-start", "role": "ai"},
+                _tool_call_start(0, id_=None, name=None),
+                _tool_call_delta(0, '{"path": '),
+                _block_delta(0, id="c1", name="write_file"),
+                _tool_call_delta(0, '"a.py"}'),
+                _tool_call_finish(0, {"path": "a.py"}),
+                {"event": "message-finish"},
+            ],
+        )
+
+        assert list(stream.tool_calls) == [
+            {"type": "tool_call_chunk", "id": None, "name": None, "args": 
'{"path": '},
+            {
+                "type": "tool_call_chunk",
+                "id": "c1",
+                "name": "write_file",
+                "args": '"a.py"}',
+            },
+        ]
+
+    def test_unknown_delta_variant_is_ignored(self) -> None:
+        stream = ChatModelStream()
+        unknown = {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "future-delta", "args": "ignored"},
+        }
+        events = _slice_events()
+        events.insert(3, unknown)
+        _dispatch_all(stream, events)
+
+        assert list(stream.tool_calls) == _EXPECTED_DELTAS
+        assert stream.output.tool_calls[0]["args"] == {"path": "a.py", 
"content": "hi"}
+
+    def test_deltas_carry_the_provider_index_from_the_start(self) -> None:
+        stream = ChatModelStream()
+        start = _start(
+            0,
+            {
+                "type": "tool_call_chunk",
+                "id": "c1",
+                "name": "write_file",
+                "args": "",
+                "index": 3,
+            },
+        )
+        _dispatch_all(stream, [start, _tool_call_delta(0, "{}")])
+
+        assert list(stream.tool_calls) == [
+            {
+                "type": "tool_call_chunk",
+                "id": "c1",
+                "name": "write_file",
+                "args": "{}",
+                "index": 3,
+            }
+        ]
+
+    def test_tool_call_delta_without_a_seen_start_is_left_to_finish(self) -> 
None:
+        stream = ChatModelStream()
+        _dispatch_all(
+            stream,
+            [
+                {"event": "message-start", "role": "ai"},
+                # The start was trimmed from the replay window.
+                *[_tool_call_delta(0, part) for part in _ARGS_PARTS],
+                _tool_call_finish(0, {"path": "a.py", "content": "hi"}),
+                {"event": "message-finish"},
+            ],
+        )
+
+        assert list(stream.tool_calls) == []
+        assert stream.output.tool_calls[0]["args"] == {"path": "a.py", 
"content": "hi"}
+
+    def test_tool_call_deltas_assemble_a_call_with_no_finish(self) -> None:
+        stream = ChatModelStream()
+        events = [e for e in _slice_events() if e["event"] != 
"content-block-finish"]
+        _dispatch_all(stream, events)
+
+        assert stream.output.tool_calls == [
+            {
+                "type": "tool_call",
+                "id": "c1",
+                "name": "write_file",
+                "args": {"path": "a.py", "content": "hi"},
+            }
+        ]
+
+    def test_server_tool_call_deltas_append_without_yielding(self) -> None:
+        stream = ChatModelStream()
+        _dispatch_all(
+            stream,
+            [
+                {"event": "message-start", "role": "ai"},
+                _start(
+                    0,
+                    {
+                        "type": "server_tool_call_chunk",
+                        "id": "s1",
+                        "name": "web_search",
+                        "args": "",
+                    },
+                ),
+                _tool_call_delta(0, '{"query": '),
+                _tool_call_delta(0, '"weather"}'),
+                # No content-block-finish: the message-finish sweep
+                # parses what the deltas appended.
+                {"event": "message-finish"},
+            ],
+        )
+
+        assert list(stream.tool_calls) == []
+        content = cast("list[dict[str, Any]]", stream.output.content)
+        assert content[0]["type"] == "server_tool_call"
+        assert content[0]["id"] == "s1"
+        assert content[0]["args"] == {"query": "weather"}
+
+    def test_args_on_the_start_are_the_first_delta(self) -> None:
+        stream = ChatModelStream()
+        start = _start(
+            0,
+            {
+                "type": "tool_call_chunk",
+                "id": "c1",
+                "name": "write_file",
+                "args": _ARGS_PARTS[0],
+            },
+        )
+        _dispatch_all(
+            stream, [start, *[_tool_call_delta(0, part) for part in 
_ARGS_PARTS[1:]]]
+        )
+
+        assert list(stream.tool_calls) == _EXPECTED_DELTAS
+
+    def test_legacy_content_block_deltas_carry_id_and_name(self) -> None:
+        stream = ChatModelStream()
+        legacy_parts = [
+            {"type": "tool_call_chunk", "id": "c1", "name": "write_file", 
"args": ""},
+            *[{"type": "tool_call_chunk", "args": part} for part in 
_ARGS_PARTS],
+        ]
+        _dispatch_all(
+            stream,
+            [
+                {"event": "content-block-delta", "index": 0, "content_block": 
part}
+                for part in legacy_parts
+            ],
+        )
+
+        assert list(stream.tool_calls) == _EXPECTED_DELTAS
+
+    @pytest.mark.asyncio
+    async def test_async_stream_yields_the_same_deltas(self) -> None:
+        stream = AsyncChatModelStream()
+        for event in _slice_events():
+            stream.dispatch(event)
+
+        assert [d async for d in stream.tool_calls] == _EXPECTED_DELTAS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.6.12/tests/unit_tests/language_models/test_chat_model_v3_stream.py
 
new/langchain_core-1.7.0/tests/unit_tests/language_models/test_chat_model_v3_stream.py
--- 
old/langchain_core-1.6.12/tests/unit_tests/language_models/test_chat_model_v3_stream.py
     2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.7.0/tests/unit_tests/language_models/test_chat_model_v3_stream.py
      2020-02-02 01:00:00.000000000 +0100
@@ -1365,6 +1365,88 @@
         assert text_blocks[0]["text"] == "Hello Boston!"
 
 
+_ARGS_SLICES = ['{"path": ', '"a.py", ', '"content": ', '"hi"}']
+
+
+class _StreamingToolCallModel(BaseChatModel):
+    """Fake model that streams one tool call's args across several chunks."""
+
+    @property
+    def _llm_type(self) -> str:
+        return "streaming-tool-call-fake"
+
+    def _generate(
+        self,
+        messages: list[BaseMessage],
+        stop: list[str] | None = None,
+        run_manager: CallbackManagerForLLMRun | None = None,
+        **kwargs: Any,
+    ) -> ChatResult:
+        del messages, stop, run_manager, kwargs
+        raise NotImplementedError
+
+    def _stream(
+        self,
+        messages: list[BaseMessage],
+        stop: list[str] | None = None,
+        run_manager: CallbackManagerForLLMRun | None = None,
+        **kwargs: Any,
+    ) -> Iterator[ChatGenerationChunk]:
+        del messages, stop, run_manager, kwargs
+        # Opener carries the id and name; the args follow in slices.
+        for i, part in enumerate(["", *_ARGS_SLICES]):
+            yield ChatGenerationChunk(
+                message=AIMessageChunk(
+                    content="",
+                    tool_call_chunks=[
+                        {
+                            "name": "write_file" if i == 0 else None,
+                            "args": part,
+                            "id": "call_1" if i == 0 else None,
+                            "index": 0,
+                        }
+                    ],
+                )
+            )
+
+
+_TOOL_CALL_DELTAS = [
+    {
+        "type": "tool_call_chunk",
+        "id": "call_1",
+        "name": "write_file",
+        "args": args,
+        "index": 0,
+    }
+    for args in _ARGS_SLICES
+]
+
+
+class TestToolCallsYieldDeltas:
+    """`.tool_calls` yields args deltas, as the bridge emits them."""
+
+    def test_stream_events_v3_tool_calls_yield_deltas(self) -> None:
+        stream = _StreamingToolCallModel().stream_events("hi", version="v3")
+
+        assert list(stream.tool_calls) == _TOOL_CALL_DELTAS
+
+    @pytest.mark.asyncio
+    async def test_astream_events_v3_tool_calls_yield_deltas(self) -> None:
+        stream = await _StreamingToolCallModel().astream_events("hi", 
version="v3")
+
+        assert [d async for d in stream.tool_calls] == _TOOL_CALL_DELTAS
+
+    def test_replayed_events_carry_each_args_slice(self) -> None:
+        stream = _StreamingToolCallModel().stream_events("hi", version="v3")
+        assert stream.output.tool_calls[0]["args"] == {"path": "a.py", 
"content": "hi"}
+
+        replayed = [e["delta"] for e in stream if e["event"] == 
"content-block-delta"]
+
+        assert replayed == [
+            {"type": "tool-call-delta", "args": args} for args in _ARGS_SLICES
+        ]
+
+
 class _CachedUsageModel(BaseChatModel):
     """Replays an Anthropic-shaped usage split across stream chunks.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.6.12/tests/unit_tests/language_models/test_compat_bridge.py
 new/langchain_core-1.7.0/tests/unit_tests/language_models/test_compat_bridge.py
--- 
old/langchain_core-1.6.12/tests/unit_tests/language_models/test_compat_bridge.py
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.7.0/tests/unit_tests/language_models/test_compat_bridge.py 
    2020-02-02 01:00:00.000000000 +0100
@@ -446,6 +446,361 @@
     }
 
 
+def _tool_call_chunk(
+    args: str,
+    *,
+    id_: str | None = None,
+    name: str | None = None,
+    index: int = 0,
+) -> ChatGenerationChunk:
+    return ChatGenerationChunk(
+        message=AIMessageChunk(
+            content="",
+            id="msg-1",
+            tool_call_chunks=[
+                {
+                    "type": "tool_call_chunk",
+                    "index": index,
+                    "id": id_,
+                    "name": name,
+                    "args": args,
+                }
+            ],
+        )
+    )
+
+
+def _sliced_tool_call_chunks() -> list[ChatGenerationChunk]:
+    # Like OpenAI, the opener has the `id` and `name` but no args.
+    return [
+        _tool_call_chunk("", id_="call_1", name="write_file"),
+        _tool_call_chunk('{"path": '),
+        _tool_call_chunk('"a.py"}'),
+    ]
+
+
+def test_chunks_to_events_tool_call_streams_args_as_slices() -> None:
+    """Each chunk's new args go out as a `tool-call-delta` carrying only them.
+
+    The call's `id`, `name` and provider `index` go on its start. An
+    opener with no args sends no delta.
+    """
+    events = list(
+        chunks_to_events(iter(_sliced_tool_call_chunks()), message_id="msg-1")
+    )
+
+    _assert_tool_call_streamed_as_slices(events)
+
+
[email protected]
+async def test_achunks_to_events_tool_call_streams_args_as_slices() -> None:
+    """Async bridge streams tool-call args as slices too."""
+    events = [
+        event
+        async for event in achunks_to_events(
+            _aiter_chunks(_sliced_tool_call_chunks()), message_id="msg-1"
+        )
+    ]
+
+    _assert_tool_call_streamed_as_slices(events)
+
+
+def _assert_tool_call_streamed_as_slices(events: list[Any]) -> None:
+    assert_valid_event_stream(events)
+    assert events[1:-1] == [
+        {
+            "event": "content-block-start",
+            "index": 0,
+            "content": {
+                "type": "tool_call_chunk",
+                "id": "call_1",
+                "name": "write_file",
+                "args": "",
+                "index": 0,
+            },
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '{"path": '},
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '"a.py"}'},
+        },
+        {
+            "event": "content-block-finish",
+            "index": 0,
+            "content": {
+                "type": "tool_call",
+                "id": "call_1",
+                "name": "write_file",
+                "args": {"path": "a.py"},
+            },
+        },
+    ]
+
+
+def _anthropic_server_tool_call_chunks() -> list[ChatGenerationChunk]:
+    blocks: list[dict[str, Any]] = [
+        {
+            "type": "server_tool_use",
+            "id": "srvtoolu_1",
+            "name": "web_search",
+            "input": {},
+        },
+        {"type": "input_json_delta", "partial_json": '{"query": '},
+        {"type": "input_json_delta", "partial_json": '"weather"}'},
+    ]
+    return [
+        ChatGenerationChunk(
+            message=AIMessageChunk(
+                content=[{"index": 0, **block}],
+                id="msg-1",
+                response_metadata={"model_provider": "anthropic"},
+            )
+        )
+        for block in blocks
+    ]
+
+
+def test_chunks_to_events_server_tool_call_streams_args_as_slices() -> None:
+    """Anthropic server-tool `partial_json` streams as `tool-call-delta` 
too."""
+    events = list(
+        chunks_to_events(iter(_anthropic_server_tool_call_chunks()), 
message_id="msg-1")
+    )
+
+    _assert_server_tool_call_streamed_as_slices(events)
+
+
[email protected]
+async def test_achunks_to_events_server_tool_call_streams_args_as_slices() -> 
None:
+    """Async bridge streams server-tool args as slices too."""
+    events = [
+        event
+        async for event in achunks_to_events(
+            _aiter_chunks(_anthropic_server_tool_call_chunks()), 
message_id="msg-1"
+        )
+    ]
+
+    _assert_server_tool_call_streamed_as_slices(events)
+
+
+def _assert_server_tool_call_streamed_as_slices(events: list[Any]) -> None:
+    assert_valid_event_stream(events)
+    assert events[1:-1] == [
+        {
+            "event": "content-block-start",
+            "index": 0,
+            "content": {
+                "type": "server_tool_call_chunk",
+                "id": "srvtoolu_1",
+                "name": "web_search",
+                "args": "",
+                "index": 0,
+            },
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '{"query": '},
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '"weather"}'},
+        },
+        {
+            "event": "content-block-finish",
+            "index": 0,
+            "content": {
+                "type": "server_tool_call",
+                "id": "srvtoolu_1",
+                "name": "web_search",
+                "args": {"query": "weather"},
+                "index": 0,
+            },
+        },
+    ]
+
+
+def test_chunks_to_events_late_tool_call_id_and_name_arrive_on_finish() -> 
None:
+    """A call's `id` and `name` aren't updated after its start.
+
+    `tool-call-delta` carries only args. An `id` or `name` the start
+    didn't have reaches consumers on `content-block-finish`.
+    """
+    chunks = [
+        _tool_call_chunk('{"q": '),
+        _tool_call_chunk('"x"}', id_="c1", name="search"),
+    ]
+
+    events = list(chunks_to_events(iter(chunks), message_id="msg-1"))
+
+    assert_valid_event_stream(events)
+    assert events[1:-1] == [
+        {
+            "event": "content-block-start",
+            "index": 0,
+            "content": {
+                "type": "tool_call_chunk",
+                "id": None,
+                "name": None,
+                "args": "",
+                "index": 0,
+            },
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '{"q": '},
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '"x"}'},
+        },
+        {
+            "event": "content-block-finish",
+            "index": 0,
+            "content": {
+                "type": "tool_call",
+                "id": "c1",
+                "name": "search",
+                "args": {"q": "x"},
+            },
+        },
+    ]
+
+
+def test_chunks_to_events_tool_call_extras_only_on_finish() -> None:
+    """Provider `extras` are for round-tripping, which reads the finished 
block."""
+    chunks = [
+        ChatGenerationChunk(
+            message=AIMessageChunk(
+                content=[
+                    {
+                        "type": "tool_call_chunk",
+                        "id": "c1",
+                        "name": "search",
+                        "args": '{"q": "x"}',
+                        "index": 0,
+                        "extras": {"item_id": "fc_1"},
+                    }
+                ],
+                response_metadata={"output_version": "v1"},
+            )
+        )
+    ]
+
+    events: list[Any] = list(chunks_to_events(iter(chunks), 
message_id="msg-1"))
+
+    start, delta, finish = events[1:-1]
+    assert start["content"] == {
+        "type": "tool_call_chunk",
+        "id": "c1",
+        "name": "search",
+        "args": "",
+        "index": 0,
+    }
+    assert delta["delta"] == {"type": "tool-call-delta", "args": '{"q": "x"}'}
+    assert finish["content"] == {
+        "type": "tool_call",
+        "id": "c1",
+        "name": "search",
+        "args": {"q": "x"},
+        "extras": {"item_id": "fc_1"},
+    }
+
+
+def _blank_args(event: Any) -> Any:
+    """Blank a delta's args in place, as a stream transformer might."""
+    if event["event"] == "content-block-delta":
+        # Also reaches `block-delta` snapshots, whose `fields` were the
+        # bridge's own running copy of the args, so going back to them
+        # fails these tests.
+        delta = event["delta"]
+        delta.get("fields", delta)["args"] = ""
+    return event
+
+
+def test_chunks_to_events_tool_call_deltas_are_independent_of_the_call() -> 
None:
+    """Editing a delta in place can't corrupt the call.
+
+    Each delta is a new object, not the bridge's running copy of the
+    args, so the finished call doesn't see the edit.
+    """
+    events = [
+        _blank_args(event)
+        for event in chunks_to_events(iter(_sliced_tool_call_chunks()))
+    ]
+
+    finishes = [e for e in events if e["event"] == "content-block-finish"]
+    assert finishes[0]["content"]["args"] == {"path": "a.py"}
+
+
[email protected]
+async def 
test_achunks_to_events_tool_call_deltas_are_independent_of_the_call() -> None:
+    """Editing a delta in place can't corrupt the call in the async bridge 
either."""
+    events = [
+        _blank_args(event)
+        async for event in 
achunks_to_events(_aiter_chunks(_sliced_tool_call_chunks()))
+    ]
+
+    finishes = [e for e in events if e["event"] == "content-block-finish"]
+    assert finishes[0]["content"]["args"] == {"path": "a.py"}
+
+
+def test_message_to_events_tool_call_chunk_sends_its_args_as_one_slice() -> 
None:
+    """Replaying an unfinished chunk sends its args as a single 
`tool-call-delta`."""
+    msg = AIMessageChunk(
+        content="",
+        id="msg-1",
+        tool_call_chunks=[
+            {
+                "type": "tool_call_chunk",
+                "index": 0,
+                "id": "c1",
+                "name": "search",
+                "args": '{"q": "x"}',
+            }
+        ],
+    )
+
+    events = list(message_to_events(msg))
+
+    assert_valid_event_stream(events)
+    assert events[1:-1] == [
+        {
+            "event": "content-block-start",
+            "index": 0,
+            "content": {
+                "type": "tool_call_chunk",
+                "id": "c1",
+                "name": "search",
+                "args": "",
+                "index": 0,
+            },
+        },
+        {
+            "event": "content-block-delta",
+            "index": 0,
+            "delta": {"type": "tool-call-delta", "args": '{"q": "x"}'},
+        },
+        {
+            "event": "content-block-finish",
+            "index": 0,
+            "content": {
+                "type": "tool_call",
+                "id": "c1",
+                "name": "search",
+                "args": {"q": "x"},
+            },
+        },
+    ]
+
+
 def test_chunks_to_events_invalid_tool_call_keeps_stop_reason() -> None:
     """Malformed tool-args become invalid_tool_call; finish_reason stays 
`stop`."""
     chunks = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.6.12/uv.lock 
new/langchain_core-1.7.0/uv.lock
--- old/langchain_core-1.6.12/uv.lock   2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.7.0/uv.lock    2020-02-02 01:00:00.000000000 +0100
@@ -268,11 +268,11 @@
 
 [[package]]
 name = "certifi"
-version = "2025.11.12"
+version = "2026.10.19"
 source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz";,
 hash = 
"sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size 
= 160538, upload-time = "2025-11-12T02:54:51.517Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz";,
 hash = 
"sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size 
= 137598, upload-time = "2026-10-20T02:24:16.604Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl";,
 hash = 
"sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size 
= 159438, upload-time = "2025-11-12T02:54:49.735Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl";,
 hash = 
"sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size 
= 136464, upload-time = "2026-10-20T02:24:15.072Z" },
 ]
 
 [[package]]
@@ -525,7 +525,7 @@
 version = "1.3.1"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz";,
 hash = 
"sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size 
= 30371, upload-time = "2025-11-21T23:01:54.787Z" }
 wheels = [
@@ -625,8 +625,8 @@
 version = "2.13.1"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "h11", marker = "python_full_version != '3.12.*' or sys_platform 
!= 'emscripten'" },
-    { name = "truststore", marker = "python_full_version != '3.12.*' or 
sys_platform != 'emscripten'" },
+    { name = "h11" },
+    { name = "truststore" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz";,
 hash = 
"sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size 
= 68071, upload-time = "2026-09-23T07:47:22.372Z" }
 wheels = [
@@ -726,17 +726,17 @@
     "python_full_version < '3.11' and platform_python_implementation != 
'PyPy'",
 ]
 dependencies = [
-    { name = "colorama", marker = "python_full_version < '3.11' and 
sys_platform == 'win32'" },
-    { name = "decorator", marker = "python_full_version < '3.11'" },
-    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
-    { name = "jedi", marker = "python_full_version < '3.11'" },
-    { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
-    { name = "pexpect", marker = "python_full_version < '3.11' and 
sys_platform != 'emscripten' and sys_platform != 'win32'" },
-    { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
-    { name = "pygments", marker = "python_full_version < '3.11'" },
-    { name = "stack-data", marker = "python_full_version < '3.11'" },
-    { name = "traitlets", marker = "python_full_version < '3.11'" },
-    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "decorator" },
+    { name = "exceptiongroup" },
+    { name = "jedi" },
+    { name = "matplotlib-inline" },
+    { name = "pexpect", marker = "sys_platform != 'emscripten' and 
sys_platform != 'win32'" },
+    { name = "prompt-toolkit" },
+    { name = "pygments" },
+    { name = "stack-data" },
+    { name = "traitlets" },
+    { name = "typing-extensions" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz";,
 hash = 
"sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size 
= 5606088, upload-time = "2025-05-31T16:39:09.613Z" }
 wheels = [
@@ -760,17 +760,17 @@
     "(python_full_version >= '3.11' and python_full_version < '3.13' and 
platform_python_implementation != 'PyPy' and sys_platform != 'emscripten') or 
(python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' 
and sys_platform == 'emscripten')",
 ]
 dependencies = [
-    { name = "colorama", marker = "python_full_version >= '3.11' and 
sys_platform == 'win32'" },
-    { name = "decorator", marker = "python_full_version >= '3.11'" },
-    { name = "ipython-pygments-lexers", marker = "python_full_version >= 
'3.11'" },
-    { name = "jedi", marker = "python_full_version >= '3.11'" },
-    { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
-    { name = "pexpect", marker = "python_full_version >= '3.11' and 
sys_platform != 'emscripten' and sys_platform != 'win32'" },
-    { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
-    { name = "pygments", marker = "python_full_version >= '3.11'" },
-    { name = "stack-data", marker = "python_full_version >= '3.11'" },
-    { name = "traitlets", marker = "python_full_version >= '3.11'" },
-    { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "decorator" },
+    { name = "ipython-pygments-lexers" },
+    { name = "jedi" },
+    { name = "matplotlib-inline" },
+    { name = "pexpect", marker = "sys_platform != 'emscripten' and 
sys_platform != 'win32'" },
+    { name = "prompt-toolkit" },
+    { name = "pygments" },
+    { name = "stack-data" },
+    { name = "traitlets" },
+    { name = "typing-extensions", marker = "python_full_version < '3.12'" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/12/51/a703c030f4928646d390b4971af4938a1b10c9dfce694f0d99a0bb073cb2/ipython-9.8.0.tar.gz";,
 hash = 
"sha256:8e4ce129a627eb9dd221c41b1d2cdaed4ef7c9da8c17c63f6f578fe231141f83", size 
= 4424940, upload-time = "2025-12-03T10:18:24.353Z" }
 wheels = [
@@ -782,7 +782,7 @@
 version = "1.1.1"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "pygments", marker = "python_full_version >= '3.11'" },
+    { name = "pygments" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz";,
 hash = 
"sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size 
= 8393, upload-time = "2025-01-17T11:24:34.505Z" }
 wheels = [
@@ -1130,7 +1130,7 @@
 
 [[package]]
 name = "langchain-core"
-version = "1.6.12"
+version = "1.7.0"
 source = { editable = "." }
 dependencies = [
     { name = "httpx" },
@@ -1183,7 +1183,7 @@
 requires-dist = [
     { name = "httpx", specifier = ">=0.23.0,<1.0.0" },
     { name = "jsonpatch", specifier = ">=1.33.0,<2.0.0" },
-    { name = "langchain-protocol", specifier = ">=0.0.17" },
+    { name = "langchain-protocol", specifier = ">=0.0.20" },
     { name = "langsmith", specifier = ">=0.14.9,<1.0.0" },
     { name = "packaging", specifier = ">=23.2.0" },
     { name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
@@ -1228,14 +1228,14 @@
 
 [[package]]
 name = "langchain-protocol"
-version = "0.0.18"
+version = "0.0.20"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "typing-extensions" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz";,
 hash = 
"sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size 
= 6150, upload-time = "2026-06-18T17:08:26.959Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/cb/8b/d908d082c3cf5368ddd1816ed114de79c873f979ba7f98b95fab0e841643/langchain_protocol-0.0.20.tar.gz";,
 hash = 
"sha256:0f5bd002474265648e91161771a9e0438fe9480bd55a416e51af0f60070637ec", size 
= 6280, upload-time = "2026-10-20T11:13:50.84Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl";,
 hash = 
"sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size 
= 7221, upload-time = "2026-06-18T17:08:25.996Z" },
+    { url = 
"https://files.pythonhosted.org/packages/02/4b/f1c99a0b27c5ff8b4b131c0d6e62012c17cc424089efe213767db927afd2/langchain_protocol-0.0.20-py3-none-any.whl";,
 hash = 
"sha256:3c935e5e46b0f6f9cb52f45ba7cfa53d1b9fa80c82c4a277d47f03e07f7385b1", size 
= 7347, upload-time = "2026-10-20T11:13:49.566Z" },
 ]
 
 [[package]]
@@ -2069,7 +2069,7 @@
 version = "4.9.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "ptyprocess", marker = "python_full_version != '3.12.*' or 
sys_platform != 'emscripten'" },
+    { name = "ptyprocess" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz";,
 hash = 
"sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size 
= 166450, upload-time = "2023-11-25T09:07:26.339Z" }
 wheels = [

Reply via email to