Bumps [anyio](https://github.com/agronholm/anyio) from 4.12.0 to 4.14.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.14.2</h2>
<ul>
<li>Changed <code>ByteReceiveStream.receive()</code> implementations to
raise a <code>ValueError</code> when <code>max_bytes</code> is not a
positive integer (<a
href="https://redirect.github.com/agronholm/anyio/pull/1191">#1191</a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting
<code>float("inf")</code> when the limiter was instantiated
outside of an event loop. The adapter setter checked for infinity by
identity (<code>value is math.inf</code>), so only the exact
<code>math.inf</code> singleton was accepted, while every backend setter
(using <code>math.isinf()</code>) accepts any positive infinity (<a
href="https://redirect.github.com/agronholm/anyio/pull/1189">#1189</a>;
PR by <a
href="https://github.com/greymoth-jp"><code>@greymoth-jp</code></a>).</li>
<li>Fixed <code>to_process.run_sync()</code> deadlocking when the worker
function writes enough data to <code>sys.stderr</code> to fill the
(undrained) pipe buffer. The worker process now redirects
<code>sys.stderr</code> to <code>os.devnull</code> as well, matching the
documented behavior</li>
<li>Fixed <code>TLSStream.wrap()</code> matching an internationalized
(unicode) host name against the peer certificate using IDNA 2003 (via
the standard library) instead of IDNA 2008, which could cause the host
name to be matched against the wrong certificate (<a
href="https://redirect.github.com/agronholm/anyio/pull/1208">#1208</a>)</li>
<li>Fixed <code>anyio.open_process()</code> (and
<code>run_process()</code>) ignoring the <code>extra_groups</code>
argument, as it mistakenly passed the value of the <code>group</code>
argument instead (<a
href="https://redirect.github.com/agronholm/anyio/pull/1209">#1209</a>)</li>
<li>Fixed <code>CapacityLimiter.acquire_nowait()</code> and
<code>CapacityLimiter.acquire_nowait_on_behalf_of()</code> raising
<code>trio.WouldBlock</code> instead of <code>anyio.WouldBlock</code> on
the <code>trio</code> backend when there are no tokens available (<a
href="https://redirect.github.com/agronholm/anyio/pull/1218">#1218</a>)</li>
<li>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens (<code>borrowed_tokens</code> exceeding
<code>total_tokens</code> and <code>available_tokens</code> going
negative) when a non-blocking acquire was made in the window between a
token being released and the notified waiter resuming. The freed token
is now reserved for the woken waiter right away, so the non-blocking
acquire correctly raises <code>WouldBlock</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1170">#1170</a>;
PR by <a
href="https://github.com/gaoflow"><code>@gaoflow</code></a>)</li>
<li>Fixed unnecessary CPU spin when delivering cancellation from
<code>CancelScope</code> on asyncio under certain conditions, including
improper cancel scope nesting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1111">#1111</a>)</li>
</ul>
<h2>4.14.1</h2>
<ul>
<li>Fixed teardown of higher-scoped async fixtures failing on asyncio
with <code>RuntimeError: Attempted to exit cancel scope in a different
task than it was entered in</code> when an async test raise an outcome
exception (e.g., <code>pytest.skip()</code>,
<code>pytest.xfail()</code>, or <code>pytest.fail()</code>) (<a
href="https://redirect.github.com/agronholm/anyio/issues/1179">#1179</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@EmmanuelNiyonshuti</code></a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting a value of
<code>0</code> when the limiter was instantiated outside of an event
loop, contradicting the documented behavior of allowing 0 total tokens
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1183">#1183</a>;
PR by <a
href="https://github.com/nyxst4ck"><code>@nyxst4ck</code></a>)</li>
</ul>
<h2>4.14.0</h2>
<ul>
<li>
<p>Added support for Python 3.15</p>
</li>
<li>
<p>Added an asynchronous implementation of the <code>itertools</code>
module (<a
href="https://redirect.github.com/agronholm/anyio/issues/998">#998</a>;
PR by <a href="https://github.com/11kkw"><code>@11kkw</code></a>)</p>
</li>
<li>
<p>Added the <code>local_port</code> parameter to
<code>connect_tcp()</code> to allow binding to a specific local port
before connecting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1067">#1067</a>;
PR by <a
href="https://github.com/nullwiz"><code>@nullwiz</code></a>)</p>
</li>
<li>
<p>Added support for custom capacity limiters in async path and file I/O
functions and classes</p>
</li>
<li>
<p>Added the <code>create_task()</code> task group method for easier
asyncio migration (returns a <code>TaskHandle</code>) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1098">#1098</a>)</p>
</li>
<li>
<p>Changed <code>TaskGroup.start_soon()</code> to return a
<code>TaskHandle</code></p>
</li>
<li>
<p>Added an option for <code>TaskGroup.start()</code> to return a
<code>TaskHandle</code> (which then contains the start value in the
<code>start_value</code> property)</p>
</li>
<li>
<p>Added the <code>cancel()</code> convenience method to
<code>TaskGroup</code> as a shortcut for cancelling the task group's
cancel scope</p>
</li>
<li>
<p>Improved the error message when a known backend is not installed to
suggest the install command (<a
href="https://redirect.github.com/agronholm/anyio/pull/1115">#1115</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Improved <code>anyio.Path</code> to preserve subclass types by
returning <code>Self</code> in methods that return path objects (<a
href="https://redirect.github.com/agronholm/anyio/issues/1130">#1130</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Changed the parameter type annotation in
<code>anyio.Path.write_bytes()</code> to accept any
<code>ReadableBuffer</code>, thus allowing it to accept
<code>bytearray</code> and <code>memoryview</code> to match
<code>pathlib.Path.write_bytes()</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1135">#1135</a>;
PR by <a href="https://github.com/SAY-5"><code>@SAY-5</code></a>)</p>
</li>
<li>
<p>Changed several type annotations to only accept callables returning
coroutine-like objects instead of arbitrary awaitables:</p>
<ul>
<li><code>TaskGroup.start_soon()</code></li>
<li><code>TaskGroup.start()</code></li>
<li><code>anyio.from_thread.run()</code></li>
</ul>
<p>This reverts an earlier change from v3.7.0 which was made in error.
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1153">#1153</a>)</p>
</li>
<li>
<p>Changed <code>anyio.run</code> to support callables returning
arbitrary awaitables at runtime on all backends. Previously, this only
worked on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/pull/1171">#1171</a>;
PR by <a
href="https://github.com/gschaffner"><code>@gschaffner</code></a>)</p>
</li>
<li>
<p>Changed several classes (and their subclasses) to have
<code>__slots__</code> (with <code>__weakref__</code>):</p>
<ul>
<li><code>anyio.CancelScope</code></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/c384f99687c64c59ed8a11c3a0f11a2d57daff71"><code>c384f99</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/dbba29d1ade7936f18fb71ba24aa92978673482a"><code>dbba29d</code></a>
Fixed 100% CPU spin on cancel scope misuse (<a
href="https://redirect.github.com/agronholm/anyio/issues/1217">#1217</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bbc6c33caabc13af5bc4256f745027cf8d5d7b8"><code>6bbc6c3</code></a>
Fix CapacityLimiter over-granting tokens on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1172">#1172</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6f82b2537cbbe98f3df3f295499056ab7de0b15b"><code>6f82b25</code></a>
Refactored TestTLSStream.test_receive_invalid_max_bytes() to be less
flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/be24b0414f67f604bcbdd5ea3bcc56ab920d872e"><code>be24b04</code></a>
Relaxed timeouts to fix test flakiness</li>
<li><a
href="https://github.com/agronholm/anyio/commit/81135065749b4f60c06619b9caaf0a11871c1ddf"><code>8113506</code></a>
Fix test flakiness caused by slow callback duration logging</li>
<li><a
href="https://github.com/agronholm/anyio/commit/1e988b617b69588e33fecb75e36a9837245f562f"><code>1e988b6</code></a>
Fixed CapacityLimiter raising trio.WouldBlock instead of
anyio.WouldBlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1">#1</a>...</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44713f345cd29dd4e7d76553c134543a1296cc62"><code>44713f3</code></a>
Pin setup-uv to a commit sha across downstream jobs (<a
href="https://redirect.github.com/agronholm/anyio/issues/1213">#1213</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f1b7301c8264b0d2e8d24a5788fd29e93dea4040"><code>f1b7301</code></a>
Fixed stderr writes in a worker subprocess causing a deadlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1207">#1207</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/212be93c2cf2c841e753e95e5e2c543ee7feca90"><code>212be93</code></a>
Fix flaky test_tcp_listener_same_port using a hardcoded port (<a
href="https://redirect.github.com/agronholm/anyio/issues/1206">#1206</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.12.0...4.14.2">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langchain/network/alerts).
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
With custom configs, the actual model name and provider the gateway
actually runs are different from what are requested. This reconciles
clientside tracing.
## Problem
`StructuredTool` cannot be dumped to JSON:
```python
@tool
def write_file(file_path: str, content: str) -> str:
"""Write content to the given path."""
return "ok"
write_file.model_dump(mode="json")
# PydanticSerializationError: Unable to serialize unknown type:
# <class 'pydantic._internal._model_construction.ModelMetaclass'>
```
`args_schema` holds a Pydantic model class, and `func` / `coroutine`
hold callables. None of them have a JSON form. Python-mode
`model_dump()` works; only the JSON modes raise.
This also costs tracing performance. The [LangSmith
SDK](https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/_internal/_serde.py)
catches the error, dumps again in Python mode, and then sends every
class and function left in the result through its own `default` hook.
## Change
A `PlainSerializer(..., when_used="json-unless-none")` on the three
fields.
- `args_schema` dumps as its own JSON schema: `model_json_schema()` for
a Pydantic v2 class, `schema()` for a v1 one. A dict schema passes
through unchanged. A schema holding an arbitrary type has no JSON schema
at all, so it falls back to its repr instead of raising.
- `func` and `coroutine` dump as strings.
- Python-mode dumps are unchanged — still the live schema class and
callables.
- `exclude` / `include` / `exclude_none` keep working.
- Attached with `Annotated`, not `@field_serializer`. A field has only
one serializer slot, so `@field_serializer` would break any subclass
that declares its own serializer for the same field.
- Schema generation is cached per class. Pydantic does not memoize it,
and tracing would pay for it on every run.
## Result
The dump is JSON-native, and the schema it carries has the same shape a
dict `args_schema` already has, so it validates back into a working
tool.
Measured on the 8 filesystem tools of a deepagents agent, dumped through
the LangSmith serializer:
| | master | this PR |
|---|---|---|
| time per dump | 0.119 ms | **0.025 ms** |
| payload | 7,706 B | 12,213 B |
| objects reaching the SDK's `default` hook | 32 | 8 |
The payload grows because `args_schema` now carries the real schema
instead of `"<class ...>"`. Only the tool itself still enters the
`default` hook; the class and functions inside it no longer do. Without
the per-class cache the same dump takes 1.10 ms, so the cache is what
makes this a win rather than a regression.
## Why not on `BaseTool`
`args_schema` is declared there as well, so `Tool` and custom subclasses
hit the same error. But an `Annotated` serializer only applies where the
field is declared, and `StructuredTool` redeclares `args_schema` — it
would not inherit one from `BaseTool`.
## Tests
In `libs/core/tests/unit_tests/test_tools.py`: JSON round trip, Python
mode unchanged, dump options respected, dict `args_schema` preserved,
Pydantic v1 schema class, arbitrary-type fallback, and a subclass
declaring its own serializers for the same fields.
Closes#39568
Related: #33999
> [!WARNING]
> This PR expands the surface area for arbitrary code execution during
tool setup. Detecting injected arguments now requires calling
`typing.get_type_hints`, which *evaluates* string annotations (e.g.
those created by `from __future__ import annotations` or quoted forward
references) as Python expressions. As with existing type-hint resolution
paths, wrapped tool callables must be trusted application code — never
point `StructuredTool` at a callable whose module or annotations come
from an untrusted source.
Tools with a custom `args_schema` could drop injected arguments such as
`ToolRuntime` when the wrapped function's module uses postponed
annotations. The injected value was removed during input validation, so
an otherwise valid tool call failed at invocation.
---
`StructuredTool` now resolves annotations with
`typing.get_type_hints(..., include_extras=True)` before identifying
injected parameters. If an unrelated forward reference prevents
resolving the complete signature, each string annotation is resolved
independently so resolvable injected arguments are still preserved.
Callable wrappers resolve annotations from the source of their effective
signature, honoring `__wrapped__` and `__signature__`, while other
callable objects use their `__call__` method. `functools.partial`
callables retain their effective signature so already-bound injected
arguments remain excluded.
<details>
<summary><b>Before/after:</b> injected arg dropped under <code>from
__future__ import annotations</code></summary>
With postponed annotations, every annotation is stored as a plain
string. Previously `_injected_args_keys` read the raw `signature()`
annotations, so `runtime` was never recognized as injected and was
stripped during `args_schema` validation:
```python
from __future__ import annotations # all annotations become strings
from pydantic import BaseModel
from langchain_core.tools import tool, ToolRuntime
class InputSchema(BaseModel):
query: str
@tool(args_schema=InputSchema)
def my_tool(query: str, runtime: ToolRuntime) -> str:
"""Echo the query."""
return query
```
| | Behavior |
|---|---|
| **Before** | `runtime` not detected as injected → removed during
validation → tool call fails at invocation |
| **After** | `runtime` detected via `get_type_hints` → survives
validation and is injected at invocation; hidden from the model-facing
schema |
</details>
<details>
<summary><b>Before/after:</b> one unresolvable annotation disabling
injection for the whole signature</summary>
`get_type_hints` resolves *all* annotations at once and raises on the
first failure. A single unresolvable forward reference — even on an
unrelated parameter — previously meant *no* hints were available, so the
resolvable injected arg was dropped too:
```python
@tool(args_schema=InputSchema)
def my_tool(
query: "SomeTypeThatDoesNotExist", # unresolvable forward reference
runtime: "ToolRuntime", # resolvable injected arg
) -> str:
"""Echo the query."""
return query
```
| | Behavior |
|---|---|
| **Before** | `get_type_hints` raises on `query` → all hints discarded
→ `runtime` not detected as injected |
| **After** | each annotation is retried independently → `query` falls
back to its raw string (not injected), `runtime` still resolves and is
injected |
</details>
<details>
<summary><b>Before/after:</b> callable objects and wrappers</summary>
For non-function callables, the annotations now come from the source of
the *effective* signature: `__call__` for callable objects, and the
wrapped function for wrappers (`__wrapped__` / `__signature__`):
```python
class MyCallableTool:
def __call__(self, query: str, runtime: ToolRuntime) -> str:
return query
tool = StructuredTool.from_function(
func=MyCallableTool(),
name="my_tool",
description="Echo the query.",
args_schema=InputSchema,
)
```
| | Behavior |
|---|---|
| **Before** | annotations read from the wrong callable (or left as
unresolved strings) → `runtime` dropped |
| **After** | annotations resolved from `__call__` / the unwrapped
function → `runtime` injected correctly |
</details>
<details>
<summary><b>Unchanged:</b> <code>functools.partial</code> with an
already-bound injected arg</summary>
A `partial` that already binds an injected argument keeps its effective
signature — the bound parameter is absent, so nothing is re-injected
over it:
```python
from functools import partial
def fn(x: int, runtime: ToolRuntime, y: int) -> int:
return x + y
tool = StructuredTool.from_function(
func=partial(fn, 1, bound_runtime),
name="fn",
description="Add two numbers.",
args_schema=InputSchema,
)
```
**Before & after:** `runtime` is already bound by the `partial` →
excluded from the signature → the bound value is used as-is
</details>
Co-authored-by: Soban Shankar
<165470467+Soban-2004@users.noreply.github.com>
fixes#39099
We currently allow forward refs in pydantic v2 schemas upon creation:
```python
class Container(BaseModel):
rows: list["Row"] = [] # "Row" is declared below, after the tool is decorated
@tool
def my_tool(container: Container):
"""A tool whose schema depends on a forward reference that is not resolvable yet."""
return "ok"
class Row(BaseModel):
name: str
```
When it comes time to introspect the tool schema (notably in
`count_tokens_approximately` and `convert_to_openai_tool`), we rely on
[signature
introspection](https://github.com/langchain-ai/langchain/blob/943dd700ef7c33e3f1f21d3e280c9c249b88259c/libs/core/langchain_core/tools/base.py#L1654-L1661)
to extract the tool's input schema. If that contains invalid forward
references, there's no schema fields to extract which results in an
empty dict:
<details>
<summary>Invalid forward reference MRE</summary>
```python
from __future__ import annotations
import inspect
from pydantic import BaseModel, Field
from pydantic.errors import PydanticUndefinedAnnotation
from langchain_core.tools.base import get_all_basemodel_annotations
from langchain_core.utils.pydantic import _create_subset_model, model_json_schema
class Container(BaseModel):
"""A model with a nested forward reference that can never resolve."""
rows: list["UndefinedRow"] = Field(default_factory=list)
def main() -> None:
"""Print the field-selection inputs and their zero-field subset result."""
selected_annotations = get_all_basemodel_annotations(Container)
subset_schema = _create_subset_model(
"ContainerSubset",
Container,
list(selected_annotations),
fn_description=Container.__doc__,
)
print(f"Pydantic complete: {Container.__pydantic_complete__}")
print(f"Pydantic fields: {list(Container.model_fields)}")
print(f"inspect.signature: {inspect.signature(Container)}")
print(f"Fields selected by get_all_basemodel_annotations: {selected_annotations}")
print(f"Subset properties: {model_json_schema(subset_schema)['properties']}")
if __name__ == "__main__":
main()
```
```output
Pydantic complete: False
Pydantic fields: ['rows']
inspect.signature: (**data: 'Any') -> 'None'
Fields selected by get_all_basemodel_annotations: {}
Subset properties: {}
```
</details>
<details>
<summary>Valid forward reference MRE</summary>
```python
from __future__ import annotations
import inspect
from pydantic import BaseModel, Field
from pydantic.errors import PydanticUndefinedAnnotation
from langchain_core.tools.base import get_all_basemodel_annotations
from langchain_core.utils.pydantic import _create_subset_model, model_json_schema
class Container(BaseModel):
"""A model with a nested forward reference that can never resolve."""
rows: list["UndefinedRow"] = Field(default_factory=list)
class UndefinedRow(BaseModel):
name: str = Field()
def main() -> None:
"""Print the field-selection inputs and their zero-field subset result."""
Container.model_rebuild()
selected_annotations = get_all_basemodel_annotations(Container)
subset_schema = _create_subset_model(
"ContainerSubset",
Container,
list(selected_annotations),
fn_description=Container.__doc__,
)
print(f"Pydantic complete: {Container.__pydantic_complete__}")
print(f"Pydantic fields: {list(Container.model_fields)}")
print(f"inspect.signature: {inspect.signature(Container)}")
print(f"Fields selected by get_all_basemodel_annotations: {selected_annotations}")
print(f"Subset properties: {model_json_schema(subset_schema)['properties']}")
if __name__ == "__main__":
main()
```
```output
Pydantic complete: True
Pydantic fields: ['rows']
inspect.signature: (*, rows: list[__main__.UndefinedRow] = <factory>) -> None
Fields selected by get_all_basemodel_annotations: {'rows': list[__main__.UndefinedRow]}
Subset properties: {'rows': {'items': {'$ref': '#/$defs/UndefinedRow'}, 'title': 'Rows', 'type': 'array'}}
```
</details>
---
The fix is to
* at introspection time, resolve forward references using
`.model_rebuild()` that raises a pydantic exception if forward
references cant be resolved
* i'm also widening a pydantic utility to use a type guard instead of
having to use bool + cast
I'm intentionally not rebuilding pydantic v1 schemas in the same way
since
* forward references are specified by explicitly passing names into
`update_forward_refs`
* pydantic v1 is old news
Runnable snapshots currently embed the exact `langchain-core` version,
forcing unrelated snapshot rewrites during every release. Normalize only
the current `VERSION` to a stable placeholder before comparison, so
missing or stale version metadata still fails.
Made by [Open
SWE](https://openswe.vercel.app/agents/bfd72574-359e-544e-dbf5-78f8bae3636a)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This PR sends gateway metadata information (if present in the client
response) as metadata for the llm invocation.
Requires changes corresponding changes in the ChatModel implementations
(e.g., ChatOpenAI) so gateway metadata is picked up from the gateway
response headers.
---------
Signed-off-by: Eugene Yurtsev <eugene@langchain.dev>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eyurtsev <3205522+eyurtsev@users.noreply.github.com>
Closes#38064, #35259, #38850
`merge_dicts`, `merge_lists`, and `AddableDict` all guessed at merge
semantics for streaming chunks in ways that silently corrupted data
instead of failing loudly:
- `merge_dicts` fell into the `int` branch for differing `bool` values
(since `bool` subclasses `int`) and summed them, turning `True + False`
into `1`. It now raises `TypeError`, consistent with other unmergeable
types.
- `merge_lists` used `"index" in e_left` on untyped list elements; when
an element was a plain `str` containing the literal substring `"index"`,
it then subscripted the string and raised an unrelated `TypeError`. It
now checks `isinstance(e_left, dict)` first.
- `AddableDict.__add__`/`__radd__` silently discarded the left-hand
value on any `TypeError` from `chunk[key] + other[key]`. They now
re-raise a `TypeError` naming the key and both types.
### Release note
`merge_dicts` now raises `TypeError` for differing boolean values at the
same key instead of silently summing them to an `int`; `AddableDict`
addition now raises `TypeError` on type-incompatible keys instead of
silently dropping data; `merge_lists` no longer misidentifies non-dict
elements as index-keyed.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>