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 = [