mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-12 11:21:37 +00:00
cc/tool_error
240 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
27160cd1fe | support async on_error | ||
|
|
ebc0107633 | x | ||
|
|
fca0a977a6 | fix(langchain): propagate interrupts through ToolRetryMiddleware (#38722) | ||
|
|
a586e9044c |
fix(langchain): avoid shared process-group kill in shell middleware (#36359)
Fixes #36358 --- **Summary** This PR fixes a process termination safety bug in ShellToolMiddleware where cleanup could kill the caller process group when create_process_group is False. **Why this change** When the shell child is started without a dedicated process group, it can share the parent group. The existing cleanup path used group kill unconditionally, which could terminate unrelated processes including the caller. This is a high-impact availability risk. **What changed** Updated shell session kill logic to use group kill only when the child is in a different process group than the caller. Added safe fallback to child-only kill when both share the same process group. Added regression tests for both scenarios: Shared process group: no group kill, child-only kill. Dedicated process group: existing group kill behavior is preserved. **Validation** Targeted new tests passed. Full shell tool unit test file passed. AI assistance disclosure This contribution was prepared with assistance from an AI coding agent. I reviewed, validated, and finalized the proposed changes and test coverage before submission. **Areas for careful review** Process-group detection behavior on Linux and other POSIX environments. Any implications for existing timeout and shutdown flows in shell middleware. Whether additional integration-level coverage is desirable for process cleanup behavior. --------- Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
4cdd47b253 |
fix(langchain): sanitize anthropic cache markers on fallback retries (#37867)
## Summary Fixes #33709. When `AnthropicPromptCachingMiddleware` runs before `ModelFallbackMiddleware`, Anthropic-specific `cache_control` markers can leak into fallback attempts targeting non-Anthropic models. This change makes fallback retries sanitize those markers only on non-primary attempts, preserving primary-call behavior and avoiding API changes. Related: langchain-ai/deepagentsjs#551. ## Changes ### libs/langchain_v1 - Added a private fallback sanitizer in `model_fallback.py` to remove Anthropic `cache_control` markers from fallback requests. - Sanitization covers `model_settings`, system/content message blocks, and tool payloads (`BaseTool.extras` plus dict-style tools/extras). - Applied sanitization in both sync and async fallback retry paths (`wrap_model_call` and `awrap_model_call`) while leaving the primary attempt unchanged. ### libs/langchain_v1 tests - Added sync and async regression tests in `test_model_fallback.py` that simulate primary failure and assert fallback calls only succeed when cache markers are stripped. - Verified non-cache settings (for example `temperature` and `top_p`) are preserved on fallback. - Preserved existing fallback behavior coverage (ordering, exhaustion, and error propagation). ## Compatibility with `BedrockPromptCachingMiddleware` The sanitizer also covers `BedrockPromptCachingMiddleware`, which injects `cache_control` into `model_settings` only. Bedrock-specific markers (`cachePoint` blocks, content-block `cache_control`) are applied by the chat model classes at API-call time and never appear in the `ModelRequest`, so no additional handling is needed. --------- Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
f912638334 |
style: fix some ruff preview rules in langchain_v1 and standard-tests (#38657)
|
||
|
|
64177b6fc5 |
chore(langchain): add types in agent middleware tests (#38188)
Co-authored-by: Mason Daugherty <github@mdrxy.com> Co-authored-by: Mason Daugherty <mason@langchain.dev> |
||
|
|
9ef324c9ab |
fix(langchain,openai): only set strict=True on tools for OpenAI-compatible models in ProviderStrategy (#38370)
When using `ProviderStrategy`, `create_agent` unnecessarily sets `strict=True` on tools for all providers. This is only needed for OpenAI / chat completions. Here we unset `strict`. For OpenAI: 1. We set it in `BaseChatOpenAI.bind_tools` (as a convenience to users calling `model.bind_tools` directly) 2. We (redundantly) special-case OpenAI in the `create_agent` factory logic so that things will not break for users who upgrade `langchain` but not `langchain-openai`. Note: payloads for OpenAI are tested here and appear unchanged: https://github.com/langchain-ai/langchain/blob/master/libs/langchain_v1/tests/unit_tests/agents/test_response_format_integration.py Quick test: ```python from langchain.agents import create_agent from langchain.agents.structured_output import ProviderStrategy from pydantic import BaseModel class Weather(BaseModel): temperature: float condition: str def weather_tool(location: str) -> str: """Get the weather at a location.""" return "Sunny and 75 degrees F." for model in [ "anthropic:claude-sonnet-4-6", "openai:gpt-5.4", "google_genai:gemini-3.5-flash", ]: agent = create_agent( model=model, tools=[weather_tool], response_format=ProviderStrategy(Weather), ) result = agent.invoke({ "messages": [{"role": "user", "content": "What's the weather in SF?"}] }) print(result["structured_response"]) ``` |
||
|
|
15c38c8555 |
fix(langchain): switch summary format (#38171)
Part of https://github.com/langchain-ai/deepagents/issues/2873 --- `SummarizationMiddleware` now serializes the history passed to the summarizer with XML formatting so URL-backed multimodal content remains available in the prompt. The existing behavior avoided dumping raw message metadata into the token budget, but the prefix serialization path omitted image/audio/video URL blocks before the summary model saw them. ## Changes - Update `SummarizationMiddleware._create_summary` and `SummarizationMiddleware._acreate_summary` to call `get_buffer_string(..., format="xml")` for trimmed conversation history - Preserve URL-backed multimodal blocks in the summary prompt while still avoiding raw message metadata expansion - Add sync and async unit coverage with a prompt-capturing chat model to assert image URLs survive summarization input serialization --------- Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
ae1c9418b5 |
fix(langchain): detect provider strategy for dated gpt-5.2/gpt-5.4 snapshots (#38222)
Closes #38220 --- Users calling `create_agent(..., response_format=<schema>)` with an OpenAI model pinned to a dated snapshot (e.g. `gpt-5.4-2026-03-05`) were silently downgraded from native structured output (`ProviderStrategy`) to tool-calling (`ToolStrategy`). This changes runtime behavior: extra tool-call traces, different token usage, and no provider-side schema enforcement. The cause is in `_supports_provider_strategy`'s fallback patterns: the `gpt-5.2` and `gpt-5.4` base patterns terminated with `($|[/:])`, which — unlike their sibling families — rejected a trailing `-`, so OpenAI's `-YYYY-MM-DD` dated-snapshot suffix matched none of the patterns. The base patterns were deliberately strict to keep `gpt-5.2-pro`/`gpt-5.4-pro` blocked, so rather than allowing any trailing `-` (which would re-admit those `-pro` variants) this change adds an optional dated-snapshot group `(-\d{4}-\d{2}-\d{2})?`. Dated snapshots now resolve to `ProviderStrategy` while `-pro` variants stay blocked. Made by [Open SWE](https://openswe.vercel.app/agents/c5ebcb29-8ce5-dda0-73f6-198e49f0c36c) Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> |
||
|
|
5d044cd326 |
chore(langchain): improve typing in tests (#38163)
Co-authored-by: Mason Daugherty <github@mdrxy.com> Co-authored-by: Mason Daugherty <mason@langchain.dev> |
||
|
|
dcaf7795a3 |
fix(langchain,anthropic): confine file-search results and tighten anthropic allowed_prefixes (#38106)
|
||
|
|
5d20596d73 |
style(core,langchain,langchain-classic,partners): replace double backticks in docstrings (#38095)
Standardizes inline code markup in Python docstrings and comments by replacing Sphinx-style double backticks with single-backtick Markdown. The cleanup keeps existing code fences intact while aligning inline references with the repo's docstring convention. ## Changes - Converted inline code references in core prompt-loading docs and LangSmith tracer comments, including `..`, `allow_dangerous_paths`, and inheritable metadata keys. - Normalized agent-related docstrings and comments around `wrap_model_call`, `ExtendedModelResponse`, `Command`, `create_structured_chat_agent`, and `DockerExecutionPolicy`. - Updated partner package docstrings for inline references such as `json_schema`, `ToolCall`, `apply_patch_call_output`, OpenRouter content block keys, and Perplexity tool-call serialization. - Cleaned test and helper docstrings that referenced command separators, fake `resource` modules, stream event names, and xdist rate-limit environment variables. |
||
|
|
d5f7d33f88 |
chore(langchain): add overloads to create_agent (#34309)
This way mypy can infer the return type when `ResponseT` is not passed. --------- Co-authored-by: Mason Daugherty <61371264+mdrxy@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
6b9e22dbbc |
fix(langchain): tighten structured output model fallbacks (#38042)
Provider-native structured output fallback detection now uses bounded model-name patterns instead of broad substring checks, reducing false positives for unrelated model IDs. The model examples and test fixtures across OpenAI/OpenRouter-facing code were refreshed around current OpenAI model families while preserving shipped defaults. ## Changes - Tightened `FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT` from loose string fragments to regex patterns, with `_supports_provider_strategy` matching full model-name segments instead of arbitrary substrings. - Expanded structured-output fallback coverage for newer OpenAI, Anthropic, and xAI/Grok model families, including `gpt-5.x`, newer Claude 4/5-style names, and `grok-build`. - Reused `_attempt_infer_model_provider` in provider tool search routing so `_provider_from_model_name` follows the same provider inference behavior as `init_chat_model`. - Suppressed irrelevant provider-inference deprecation warnings during provider tool search registry lookup. - Refreshed OpenAI, Azure OpenAI, OpenRouter, core metadata, and example model references from older fixtures like `gpt-4`, `gpt-4o`, `o1`, and `o4-mini` to current test/profile models such as `gpt-5.5`, `gpt-5-nano`, and `gpt-4.1-mini`. - Removed outdated OpenAI test assumptions around legacy `o1` behavior and narrowed legacy structured-output checks to explicitly legacy model names. |
||
|
|
3eee4002d9 |
refactor(langchain): refactor test_create_agent_tool_validation (#34443)
Simplify test for `create_agent` errors. * Remove duplicate tests * Test sync and async with common logic --------- Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
7ffe092657 |
style(langchain): add ruff rules ARG (#34435)
In this order: * used `@override` when overriding a parent method. * prefixed param with `_` when the param could be renamed. * used `*_args, **_kwargs` when it was not possible to rename (eg: protocol) * used `_ = some_variable` when the variable name is inspected (in tools) --------- Co-authored-by: Mason Daugherty <github@mdrxy.com> Co-authored-by: Mason Daugherty <mason@langchain.dev> |
||
|
|
92ee772761 |
feat(langchain): add ProviderToolSearchMiddleware (#37969)
[Docs](https://github.com/langchain-ai/docs/pull/4355) Adds `ProviderToolSearchMiddleware` to let agents defer selected tools behind OpenAI/Anthropic provider-native tool search while preserving existing `extras={"defer_loading": True}` behavior. The middleware validates searchable tool names, injects the provider search tool only when a tool is deferred, and rejects unsupported providers up front. Made by [Open SWE](https://openswe.vercel.app) --------- Co-authored-by: Alexander Olsen <13665641+aolsenjazz@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Mason Daugherty <github@mdrxy.com> Co-authored-by: Mason Daugherty <mason@langchain.dev> |
||
|
|
77bbf8ba39 |
test(langchain): mark legacy trigger view for 2.0 removal (#38002)
`SummarizationMiddleware._trigger_conditions` is now explicitly marked as a temporary compatibility view for private consumers. The regression test is tied to the package major version so the 2.0 release path fails loudly until the legacy attr and test are removed. |
||
|
|
8c5b36c851 |
fix(langchain): preserve summarization trigger compatibility (#38000)
`SummarizationMiddleware` now uses `_trigger_clauses` as the canonical
internal representation for AND/OR trigger evaluation while keeping
`_trigger_conditions` as a tuple-shaped compatibility view. This keeps
the new dict-style `TriggerClause` behavior intact without breaking
private consumers that still inspect the old tuple-normalized trigger
state.
## Changes
- Added `_trigger_clauses` as the source of truth for summarization
trigger evaluation, profile requirement checks, and compound AND clause
handling.
- Restored `_trigger_conditions` as a legacy compatibility projection
for tuple-expressible triggers, so tuple and single-key dict triggers
remain visible in the previous private shape.
- Avoided misrepresenting compound `TriggerClause` inputs like
`{"tokens": 1000, "messages": 5}` as independent OR-style tuple
conditions.
|
||
|
|
05fe08201c |
feat(langchain): port AND-capable trigger conditions to SummarizationMiddleware (#34576)
Closes #34442 [Docs](https://github.com/langchain-ai/docs/pull/4377) --- Add parity with LangChain.js trigger semantics for Python `SummarizationMiddleware`. `trigger` can now express AND conditions within a single dict-style `TriggerClause` while preserving the existing tuple and list-of-tuples behavior. A simple user story: a support agent is helping debug an issue over a long conversation. One tool call may return a large log snippet, briefly pushing the token count over a limit, but the conversation is still only a few messages long and the recent context is valuable. Separately, the user may send many short follow-up messages that increase message count without using much context. With `trigger={"tokens": 4000, "messages": 10}`, both thresholds must be met at the same time: at least 4,000 tokens and at least 10 messages. This means 5,000 tokens across only 3 messages does not summarize, and 20 short messages totaling only 1,000 tokens does not summarize either. Summarization waits until the conversation is large enough by both measures, making it less likely to discard useful recent context too early. ## Changes - Add `TriggerClause` support so `trigger={"tokens": 4000, "messages": 10}` only summarizes when all configured thresholds are met - Export `TriggerClause` from `langchain.agents.middleware` so users can import and annotate dict-style trigger clauses from the public middleware entrypoint - Normalize tuple and mapping trigger inputs through `_normalize_trigger`, preserving existing `ContextSize` tuple semantics as single-condition clauses - Defensively copy mutable trigger list and dict inputs during initialization so caller-side mutations do not change the middleware's stored public configuration after construction - Keep list inputs as OR semantics across clauses, including mixed lists like `[{"tokens": 4000, "messages": 10}, ("messages", 50)]` - Update `_should_summarize` to evaluate AND within each clause and OR across clauses for `tokens`, `messages`, and `fraction` - Update the docs and API link map so `TriggerClause` resolves in the Python middleware docs - Preserve tuple-trigger compatibility while allowing message-based `keep` configurations to summarize at least one message when a trigger fires near the cutoff boundary AI assistance was used to help draft and refine this contribution. --------- Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
17d1c274c4 |
fix(langchain): improve HITL rejection guidance (#37859)
Improves the default `reject` `ToolMessage` so models see that the human denied the action, the tool was not executed, and the same call should not be retried unless the user asks. Also documents that clients can provide a custom `reject` message for domain-specific guidance. [Docs](https://github.com/langchain-ai/docs/pull/4269) _Opened collaboratively by Mason Daugherty and open-swe._ --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Mason Daugherty <61371264+mdrxy@users.noreply.github.com> Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
dfca7f4424 | feat(langchain): project subagent runs onto typed run.subagents channel (#37739) | ||
|
|
36be77b0f1 |
feat(langchain): add interrupt_mode and when predicate to HumanInTheLoopMiddleware (#37579)
Adds an optional `when` predicate to `InterruptOnConfig`, allowing
dynamic per-tool-call control over whether a HITL interrupt fires.
---
**`when` predicate in `InterruptOnConfig`**
```python
class InterruptOnConfig(TypedDict):
allowed_decisions: list[DecisionType]
description: NotRequired[str | _DescriptionFactory]
args_schema: NotRequired[dict[str, Any]]
when: NotRequired[Callable[[ToolCallRequest], bool]] # new
```
When provided, `when` is called before adding a tool call to the batch
interrupt. If it returns `False`, the call is auto-approved and
excluded. If it returns `True` (or `when` is absent), existing behaviour
is unchanged.
The predicate receives a `ToolCallRequest` with:
- `tool_call` — the raw tool call dict (name, args, id)
- `tool` — `None` (no `BaseTool` instance is available at the
`after_model` stage)
- `state` — current agent state
- `runtime` — a `ToolRuntime` constructed from the node-level `Runtime`,
with `tool_call_id` populated
Example:
```python
HumanInTheLoopMiddleware(
interrupt_on={
"delete_file": InterruptOnConfig(
allowed_decisions=["approve", "reject"],
when=lambda req: req.tool_call["args"].get("path", "").startswith("/etc"),
)
}
)
```
This change is fully backwards-compatible — `when` is `NotRequired` and
existing configs without it behave identically.
> This PR was developed with AI-agent assistance.
|
||
|
|
d08245f70d |
feat(langchain): redact streamed PII in flight on PIIMiddleware (#37616)
`PIIMiddleware` previously scrubbed detected PII only at the state level via its `after_model` / `before_model` hooks. Consumers reading the live stream — `astream_events(version="v3")` or `run.messages` / `run.tool_calls` / `run.values` — saw the raw model text, the raw tool-call args, the raw tool outputs, and the raw state snapshots until the run finished and the canonical conversation history was written. This change registers a stream transformer ahead of `MessagesTransformer` that redacts every wire surface of an agent run. The transformer holds a sliding lookback buffer (default 128 characters) per `(run_id, content-block index)` so PII patterns that straddle delta boundaries are caught before the safe prefix is released downstream. Anything older than the lookback is run through the configured detector and emitted; the trailing tail stays buffered until a later delta extends it past the cap or the block finishes. `_finalize_block` always re-runs detection over the full block snapshot so the finalized content lands fully redacted even when the in-flight buffer never released a tail (short responses, or PII arriving in the final delta). The `block` strategy is now supported on the streaming path via a buffering mode that withholds every delta until the block resolves — clean blocks release the full text at finalize, PII-bearing blocks zero the wire and let `after_model` / `apply_to_tool_results` raise `PIIDetectionError` on the original state message. Activation is gated on `apply_to_output=True`, matching the existing post-hoc semantics. The middleware's transformer factory is cloned by `StreamMux._make_child` into every subgraph scope, so attaching `PIIMiddleware` at the outer agent also redacts streamed deltas from sub-agents invoked inside tools. ## Tool-call and tools-channel coverage The transformer covers every wire surface of an agent run, not just AI message text: - **Streamed AI text deltas** (`content-block-delta` of type `text-delta`) — lookback machinery, redacted in place. - **Streamed tool-call args** (`content-block-delta` with `tool_call_chunk` / `server_tool_call_chunk` fields) — each delta carries the full cumulative args string; detection runs on the field directly and redacts in place. Verified empirically against `_compat_bridge.py` and the consumer-side `_merge_block_delta_into_store` snapshot-replace semantics. - **Finalized tool-call blocks** (`content-block-finish` with `tool_call` / `server_tool_call` / `invalid_tool_call`) — `args` dict walked recursively and each string leaf redacted. - **Tool execution events on the `tools` channel** — `tool-started.input`, `tool-output-delta`, `tool-finished.output`, `tool-error.message` all run through detection. String deltas use the same lookback machinery as text-deltas keyed by `tool_call_id`; structured payloads walk recursively. - **State snapshots on the `values` channel** — message lists are walked and each message's `.content` is redacted on a fresh copy. Graph state itself stays intact for the state-level enforcer (`apply_to_tool_results` via `before_model`) to act on independently. - **Legacy `(BaseMessage, metadata)` payloads** on the `messages` channel (Python 3.10 path, where `langgraph`'s `ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11)` falls back to a code path that doesn't propagate the streaming callback into the chat model) — `.content` and `AIMessage.tool_calls[*].args` are scrubbed. For `block`, the event's `data` tuple is replaced with an empty-content copy so the original message stays in state for `after_model` to raise on. ## Worth a careful look - `_PIIStreamTransformer._mutate_text_delta` — lookback partition. Anything older than `lookback` characters is released after redaction; the tail stays buffered. Bulletproof against whitespace-permissive detectors (notably `credit_card`, whose regex matches across spaces). - `_PIIStreamTransformer._mutate_tool_call_chunk_delta` — direct in-place redaction of the cumulative args string. No buffer; the wire shape is cumulative-snapshot, the consumer-side merge is replace-not-append. - `_PIIStreamTransformer._mutate_legacy_payload` — the dual path: mutate-in-place for non-`block` (idempotent with `after_model`), replace-with-empty-copy for `block` (keeps original in graph state for `after_model` to raise on). - `_PIIStreamTransformer._redact_value` — the recursive walker. `BaseMessage` branch returns a fresh `.content`-redacted copy via `model_copy(update=...)` — never mutates in place — so tool-output payloads that wrap a `ToolMessage` and message lists in state snapshots flow through cleanly. - The new `transformers` attribute on `PIIMiddleware`: this is what makes `create_agent` pick the factory up. Multiple `PIIMiddleware` instances each register one transformer; ordering is preserved within the `before_builtins` lane. ## Compatibility Bumps `langgraph` to `>=1.2.1` for the `before_builtins` opt-in on `StreamTransformer`. |
||
|
|
1aa4496fb4 |
feat(langchain): register stream transformers on middleware (#37591)
Adds a `transformers` attribute to `AgentMiddleware` so middleware can
declare scope-aware `StreamTransformer` factories alongside their
`tools` and lifecycle hooks. `create_agent` merges middleware-registered
factories with any caller-supplied ones at compile time.
## API
```python
class MyMiddleware(AgentMiddleware):
transformers = (MyTransformer,) # factory: (scope,) -> StreamTransformer
```
When the agent compiles, the final transformer order on the run mux is:
1. Built-in ``ToolCallTransformer``
2. Middleware-registered factories, in middleware order
3. Caller-supplied ``transformers=`` from ``create_agent``
This ordering keeps the built-in tool-call projection in front of any
consumer transformers and gives caller-supplied entries the final word.
|
||
|
|
d328d84b78 |
fix(langchain): sort glob_search results by mtime (newest first) (#37462)
Closes #37369 --- The `glob_search` tool in `FilesystemFileSearchMiddleware` documents that results are "sorted by modification time (most recently modified first)", but the implementation was returning files in the arbitrary order provided by `Path.glob()`. This change adds a sort by modification timestamp (`modified_at`), in descending order, immediately before extracting the file paths for the return value. No public API changes. --------- Co-authored-by: Mason Daugherty <github@mdrxy.com> |
||
|
|
36c381b149 | fix(langchain): alias Bedrock providers in summarization token check (#37453) | ||
|
|
da380bccf8 | chore(infra): merge v1.4 into master (#37350) | ||
|
|
9c48a120b9 | revert: feat(langchain): ls_agent_type tag on create_agent calls (#37249) | ||
|
|
ba56ac6f03 |
feat(langchain): add respond decision to HITL middleware (#37095)
Extends `HumanInTheLoopMiddleware` with a new `respond` decision type
for "ask user" style tools — tools whose real implementation is the
human's response. The interrupt is raised with the tool call as usual;
the resume payload becomes the body of a synthetic `ToolMessage` with
`status="success"`, and the tool itself is not executed.
This complements `reject` (which produces a synthetic `ToolMessage` with
`status="error"`) by enabling the symmetric success path: a reviewer can
answer on the tool's behalf without invoking it.
## Changes
- New `RespondDecision` `TypedDict` with a required `message: str`
field; added to the `Decision` union.
- `"respond"` added to the `DecisionType` literal.
- `_process_decision` handles `"respond"` by emitting a `ToolMessage`
with `status="success"` and preserving the original tool call on the
`AIMessage` so provider-required tool-call/tool-message pairing is
maintained.
- The `True` shortcut in `interrupt_on` now expands to `["approve",
"edit", "reject", "respond"]`, so existing callers that opted into "all
decisions" pick up the new capability without code changes. The `reject`
decision already permits a reviewer to inject arbitrary `ToolMessage`
content, so `respond` extends the same trust model — not a new
capability class.
## Example
```python
from langchain.agents.middleware import HumanInTheLoopMiddleware
middleware = HumanInTheLoopMiddleware(
interrupt_on={"ask_user": {"allowed_decisions": ["respond"]}}
)
# Resume payload: {"decisions": [{"type": "respond", "message": "blue"}]}
# → synthetic ToolMessage(content="blue", status="success") for `ask_user`.
```
---
*Implementation drafted with AI-agent assistance.*
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ee95ad6907 |
feat(langchain): ls_agent_type tag on create_agent calls (#36774)
|
||
|
|
c87cd04927 |
release(core): release 1.3.0 (#36851)
xRelease 1.3.0 |
||
|
|
8f5c800f41 |
test(langchain): test cache hit/miss on lru (#36659)
add quick test |
||
|
|
1ca47a5411 |
perf(langchain): add another init test with middleware (#36644)
add another init test with middleware |
||
|
|
af4d711a2f |
chore(core): reduce streaming metadata / perf (#36588)
- looking into reducing streaming metadata / perfm --------- Co-authored-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> |
||
|
|
f0c5a28fa0 |
perf(langchain): add benchmark command (#36641)
add benchmark in Makefile |
||
|
|
4e55c555ad |
test(langchain): cover runtime recursion limit override in create_agent (#36376)
Extends the existing unit test for to verify that a per-invoke override is visible inside the tool runtime config. This keeps the coverage in the existing fake-model end-to-end test and exercises both the default config path and the override path in one place. Created with [Deep Agents CLI](https://docs.langchain.com/oss/python/deepagents/cli/overview) using gpt-5.4 (provider: openai). |
||
|
|
c7a677bba5 |
chore(langchain): add async implementation to todolist and test (#36313)
add async func as well |
||
|
|
2f64d80cc6 |
fix(core,model-profiles): add missing ModelProfile fields, warn on schema drift (#36129)
PR #35788 added 7 new fields to the `langchain-profiles` CLI output (`name`, `status`, `release_date`, `last_updated`, `open_weights`, `attachment`, `temperature`) but didn't update `ModelProfile` in `langchain-core`. Partner packages like `langchain-aws` that set `extra="forbid"` on their Pydantic models hit `extra_forbidden` validation errors when Pydantic encountered undeclared TypedDict keys at construction time. This adds the missing fields, makes `ModelProfile` forward-compatible, provides a base-class hook so partners can stop duplicating model-profile validator boilerplate, migrates all in-repo partners to the new hook, and adds runtime + CI-time warnings for schema drift. ## Changes ### `langchain-core` - Add `__pydantic_config__ = ConfigDict(extra="allow")` to `ModelProfile` so unknown profile keys pass Pydantic validation even on models with `extra="forbid"` — forward-compatibility for when the CLI schema evolves ahead of core - Declare the 7 missing fields on `ModelProfile`: `name`, `status`, `release_date`, `last_updated`, `open_weights` (metadata) and `attachment`, `temperature` (capabilities) - Add `_warn_unknown_profile_keys()` in `model_profile.py` — emits a `UserWarning` when a profile dict contains keys not in `ModelProfile`, suggesting a core upgrade. Wrapped in a bare `except` so introspection failures never crash model construction - Add `BaseChatModel._resolve_model_profile()` hook that returns `None` by default. Partners can override this single method instead of redefining the full `_set_model_profile` validator — the base validator calls it automatically - Add `BaseChatModel._check_profile_keys` as a separate `model_validator` that calls `_warn_unknown_profile_keys`. Uses a distinct method name so partner overrides of `_set_model_profile` don't inadvertently suppress the check ### `langchain-profiles` CLI - Add `_warn_undeclared_profile_keys()` to the CLI (`cli.py`), called after merging augmentations in `refresh()` — warns at profile-generation time (not just runtime) when emitted keys aren't declared in `ModelProfile`. Gracefully skips if `langchain-core` isn't installed - Add guard test `test_model_data_to_profile_keys_subset_of_model_profile` in model-profiles — feeds a fully-populated model dict to `_model_data_to_profile()` and asserts every emitted key exists in `ModelProfile.__annotations__`. CI fails before any release if someone adds a CLI field without updating the TypedDict ### Partner packages - Migrate all 10 in-repo partners to the `_resolve_model_profile()` hook, replacing duplicated `@model_validator` / `_set_model_profile` overrides: anthropic, deepseek, fireworks, groq, huggingface, mistralai, openai (base + azure), openrouter, perplexity, xai - Anthropic retains custom logic (context-1m beta → `max_input_tokens` override); all others reduce to a one-liner - Add `pr_lint.yml` scope for the new `model-profiles` package |
||
|
|
9e4a6013be | fix(openai): add type: message to Responses API input items (#35693) | ||
|
|
46fdade7e6 | fix(langchain): normalize custom detector output to prevent KeyError in hash/mask strategies (#35651) | ||
|
|
bed4d2686a | chore(langchain): switch refs from gemini-3 to gemini-3.1 (#35535) | ||
|
|
83f81d65af |
fix(langchain): allow Gemini 3 models to use ProviderStrategy with tools (#34464)
|
||
|
|
8f1bc0d3ae | feat(openai): support automatic server-side compaction (#35212) | ||
|
|
65c224a3da |
chore(core): raise more descriptive model error in init_chat_model (#35167)
|
||
|
|
de05838fca |
chore(deps): bump the langchain-deps group across 3 directories with 40 updates (#35129)
Bumps the langchain-deps group with 10 updates in the /libs/core directory: | Package | From | To | | --- | --- | --- | | [langsmith](https://github.com/langchain-ai/langsmith-sdk) | `0.6.0` | `0.7.1` | | [tenacity](https://github.com/jd/tenacity) | `9.1.2` | `9.1.4` | | [packaging](https://github.com/pypa/packaging) | `25.0` | `26.0` | | [uuid-utils](https://github.com/aminalaee/uuid-utils) | `0.12.0` | `0.14.0` | | [ruff](https://github.com/astral-sh/ruff) | `0.14.11` | `0.15.0` | | [types-requests](https://github.com/typeshed-internal/stub_uploader) | `2.31.0.6` | `2.32.4.20260107` | | [setuptools](https://github.com/pypa/setuptools) | `78.1.1` | `82.0.0` | | [pytest](https://github.com/pytest-dev/pytest) | `8.4.2` | `9.0.2` | | [pytest-watcher](https://github.com/olzhasar/pytest-watcher) | `0.4.3` | `0.6.3` | | [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed) | `4.2.0` | `4.3.0` | Bumps the langchain-deps group with 31 updates in the /libs/langchain directory: | Package | From | To | | --- | --- | --- | | [langsmith](https://github.com/langchain-ai/langsmith-sdk) | `0.4.31` | `0.7.1` | | [packaging](https://github.com/pypa/packaging) | `25.0` | `26.0` | | [pydantic](https://github.com/pydantic/pydantic) | `2.12.1` | `2.12.5` | | [ruff](https://github.com/astral-sh/ruff) | `0.14.11` | `0.15.0` | | [types-requests](https://github.com/typeshed-internal/stub_uploader) | `2.31.0.6` | `2.32.4.20260107` | | [setuptools](https://github.com/pypa/setuptools) | `80.9.0` | `82.0.0` | | [pytest](https://github.com/pytest-dev/pytest) | `8.4.2` | `9.0.2` | | [pytest-watcher](https://github.com/olzhasar/pytest-watcher) | `0.4.3` | `0.6.3` | | [numpy](https://github.com/numpy/numpy) | `1.26.4` | `2.2.6` | | [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) | `2.0.43` | `2.0.46` | | [langchain-anthropic](https://github.com/langchain-ai/langchain) | `0.0.1.post1` | `1.3.2` | | [langchain-google-vertexai](https://github.com/langchain-ai/langchain-google) | `2.1.2` | `3.2.2` | | [langchain-google-genai](https://github.com/langchain-ai/langchain-google) | `2.1.12` | `4.2.0` | | [langchain-fireworks](https://github.com/langchain-ai/langchain) | `1.0.0a1` | `1.1.0` | | [langchain-ollama](https://github.com/langchain-ai/langchain) | `1.0.0a1` | `1.0.1` | | [langchain-mistralai](https://github.com/langchain-ai/langchain) | `0.2.12` | `1.1.1` | | [langchain-huggingface](https://github.com/langchain-ai/langchain) | `1.0.0` | `1.2.0` | | [langchain-groq](https://github.com/langchain-ai/langchain) | `1.0.0a1` | `1.1.2` | | [langchain-aws](https://github.com/langchain-ai/langchain-aws) | `1.0.0a1` | `1.2.3` | | [langchain-deepseek](https://github.com/langchain-ai/langchain) | `1.0.0a1` | `1.0.1` | | [langchain-xai](https://github.com/langchain-ai/langchain) | `1.0.0a1` | `1.2.2` | | [langchain-perplexity](https://github.com/langchain-ai/langchain) | `1.0.0a1` | `1.1.0` | | [pytest-cov](https://github.com/pytest-dev/pytest-cov) | `4.1.0` | `7.0.0` | | [lark](https://github.com/lark-parser/lark) | `1.3.0` | `1.3.1` | | [wrapt](https://github.com/GrahamDumpleton/wrapt) | `1.17.3` | `2.1.1` | | [python-dotenv](https://github.com/theskumar/python-dotenv) | `1.1.1` | `1.2.1` | | langchainhub | `0.1.18` | `0.1.21` | | [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) | `3.6.0` | `5.0.0` | | [types-pytz](https://github.com/typeshed-internal/stub_uploader) | `2023.4.0.20240130` | `2025.2.0.20251108` | | [fastapi](https://github.com/fastapi/fastapi) | `0.128.0` | `0.128.6` | | [playwright](https://github.com/microsoft/playwright-python) | `1.55.0` | `1.58.0` | Bumps the langchain-deps group with 17 updates in the /libs/langchain_v1 directory: | Package | From | To | | --- | --- | --- | | [pydantic](https://github.com/pydantic/pydantic) | `2.12.4` | `2.12.5` | | [ruff](https://github.com/astral-sh/ruff) | `0.14.11` | `0.15.0` | | [pytest](https://github.com/pytest-dev/pytest) | `8.4.2` | `9.0.2` | | [pytest-watcher](https://github.com/olzhasar/pytest-watcher) | `0.4.3` | `0.6.3` | | [langchain-google-vertexai](https://github.com/langchain-ai/langchain-google) | `3.0.3` | `3.2.2` | | [langchain-google-genai](https://github.com/langchain-ai/langchain-google) | `3.0.3` | `4.2.0` | | [langchain-fireworks](https://github.com/langchain-ai/langchain) | `1.0.0` | `1.1.0` | | [langchain-ollama](https://github.com/langchain-ai/langchain) | `1.0.0` | `1.0.1` | | [langchain-mistralai](https://github.com/langchain-ai/langchain) | `1.0.1` | `1.1.1` | | [langchain-huggingface](https://github.com/langchain-ai/langchain) | `1.0.1` | `1.2.0` | | [langchain-groq](https://github.com/langchain-ai/langchain) | `1.0.1` | `1.1.2` | | [langchain-aws](https://github.com/langchain-ai/langchain-aws) | `1.0.0` | `1.2.3` | | [langchain-xai](https://github.com/langchain-ai/langchain) | `1.0.0` | `1.2.2` | | [langchain-perplexity](https://github.com/langchain-ai/langchain) | `1.0.0` | `1.1.0` | | [wrapt](https://github.com/GrahamDumpleton/wrapt) | `1.17.3` | `2.1.1` | | [langgraph](https://github.com/langchain-ai/langgraph) | `1.0.7` | `1.0.8` | | [langchain-azure-ai](https://github.com/langchain-ai/langchain-azure) | `1.0.3` | `1.0.4` | Updates `langsmith` from 0.6.0 to 0.7.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/langchain-ai/langsmith-sdk/releases">langsmith's releases</a>.</em></p> <blockquote> <h2>v0.7.1</h2> <h2>What's Changed</h2> <ul> <li>release(js): 0.5.0 by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2381">langchain-ai/langsmith-sdk#2381</a></li> <li>fix(ci): Convert JS release workflow to use OIDC by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2383">langchain-ai/langsmith-sdk#2383</a></li> <li>feat(js): run evaluations as examples complete by <a href="https://github.com/bees"><code>@bees</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2349">langchain-ai/langsmith-sdk#2349</a></li> <li>fix(js): Restore default concurrency behavior for JS eval runner by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2385">langchain-ai/langsmith-sdk#2385</a></li> <li>fix(js,py): Adds backcompat for old prompt cache API by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2386">langchain-ai/langsmith-sdk#2386</a></li> <li>fix(js): Add system message visibility to Anthropic wrapper traces by <a href="https://github.com/veeceey"><code>@veeceey</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2375">langchain-ai/langsmith-sdk#2375</a></li> <li>release(py): 0.7.1 by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2388">langchain-ai/langsmith-sdk#2388</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/veeceey"><code>@veeceey</code></a> made their first contribution in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2375">langchain-ai/langsmith-sdk#2375</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.0...v0.7.1">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.0...v0.7.1</a></p> <h2>v0.7.0</h2> <h2>What's Changed</h2> <ul> <li>chore(deps): bump form-data from 4.0.3 to 4.0.5 in /js by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2282">langchain-ai/langsmith-sdk#2282</a></li> <li>chore(deps-dev): bump langchain from 0.3.29 to 0.3.37 in /js by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2283">langchain-ai/langsmith-sdk#2283</a></li> <li>chore(deps): bump vite from 6.3.5 to 6.4.1 in /js by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2284">langchain-ai/langsmith-sdk#2284</a></li> <li>chore:deps by <a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2356">langchain-ai/langsmith-sdk#2356</a></li> <li>chore(deps): bump python-multipart from 0.0.21 to 0.0.22 in /python by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2351">langchain-ai/langsmith-sdk#2351</a></li> <li>chore(deps): bump protobuf from 6.33.2 to 6.33.5 in /python by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2352">langchain-ai/langsmith-sdk#2352</a></li> <li>chore(deps): bump pyasn1 from 0.6.1 to 0.6.2 in /python by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2353">langchain-ai/langsmith-sdk#2353</a></li> <li>chore(deps-dev): bump esbuild from 0.20.2 to 0.25.0 in /js/internal/environment_tests/test-exports-esbuild in the npm_and_yarn group across 1 directory by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2354">langchain-ai/langsmith-sdk#2354</a></li> <li>chore(deps): bump jws from 4.0.0 to 4.0.1 in /js by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2355">langchain-ai/langsmith-sdk#2355</a></li> <li>chore(deps): bump actions/setup-node from 3 to 6 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2357">langchain-ai/langsmith-sdk#2357</a></li> <li>chore(deps): bump actions/github-script from 6 to 8 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2358">langchain-ai/langsmith-sdk#2358</a></li> <li>chore(deps): bump actions/checkout from 2 to 6 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2359">langchain-ai/langsmith-sdk#2359</a></li> <li>chore(deps): bump astral-sh/setup-uv from 6 to 7 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2360">langchain-ai/langsmith-sdk#2360</a></li> <li>chore(deps): bump actions/cache from 4 to 5 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2361">langchain-ai/langsmith-sdk#2361</a></li> <li>chore(deps-dev): bump ty from 0.0.10 to 0.0.15 in /python in the py-minor-and-patch group by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2362">langchain-ai/langsmith-sdk#2362</a></li> <li>chore(deps): update httpx requirement from <0.28.0,>=0.23.0 to >=0.23.0,<0.29.0 in /python by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2363">langchain-ai/langsmith-sdk#2363</a></li> <li>chore(deps-dev): bump eslint-plugin-prettier from 4.2.1 to 4.2.5 in /js by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2366">langchain-ai/langsmith-sdk#2366</a></li> <li>chore(deps-dev): bump babel-jest from 29.7.0 to 30.2.0 in /js by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2365">langchain-ai/langsmith-sdk#2365</a></li> <li>chore: adding workflow perms by <a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2367">langchain-ai/langsmith-sdk#2367</a></li> <li>fix: esbuild CVE dependabot 78 by <a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2369">langchain-ai/langsmith-sdk#2369</a></li> <li>fix(deps): resolve glob CLI command injection (CVE-2025-64756) by <a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2368">langchain-ai/langsmith-sdk#2368</a></li> <li>fix(deps): resolve js-yaml prototype pollution vulnerability by <a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2370">langchain-ai/langsmith-sdk#2370</a></li> <li>chore(deps): bump the js-minor-and-patch group in /js with 20 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2364">langchain-ai/langsmith-sdk#2364</a></li> <li>chore: rm unused fn by <a href="https://github.com/hinthornw"><code>@hinthornw</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2346">langchain-ai/langsmith-sdk#2346</a></li> <li>feat(py,js): Use global singleton for prompt cache, enable by default by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2371">langchain-ai/langsmith-sdk#2371</a></li> <li>chore: Support jwt auth in replicas by <a href="https://github.com/hinthornw"><code>@hinthornw</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2293">langchain-ai/langsmith-sdk#2293</a></li> <li>release(py): 0.7.0 by <a href="https://github.com/jacoblee93"><code>@jacoblee93</code></a> in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2382">langchain-ai/langsmith-sdk#2382</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> made their first contribution in <a href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2356">langchain-ai/langsmith-sdk#2356</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.6.9...v0.7.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.6.9...v0.7.0</a></p> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
0040e1a8aa | fix(langchain): fix token counting on partial message sequences (#35101) | ||
|
|
ce5f73e07c |
refactor(langchain): rename _SUPPORTED_PROVIDERS -> _BUILTIN_PROVIDERS (#35100)
|
||
|
|
8767a462ca |
feat: support state updates from wrap_model_call with command(s) (#35033)
Alternative to https://github.com/langchain-ai/langchain/pull/35024. Paving the way for summarization in `wrap_model_call` (which requires state updates). --- Add `ExtendedModelResponse` dataclass that allows `wrap_model_call` middleware to return a `Command` alongside the model response for additional state updates. ```py @dataclass class ExtendedModelResponse(Generic[ResponseT]): model_response: ModelResponse[ResponseT] command: Command ``` ## Motivation Previously, `wrap_model_call` middleware could only return a `ModelResponse` or `AIMessage` — there was no way to inject additional state updates (e.g. custom state fields) from the model call middleware layer. `ExtendedModelResponse` fills this gap by accepting an optional `Command`. This feature is needed by the summarization middleware, which needs to track summarization trigger points calculated during `wrap_model_call`. ## Why `Command` instead of a plain `state_update` dict? We chose `Command` rather than the raw `state_update: dict` approach from the earlier iteration because `Command` is the established LangGraph primitive for state updates from nodes. Using `Command` means: - State updates flow through the graph's reducers (e.g. `add_messages`) rather than being merged as raw dicts. This makes messages updates additive alongside the model response instead of replacing them. - Consistency with `wrap_tool_call`, which already returns `Command`. - Future-proof: as `Command` gains new capabilities (e.g. `goto`, `send`), middleware can leverage them without API changes. ## Why keep `model_response` separate instead of using `Command` directly? The model node needs to distinguish the model's actual response (messages + structured output) from supplementary middleware state updates. If middleware returned only a `Command`, there would be no clean way to extract the `ModelResponse` for structured output handling, response validation, and the core model-to-tools routing logic. Keeping `model_response` explicit preserves a clear boundary between "what the model said" and "what middleware wants to update." Also, in order to avoid breaking, the `handler` passed to `wrap_tool_call` needs to always return a `ModelResponse`. There's no easy way to preserve this if we pump it into a `Command`. One nice thing about having this `ExtendedModelResponse` structure is that it's extensible if we want to add more metadata in the future. ## Composition When multiple middleware layers return `ExtendedModelResponse`, their commands compose naturally: - **Inner commands propagate outward:** At composition boundaries, `ExtendedModelResponse` is unwrapped to its underlying `ModelResponse` so outer middleware always sees a plain `ModelResponse` from `handler()`. The inner command is captured and accumulated. - **Commands are applied through reducers:** Each `Command` becomes a separate state update applied through the graph's reducers. For messages, this means they're additive (via `add_messages`), not replacing. - **Outer wins on conflicts:** For non-reducer state fields, commands are applied inner-first then outer, so the outermost middleware's value takes precedence on conflicting keys. - **Retry-safe:** When outer middleware retries by calling `handler()` again, accumulated inner commands are cleared and re-collected from the fresh call. ```python class Outer(AgentMiddleware): def wrap_model_call(self, request, handler): response = handler(request) # sees ModelResponse, not ExtendedModelResponse return ExtendedModelResponse( model_response=response, command=Command(update={"outer_key": "val"}), ) class Inner(AgentMiddleware): def wrap_model_call(self, request, handler): response = handler(request) return ExtendedModelResponse( model_response=response, command=Command(update={"inner_key": "val"}), ) # Final state merges both commands: {"inner_key": "val", "outer_key": "val"} ``` ## Backwards compatibility Fully backwards compatible. The `ModelCallResult` type alias is widened from `ModelResponse | AIMessage` to `ModelResponse | AIMessage | ExtendedModelResponse`, but existing middleware returning `ModelResponse` or `AIMessage` continues to work identically. ## Internals - `model_node` / `amodel_node` now return `list[Command]` instead of `dict[str, Any]` - `_build_commands` converts the model response + accumulated middleware commands into a list of `Command` objects for LangGraph - `_ComposedExtendedModelResponse` is the internal type that accumulates commands across layers during composition |