Skip to content

V1.4#6

Open
DhirenMhatre wants to merge 16 commits into
masterfrom
v1.4
Open

V1.4#6
DhirenMhatre wants to merge 16 commits into
masterfrom
v1.4

Conversation

@DhirenMhatre
Copy link
Copy Markdown

Fixes #


Read the full contributing guidelines: https://docs.langchain.com/oss/python/contributing/overview

All contributions must be in English. See the language policy.

If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!

Thank you for contributing to LangChain! Follow these steps to have your pull request considered as ready for review.

  1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION
  1. PR description:
  • Write 1-2 sentences summarizing the change.
  • The Fixes #xx line at the top is required for external contributions — update the issue number and keep the keyword. This links your PR to the approved issue and auto-closes it on merge.
  • If there are any breaking changes, please clearly describe them.
  • If this PR depends on another PR being merged first, please include "Depends on #PR_NUMBER" in the description.
  1. Run make format, make lint and make test from the root of the package(s) you've modified.
  • We will not consider a PR unless these three are passing in CI.
  1. How did you verify your code works?

Additional guidelines:

  • All external PRs must link to an issue or discussion where a solution has been approved by a maintainer, and you must be assigned to that issue. PRs without prior approval will be closed.
  • PRs should not touch more than one package unless absolutely necessary.
  • Do not update the uv.lock files or add dependencies to pyproject.toml files (even optional ones) unless you have explicit permission to do so by a maintainer.

Social handles (optional)

Twitter: @
LinkedIn: https://linkedin.com/in/

nick-hollon-lc and others added 16 commits May 1, 2026 09:20
…e_schema wins (langchain-ai#37223)

## Problem

`_resolve_schemas` collected middleware state schemas into a
`set[type]`, making field-conflict resolution non-deterministic. When
two schemas declare the same field with different annotations (e.g.
`DeltaChannel` vs `BinaryOperatorAggregate` on `messages`), the winner
depended on set iteration order — effectively random across runs.

This forced callers to work around it with post-compilation patches
like:

```python
if not isinstance(agent.channels.get("messages"), DeltaChannel):
    agent.channels["messages"] = DeltaChannel(...)
```

## Fix

Change `_resolve_schemas` to accept `list[type]` and build it as:

```python
state_schemas = [*(m.state_schema for m in middleware), base_state]
```

`base_state` (the caller's explicit `state_schema`, or `AgentState` by
default) goes **last** and wins any conflict. Middleware schemas precede
it in registration order.

This means callers can now pass `state_schema=MyCustomState` and be
guaranteed it overrides any conflicting middleware annotation — no
post-compilation patching needed.

## Test plan

- All 9 existing `test_state_schema.py` tests pass
- Updated the two direct `_resolve_schemas({...})` call sites in tests
to use lists
Bumps `langchain` from `1.3.0a1` → `1.3.0a2`.
Bumps `langchain` from `1.3.0a1` → `1.3.0a2`. Also locks
`langgraph==1.2.0a7` / `langgraph-checkpoint==4.1.0a4` and adds
`prerelease = "allow"` to `[tool.uv]` so the resolver picks up
transitive alpha deps automatically.
Bumps `langchain-core` from `1.4.0a2` → `1.4.0`
@codity-dm
Copy link
Copy Markdown

codity-dm Bot commented May 21, 2026

Policy Check Failed

✗ 3/3 policy checks failed:

• Need 2 more approval(s) (0/2) — comment LGTM or approve via review
• Missing ticket reference (expected: JIRA-, ENG-, #*)
• 29 code file(s) changed but no test files added


To merge this PR:

  1. Address the failed checks listed above
  2. Ensure branch protection requires the codity/policy-check status

Configure policies in your dashboard

@codity-dm
Copy link
Copy Markdown

codity-dm Bot commented May 21, 2026

PR Summary

What Changed

  • Replaced the experimental stream_v2()/astream_v2() API with stream_events(version="v3")/astream_events(version="v3") across the codebase.
  • Updated the streaming protocol to use explicit delta types (text-delta, reasoning-delta, data-delta, block-delta) and changed event field names (content_blockcontent, message_idid).
  • Added support for interleaved parallel tool-call chunks with stable block indices throughout the streaming lifecycle.

Key Changes by Area

Streaming API: Unified stream_events/astream_events with version parameter supporting "v1", "v2" (default), and "v3"; removed deprecated stream_v2/astream_v2 methods.

Event Protocol: ContentBlockDeltaData now uses delta field with typed deltas; ContentBlockStartData/ContentBlockFinishData use content field; MessageStartData uses id instead of message_id.

Compat Bridge: Added multi-block tracking with stable indices for interleaved content blocks; added achunks_to_events for async conversion; added _to_content_delta() for explicit delta type conversion.

Agent Streaming: Added transformers parameter to create_agent for custom StreamTransformer factories; changed schema resolution from set to list for deterministic merge order.

Type Safety: Added @overload signatures for version-based return types; added _ChatModelBinding class to preserve typed v3 overloads through .bind() calls.

Files Changed

File Changes Summary
libs/core/langchain_core/language_models/_compat_bridge.py Added multi-block tracking, _to_content_delta(), achunks_to_events, data block accumulation
libs/core/langchain_core/language_models/chat_model_stream.py Added _merge_block_delta_into_store(), _event_content_block(), _event_delta() helpers; updated _push_content_block_delta() for explicit delta types
libs/core/langchain_core/language_models/chat_models.py Added stream_events/astream_events with version parameter; added _ChatModelBinding; renamed internal methods to _chat_model_stream_v3
libs/core/langchain_core/runnables/base.py Added v3 support in Runnable and RunnableBindingBase; added stream_events sync method
libs/core/langchain_core/tracers/_streaming.py Updated docstring references to new API
libs/core/pyproject.toml Bumped version to 1.4.0; updated langchain-protocol to >=0.0.14
libs/core/tests/unit_tests/language_models/test_chat_model_streamer.py New unit tests for v3 streaming
libs/core/tests/unit_tests/language_models/test_chat_model_v3_stream.py Renamed from test_stream_v2.py; updated event structures
libs/core/tests/unit_tests/language_models/test_compat_bridge.py Updated event structures; added interleaved tool-call tests
libs/core/tests/unit_tests/runnables/test_runnable_events_v3.py New tests for v3 dispatch path
libs/langchain_v1/langchain/agents/agent.py Added transformers parameter; changed schema resolution to list
libs/langchain_v1/tests/unit_tests/agents/test_agent_streaming.py New comprehensive streaming tests (285 lines)
libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py Updated API calls to v3
libs/partners/anthropic/tests/unit_tests/test_chat_models.py Updated API calls; added test_anthropic_stream_events_v3_lifecycle
libs/standard-tests/langchain_tests/utils/stream_lifecycle.py Updated validator for interleaved blocks and new delta types

Review Focus Areas

  • Delta type handling in _compat_bridge.py for parallel tool-call interleaving.
  • Version routing logic in chat_models.py overloads and RunnableBindingBase.
  • Backward compatibility for legacy delta formats in chat_model_stream.py.

Architecture

Design Decisions: The v3 protocol uses explicit delta types instead of raw content blocks to enable richer streaming projections (.text, .reasoning, .tool_calls, .output). Multi-block tracking with stable indices is intentional to support Anthropic-style interleaved tool calls. The version parameter unifies three streaming protocols under one method signature, with v2 remaining the default for backward compatibility.

Scalability & Extensibility: The transformers parameter in create_agent allows custom stream processing without core changes. The _ChatModelBinding class preserves type safety through .bind() chains. Out of scope: full graph streaming beyond chat models.

Risks:

  • Intentional: Base Runnable raises NotImplementedError for v3, requiring explicit opt-in from subclasses. This is acceptable but creates a migration burden for custom runnables.
  • Unintentional: The Mapping[str, Any] relaxation in dispatch methods reduces type safety; reviewers should check this does not mask real type errors.

Merge Status

NOT MERGEABLE — PR Score 32/100, below threshold (50)

  • [H4] PR quality score (32) is below merge floor (50)
  • [H5] 3 HIGH-severity inline review findings need resolution (threshold: 3)
  • [H6] Code quality raw score (17) is below merge floor (40)

Comment on lines +329 to +333
for key, value in delta.items():
if key in ("type", "data") or value is None:
continue
if key == "extras" and isinstance(value, dict):
state["extras"] = {**(state.get("extras") or {}), **value}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional High

Using dict.update() for input_token_details and output_token_details overwrites per-key counts across chunks instead of summing them, so nested usage totals become incorrect; sum numeric subkeys when merging.

Suggested fix
            if detail_key not in current:
                current[detail_key] = {}
            for subkey, subvalue in delta[detail_key].items():
                current[detail_key][subkey] = current[detail_key].get(subkey, 0) + subvalue
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert python developer with deep knowledge of security, performance, and best practices.

### Context

File: libs/core/langchain_core/language_models/_compat_bridge.py
Lines: 329-333
Issue Type: functional-high
Severity: high

Issue Description:
Using `dict.update()` for `input_token_details` and `output_token_details` overwrites per-key counts across chunks instead of summing them, so nested usage totals become incorrect; sum numeric subkeys when merging.

Current Code:
            if detail_key not in current:
                current[detail_key] = {}
            current[detail_key].update(delta[detail_key])

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow python best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

Comment on lines +68 to +78
def _merge_block_delta_into_store(
store: dict[int, dict[str, Any]],
idx: int,
fields: dict[str, Any],
) -> None:
"""Shallow-merge a block-delta snapshot into an indexed chunk store."""
existing = store.get(idx, {})
for key, value in fields.items():
if value is not None:
existing[key] = value
store[idx] = existing
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional High

The new block-delta path overwrites previously accumulated tool-call args chunks, so partial JSON fragments are lost; append args like _merge_chunk_into_store does or special-case that field during merge.

Suggested fix
def _merge_block_delta_into_store(
    store: dict[int, dict[str, Any]],
    idx: int,
    fields: dict[str, Any],
) -> None:
    """Merge a block-delta snapshot into an indexed chunk store."""
    existing = store.get(idx, {})
    for key, value in fields.items():
        if value is None:
            continue
        if key == "args":
            existing[key] = existing.get(key, "") + value
        else:
            existing[key] = value
    store[idx] = existing
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert python developer with deep knowledge of security, performance, and best practices.

### Context

File: libs/core/langchain_core/language_models/chat_model_stream.py
Lines: 68-78
Issue Type: functional-high
Severity: high

Issue Description:
The new `block-delta` path overwrites previously accumulated tool-call `args` chunks, so partial JSON fragments are lost; append `args` like `_merge_chunk_into_store` does or special-case that field during merge.

Current Code:
def _merge_block_delta_into_store(
    store: dict[int, dict[str, Any]],
    idx: int,
    fields: dict[str, Any],
) -> None:
    """Shallow-merge a block-delta snapshot into an indexed chunk store."""
    existing = store.get(idx, {})
    for key, value in fields.items():
        if value is not None:
            existing[key] = value
    store[idx] = existing

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow python best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

Comment on lines +172 to +186
dtype = delta.get("type")
if dtype == "text-delta":
accum["text"] = accum.get("text", "") + delta.get("text", "")
elif dtype == "reasoning-delta":
accum["reasoning"] = accum.get("reasoning", "") + delta.get("reasoning", "")
elif dtype == "data-delta":
accum["data"] = accum.get("data", "") + delta.get("data", "")
elif dtype == "block-delta":
fields = delta.get("fields")
if not isinstance(fields, dict):
return
btype = fields.get("type")
if btype not in _DELTAABLE_TYPES:
return
accum.update({k: v for k, v in fields.items() if v is not None})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional High

Tool-call chunk deltas are no longer accumulated into args, id, and name; add explicit handling for tool_call_chunk and server_tool_call_chunk delta shapes so finish validation still works.

Suggested fix
    dtype = delta.get("type")
    if dtype == "text-delta":
        accum["text"] = accum.get("text", "") + delta.get("text", "")
    elif dtype == "reasoning-delta":
        accum["reasoning"] = accum.get("reasoning", "") + delta.get("reasoning", "")
    elif dtype == "data-delta":
        accum["data"] = accum.get("data", "") + delta.get("data", "")
    elif dtype in {"tool_call_chunk-delta", "server_tool_call_chunk-delta"}:
        accum["args"] = accum.get("args", "") + (delta.get("args") or "")
        if delta.get("id") is not None:
            accum["id"] = delta["id"]
        if delta.get("name") is not None:
            accum["name"] = delta["name"]
    elif dtype == "block-delta":
        fields = delta.get("fields")
        if not isinstance(fields, dict):
            return
        btype = fields.get("type")
        if btype not in _DELTAABLE_TYPES:
            return
        if btype in {"tool_call_chunk", "server_tool_call_chunk"}:
            accum["args"] = accum.get("args", "") + (fields.get("args") or "")
            if fields.get("id") is not None:
                accum["id"] = fields["id"]
            if fields.get("name") is not None:
                accum["name"] = fields["name"]
        else:
            accum.update({k: v for k, v in fields.items() if v is not None})
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert python developer with deep knowledge of security, performance, and best practices.

### Context

File: libs/standard-tests/langchain_tests/utils/stream_lifecycle.py
Lines: 172-186
Issue Type: functional-high
Severity: high

Issue Description:
Tool-call chunk deltas are no longer accumulated into `args`, `id`, and `name`; add explicit handling for `tool_call_chunk` and `server_tool_call_chunk` delta shapes so finish validation still works.

Current Code:
    dtype = delta.get("type")
    if dtype == "text-delta":
        accum["text"] = accum.get("text", "") + delta.get("text", "")
    elif dtype == "reasoning-delta":
        accum["reasoning"] = accum.get("reasoning", "") + delta.get("reasoning", "")
    elif dtype == "data-delta":
        accum["data"] = accum.get("data", "") + delta.get("data", "")
    elif dtype == "block-delta":
        fields = delta.get("fields")
        if not isinstance(fields, dict):
            return
        btype = fields.get("type")
        if btype not in _DELTAABLE_TYPES:
            return
        accum.update({k: v for k, v in fields.items() if v is not None})

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow python best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

@codity-dm
Copy link
Copy Markdown

codity-dm Bot commented May 21, 2026

Security Scan Summary

Metric Value
Vulnerabilities Critical: 0
Overall Risk Clean
Files Scanned 37

No critical security issues detected

Scan completed in 79.0s

Security scan powered by Codity.ai

@codity-dm
Copy link
Copy Markdown

codity-dm Bot commented May 21, 2026

License Compliance Scan

Metric Value
Packages Scanned 0
High Risk (Strong Copyleft) 0
Medium Risk (Weak Copyleft) 0
Low Risk (Permissive) 0
Unknown License 0

All licenses are low-risk and compliant

Powered by Codity.ai · Docs

@codity-dm
Copy link
Copy Markdown

codity-dm Bot commented May 21, 2026

Code Quality Report — test-org-codity/langchain · PR #6

Scanned: 2026-05-21 13:07 UTC | Score: 17/100 | Provider: github

Executive Summary

Severity Count
Critical 0
High 0
Medium 4
Low 179
Top Findings

[CQ-LLM-004] libs/core/langchain_core/language_models/_compat_bridge.py:102 (Complexity · MEDIUM)

Issue: The _to_content_delta function has deep nesting due to multiple if statements.
Suggestion: Consider refactoring the function to reduce nesting, possibly by using early returns.

if btype == "text": ... elif btype == "reasoning": ...

[CQ-LLM-005] libs/core/langchain_core/language_models/_compat_bridge.py:110 (Maintainability · MEDIUM)

Issue: The use of magic strings like 'text', 'reasoning', and 'data' can lead to maintainability issues.
Suggestion: Define constants for these magic strings to improve maintainability.

if btype == "text": ... if btype == "reasoning": ...

[CQ-LLM-006] libs/core/langchain_core/language_models/_compat_bridge.py:111 (Error_Handling · MEDIUM)

Issue: The _to_content_delta function does not handle cases where block may not have expected keys.
Suggestion: Add checks to ensure that the expected keys exist in block before accessing them.

return cast("TextDelta", {"type": "text-delta", "text": block.get("text", "")})

[CQ-012] libs/standard-tests/langchain_tests/utils/stream_lifecycle.py:89 (Performance · MEDIUM)

Issue: Synchronous I/O call may block the event loop in async context
Suggestion: Use async alternatives (aiofiles, httpx, asyncio.sleep)

f"still open (event {i})"

[CQ-LLM-001] libs/core/langchain_core/callbacks/base.py:135 (Documentation · LOW)

Issue: Docstring for on_stream_event method is updated but lacks detailed parameter descriptions.
Suggestion: Add detailed descriptions for parameters in the docstring.

"""Run on each protocol event from `stream_events(version="v3")`."""

[CQ-LLM-002] libs/core/langchain_core/callbacks/manager.py:750 (Documentation · LOW)

Issue: Docstring for on_stream_event method is updated but lacks detailed parameter descriptions.
Suggestion: Add detailed descriptions for parameters in the docstring.

"""Run on each protocol event from `stream_events(version="v3")`."""

[CQ-LLM-003] libs/core/langchain_core/language_models/_compat_bridge.py:111 (Documentation · LOW)

Issue: Docstring for _to_content_delta method is missing.
Suggestion: Add a docstring to explain the purpose and parameters of the _to_content_delta function.

def _to_content_delta(block: CompatBlock) -> ContentBlockDelta:

[CQ-002] libs/core/langchain_core/language_models/_compat_bridge.py:604 (Complexity · LOW)

Issue: Deep nesting detected (depth ~5)
Suggestion: Extract nested blocks into helper functions

index=wire_idx,

[CQ-002] libs/core/langchain_core/language_models/_compat_bridge.py:605 (Complexity · LOW)

Issue: Deep nesting detected (depth ~5)
Suggestion: Extract nested blocks into helper functions

content=_start_skeleton(block),

[CQ-002] libs/core/langchain_core/language_models/_compat_bridge.py:613 (Complexity · LOW)

Issue: Deep nesting detected (depth ~5)
Suggestion: Extract nested blocks into helper functions

"tool_call_chunk",

Per-File Breakdown

File Critical High Medium Low Total
libs/core/langchain_core/callbacks/base.py 0 0 0 1 1
libs/core/langchain_core/callbacks/manager.py 0 0 0 1 1
libs/core/langchain_core/language_models/_compat_bridge.py 0 0 3 13 16
libs/core/langchain_core/language_models/chat_model_stream.py 0 0 0 21 21
libs/core/langchain_core/language_models/chat_models.py 0 0 0 12 12
libs/core/langchain_core/runnables/base.py 0 0 0 13 13
libs/core/tests/unit_tests/language_models/test_chat_model_streamer.py 0 0 0 7 7
libs/core/tests/unit_tests/language_models/test_chat_model_v3_stream.py 0 0 0 12 12
libs/core/tests/unit_tests/language_models/test_compat_bridge.py 0 0 0 37 37
libs/core/tests/unit_tests/runnables/test_runnable_events_v3.py 0 0 0 2 2
libs/core/uv.lock 0 0 0 4 4
libs/langchain/langchain_classic/chat_models/base.py 0 0 0 1 1
libs/langchain/uv.lock 0 0 0 4 4
libs/langchain_v1/langchain/chat_models/base.py 0 0 0 1 1
libs/langchain_v1/tests/unit_tests/agents/test_agent_streaming.py 0 0 0 10 10
libs/langchain_v1/tests/unit_tests/agents/test_state_schema.py 0 0 0 3 3
libs/langchain_v1/uv.lock 0 0 0 16 16
libs/partners/anthropic/tests/unit_tests/test_chat_models.py 0 0 0 1 1
libs/partners/anthropic/uv.lock 0 0 0 4 4
libs/partners/openai/uv.lock 0 0 0 12 12
libs/standard-tests/langchain_tests/utils/stream_lifecycle.py 0 0 1 0 1
libs/standard-tests/uv.lock 0 0 0 4 4

Recommendations

  • Run automated tests after applying fixes to verify no regressions.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR introduces the stream_events(version="v3") / astream_events(version="v3") API as the new public entry point for the content-block-centric streaming protocol, replacing the now-private _chat_model_stream_v3 / _achat_model_stream_v3 internals that had previously been exposed as stream_v2 / astream_v2. The protocol itself gains support for interleaved parallel content blocks, field renames (content_block → content/delta, message_id → id), and a new data-delta block type.

  • BaseChatModel.stream_events(version=\"v3\") and astream_events(version=\"v3\") replace the old @beta() stream_v2 / astream_v2 public methods (now removed); _ChatModelBinding preserves typed overloads across .bind() chains.
  • chunks_to_events / achunks_to_events are refactored from a single-open-block model to a parallel dict-of-open-blocks, enabling Anthropic-style interleaved tool-call chunks.
  • create_agent gains a transformers parameter and fixes schema merge ordering (set → list) to give the explicit state_schema priority over middleware annotations.

Confidence Score: 3/5

The core streaming refactor is logically sound but removes a previously public beta API without a migration path, which will break callers on upgrade.

The removal of stream_v2 / astream_v2 without any deprecation alias means any code that called those methods will receive an unhandled AttributeError after upgrading. Additionally, tool-call content-block-delta events now carry the full accumulated state rather than incremental bytes — external consumers following the "append each delta" convention will produce doubled args. The message-start field id also changes from absent-when-unset to always-present-as-empty-string, silently altering the protocol contract for consumers that check field presence.

chat_models.py and _compat_bridge.py deserve careful review: the former for the missing deprecation path, the latter for tool-call delta semantics and the id field default.

Important Files Changed

Filename Overview
libs/core/langchain_core/language_models/chat_models.py Replaces stream_v2/astream_v2 with stream_events(version="v3") / astream_events(version="v3") overloads; adds _ChatModelBinding to preserve typed returns across .bind(). The old beta methods are removed entirely with no deprecation shim, which is a breaking change for any code using the previous API.
libs/core/langchain_core/language_models/_compat_bridge.py Refactors block tracking from single-open-block to a parallel blocks dict supporting interleaved tool calls; renames protocol fields (content_block→content/delta, message_id→id); adds data-delta support. Tool-call delta events now carry cumulative state; message-start always emits id: "".
libs/core/langchain_core/runnables/base.py Removes stream_v2/astream_v2 from Runnable/RunnableBindingBase; adds stream_events (sync, v3-only on supported subclasses) and updated astream_events overloads that route v3 to a dedicated coroutine. RunnableEachBase.astream_events refactored from async generator to regular function dispatching two helper coroutines. Logic appears sound.
libs/core/langchain_core/language_models/chat_model_stream.py Updates dispatch / _push_content_block_delta to handle new delta shapes (text-delta, reasoning-delta, block-delta, data-delta, legacy-block-delta); adds _event_content_block / _event_delta helpers for backwards-compatible field reading. Changes look correct; the new legacy fallback path adds complexity but maintains compatibility.
libs/langchain_v1/langchain/agents/factory.py Fixes schema merge ordering (set→list) so caller's state_schema wins field conflicts; adds transformers param to create_agent that appends after the built-in ToolCallTransformer. Clean change.
libs/standard-tests/langchain_tests/utils/stream_lifecycle.py Validator updated from single-open-block (open_idx) to parallel open_indices set; handles both new content/delta fields and legacy content_block field names. Logic appears correct for the new interleaved protocol.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant BaseChatModel
    participant _chat_model_stream_v3
    participant chunks_to_events
    participant ChatModelStream

    Caller->>BaseChatModel: "stream_events(input, version="v3")"
    BaseChatModel->>_chat_model_stream_v3: _chat_model_stream_v3(input, config)
    _chat_model_stream_v3->>ChatModelStream: ChatModelStream()
    _chat_model_stream_v3->>_chat_model_stream_v3: on_chat_model_start → set_message_id(run_id)
    _chat_model_stream_v3-->>Caller: return ChatModelStream (lazy)

    Note over Caller,ChatModelStream: Caller iterates stream.text / stream.output

    Caller->>ChatModelStream: pump_one()
    ChatModelStream->>chunks_to_events: _stream() chunks
    chunks_to_events->>chunks_to_events: build blocks dict (parallel tracking)
    chunks_to_events-->>ChatModelStream: message-start event
    ChatModelStream->>ChatModelStream: _push_message_start
    chunks_to_events-->>ChatModelStream: content-block-start event
    chunks_to_events-->>ChatModelStream: content-block-delta event
    ChatModelStream->>ChatModelStream: _push_content_block_delta
    chunks_to_events-->>ChatModelStream: content-block-finish event
    chunks_to_events-->>ChatModelStream: message-finish event
    ChatModelStream->>ChatModelStream: _finish → assemble AIMessage
    ChatModelStream-->>Caller: stream.output (AIMessage)
Loading

Reviews (1): Last reviewed commit: "chore(langchain): drop `prerelease = "al..." | Re-trigger Greptile

@@ -969,54 +971,30 @@ async def astream(
LLMResult(generations=[[generation]]),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Breaking removal of stream_v2 / astream_v2 without a deprecation shim

stream_v2() and astream_v2() were @beta() public methods on BaseChatModel. This PR removes them entirely and routes callers to the new stream_events(version="v3") / astream_events(version="v3") entry points. Any downstream code that currently calls model.stream_v2(...), model.bind_tools([...]).stream_v2(...), or tests using these names will raise AttributeError immediately after upgrading to 1.4.0, with no deprecation warning and no migration period. Beta APIs can break, but silent removal with no shim makes the upgrade harder to diagnose. A one-line alias (or at least a DeprecationWarning re-routing call) would let callers discover the replacement without an unhandled AttributeError.

Comment on lines 610 to 621
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
yield ContentBlockDeltaData(
event="content-block-delta",
index=open_wire_idx,
content_block=_to_protocol_block(block),
index=wire_idx,
delta=_to_content_delta(delta_source or block),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tool-call delta events carry cumulative state, not incremental chunks

For tool_call_chunk and server_tool_call_chunk blocks, delta_source = current (the fully accumulated block state), so every content-block-delta emits fields.args equal to the total concatenated args up to that point rather than just the new bytes from the incoming block. A consumer that appends each delta's args field to build the final tool call will produce doubled content. _push_content_block_delta's block-delta handler calls _merge_block_delta_into_store which overwrites fields on each delta, so it converges to the correct state, but external SDK consumers following the naive "concatenate each delta" pattern will produce incorrect args. Additionally, delta_source or block is always equivalent to delta_source because delta_source is always assigned a non-None dict (current or block) — the or block fallback is dead code.

Comment on lines +500 to +503
start_data = MessageStartData(event="message-start", role="ai", id="")
resolved_id = message_id if message_id is not None else getattr(msg, "id", None)
if resolved_id:
start_data["message_id"] = resolved_id
start_data["id"] = resolved_id
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 message-start always emits id: "" when no message ID is available

Initialising MessageStartData with id="" means the message-start event always contains the id key, even when no provider message ID was resolved. Previously the field was absent. Consumers that distinguish "has an ID" from "no ID" via "id" in event (rather than if event.get("id")) will now always see the key, treating the empty string as a valid ID. The _push_message_start guard (if message_id:) correctly ignores the empty string internally, but external consumers of the raw event may not apply the same guard. Omitting the field when no ID is available would match the previous protocol contract.

Suggested change
start_data = MessageStartData(event="message-start", role="ai", id="")
resolved_id = message_id if message_id is not None else getattr(msg, "id", None)
if resolved_id:
start_data["message_id"] = resolved_id
start_data["id"] = resolved_id
start_data = MessageStartData(event="message-start", role="ai")
resolved_id = message_id if message_id is not None else getattr(msg, "id", None)
if resolved_id:
start_data["id"] = resolved_id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants