Compare commits

..
Author SHA1 Message Date
Nick Hollon 7505c73243 Revert "Terminate sync stream iterators on stream-end, not on root-terminal lifecycle"
This reverts commit bbc71c398b.
2026-06-02 10:03:39 -04:00
Nick Hollon bbc71c398b Terminate sync stream iterators on stream-end, not on root-terminal lifecycle
The sync stream controller pushed the terminal None sentinel into every subscription queue the instant it saw a root-namespace completed/failed lifecycle event. Any event the server emits after that (e.g. a direct-child subgraph lifecycle/tasks event that feeds thread.subgraphs) was then queued behind the None and never consumed, because iterators return on the first None. This surfaced as thread.subgraphs yielding no direct-child handles for a factory/deep-agent graph on the sync client while the async client worked.

Align the sync controller with the async controller: terminate projection iterators when the shared stream ends (or on interrupt, signaled by the thread-stream), not on a mid-stream root-terminal event. Adds a regression test.
2026-06-01 22:42:33 -04:00
Nick HollonandGitHub 83dd61feac release(langgraph): 1.2.3 (#7945) 2026-06-01 14:51:31 -04:00
Nick HollonandGitHub 13f2ecc84b release(sdk-py): 0.4.2 (#7955) 2026-06-01 13:49:09 -04:00
Nick HollonandGitHub af5dab5b77 fix(sdk-py): percent-encode thread_id in v3 stream transport default paths (#7954)
## Problem

Fixes #7953.

The v3 SSE and WebSocket stream transports build their default paths by
interpolating `thread_id` directly into the URL:

```python
self._commands_url = commands_path or f"/threads/{thread_id}/commands"
self._stream_url   = stream_path   or f"/threads/{thread_id}/stream/events"
```

This skips the `_quote_path_param` escaping that the rest of the SDK
adopted in #7893. A `thread_id` containing reserved characters or
dot-segments is then normalized by the HTTP/WebSocket stack before
transmission. For example, `thread_id = "../assistants/abc"`:

| | before |
|---|---|
| constructed | `/threads/../assistants/abc/commands` |
| wire path | `/assistants/abc/commands` |

So the value stops being one opaque identifier under
`/threads/{thread_id}/...` and silently hits a different resource.

## Fix

Reuse the existing `_quote_path_param` helper for the **default** paths
in all four transports (`http`, `sync_http`, `ws`, `sync_ws`). Explicit
`commands_path` / `stream_path` overrides are left untouched, so callers
that pass their own paths opt out of encoding as before.

`_quote_path_param("../assistants/abc")` → `..%2Fassistants%2Fabc`,
which the HTTP/WS stack no longer collapses.

## Tests

Adds `tests/streaming/test_transport_path_encoding.py` covering all four
transports:
- default `_commands_url` / `_stream_url` / `_stream_path` are
percent-encoded,
- the actual SSE wire path (async + sync) stays under `/threads/`,
- the built WebSocket URL (async + sync) stays under `/threads/`,
- explicit path overrides are left untouched.

`make format`, `make lint`, and `make test` all pass in `libs/sdk-py`
(491 passed).
2026-06-01 13:20:32 -04:00
Nick HollonandGitHub 312c6d0ac1 feat(langgraph): wire RemoteGraph.interleave to sdk-py interleave_projections (#7938) 2026-06-01 12:42:42 -04:00
Nick HollonandGitHub f1dc4577e2 release(sdk-py): 0.4.1 (#7944) 2026-06-01 11:19:53 -04:00
Nick HollonandGitHub 1fcb768182 feat(sdk-py): extract stream decoders and add interleave_projections (#7935)
## Summary

Refactors the four (now five) sdk-py streaming projections into
reusable, transport-agnostic `Decoder` classes and adds a new
`interleave_projections(channels)` method to `AsyncThreadStream` and
`SyncThreadStream` that drives multiple decoders from one shared
subscription, yielding `(channel_name, item)` tuples in arrival order
(the SDK analog of local `GraphRunStream.interleave`).

- **New `langgraph_sdk/stream/decoders.py`**: pure `feed(event) ->
Iterable[item]` state machines — `ValuesDecoder`, `MessagesDecoder`,
`ToolCallsDecoder`, `SubgraphsDecoder`, `ExtensionsDecoder` — behind a
`Decoder` Protocol. No subscription/queue/thread access.
- **Projection migration (async + sync)**: the five existing projections
now delegate their per-event logic to the decoders. Behavior-preserving
— the existing test suite is the regression net. Thread-coupled side
effects (active-stream registration, terminal-error-on-close, root-inbox
forwarding) stay in the projection wrappers; sync messages/tool_calls
keep their pre-dispatch contract (handle/stream resolved on yield) via a
FIFO-head buffer.
- **`interleave_projections`**: flat-namespace channel list (built-ins +
extension names), `tool_calls`↔`tools` wire mapping, subgraphs fed every
event, extensions keyed by bare name.

### Notable
- Fixes a latent sync bug surfaced by the refactor: two tool calls /
messages whose events interleave previously dropped the second; both now
surface. Locked in with a regression test.
- `Decoder.feed` takes `Mapping[str, Any]` (read-only), so the Protocol
is load-bearing in both stream files.

### Deferred (not in this PR)
- Wiring `RemoteGraph._RemoteGraphRunStream.interleave` to
`interleave_projections` (gated on #7927).
- Migrating the handle-scoped projections (`_Handle*Projection`) to the
decoders — hence the small, verified-identical helper duplication
between `decoders.py` and `_async/stream.py`.
- `interleave_projections` handles aren't registered for thread-close
cleanup (additive follow-up).

## Test Plan
- [x] `make test` in `libs/sdk-py` — 464 passed, 0 failures
- [x] `make format` / `make lint` (ruff + ty) clean
- [x] Per-decoder unit tests in `tests/streaming/test_decoders.py`
- [x] `interleave_projections` tests (single-channel, multi-channel
arrival order, builtin+extension mix, tool_calls public-name, subgraphs
discovery) async + sync
- [x] Existing projection suites pass unchanged (regression net for the
migration)
2026-05-29 17:14:14 -04:00
Nick HollonandGitHub 68fa011fc9 feat(langgraph): add v3 streaming support to RemoteGraph (#7927)
## Summary

- Adds `stream_events(version="v3")` and `astream_events(version="v3")`
to `RemoteGraph`, matching the local `CompiledStateGraph` surface and
unblocking polymorphic v3 streaming over `Graph | RemoteGraph`.
- Implementation is a thin adapter
(`libs/langgraph/langgraph/pregel/_remote_run_stream.py`) that wraps the
v3 SDK's `AsyncThreadStream` / `SyncThreadStream` and duck-types
`GraphRunStream` / `AsyncGraphRunStream`. No coupling to local v3 mux
internals.
- `v1` / `v2` paths unchanged. `astream_events(version='v1'|'v2')` still
raises `NotImplementedError` (separate gap).

### Scope decisions baked into this PR

- Unsupported v3 kwargs hard-reject at dispatch with
`NotImplementedError`: `control`, `transformers`, `interrupt_before`,
`interrupt_after`, and any unknown `**kwargs`. Server / SDK don't plumb
these through v3 yet; easy to lift later.
- Sync `interleave()` raises `NotImplementedError` pointing callers at
`astream_events`. Real sync interleave would need drainer threads;
deferred since most sync RemoteGraph callers just iterate raw events.
- Async `interleave()` is best-effort ordering (client receive order),
documented as a divergence from local v3's monotonic stamp ordering.
- Adapter `interrupted` / `interrupts` properties are **non-blocking**
snapshots of the SDK's current state. This differs from local
`(Async)GraphRunStream.interrupted`, which pump-drives the run to
terminal before returning. Callers needing a wait-for-interrupt pattern
should drain a projection (e.g., `interleave('values')`) until the SDK's
paused sentinel fires. Documented in the adapter docstrings.

### Audit of impact

Existing RemoteGraph callers in this org all use the v2 `.stream()` /
`.astream()` path (deepagents production wrapper, langgraph-api test
graphs, langgraph-supervisor TS type guard). **Zero callers** use
`stream_events` / `astream_events` on RemoteGraph today, so the new v3
methods are net-new surface — no risk of breaking existing consumers.

### Out of scope (follow-ups)

- Bumping `libs/langgraph/pyproject.toml`'s `langgraph-sdk` constraint
from `<0.4.0` to `<0.5.0`. Deferred until 0.4.0 publishes to PyPI; dev
resolution unaffected via the editable workspace dep.
- Real `astream_events(version='v1'|'v2')` implementation.
- Server-side plumbing for `control` / `interrupt_before` /
`interrupt_after` on v3 runs.
- Sync `interleave()` via drainer threads.


## Test plan

- [x] \`make test\` in \`libs/langgraph/\`: 1874 passed, 4 skipped (43
new in \`test_remote_graph_v3.py\`)
- [x] \`make lint\` in \`libs/langgraph/\`: ruff + mypy clean
- [x] \`pytest -m integration
tests/integration/test_remote_graph_v3.py\` in \`libs/sdk-py/\` against
the docker stack: 4/4 passed in 1.35s
- [x] Manual smoke: \`RemoteGraph('tools_agent',
url='http://localhost:2024').astream_events(..., version='v3')\`
end-to-end against the v3 integration api
- [x] Existing RemoteGraph v2 tests untouched (31 passed, 3 skipped with
docker up)
- [x] Will need rebase after \`langgraph-sdk 0.4.0\` lands on PyPI and
the version constraint is bumped in a separate PR
2026-05-29 17:13:43 -04:00
Nick HollonandGitHub ac3f5b007b feat(langgraph): name tool-dispatched subagents via lc_agent_name (#7928) 2026-05-29 16:46:45 -04:00
syachamaneni-lcandGitHub a9b0a05fb5 chore(langgraph): Track ADK/other library usage when deploying using cli (#7939)
<!-- Replace everything above this line with a 1-2 sentence description
of your change. Keep the "Fixes #xx" keyword and update the issue
number. -->
- Add a property to revisions table metadata column for Google ADK
version
- This helps us track the deployments that use Google ADK
- Similar to this PR:
https://github.com/langchain-ai/langchainplus/pull/22087

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

> **All contributions must be in English.** See the [language
policy](https://docs.langchain.com/oss/python/contributing/overview#language-policy).

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

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

1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION

    - feat(langgraph): add multi-tenant support
- Allowed TYPE and SCOPE values:
https://github.com/langchain-ai/langgraph/blob/main/.github/workflows/pr_lint.yml#L19-L43

2. 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.

3. 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.

4. 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)
<!-- If you'd like a shoutout on release, add your socials below -->
Twitter: @
LinkedIn: https://linkedin.com/in/
2026-05-29 13:46:10 -07:00
Nick HollonandGitHub b7fd3cf9c4 fix(langgraph): rename ProtocolEvent.eventId to event_id to match the wire field (#7942) 2026-05-29 14:46:59 -04:00
Nick HollonandGitHub 64bd4d1f13 fix(langgraph): merge instead of overwrite in ensure_config for callbacks, tags, metadata, configurable (#7926) 2026-05-29 09:40:22 -04:00
syachamaneni-lcandGitHub f25a0f4f4c fix(langgraph): [LSD-1507] Distinguish between user cancelled and other cancellations (#7920)
- Distinguish between Node cancellations and other cancellations
- Use python 3.11+ feature where `cancelling() == 0` when it is the node
cancelling
- Bubble up the node cancellation example so the client can take care of
it instead of silently failing without reporting it.

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

> **All contributions must be in English.** See the [language
policy](https://docs.langchain.com/oss/python/contributing/overview#language-policy).

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

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

1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION

    - feat(langgraph): add multi-tenant support
- Allowed TYPE and SCOPE values:
https://github.com/langchain-ai/langgraph/blob/main/.github/workflows/pr_lint.yml#L19-L43

2. 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.

3. 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.

4. How did you verify your code works?
- Existing unit tests pass
- New unit tests added
- Used langgraph deployment to make sure the feature is working as
expected.

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)
<!-- If you'd like a shoutout on release, add your socials below -->
Twitter: @
LinkedIn: https://linkedin.com/in/
2026-05-28 14:34:18 -07:00
Nick HollonandGitHub ea4aa79a60 fix(sdk-py): make tools_agent fake model stateless (#7930) 2026-05-28 16:42:45 -04:00
Nick HollonandGitHub c7792608e3 release(sdk-py): 0.4.0 (#7923)
## Summary

Bumps `langgraph-sdk` `0.3.15` → `0.4.0`.

Minor bump to reflect the v3 streaming public API that landed
since 0.3.15:

- `client.threads.stream(...)` — new thread-centric streaming entry
point (async + sync)
- SSE and WebSocket transports (`ProtocolSseTransport`,
`ProtocolWebSocketTransport`) with reconnect handling and stream
selection
- Shared stream subscriptions, lifecycle / interrupts state, output /
values projections, messages / tool-call projections, scoped subgraph
handles, thread stream helpers
2026-05-28 10:08:47 -04:00
Josh RogersandGitHub 7282301720 release(cli): 0.4.27 (#7925)
Bumping the CLI to 0.4.27.
2026-05-28 10:03:26 -04:00
6e4a295ba5 fix(cli): pin internal_docker deploy images by digest (#7924)
## Summary

`langgraph deploy` now pins images by digest when handing the URI to the
LangGraph host backend. After `docker push`, the CLI reads the manifest
digest from the local Docker daemon's `RepoDigests` and sends
`registry/repo@sha256:<hex>` to the host backend instead of the
tag-based reference. Mutable tags cause downstream inconsistency — the
same revision can refer to different images over time.

The image is still pushed under the user-supplied `--tag` (default
`:latest`) so it stays discoverable by tag in the registry — only the
URI persisted with the revision changes.

## Behavior on failure

If the digest can't be resolved (empty `RepoDigests`, or no entry
matching the just-pushed repo), the CLI warns and falls back to the
tag-based reference. Deploys never fail on a digest-resolution issue.

## Test plan

- [x] `make test` passes (new `TestResolvePushedImageDigest` cases
included)
- [x] `--verbose` deploy shows the `docker image inspect` call resolving
  the digest
- [x] Deploy with a clean Docker daemon (no matching `RepoDigests`)
  emits the fallback warning and still completes successfully

---------

Co-authored-by: Josh Rogers <josh@langchain.dev>
2026-05-27 18:26:39 -04:00
Josh RogersandGitHub b4018e8222 fix(cli): bump api bound to 0.10.0 (#7922)
Allows the CLI to support langgraph-api versions 0.9.x
2026-05-27 18:01:21 -04:00
Nick HollonandGitHub 8cb0f3a96d feat(sdk-py): add thread stream helpers (#7833) 2026-05-27 17:02:59 -04:00
Nick HollonandGitHub fd4257300e feat(sdk-py): wire websocket stream selection (#7832) 2026-05-27 16:48:22 -04:00
Nick HollonandGitHub d482fca105 feat(sdk-py): add websocket stream transports (#7830) 2026-05-27 16:01:01 -04:00
Nick HollonandGitHub 4f3ab2f969 feat(sdk-py): harden streaming reconnects (#7829) 2026-05-27 15:24:25 -04:00
Nick HollonandGitHub 3282ac10e3 feat(sdk-py): add sync scoped subgraphs (#7828) 2026-05-27 14:23:56 -04:00
Nick HollonandGitHub 3d61d1b32f feat(sdk-py): add sync messages and tool calls (#7827) 2026-05-27 14:04:34 -04:00
Nick HollonandGitHub fe1c683fe1 feat(sdk-py): add sync thread stream core (#7826) 2026-05-27 13:41:55 -04:00
Nick HollonandGitHub 10b701cf41 feat(sdk-py): add async stream reconnect support (#7825) 2026-05-27 13:30:01 -04:00
Nick HollonandGitHub bb9cfe7a22 feat(sdk-py): add scoped subgraph handles (#7824) 2026-05-27 13:18:52 -04:00
Nick HollonandGitHub 30fea64687 feat(sdk-py): add messages and tool call projections (#7823) 2026-05-27 12:11:06 -04:00
Nick HollonandGitHub 66ec594540 feat(sdk-py): add output, values, and controller extraction (#7822) 2026-05-27 11:27:11 -04:00
Nick HollonandGitHub 221deee774 feat(sdk-py): wire lifecycle state and output prerequisites (#7821) 2026-05-27 11:06:37 -04:00
Nick HollonandGitHub d03310abbb feat(sdk-py): add shared stream subscriptions (#7820) 2026-05-27 10:53:26 -04:00
Nick HollonandGitHub 22259558cc feat(sdk-py): add async thread stream skeleton (#7819) 2026-05-27 10:41:50 -04:00
Nick HollonandGitHub 3268a54791 feat(sdk-py): add v3 streaming primitives and SSE transport (#7818) 2026-05-27 10:10:09 -04:00
Sydney RunkleandGitHub add269632b chore(langgraph): bump version to 1.2.2 (#7914)
Patch bump to 1.2.2 following the 1.2.1 release.
2026-05-26 13:59:41 -04:00
Sydney RunkleandGitHub 5d5a64120e fix(langgraph): assign stable IDs to id=None BaseMessages before DeltaChannel checkpoint writes (#7913)
## TL;DR

Applications depend on stable message IDs — LangSmith traces, message
views, and `RemoveMessage` all break when the same message gets a
different ID on every load.

ID assignment has historically lived in the `add_messages` reducer, but
that's the wrong place: reducer logic runs *after* checkpoint
serialisation has already started. The correct long-term fix is to
assign IDs in `BaseMessage.__init__`, but that requires changes to
`langchain-core` equality semantics first.

**This PR is the non-breaking interim fix:** assign IDs to `BaseMessage`
objects in `put_writes()`, before the DeltaChannel write is handed to
the background serialiser. This unblocks applications now without
touching `BaseMessage`.

---

## Problem

In the default durability mode, `put_writes()` submits checkpoint writes
to a background thread before `apply_writes()` runs. For DeltaChannel
writes containing `BaseMessage` objects with `id=None`, the background
thread may serialise `id=None` before the reducer ever sees them. Every
`get_state()` call then replays `id=None` and assigns a fresh UUID — the
same message has a different ID on every load and across every resumed
invocation.

## Fix

Add `ensure_message_ids(value)` to `pregel/_messages.py` and call it in
`put_writes()` for DeltaChannel writes **before** submitting to the
background executor. This assigns UUIDs synchronously so the serialised
bytes always carry a stable ID.

Both `SyncPregelLoop` and `AsyncPregelLoop` delegate to
`super().put_writes()`, so both paths are covered with one change.

## Long-term

ID assignment belongs in `BaseMessage.__init__`. That requires first
excluding `id` from `BaseMessage.__eq__` (currently `HumanMessage("x")
!= HumanMessage("x")` if both get different auto-assigned UUIDs). That
is a `langchain-core` change tracked separately.

## Changes

- `pregel/_messages.py` — `ensure_message_ids(value)`: assigns a UUID to
any `id=None` `BaseMessage`, handles single messages and lists
- `pregel/_loop.py` — call it for DeltaChannel writes in `put_writes()`
- `tests/test_delta_channel_id_stability.py` — two tests (sync + async)
using a plain append reducer (no ID logic), confirmed to **fail on
`main`** and pass with this fix

Related deepagents PR:
https://github.com/langchain-ai/deepagents/pull/3590
2026-05-26 13:55:14 -04:00
Nick HollonandGitHub d1e2ff0561 release(checkpoint): 4.1.1 (#7890)
## Summary

Bumps `langgraph-checkpoint` `4.1.0` → `4.1.1` and updates all
downstream `uv.lock` files.

## Test plan

- [x] `make format` / `make lint` / `make test` from `libs/checkpoint/`
2026-05-22 12:52:05 -04:00
Nick HollonandGitHub e787af200e release(sdk-py): 0.3.15 (#7891)
## Summary

Bumps `langgraph-sdk` `0.3.14` → `0.3.15`.

## Test plan

- [x] `make format` / `make lint` / `make test` from `libs/sdk-py/`
2026-05-22 12:51:52 -04:00
Nick HollonandGitHub 604534e1b7 fix(sdk-py): percent-encode caller-supplied identifiers in URL paths (#7893)
## Summary

Wraps every caller-supplied identifier (thread_id, assistant_id, run_id,
cron_id, checkpoint_id, namespace) interpolated into request URL paths
with `urllib.parse.quote(safe="")` via a new `_quote_path_param` helper.
All-dot strings are encoded as `%2E` triplets so they aren't collapsed
by httpx's client-side path normalization. Helper raises `TypeError` for
None/bytes inputs.

## Test plan

- [x] `make format` / `make lint` / `make test` from `libs/sdk-py`
2026-05-22 12:51:37 -04:00
Nick HollonandGitHub 346aa97425 fix(checkpoint): restrict lc:2 envelope revival to default constructor (#7892)
## Summary

Restricts lc:2 JSON envelope revival in `JsonPlusSerializer` to the
default constructor; the `method` field is now ignored. Adds a
`logger.warning` when the default constructor raises so legacy payloads
that previously fell back to `construct(**kwargs)` are observable
instead of silently degrading.

## Test plan

- [x] `make format` / `make lint` / `make test` from `libs/checkpoint`
2026-05-22 12:51:21 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
82b3872820 chore(deps): bump the uv group across 2 directories with 1 update (#7853)
Bumps the uv group with 1 update in the /libs/cli/uv-examples/monorepo
directory: [langsmith](https://github.com/langchain-ai/langsmith-sdk).
Bumps the uv group with 1 update in the /libs/cli/uv-examples/simple
directory: [langsmith](https://github.com/langchain-ai/langsmith-sdk).

Updates `langsmith` from 0.7.31 to 0.8.0
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `langsmith` from 0.7.31 to 0.8.0
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 19:15:23 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fcc4ab8dd8 chore(deps): bump idna from 3.11 to 3.15 in /libs/checkpoint (#7860)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 12:09:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
701d34494c chore(deps): bump idna from 3.11 to 3.15 in /libs/checkpoint-postgres (#7861)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 12:08:47 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>John Kennedy
2c7967ca96 chore(deps): bump idna from 3.11 to 3.15 in /libs/cli (#7865)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
2026-05-21 18:33:33 +00:00
Nick HollonandGitHub bf7fec0bd1 release(langgraph): 1.2.1 (#7883)
releasing 1.2.1

Notable changes since 1.2.0:

- feat(langgraph): add `before_builtins` opt-in for stream transformers
(#7882)
- fix(langgraph): keep tool results out of v3 messages (#7838)
- chore(deps): bump langsmith from 0.7.31 to 0.8.0 (#7788)
- chore(deps): bump idna from 3.11 to 3.15 (#7866)
2026-05-21 14:28:25 -04:00
Nick HollonandGitHub 8215a9d024 feat(langgraph): add before_builtins opt-in for stream transformers (#7882) 2026-05-21 11:54:33 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
aa322c13cd chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/checkpoint-sqlite (#7786)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.31 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=uv&previous-version=0.7.31&new-version=0.8.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 01:14:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>John Kennedy
d6319126ef chore(deps): bump idna from 3.11 to 3.15 in /libs/prebuilt (#7864)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
2026-05-20 01:06:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>John Kennedy
9b0864a83c chore(deps): bump idna from 3.11 to 3.15 in /libs/checkpoint-sqlite (#7862)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
2026-05-20 01:06:14 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e68726d639 chore(deps): bump turbo from 2.9.7 to 2.9.14 in /libs/cli/js-monorepo-example (#7868)
Bumps [turbo](https://github.com/vercel/turborepo) from 2.9.7 to 2.9.14.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/turborepo/releases">turbo's
releases</a>.</em></p>
<blockquote>
<h2>Turborepo v2.9.14</h2>
<blockquote>
<p>[!NOTE]
This release contains important security fixes.</p>
</blockquote>
<h3>High:</h3>
<ul>
<li><a
href="https://github.com/vercel/turborepo/security/advisories/GHSA-5xc8-49mv-x4mm">GHSA-5xc8-49mv-x4mm:
Turborepo VSCode Extension command injection</a></li>
</ul>
<h3>Low:</h3>
<ul>
<li><a
href="https://github.com/vercel/turborepo/security/advisories/GHSA-hcf7-66rw-9f5r">GHSA-hcf7-66rw-9f5r:
Login callback CSRF/session fixation</a></li>
<li><a
href="https://github.com/vercel/turborepo/security/advisories/GHSA-3qcw-2rhx-2726">GHSA-3qcw-2rhx-2726:
Unexpected local code execution during Yarn Berry detection</a></li>
</ul>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changelog</h3>
<ul>
<li>release(turborepo): 2.9.12 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12774">vercel/turborepo#12774</a></li>
<li>fix: Restore docs mobile menu by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12782">vercel/turborepo#12782</a></li>
<li>ci: Use <code>pull_request</code> for PR title linting by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12787">vercel/turborepo#12787</a></li>
<li>ci: Scope GitHub Actions caches by branch by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12788">vercel/turborepo#12788</a></li>
<li>test: Validate lockfiles without dependency downloads by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12789">vercel/turborepo#12789</a></li>
<li>Removed unneeded import form hash creation script in docs by <a
href="https://github.com/dancrumb"><code>@​dancrumb</code></a> in <a
href="https://redirect.github.com/vercel/turborepo/pull/12799">vercel/turborepo#12799</a></li>
<li>fix: Validate auth callback state by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12802">vercel/turborepo#12802</a></li>
<li>fix: Harden VS Code extension command execution by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12800">vercel/turborepo#12800</a></li>
<li>fix: Avoid project-local Yarn during detection by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12801">vercel/turborepo#12801</a></li>
<li>chore: Release 2.9.13 by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12803">vercel/turborepo#12803</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/dancrumb"><code>@​dancrumb</code></a>
made their first contribution in <a
href="https://redirect.github.com/vercel/turborepo/pull/12799">vercel/turborepo#12799</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/vercel/turborepo/compare/v2.9.12...v2.9.14">https://github.com/vercel/turborepo/compare/v2.9.12...v2.9.14</a></p>
<h2>Turborepo v2.9.13-canary.1</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changelog</h3>
<ul>
<li>release(turborepo): 2.9.11-canary.7 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12768">vercel/turborepo#12768</a></li>
<li>fix: Allow <code>$TURBO_EXTENDS$</code> in LSP diagnostics by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12770">vercel/turborepo#12770</a></li>
<li>release(turborepo): 2.9.11 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12771">vercel/turborepo#12771</a></li>
<li>fix: Allow transit nodes in LSP diagnostics by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12773">vercel/turborepo#12773</a></li>
<li>release(turborepo): 2.9.12 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12774">vercel/turborepo#12774</a></li>
<li>fix: Restore docs mobile menu by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12782">vercel/turborepo#12782</a></li>
<li>ci: Use <code>pull_request</code> for PR title linting by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12787">vercel/turborepo#12787</a></li>
<li>ci: Scope GitHub Actions caches by branch by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12788">vercel/turborepo#12788</a></li>
<li>test: Validate lockfiles without dependency downloads by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12789">vercel/turborepo#12789</a></li>
<li>Removed unneeded import form hash creation script in docs by <a
href="https://github.com/dancrumb"><code>@​dancrumb</code></a> in <a
href="https://redirect.github.com/vercel/turborepo/pull/12799">vercel/turborepo#12799</a></li>
<li>fix: Validate auth callback state by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12802">vercel/turborepo#12802</a></li>
<li>fix: Harden VS Code extension command execution by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12800">vercel/turborepo#12800</a></li>
<li>fix: Avoid project-local Yarn during detection by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12801">vercel/turborepo#12801</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/turborepo/commit/fc62fe0d9c347d1d24f0ed8946284856593ddb93"><code>fc62fe0</code></a>
publish 2.9.14 to registry</li>
<li><a
href="https://github.com/vercel/turborepo/commit/fb8c9aec0f9e83f95783659a5ce9c4478cf62cb9"><code>fb8c9ae</code></a>
chore: Release 2.9.13 (<a
href="https://redirect.github.com/vercel/turborepo/issues/12803">#12803</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/e8e629da4e1fb75231089e91b19be9d327a3e649"><code>e8e629d</code></a>
fix: Avoid project-local Yarn during detection (<a
href="https://redirect.github.com/vercel/turborepo/issues/12801">#12801</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/91c90cbf12f524c5c29b713d6472dd5fcdecb309"><code>91c90cb</code></a>
fix: Harden VS Code extension command execution (<a
href="https://redirect.github.com/vercel/turborepo/issues/12800">#12800</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/84f450894e87da1eed864d51f6f637f26980d560"><code>84f4508</code></a>
fix: Validate auth callback state (<a
href="https://redirect.github.com/vercel/turborepo/issues/12802">#12802</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/1779ad7901384f106236a6e196059e4929745514"><code>1779ad7</code></a>
Removed unneeded import form hash creation script in docs (<a
href="https://redirect.github.com/vercel/turborepo/issues/12799">#12799</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/71f8c90a807ffb9b9876ea8a04f523f473bf5c8d"><code>71f8c90</code></a>
test: Validate lockfiles without dependency downloads (<a
href="https://redirect.github.com/vercel/turborepo/issues/12789">#12789</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/5fcb96024d503127bb0ed760ebe159b7716c52b3"><code>5fcb960</code></a>
ci: Scope GitHub Actions caches by branch (<a
href="https://redirect.github.com/vercel/turborepo/issues/12788">#12788</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/4cf9fabc9a6f6c99fe4e2f2da9f35be631be062a"><code>4cf9fab</code></a>
ci: Use <code>pull_request</code> for PR title linting (<a
href="https://redirect.github.com/vercel/turborepo/issues/12787">#12787</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/859c629bc401f239ac7980a132746ca90478e17c"><code>859c629</code></a>
fix: Restore docs mobile menu (<a
href="https://redirect.github.com/vercel/turborepo/issues/12782">#12782</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/turborepo/compare/v2.9.7...v2.9.14">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=turbo&package-manager=npm_and_yarn&previous-version=2.9.7&new-version=2.9.14)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 01:02:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a45902061b chore(deps): bump idna from 3.11 to 3.15 in /libs/langgraph (#7866)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 17:55:13 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5d3f11ef04 chore(deps): bump idna from 3.11 to 3.15 in /libs/sdk-py (#7863)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
<h2>3.13 (2026-04-22)</h2>
<ul>
<li>Correct classification error for codepoint U+A7F1</li>
</ul>
<h2>3.12 (2026-04-21)</h2>
<ul>
<li>Update to Unicode 17.0.0.</li>
<li>Issue a deprecation warning for the transitional argument.</li>
<li>Added lazy-loading to provide some performance improvements.</li>
<li>Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.</li>
</ul>
<p>Thanks to Rodrigo Nogueira for contributions to this release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.11...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.11&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 17:55:04 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
37dc5c13c8 chore(deps): bump idna from 3.13 to 3.15 in /libs/checkpoint-conformance (#7867)
Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kjd/idna/blob/master/HISTORY.md">idna's
changelog</a>.</em></p>
<blockquote>
<h2>3.15 (2026-05-12)</h2>
<ul>
<li>Enforce DNS-length cap on individual labels early in
<code>check_label</code>,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.</li>
<li>Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared <code>_unicode_dots_re</code> from
<code>idna.core</code> in the codec module.</li>
<li>Use <code>raise ... from err</code> for proper exception chaining
and
switch internal string formatting to f-strings.</li>
<li>Allow <code>flit_core</code> 4.x in the build backend.</li>
<li>Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.</li>
<li>Add Dependabot configuration for GitHub Actions.</li>
<li>Convert README and HISTORY from reStructuredText to Markdown.</li>
<li>Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.</li>
</ul>
<p>Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.</p>
<h2>3.14 (2026-05-10)</h2>
<ul>
<li>Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [CVE-2026-45409]</li>
</ul>
<p>Thanks to Stan Ulbrych for reporting the issue.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kjd/idna/commit/af30a092e158181d0b35ac66dfa813788126bdd8"><code>af30a09</code></a>
Release 3.15</li>
<li><a
href="https://github.com/kjd/idna/commit/30314d4628744ca14cf2b5820564e5127a9f86f2"><code>30314d4</code></a>
Pre-release 3.15rc0</li>
<li><a
href="https://github.com/kjd/idna/commit/05d4b219aa9eddc47371fcbd2000f0301016f3e9"><code>05d4b21</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/237">#237</a> from
kjd/convert-docs-to-markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/2987fdba1962bbb2358399e0084ba062b98a0bee"><code>2987fdb</code></a>
Convert README and HISTORY from reStructuredText to Markdown</li>
<li><a
href="https://github.com/kjd/idna/commit/59fa8002d514bf4a5ce7b58f67b9ec587d53fa9c"><code>59fa800</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/236">#236</a> from
kjd/dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/def69834ced5d4b3c50439d8b99c4c856ec19ca2"><code>def6983</code></a>
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea</li>
<li><a
href="https://github.com/kjd/idna/commit/bbd8004a797185d8c56bb555cd5c88fde05e0631"><code>bbd8004</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/234">#234</a> from
StanFromIreland/patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/edd07c05024344a6ccb517414ccb36683aee99fc"><code>edd07c0</code></a>
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group</li>
<li><a
href="https://github.com/kjd/idna/commit/5557db030c11bdec50d62aa5f631d705d33ba123"><code>5557db0</code></a>
Merge branch 'master' into patch-1</li>
<li><a
href="https://github.com/kjd/idna/commit/f11746cf4981d25123ef7830d3ee60f07de8ae3d"><code>f11746c</code></a>
Merge pull request <a
href="https://redirect.github.com/kjd/idna/issues/235">#235</a> from
StanFromIreland/patch-2</li>
<li>Additional commits viewable in <a
href="https://github.com/kjd/idna/compare/v3.13...v3.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=uv&previous-version=3.13&new-version=3.15)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 17:54:10 -07:00
Christian BromannandGitHub ea44df3476 fix(langgraph): keep tool results out of v3 messages (#7838)
## Summary

- Filter `ToolMessage` from v3 `run.messages` streaming (handler +
`MessagesTransformer`) so tool results do not appear as chat text
deltas.
- Normalize `ToolCallStream.output` in `ToolCallTransformer` so live and
serialized `ToolMessage` payloads resolve to raw `content`.
- Add regression tests for message filtering and tool-output unwrapping.

<details>
<summary>Reproducible script</summary>

```python
"""Repro: v3 tool results must not leak through run.messages."""

from __future__ import annotations

import asyncio
from collections.abc import Callable, Sequence
from typing import Any
from uuid import uuid4

from deepagents import create_deep_agent
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.language_models import BaseChatModel, LanguageModelInput
from langchain_core.messages import AIMessage, HumanMessage, ToolCall, ToolMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.runnables import Runnable
from langchain_core.tools import BaseTool, tool
from pydantic import Field

TOOL_RESULT_SENTINEL = "[]"


class ScriptedChatModel(BaseChatModel):
    responses: list[AIMessage] = Field(default_factory=list)
    tools: Sequence[dict[str, Any] | type | Callable | BaseTool] = ()
    _idx: int = 0

    @property
    def _llm_type(self) -> str:
        return "scripted"

    def _generate(
        self,
        messages: Sequence[Any],
        stop: list[str] | None = None,
        run_manager: CallbackManagerForLLMRun | None = None,
        **kwargs: Any,
    ) -> ChatResult:
        del messages, stop, run_manager, kwargs
        idx = min(self._idx, len(self.responses) - 1)
        self._idx += 1
        return ChatResult(generations=[ChatGeneration(message=self.responses[idx])])

    def bind_tools(
        self,
        tools: Sequence[dict[str, Any] | type | Callable | BaseTool],
        *,
        tool_choice: str | None = None,
        **kwargs: Any,
    ) -> Runnable[LanguageModelInput, AIMessage]:
        del tool_choice, kwargs
        self.tools = tools
        return self


@tool
def list_items() -> str:
    """List available items."""
    return TOOL_RESULT_SENTINEL


def _tool_call_message() -> AIMessage:
    return AIMessage(
        content="",
        tool_calls=[
            ToolCall(id="call_list", name="list_items", args={}),
        ],
    )


def _extract_text_delta(event: Any) -> str | None:
    if isinstance(event, dict):
        if event.get("event") != "content-block-delta":
            return None
        delta = event.get("delta")
        if isinstance(delta, dict) and delta.get("type") == "text-delta":
            text = delta.get("text")
            return text if isinstance(text, str) else None
    elif getattr(event, "event", None) == "content-block-delta":
        delta = getattr(event, "delta", None)
        if isinstance(delta, dict) and delta.get("type") == "text-delta":
            text = delta.get("text")
            return text if isinstance(text, str) else None
        text = getattr(delta, "text", None)
        return text if isinstance(text, str) else None
    return None


async def _tool_output(tool_call: Any) -> Any:
    output = getattr(tool_call, "output", None)
    if callable(output):
        return await output()
    if hasattr(output, "__await__"):
        return await output
    return output


async def main() -> None:
    model = ScriptedChatModel(
        responses=[
            _tool_call_message(),
            AIMessage(content="No items found."),
        ]
    )
    agent = create_deep_agent(model=model, tools=[list_items])
    run = await agent.astream_events(
        {"messages": [HumanMessage(content="List items")]},
        version="v3",
        configurable={"thread_id": str(uuid4())},
        recursion_limit=50,
    )

    async def collect_message_texts() -> list[str]:
        texts: list[str] = []
        async for message_stream in run.messages:
            async for event in message_stream:
                text = _extract_text_delta(event)
                if text is not None:
                    texts.append(text)
        return texts

    async def collect_tool_outputs() -> list[Any]:
        outputs: list[Any] = []
        async for tool_call in run.tool_calls:
            outputs.append(await _tool_output(tool_call))
        return outputs

    message_texts, tool_outputs, final_state = await asyncio.gather(
        collect_message_texts(),
        collect_tool_outputs(),
        run.output(),
    )

    final_messages = final_state["messages"]
    tool_message = next((m for m in final_messages if isinstance(m, ToolMessage)), None)

    print("run.messages text deltas:", message_texts)
    print("run.tool_calls outputs:", tool_outputs)
    print("final state message roles:", [m.type for m in final_messages])

    if TOOL_RESULT_SENTINEL in message_texts:
        raise AssertionError("Tool result leaked through run.messages.")
    if TOOL_RESULT_SENTINEL not in tool_outputs:
        raise AssertionError("Tool output was not surfaced through run.tool_calls.")
    if tool_message is None or tool_message.tool_call_id != "call_list":
        raise AssertionError("Final state does not contain the expected ToolMessage.")

    print("Reproduction passed: tool output stayed out of run.messages.")


if __name__ == "__main__":
    asyncio.run(main())
```

</details>

<details>
<summary>Current behavior</summary>

```text
run.messages text deltas: ['[]', 'No items found.']
run.tool_calls outputs: [ToolMessage(content='[]', ...)]
final state message roles: ['human', 'ai', 'tool', 'ai']

AssertionError: Tool result leaked through run.messages.
```

</details>

<details>
<summary>Expected behavior</summary>

```text
run.messages text deltas: ['No items found.']
run.tool_calls outputs: ['[]']
final state message roles: ['human', 'ai', 'tool', 'ai']
Reproduction passed: tool output stayed out of run.messages.
```

</details>

## Test plan

- [ ] `uv run --project libs/langgraph pytest
libs/langgraph/tests/test_stream_messages_transformer.py`
- [ ] `uv run --project libs/prebuilt pytest
libs/prebuilt/tests/test_tool_call_transformer.py`
- [ ] `uv run --project libs/langgraph ruff check` (touched files)
- [ ] `uv run --project libs/prebuilt ruff check` (touched files)

Related:
[langchain-ai/langchainjs#10900](https://github.com/langchain-ai/langchainjs/pull/10900)
2026-05-19 11:36:16 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
49e0ac6aa8 chore(deps): bump langsmith from 0.6.3 to 0.7.1 in /libs/cli/js-monorepo-example (#7854)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.6.3 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 &lt;0.28.0,&gt;=0.23.0 to
&gt;=0.23.0,&lt;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>See full diff in <a
href="https://github.com/langchain-ai/langsmith-sdk/commits/v0.7.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=npm_and_yarn&previous-version=0.6.3&new-version=0.7.1)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:23:05 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
443bb7ba35 chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/checkpoint (#7784)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.31 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 02:21:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1c4fbb3709 chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /libs/sdk-py (#7764)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.7.0</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Security</h2>
<p>Addressed high-severity security issues. Impact was limited to
specific use cases detailed in the accompanying advisories; overall user
exposure was estimated to be marginal.</p>
<ul>
<li>
<p>Decompression-bomb safeguards of the streaming API were bypassed:</p>
<ol>
<li>When <code>HTTPResponse.drain_conn()</code> was called after the
response had been read and decompressed partially. (Reported by <a
href="https://github.com/Cycloctane"><code>@​Cycloctane</code></a>)</li>
<li>During the second <code>HTTPResponse.read(amt=N)</code> or
<code>HTTPResponse.stream(amt=N)</code> call when the response was
decompressed using the official <a
href="https://pypi.org/project/brotli/">Brotli</a> library. (Reported by
<a
href="https://github.com/kimkou2024"><code>@​kimkou2024</code></a>)</li>
</ol>
<p>See GHSA-mf9v-mfxr-j63j for details.</p>
</li>
<li>
<p>HTTP pools created using
<code>ProxyManager.connection_from_url</code> did not strip sensitive
headers specified in <code>Retry.remove_headers_on_redirect</code> when
redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by <a
href="https://github.com/christos-spearbit"><code>@​christos-spearbit</code></a>)</p>
</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Used <code>FutureWarning</code> instead of
<code>DeprecationWarning</code> for better visibility of existing
deprecation notices. Rescheduled the removal of deprecated features to
version 3.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3763">urllib3/urllib3#3763</a>)</li>
<li>Removed support for end-of-life Python 3.9. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3720">urllib3/urllib3#3720</a>)</li>
<li>Removed support for end-of-life PyPy3.10. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4979">urllib3/urllib3#4979</a>)</li>
<li>Bumped the minimum supported pyOpenSSL version to 19.0.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3777">urllib3/urllib3#3777</a>)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>Fixed a bug where <code>HTTPResponse.read(amt=None)</code> was
ignoring decompressed data buffered from previous partial reads. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3636">urllib3/urllib3#3636</a>)</li>
<li>Fixed a bug where <code>HTTPResponse.read()</code> could cache only
part of the response after a partial read when
<code>cache_content=True</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4967">urllib3/urllib3#4967</a>)</li>
<li>Fixed <code>HTTPResponse.stream()</code> and
<code>HTTPResponse.read_chunked()</code> to handle <code>amt=0</code>.
(<a
href="https://redirect.github.com/urllib3/urllib3/issues/3793">urllib3/urllib3#3793</a>)</li>
<li>Updated <code>_TYPE_BODY</code> type alias to include missing
<code>Iterable[str]</code>, matching the documented and runtime behavior
of chunked request bodies. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3798">urllib3/urllib3#3798</a>)</li>
<li>Fixed <code>LocationParseError</code> when paths resembling
schemeless URIs were passed to
<code>HTTPConnectionPool.urlopen()</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3352">urllib3/urllib3#3352</a>)</li>
<li>Fixed <code>BaseHTTPResponse.readinto()</code> type annotation to
accept <code>memoryview</code> in addition to <code>bytearray</code>,
matching the <code>io.RawIOBase.readinto</code> contract and enabling
use with <code>io.BufferedReader</code> without type errors. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3764">urllib3/urllib3#3764</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.7.0 (2026-05-07)</h1>
<h2>Security</h2>
<p>Addressed high-severity security issues.
Impact was limited to specific use cases detailed in the accompanying
advisories; overall user exposure was estimated to be marginal.</p>
<ul>
<li>
<p>Decompression-bomb safeguards of the streaming API were bypassed:</p>
<ol>
<li>When <code>HTTPResponse.drain_conn()</code> was called after the
response had been
read and decompressed partially.</li>
<li>During the second <code>HTTPResponse.read(amt=N)</code> or
<code>HTTPResponse.stream(amt=N)</code> call when the response was
decompressed
using the official <code>Brotli
&lt;https://pypi.org/project/brotli/&gt;</code>__ library.</li>
</ol>
<p>See <code>GHSA-mf9v-mfxr-j63j
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j&gt;</code>__
for details.</p>
</li>
<li>
<p>HTTP pools created using
<code>ProxyManager.connection_from_url</code> did not strip
sensitive headers specified in
<code>Retry.remove_headers_on_redirect</code> when
redirecting to a different host.
(<code>GHSA-qccp-gfcp-xxvc
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc&gt;</code>__)</p>
</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Used <code>FutureWarning</code> instead of
<code>DeprecationWarning</code> for better
visibility of existing deprecation notices. Rescheduled the removal of
deprecated features to version 3.0.
(<code>[#3763](https://github.com/urllib3/urllib3/issues/3763)
&lt;https://github.com/urllib3/urllib3/issues/3763&gt;</code>__)</li>
<li>Removed support for end-of-life Python 3.9.
(<code>[#3720](https://github.com/urllib3/urllib3/issues/3720)
&lt;https://github.com/urllib3/urllib3/issues/3720&gt;</code>__)</li>
<li>Removed support for end-of-life PyPy3.10.
(<code>[#4979](https://github.com/urllib3/urllib3/issues/4979)
&lt;https://github.com/urllib3/urllib3/issues/4979&gt;</code>__)</li>
<li>Bumped the minimum supported pyOpenSSL version to 19.0.0.
(<code>[#3777](https://github.com/urllib3/urllib3/issues/3777)
&lt;https://github.com/urllib3/urllib3/issues/3777&gt;</code>__)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>Fixed a bug where <code>HTTPResponse.read(amt=None)</code> was
ignoring decompressed
data buffered from previous partial reads.
(<code>[#3636](https://github.com/urllib3/urllib3/issues/3636)
&lt;https://github.com/urllib3/urllib3/issues/3636&gt;</code>__)</li>
<li>Fixed a bug where <code>HTTPResponse.read()</code> could cache only
part of the
response after a partial read when <code>cache_content=True</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/urllib3/urllib3/commit/9a950b92d999f906b6020bb2d1076ee56cddd5d2"><code>9a950b9</code></a>
Release 2.7.0</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/5ec0de499b9166ca71c65ab04f2a7e4eb0d66fcc"><code>5ec0de4</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2bdcc44d1e163fb5cc48a8662425e35e15adfe6a"><code>2bdcc44</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/f45b0df09d8620ac6ed0491eb9362c8c87b7bc2c"><code>f45b0df</code></a>
Fix a misleading example for <code>ProxyManager</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4970">#4970</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/577193ca029872384f82c133449e0935f6d8a64b"><code>577193c</code></a>
Switch to nightly PyPy3.11 in CI for now (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4984">#4984</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/e90af45bb006c3a452a3a21644a2681523f5c7fc"><code>e90af45</code></a>
Avoid infinite loop in <code>HTTPResponse.read_chunked</code> when
<code>amt=0</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4974">#4974</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/67ed74fdaec6659a6534621ec8e3aaaa6f976210"><code>67ed74f</code></a>
Bump dev dependencies (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4972">#4972</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/3abd481097b54d87b574ac7ea593c3f40938a84d"><code>3abd481</code></a>
Upgrade mypy to version 1.20.2 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4978">#4978</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2b8725dfcac4f21d4d93cc0cc3a64a33af08f890"><code>2b8725d</code></a>
Drop support for EOL PyPy3.10 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4979">#4979</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2944b2a0a6c573f5548a39cfd17196f98ee21b33"><code>2944b2a</code></a>
Upgrade <code>setup-chrome</code> and <code>setup-firefox</code> to fix
warnings (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4973">#4973</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=uv&previous-version=2.6.3&new-version=2.7.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 02:21:20 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
02e042d58c chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/prebuilt (#7787)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.31 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=uv&previous-version=0.7.31&new-version=0.8.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:19:17 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bb6bb60688 chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/checkpoint-postgres (#7785)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.31 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=uv&previous-version=0.7.31&new-version=0.8.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:19:05 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
296a83b8ff chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/langgraph (#7788)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.31 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=uv&previous-version=0.7.31&new-version=0.8.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 02:17:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
63a23ee999 chore(deps): bump langsmith from 0.7.32 to 0.8.0 in /libs/cli (#7791)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.32 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.32...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=uv&previous-version=0.7.32&new-version=0.8.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:13:01 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ebd2f7bb59 chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/sdk-py (#7789)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.7.31 to 0.8.0.
<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.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js,py): JS 0.6.0, Py 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2831">langchain-ai/langsmith-sdk#2831</a></li>
<li>release(js): 0.6.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2832">langchain-ai/langsmith-sdk#2832</a></li>
<li>release(py): 0.8.0 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2833">langchain-ai/langsmith-sdk#2833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.38...v0.8.0</a></p>
<h2>v0.7.38</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): add tracing of opencode by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2776">langchain-ai/langsmith-sdk#2776</a></li>
<li>chore(js): Remove types/uuid by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2814">langchain-ai/langsmith-sdk#2814</a></li>
<li>docs(sandbox): document default idle TTL of 10 minutes by <a
href="https://github.com/DanielKneipp"><code>@​DanielKneipp</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2788">langchain-ai/langsmith-sdk#2788</a></li>
<li>ci(py): Bump pytest timeout to 2m by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2815">langchain-ai/langsmith-sdk#2815</a></li>
<li>chore(deps-dev): bump the js-minor-and-patch group across 1
directory with 4 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/2803">langchain-ai/langsmith-sdk#2803</a></li>
<li>chore(deps): update sphinx-autobuild requirement from &gt;=2024 to
&gt;=2024.10.3 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/2809">langchain-ai/langsmith-sdk#2809</a></li>
<li>chore(deps): update myst-nb requirement from &gt;=1.1.1 to
&gt;=1.4.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/2810">langchain-ai/langsmith-sdk#2810</a></li>
<li>chore(deps-dev): bump types-pyyaml from 6.0.12.20250915 to
6.0.12.20260408 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/2812">langchain-ai/langsmith-sdk#2812</a></li>
<li>chore(deps-dev): bump <code>@​langchain/openai</code> from 0.5.18 to
0.6.17 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/2806">langchain-ai/langsmith-sdk#2806</a></li>
<li>chore(deps): bump the py-minor-and-patch group across 1 directory
with 18 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/2808">langchain-ai/langsmith-sdk#2808</a></li>
<li>feat(py): Adds strands OTEL exporter by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2817">langchain-ai/langsmith-sdk#2817</a></li>
<li>chore(js): Switch to oxfmt and oxlint by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2819">langchain-ai/langsmith-sdk#2819</a></li>
<li>fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic BaseModel by <a
href="https://github.com/QuentinBrosse"><code>@​QuentinBrosse</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2820">langchain-ai/langsmith-sdk#2820</a></li>
<li>chore: add apac support by <a
href="https://github.com/joaquin-borggio-lc"><code>@​joaquin-borggio-lc</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2821">langchain-ai/langsmith-sdk#2821</a></li>
<li>fix(js): Pull Claude Agent SDK subagent runs from transcript, add
tool span for subagents, merge message blocks by id by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2816">langchain-ai/langsmith-sdk#2816</a></li>
<li>release(js): 0.5.26 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2824">langchain-ai/langsmith-sdk#2824</a></li>
<li>release(py): 0.7.38 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2825">langchain-ai/langsmith-sdk#2825</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.37...v0.7.38</a></p>
<h2>v0.7.37</h2>
<h2>What's Changed</h2>
<ul>
<li>perf(js): Offload serialize to worker thread at flush time by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2781">langchain-ai/langsmith-sdk#2781</a></li>
<li>release(js): 0.5.24 by <a
href="https://github.com/emil-lc"><code>@​emil-lc</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2790">langchain-ai/langsmith-sdk#2790</a></li>
<li>chore(js): Fix perf test flagging by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2792">langchain-ai/langsmith-sdk#2792</a></li>
<li>feat(js,python): Adds hub model config and provider to schemas by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2793">langchain-ai/langsmith-sdk#2793</a></li>
<li>fix(js): minor test improvements by <a
href="https://github.com/christian-bromann"><code>@​christian-bromann</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2429">langchain-ai/langsmith-sdk#2429</a></li>
<li>fix(js): Include auth headers on info requests by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2800">langchain-ai/langsmith-sdk#2800</a></li>
<li>release(js): 0.5.25 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2801">langchain-ai/langsmith-sdk#2801</a></li>
<li>fix(python): flush both tracing_queue and compressed_traces in
flush() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2796">langchain-ai/langsmith-sdk#2796</a></li>
<li>chore(deps): bump postcss from 8.5.8 to 8.5.10 in
/js/internal/environment_tests/test-exports-vite 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/2791">langchain-ai/langsmith-sdk#2791</a></li>
<li>chore(deps-dev): bump google-adk from 1.10.0 to 1.28.1 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/2794">langchain-ai/langsmith-sdk#2794</a></li>
<li>fix(python): flush pending traces during Client.cleanup() by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2799">langchain-ai/langsmith-sdk#2799</a></li>
<li>fix(py): Fix concurrency for multiple Claude Agent SDK sessions by
<a href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2795">langchain-ai/langsmith-sdk#2795</a></li>
<li>release(py): 0.7.37 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2802">langchain-ai/langsmith-sdk#2802</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37">https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.36...v0.7.37</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/cf01c873d50f2f50249f1975b8d443a546168a85"><code>cf01c87</code></a>
release(py): 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2833">#2833</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/fd049c84648aa168e42636744587be0e0589da01"><code>fd049c8</code></a>
release(js): 0.6.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2832">#2832</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/092a8866c473810712fb7983459923cbbfce3069"><code>092a886</code></a>
feat(js,py): JS 0.6.0, Py 0.8.0 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2831">#2831</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/ff180c04237511b341ed455cfbfe6cadd9a5eeab"><code>ff180c0</code></a>
release(py): 0.7.38 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2825">#2825</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/d9de3ca801dc8a9e81d6e7dbb0bc5547a505f115"><code>d9de3ca</code></a>
release(js): 0.5.26 (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2824">#2824</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/14283948316ba4a274622440d537e61aec85b8c3"><code>1428394</code></a>
fix(js): Pull Claude Agent SDK subagent runs from transcript, add tool
span f...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/838e957d8027a8ff2e9e7cf8a10e874bfae328d7"><code>838e957</code></a>
chore: add apac support (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2821">#2821</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/003f22a768f7dad7efdeb775d0b1199a125ce4fe"><code>003f22a</code></a>
fix(py): fix RunTree ValidationError when inputs or outputs is a
Pydantic Bas...</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/8f5ef27c2d76dd62353b22f63b32a987e5d07407"><code>8f5ef27</code></a>
chore(js): Switch to oxfmt and oxlint (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2819">#2819</a>)</li>
<li><a
href="https://github.com/langchain-ai/langsmith-sdk/commit/9873633c9f066fc1c3ed7641ad54d646c90dae09"><code>9873633</code></a>
feat(py): Adds strands OTEL exporter (<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/issues/2817">#2817</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.7.31...v0.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=uv&previous-version=0.7.31&new-version=0.8.0)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:12:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1e8219a5c8 chore(deps): bump langsmith from 0.5.20 to 0.6.3 in /libs/cli/js-monorepo-example (#7783)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.5.20 to 0.6.3.
<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.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>ci: test more bundlers by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2263">langchain-ai/langsmith-sdk#2263</a></li>
<li>feat(python sdk): Add support for setting commit tags when pushing a
prompt by <a href="https://github.com/bees"><code>@​bees</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2265">langchain-ai/langsmith-sdk#2265</a></li>
<li>feat: Pass in Cache, rename by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2264">langchain-ai/langsmith-sdk#2264</a></li>
<li>chore: bump sdk by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2268">langchain-ai/langsmith-sdk#2268</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.6.0...v0.6.1">https://github.com/langchain-ai/langsmith-sdk/compare/v0.6.0...v0.6.1</a></p>
<h2>v0.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(js): bump JS to 0.4.3 by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2253">langchain-ai/langsmith-sdk#2253</a></li>
<li>Revert &quot;feat: add js prompt caching&quot; by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2258">langchain-ai/langsmith-sdk#2258</a></li>
<li>Revert &quot;feat: Replace UUID5 with deterministic UUID7 for
replicas&quot; by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2257">langchain-ai/langsmith-sdk#2257</a></li>
<li>release(js): bump to 0.4.4 by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2259">langchain-ai/langsmith-sdk#2259</a></li>
<li>feat: add prompt cache back and setup environment tests by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2260">langchain-ai/langsmith-sdk#2260</a></li>
<li>feat(python): Bump pydantic to v2 by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2248">langchain-ai/langsmith-sdk#2248</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.2...v0.6.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.2...v0.6.0</a></p>
<h2>v0.6.0rc0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): Add support for tracing AI SDK 6 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2237">langchain-ai/langsmith-sdk#2237</a></li>
<li>fix(js): Remove default Jestlike timeout by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2243">langchain-ai/langsmith-sdk#2243</a></li>
<li>feat(js): Add support for tracing tool loop agent by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2244">langchain-ai/langsmith-sdk#2244</a></li>
<li>feat: Replace UUID5 with deterministic UUID7 for replicas by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2249">langchain-ai/langsmith-sdk#2249</a></li>
<li>feat: add prompt caching to python sdk by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2246">langchain-ai/langsmith-sdk#2246</a></li>
<li>feat: add js prompt caching by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2251">langchain-ai/langsmith-sdk#2251</a></li>
<li>fix(claude): correctly parse llm and tool inputs in claude agent sdk
by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2255">langchain-ai/langsmith-sdk#2255</a></li>
<li>bump(python): 0.5.2 by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2256">langchain-ai/langsmith-sdk#2256</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.1...v0.6.0rc0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.1...v0.6.0rc0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/langchain-ai/langsmith-sdk/commits">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=npm_and_yarn&previous-version=0.5.20&new-version=0.6.3)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:12:29 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8411ffd98c chore(deps): bump langsmith from 0.5.20 to 0.6.3 in /libs/cli/js-examples (#7782)
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from
0.5.20 to 0.6.3.
<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.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>ci: test more bundlers by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2263">langchain-ai/langsmith-sdk#2263</a></li>
<li>feat(python sdk): Add support for setting commit tags when pushing a
prompt by <a href="https://github.com/bees"><code>@​bees</code></a> in
<a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2265">langchain-ai/langsmith-sdk#2265</a></li>
<li>feat: Pass in Cache, rename by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2264">langchain-ai/langsmith-sdk#2264</a></li>
<li>chore: bump sdk by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2268">langchain-ai/langsmith-sdk#2268</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.6.0...v0.6.1">https://github.com/langchain-ai/langsmith-sdk/compare/v0.6.0...v0.6.1</a></p>
<h2>v0.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(js): bump JS to 0.4.3 by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2253">langchain-ai/langsmith-sdk#2253</a></li>
<li>Revert &quot;feat: add js prompt caching&quot; by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2258">langchain-ai/langsmith-sdk#2258</a></li>
<li>Revert &quot;feat: Replace UUID5 with deterministic UUID7 for
replicas&quot; by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2257">langchain-ai/langsmith-sdk#2257</a></li>
<li>release(js): bump to 0.4.4 by <a
href="https://github.com/dqbd"><code>@​dqbd</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2259">langchain-ai/langsmith-sdk#2259</a></li>
<li>feat: add prompt cache back and setup environment tests by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2260">langchain-ai/langsmith-sdk#2260</a></li>
<li>feat(python): Bump pydantic to v2 by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2248">langchain-ai/langsmith-sdk#2248</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.2...v0.6.0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.2...v0.6.0</a></p>
<h2>v0.6.0rc0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(js): Add support for tracing AI SDK 6 by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2237">langchain-ai/langsmith-sdk#2237</a></li>
<li>fix(js): Remove default Jestlike timeout by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2243">langchain-ai/langsmith-sdk#2243</a></li>
<li>feat(js): Add support for tracing tool loop agent by <a
href="https://github.com/jacoblee93"><code>@​jacoblee93</code></a> in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2244">langchain-ai/langsmith-sdk#2244</a></li>
<li>feat: Replace UUID5 with deterministic UUID7 for replicas by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2249">langchain-ai/langsmith-sdk#2249</a></li>
<li>feat: add prompt caching to python sdk by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2246">langchain-ai/langsmith-sdk#2246</a></li>
<li>feat: add js prompt caching by <a
href="https://github.com/langchain-infra"><code>@​langchain-infra</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2251">langchain-ai/langsmith-sdk#2251</a></li>
<li>fix(claude): correctly parse llm and tool inputs in claude agent sdk
by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2255">langchain-ai/langsmith-sdk#2255</a></li>
<li>bump(python): 0.5.2 by <a
href="https://github.com/angus-langchain"><code>@​angus-langchain</code></a>
in <a
href="https://redirect.github.com/langchain-ai/langsmith-sdk/pull/2256">langchain-ai/langsmith-sdk#2256</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.1...v0.6.0rc0">https://github.com/langchain-ai/langsmith-sdk/compare/v0.5.1...v0.6.0rc0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/langchain-ai/langsmith-sdk/commits">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=npm_and_yarn&previous-version=0.5.20&new-version=0.6.3)](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/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:12:15 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2add40fdfa chore(deps): bump the uv group across 2 directories with 1 update (#7769)
Bumps the uv group with 1 update in the /libs/cli/uv-examples/monorepo
directory: [urllib3](https://github.com/urllib3/urllib3).
Bumps the uv group with 1 update in the /libs/cli/uv-examples/simple
directory: [urllib3](https://github.com/urllib3/urllib3).

Updates `urllib3` from 2.6.3 to 2.7.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.7.0</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Security</h2>
<p>Addressed high-severity security issues. Impact was limited to
specific use cases detailed in the accompanying advisories; overall user
exposure was estimated to be marginal.</p>
<ul>
<li>
<p>Decompression-bomb safeguards of the streaming API were bypassed:</p>
<ol>
<li>When <code>HTTPResponse.drain_conn()</code> was called after the
response had been read and decompressed partially. (Reported by <a
href="https://github.com/Cycloctane"><code>@​Cycloctane</code></a>)</li>
<li>During the second <code>HTTPResponse.read(amt=N)</code> or
<code>HTTPResponse.stream(amt=N)</code> call when the response was
decompressed using the official <a
href="https://pypi.org/project/brotli/">Brotli</a> library. (Reported by
<a
href="https://github.com/kimkou2024"><code>@​kimkou2024</code></a>)</li>
</ol>
<p>See GHSA-mf9v-mfxr-j63j for details.</p>
</li>
<li>
<p>HTTP pools created using
<code>ProxyManager.connection_from_url</code> did not strip sensitive
headers specified in <code>Retry.remove_headers_on_redirect</code> when
redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by <a
href="https://github.com/christos-spearbit"><code>@​christos-spearbit</code></a>)</p>
</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Used <code>FutureWarning</code> instead of
<code>DeprecationWarning</code> for better visibility of existing
deprecation notices. Rescheduled the removal of deprecated features to
version 3.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3763">urllib3/urllib3#3763</a>)</li>
<li>Removed support for end-of-life Python 3.9. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3720">urllib3/urllib3#3720</a>)</li>
<li>Removed support for end-of-life PyPy3.10. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4979">urllib3/urllib3#4979</a>)</li>
<li>Bumped the minimum supported pyOpenSSL version to 19.0.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3777">urllib3/urllib3#3777</a>)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>Fixed a bug where <code>HTTPResponse.read(amt=None)</code> was
ignoring decompressed data buffered from previous partial reads. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3636">urllib3/urllib3#3636</a>)</li>
<li>Fixed a bug where <code>HTTPResponse.read()</code> could cache only
part of the response after a partial read when
<code>cache_content=True</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4967">urllib3/urllib3#4967</a>)</li>
<li>Fixed <code>HTTPResponse.stream()</code> and
<code>HTTPResponse.read_chunked()</code> to handle <code>amt=0</code>.
(<a
href="https://redirect.github.com/urllib3/urllib3/issues/3793">urllib3/urllib3#3793</a>)</li>
<li>Updated <code>_TYPE_BODY</code> type alias to include missing
<code>Iterable[str]</code>, matching the documented and runtime behavior
of chunked request bodies. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3798">urllib3/urllib3#3798</a>)</li>
<li>Fixed <code>LocationParseError</code> when paths resembling
schemeless URIs were passed to
<code>HTTPConnectionPool.urlopen()</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3352">urllib3/urllib3#3352</a>)</li>
<li>Fixed <code>BaseHTTPResponse.readinto()</code> type annotation to
accept <code>memoryview</code> in addition to <code>bytearray</code>,
matching the <code>io.RawIOBase.readinto</code> contract and enabling
use with <code>io.BufferedReader</code> without type errors. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3764">urllib3/urllib3#3764</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.7.0 (2026-05-07)</h1>
<h2>Security</h2>
<p>Addressed high-severity security issues.
Impact was limited to specific use cases detailed in the accompanying
advisories; overall user exposure was estimated to be marginal.</p>
<ul>
<li>
<p>Decompression-bomb safeguards of the streaming API were bypassed:</p>
<ol>
<li>When <code>HTTPResponse.drain_conn()</code> was called after the
response had been
read and decompressed partially.</li>
<li>During the second <code>HTTPResponse.read(amt=N)</code> or
<code>HTTPResponse.stream(amt=N)</code> call when the response was
decompressed
using the official <code>Brotli
&lt;https://pypi.org/project/brotli/&gt;</code>__ library.</li>
</ol>
<p>See <code>GHSA-mf9v-mfxr-j63j
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j&gt;</code>__
for details.</p>
</li>
<li>
<p>HTTP pools created using
<code>ProxyManager.connection_from_url</code> did not strip
sensitive headers specified in
<code>Retry.remove_headers_on_redirect</code> when
redirecting to a different host.
(<code>GHSA-qccp-gfcp-xxvc
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc&gt;</code>__)</p>
</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Used <code>FutureWarning</code> instead of
<code>DeprecationWarning</code> for better
visibility of existing deprecation notices. Rescheduled the removal of
deprecated features to version 3.0.
(<code>[#3763](https://github.com/urllib3/urllib3/issues/3763)
&lt;https://github.com/urllib3/urllib3/issues/3763&gt;</code>__)</li>
<li>Removed support for end-of-life Python 3.9.
(<code>[#3720](https://github.com/urllib3/urllib3/issues/3720)
&lt;https://github.com/urllib3/urllib3/issues/3720&gt;</code>__)</li>
<li>Removed support for end-of-life PyPy3.10.
(<code>[#4979](https://github.com/urllib3/urllib3/issues/4979)
&lt;https://github.com/urllib3/urllib3/issues/4979&gt;</code>__)</li>
<li>Bumped the minimum supported pyOpenSSL version to 19.0.0.
(<code>[#3777](https://github.com/urllib3/urllib3/issues/3777)
&lt;https://github.com/urllib3/urllib3/issues/3777&gt;</code>__)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>Fixed a bug where <code>HTTPResponse.read(amt=None)</code> was
ignoring decompressed
data buffered from previous partial reads.
(<code>[#3636](https://github.com/urllib3/urllib3/issues/3636)
&lt;https://github.com/urllib3/urllib3/issues/3636&gt;</code>__)</li>
<li>Fixed a bug where <code>HTTPResponse.read()</code> could cache only
part of the
response after a partial read when <code>cache_content=True</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/urllib3/urllib3/commit/9a950b92d999f906b6020bb2d1076ee56cddd5d2"><code>9a950b9</code></a>
Release 2.7.0</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/5ec0de499b9166ca71c65ab04f2a7e4eb0d66fcc"><code>5ec0de4</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2bdcc44d1e163fb5cc48a8662425e35e15adfe6a"><code>2bdcc44</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/f45b0df09d8620ac6ed0491eb9362c8c87b7bc2c"><code>f45b0df</code></a>
Fix a misleading example for <code>ProxyManager</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4970">#4970</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/577193ca029872384f82c133449e0935f6d8a64b"><code>577193c</code></a>
Switch to nightly PyPy3.11 in CI for now (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4984">#4984</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/e90af45bb006c3a452a3a21644a2681523f5c7fc"><code>e90af45</code></a>
Avoid infinite loop in <code>HTTPResponse.read_chunked</code> when
<code>amt=0</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4974">#4974</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/67ed74fdaec6659a6534621ec8e3aaaa6f976210"><code>67ed74f</code></a>
Bump dev dependencies (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4972">#4972</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/3abd481097b54d87b574ac7ea593c3f40938a84d"><code>3abd481</code></a>
Upgrade mypy to version 1.20.2 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4978">#4978</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2b8725dfcac4f21d4d93cc0cc3a64a33af08f890"><code>2b8725d</code></a>
Drop support for EOL PyPy3.10 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4979">#4979</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2944b2a0a6c573f5548a39cfd17196f98ee21b33"><code>2944b2a</code></a>
Upgrade <code>setup-chrome</code> and <code>setup-firefox</code> to fix
warnings (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4973">#4973</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `urllib3` from 2.6.3 to 2.7.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.7.0</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Security</h2>
<p>Addressed high-severity security issues. Impact was limited to
specific use cases detailed in the accompanying advisories; overall user
exposure was estimated to be marginal.</p>
<ul>
<li>
<p>Decompression-bomb safeguards of the streaming API were bypassed:</p>
<ol>
<li>When <code>HTTPResponse.drain_conn()</code> was called after the
response had been read and decompressed partially. (Reported by <a
href="https://github.com/Cycloctane"><code>@​Cycloctane</code></a>)</li>
<li>During the second <code>HTTPResponse.read(amt=N)</code> or
<code>HTTPResponse.stream(amt=N)</code> call when the response was
decompressed using the official <a
href="https://pypi.org/project/brotli/">Brotli</a> library. (Reported by
<a
href="https://github.com/kimkou2024"><code>@​kimkou2024</code></a>)</li>
</ol>
<p>See GHSA-mf9v-mfxr-j63j for details.</p>
</li>
<li>
<p>HTTP pools created using
<code>ProxyManager.connection_from_url</code> did not strip sensitive
headers specified in <code>Retry.remove_headers_on_redirect</code> when
redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by <a
href="https://github.com/christos-spearbit"><code>@​christos-spearbit</code></a>)</p>
</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Used <code>FutureWarning</code> instead of
<code>DeprecationWarning</code> for better visibility of existing
deprecation notices. Rescheduled the removal of deprecated features to
version 3.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3763">urllib3/urllib3#3763</a>)</li>
<li>Removed support for end-of-life Python 3.9. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3720">urllib3/urllib3#3720</a>)</li>
<li>Removed support for end-of-life PyPy3.10. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4979">urllib3/urllib3#4979</a>)</li>
<li>Bumped the minimum supported pyOpenSSL version to 19.0.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3777">urllib3/urllib3#3777</a>)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>Fixed a bug where <code>HTTPResponse.read(amt=None)</code> was
ignoring decompressed data buffered from previous partial reads. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3636">urllib3/urllib3#3636</a>)</li>
<li>Fixed a bug where <code>HTTPResponse.read()</code> could cache only
part of the response after a partial read when
<code>cache_content=True</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4967">urllib3/urllib3#4967</a>)</li>
<li>Fixed <code>HTTPResponse.stream()</code> and
<code>HTTPResponse.read_chunked()</code> to handle <code>amt=0</code>.
(<a
href="https://redirect.github.com/urllib3/urllib3/issues/3793">urllib3/urllib3#3793</a>)</li>
<li>Updated <code>_TYPE_BODY</code> type alias to include missing
<code>Iterable[str]</code>, matching the documented and runtime behavior
of chunked request bodies. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3798">urllib3/urllib3#3798</a>)</li>
<li>Fixed <code>LocationParseError</code> when paths resembling
schemeless URIs were passed to
<code>HTTPConnectionPool.urlopen()</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3352">urllib3/urllib3#3352</a>)</li>
<li>Fixed <code>BaseHTTPResponse.readinto()</code> type annotation to
accept <code>memoryview</code> in addition to <code>bytearray</code>,
matching the <code>io.RawIOBase.readinto</code> contract and enabling
use with <code>io.BufferedReader</code> without type errors. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3764">urllib3/urllib3#3764</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.7.0 (2026-05-07)</h1>
<h2>Security</h2>
<p>Addressed high-severity security issues.
Impact was limited to specific use cases detailed in the accompanying
advisories; overall user exposure was estimated to be marginal.</p>
<ul>
<li>
<p>Decompression-bomb safeguards of the streaming API were bypassed:</p>
<ol>
<li>When <code>HTTPResponse.drain_conn()</code> was called after the
response had been
read and decompressed partially.</li>
<li>During the second <code>HTTPResponse.read(amt=N)</code> or
<code>HTTPResponse.stream(amt=N)</code> call when the response was
decompressed
using the official <code>Brotli
&lt;https://pypi.org/project/brotli/&gt;</code>__ library.</li>
</ol>
<p>See <code>GHSA-mf9v-mfxr-j63j
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j&gt;</code>__
for details.</p>
</li>
<li>
<p>HTTP pools created using
<code>ProxyManager.connection_from_url</code> did not strip
sensitive headers specified in
<code>Retry.remove_headers_on_redirect</code> when
redirecting to a different host.
(<code>GHSA-qccp-gfcp-xxvc
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc&gt;</code>__)</p>
</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Used <code>FutureWarning</code> instead of
<code>DeprecationWarning</code> for better
visibility of existing deprecation notices. Rescheduled the removal of
deprecated features to version 3.0.
(<code>[#3763](https://github.com/urllib3/urllib3/issues/3763)
&lt;https://github.com/urllib3/urllib3/issues/3763&gt;</code>__)</li>
<li>Removed support for end-of-life Python 3.9.
(<code>[#3720](https://github.com/urllib3/urllib3/issues/3720)
&lt;https://github.com/urllib3/urllib3/issues/3720&gt;</code>__)</li>
<li>Removed support for end-of-life PyPy3.10.
(<code>[#4979](https://github.com/urllib3/urllib3/issues/4979)
&lt;https://github.com/urllib3/urllib3/issues/4979&gt;</code>__)</li>
<li>Bumped the minimum supported pyOpenSSL version to 19.0.0.
(<code>[#3777](https://github.com/urllib3/urllib3/issues/3777)
&lt;https://github.com/urllib3/urllib3/issues/3777&gt;</code>__)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>Fixed a bug where <code>HTTPResponse.read(amt=None)</code> was
ignoring decompressed
data buffered from previous partial reads.
(<code>[#3636](https://github.com/urllib3/urllib3/issues/3636)
&lt;https://github.com/urllib3/urllib3/issues/3636&gt;</code>__)</li>
<li>Fixed a bug where <code>HTTPResponse.read()</code> could cache only
part of the
response after a partial read when <code>cache_content=True</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/urllib3/urllib3/commit/9a950b92d999f906b6020bb2d1076ee56cddd5d2"><code>9a950b9</code></a>
Release 2.7.0</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/5ec0de499b9166ca71c65ab04f2a7e4eb0d66fcc"><code>5ec0de4</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2bdcc44d1e163fb5cc48a8662425e35e15adfe6a"><code>2bdcc44</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/f45b0df09d8620ac6ed0491eb9362c8c87b7bc2c"><code>f45b0df</code></a>
Fix a misleading example for <code>ProxyManager</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4970">#4970</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/577193ca029872384f82c133449e0935f6d8a64b"><code>577193c</code></a>
Switch to nightly PyPy3.11 in CI for now (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4984">#4984</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/e90af45bb006c3a452a3a21644a2681523f5c7fc"><code>e90af45</code></a>
Avoid infinite loop in <code>HTTPResponse.read_chunked</code> when
<code>amt=0</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4974">#4974</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/67ed74fdaec6659a6534621ec8e3aaaa6f976210"><code>67ed74f</code></a>
Bump dev dependencies (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4972">#4972</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/3abd481097b54d87b574ac7ea593c3f40938a84d"><code>3abd481</code></a>
Upgrade mypy to version 1.20.2 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4978">#4978</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2b8725dfcac4f21d4d93cc0cc3a64a33af08f890"><code>2b8725d</code></a>
Drop support for EOL PyPy3.10 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4979">#4979</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/2944b2a0a6c573f5548a39cfd17196f98ee21b33"><code>2944b2a</code></a>
Upgrade <code>setup-chrome</code> and <code>setup-firefox</code> to fix
warnings (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4973">#4973</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0">compare
view</a></li>
</ul>
</details>
<br />


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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:12:05 -07:00
Mason DaughertyandGitHub 076e2a3627 fix(ci): avoid inline benchmark output interpolation (#7779)
Avoid interpolating changed-file and benchmark outputs directly into the
`github-script` source for benchmark annotations.
2026-05-12 22:27:38 -07:00
Mason DaughertyandGitHub 4a86705bd7 ci: disable uv cache in publish pipeline (#7777)
Disable `uv` dependency caching throughout the release workflow so
build, validation, PyPI publish, and GitHub release jobs do not restore
shared cache state.

Release jobs already pass the built distributions through explicit
artifacts; keeping dependency caches out of this path avoids
cache-poisoning risk and makes the existing pre-release validation
comment match the actual workflow behavior.
2026-05-12 10:10:50 -07:00
153 changed files with 24622 additions and 637 deletions
@@ -0,0 +1,85 @@
name: sdk-py integration test
on:
workflow_call:
secrets:
LANGSMITH_API_KEY:
required: false
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_RO_TOKEN:
required: false
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
name: "sdk-py integration"
defaults:
run:
working-directory: libs/sdk-py
env:
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: ./.github/actions/uv_setup
with:
python-version: "3.13"
cache-suffix: sdk-py-integration
working-directory: libs/sdk-py
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
- name: Install dependencies
shell: bash
run: uv sync --frozen --group test --no-dev
- name: Skip if LANGSMITH_API_KEY is not available
if: env.HAS_LANGSMITH_API_KEY != 'true'
run: |
echo "LANGSMITH_API_KEY is not set (likely a fork PR). Skipping integration tests."
exit 0
- name: Bring up integration stack
if: env.HAS_LANGSMITH_API_KEY == 'true'
working-directory: libs/sdk-py/integration
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: docker compose up -d --build
- name: Wait for API healthcheck
if: env.HAS_LANGSMITH_API_KEY == 'true'
run: |
for i in $(seq 1 60); do
if curl -sf http://localhost:2024/ok >/dev/null; then
echo "API ready after ${i}s"
exit 0
fi
sleep 2
done
echo "API failed to become healthy within 120s"
docker compose -f libs/sdk-py/integration/docker-compose.yml logs api | tail -100
exit 1
- name: Run integration suite
if: env.HAS_LANGSMITH_API_KEY == 'true'
run: uv run pytest tests/integration/ -m integration
- name: Dump api logs on failure
if: failure() && env.HAS_LANGSMITH_API_KEY == 'true'
working-directory: libs/sdk-py/integration
run: docker compose logs api | tail -200
- name: Tear down stack
if: always() && env.HAS_LANGSMITH_API_KEY == 'true'
working-directory: libs/sdk-py/integration
run: docker compose down -v
+7 -3
View File
@@ -58,14 +58,18 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Annotation
uses: actions/github-script@v9
env:
CHANGED_FILES: ${{ steps.files.outputs.added_modified_renamed }}
BENCHMARK_OUTPUT: ${{ steps.benchmark.outputs.OUTPUT }}
COMPARE_OUTPUT: ${{ steps.compare.outputs.OUTPUT }}
with:
script: |
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
core.notice(`${{ steps.benchmark.outputs.OUTPUT }}`, {
const file = JSON.parse(process.env.CHANGED_FILES || "[]")[0]
core.notice(process.env.BENCHMARK_OUTPUT || "", {
title: 'Benchmark results',
file,
})
core.notice(`${{ steps.compare.outputs.OUTPUT }}`, {
core.notice(process.env.COMPARE_OUTPUT || "", {
title: 'Comparison against main',
file,
})
+13
View File
@@ -28,6 +28,7 @@ jobs:
outputs:
python: ${{ steps.filter.outputs.python || 'true' }}
deps: ${{ steps.filter.outputs.deps || 'true' }}
sdk_py: ${{ steps.filter.outputs.sdk_py || 'true' }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
@@ -47,6 +48,10 @@ jobs:
deps:
- '**/pyproject.toml'
- '**/uv.lock'
sdk_py:
- 'libs/sdk-py/**'
- 'libs/langgraph/langgraph/pregel/remote.py'
- 'libs/langgraph/langgraph/pregel/_remote_run_stream.py'
lint:
needs: changes
@@ -156,6 +161,13 @@ jobs:
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
sdk-py-integration-test:
needs: changes
if: needs.changes.outputs.sdk_py == 'true'
name: "sdk-py integration test"
uses: ./.github/workflows/_sdk_integration_test.yml
secrets: inherit
ci_success:
name: "CI Success"
needs:
@@ -166,6 +178,7 @@ jobs:
check-sdk-methods,
check-schema,
integration-test,
sdk-py-integration-test,
]
if: |
always()
+3
View File
@@ -32,6 +32,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache-suffix: "release"
enable-cache: false
working-directory: ${{ inputs.working-directory }}
# We want to keep this build stage *separate* from the release stage,
@@ -268,6 +269,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache-suffix: "release"
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@v8
@@ -309,6 +311,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache-suffix: "release"
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@v8
+4 -4
View File
@@ -208,11 +208,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.13"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -279,7 +279,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
+7 -7
View File
@@ -205,11 +205,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -276,7 +276,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -399,7 +399,7 @@ test = [
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -412,9 +412,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[[package]]
+7 -7
View File
@@ -214,11 +214,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -285,7 +285,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -402,7 +402,7 @@ test = [
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -415,9 +415,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[[package]]
@@ -157,27 +157,6 @@ class JsonPlusSerializer(SerializerProtocol):
)
return clone
def _encode_constructor_args(
self,
constructor: Callable | type[Any],
*,
method: None | str | Sequence[None | str] = None,
args: Sequence[Any] | None = None,
kwargs: dict[str, Any] | None = None,
) -> dict[str, Any]:
out = {
"lc": 2,
"type": "constructor",
"id": (*constructor.__module__.split("."), constructor.__name__),
}
if method is not None:
out["method"] = method
if args is not None:
out["args"] = args
if kwargs is not None:
out["kwargs"] = kwargs
return out
def _reviver(self, value: dict[str, Any]) -> Any:
if (
value.get("lc", None) == 2
@@ -203,45 +182,46 @@ class JsonPlusSerializer(SerializerProtocol):
self._check_allowed_json_modules(value)
[*module, name] = value["id"]
# The `method` field on lc:2 envelopes is intentionally ignored.
# Revival is restricted to the default constructor (no `getattr`
# dispatch on attacker-influenced names). The framework's own
# encoder has not emitted `method=` since the msgpack migration,
# and the only legacy emission was `method=(None, "construct")`
# for pydantic models, where the first entry (`None`) already
# meant "default constructor", which is what we do here.
try:
mod = importlib.import_module(".".join(module))
cls = getattr(mod, name)
method = value.get("method")
if isinstance(method, str):
methods = [getattr(cls, method)]
elif isinstance(method, list):
methods = [cls if m is None else getattr(cls, m) for m in method]
else:
methods = [cls]
if isclass(cls) and issubclass(cls, BaseException):
return None
args = value.get("args")
kwargs = value.get("kwargs")
for method in methods:
try:
if isclass(method) and issubclass(method, BaseException):
return None
if args and kwargs:
return method(*args, **kwargs)
elif args:
return method(*args)
elif kwargs:
return method(**kwargs)
else:
return method()
except Exception:
continue
except Exception:
if args and kwargs:
return cls(*args, **kwargs)
elif args:
return cls(*args)
elif kwargs:
return cls(**kwargs)
else:
return cls()
except Exception as exc:
# Method-field dispatch has been removed (GHSA-fjqc-hq36-qh5p), so
# legacy pydantic payloads emitting `method=[None, "construct"]`
# no longer fall back to `cls.construct(**kwargs)` when the
# default constructor rejects the serialized kwargs. Surface a
# one-line warning so operators can spot payloads that now revive
# to `None` instead of silently degrading to the raw envelope.
logger.warning(
"Failed to revive lc:2 envelope %s "
"(legacy_method_field=%s, error=%s); returning None",
".".join((*module, name)),
"method" in value,
type(exc).__name__,
)
return None
def _check_allowed_json_modules(self, value: dict[str, Any]) -> None:
needed = tuple(value["id"])
method = value.get("method")
if isinstance(method, list):
method_display = ",".join(m or "<init>" for m in method)
elif isinstance(method, str):
method_display = method
else:
method_display = "<init>"
dotted = ".".join(needed)
# Safe types (the same set already allowed for msgpack deserialization) are
# permitted without an explicit allowlist — they are known-safe LangGraph and
@@ -252,7 +232,7 @@ class JsonPlusSerializer(SerializerProtocol):
if not self._allowed_json_modules:
raise InvalidModuleError(
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
f"Refused to deserialize JSON constructor: {dotted}. "
"No allowed_json_modules configured.\n\n"
"Unblock with ONE of:\n"
f" • JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
@@ -267,7 +247,7 @@ class JsonPlusSerializer(SerializerProtocol):
return
raise InvalidModuleError(
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
f"Refused to deserialize JSON constructor: {dotted}. "
"Symbol is not in the deserialization allowlist.\n\n"
"Add exactly this symbol to unblock:\n"
f" JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.10"
+187
View File
@@ -398,6 +398,193 @@ def test_deserde_invalid_module() -> None:
serde.loads_typed(("json", json.dumps(load).encode("utf-8")))
def test_lc2_json_method_field_is_ignored() -> None:
"""The `method` field on lc=2 envelopes is ignored.
Regression test for GHSA-fjqc-hq36-qh5p: `_revive_lc2` previously resolved
`getattr(cls, method)` from the envelope, which let an attacker pivot a
safe pydantic class (e.g., AIMessage) to `parse_raw(..., allow_pickle=True)`
and reach `pickle.loads`. Revival now uses only the default constructor.
Verifies that an envelope carrying ``method="parse_raw"`` does not dispatch
to that method: the result is whatever ``AIMessage(*args, **kwargs)`` would
produce, which proves the default constructor ran instead of ``parse_raw``.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer()
load = {
"lc": 2,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"method": "parse_raw",
"args": ["default-ctor-ran"],
"kwargs": {"content_type": "application/pickle", "allow_pickle": True},
}
result = serde._revive_lc2(load)
# Default constructor accepts `content` as first positional arg. If parse_raw
# had been invoked instead, it would have attempted JSON/pickle parsing and
# raised (or executed the pickle gadget); neither would produce this result.
assert isinstance(result, AIMessage)
assert result.content == "default-ctor-ran"
def test_lc2_json_method_field_is_ignored_for_allowlisted_types() -> None:
"""The `method` field is ignored even when the class is explicitly allowlisted.
A user who configures ``allowed_json_modules`` for a class no longer gets
method dispatch as a side effect. Revival is restricted to the default
constructor regardless of how the class reached the revival path.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer(
allowed_json_modules=[("langchain_core.messages.ai", "AIMessage")]
)
load = {
"lc": 2,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"method": "parse_raw",
"args": ["default-ctor-ran"],
}
result = serde._revive_lc2(load)
assert isinstance(result, AIMessage)
assert result.content == "default-ctor-ran"
def test_lc2_json_safe_type_init_still_works() -> None:
"""SAFE-type lc=2 revival without a `method` field still constructs the class."""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer()
load = {
"lc": 2,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"kwargs": {"content": "hi", "type": "ai"},
}
result = serde._revive_lc2(load)
assert isinstance(result, AIMessage)
assert result.content == "hi"
def test_lc2_json_legacy_pydantic_method_list_falls_back_to_default() -> None:
"""Legacy ``method=(None, "construct")`` envelopes still revive via the default ctor.
Pre-October-2025 langgraph emitted pydantic models with
``method=(None, "construct")`` meaning "try default constructor, fall back
to pydantic ``construct``". The first entry (``None``) was always the
default constructor, which is what we now do unconditionally. Envelopes of
this shape continue to revive correctly as long as the default constructor
accepts the serialized kwargs.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer()
load = {
"lc": 2,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"method": [None, "construct"],
"kwargs": {"content": "legacy", "type": "ai"},
}
result = serde._revive_lc2(load)
assert isinstance(result, AIMessage)
assert result.content == "legacy"
def test_lc2_json_legacy_construct_payload_logs_warning_when_default_init_rejects(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Legacy `method=[None, "construct"]` envelopes whose kwargs the default
`__init__` rejects now revive to `None` and emit an observable warning.
Pre-October-2025 langgraph emitted pydantic payloads with
`method=[None, "construct"]` so the reviver could fall back to
`cls.construct(**kwargs)` when the default constructor raised a
validation error. That fallback was removed with method-field dispatch
(GHSA-fjqc-hq36-qh5p), so these payloads now silently fail validation. A
`logger.warning` makes the regression observable to operators instead of
letting the envelope quietly degrade to its raw-dict form.
"""
serde = JsonPlusSerializer()
load = {
"lc": 2,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
# Legacy two-entry method tuple: try default ctor, fall back to construct.
"method": [None, "construct"],
# ``type="not-a-real-message-type"`` fails AIMessage's Literal["ai"]
# validator under the default constructor. Before the GHSA patch this
# would have fallen back to ``cls.construct(**kwargs)``; now it must
# return None and log.
"kwargs": {"content": "legacy", "type": "not-a-real-message-type"},
}
with caplog.at_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus"):
result = serde._revive_lc2(load)
assert result is None, (
"Legacy method=[None, 'construct'] payloads with kwargs the default "
"ctor rejects must now return None (no construct() fallback)."
)
matching = [
r
for r in caplog.records
if r.name == "langgraph.checkpoint.serde.jsonplus"
and r.levelno == logging.WARNING
and "langchain_core.messages.ai.AIMessage" in r.getMessage()
and "legacy_method_field=True" in r.getMessage()
]
assert matching, (
"Expected a WARNING from langgraph.checkpoint.serde.jsonplus "
"referencing the class id and legacy_method_field=True; "
f"got records: {[(r.name, r.levelname, r.getMessage()) for r in caplog.records]}"
)
def test_lc2_json_safe_type_pickle_payload_does_not_execute() -> None:
"""End-to-end: a `parse_raw` pickle gadget payload on a SAFE type must not run.
With method dispatch removed from `_revive_lc2`, the gadget bytes are never
passed to `parse_raw` and therefore never reach `pickle.loads`.
"""
import os
import pickle
import tempfile
marker = tempfile.NamedTemporaryFile(
prefix="lc2_block_proof_", suffix=".out", delete=False
).name
os.remove(marker) # ensure absent before the test runs
class _Gadget:
def __reduce__(self) -> tuple:
return (os.system, (f"touch {marker}",))
gadget_bytes = pickle.dumps(_Gadget(), protocol=0).decode("latin1")
envelope = {
"lc": 2,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"method": "parse_raw",
"args": [gadget_bytes],
"kwargs": {"content_type": "application/pickle", "allow_pickle": True},
}
serde = JsonPlusSerializer()
try:
serde.loads_typed(("json", json.dumps(envelope).encode()))
except Exception:
pass
assert not os.path.exists(marker), (
"Pickle gadget executed via parse_raw on AIMessage lc=2 envelope"
)
def test_serde_jsonplus_bytearray() -> None:
serde = JsonPlusSerializer()
+7 -7
View File
@@ -229,11 +229,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -300,7 +300,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -383,7 +383,7 @@ test = [
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -396,9 +396,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[[package]]
+4 -5
View File
@@ -3675,12 +3675,11 @@ keyv@^4.5.4:
json-buffer "3.0.1"
"langsmith@>=0.5.0 <1.0.0":
version "0.5.20"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.20.tgz#4021847d2ccd5a86c5eb96060f9bb5f19f80eca5"
integrity sha512-ULhLM8RswvQDXufLtNtvclHrWCBx8Cb5UPI6lAZC+8Dq59iHsVPz/3Ac9khWNm1VIvChRsuykixD/WrmzuuA3Q==
version "0.6.3"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.6.3.tgz#a3d8ad58d66a47d3697e3c69b2be3f6df5233190"
integrity sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==
dependencies:
p-queue "6.6.2"
uuid "10.0.0"
leven@^3.1.0:
version "3.1.0"
@@ -4848,7 +4847,7 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
uuid@10.0.0, uuid@^10.0.0:
uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
+1 -1
View File
@@ -17,7 +17,7 @@
"lint": "eslint 'apps/**/*.ts' 'libs/**/*.ts'"
},
"devDependencies": {
"turbo": "^2.9.7",
"turbo": "^2.9.14",
"typescript": "^5.9.3",
"@tsconfig/recommended": "^1.0.13",
"@eslint/eslintrc": "^3.3.5",
+38 -39
View File
@@ -166,35 +166,35 @@
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.13.tgz#269fce3ad04ca70b93269ff44cca81b950f542da"
integrity sha512-sySRuBfMKyKO/j2ZAhR8kSembhjuPEV4Ra3AHtmWLq51+iGaudr45crPSzNC5b7/Ctrh9dfUpBuTlYrH6rM58Q==
"@turbo/darwin-64@2.9.7":
version "2.9.7"
resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.9.7.tgz#46fddae01ea7817192dc6ff36a678cc3879b753b"
integrity sha512-wnvOWuVWJ5EUHNKxExEWiGlTeVpLG1L0PCu5MUozyC1P2SHGiWsmpW6/yAuShH91Fa2TAHOvdCRBzriZh4j4Eg==
"@turbo/darwin-64@2.9.14":
version "2.9.14"
resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.9.14.tgz#b9ec6ac637b9c5fdba5dae9d743f5d121f091242"
integrity sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==
"@turbo/darwin-arm64@2.9.7":
version "2.9.7"
resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.9.7.tgz#cd7cf3a024509af0f59ae3885216d62fd090fdb6"
integrity sha512-mA0FIPMwwN3lodDkQYaGxj6PeT7ZaN5aCEbkKn/WB+ZB9yJdVWA4J83GH7t43jqDc5dcnVluVN5UFx3plRiXhA==
"@turbo/darwin-arm64@2.9.14":
version "2.9.14"
resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.9.14.tgz#99d19f3e59842c595d2828c72a2e4ef537408f38"
integrity sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==
"@turbo/linux-64@2.9.7":
version "2.9.7"
resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.9.7.tgz#25ee9a2cb3042498a3203e14653b14041105020f"
integrity sha512-fEbUYpgb5l7P+q+5tsWF2gw+/GSjUsuUTcnfm+f0lozUjgcjLKyOat6PgtAChmIFcTPchCL/8rJ3TvkBy01gfA==
"@turbo/linux-64@2.9.14":
version "2.9.14"
resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.9.14.tgz#9c907434f091cd75529f5496516f79b71935d2bb"
integrity sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==
"@turbo/linux-arm64@2.9.7":
version "2.9.7"
resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.9.7.tgz#5b19d6249a50cb8153b8f2bc22ad10bbda614fbe"
integrity sha512-VkUjulo9ytfHKUHOS5gy0XPoh4CTKPXWCL8nLdrlHVi9fSut31ECeUqnm/dAbETP5D4xo9mH9XkJ+qMzGe/zmg==
"@turbo/linux-arm64@2.9.14":
version "2.9.14"
resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.9.14.tgz#79ae9060e6ee9e0fb784ae0747e980f582e75313"
integrity sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==
"@turbo/windows-64@2.9.7":
version "2.9.7"
resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.9.7.tgz#900645776fa44ff8333e801d890992736f6dbc72"
integrity sha512-/GWdY6/x4aIHqkYJq596Rpdk1x0MkpRPkJcLAoB3yGRwyUms0+u2F1GnV54IbyAZTeKLRWSJKzNC+QwVGdYchA==
"@turbo/windows-64@2.9.14":
version "2.9.14"
resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.9.14.tgz#68a80f299f35189314184c88301caad982d1c0c2"
integrity sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==
"@turbo/windows-arm64@2.9.7":
version "2.9.7"
resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.9.7.tgz#e678d73fdbcbb12a679403fe7ba070fb67f37810"
integrity sha512-xBBgxCC5PK2+WZ1PPRZdp+aJ0bMBcEbweXWux3RUHJvX9ZodcoQySkrW6qt+ahb+uk8ZjyQodLfDwtVSoYds1w==
"@turbo/windows-arm64@2.9.14":
version "2.9.14"
resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.9.14.tgz#4dc16684f0ddce53fafe56ad8f55205c4473d17a"
integrity sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==
"@types/esrecurse@^4.3.1":
version "4.3.1"
@@ -1327,12 +1327,11 @@ keyv@^4.5.4:
json-buffer "3.0.1"
"langsmith@>=0.5.0 <1.0.0":
version "0.5.20"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.20.tgz#4021847d2ccd5a86c5eb96060f9bb5f19f80eca5"
integrity sha512-ULhLM8RswvQDXufLtNtvclHrWCBx8Cb5UPI6lAZC+8Dq59iHsVPz/3Ac9khWNm1VIvChRsuykixD/WrmzuuA3Q==
version "0.7.1"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.7.1.tgz#d721ad2e5211f9a5373e8a84eb4fed36b5621971"
integrity sha512-Wjk90UjNoY5cBHMlNAC/eZx5clI8jnjBOBW8uJu8+MWBtx0QesNjsUiLtjI+I3UnrpxFFpDqGXcnhBjH654Mqg==
dependencies:
p-queue "6.6.2"
uuid "10.0.0"
levn@^0.4.1:
version "0.4.1"
@@ -1814,17 +1813,17 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
turbo@^2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.9.7.tgz#da6d5a821f0bde5174e1a55af6690dad5c2fb004"
integrity sha512-epxzqVO2s0IxcSWcgb+qKrtco8isfe7g3VtiS6hkYnEK4A9XQDZbrtavQ6MtWR1KoQn+1fUomaQth2rfRHlUlg==
turbo@^2.9.14:
version "2.9.14"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.9.14.tgz#d412fcc4c9bd8dba29cec5bbd54d5a74ab2b1bee"
integrity sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==
optionalDependencies:
"@turbo/darwin-64" "2.9.7"
"@turbo/darwin-arm64" "2.9.7"
"@turbo/linux-64" "2.9.7"
"@turbo/linux-arm64" "2.9.7"
"@turbo/windows-64" "2.9.7"
"@turbo/windows-arm64" "2.9.7"
"@turbo/darwin-64" "2.9.14"
"@turbo/darwin-arm64" "2.9.14"
"@turbo/linux-64" "2.9.14"
"@turbo/linux-arm64" "2.9.14"
"@turbo/windows-64" "2.9.14"
"@turbo/windows-arm64" "2.9.14"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
@@ -1900,7 +1899,7 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
uuid@10.0.0, uuid@^10.0.0:
uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.26"
__version__ = "0.4.27"
@@ -0,0 +1,141 @@
"""Detection of tracked Python packages in a local LangGraph project.
Mirrors host-backend's `host.models.dependency_tracking` so that CLI-based
deploys report the same `tracked_packages` revision metadata that
GitHub-based deploys do. The host backend strictly validates each entry
against `<package-name>:<version>` with package-name in `TRACKED_PACKAGES`,
so the detection rules here must match exactly.
"""
from __future__ import annotations
import pathlib
import re
# Single source of truth for which packages the host backend cares about.
# Keep in sync with host-backend/host/models/tracked_packages.py.
TRACKED_PACKAGES: tuple[str, ...] = ("google-adk",)
_MAX_READ_BYTES = 5 * 1024 * 1024
_PACKAGES_ALT = "|".join(re.escape(p) for p in TRACKED_PACKAGES)
_DEPS_RE = re.compile(
rf"(?<![a-zA-Z0-9_-])({_PACKAGES_ALT})"
r"(?:\[[^\]]*\])?"
r"\s*((?:(?:==|>=|<=|~=|!=|>|<)\s*[\w.*]+\s*,?\s*)+)"
)
_UV_LOCK_RE = re.compile(
rf'name\s*=\s*"({_PACKAGES_ALT})"\s*\n\s*version\s*=\s*"([^"]+)"'
)
_BARE_RE = re.compile(rf'(?<![a-zA-Z0-9_-])({_PACKAGES_ALT})(?:\[[^\]]*\])?\s*[,"\'\n]')
_EXTRAS_BRACKET_RE = re.compile(r"\[([a-zA-Z0-9_.\- ,\t]+)\]")
def _appears_in_extras(content: str, pkg: str) -> bool:
for m in _EXTRAS_BRACKET_RE.finditer(content):
for token in m.group(1).split(","):
if token.strip() == pkg:
return True
return False
def _read_text(path: pathlib.Path) -> str | None:
try:
if not path.is_file():
return None
with open(path, "rb") as f:
data = f.read(_MAX_READ_BYTES + 1)
except OSError:
return None
if len(data) > _MAX_READ_BYTES:
data = data[:_MAX_READ_BYTES]
return data.decode("utf-8", errors="replace")
def _find_version_for(
pkg: str,
lock_content: str | None,
pyproject_content: str | None,
requirements_content: str | None,
) -> str | None:
if lock_content is not None:
for m in _UV_LOCK_RE.finditer(lock_content):
if m.group(1) == pkg:
return m.group(2)
for content in (pyproject_content, requirements_content):
if content is None:
continue
for m in _DEPS_RE.finditer(content):
if m.group(1) == pkg:
return m.group(2).strip().rstrip(",")
for m in _BARE_RE.finditer(content):
if m.group(1) == pkg:
return "unknown"
if _appears_in_extras(content, pkg):
return "unknown"
return None
def _resolved_dep_base(
project_root: pathlib.Path, dep_path: str
) -> pathlib.Path | None:
"""Return the resolved dep directory if it stays inside the project root."""
try:
candidate = (project_root / dep_path).resolve()
except (OSError, RuntimeError):
return None
try:
candidate.relative_to(project_root)
except ValueError:
return None
return candidate
def find_tracked_packages(
config: pathlib.Path,
config_json: dict,
) -> list[str]:
"""Return every tracked package found in deps as `<name>:<version>` entries.
`config` is the absolute path to `langgraph.json`; dep paths in
`config_json["dependencies"]` are resolved relative to its parent.
Detection precedence per package: uv.lock resolved > pyproject.toml /
requirements.txt specifier > bare reference > extras bracket (last
two recorded as "unknown"). Output is ordered by `TRACKED_PACKAGES`.
"""
try:
project_root = config.parent.resolve()
except (OSError, RuntimeError):
return []
dep_paths = config_json.get("dependencies") or ["."]
found: dict[str, str] = {}
for dep_path in dep_paths:
if all(pkg in found for pkg in TRACKED_PACKAGES):
break
if not isinstance(dep_path, str):
continue
base = _resolved_dep_base(project_root, dep_path)
if base is None or not base.is_dir():
continue
lock_content = _read_text(base / "uv.lock")
pyproject_content = _read_text(base / "pyproject.toml")
requirements_content = _read_text(base / "requirements.txt")
for pkg in TRACKED_PACKAGES:
if pkg in found:
continue
version = _find_version_for(
pkg, lock_content, pyproject_content, requirements_content
)
if version is not None:
found[pkg] = version
return [f"{pkg}:{found[pkg]}" for pkg in TRACKED_PACKAGES if pkg in found]
+63 -1
View File
@@ -20,6 +20,7 @@ from dotenv import dotenv_values, set_key
import langgraph_cli.config
from langgraph_cli.analytics import log_command
from langgraph_cli.constants import DEFAULT_CONFIG
from langgraph_cli.dependency_tracking import find_tracked_packages
from langgraph_cli.docker import build_docker_image, can_build_locally
from langgraph_cli.exec import Runner, subp_exec
from langgraph_cli.host_backend import HostBackendClient, HostBackendError
@@ -849,6 +850,41 @@ def _upload_to_gcs(signed_url: str, file_path: str, file_size: int) -> None:
# ---------------------------------------------------------------------------
def _resolve_pushed_image_digest(
runner,
*,
remote_image: str,
docker_config_dir: str | None,
verbose: bool,
) -> str:
"""Return ``{registry}/{repo}@sha256:<hex>`` for a freshly-pushed image.
Reads ``RepoDigests`` via ``docker image inspect`` — the local daemon
records the registry's manifest digest there after a successful push.
Falls back to ``remote_image`` with a warning if no matching digest is
found, rather than failing the deploy.
"""
# rsplit preserves ``:port`` in the registry host.
repo_no_tag = remote_image.rsplit(":", 1)[0]
args: list[str] = ["docker"]
if docker_config_dir:
args += ["--config", docker_config_dir]
args += ["image", "inspect", "--format", "{{json .RepoDigests}}", remote_image]
stdout, _ = runner.run(subp_exec(*args, collect=True, verbose=verbose))
try:
digests = json_mod.loads(stdout or "[]") or []
except json_mod.JSONDecodeError:
digests = []
for d in digests:
if isinstance(d, str) and d.startswith(f"{repo_no_tag}@sha256:"):
return d
_get_emitter().warn(
f"Could not resolve image digest for {remote_image}; "
"falling back to the tag-based reference. Re-run with --verbose for details."
)
return remote_image
def _run_local_build(
*,
client: HostBackendClient,
@@ -867,6 +903,7 @@ def _run_local_build(
build_command: str | None,
docker_build_args: Sequence[str],
secrets: list[dict[str, str]],
tracked_packages: list[str] | None,
) -> BuildResult:
"""Build locally with Docker, push to registry, update deployment."""
# Use buildx to cross-compile for amd64 when running on a non-x86_64 host
@@ -1015,9 +1052,21 @@ def _run_local_build(
raise
step += 1
resolved_image = _resolve_pushed_image_digest(
runner,
remote_image=remote_image,
docker_config_dir=None,
verbose=verbose,
)
# -- Step: Update deployment --
_log_deploy_step(step, f"Updating deployment {deployment_id}")
updated = client.update_deployment(deployment_id, remote_image, secrets=secrets)
updated = client.update_deployment(
deployment_id,
resolved_image,
secrets=secrets,
tracked_packages=tracked_packages,
)
return BuildResult(
updated=updated if isinstance(updated, dict) else {},
@@ -1039,6 +1088,7 @@ def _run_remote_build(
install_command: str | None,
build_command: str | None,
secrets: list[dict[str, str]],
tracked_packages: list[str] | None,
) -> BuildResult:
"""Upload source tarball and trigger a remote build."""
from langgraph_cli.archive import create_archive
@@ -1069,6 +1119,7 @@ def _run_remote_build(
secrets=secrets,
install_command=install_command,
build_command=build_command,
tracked_packages=tracked_packages,
)
log_offset: str | None = None
@@ -1553,6 +1604,15 @@ def _deploy_cmd(
if not deployment_id:
raise click.ClickException("Failed to determine deployment ID")
# Scan local sources for tracked packages so the new revision carries
# the same metadata GitHub-backed deploys produce. Failures must never
# block a deploy.
try:
tracked_packages = find_tracked_packages(config, config_json) or None
except Exception as exc:
em.warn(f"Skipped tracked-package scan: {exc}")
tracked_packages = None
# -- 3. Build (divergent path) --
if use_remote_build:
build_result = _run_remote_build(
@@ -1565,6 +1625,7 @@ def _deploy_cmd(
install_command=install_command,
build_command=build_command,
secrets=secrets,
tracked_packages=tracked_packages,
)
else:
build_result = _run_local_build(
@@ -1584,6 +1645,7 @@ def _deploy_cmd(
build_command=build_command,
docker_build_args=docker_build_args,
secrets=secrets,
tracked_packages=tracked_packages,
)
# -- 4. Shared wait + result --
+6
View File
@@ -122,11 +122,14 @@ class HostBackendClient:
deployment_id: str,
image_uri: str,
secrets: list[dict[str, str]] | None = None,
tracked_packages: list[str] | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"revision_source": "internal_docker",
"source_revision_config": {"image_uri": image_uri},
}
if tracked_packages:
payload["tracked_packages"] = tracked_packages
if secrets is not None:
payload["secrets"] = secrets
return self._request(
@@ -143,6 +146,7 @@ class HostBackendClient:
secrets: list[dict[str, str]] | None = None,
install_command: str | None = None,
build_command: str | None = None,
tracked_packages: list[str] | None = None,
) -> dict[str, Any]:
"""Trigger a remote build revision with the uploaded tarball."""
payload: dict[str, Any] = {
@@ -152,6 +156,8 @@ class HostBackendClient:
"langgraph_config_path": config_path,
},
}
if tracked_packages:
payload["tracked_packages"] = tracked_packages
source_config: dict[str, Any] = {}
if install_command is not None:
+1 -1
View File
@@ -23,7 +23,7 @@ dependencies = [
path = "langgraph_cli/__init__.py"
[project.optional-dependencies]
inmem = [
"langgraph-api>=0.5.35,<0.9.0 ; python_version >= '3.11'",
"langgraph-api>=0.5.35,<0.10.0 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.7 ; python_version >= '3.11'",
]
@@ -0,0 +1,134 @@
import pathlib
import pytest
from langgraph_cli.dependency_tracking import (
TRACKED_PACKAGES,
find_tracked_packages,
)
def _write_project(
tmp_path: pathlib.Path,
*,
dep_subdir: str = ".",
uv_lock: str | None = None,
pyproject: str | None = None,
requirements: str | None = None,
dependencies: list[str] | None = None,
) -> tuple[pathlib.Path, dict]:
project_root = tmp_path
dep_dir = (project_root / dep_subdir).resolve()
dep_dir.mkdir(parents=True, exist_ok=True)
if uv_lock is not None:
(dep_dir / "uv.lock").write_text(uv_lock)
if pyproject is not None:
(dep_dir / "pyproject.toml").write_text(pyproject)
if requirements is not None:
(dep_dir / "requirements.txt").write_text(requirements)
config = project_root / "langgraph.json"
config.write_text("{}")
config_json = {"dependencies": dependencies or [dep_subdir]}
return config, config_json
def test_uv_lock_resolved_version_preferred(tmp_path: pathlib.Path) -> None:
config, config_json = _write_project(
tmp_path,
uv_lock='name = "google-adk"\nversion = "1.2.3"\n',
pyproject='dependencies = ["google-adk>=0.5"]',
)
assert find_tracked_packages(config, config_json) == ["google-adk:1.2.3"]
def test_pyproject_specifier_used_when_no_lock(tmp_path: pathlib.Path) -> None:
config, config_json = _write_project(
tmp_path,
pyproject='dependencies = ["google-adk>=0.5,<2"]',
)
assert find_tracked_packages(config, config_json) == ["google-adk:>=0.5,<2"]
def test_requirements_txt_specifier(tmp_path: pathlib.Path) -> None:
config, config_json = _write_project(
tmp_path,
requirements="google-adk==1.0.0\n",
)
assert find_tracked_packages(config, config_json) == ["google-adk:==1.0.0"]
def test_bare_reference_records_unknown(tmp_path: pathlib.Path) -> None:
config, config_json = _write_project(
tmp_path,
requirements="google-adk\nother-pkg==1.0\n",
)
assert find_tracked_packages(config, config_json) == ["google-adk:unknown"]
def test_extras_bracket_records_unknown(tmp_path: pathlib.Path) -> None:
config, config_json = _write_project(
tmp_path,
pyproject='dependencies = ["deployments-wrap-sdk[google-adk]>=0.0.1"]',
)
assert find_tracked_packages(config, config_json) == ["google-adk:unknown"]
def test_no_match_returns_empty(tmp_path: pathlib.Path) -> None:
config, config_json = _write_project(
tmp_path,
pyproject='dependencies = ["langgraph>=0.2"]',
)
assert find_tracked_packages(config, config_json) == []
def test_traversal_dep_path_is_skipped(tmp_path: pathlib.Path) -> None:
outside = tmp_path.parent / "outside-project"
outside.mkdir(exist_ok=True)
(outside / "uv.lock").write_text('name = "google-adk"\nversion = "9.9.9"\n')
project_root = tmp_path / "project"
project_root.mkdir()
config = project_root / "langgraph.json"
config.write_text("{}")
config_json = {"dependencies": ["../outside-project"]}
assert find_tracked_packages(config, config_json) == []
def test_dep_paths_scanned_in_order(tmp_path: pathlib.Path) -> None:
project_root = tmp_path
(project_root / "first").mkdir()
(project_root / "second").mkdir()
(project_root / "second" / "uv.lock").write_text(
'name = "google-adk"\nversion = "2.0.0"\n'
)
config = project_root / "langgraph.json"
config.write_text("{}")
config_json = {"dependencies": ["first", "second"]}
assert find_tracked_packages(config, config_json) == ["google-adk:2.0.0"]
def test_non_string_dep_entry_ignored(tmp_path: pathlib.Path) -> None:
project_root = tmp_path
config = project_root / "langgraph.json"
config.write_text("{}")
config_json = {"dependencies": [123, None]}
assert find_tracked_packages(config, config_json) == []
def test_oversized_file_is_truncated_not_raised(tmp_path: pathlib.Path) -> None:
project_root = tmp_path
config = project_root / "langgraph.json"
config.write_text("{}")
# 6 MB of irrelevant content followed by the tracked-package marker —
# the read cap drops the marker, so nothing should be found.
padded = ("x" * (6 * 1024 * 1024)) + '\nname = "google-adk"\nversion = "1.0.0"\n'
(project_root / "uv.lock").write_text(padded)
assert find_tracked_packages(config, {"dependencies": ["."]}) == []
@pytest.mark.parametrize("pkg", TRACKED_PACKAGES)
def test_every_tracked_package_is_detectable(tmp_path: pathlib.Path, pkg: str) -> None:
config, config_json = _write_project(
tmp_path,
uv_lock=f'name = "{pkg}"\nversion = "1.0.0"\n',
)
assert find_tracked_packages(config, config_json) == [f"{pkg}:1.0.0"]
@@ -3,6 +3,7 @@ import io
import json
import os
import sys
from unittest.mock import MagicMock
import click
import httpx
@@ -16,6 +17,7 @@ from langgraph_cli.deploy import (
_env_without_deployment_name,
_parse_env_from_config,
_resolve_env_path,
_resolve_pushed_image_digest,
_smith_dashboard_base_url,
normalize_image_tag,
normalize_name,
@@ -532,3 +534,156 @@ class TestSmithDashboardBaseUrl:
_smith_dashboard_base_url("https://custom.example.com")
== "https://smith.langchain.com"
)
class TestResolvePushedImageDigest:
"""Tests for ``_resolve_pushed_image_digest`` — runner is mocked to
return the ``(stdout, stderr)`` tuple that ``subp_exec(collect=True)``
would produce.
"""
@staticmethod
def _runner(stdout: str | None) -> MagicMock:
# Close the unawaited subp_exec coroutine to silence gc warnings.
runner = MagicMock()
def _run(coro, *args, **kwargs):
if hasattr(coro, "close"):
coro.close()
return (stdout, "")
runner.run.side_effect = _run
return runner
def test_happy_path_returns_digest(self):
runner = self._runner('["us-central1-docker.pkg.dev/proj/repo@sha256:abc123"]')
out = _resolve_pushed_image_digest(
runner,
remote_image="us-central1-docker.pkg.dev/proj/repo:latest",
docker_config_dir=None,
verbose=False,
)
assert out == "us-central1-docker.pkg.dev/proj/repo@sha256:abc123"
def test_filters_to_matching_repo(self):
# Same image ID can hold digests for multiple repos — pick the one
# matching the just-pushed repo.
runner = self._runner(
json.dumps(
[
"other-registry.example.com/some/repo@sha256:000000",
"us-central1-docker.pkg.dev/proj/repo@sha256:abc123",
]
)
)
out = _resolve_pushed_image_digest(
runner,
remote_image="us-central1-docker.pkg.dev/proj/repo:v1.2.3",
docker_config_dir=None,
verbose=False,
)
assert out == "us-central1-docker.pkg.dev/proj/repo@sha256:abc123"
def test_empty_repodigests_falls_back_with_warning(self, mocker):
emitter = mocker.MagicMock()
mocker.patch("langgraph_cli.deploy._get_emitter", return_value=emitter)
runner = self._runner("[]")
remote = "us-central1-docker.pkg.dev/proj/repo:latest"
out = _resolve_pushed_image_digest(
runner,
remote_image=remote,
docker_config_dir=None,
verbose=False,
)
assert out == remote
assert emitter.warn.called
assert remote in emitter.warn.call_args.args[0]
def test_null_repodigests_falls_back_with_warning(self, mocker):
# ``docker inspect --format '{{json .RepoDigests}}'`` emits ``null``
# when the field is absent.
emitter = mocker.MagicMock()
mocker.patch("langgraph_cli.deploy._get_emitter", return_value=emitter)
runner = self._runner("null")
remote = "us-central1-docker.pkg.dev/proj/repo:latest"
out = _resolve_pushed_image_digest(
runner,
remote_image=remote,
docker_config_dir=None,
verbose=False,
)
assert out == remote
assert emitter.warn.called
def test_no_matching_repo_falls_back_with_warning(self, mocker):
# No matching digest for the pushed repo — warn and fall back to the
# tag-based ref rather than failing the deploy.
emitter = mocker.MagicMock()
mocker.patch("langgraph_cli.deploy._get_emitter", return_value=emitter)
runner = self._runner('["other-registry.example.com/some/repo@sha256:000000"]')
remote = "us-central1-docker.pkg.dev/proj/repo:latest"
out = _resolve_pushed_image_digest(
runner,
remote_image=remote,
docker_config_dir=None,
verbose=False,
)
assert out == remote
assert emitter.warn.called
def test_registry_with_port_in_host(self):
# Only the rightmost ``:`` (the ``:latest`` tag) should be stripped.
runner = self._runner('["localhost:5000/repo@sha256:deadbeef"]')
out = _resolve_pushed_image_digest(
runner,
remote_image="localhost:5000/repo:latest",
docker_config_dir=None,
verbose=False,
)
assert out == "localhost:5000/repo@sha256:deadbeef"
@staticmethod
def _capturing_runner(stdout: str) -> tuple[MagicMock, dict]:
"""Like ``_runner`` but exposes the coroutine for arg introspection.
Caller must close ``captured["coro"]``.
"""
runner = MagicMock()
captured: dict = {}
def _run(coro, *args, **kwargs):
captured["coro"] = coro
return (stdout, "")
runner.run.side_effect = _run
return runner, captured
def test_passes_docker_config_dir(self):
runner, captured = self._capturing_runner(
'["us-central1-docker.pkg.dev/proj/repo@sha256:abc"]'
)
_resolve_pushed_image_digest(
runner,
remote_image="us-central1-docker.pkg.dev/proj/repo:latest",
docker_config_dir="/tmp/some-cfg",
verbose=False,
)
frame_locals = captured["coro"].cr_frame.f_locals
assert frame_locals["cmd"] == "docker"
assert "--config" in frame_locals["args"]
cfg_idx = frame_locals["args"].index("--config")
assert frame_locals["args"][cfg_idx + 1] == "/tmp/some-cfg"
captured["coro"].close()
def test_omits_docker_config_dir_when_none(self):
runner, captured = self._capturing_runner(
'["us-central1-docker.pkg.dev/proj/repo@sha256:abc"]'
)
_resolve_pushed_image_digest(
runner,
remote_image="us-central1-docker.pkg.dev/proj/repo:latest",
docker_config_dir=None,
verbose=False,
)
frame_locals = captured["coro"].cr_frame.f_locals
assert "--config" not in frame_locals["args"]
captured["coro"].close()
@@ -1,3 +1,5 @@
import json
import httpx
import pytest
@@ -176,6 +178,69 @@ def test_update_deployment_no_secrets(client):
assert result == {"ok": True}
def _capturing_client(captured: dict) -> HostBackendClient:
def handler(req: httpx.Request) -> httpx.Response:
captured["body"] = req.read()
return httpx.Response(200, json={"ok": True})
c = HostBackendClient("https://api.example.com", "key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "key", "Accept": "application/json"},
timeout=30,
)
return c
def test_update_deployment_forwards_tracked_packages():
captured: dict = {}
c = _capturing_client(captured)
c.update_deployment(
"dep-123",
"image:latest",
tracked_packages=["google-adk:1.0.0"],
)
body = json.loads(captured["body"])
assert body["tracked_packages"] == ["google-adk:1.0.0"]
assert "tracked_packages" not in body["source_revision_config"]
def test_update_deployment_omits_tracked_packages_when_absent():
captured: dict = {}
c = _capturing_client(captured)
c.update_deployment("dep-123", "image:latest")
body = json.loads(captured["body"])
assert "tracked_packages" not in body
def test_update_deployment_internal_source_forwards_tracked_packages():
captured: dict = {}
c = _capturing_client(captured)
c.update_deployment_internal_source(
"dep-123",
source_tarball_path="path/to/tarball",
config_path="langgraph.json",
tracked_packages=["google-adk:>=0.5"],
)
body = json.loads(captured["body"])
assert body["tracked_packages"] == ["google-adk:>=0.5"]
assert body["source_revision_config"]["source_tarball_path"] == "path/to/tarball"
assert "tracked_packages" not in body["source_revision_config"]
def test_update_deployment_internal_source_omits_tracked_packages_when_absent():
captured: dict = {}
c = _capturing_client(captured)
c.update_deployment_internal_source(
"dep-123",
source_tarball_path="path/to/tarball",
config_path="langgraph.json",
)
body = json.loads(captured["body"])
assert "tracked_packages" not in body
def test_list_revisions(client):
result = client.list_revisions("dep-123", limit=5)
assert result == {"ok": True}
@@ -7,6 +7,7 @@ dependencies = [
"langgraph>=0.6.0,<2",
"langchain-core>=0.2.14",
"shared",
"langgraph-checkpoint-postgres>=3.0.0"
]
[build-system]
@@ -6,6 +6,7 @@ requires-python = ">=3.11"
dependencies = [
"langgraph>=0.6.0,<2",
"langchain-core>=1.3.3",
"langgraph-checkpoint-postgres>=3.0.0"
]
[tool.uv.workspace]
+61 -8
View File
@@ -16,13 +16,15 @@ source = { editable = "apps/agent" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph" },
{ name = "langgraph-checkpoint-postgres" },
{ name = "shared" },
]
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=1.3.3" },
{ name = "langchain-core", specifier = ">=0.2.14" },
{ name = "langgraph", specifier = ">=0.6.0,<2" },
{ name = "langgraph-checkpoint-postgres", specifier = ">=3.0.0" },
{ name = "shared", editable = "libs/shared" },
]
@@ -275,6 +277,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" },
]
[[package]]
name = "langgraph-checkpoint-postgres"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langgraph-checkpoint" },
{ name = "orjson" },
{ name = "psycopg" },
{ name = "psycopg-pool" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7a/8f439966643d32111248a225e6cb33a182d07c90de780c4dbfc1e0377832/langgraph_checkpoint_postgres-3.0.5.tar.gz", hash = "sha256:a8fd7278a63f4f849b5cbc7884a15ca8f41e7d5f7467d0a66b31e8c24492f7eb", size = 127856, upload-time = "2026-03-18T21:25:29.785Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/87/b0f98b33a67204bca9d5619bcd9574222f6b025cf3c125eedcec9a50ecbc/langgraph_checkpoint_postgres-3.0.5-py3-none-any.whl", hash = "sha256:86d7040a88fd70087eaafb72251d796696a0a2d856168f5c11ef620771411552", size = 42907, upload-time = "2026-03-18T21:25:28.75Z" },
]
[[package]]
name = "langgraph-prebuilt"
version = "1.0.9"
@@ -303,7 +320,7 @@ wheels = [
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -316,9 +333,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[[package]]
@@ -446,6 +463,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "psycopg"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
]
[[package]]
name = "psycopg-pool"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
@@ -676,12 +718,21 @@ wheels = [
]
[[package]]
name = "urllib3"
version = "2.6.3"
name = "tzdata"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
@@ -720,12 +771,14 @@ source = { virtual = "." }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph" },
{ name = "langgraph-checkpoint-postgres" },
]
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=1.3.3" },
{ name = "langgraph", specifier = ">=0.6.0,<2" },
{ name = "langgraph-checkpoint-postgres", specifier = ">=3.0.0" },
]
[[package]]
+417 -306
View File
@@ -26,11 +26,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.2.25"
version = "2026.5.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
]
[[package]]
@@ -161,11 +161,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
]
[[package]]
@@ -191,7 +191,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.3.3"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -204,9 +204,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/8b74458fc3850ec3d150eb9f45e857db129dafa801fb5cf173dfc9f8bbf3/langchain_core-1.3.3.tar.gz", hash = "sha256:fa510a5db8efdc0c6ff41c0939fb5c00a0183c11f6b84233e892e3227ff69182", size = 915041, upload-time = "2026-05-05T19:02:36.612Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/01/4771b7ab2af1d1aba5b710bd8f13d9225c609425214b357590a17b01be77/langchain_core-1.3.3-py3-none-any.whl", hash = "sha256:18aae8506f37da7f74398492279a7d6efcee4f8e23c4c41c7af080eeb7ef7bd1", size = 543857, upload-time = "2026-05-05T19:02:34.52Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
]
[[package]]
@@ -223,7 +223,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.1.6"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -233,53 +233,53 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/e5/d3f72ead3c7f15769d5a9c07e373628f1fbaf6cbe7735694d7085859acf6/langgraph-1.1.6.tar.gz", hash = "sha256:1783f764b08a607e9f288dbcf6da61caeb0dd40b337e5c9fb8b412341fbc0b60", size = 549634, upload-time = "2026-04-03T19:01:32.561Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/e6/b36ecdb3ff4ba9a290708d514bae89ebbe2f554b6abbe4642acf3fddbe51/langgraph-1.1.6-py3-none-any.whl", hash = "sha256:fdbf5f54fa5a5a4c4b09b7b5e537f1b2fa283d2f0f610d3457ddeecb479458b9", size = 169755, upload-time = "2026-04-03T19:01:30.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "ormsgpack" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" }
sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" },
]
[[package]]
name = "langgraph-prebuilt"
version = "1.0.9"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph-checkpoint" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/4c/06dac899f4945bedb0c3a1583c19484c2cc894114ea30d9a538dd270086e/langgraph_prebuilt-1.0.9.tar.gz", hash = "sha256:93de7512e9caade4b77ead92428f6215c521fdb71b8ffda8cd55f0ad814e64de", size = 165850, upload-time = "2026-04-03T14:06:37.721Z" }
sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a2/8368ac187b75e7f9d938ca075d34f116683f5cfc48d924029ee79aea147b/langgraph_prebuilt-1.0.9-py3-none-any.whl", hash = "sha256:776c8e3154a5aef5ad0e5bf3f263f2dcaab3983786cc20014b7f955d99d2d1b2", size = 35958, upload-time = "2026-04-03T14:06:36.58Z" },
{ url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" },
]
[[package]]
name = "langgraph-sdk"
version = "0.3.13"
version = "0.3.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "orjson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/db/77a45127dddcfea5e4256ba916182903e4c31dc4cfca305b8c386f0a9e53/langgraph_sdk-0.3.13.tar.gz", hash = "sha256:419ca5663eec3cec192ad194ac0647c0c826866b446073eb40f384f950986cd5", size = 196360, upload-time = "2026-04-07T20:34:18.766Z" }
sdist = { url = "https://files.pythonhosted.org/packages/66/af/cdd4d6f3c05b3c1112ed3f12ef830faf15951b21d22cbc622a4becbbe25c/langgraph_sdk-0.3.15.tar.gz", hash = "sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de", size = 201924, upload-time = "2026-05-22T16:54:27.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/ef/64d64e9f8eea47ce7b939aa6da6863b674c8d418647813c20111645fcc62/langgraph_sdk-0.3.13-py3-none-any.whl", hash = "sha256:aee09e345c90775f6de9d6f4c7b847cfc652e49055c27a2aed0d981af2af3bd0", size = 96668, upload-time = "2026-04-07T20:34:17.866Z" },
{ url = "https://files.pythonhosted.org/packages/be/a5/0196d9c05749c25bc198e4909d68c998bc3120297e14944921baf2f4c384/langgraph_sdk-0.3.15-py3-none-any.whl", hash = "sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d", size = 98166, upload-time = "2026-05-22T16:54:26.013Z" },
]
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -292,77 +292,77 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/17/eb/8883d1158c743d0aac350f09df7880714d27283497e8c80bb9fe3480f165/langsmith-0.8.5.tar.gz", hash = "sha256:3615243d99c12f4047f13042bdc05a373dce232d106a6511b3ca7b48c5af1c2c", size = 4462348, upload-time = "2026-05-15T21:31:41.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/23/85/968c88a63e32a59b3e5c68afd2fe114ce0708a125db0be1a85efc25fb2ea/langsmith-0.8.5-py3-none-any.whl", hash = "sha256:efc779f9d450dcaf9d97bc8894f4926276509d6e730e05289af9a64debce06ae", size = 399564, upload-time = "2026-05-15T21:31:39.046Z" },
]
[[package]]
name = "orjson"
version = "3.11.8"
version = "3.11.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" },
{ url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" },
{ url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" },
{ url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" },
{ url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" },
{ url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" },
{ url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" },
{ url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" },
{ url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" },
{ url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" },
{ url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" },
{ url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" },
{ url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" },
{ url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" },
{ url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" },
{ url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" },
{ url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" },
{ url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" },
{ url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" },
{ url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" },
{ url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" },
{ url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" },
{ url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" },
{ url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" },
{ url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" },
{ url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" },
{ url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" },
{ url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" },
{ url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" },
{ url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" },
{ url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" },
{ url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" },
{ url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" },
{ url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" },
{ url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" },
{ url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" },
{ url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" },
{ url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" },
{ url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" },
{ url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" },
{ url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" },
{ url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" },
{ url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" },
{ url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" },
{ url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" },
{ url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" },
{ url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" },
{ url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" },
{ url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" },
{ url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" },
{ url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" },
{ url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" },
{ url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" },
{ url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" },
{ url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" },
{ url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" },
{ url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" },
{ url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" },
{ url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" },
{ url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" },
{ url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" },
{ url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" },
{ url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" },
{ url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" },
{ url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" },
{ url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" },
{ url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" },
{ url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" },
{ url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" },
{ url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" },
{ url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" },
{ url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" },
{ url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" },
{ url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" },
{ url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" },
{ url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" },
{ url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" },
{ url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" },
{ url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" },
{ url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" },
{ url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" },
{ url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" },
{ url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" },
{ url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" },
{ url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" },
{ url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" },
{ url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" },
{ url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" },
{ url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" },
{ url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" },
{ url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" },
{ url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" },
{ url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" },
{ url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" },
{ url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" },
{ url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" },
{ url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" },
{ url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" },
{ url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" },
{ url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" },
{ url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" },
{ url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" },
{ url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" },
{ url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" },
{ url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" },
{ url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" },
{ url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" },
{ url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" },
{ url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" },
{ url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" },
]
[[package]]
@@ -415,16 +415,16 @@ wheels = [
[[package]]
name = "packaging"
version = "26.0"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
version = "2.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -432,106 +432,111 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
@@ -591,7 +596,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.33.1"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -599,9 +604,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
@@ -663,143 +668,249 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.6.3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
name = "uuid-utils"
version = "0.14.1"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" }
sdist = { url = "https://files.pythonhosted.org/packages/01/a1/822ceef22d1c139cffebe4b1b660cfaa10253d5c770aa2598dc8e9497593/uuid_utils-0.16.0.tar.gz", hash = "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7", size = 42596, upload-time = "2026-05-19T07:44:23.28Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" },
{ url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" },
{ url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" },
{ url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" },
{ url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" },
{ url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" },
{ url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" },
{ url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" },
{ url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" },
{ url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" },
{ url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" },
{ url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" },
{ url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" },
{ url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" },
{ url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" },
{ url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" },
{ url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" },
{ url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" },
{ url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" },
{ url = "https://files.pythonhosted.org/packages/24/24/0e18177e2fbb0b9f54f90fd48fe3302dfda731e22ad650d6e6f8f4b3d3d3/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04af9966ecd82b78eeba5725e29aa1e86fb8eb84b5443dd6a9935f9fadb6678e", size = 565929, upload-time = "2026-05-19T07:44:06.496Z" },
{ url = "https://files.pythonhosted.org/packages/5a/7e/bb91b04b2c8a081a4df2d50f1a50dd85502e2391c6eaed71b339ec9f2524/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3d86ca394e0ea21bdb53784eb99276d263b93d1586f56678cab1414b7ae1d0f3", size = 290556, upload-time = "2026-05-19T07:43:44.973Z" },
{ url = "https://files.pythonhosted.org/packages/69/2a/47ee18b294af59754ef5acfa96eb027137c98cef7521199b6f70be705de4/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f504efeb20ffd9571621658f7c8093c646d33150406d5742e49ff7cd861615", size = 328059, upload-time = "2026-05-19T07:45:30.533Z" },
{ url = "https://files.pythonhosted.org/packages/89/7c/ed6d8bb48eeecaed6722af1187d722c5243334be750419d10d5f05dffeb2/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d85f48535dc541060f6b82f277cbcd12b78c04008ccc1039546cfcec027327", size = 334759, upload-time = "2026-05-19T07:45:07.715Z" },
{ url = "https://files.pythonhosted.org/packages/ff/33/371bddf9fd47e045c375df9668eea0d96ce9201ab6a03985b0155498e376/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39453f1ebf4398fbeb71607f3437e2ac469c9e38b5921755c1e17ad0158a8907", size = 448927, upload-time = "2026-05-19T07:45:11.464Z" },
{ url = "https://files.pythonhosted.org/packages/dc/f1/b201d5ee005d4987fc072714fcb9f6e75303520cf19d4deec0b4df44bf40/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50361aca5c2a770728a6343df85109fe57f89ac026827f34fe0153563cdc9ce7", size = 327178, upload-time = "2026-05-19T07:44:02.255Z" },
{ url = "https://files.pythonhosted.org/packages/b1/6a/04b4c02ce5c24a3602baa12e59bd3ec853ae73c3e9319b706c4620f47a05/uuid_utils-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:948485c47d8569a8bf6e86f522a2599fa9134674bee9f483898e601e68c3caca", size = 352981, upload-time = "2026-05-19T07:44:25.578Z" },
{ url = "https://files.pythonhosted.org/packages/2c/19/25db019727d14630c75c2a75a8ea66dd712bb468adcf410bac8d01ff19fd/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceef237cf8467fddbf6d8466cc1f6e2c04605ec919046ef5eba10a895b559fcf", size = 504686, upload-time = "2026-05-19T07:43:46.43Z" },
{ url = "https://files.pythonhosted.org/packages/5d/93/c000cd42ebfdd37cc74981ed31c979a1270156572bdebab8b5d61460e750/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:24e6fa0d0ade7a9ad60a3c296022474983243df5b4e863babb4828a85ef2e52c", size = 610102, upload-time = "2026-05-19T07:45:53.765Z" },
{ url = "https://files.pythonhosted.org/packages/15/1d/7dd239909c82616722b9ee53fa1b4657c6244fb4fd026890300ebf6db22b/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1c2df42314b014c9d23330f92887e21d2fc72fde0beb170c7833cd2d22d845a1", size = 569048, upload-time = "2026-05-19T07:45:41.596Z" },
{ url = "https://files.pythonhosted.org/packages/f1/49/b6a688648368a9cc0137e183657956853a91dc06ef73deda27290d586155/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e2f369dd734050fe96ae4905c58779b09276d47d5e9a0e5cd33ec7982784341", size = 532255, upload-time = "2026-05-19T07:45:16.936Z" },
{ url = "https://files.pythonhosted.org/packages/3f/fb/34f221ae93d5ea249a0d7056bdf45313b8d267d6aa9c5d0673ac1a4746c7/uuid_utils-0.16.0-cp311-cp311-win32.whl", hash = "sha256:733da81d51ea578862d8b9b754e8968b6da2be2b7840aee868917c23cae84015", size = 171081, upload-time = "2026-05-19T07:45:26.578Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/c2a608a813f655834ee6df4ce53ea46edad4d54f774eac1890be5c7e4e1c/uuid_utils-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:10d21fddb086e69245c4f0f77c7b442471f3a242aa85f62954bff157baa1c5f2", size = 176770, upload-time = "2026-05-19T07:43:49.102Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c3/8ab4eff328a833c065f280b2e0d9ac873505b5e5282f2bc5133a9843d4dd/uuid_utils-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:98e2404713677070cee9a99a1f1e24afd496c18e833ee1b31a0587659452ff80", size = 175274, upload-time = "2026-05-19T07:44:27.216Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4c/b4cf43a5d22bcdb91727acdf54be0d78e83e595b73c5a9a8a4291875f059/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0", size = 562183, upload-time = "2026-05-19T07:45:02.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/fb/4b0d1c4b5e9f8679ca41b9cdbce5749e1d5db3d3d42a07060d6ce61ac583/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66a9c8cedf7695c28e700f6a66bde0809c3b2e0d8a70968be7bfd47c908952e5", size = 289018, upload-time = "2026-05-19T07:44:07.726Z" },
{ url = "https://files.pythonhosted.org/packages/de/43/2dc6c7401c8fab86e46b0b33ada6dcfde949b2fd48877ba6f880862be80e/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f", size = 326171, upload-time = "2026-05-19T07:45:25.186Z" },
{ url = "https://files.pythonhosted.org/packages/9b/f5/48f11fb91f36453611ca148bc441436f279870b1ec6b576dc5167fb6e680/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9", size = 332222, upload-time = "2026-05-19T07:45:09.036Z" },
{ url = "https://files.pythonhosted.org/packages/30/cb/b2b49528521e4a097f129e8bf7850a26f00af46afba778832cf3458a5c00/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247", size = 444801, upload-time = "2026-05-19T07:45:37.517Z" },
{ url = "https://files.pythonhosted.org/packages/a9/b3/a28d9c6f7c701dfe01c8020b30e33899a28eb9e4d056b07e7388f50ebf67/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d", size = 325594, upload-time = "2026-05-19T07:44:44.682Z" },
{ url = "https://files.pythonhosted.org/packages/cf/65/e1ff41dc44966e396ead86e104ba21b35ddb07ff7a64bb55013074ee77fe/uuid_utils-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b2e981b1258db444df4cf4bf4c79673570d081d48d35f22d0f86471e0ad795c5", size = 349312, upload-time = "2026-05-19T07:45:15.582Z" },
{ url = "https://files.pythonhosted.org/packages/ed/57/fb19b7951f66a46e03bd1943a61ee9d59c83e994e56e8c97d79aff1f0e47/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e", size = 502115, upload-time = "2026-05-19T07:43:38.756Z" },
{ url = "https://files.pythonhosted.org/packages/2f/8e/9a129c469b7b77afb62da5c6b7e92591073b845bd0c3108c0d0aa65389fb/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1", size = 607433, upload-time = "2026-05-19T07:44:11.675Z" },
{ url = "https://files.pythonhosted.org/packages/4a/56/2ef71fad168cc3d894f7094fa458086c093635d7835381c91470b19c9ad3/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:38126b353527c5f001e4b24db9e62351eb768d0367febcd68100a4b39a035109", size = 566076, upload-time = "2026-05-19T07:44:35.453Z" },
{ url = "https://files.pythonhosted.org/packages/95/bf/68e60ea053ca30f35df877b96001331398140d5c4983561affa1350331b1/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5", size = 530645, upload-time = "2026-05-19T07:45:49.278Z" },
{ url = "https://files.pythonhosted.org/packages/42/19/b521f7d73094fca4c0c44002f4a42bfcbcf0b770fdc3c4b9a596dda25734/uuid_utils-0.16.0-cp312-cp312-win32.whl", hash = "sha256:52d2cc8c12a3466cd1727883e0746d8bad5dddd670369eb553ba17fdc3b565ca", size = 168887, upload-time = "2026-05-19T07:45:45.502Z" },
{ url = "https://files.pythonhosted.org/packages/87/1f/4126c3ccbc2d98a613664e55f6ab6d7bd4b98424a04486e4fcc76549af15/uuid_utils-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97625e5edfda8b118160ce1e88756f92b1635775f836c168be7bf10928d97fa", size = 174607, upload-time = "2026-05-19T07:43:52.938Z" },
{ url = "https://files.pythonhosted.org/packages/74/62/b83ccc8446ae39dcc0bda2cb3b525b6af6a2036383afe1d1d5fe7b234c2c/uuid_utils-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:baf79c8050eb784b252dd34807df73f61130fe8676b61231baccab62530f20ec", size = 173021, upload-time = "2026-05-19T07:45:10.204Z" },
{ url = "https://files.pythonhosted.org/packages/60/9b/74c1f47a9b4f138a254e51528e5ffaeba6bf99ecead9f0c4b6fccccfbfcb/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4", size = 563166, upload-time = "2026-05-19T07:44:10.494Z" },
{ url = "https://files.pythonhosted.org/packages/7c/1c/009e37b70f1f0ff17e7103a36bafde33d503d9ea7fe739761aa3e3c9fde6/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0681d1bdb7956e0c6d581e7601dabcfb2b08c25d2a65189f4e9b102c94f5ff46", size = 289529, upload-time = "2026-05-19T07:43:54.466Z" },
{ url = "https://files.pythonhosted.org/packages/5e/5e/e0323d54321166639eb2be5e8a464f5cb0fc04d72d91f3e78944bb6a1da8/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0", size = 326328, upload-time = "2026-05-19T07:45:31.901Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a3/046f6cb958467c3bf4a163a8a53b178b64a62e21ed8ad5b2c1dacb3a2cfc/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b", size = 332322, upload-time = "2026-05-19T07:43:41.284Z" },
{ url = "https://files.pythonhosted.org/packages/67/80/01914e3949744db7acd0006885e5542fbebb6e39114857d007d29b3265c2/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e", size = 445787, upload-time = "2026-05-19T07:45:36.102Z" },
{ url = "https://files.pythonhosted.org/packages/14/ef/f6908f41279f205d70c8a0d5dcb25dd6802741d7f88e3f0123453c3584d3/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0", size = 324678, upload-time = "2026-05-19T07:45:12.77Z" },
{ url = "https://files.pythonhosted.org/packages/11/4a/bf841ba90f829c7779d82155e0f4b88ef6726ccc25507d064d50ac2cd329/uuid_utils-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:95b7f480010ea98a29ee809857a98aa923008c68129af1b39244adccff7377fb", size = 349704, upload-time = "2026-05-19T07:44:47.172Z" },
{ url = "https://files.pythonhosted.org/packages/e6/31/3b5c60172b8c57bf4ca485484b8e4edef550ca324f9287f1183be97422e2/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306", size = 502456, upload-time = "2026-05-19T07:45:00.821Z" },
{ url = "https://files.pythonhosted.org/packages/88/bf/3da8d497af80fd51d8bf85551c77ede67f07825924ec5987bf9b6031014a/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b", size = 607727, upload-time = "2026-05-19T07:44:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/bd/4e/7c8cf03ec15cd6f40e4cbab81b2b4a625461327f68c7971e54723280ec3e/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f235ac5827d74ac630cc87f29278cdaa5d2f273613a6e05bbd96df7aa4170776", size = 566204, upload-time = "2026-05-19T07:44:51.225Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5f/af955feae69cce7fd2121ca3f790ff4b85ad2e17b2149546f50753e1a047/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0", size = 529986, upload-time = "2026-05-19T07:45:57.85Z" },
{ url = "https://files.pythonhosted.org/packages/10/cf/3fec757e51bef10eb41ae8075f5442c60e85ff456b42d16a3063f5dc6c80/uuid_utils-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10", size = 98683, upload-time = "2026-05-19T07:44:16.369Z" },
{ url = "https://files.pythonhosted.org/packages/40/a7/cd1adbea7ef882a70db064c00cd93b12e11027b4cdd7ffd79e95c35fc3e3/uuid_utils-0.16.0-cp313-cp313-win32.whl", hash = "sha256:924a8de04460e4cf65998ad0b6568084f7c51740ebd3254d07a0bcde35a84af6", size = 168822, upload-time = "2026-05-19T07:44:24.09Z" },
{ url = "https://files.pythonhosted.org/packages/74/99/617ceb9e3a95b23837012740979baf71afad723b70daf34862da3f7c17a1/uuid_utils-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:5279bc7ab3c6683f1c67314695bee14d869015acbbc677bdb0015190fe753d16", size = 174967, upload-time = "2026-05-19T07:44:56.022Z" },
{ url = "https://files.pythonhosted.org/packages/d9/d8/148ae707bfc36d482e39db679c86b81bdce264d4feb9df5d40a03b7687e3/uuid_utils-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:61a9c4c26ad12ac66fa4bfd0fdb8494724fe7a5b98a9fcd43e78e2b388663dbb", size = 173142, upload-time = "2026-05-19T07:43:50.171Z" },
{ url = "https://files.pythonhosted.org/packages/21/05/ca6d60705e71fdeaa3431dad94e279a8213c5573cb2925e1aabf3dc0330a/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0", size = 564408, upload-time = "2026-05-19T07:44:38.351Z" },
{ url = "https://files.pythonhosted.org/packages/eb/8c/b9a0462c38535c1662acb1025768e2d626bee5ce9e1790bad6b5381162ea/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f1614572fd9345cdc3dde3f40c237345719fabca1aa87d2d87b321d523cfa34d", size = 289923, upload-time = "2026-05-19T07:45:19.611Z" },
{ url = "https://files.pythonhosted.org/packages/f2/33/a53afeef1a56051551a0f5a801e4bce411dd73c6a8c99bad16902651256d/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a", size = 325762, upload-time = "2026-05-19T07:45:18.261Z" },
{ url = "https://files.pythonhosted.org/packages/72/ca/4462a4f36365d7ee72d41e05e6bcfe127e861b073ab37c25b2c8a518317c/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965", size = 332359, upload-time = "2026-05-19T07:45:34.886Z" },
{ url = "https://files.pythonhosted.org/packages/c5/67/9d3373fa7c5a746fdecc64e30caf915c29eb632203508d87676f9243ed03/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71", size = 445483, upload-time = "2026-05-19T07:44:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/57/08/ce01aa6d897fc7f875844fe58cad0a542c8ebf089d9242b654b56260ecb8/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98", size = 326281, upload-time = "2026-05-19T07:44:59.677Z" },
{ url = "https://files.pythonhosted.org/packages/76/ef/2c719b2c26bb5b5e5061a1435c11ad2bd33ac3cd6d4cd0c7c3ac1d3396ed/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:caac9c8b1d50e8fbddc76e93bfefbef472978eb45adbfdb6289d578816992953", size = 350809, upload-time = "2026-05-19T07:45:28.076Z" },
{ url = "https://files.pythonhosted.org/packages/e0/9b/c1ed447328b32229cca38ac4c62d309eab006e5e9c4020e2056a175bc607/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c", size = 502088, upload-time = "2026-05-19T07:44:09.208Z" },
{ url = "https://files.pythonhosted.org/packages/c1/e0/8442f4efe7bde72f0b4ae5f675d0c7fbe209ad0b54718b8ddf43c46c6fae/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada", size = 607631, upload-time = "2026-05-19T07:44:19.384Z" },
{ url = "https://files.pythonhosted.org/packages/f1/1e/9a9fa261edf4c972f28ae83421377e3ab8dbd0bd7db58fd316e782d09a3b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1b0dcedf9266bf34a54d5cbe78648eaa627e02352f2a6923ed647530aea2f661", size = 567618, upload-time = "2026-05-19T07:43:58.478Z" },
{ url = "https://files.pythonhosted.org/packages/cc/f7/1bcfdb9d539bd42736dd6076470a42fbb5db23f79712c0a06aa0a3752f7b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71", size = 530971, upload-time = "2026-05-19T07:45:06.348Z" },
{ url = "https://files.pythonhosted.org/packages/24/0c/18945f417d6bb4d0dd2b7652fe36c58c4e83bcf593b9b326b83aa40b853a/uuid_utils-0.16.0-cp313-cp313t-win32.whl", hash = "sha256:7f8cf49c05d58523a0f977cb7f11afc05791a0fa164d7303b8365a34750638e7", size = 169369, upload-time = "2026-05-19T07:44:32.581Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cc/c0eb0c3fab2ed80d706369b750029143b53126809b77b36bcbb77da66bab/uuid_utils-0.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e99f9a8b2420b228faba23a637e96efaf5c6a678b2e225870f24431c82707f50", size = 175384, upload-time = "2026-05-19T07:45:56.623Z" },
{ url = "https://files.pythonhosted.org/packages/b7/77/50ac87b6e18b1c686f700aa38c9471a990683c6a955f71ac1a6677ed8145/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6853b627983aa1b4fd95aa52d9e87136eb94a7b3b7de0fbb1db8a498d457eeec", size = 564108, upload-time = "2026-05-19T07:43:55.609Z" },
{ url = "https://files.pythonhosted.org/packages/83/16/65046676de246bb5334d9f58aa96d2feb9fc347fda3556aaff7da1c2fc7a/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f44b65ae0c329843817d9c90e36a7a3c677b413bf407c99e67db874dac49dad3", size = 289967, upload-time = "2026-05-19T07:45:38.886Z" },
{ url = "https://files.pythonhosted.org/packages/91/d6/54fa988606a15dfd2028e925d8eb9c3ee6edbf1eb7692a67b37282880b56/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de8a365795a76f347f5622621c2bee543cffa0c70949f3ee093bdefc9d926dcc", size = 325835, upload-time = "2026-05-19T07:44:42.02Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1b/50622f967ceacea1f89fd065d9bfd395b51acb02cfb0a4ddc8fa9ff0c983/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:426a8c9af90242d879706ccf29da56f0b0712e7739fb0bbe16baacabc75596e2", size = 332607, upload-time = "2026-05-19T07:43:42.42Z" },
{ url = "https://files.pythonhosted.org/packages/12/f5/4059706be6617e2787e375ea52994ce3c3fa3920b7d4a9c8ebf7895681a5/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833bc4b3c3fc24be541f67b01b4a75b6b9942a9b7137395b4eb35435948bd6da", size = 444287, upload-time = "2026-05-19T07:43:37.106Z" },
{ url = "https://files.pythonhosted.org/packages/65/d5/f44b2710563da687a368f0ce4dcbd462dfb6708bcd46439d831991d595c7/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb5252d7c00d586077f10e169d6e6d0b0d0f806d8a085073f0d19b4737aef4e", size = 324949, upload-time = "2026-05-19T07:45:33.175Z" },
{ url = "https://files.pythonhosted.org/packages/3a/a7/a69e859e37d26c5603f0bc0ae481860f691224f140e5a832f325b804770d/uuid_utils-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b3377ce388fd7bf8d231ec9d1d4f58c8e87888ddea93581f60ed6f878a4f722", size = 349651, upload-time = "2026-05-19T07:43:59.998Z" },
{ url = "https://files.pythonhosted.org/packages/db/73/4139cd3ca7b81ea283c1c8769373e9b2008241c0744a8ffb25f0a1b31325/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:12b6310beb38adc173ec5dc89e98812fd7e3d98f87f3ef01d2ea6ecb5d87994f", size = 502326, upload-time = "2026-05-19T07:45:40.292Z" },
{ url = "https://files.pythonhosted.org/packages/cb/8c/858101583fbad1b3fa04da88b1f7170836aa0f00b4cb712063325c44466d/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a49b5a75497643479c919e2e537a4a36224ac3aaa0fada61b75d87024021ac3e", size = 607689, upload-time = "2026-05-19T07:44:48.355Z" },
{ url = "https://files.pythonhosted.org/packages/5e/bd/8f3d54a4763dd91ebd0f3d7b0c2ec434e4e0b1fc667b03a44d611a465ec6/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:63bfdf00be51b6b3b79275d6767d034ea5c7a0caa067a35d72861284100cb60a", size = 566214, upload-time = "2026-05-19T07:44:53.519Z" },
{ url = "https://files.pythonhosted.org/packages/54/76/4c9a8d9baaa243c7902d84dbba4d51b1ab51c379c66d3fd6368ff6933ecf/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7525bc59ac4579c32317d2493dd42cf134b9bb50cd0bc6a41dd9f77e4740dde6", size = 529989, upload-time = "2026-05-19T07:44:43.141Z" },
{ url = "https://files.pythonhosted.org/packages/6d/13/d32cea997f880cedde415730ce0e872ebfd7a040155ae0bbda70eccd208e/uuid_utils-0.16.0-cp314-cp314-win32.whl", hash = "sha256:fbcac6e6710aa2e4bfbb81762758e01470dc56d5048ba4253acc77c9833568ff", size = 169146, upload-time = "2026-05-19T07:45:46.655Z" },
{ url = "https://files.pythonhosted.org/packages/1c/19/9fc55172d8fe59e1f27a14d598b427fa508a7ebb35fa7b7b99c24fa0ef13/uuid_utils-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:d23fcaf37368a1647319187ef6f8b741bf079f033065899bc2d00a44b0a1214a", size = 175364, upload-time = "2026-05-19T07:45:55.335Z" },
{ url = "https://files.pythonhosted.org/packages/89/5d/fcd9226b715c5aa0638fcdd6deaf0de6c6c3c451c692cd76bfca810c6512/uuid_utils-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:ea3265f8e2b452a4870f3298cb1d183dc4e36a3682cbb264dbe46af31267e706", size = 173268, upload-time = "2026-05-19T07:44:31.19Z" },
{ url = "https://files.pythonhosted.org/packages/c1/64/97ec9af95e58b8187f2934008ffab26e1604d149e34fe01c388b0543a24f/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:99f8420c3ed59f89a086782ac197e257f4b1debb4545dffa90cf5db23f96c892", size = 564464, upload-time = "2026-05-19T07:44:40.856Z" },
{ url = "https://files.pythonhosted.org/packages/3e/6d/e4082f407484ac28923c0bf8e861e71d277118d8b7542d0a350340e45350/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:259bab73c241743d684dcc3507feb76f484d720545e4e4805582aeff8e19700b", size = 290087, upload-time = "2026-05-19T07:44:01.084Z" },
{ url = "https://files.pythonhosted.org/packages/8c/43/c5c5f273c0ff889f20f10344784f9197dd00eb81ccc294330d4b949fea7e/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:897e8ef0dc5e4ac0b17cf9cae84bb41e560d806280ec5b93db7475b504022105", size = 325532, upload-time = "2026-05-19T07:43:47.508Z" },
{ url = "https://files.pythonhosted.org/packages/13/7f/669aa899ab5378374d28a28231e6978f739921a1af394c7ebd6cc86e2639/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5af79cde16a7600dfccb7d431aec0afd3088ff170b6a09887bf3f7ab3cc7c81", size = 332209, upload-time = "2026-05-19T07:43:51.528Z" },
{ url = "https://files.pythonhosted.org/packages/2b/57/a2a32406d79a222794ef98a19254fd9a81a029a0f32d7740fba9873bff1f/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bece1a6f677ca36047442c465d8166643eed9818b9e43e0bf42d3cf73e92dcff", size = 445507, upload-time = "2026-05-19T07:44:20.541Z" },
{ url = "https://files.pythonhosted.org/packages/26/6b/85459a35bfa7d73e79acbc4eab1cf6aa6e4d9d022c3260ed9dea539c7f0b/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3444498e7b099499c8a607d7771377020fa55f7274e46f54106af19f752d7", size = 326154, upload-time = "2026-05-19T07:45:23.587Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/e965efdbb503ed14d6e57aec1a22b98326ed24cc2fb48e750c4d192267a0/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:542098f6cb6874aebeff98715f3ab7646fbe0f2ffb24509ca372828c68c4ed0e", size = 350905, upload-time = "2026-05-19T07:44:36.957Z" },
{ url = "https://files.pythonhosted.org/packages/23/ae/4321867888a783d03b7c053c0b68ca45d03974d86fcebf44d4ec268db397/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7207b25fe534bcf4d57e0110f90670e61c1c38b6f4598ba855af69ab428fc118", size = 502098, upload-time = "2026-05-19T07:44:17.696Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/914a47bf42479bff0ce3e1fa1cbe3585354708edc928e27687cf91de9c26/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:16dc5c6e439f75b0456114e955983e2156c1f38887733e54d54205d3005223e4", size = 607032, upload-time = "2026-05-19T07:44:22.151Z" },
{ url = "https://files.pythonhosted.org/packages/85/4c/2abacd6badba61a047eaa39c8347656229d12843bd9bbe4906daa6dc752c/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6d3ee32c57898d8415242b08d5dd086bc4f7bcbbb3fc102ef257f3d793eb294", size = 567664, upload-time = "2026-05-19T07:45:21.043Z" },
{ url = "https://files.pythonhosted.org/packages/53/1f/9d1a09521276424da19dc0d74456aed3311170fec181b28fa6acba45d963/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7555f120a2282d1901c9a632c2398a614101af4fe3f7c8114aa0f1d8c1978855", size = 530996, upload-time = "2026-05-19T07:45:44.229Z" },
{ url = "https://files.pythonhosted.org/packages/b4/22/14dbedb6b61f492d5524077fd10bbfb137583b0f0aafa6cd870ccb43f39a/uuid_utils-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:756575d082ea4cb7d2f923d5b640c0efe7c82573aab49220c4e09b62d13737ff", size = 169358, upload-time = "2026-05-19T07:45:05.146Z" },
{ url = "https://files.pythonhosted.org/packages/25/f4/a636806c98401a1108f2456e9cc3fa39a618145bfb1d0860c57203159cfe/uuid_utils-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:aa50261a83991dbb570a00573741455bd8f3249444f7329e5bdcd494799d1504", size = 174813, upload-time = "2026-05-19T07:45:59.579Z" },
{ url = "https://files.pythonhosted.org/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", size = 171733, upload-time = "2026-05-19T07:45:29.283Z" },
{ url = "https://files.pythonhosted.org/packages/d3/89/655408a5485c56bf2c4561eb85f5bca119b1f4020370b4daaeb8d13e46fb/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e35e9a986e86806a61288fac3afbb51317f2580929feefd1661891ffd7b8c24", size = 569295, upload-time = "2026-05-19T07:45:22.325Z" },
{ url = "https://files.pythonhosted.org/packages/24/1c/a7c5506a4e2cf95ac98fec0996c56daa14e41f2ab1858f569b3556a202f9/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b35706350cf9bd4813f1811bebe03cac09795a5a379f90cb3616171f4e9ffc9e", size = 292316, upload-time = "2026-05-19T07:43:57.044Z" },
{ url = "https://files.pythonhosted.org/packages/dd/75/4267ab8baa1e6a8ad7c262e204484b44df0fde0920025ea9b43c2b869726/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4fd5c7936a876ba2606ba124603b559a5c2cea458c59b9c31677e6acc3c53cc", size = 329619, upload-time = "2026-05-19T07:44:12.928Z" },
{ url = "https://files.pythonhosted.org/packages/15/77/c794102831e331564f651099cac55006694677938d70f1033b35da451a89/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:130f7452c1b87b7c16d0bdc1f32a1de531ae4cc4220ed4e691402bbcfc39e0a9", size = 335121, upload-time = "2026-05-19T07:45:47.974Z" },
{ url = "https://files.pythonhosted.org/packages/8b/3e/458a0a2da75c596b151182a6c7550c6c3d30f479e14e40f69c0336579e59/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5ee0bbbd4ca3968422cd8308f0072520bc73dc760cb26c6fa75ca1aca14d210", size = 449631, upload-time = "2026-05-19T07:45:50.645Z" },
{ url = "https://files.pythonhosted.org/packages/ed/15/dd1fab6f7fcd15f2c331d0c1f0f516bb1113a640216460f82be53db3dcf8/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0824a31898ef46a9d84d748c3abe27cdb615ac3773c53cc1f84fc8e66dc7c4", size = 328418, upload-time = "2026-05-19T07:44:52.38Z" },
{ url = "https://files.pythonhosted.org/packages/96/56/62dcd551b140cbeb0f87522da2015b4b9e5818327b920506ad88d28562b0/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abfbf5e0c47fb31b37164a99515104e449a0bee36a071dc8b105457a2b35a5e6", size = 356177, upload-time = "2026-05-19T07:45:42.856Z" },
{ url = "https://files.pythonhosted.org/packages/44/e7/3937b9a9d6745b94dbe7b86531e098db8c53b77c8d07df7daa9577a47b8e/uuid_utils-0.16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:680799a9ade01d69c53cb9d41392ced24919d4f600bfab5060b61fca37510097", size = 178508, upload-time = "2026-05-19T07:43:43.774Z" },
]
[[package]]
name = "xxhash"
version = "3.6.0"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" }
sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" },
{ url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" },
{ url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" },
{ url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" },
{ url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" },
{ url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" },
{ url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" },
{ url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" },
{ url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" },
{ url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" },
{ url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" },
{ url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" },
{ url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" },
{ url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" },
{ url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" },
{ url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" },
{ url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" },
{ url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" },
{ url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" },
{ url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" },
{ url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" },
{ url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" },
{ url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" },
{ url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" },
{ url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" },
{ url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" },
{ url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" },
{ url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" },
{ url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" },
{ url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" },
{ url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" },
{ url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" },
{ url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" },
{ url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" },
{ url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" },
{ url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" },
{ url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" },
{ url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" },
{ url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" },
{ url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" },
{ url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" },
{ url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" },
{ url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" },
{ url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" },
{ url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" },
{ url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" },
{ url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" },
{ url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" },
{ url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" },
{ url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" },
{ url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" },
{ url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" },
{ url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" },
{ url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" },
{ url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" },
{ url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" },
{ url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" },
{ url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" },
{ url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" },
{ url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" },
{ url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" },
{ url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" },
{ url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" },
{ url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" },
{ url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" },
{ url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" },
{ url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" },
{ url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" },
{ url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" },
{ url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" },
{ url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" },
{ url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" },
{ url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" },
{ url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" },
{ url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" },
{ url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" },
{ url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" },
{ url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" },
{ url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" },
{ url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" },
{ url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" },
{ url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" },
{ url = "https://files.pythonhosted.org/packages/3b/f4/7bd35089ff1f8e2c96baa2dce05775a122aacd2e3830a73165e27a4d0848/xxhash-3.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdc7d06929ae28dda98297a18eef7b0fd38991a3b405d8d7b55c9ef24c296958", size = 33423, upload-time = "2026-04-25T11:05:47.628Z" },
{ url = "https://files.pythonhosted.org/packages/a3/26/4e00c88a6a2c8a759cfb77d2a9a405f901e8aa66e60ef1fd0aeb35edda48/xxhash-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712", size = 30857, upload-time = "2026-04-25T11:05:49.189Z" },
{ url = "https://files.pythonhosted.org/packages/82/2f/eeb942c17a5a761a8f01cb9180a0b76bfb62a2c39e6f46b1f9001899027a/xxhash-3.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9e6c0d843f1daf85ea23aeb053579135552bde575b7b98af20bfc667b6e4548d", size = 194702, upload-time = "2026-04-25T11:05:50.457Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fd/96f132c08b1e5951c68691d3b9ec351ec2edc028f6a01fcd294f46b9d9f0/xxhash-3.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60", size = 213613, upload-time = "2026-04-25T11:05:52.571Z" },
{ url = "https://files.pythonhosted.org/packages/82/89/d4e92b796c5ed052d29ed324dbfc1dc1188e0c4bf64bebbf0f8fc20698df/xxhash-3.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2", size = 236726, upload-time = "2026-04-25T11:05:54.395Z" },
{ url = "https://files.pythonhosted.org/packages/40/f1/81fc4361921dc6e557a9c60cb3712f36d244d06eeeb71cd2f4252ac42678/xxhash-3.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a", size = 212443, upload-time = "2026-04-25T11:05:56.334Z" },
{ url = "https://files.pythonhosted.org/packages/6a/d0/afeddd4cff50a332f50d4b8a2e8857673153ab0564ef472fcdeb0b5430df/xxhash-3.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800", size = 445793, upload-time = "2026-04-25T11:05:58.953Z" },
{ url = "https://files.pythonhosted.org/packages/f7/d0/3c91e4e6a05ca4d7df8e39ec3a75b713609258ec84705ab34be6430826a1/xxhash-3.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8", size = 193937, upload-time = "2026-04-25T11:06:00.546Z" },
{ url = "https://files.pythonhosted.org/packages/4e/3a/a6b0772d9801dd4bea4ca4fd34734d6e9b51a711c8a611a24a79de26a878/xxhash-3.7.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5", size = 285188, upload-time = "2026-04-25T11:06:01.96Z" },
{ url = "https://files.pythonhosted.org/packages/6c/f8/cf8e31fd7282230fe7367cd501a2e75b4b67b222bfc7eacccfc20d2652cb/xxhash-3.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4", size = 210966, upload-time = "2026-04-25T11:06:03.453Z" },
{ url = "https://files.pythonhosted.org/packages/cc/f0/fd36cc4a81bf52ee5633275daae2b93dd958aace67fd4f5d466ec83b5f35/xxhash-3.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb", size = 241994, upload-time = "2026-04-25T11:06:05.264Z" },
{ url = "https://files.pythonhosted.org/packages/08/e1/67f5d9c9369be42eaf99ba02c01bf14c5ecd67087b02567960bfcee43b63/xxhash-3.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f420ad3d41e38194353a498bbc9561fd5a9973a27b536ce46d8583479cf44335", size = 198707, upload-time = "2026-04-25T11:06:07.044Z" },
{ url = "https://files.pythonhosted.org/packages/50/17/a4c865ca22d2da6b1bc7d739bf88cab209533cf52ba06ca9da27c3039bee/xxhash-3.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04", size = 210917, upload-time = "2026-04-25T11:06:08.853Z" },
{ url = "https://files.pythonhosted.org/packages/49/8b/453b35810d697abac3c96bde3528bece685869227da274eb80a4a4d4a119/xxhash-3.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af", size = 275772, upload-time = "2026-04-25T11:06:10.645Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ad/4eed7eab07fd3ee6678f416190f0413d097ab5d7c1278906bf1e9549d789/xxhash-3.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31", size = 414068, upload-time = "2026-04-25T11:06:12.511Z" },
{ url = "https://files.pythonhosted.org/packages/d3/4e/fd6f8a680ba248fdb83054fa71a8bfa3891225200de1708b888ef2c49829/xxhash-3.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923", size = 191459, upload-time = "2026-04-25T11:06:14.07Z" },
{ url = "https://files.pythonhosted.org/packages/50/7c/8cb34b3bed4f44ca6827a534d50833f9bc6c006e83b0eb410ac9fa0793bd/xxhash-3.7.0-cp311-cp311-win32.whl", hash = "sha256:3281ba1d1e60ee7a382a7b958513ba03c2c0d5fcbd9a6f7517c0a81251a23422", size = 30628, upload-time = "2026-04-25T11:06:15.802Z" },
{ url = "https://files.pythonhosted.org/packages/0b/47/a49767bd7b40782bedae9ff0721bfe1d7e4dd9dc1585dea684e57ba67c20/xxhash-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7f25baec4c5d851d40718d6fae52285b31683093d4ff5207e63ab306ccf14a5", size = 31461, upload-time = "2026-04-25T11:06:17.104Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c6/3957bfacfb706bd687be246dfa8dd60f8df97c44186d229f7fd6e26c4b7e/xxhash-3.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:4c2454448ce847c72635827bb75c15c5a3434b03ee1afd28cb6dc6fb2597d830", size = 27746, upload-time = "2026-04-25T11:06:18.716Z" },
{ url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" },
{ url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" },
{ url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" },
{ url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" },
{ url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" },
{ url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" },
{ url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" },
{ url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" },
{ url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" },
{ url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" },
{ url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" },
{ url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" },
{ url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" },
{ url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" },
{ url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" },
{ url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" },
{ url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" },
{ url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" },
{ url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" },
{ url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" },
{ url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" },
{ url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" },
{ url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" },
{ url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" },
{ url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" },
{ url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" },
{ url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" },
{ url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" },
{ url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" },
{ url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" },
{ url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" },
{ url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" },
{ url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" },
{ url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" },
{ url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" },
{ url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" },
{ url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" },
{ url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" },
{ url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" },
{ url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" },
{ url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" },
{ url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" },
{ url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" },
{ url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" },
{ url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" },
{ url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" },
{ url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" },
{ url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" },
{ url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" },
{ url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" },
{ url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" },
{ url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" },
{ url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" },
{ url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" },
{ url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" },
{ url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" },
{ url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" },
{ url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" },
{ url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" },
{ url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" },
{ url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" },
{ url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" },
{ url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" },
{ url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" },
{ url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" },
{ url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" },
{ url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" },
{ url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" },
{ url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" },
{ url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" },
{ url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" },
{ url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" },
{ url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" },
{ url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" },
{ url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" },
{ url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" },
{ url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" },
{ url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" },
{ url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" },
{ url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" },
{ url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" },
{ url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" },
{ url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" },
{ url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" },
{ url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" },
{ url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" },
{ url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" },
{ url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" },
{ url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" },
{ url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" },
{ url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" },
{ url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" },
{ url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" },
{ url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" },
{ url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" },
{ url = "https://files.pythonhosted.org/packages/54/c1/e57ac7317b1f58a92bab692da6d497e2a7ce44735b224e296347a7ecc754/xxhash-3.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad3aa71e12ee634f22b39a0ff439357583706e50765f17f05550f92dbf128a23", size = 31232, upload-time = "2026-04-25T11:10:21.51Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4e/075559bd712bc62e84915ea46bbee859f935d285659082c129bdbff679dd/xxhash-3.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1", size = 28553, upload-time = "2026-04-25T11:10:23.1Z" },
{ url = "https://files.pythonhosted.org/packages/92/ca/a9c78cb384d4b033b0c58196bd5c8509873cabe76389e195127b0302a741/xxhash-3.7.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7fbec49f5341bbdea0c471f7d1e2fb41ae8925af9b6f28025c28defd8eb94274", size = 41109, upload-time = "2026-04-25T11:10:25.022Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b1/dfe2629f7c77eb2fa234c72ff537cdd64939763df704e256446ed364a16d/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde", size = 36307, upload-time = "2026-04-25T11:10:26.949Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f7/5a484afce0f48dd8083208b42e4911f290a82c7b52458ef2927e4d421a45/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc", size = 32534, upload-time = "2026-04-25T11:10:29.01Z" },
{ url = "https://files.pythonhosted.org/packages/0f/5f/4acfcd490db9780cf36c58534d828003c564cde5350220a1c783c4d10776/xxhash-3.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ec101643395d7f21405b640f728f6f627e6986557027d740f2f9b220955edafe", size = 31552, upload-time = "2026-04-25T11:10:30.727Z" },
]
[[package]]
+7 -7
View File
@@ -797,11 +797,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -1094,7 +1094,7 @@ test = [
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "httpx", specifier = ">=0.24.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.9.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.10.0" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.7" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
{ name = "pathspec", specifier = ">=0.11.0" },
@@ -1174,7 +1174,7 @@ wheels = [
[[package]]
name = "langsmith"
version = "0.7.32"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx", marker = "python_full_version >= '3.11'" },
@@ -1187,9 +1187,9 @@ dependencies = [
{ name = "xxhash", marker = "python_full_version >= '3.11'" },
{ name = "zstandard", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/b4/a0b4a501bee6b8a741ce29f8c48155b132118483cddc6f9247735ddb38fa/langsmith-0.7.32.tar.gz", hash = "sha256:b59b8e106d0e4c4842e158229296086e2aa7c561e3f602acda73d3ad0062e915", size = 1184518, upload-time = "2026-04-15T23:42:41.885Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/bc/148f98ac7dad73ac5e1b1c985290079cfeeb9ba13d760a24f25002beb2c9/langsmith-0.7.32-py3-none-any.whl", hash = "sha256:e1fde928990c4c52f47dc5132708cec674355d9101723d564183e965f383bf5f", size = 378272, upload-time = "2026-04-15T23:42:39.905Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[package.optional-dependencies]
+79 -29
View File
@@ -79,6 +79,34 @@ def patch_checkpoint_map(
return config
def _merge_callbacks(base: Callbacks, new: Callbacks) -> Callbacks:
"""Merge two callbacks values (None / list / BaseCallbackManager).
Six cases total (3 base types x 2 non-None new types).
"""
if new is None:
return base
if base is None:
return new.copy() if isinstance(new, (list, BaseCallbackManager)) else new
if isinstance(new, list):
if isinstance(base, list):
return base + new
if isinstance(base, BaseCallbackManager):
mngr = base.copy()
for cb in new:
mngr.add_handler(cb, inherit=True)
return mngr
elif isinstance(new, BaseCallbackManager):
if isinstance(base, list):
mngr = new.copy()
for cb in base:
mngr.add_handler(cb, inherit=True)
return mngr
if isinstance(base, BaseCallbackManager):
return base.merge(new)
raise NotImplementedError(f"Unsupported callback types: {type(base)}, {type(new)}")
def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
"""Merge multiple configs into one.
@@ -113,34 +141,9 @@ def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
else:
base[key] = value
elif key == "callbacks":
base_callbacks = base.get("callbacks")
# callbacks can be either None, list[handler] or manager
# so merging two callbacks values has 6 cases
if isinstance(value, list):
if base_callbacks is None:
base["callbacks"] = value.copy()
elif isinstance(base_callbacks, list):
base["callbacks"] = base_callbacks + value
else:
# base_callbacks is a manager
mngr = base_callbacks.copy()
for callback in value:
mngr.add_handler(callback, inherit=True)
base["callbacks"] = mngr
elif isinstance(value, BaseCallbackManager):
# value is a manager
if base_callbacks is None:
base["callbacks"] = value.copy()
elif isinstance(base_callbacks, list):
mngr = value.copy()
for callback in base_callbacks:
mngr.add_handler(callback, inherit=True)
base["callbacks"] = mngr
else:
# base_callbacks is also a manager
base["callbacks"] = base_callbacks.merge(value)
else:
raise NotImplementedError
base["callbacks"] = _merge_callbacks(
base.get("callbacks"), cast(Callbacks, value)
)
elif key == "recursion_limit":
if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT:
base["recursion_limit"] = config["recursion_limit"]
@@ -309,7 +312,40 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
for k, v in config.items():
if _is_not_empty(v) and k in CONFIG_KEYS:
if k == CONF:
empty[k] = cast(dict, v).copy()
# Shallow-merge configurable dicts across configs so values
# bound via with_config(...) (e.g. ls_agent_type) are
# preserved when later configs (e.g. invoke-time) only
# specify a subset of keys like thread_id.
existing = empty.get(k)
empty[k] = (
{**cast(dict, existing), **cast(dict, v)}
if existing
else cast(dict, v).copy()
)
elif k == "callbacks":
empty["callbacks"] = _merge_callbacks(
empty.get("callbacks"), cast(Callbacks, v)
)
elif k == "metadata":
# Shallow-merge metadata dicts across configs so values
# bound via with_config(...) (e.g. user_id) are preserved
# when later configs supply other metadata keys.
existing = empty.get("metadata")
empty["metadata"] = (
{**cast(dict, existing), **cast(dict, v)}
if existing
else cast(dict, v).copy()
)
elif k == "tags":
# Concatenate tags across configs so values bound via
# with_config(...) are preserved when later configs
# supply additional tags. Matches merge_configs.
existing_tags: list[str] | None = empty.get("tags")
empty["tags"] = (
[*existing_tags, *cast(list, v)]
if existing_tags
else list(cast(list, v))
)
else:
empty[k] = v # type: ignore[literal-required]
for k, v in config.items():
@@ -366,3 +402,17 @@ _PROPAGATE_TO_METADATA = frozenset(
"graph_id",
)
)
def filter_to_user_tags(tags: Sequence[str] | None) -> list[str] | None:
"""Drop langgraph's internal `seq:step:*` bookkeeping tags.
`seq:step:N` tags are added internally to mark sequence steps; everything
else (user-supplied tags and any other framework tags) is kept. Returns the
surviving tags, or `None` if none remain. Shared by the `messages` and
`tasks` stream handlers so both surface the same tag set on their metadata.
"""
if not tags:
return None
filtered = [t for t in tags if not t.startswith("seq:step")]
return filtered or None
+23
View File
@@ -21,6 +21,7 @@ __all__ = (
"InvalidUpdateError",
"GraphBubbleUp",
"GraphInterrupt",
"NodeCancelledError",
"NodeError",
"NodeInterrupt",
"NodeTimeoutError",
@@ -164,6 +165,28 @@ class NodeError:
"""Exception raised by the failed node."""
class NodeCancelledError(Exception):
"""Raised when a node body raises ``asyncio.CancelledError`` itself.
``asyncio.CancelledError`` is a ``BaseException`` and the pregel runner
treats cancelled task futures as silent tear-down (e.g. when it stops
sibling tasks after a peer fails). That is the correct behaviour for
*framework-initiated* cancellation, but a user node that raises
``asyncio.CancelledError`` from its own body should surface as a node
failure, the same way any other exception would.
The retry layer converts user-raised ``asyncio.CancelledError`` into this
type so it flows through the normal error path and the run reports as
``error`` instead of silently succeeding.
"""
node: str
def __init__(self, node: str, message: str | None = None) -> None:
super().__init__(message or f"Node {node!r} raised asyncio.CancelledError")
self.node = node
class NodeTimeoutError(Exception):
"""Raised when a node invocation exceeds one of its configured timeouts.
+9
View File
@@ -115,6 +115,7 @@ from langgraph.pregel._io import (
map_output_values,
read_channels,
)
from langgraph.pregel._messages import ensure_message_ids
from langgraph.pregel._read import PregelNode
from langgraph.pregel._utils import get_new_channel_versions, is_xxh3_128_hexdigest
from langgraph.pregel.debug import (
@@ -447,6 +448,14 @@ class PregelLoop:
# save writes
self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes)
# Assign stable IDs to any id=None BaseMessages in DeltaChannel writes
# before the background thread serialises them. Without this, reducers
# that assign IDs inside apply_writes() race with serialisation and
# store id=None, causing get_state() replays to produce a different UUID
# on every call.
for c, v in writes_to_save:
if isinstance(self.specs.get(c), DeltaChannel):
ensure_message_ids(v)
if self.durability != "exit" and self.checkpointer_put_writes is not None:
config = patch_configurable(
self.checkpoint_config,
+87 -4
View File
@@ -10,10 +10,12 @@ from typing import (
from uuid import UUID, uuid4
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.messages import BaseMessage
from langchain_core.messages import BaseMessage, ToolMessage
from langchain_core.messages.utils import convert_to_messages
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
from pydantic import BaseModel
from langgraph._internal._config import filter_to_user_tags
from langgraph._internal._constants import NS_SEP
from langgraph.constants import TAG_HIDDEN, TAG_NOSTREAM
from langgraph.pregel.protocol import StreamChunk
@@ -142,9 +144,8 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
]
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
return
if tags:
if filtered_tags := [t for t in tags if not t.startswith("seq:step")]:
metadata["tags"] = filtered_tags
if (filtered_tags := filter_to_user_tags(tags)) is not None:
metadata["tags"] = filtered_tags
self.metadata[run_id] = (ns, metadata)
def on_llm_new_token(
@@ -303,6 +304,35 @@ class StreamMessagesHandlerV2(StreamMessagesHandler, _V2StreamingCallbackHandler
super().__init__(stream, subgraphs, parent_ns=parent_ns)
self._streamed_run_ids: set[UUID] = set()
def _find_and_emit_messages(self, meta: Meta, response: Any) -> None:
"""Like the v1 handler, but skip ToolMessage from node outputs.
Tool results belong on the tools channel / state in v3; v2-flagged streams
must not replay finalized ToolMessages as chat tokens (see MessagesTransformer).
Legacy v1-only `stream_mode="messages"` still emits ToolMessages (see subgraph
streaming tests).
"""
if isinstance(response, BaseMessage) and not isinstance(response, ToolMessage):
self._emit(meta, response, dedupe=True)
elif isinstance(response, Sequence):
for value in response:
if isinstance(value, BaseMessage) and not isinstance(
value, ToolMessage
):
self._emit(meta, value, dedupe=True)
else:
for value in _state_values(response):
if isinstance(value, BaseMessage) and not isinstance(
value, ToolMessage
):
self._emit(meta, value, dedupe=True)
elif isinstance(value, Sequence):
for item in value:
if isinstance(item, BaseMessage) and not isinstance(
item, ToolMessage
):
self._emit(meta, item, dedupe=True)
def on_llm_end(
self,
response: LLMResult,
@@ -376,3 +406,56 @@ class StreamMessagesHandlerV2(StreamMessagesHandler, _V2StreamingCallbackHandler
self.seen.add(msg_id)
v2_meta = {**meta[1], "run_id": str(run_id)}
self.stream((meta[0], "messages", (event, v2_meta)))
# Known role values (OpenAI-style) and type values (LangChain serialisation)
# that identify a dict as a message. Checked before coercing to BaseMessage so
# we don't accidentally touch unrelated dicts that happen to have a "role" key.
_MESSAGE_ROLES: frozenset[str] = frozenset(
{"user", "human", "assistant", "ai", "tool", "system", "function"}
)
_MESSAGE_TYPES: frozenset[str] = frozenset(
{"human", "ai", "tool", "system", "function", "remove"}
)
def _is_message_dict(item: dict) -> bool:
return item.get("role") in _MESSAGE_ROLES or item.get("type") in _MESSAGE_TYPES
def ensure_message_ids(value: Any) -> None:
"""Coerce message-like write values to typed BaseMessages with stable IDs.
Called in put_writes() before DeltaChannel writes are submitted to the
checkpointer. Without this the checkpoint may store raw dicts or id=None
BaseMessages; every get_state() replay then produces a different UUID and
the same message appears with a different ID in each LangSmith trace.
Handles three input shapes:
- BaseMessage objects: assign a UUID if id is None.
- Dicts with a known "role" (OpenAI-style) or "type" (LangChain format) at
the root level: stamp "id" into the dict in-place. The reducer's
convert_to_messages call will forward the id to the resulting BaseMessage.
- Lists of the above: apply the same logic to each element, replacing dict
items with coerced BaseMessages so the shared list reference seen by
checkpoint_pending_writes and the background thread both get typed messages.
Mutating synchronously here (before the background thread is submitted) is
safe: the serialised bytes always reflect the post-coercion state.
"""
if isinstance(value, BaseMessage):
if value.id is None:
value.id = str(uuid4())
elif isinstance(value, dict) and _is_message_dict(value):
if not value.get("id"):
value["id"] = str(uuid4())
elif isinstance(value, list):
for i, item in enumerate(value):
if isinstance(item, BaseMessage):
if item.id is None:
item.id = str(uuid4())
elif isinstance(item, dict) and _is_message_dict(item):
msg = convert_to_messages([item])[0]
if msg.id is None:
msg.id = str(uuid4())
value[i] = msg
@@ -0,0 +1,374 @@
# libs/langgraph/langgraph/pregel/_remote_run_stream.py
from __future__ import annotations
import logging
import sys
from collections.abc import AsyncIterator, Iterator, Mapping
from types import TracebackType
from typing import Any, cast
from langchain_core.runnables import RunnableConfig
from langgraph_sdk._async.stream import AsyncThreadStream
from langgraph_sdk._sync.stream import SyncThreadStream
from langgraph_sdk.client import LangGraphClient, SyncLangGraphClient
from langgraph_sdk.stream.decoders import DataDecoder
from langgraph.types import Command
logger = logging.getLogger(__name__)
def _translate_command_input(input: Any) -> Any:
"""Translate a local `Command` into the v3 wire `input`, else passthrough.
The v3 server decides start-vs-resume from thread state (an interrupted
run or pending interrupts) and, on resume, wraps the whole `input` as
`{"resume": input}` itself. So a resume `Command` must surface its raw
`resume` value as the wire `input` (not the serialized dataclass, which
the server would double-wrap). The v3 `run.start` path has no `goto` /
`update` channel, so those are rejected.
`langgraph_sdk` is upstream of `langgraph`, so this `Command`-aware
marshalling lives here on the adapter (langgraph) side of the boundary.
"""
if isinstance(input, Command):
if input.goto or input.update:
raise NotImplementedError(
"RemoteGraph v3 streaming supports `Command(resume=...)` only; "
"`goto` / `update` are not supported by the v3 `run.start` path."
)
return input.resume
return input
class _ChannelProjection:
"""Decoded projection for a wire channel the SDK doesn't type natively.
Subscribes to `channel` and decodes each event's `params["data"]` through the
SDK's `DataDecoder` — the same decoder the SDK's own plain-payload projections
(`values` / `updates` / `checkpoints` / `tasks`) use, which yields the item
shape that local's `UpdatesTransformer` / `CheckpointsTransformer` /
`TasksTransformer` / `CustomTransformer` push, so iterating this matches the
corresponding local projection. Iterate with `for` against a sync stream and
`async for` against an async stream (matching the underlying SDK). Opening
the subscription requires the stream to be entered (`with` / `async with`).
"""
def __init__(self, sdk: AsyncThreadStream | SyncThreadStream, channel: str) -> None:
self._sdk = sdk
self._channel = channel
def __iter__(self) -> Iterator[Any]:
# Sync lane: the sync adapter's SDK returns a sync iterator here.
decoder = DataDecoder(self._channel)
events = cast(Iterator[Any], self._sdk.subscribe([self._channel]))
for event in events:
yield from decoder.feed(event)
def __aiter__(self) -> AsyncIterator[Any]:
return self._aiter()
async def _aiter(self) -> AsyncIterator[Any]:
# Async lane: the async adapter's SDK returns an async iterator here.
decoder = DataDecoder(self._channel)
events = cast(AsyncIterator[Any], self._sdk.subscribe([self._channel]))
async for event in events:
for item in decoder.feed(event):
yield item
class _ProjectionRegistry(Mapping[str, Any]):
"""Read-only name -> projection registry mirroring local `GraphRunStream.extensions`.
Resolution follows the langchain-protocol wire channels, and every entry
yields the same decoded item shape local does (`params.data`):
- `values` / `messages` / `tool_calls` / `subgraphs` resolve to the SDK's
decoded typed projections. `tool_calls` is the `tools` channel tool
*execution* events, distinct from the tool-call *inputs* inside `messages`.
- `updates` / `checkpoints` / `tasks` / `custom` have no typed SDK
projection, so they resolve to a `_ChannelProjection` that subscribes to
the channel and yields `params.data` matching the local transformer
output for those channels.
- any other name is a specific custom-extension channel
(`thread.extensions[name]`, i.e. `custom:<name>`).
`lifecycle` is intentionally absent: local derives a status payload from it
rather than yielding `params.data`, and the SDK consumes it as control-plane
(driving `output` / `interrupted`), so its shape can't be matched — it
remains reachable via the raw `events` iterator. `debug` is absent too: it
is not a v3 wire channel.
"""
# Channels the SDK decodes into typed projections.
_TYPED = ("values", "messages", "tool_calls", "subgraphs")
# Wire channels with no typed SDK projection — decoded here to match local.
_DECODED = ("updates", "checkpoints", "tasks", "custom")
_NATIVE = _TYPED + _DECODED
def __init__(self, sdk: AsyncThreadStream | SyncThreadStream) -> None:
self._sdk = sdk
def __getitem__(self, name: str) -> Any:
if name in self._TYPED:
return getattr(self._sdk, name)
if name in self._DECODED:
return _ChannelProjection(self._sdk, name)
return self._sdk.extensions[name]
def __iter__(self) -> Iterator[str]:
return iter(self._NATIVE)
def __len__(self) -> int:
return len(self._NATIVE)
class _RemoteGraphRunStream:
"""Sync adapter: SyncThreadStream -> GraphRunStream surface."""
def __init__(
self,
*,
sync_client: SyncLangGraphClient,
sdk_thread: SyncThreadStream,
input: Any,
config: RunnableConfig | None,
metadata: dict[str, Any] | None,
) -> None:
self._client = sync_client
self._sdk = sdk_thread
self._start_kwargs: dict[str, Any] = {
"input": _translate_command_input(input),
"config": config,
"metadata": metadata,
}
self._run_id: str | None = None
self._closed = False
self._events_iter: Iterator[Any] | None = None
def __enter__(self) -> _RemoteGraphRunStream:
if self._closed:
raise RuntimeError("_RemoteGraphRunStream already closed")
self._sdk.__enter__()
try:
result = self._sdk.run.start(**self._start_kwargs)
except BaseException:
self._sdk.__exit__(*sys.exc_info())
raise
self._run_id = result["run_id"]
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
if self._closed:
return
self._closed = True
self._sdk.__exit__(exc_type, exc, tb)
@property
def output(self) -> Any:
return self._sdk.output
@property
def interrupted(self) -> bool:
"""Whether the remote run is currently paused at an interrupt.
Reads the SDK's current value without blocking. This differs from
local `GraphRunStream.interrupted`, which drives the run to terminal
before returning the flag. Sync callers needing a wait-for-interrupt
pattern should switch to the async API and drain a projection.
"""
return self._sdk.interrupted
@property
def interrupts(self) -> list[Any]:
"""Current outstanding interrupt payloads (non-blocking snapshot)."""
return list(self._sdk.interrupts)
@property
def values(self) -> Any:
"""Live state-snapshot projection (mirrors local `run.values`)."""
return self._sdk.values
@property
def messages(self) -> Any:
"""Live message-stream projection (mirrors local `run.messages`)."""
return self._sdk.messages
@property
def subgraphs(self) -> Any:
"""Subgraph-handle projection (mirrors local `run.subgraphs`)."""
return self._sdk.subgraphs
@property
def tool_calls(self) -> Any:
"""Tool-execution projection (the `tools` channel).
These are tool *execution* events (started / output / finished),
distinct from the tool-call *inputs* carried inside `messages`.
"""
return self._sdk.tool_calls
@property
def extensions(self) -> Mapping[str, Any]:
"""Name -> projection registry (mirrors local `run.extensions`)."""
return _ProjectionRegistry(self._sdk)
def abort(self) -> None:
if self._closed:
return
self._closed = True
if self._run_id is not None:
try:
self._client.runs.cancel(self._sdk.thread_id, self._run_id, wait=False)
except Exception:
logger.debug("abort: runs.cancel failed", exc_info=True)
try:
self._sdk.close()
except Exception:
logger.debug("abort: sdk.close failed", exc_info=True)
def __iter__(self) -> Iterator[Any]:
if self._events_iter is None:
self._events_iter = iter(self._sdk.events)
return self._events_iter
def interleave(self, *names: str) -> Iterator[tuple[str, Any]]:
yield from self._sdk.interleave_projections(list(names))
class _AsyncRemoteGraphRunStream:
"""Async adapter: AsyncThreadStream -> AsyncGraphRunStream surface."""
def __init__(
self,
*,
client: LangGraphClient,
sdk_thread: AsyncThreadStream,
input: Any,
config: RunnableConfig | None,
metadata: dict[str, Any] | None,
) -> None:
self._client = client
self._sdk = sdk_thread
self._start_kwargs: dict[str, Any] = {
"input": _translate_command_input(input),
"config": config,
"metadata": metadata,
}
self._run_id: str | None = None
self._closed = False
self._events_aiter: AsyncIterator[Any] | None = None
async def __aenter__(self) -> _AsyncRemoteGraphRunStream:
if self._closed:
raise RuntimeError("_AsyncRemoteGraphRunStream already closed")
await self._sdk.__aenter__()
try:
result = await self._sdk.run.start(**self._start_kwargs)
except BaseException:
await self._sdk.__aexit__(*sys.exc_info())
raise
self._run_id = result["run_id"]
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
if self._closed:
return
self._closed = True
await self._sdk.__aexit__(exc_type, exc, tb)
async def output(self) -> Any:
"""Drive the remote run to completion and return the final state.
Awaits the SDK's terminal-state awaitable, matching local
`AsyncGraphRunStream.output()` (a method, not a property, so
`run.output` without `await` fails at type-check time rather than
silently yielding a coroutine).
"""
return await self._sdk.output
async def interrupted(self) -> bool:
"""Whether the remote run is currently paused at an interrupt.
Reads the SDK's current value without blocking. This differs from
local `AsyncGraphRunStream.interrupted()`, which drives the run to
terminal before returning the flag. Callers that need a
wait-for-interrupt pattern should drain a projection (e.g.,
`async for snap in stream._sdk.values`) until the SDK's paused
sentinel fires, then call this method.
"""
return self._sdk.interrupted
async def interrupts(self) -> list[Any]:
"""Current outstanding interrupt payloads.
Non-blocking; reads the SDK's current snapshot. See `interrupted`
for the divergence from local v3 semantics.
"""
return list(self._sdk.interrupts)
@property
def values(self) -> Any:
"""Live state-snapshot projection (mirrors local `run.values`)."""
return self._sdk.values
@property
def messages(self) -> Any:
"""Live message-stream projection (mirrors local `run.messages`)."""
return self._sdk.messages
@property
def subgraphs(self) -> Any:
"""Subgraph-handle projection (mirrors local `run.subgraphs`)."""
return self._sdk.subgraphs
@property
def tool_calls(self) -> Any:
"""Tool-execution projection (the `tools` channel).
These are tool *execution* events (started / output / finished),
distinct from the tool-call *inputs* carried inside `messages`.
"""
return self._sdk.tool_calls
@property
def extensions(self) -> Mapping[str, Any]:
"""Name -> projection registry (mirrors local `run.extensions`)."""
return _ProjectionRegistry(self._sdk)
async def abort(self) -> None:
if self._closed:
return
self._closed = True
if self._run_id is not None:
try:
await self._client.runs.cancel(
self._sdk.thread_id, self._run_id, wait=False
)
except Exception:
logger.debug("abort: runs.cancel failed", exc_info=True)
try:
await self._sdk.close()
except Exception:
logger.debug("abort: sdk.close failed", exc_info=True)
def __aiter__(self) -> AsyncIterator[Any]:
if self._events_aiter is None:
self._events_aiter = self._sdk.events.__aiter__()
return self._events_aiter
# Note: deliberately no `interleave()` on the async adapter. Local
# `AsyncGraphRunStream` doesn't have one either (async callers compose
# with `asyncio.gather` / `asyncio.as_completed`). The sync adapter
# provides `interleave()` because sync callers have no comparable
# primitive for iterating multiple iterators concurrently.
+57 -1
View File
@@ -37,13 +37,23 @@ from langgraph._internal._constants import (
)
from langgraph._internal._runnable import create_task_in_config_context
from langgraph._internal._timeout import sync_timeout_unsupported
from langgraph.errors import GraphBubbleUp, NodeTimeoutError, ParentCommand
from langgraph.errors import (
GraphBubbleUp,
NodeCancelledError,
NodeTimeoutError,
ParentCommand,
)
from langgraph.pregel.protocol import StreamProtocol
from langgraph.runtime import ExecutionInfo, Runtime
from langgraph.types import Command, PregelExecutableTask, RetryPolicy, TimeoutPolicy
logger = logging.getLogger(__name__)
SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
# `asyncio.Task.cancelling()` was added in Python 3.11. It reports the number of
# pending cancel requests on the task: ``0`` means no external code asked us to
# cancel — so a ``CancelledError`` observed here was raised by the task body
# itself (the user's node) rather than by pregel cancelling sibling tasks.
SUPPORTS_TASK_CANCELLING = sys.version_info >= (3, 11)
def _timeout_secs(value: float | timedelta) -> float:
@@ -302,6 +312,28 @@ class _IdleProgressCallbackHandler(BaseCallbackHandler):
on_custom_event = _touch
def _is_user_raised_cancelled() -> bool:
"""Return True if the in-flight ``CancelledError`` came from the task body.
Pregel cancels sibling tasks via ``task.cancel()`` when a peer fails, which
increments ``asyncio.Task.cancelling()`` on the target before the cancel
actually fires. A user node that calls ``raise asyncio.CancelledError()``
from inside its own body raises while ``cancelling() == 0``, which is the
signal we use to convert the exception into a regular
:class:`NodeCancelledError`.
Returns ``False`` when we can't tell (``cancelling()`` unavailable, or no
current task neither should happen in practice from ``arun_with_retry``)
so framework-initiated cancellation continues to propagate unchanged.
"""
if not SUPPORTS_TASK_CANCELLING:
return False
current = asyncio.current_task()
if current is None:
return False
return current.cancelling() == 0
def _drain_cancelled(task: asyncio.Task[Any]) -> None:
# Mark the abandoned task's exception as retrieved so asyncio doesn't log it.
with suppress(asyncio.CancelledError):
@@ -600,6 +632,12 @@ def run_with_retry(
except GraphBubbleUp:
# if interrupted, end
raise
except asyncio.CancelledError as exc:
# A sync node has no asyncio context, so any ``CancelledError`` that
# reaches here was raised by the node body itself. Surface it as a
# regular exception so the pregel runner panics the run instead of
# treating the task as a silent tear-down (LSD-1507).
raise NodeCancelledError(task.name) from exc
except Exception as exc:
if SUPPORTS_EXC_NOTES:
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
@@ -736,6 +774,24 @@ async def arun_with_retry(
# if interrupted, end
_finish_timed_attempt(config, attempt_ctx)
raise
except asyncio.CancelledError as exc:
# ``CancelledError`` reaches us in two very different shapes:
# 1. Pregel cancelled this task because a sibling failed
# (``asyncio.Task.cancelling() >= 1``). The framework already
# knows the run is failing and we must let cancellation
# propagate so the watchdog/cleanup code in the runner sees a
# cancelled future.
# 2. The node body itself raised ``asyncio.CancelledError`` (
# ``cancelling() == 0``). The runner would otherwise treat
# this as silent tear-down and the run would report
# ``success`` even though the node failed (LSD-1507). Convert
# it into :class:`NodeCancelledError` so it follows the same
# path as any other node failure.
if _is_user_raised_cancelled():
_finish_timed_attempt(config, attempt_ctx, exc)
raise NodeCancelledError(task.name) from exc
_finish_timed_attempt(config, attempt_ctx, exc)
raise
except Exception as exc:
_finish_timed_attempt(config, attempt_ctx, exc)
if SUPPORTS_EXC_NOTES:
+26 -3
View File
@@ -6,9 +6,13 @@ from typing import Any
from uuid import UUID
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import CheckpointMetadata, PendingWrite
from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS,
CheckpointMetadata,
PendingWrite,
)
from langgraph._internal._config import patch_checkpoint_map
from langgraph._internal._config import filter_to_user_tags, patch_checkpoint_map
from langgraph._internal._constants import (
CONF,
CONFIG_KEY_CHECKPOINT_NS,
@@ -40,12 +44,31 @@ def map_debug_tasks(tasks: Iterable[PregelExecutableTask]) -> Iterator[TaskPaylo
if task.config is not None and TAG_HIDDEN in task.config.get("tags", []):
continue
yield {
payload: TaskPayload = {
"id": task.id,
"name": task.name,
"input": task.input,
"triggers": task.triggers,
}
# Forward user-meaningful metadata only — drop langgraph's internal
# framework keys (langgraph_node/step/triggers/path/checkpoint_ns,
# thread_id, ...), which are redundant with the task's own fields and
# namespace. Keys like `lc_agent_name`, `ls_integration`, and any
# user-supplied metadata ride along. Filtered config tags are folded in
# under `tags`, mirroring the messages stream handler. (The comprehension
# also yields a fresh dict, so mutating `md` doesn't touch task.config.)
if task.config is not None:
md = {
k: v
for k, v in (task.config.get("metadata") or {}).items()
if k not in EXCLUDED_METADATA_KEYS
}
filtered_tags = filter_to_user_tags(task.config.get("tags"))
if filtered_tags is not None:
md["tags"] = filtered_tags
if md:
payload["metadata"] = md
yield payload
def is_multiple_channel_write(value: Any) -> bool:
+126 -9
View File
@@ -55,6 +55,10 @@ from langgraph._internal._constants import (
NS_SEP,
)
from langgraph.errors import GraphInterrupt, ParentCommand
from langgraph.pregel._remote_run_stream import (
_AsyncRemoteGraphRunStream,
_RemoteGraphRunStream,
)
from langgraph.pregel.protocol import PregelProtocol, StreamProtocol
from langgraph.types import (
All,
@@ -80,6 +84,8 @@ _CONF_DROPLIST = frozenset(
),
)
_V3_SUPPORTED_KWARGS = frozenset({"metadata", "headers"})
def _sanitize_config_value(v: Any) -> Any:
"""Recursively sanitize a config value to ensure it contains only primitives."""
@@ -186,6 +192,34 @@ class RemoteGraph(PregelProtocol):
)
return self.sync_client
def _reject_v3_unsupported(
self,
*,
control: Any,
transformers: Any,
interrupt_before: Any,
interrupt_after: Any,
extra_kwargs: dict[str, Any],
) -> None:
"""Raise NotImplementedError for kwargs unsupported by the v3 streaming path."""
for name, value in (
("control", control),
("transformers", transformers),
("interrupt_before", interrupt_before),
("interrupt_after", interrupt_after),
):
if value:
raise NotImplementedError(
f"RemoteGraph.stream_events(version='v3') does not support `{name}=`."
)
unknown = set(extra_kwargs) - _V3_SUPPORTED_KWARGS
if unknown:
raise NotImplementedError(
f"RemoteGraph.stream_events(version='v3') does not support "
f"the following kwargs: {sorted(unknown)!r}. "
f"Supported: {sorted(_V3_SUPPORTED_KWARGS)!r}."
)
def copy(self, update: dict[str, Any]) -> Self:
attrs = {**self.__dict__, **update}
return self.__class__(attrs.pop("assistant_id"), **attrs)
@@ -996,21 +1030,104 @@ class RemoteGraph(PregelProtocol):
else:
yield chunk
def stream_events(
self,
input: Any,
config: RunnableConfig | None = None,
*,
version: Literal["v1", "v2", "v3"] = "v2",
interrupt_before: All | Sequence[str] | None = None,
interrupt_after: All | Sequence[str] | None = None,
control: Any = None,
transformers: Sequence[Any] | None = None,
headers: dict[str, str] | None = None,
**kwargs: Any,
) -> Any:
"""Stream events from this remote graph.
For `version="v3"`, returns a `_RemoteGraphRunStream` whose surface
matches the local `GraphRunStream`. For other versions, delegates to
`Runnable.stream_events`.
"""
if version != "v3":
return super().stream_events(input, config, version=version, **kwargs)
self._reject_v3_unsupported(
control=control,
transformers=transformers,
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
extra_kwargs=kwargs,
)
sync_client = self._validate_sync_client()
sanitized = self._sanitize_config(merge_configs(self.config, config))
thread_id = sanitized.get("configurable", {}).pop("thread_id", None)
merged_headers = (
_merge_tracing_headers(headers) if self.distributed_tracing else headers
)
sdk_thread = sync_client.threads.stream(
thread_id=thread_id,
assistant_id=self.assistant_id,
headers=merged_headers,
)
return _RemoteGraphRunStream(
sync_client=sync_client,
sdk_thread=sdk_thread,
input=input,
config=sanitized,
metadata=kwargs.get("metadata"),
)
async def astream_events(
self,
input: Any,
config: RunnableConfig | None = None,
*,
version: Literal["v1", "v2"],
include_names: Sequence[All] | None = None,
include_types: Sequence[All] | None = None,
include_tags: Sequence[All] | None = None,
exclude_names: Sequence[All] | None = None,
exclude_types: Sequence[All] | None = None,
exclude_tags: Sequence[All] | None = None,
version: Literal["v1", "v2", "v3"] = "v2",
interrupt_before: All | Sequence[str] | None = None,
interrupt_after: All | Sequence[str] | None = None,
control: Any = None,
transformers: Sequence[Any] | None = None,
headers: dict[str, str] | None = None,
**kwargs: Any,
) -> AsyncIterator[dict[str, Any]]:
raise NotImplementedError
) -> Any:
"""Async-stream events from this remote graph.
For `version="v3"`, awaits to an `_AsyncRemoteGraphRunStream`, matching
the local `Pregel.astream_events(version="v3")` awaitable contract:
`async with await rg.astream_events(..., version="v3") as run`. For
`version="v1"`/`"v2"`, raises NotImplementedError (use `astream`).
"""
if version != "v3":
raise NotImplementedError(
f"RemoteGraph.astream_events(version={version!r}) is not "
"implemented; use astream() for v1/v2 streaming or "
"version='v3'."
)
self._reject_v3_unsupported(
control=control,
transformers=transformers,
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
extra_kwargs=kwargs,
)
client = self._validate_client()
sanitized = self._sanitize_config(merge_configs(self.config, config))
thread_id = sanitized.get("configurable", {}).pop("thread_id", None)
merged_headers = (
_merge_tracing_headers(headers) if self.distributed_tracing else headers
)
sdk_thread = client.threads.stream(
thread_id=thread_id,
assistant_id=self.assistant_id,
headers=merged_headers,
)
return _AsyncRemoteGraphRunStream(
client=client,
sdk_thread=sdk_thread,
input=input,
config=sanitized,
metadata=kwargs.get("metadata"),
)
@overload
def invoke(
+30 -5
View File
@@ -62,6 +62,14 @@ class StreamMux:
`extensions`, `_native` keys are recorded in `native_keys`, and
any StreamChannel instances are bound and (if named) wired.
Transformers with `StreamTransformer.before_builtins = True` are
registered ahead of the rest, preserving relative order within
each lane. This lets content-mutating transformers (PII
redaction, content filters, etc.) run before built-ins like
`MessagesTransformer` that eagerly snapshot text fields into
their projections. See `StreamTransformer.before_builtins` for
the contract and foot-guns.
Args:
transformers: Already-built transformer instances. Registered
only on this mux they are NOT cloned into child
@@ -112,14 +120,31 @@ class StreamMux:
self._pump_fn: Callable[[], bool] | None = None
self._apump_fn: Callable[[], Awaitable[bool]] | None = None
# Factories run first (they propagate to child mini-muxes
# via `_make_child`), then any pre-built `transformers=`
# instances are registered as root-only — they aren't cloned
# for child scopes.
# Factories run first (they propagate to child mini-muxes via
# `_make_child`), then any pre-built `transformers=` instances
# are registered as root-only — they aren't cloned for child
# scopes. Within each group, transformers with
# `before_builtins = True` are registered ahead of the rest so
# they observe (and may mutate) events before built-ins like
# `MessagesTransformer`. The order *within* each lane matches
# the supplied sequence.
pre: list[StreamTransformer] = []
rest: list[StreamTransformer] = []
if factories is not None:
for factory in factories:
self._register(factory(scope))
transformer = factory(scope)
(
pre if getattr(transformer, "before_builtins", False) else rest
).append(transformer)
for transformer in (*pre, *rest):
self._register(transformer)
pre.clear()
rest.clear()
for transformer in transformers or ():
(pre if getattr(transformer, "before_builtins", False) else rest).append(
transformer
)
for transformer in (*pre, *rest):
self._register(transformer)
def transformer_by_key(self, key: str) -> StreamTransformer | None:
+18 -1
View File
@@ -35,7 +35,7 @@ class ProtocolEvent(TypedDict):
"""
type: Literal["event"]
eventId: NotRequired[str]
event_id: NotRequired[str] # snake_case to match the langchain-protocol wire field
seq: NotRequired[int]
method: str # StreamMode value: "values", "messages", "custom", etc.
params: _ProtocolEventParams
@@ -91,11 +91,28 @@ class StreamTransformer(ABC):
which modes a `stream_events(version="v3")` run requests from the graph.
Empty tuple means the transformer consumes only synthetic
events (or is purely passive).
before_builtins: Opt-in for transformers that must run *before*
built-in transformers like `MessagesTransformer` and
`ToolCallTransformer`. The mux partitions factories by this
flag at registration time: `before_builtins = True`
transformers are registered first, then everything else in
the order supplied. Within each lane, registration order is
preserved. This is the supported hook for content-mutating
transformers (PII redaction, profanity filters, etc.) whose
mutations must land before built-ins eagerly snapshot text
fields into their projections. **Foot-gun:** transformers
in this lane see `tasks` events before `LifecycleTransformer`
and `SubgraphTransformer` consume them mutating
`event["params"]["namespace"]` or the data dict's
`id` / `result` / `error` / `interrupts` fields will desync
their bookkeeping. Observe freely; mutate only fields no
built-in reads (e.g. `delta.text` on `messages` events).
"""
requires_async: ClassVar[bool] = False
supports_sync: ClassVar[bool] = False
required_stream_modes: ClassVar[tuple[str, ...]] = ()
before_builtins: ClassVar[bool] = False
def __init__(self, scope: tuple[str, ...] = ()) -> None:
"""Initialize the transformer with its mux's scope.
+115 -9
View File
@@ -8,8 +8,8 @@ from langchain_core.language_models.chat_model_stream import (
AsyncChatModelStream,
ChatModelStream,
)
from langchain_core.messages import AIMessageChunk, BaseMessage
from langchain_protocol.protocol import MessagesData
from langchain_core.messages import AIMessageChunk, BaseMessage, ToolMessage
from langchain_protocol.protocol import LifecycleCause, MessagesData
from typing_extensions import NotRequired, TypedDict
from langgraph.errors import GraphDrained, GraphInterrupt
@@ -203,6 +203,7 @@ class MessagesTransformer(StreamTransformer):
# Correlate protocol events back to a ChatModelStream by run_id
# (attached to the event's metadata by StreamMessagesHandler).
self._by_run: dict[str, ChatModelStream] = {}
self._ignored_runs: set[str] = set()
self._pump_fn: Callable[[], bool] | None = None
self._apump_fn: Callable[[], Awaitable[bool]] | None = None
# Cached as a list once for cheap equality with the protocol
@@ -276,8 +277,10 @@ class MessagesTransformer(StreamTransformer):
self._route_protocol_event(
cast("MessagesData", payload), run_id=run_id, node=node
)
elif isinstance(payload, BaseMessage) and not isinstance(
payload, AIMessageChunk
elif (
isinstance(payload, BaseMessage)
and not isinstance(payload, AIMessageChunk)
and not isinstance(payload, ToolMessage)
):
self._route_whole_message(payload, node=node)
# Legacy AIMessageChunk tuples (from on_llm_new_token) are ignored;
@@ -295,6 +298,11 @@ class MessagesTransformer(StreamTransformer):
) -> None:
event_type = event.get("event")
if event_type == "message-start":
# Tool results are exposed on the tools projection and state
# snapshots; run.messages is the chat-token projection.
if event.get("role") == "tool":
self._ignored_runs.add(run_id)
return
message_id = event.get("message_id")
stream = self._make_stream(
namespace=[],
@@ -304,6 +312,9 @@ class MessagesTransformer(StreamTransformer):
self._by_run[run_id] = stream
self._log.push(stream)
stream.dispatch(event)
elif run_id in self._ignored_runs:
if event_type == "message-finish":
self._ignored_runs.discard(run_id)
elif run_id in self._by_run:
stream = self._by_run[run_id]
stream.dispatch(event)
@@ -319,12 +330,14 @@ class MessagesTransformer(StreamTransformer):
def finalize(self) -> None:
"""Clear any routing state — streams close themselves via `message-finish`."""
self._by_run.clear()
self._ignored_runs.clear()
def fail(self, err: BaseException) -> None:
"""Propagate run error to any streams still open when the graph fails."""
for stream in list(self._by_run.values()):
stream.fail(err)
self._by_run.clear()
self._ignored_runs.clear()
SubgraphStatus = Literal["started", "completed", "failed", "interrupted", "drained"]
@@ -353,6 +366,7 @@ class LifecyclePayload(TypedDict, total=False):
namespace: list[str]
graph_name: NotRequired[str]
trigger_call_id: NotRequired[str]
cause: NotRequired[LifecycleCause]
error: NotRequired[str]
@@ -393,6 +407,18 @@ class _TasksLifecycleBase(StreamTransformer):
# Maps tracked namespace -> task_id of the parent task whose
# `TaskResultPayload` will close it.
self._open: dict[tuple[str, ...], str] = {}
# lc_agent_name observed at each namespace (first task event wins).
# Not read by the base discriminator (which only checks whether the
# current task carries an lc_agent_name); maintained as extension state
# for subclasses that project named subagents — e.g. a `run.subagents`
# transformer reads this to filter to nested runs that have a name.
self._lc_by_ns: dict[tuple[str, ...], str | None] = {}
# Pregel task_id -> triggering LLM tool_call_id, harvested from a task
# whose `input` is a `tool_call_with_context` dict (current shape) or a
# list of tool-call dicts (legacy shape). The child subgraph's segment
# `node:<task_id>` shares this task_id, so a subagent recovers the tool
# call that spawned it (cross-payload).
self._pending_tool_calls: dict[str, str] = {}
# --- Template-method hooks (subclass overrides) ---
@@ -405,6 +431,8 @@ class _TasksLifecycleBase(StreamTransformer):
ns: tuple[str, ...],
graph_name: str | None,
trigger_call_id: str | None,
*,
cause: LifecycleCause | None = None,
) -> None:
"""Fired once per discovered namespace (first observed task event)."""
raise NotImplementedError
@@ -430,18 +458,81 @@ class _TasksLifecycleBase(StreamTransformer):
if "result" in data:
self._handle_task_result(ns, data)
else:
self._handle_task_start(ns)
self._record_identity(ns, data)
self._record_pending_tool_calls(data)
self._handle_task_start(ns, data)
# Tasks events are folded into the synthesized projections;
# suppress from the main event log so iterators don't double-see
# the same information in two shapes.
return False
def _handle_task_start(self, ns: tuple[str, ...]) -> None:
def _record_identity(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
"""Record this namespace's `lc_agent_name` (first task event wins).
Runs for every task-start event, including `ns == self.scope` and
tracked children. Pregel emits parent-namespace tasks before
child-namespace tasks, so under that ordering the parent's identity is
recorded by the time a child event is evaluated in `_handle_task_start`.
"""
if ns in self._lc_by_ns:
return
metadata = data.get("metadata") or {}
self._lc_by_ns[ns] = metadata.get("lc_agent_name")
def _record_pending_tool_calls(self, data: dict[str, Any]) -> None:
"""Harvest a task's triggering tool_call_id keyed by its task id.
A tool-dispatch task seeds `task_id -> tool_call_id`; the spawned
subgraph's namespace segment `node:<task_id>` shares that id, letting
a subagent recover the tool call that caused it across payloads. Two
input shapes are handled: the current Pregel push model schedules each
tool call as its own task whose `input` is a `tool_call_with_context`
dict, while a legacy / batched model passes a list of tool-call dicts.
"""
task_id = data.get("id")
if not isinstance(task_id, str):
return
payload = data.get("input")
tool_call_id: str | None = None
# Current langgraph schedules each tool call as its own push task
# whose input is a `tool_call_with_context` dict.
if isinstance(payload, dict) and isinstance(payload.get("tool_call"), dict):
candidate = payload["tool_call"].get("id")
if isinstance(candidate, str):
tool_call_id = candidate
# Legacy / batched shape: input is a list of tool-call dicts.
elif isinstance(payload, list):
for tc in payload:
if isinstance(tc, dict) and isinstance(tc.get("id"), str):
tool_call_id = tc["id"] # first wins
break
if tool_call_id is not None:
self._pending_tool_calls[task_id] = tool_call_id
def _handle_task_start(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
if not self._should_track(ns) or ns in self._seen:
return
self._seen.add(ns)
graph_name, trigger_call_id = _parse_ns_segment(ns[-1])
self._on_started(ns, graph_name or None, trigger_call_id)
parsed_name, trigger_call_id = _parse_ns_segment(ns[-1])
metadata = data.get("metadata") or {}
child_lc = metadata.get("lc_agent_name")
# A subagent boundary is any nested run carrying an lc_agent_name (set
# by create_agent). Unnamed runs (lc_agent_name None) are excluded.
#
# A same-named nested agent — e.g. a subagent that invokes itself — is
# surfaced because it re-asserts its own lc_agent_name. The trade-off:
# a non-agent subgraph invoked inside a tool inherits the parent's
# lc_agent_name and will also surface (named after the parent). A caller
# that needs to exclude such a graph can null lc_agent_name in the
# config it invokes that graph with.
is_subagent = child_lc is not None
graph_name = child_lc if is_subagent else (parsed_name or None)
cause: LifecycleCause | None = None
if is_subagent and trigger_call_id is not None:
tool_call_id = self._pending_tool_calls.get(trigger_call_id)
if tool_call_id:
cause = {"type": "toolCall", "tool_call_id": str(tool_call_id)}
self._on_started(ns, graph_name, trigger_call_id, cause=cause)
if trigger_call_id is not None:
self._open[ns] = trigger_call_id
@@ -540,6 +631,8 @@ class LifecycleTransformer(_TasksLifecycleBase):
ns: tuple[str, ...],
graph_name: str | None,
trigger_call_id: str | None,
*,
cause: LifecycleCause | None = None,
) -> None:
if trigger_call_id is None:
# Without a task id we can't correlate a parent-result
@@ -550,6 +643,8 @@ class LifecycleTransformer(_TasksLifecycleBase):
if graph_name:
payload["graph_name"] = graph_name
payload["trigger_call_id"] = trigger_call_id
if cause is not None:
payload["cause"] = cause
self._channel.push(payload)
def _on_terminal(
@@ -612,6 +707,8 @@ class SubgraphTransformer(_TasksLifecycleBase):
ns: tuple[str, ...],
graph_name: str | None,
trigger_call_id: str | None,
*,
cause: LifecycleCause | None = None,
) -> None:
if self._mux is None:
return
@@ -620,6 +717,10 @@ class SubgraphTransformer(_TasksLifecycleBase):
except RuntimeError:
return
handle_cls = AsyncSubgraphRunStream if child_mux.is_async else SubgraphRunStream
# `cause` is intentionally ignored here: it is a wire/lifecycle-channel
# concern (carried on `LifecyclePayload`), not something the in-process
# subgraph navigation handle exposes. The argument is accepted only to
# keep the `_on_started` template signature uniform across transformers.
handle = handle_cls(
mux=child_mux,
path=ns,
@@ -724,7 +825,12 @@ class SubgraphTransformer(_TasksLifecycleBase):
for child_ns, status, error in self._pop_terminal_transitions(ns, data):
await self._aon_terminal(child_ns, status, error)
else:
self._handle_task_start(ns)
# Mirror the sync `process` bookkeeping so the async lane
# observes parent identity / tool calls before discriminating
# a subagent boundary.
self._record_identity(ns, data)
self._record_pending_tool_calls(data)
self._handle_task_start(ns, data)
keep = False
else:
keep = True
+10
View File
@@ -150,6 +150,16 @@ class TaskPayload(TypedDict):
"""Input data passed to the task."""
triggers: list[str]
"""List of triggers that caused this task to be executed (e.g. channel writes)."""
metadata: NotRequired[dict[str, Any]]
"""Framework-resolved metadata associated with the task.
Generic dict carrier following the messages-stream pattern. Populated by
`map_debug_tasks` from `task.config["metadata"]` when non-empty, so the
same keys `stream_mode="messages"` consumers see (e.g. `lc_agent_name`,
`langgraph_node`, `langgraph_step`) are available to stream transformers.
Consumers should ignore unrecognized keys.
"""
class TaskResultPayload(TypedDict):
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.2.0"
version = "1.2.3"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
@@ -26,7 +26,7 @@ classifiers = [
dependencies = [
"langchain-core>=1.4.0,<2",
"langgraph-checkpoint>=4.1.0,<5.0.0",
"langgraph-sdk>=0.3.0,<0.4.0",
"langgraph-sdk>=0.4.2,<0.5.0",
"langgraph-prebuilt>=1.1.0,<1.2.0",
"xxhash>=3.5.0",
"pydantic>=2.7.4",
+98 -1
View File
@@ -1,7 +1,8 @@
import pytest
from langchain_core.callbacks import AsyncCallbackManager
from langchain_core.callbacks import AsyncCallbackManager, BaseCallbackHandler
from langgraph._internal._config import get_async_callback_manager_for_config
from langgraph.graph import StateGraph
pytestmark = pytest.mark.anyio
@@ -17,3 +18,99 @@ def test_new_async_manager_merges_tags_with_config() -> None:
config = {"callbacks": None, "tags": ["a"]}
manager = get_async_callback_manager_for_config(config, tags=["b"])
assert manager.inheritable_tags == ["a", "b"]
class _TrackingCallback(BaseCallbackHandler):
def __init__(self) -> None:
self.called = False
def on_chain_start(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
self.called = True
async def test_with_config_callbacks_preserved_in_astream_events() -> None:
"""A callback bound via .with_config(...) must survive when
astream_events injects its own internal callback handler.
Pre-fix: ensure_config overwrites the callbacks key, dropping the
bound handler. Post-fix: the handler list is merged.
"""
builder = StateGraph(dict)
builder.add_node("node", lambda state: state)
builder.add_edge("__start__", "node")
cb = _TrackingCallback()
graph = builder.compile().with_config({"callbacks": [cb]})
async for _ in graph.astream_events({}, version="v2"):
pass
assert cb.called, "user-bound callback was dropped by ensure_config overwrite"
async def test_with_config_configurable_preserved_on_invoke() -> None:
"""A configurable key bound via .with_config(...) must survive when
invoke-time config supplies a different configurable key.
Pre-fix: ensure_config overwrites the entire configurable dict.
Post-fix: the two dicts are shallow-merged per key.
"""
builder = StateGraph(dict)
captured: dict = {}
def node(state, config): # noqa: ANN001
captured.update(config.get("configurable") or {})
return state
builder.add_node("node", node)
builder.add_edge("__start__", "node")
graph = builder.compile().with_config({"configurable": {"ls_agent_type": "root"}})
await graph.ainvoke({}, {"configurable": {"thread_id": "T1"}})
assert captured.get("ls_agent_type") == "root", (
"bound configurable key was dropped by ensure_config overwrite"
)
assert captured.get("thread_id") == "T1", "invoke-time key not present"
async def test_with_config_metadata_preserved_on_invoke() -> None:
"""A metadata key bound via .with_config(...) must survive when
invoke-time config supplies a different metadata key.
Pre-fix: ensure_config overwrites the entire metadata dict.
Post-fix: the two dicts are shallow-merged per key.
"""
builder = StateGraph(dict)
captured: dict = {}
def node(state, config): # noqa: ANN001
captured.update(config.get("metadata") or {})
return state
builder.add_node("node", node)
builder.add_edge("__start__", "node")
graph = builder.compile().with_config({"metadata": {"user_id": "U1"}})
await graph.ainvoke({}, {"metadata": {"correlation_id": "C1"}})
assert captured.get("user_id") == "U1", (
"bound metadata key was dropped by ensure_config overwrite"
)
assert captured.get("correlation_id") == "C1", "invoke-time key not present"
async def test_with_config_tags_preserved_on_invoke() -> None:
"""Tags bound via .with_config(...) must survive when invoke-time
config supplies its own tags.
Pre-fix: ensure_config overwrites the entire tags list.
Post-fix: tags are concatenated (matching merge_configs behavior;
no deduplication, no sorting).
"""
builder = StateGraph(dict)
captured: list = []
def node(state, config): # noqa: ANN001
captured.extend(config.get("tags") or [])
return state
builder.add_node("node", node)
builder.add_edge("__start__", "node")
graph = builder.compile().with_config({"tags": ["bound"]})
await graph.ainvoke({}, {"tags": ["invoke"]})
assert "bound" in captured, "bound tag was dropped by ensure_config overwrite"
assert "invoke" in captured, "invoke-time tag not present"
@@ -0,0 +1,140 @@
"""ensure_message_ids() assigns stable UUIDs to id=None BaseMessages
before DeltaChannel writes are serialised to the checkpoint.
Without this, the checkpoint stores id=None and every get_state() replay
produces a different UUID the same HumanMessage appears with a different
ID in each LangSmith trace / on every resumed invocation.
"""
from __future__ import annotations
from typing import Annotated, Any
import pytest
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel
from langgraph.graph import END, START, StateGraph
pytestmark = pytest.mark.anyio
def _append_reducer(
state: list[AnyMessage], writes: list[list[AnyMessage]]
) -> list[AnyMessage]:
"""Simple append — no ID assignment. IDs come from ensure_message_ids()."""
result = list(state)
for w in writes:
if isinstance(w, list):
result.extend(w)
else:
result.append(w) # type: ignore[arg-type]
return result
def _build_graph(checkpointer: Any) -> Any:
State = TypedDict( # noqa: UP013
"State",
{
"messages": Annotated[
list, DeltaChannel(_append_reducer, snapshot_frequency=50)
]
},
) # type: ignore[call-overload]
def agent(state: dict) -> dict:
return {"messages": [AIMessage(content="reply", id="ai-1")]}
return (
StateGraph(State)
.add_node("agent", agent)
.add_edge(START, "agent")
.add_edge("agent", END)
.compile(checkpointer=checkpointer)
)
def test_delta_channel_message_gets_id_and_stays_stable() -> None:
"""Messages written with id=None must receive a stable UUID.
ensure_message_ids() is called in put_writes() before the background
thread serialises DeltaChannel writes. The checkpoint stores the
assigned UUID, so every get_state() replay sees the same ID.
"""
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "id-stability"}}
graph.invoke({"messages": [HumanMessage(content="hello")]}, config)
ids = [
next(
m.id
for m in graph.get_state(config).values["messages"]
if isinstance(m, HumanMessage)
)
for _ in range(3)
]
assert ids[0] is not None, "ensure_message_ids should have assigned a UUID"
assert len(set(ids)) == 1, (
f"HumanMessage id must be stable across get_state() calls; "
f"got {ids}. The checkpoint is storing id=None."
)
async def test_delta_channel_message_gets_id_and_stays_stable_async() -> None:
"""Same check via ainvoke (AsyncPregelLoop path)."""
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "id-stability-async"}}
await graph.ainvoke({"messages": [HumanMessage(content="hello")]}, config)
ids = [
next(
m.id
for m in (await graph.aget_state(config)).values["messages"]
if isinstance(m, HumanMessage)
)
for _ in range(3)
]
assert ids[0] is not None, "ensure_message_ids should have assigned a UUID"
assert len(set(ids)) == 1, (
f"Async path: HumanMessage id unstable across aget_state() calls: {ids}"
)
def test_delta_channel_dict_style_message_gets_stable_id() -> None:
"""Dict-style inputs (API / over-the-wire format) must also get stable IDs.
When the graph is invoked via the LangGraph API the input arrives as a raw
dict {"role": "user", "content": "..."} rather than a BaseMessage object.
ensure_message_ids() must coerce those dicts to typed BaseMessages and
stamp a UUID so the checkpoint never stores an id-less message.
"""
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "dict-id-stability"}}
# Invoke with a raw dict (the format LangGraph API sends)
graph.invoke({"messages": [{"role": "user", "content": "hello"}]}, config)
ids = [
next(
m.id
for m in graph.get_state(config).values["messages"]
if isinstance(m, HumanMessage)
)
for _ in range(3)
]
assert ids[0] is not None, (
"dict-style message should have been coerced and assigned a UUID"
)
assert len(set(ids)) == 1, (
f"dict-style HumanMessage id must be stable across get_state() calls; got {ids}"
)
+185
View File
@@ -0,0 +1,185 @@
"""Tests for langgraph.pregel.debug helpers."""
from __future__ import annotations
from langgraph.pregel.debug import map_debug_tasks
class _FakeTask:
"""Minimal stand-in for PregelExecutableTask covering only what map_debug_tasks reads."""
def __init__(
self,
*,
id: str,
name: str,
input: object,
triggers: list[str],
config: dict | None,
) -> None:
self.id = id
self.name = name
self.input = input
self.triggers = triggers
self.config = config
def test_map_debug_tasks_forwards_metadata_when_present() -> None:
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config={"metadata": {"lc_agent_name": "weather_agent"}},
)
payloads = list(map_debug_tasks([task]))
assert len(payloads) == 1
payload = payloads[0]
assert payload["id"] == "t1"
assert payload["name"] == "tools"
assert payload["metadata"] == {"lc_agent_name": "weather_agent"}
def test_map_debug_tasks_omits_metadata_when_empty() -> None:
# Empty metadata dict in config: don't include metadata in the payload.
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config={"metadata": {}},
)
payloads = list(map_debug_tasks([task]))
assert "metadata" not in payloads[0]
def test_map_debug_tasks_omits_metadata_when_absent() -> None:
# No metadata key in config: don't include metadata in the payload.
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config={},
)
payloads = list(map_debug_tasks([task]))
assert "metadata" not in payloads[0]
def test_map_debug_tasks_handles_none_config() -> None:
# task.config can be None; should not crash.
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config=None,
)
payloads = list(map_debug_tasks([task]))
assert len(payloads) == 1
assert "metadata" not in payloads[0]
def test_map_debug_tasks_filters_framework_metadata_keys() -> None:
"""Internal framework keys are dropped from the forwarded metadata; only
user-meaningful keys (lc_agent_name, ls_integration, user metadata) ride
along. The framework keys (langgraph_*, thread_id, checkpoint_*) are
redundant with the task's own fields/namespace.
"""
md = {
"lc_agent_name": "weather_agent",
"ls_integration": "langchain_create_agent",
"my_user_key": "x",
"thread_id": "thread-1",
"langgraph_step": 1,
"langgraph_node": "tools",
"langgraph_path": ("__pregel_pull", "tools"),
"langgraph_checkpoint_ns": "tools:abc",
"checkpoint_ns": "",
}
task = _FakeTask(
id="t1", name="tools", input=[], triggers=["x"], config={"metadata": md}
)
payload = next(iter(map_debug_tasks([task])))
assert payload["metadata"] == {
"lc_agent_name": "weather_agent",
"ls_integration": "langchain_create_agent",
"my_user_key": "x",
}
def test_map_debug_tasks_omits_metadata_when_only_framework_keys() -> None:
"""A task whose metadata is entirely framework keys (e.g. a plain
StateGraph node) yields no `metadata` key after filtering.
"""
md = {
"thread_id": "thread-1",
"langgraph_step": 1,
"langgraph_node": "worker",
"langgraph_checkpoint_ns": "worker:abc",
"checkpoint_ns": "",
}
task = _FakeTask(
id="t1", name="worker", input=[], triggers=["x"], config={"metadata": md}
)
payload = next(iter(map_debug_tasks([task])))
assert "metadata" not in payload
def test_map_debug_tasks_metadata_is_copied_not_referenced() -> None:
"""Mutating the source config after emission must not affect the
payload TaskPayload.metadata is a defensive copy.
"""
md = {"lc_agent_name": "a"}
task = _FakeTask(
id="t1", name="tools", input=[], triggers=["x"], config={"metadata": md}
)
payload = next(iter(map_debug_tasks([task])))
md["lc_agent_name"] = "MUTATED"
assert payload["metadata"]["lc_agent_name"] == "a"
def test_map_debug_tasks_folds_filtered_tags_into_metadata() -> None:
"""Config tags are folded into TaskPayload.metadata under `tags`, with
langchain's internal `seq:step:*` tags filtered out — mirroring the
messages stream handler so both channels surface the same tag set."""
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config={
"metadata": {"lc_agent_name": "weather_agent"},
"tags": ["seq:step:1", "user-tag", "session-123"],
},
)
payload = next(iter(map_debug_tasks([task])))
assert payload["metadata"]["lc_agent_name"] == "weather_agent"
assert payload["metadata"]["tags"] == ["user-tag", "session-123"]
def test_map_debug_tasks_omits_tags_when_only_seq_step() -> None:
"""If the only tags are internal `seq:step:*` markers, no `tags` key is
added (matches the messages handler's `if filtered_tags:` guard)."""
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config={"metadata": {"lc_agent_name": "a"}, "tags": ["seq:step:1"]},
)
payload = next(iter(map_debug_tasks([task])))
assert "tags" not in payload["metadata"]
def test_map_debug_tasks_adds_tags_even_without_other_metadata() -> None:
"""Filtered tags surface even when config has no metadata dict."""
task = _FakeTask(
id="t1",
name="tools",
input=[],
triggers=["x"],
config={"tags": ["user-tag"]},
)
payload = next(iter(map_debug_tasks([task])))
assert payload["metadata"] == {"tags": ["user-tag"]}
@@ -0,0 +1,658 @@
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from langgraph.pregel._remote_run_stream import (
_AsyncRemoteGraphRunStream,
_ChannelProjection,
_ProjectionRegistry,
_RemoteGraphRunStream,
_translate_command_input,
)
from langgraph.pregel.remote import (
_V3_SUPPORTED_KWARGS,
RemoteGraph,
)
from langgraph.types import Command
def _make_sync_adapter(*, run_start_returns=None, run_start_raises=None):
sync_client = MagicMock()
sdk_thread = MagicMock()
sdk_thread.thread_id = "thread-abc"
sdk_thread.__enter__ = MagicMock(return_value=sdk_thread)
sdk_thread.__exit__ = MagicMock(return_value=None)
sdk_thread.run = MagicMock()
if run_start_raises is not None:
sdk_thread.run.start = MagicMock(side_effect=run_start_raises)
else:
sdk_thread.run.start = MagicMock(
return_value=run_start_returns or {"run_id": "run-xyz"}
)
adapter = _RemoteGraphRunStream(
sync_client=sync_client,
sdk_thread=sdk_thread,
input={"x": 1},
config={"configurable": {}},
metadata=None,
)
return adapter, sync_client, sdk_thread
def test_enter_calls_sdk_enter_then_run_start_and_captures_run_id():
adapter, _, sdk_thread = _make_sync_adapter()
with adapter as stream:
assert stream is adapter
sdk_thread.__enter__.assert_called_once()
sdk_thread.run.start.assert_called_once_with(
input={"x": 1}, config={"configurable": {}}, metadata=None
)
assert adapter._run_id == "run-xyz"
def test_exit_delegates_to_sdk_exit_with_exc_info():
adapter, _, sdk_thread = _make_sync_adapter()
with adapter:
pass
sdk_thread.__exit__.assert_called_once_with(None, None, None)
def test_enter_unwinds_sdk_cm_when_run_start_raises():
adapter, _, sdk_thread = _make_sync_adapter(
run_start_raises=RuntimeError("start boom")
)
with pytest.raises(RuntimeError, match="start boom"):
with adapter:
pytest.fail("body should not run")
sdk_thread.__enter__.assert_called_once()
sdk_thread.__exit__.assert_called_once()
exc_info = sdk_thread.__exit__.call_args.args
assert exc_info[0] is RuntimeError
assert isinstance(exc_info[1], RuntimeError)
assert adapter._run_id is None
def test_output_interrupted_interrupts_passthrough():
adapter, _, sdk_thread = _make_sync_adapter()
sdk_thread.output = {"foo": 1}
sdk_thread.interrupted = True
sdk_thread.interrupts = [{"interrupt_id": "i1", "namespace": [], "value": "v"}]
with adapter as stream:
assert stream.output == {"foo": 1}
assert stream.interrupted is True
assert stream.interrupts == [
{"interrupt_id": "i1", "namespace": [], "value": "v"}
]
def test_sync_projection_attrs_forward_to_sdk():
adapter, _, sdk_thread = _make_sync_adapter()
sdk_thread.values = object()
sdk_thread.messages = object()
sdk_thread.tool_calls = object()
sdk_thread.subgraphs = object()
with adapter as stream:
assert stream.values is sdk_thread.values
assert stream.messages is sdk_thread.messages
assert stream.tool_calls is sdk_thread.tool_calls
assert stream.subgraphs is sdk_thread.subgraphs
assert set(stream.extensions) == set(_ProjectionRegistry._NATIVE)
assert stream.extensions["values"] is sdk_thread.values
def test_projection_registry_typed_decoded_and_custom():
sdk = MagicMock()
sdk.values = object()
sdk.messages = object()
sdk.tool_calls = object()
sdk.subgraphs = object()
custom_named = object()
sdk.extensions = {"my_custom": custom_named}
registry = _ProjectionRegistry(sdk)
# Typed channels resolve to the SDK's decoded projections.
assert registry["values"] is sdk.values
assert registry["tool_calls"] is sdk.tool_calls
assert registry["subgraphs"] is sdk.subgraphs
# Channels without a typed projection resolve to a decoding _ChannelProjection.
ckpt = registry["checkpoints"]
assert isinstance(ckpt, _ChannelProjection)
assert ckpt._channel == "checkpoints"
assert isinstance(registry["updates"], _ChannelProjection)
# A non-protocol name is a specific custom-extension channel.
assert registry["my_custom"] is custom_named
# Enumerable set is the typed + decoded channels (no `lifecycle`, no `debug`).
assert list(registry) == [
"values",
"messages",
"tool_calls",
"subgraphs",
"updates",
"checkpoints",
"tasks",
"custom",
]
assert len(registry) == 8
def test_channel_projection_decodes_params_data():
sdk = MagicMock()
# Real wire events carry `method`; the SDK `DataDecoder` yields matching
# events' `params.data` and skips dataless and off-channel ones.
sdk.subscribe = MagicMock(
return_value=iter(
[
{"method": "checkpoints", "params": {"data": {"n": 1}}},
{"method": "checkpoints", "params": {}}, # no data -> skipped
{"method": "checkpoints", "params": {"data": {"n": 2}}},
{"method": "lifecycle", "params": {"data": {"n": 3}}}, # other channel
]
)
)
proj = _ChannelProjection(sdk, "checkpoints")
assert list(proj) == [{"n": 1}, {"n": 2}]
sdk.subscribe.assert_called_once_with(["checkpoints"])
@pytest.mark.anyio
async def test_channel_projection_decodes_params_data_async():
"""Async lane mirrors the sync lane: `async for` over the SDK's async
subscription, decoded through the same `DataDecoder`."""
class _FakeAsyncEvents:
def __init__(self, items):
self._items = list(items)
def __aiter__(self):
return self
async def __anext__(self):
if not self._items:
raise StopAsyncIteration
return self._items.pop(0)
sdk = MagicMock()
sdk.subscribe = MagicMock(
return_value=_FakeAsyncEvents(
[
{"method": "checkpoints", "params": {"data": {"n": 1}}},
{"method": "checkpoints", "params": {}}, # no data -> skipped
{"method": "checkpoints", "params": {"data": {"n": 2}}},
{"method": "lifecycle", "params": {"data": {"n": 3}}}, # other channel
]
)
)
proj = _ChannelProjection(sdk, "checkpoints")
assert [item async for item in proj] == [{"n": 1}, {"n": 2}]
sdk.subscribe.assert_called_once_with(["checkpoints"])
def test_sync_adapter_translates_command_input():
sync_client = MagicMock()
sdk_thread = MagicMock()
adapter = _RemoteGraphRunStream(
sync_client=sync_client,
sdk_thread=sdk_thread,
input=Command(resume="go"),
config=None,
metadata=None,
)
assert adapter._start_kwargs["input"] == "go"
def test_iter_caches_first_subscription():
adapter, _, sdk_thread = _make_sync_adapter()
fake_events = [object(), object(), object()]
sdk_thread.events = iter(fake_events)
with adapter as stream:
first = iter(stream)
second = iter(stream)
assert first is second
assert list(first) == fake_events
def test_abort_cancels_run_and_closes_sdk():
adapter, sync_client, sdk_thread = _make_sync_adapter()
with adapter as stream:
stream.abort()
sync_client.runs.cancel.assert_called_once_with(
"thread-abc", "run-xyz", wait=False
)
sdk_thread.close.assert_called_once()
def test_abort_before_enter_skips_cancel_but_closes_sdk():
adapter, sync_client, sdk_thread = _make_sync_adapter()
adapter.abort()
sync_client.runs.cancel.assert_not_called()
sdk_thread.close.assert_called_once()
def test_abort_is_idempotent():
adapter, sync_client, sdk_thread = _make_sync_adapter()
with adapter as stream:
stream.abort()
stream.abort()
assert sync_client.runs.cancel.call_count == 1
assert sdk_thread.close.call_count == 1
def test_abort_swallows_cancel_failure_and_still_closes():
adapter, sync_client, sdk_thread = _make_sync_adapter()
sync_client.runs.cancel.side_effect = RuntimeError("cancel boom")
with adapter as stream:
stream.abort()
sdk_thread.close.assert_called_once()
def test_sync_interleave_delegates_to_interleave_projections():
adapter, _, sdk_thread = _make_sync_adapter()
pairs = [("values", {"x": 1}), ("messages", object())]
sdk_thread.interleave_projections.return_value = pairs
with adapter as stream:
result = list(stream.interleave("values", "messages"))
assert result == pairs
sdk_thread.interleave_projections.assert_called_once_with(["values", "messages"])
def test_async_adapter_has_no_interleave():
"""Async adapter intentionally lacks `interleave` (mirrors local
`AsyncGraphRunStream`, which doesn't have one either). Async callers
compose with `asyncio.gather` / `asyncio.as_completed`.
"""
assert not hasattr(_AsyncRemoteGraphRunStream, "interleave")
def _make_async_adapter(*, run_start_returns=None, run_start_raises=None):
client = MagicMock()
client.runs.cancel = AsyncMock()
sdk_thread = MagicMock()
sdk_thread.thread_id = "thread-abc"
sdk_thread.__aenter__ = AsyncMock(return_value=sdk_thread)
sdk_thread.__aexit__ = AsyncMock(return_value=None)
sdk_thread.close = AsyncMock()
sdk_thread.run = MagicMock()
if run_start_raises is not None:
sdk_thread.run.start = AsyncMock(side_effect=run_start_raises)
else:
sdk_thread.run.start = AsyncMock(
return_value=run_start_returns or {"run_id": "run-xyz"}
)
adapter = _AsyncRemoteGraphRunStream(
client=client,
sdk_thread=sdk_thread,
input={"x": 1},
config={"configurable": {}},
metadata=None,
)
return adapter, client, sdk_thread
@pytest.mark.anyio
async def test_aenter_calls_sdk_aenter_then_run_start_and_captures_run_id():
adapter, _, sdk_thread = _make_async_adapter()
async with adapter as stream:
assert stream is adapter
sdk_thread.__aenter__.assert_awaited_once()
sdk_thread.run.start.assert_awaited_once_with(
input={"x": 1}, config={"configurable": {}}, metadata=None
)
assert adapter._run_id == "run-xyz"
@pytest.mark.anyio
async def test_aexit_delegates_to_sdk_aexit():
adapter, _, sdk_thread = _make_async_adapter()
async with adapter:
pass
sdk_thread.__aexit__.assert_awaited_once_with(None, None, None)
@pytest.mark.anyio
async def test_aenter_unwinds_sdk_cm_when_run_start_raises():
adapter, _, sdk_thread = _make_async_adapter(
run_start_raises=RuntimeError("start boom")
)
with pytest.raises(RuntimeError, match="start boom"):
async with adapter:
pytest.fail("body should not run")
sdk_thread.__aenter__.assert_awaited_once()
sdk_thread.__aexit__.assert_awaited_once()
assert adapter._run_id is None
@pytest.mark.anyio
async def test_async_output_interrupted_interrupts_passthrough():
adapter, _, sdk_thread = _make_async_adapter()
async def _fake_output_awaitable():
return {"foo": 1}
sdk_thread.output = _fake_output_awaitable()
sdk_thread.interrupted = True
sdk_thread.interrupts = [{"interrupt_id": "i1", "namespace": [], "value": "v"}]
async with adapter as stream:
assert await stream.output() == {"foo": 1}
assert await stream.interrupted() is True
assert await stream.interrupts() == [
{"interrupt_id": "i1", "namespace": [], "value": "v"}
]
@pytest.mark.anyio
async def test_async_projection_attrs_forward_to_sdk():
adapter, _, sdk_thread = _make_async_adapter()
sdk_thread.values = object()
sdk_thread.messages = object()
sdk_thread.tool_calls = object()
sdk_thread.subgraphs = object()
async with adapter as stream:
assert stream.values is sdk_thread.values
assert stream.messages is sdk_thread.messages
assert stream.tool_calls is sdk_thread.tool_calls
assert stream.subgraphs is sdk_thread.subgraphs
assert set(stream.extensions) == set(_ProjectionRegistry._NATIVE)
assert stream.extensions["messages"] is sdk_thread.messages
@pytest.mark.anyio
async def test_aiter_caches_first_subscription():
adapter, _, sdk_thread = _make_async_adapter()
class _FakeAsyncEvents:
def __init__(self, items):
self._items = list(items)
def __aiter__(self):
return self
async def __anext__(self):
if not self._items:
raise StopAsyncIteration
return self._items.pop(0)
sdk_thread.events = _FakeAsyncEvents([object(), object()])
async with adapter as stream:
first = stream.__aiter__()
second = stream.__aiter__()
assert first is second
@pytest.mark.anyio
async def test_async_abort_cancels_run_and_closes_sdk():
adapter, client, sdk_thread = _make_async_adapter()
async with adapter as stream:
await stream.abort()
client.runs.cancel.assert_awaited_once_with("thread-abc", "run-xyz", wait=False)
sdk_thread.close.assert_awaited_once()
@pytest.mark.anyio
async def test_async_abort_before_aenter_skips_cancel():
adapter, client, sdk_thread = _make_async_adapter()
await adapter.abort()
client.runs.cancel.assert_not_awaited()
sdk_thread.close.assert_awaited_once()
@pytest.mark.anyio
async def test_async_abort_swallows_cancel_failure():
adapter, client, sdk_thread = _make_async_adapter()
client.runs.cancel.side_effect = RuntimeError("cancel boom")
async with adapter as stream:
await stream.abort()
sdk_thread.close.assert_awaited_once()
def _make_remote_graph() -> RemoteGraph:
sync_client = MagicMock()
async_client = MagicMock()
rg = RemoteGraph(
"agent",
client=async_client,
sync_client=sync_client,
)
return rg
def test_reject_v3_unsupported_passes_when_all_clear():
rg = _make_remote_graph()
rg._reject_v3_unsupported(
control=None,
transformers=None,
interrupt_before=None,
interrupt_after=None,
extra_kwargs={},
)
@pytest.mark.parametrize(
"kwarg_name,kwarg_value",
[
("control", object()),
("transformers", [object()]),
("interrupt_before", ["node_a"]),
("interrupt_after", ["node_b"]),
],
)
def test_reject_v3_unsupported_raises_per_kwarg(kwarg_name, kwarg_value):
rg = _make_remote_graph()
kwargs = dict(
control=None,
transformers=None,
interrupt_before=None,
interrupt_after=None,
extra_kwargs={},
)
kwargs[kwarg_name] = kwarg_value
with pytest.raises(NotImplementedError, match=f"`{kwarg_name}=`"):
rg._reject_v3_unsupported(**kwargs)
def test_reject_v3_unsupported_raises_on_unknown_extra_kwarg():
rg = _make_remote_graph()
with pytest.raises(NotImplementedError, match="context"):
rg._reject_v3_unsupported(
control=None,
transformers=None,
interrupt_before=None,
interrupt_after=None,
extra_kwargs={"context": {}},
)
def test_reject_v3_unsupported_allows_metadata_and_headers():
rg = _make_remote_graph()
rg._reject_v3_unsupported(
control=None,
transformers=None,
interrupt_before=None,
interrupt_after=None,
extra_kwargs={"metadata": {"a": 1}, "headers": {"X": "y"}},
)
def test_translate_command_input_surfaces_raw_resume_value():
# The v3 server wraps the resume `input` as {"resume": input} itself, so the
# wire `input` must be the raw resume value, not the serialized dataclass.
assert _translate_command_input(Command(resume="go")) == "go"
assert _translate_command_input(Command(resume={"id": "v"})) == {"id": "v"}
def test_translate_command_input_rejects_goto_and_update():
with pytest.raises(NotImplementedError, match="goto"):
_translate_command_input(Command(goto="node_b"))
with pytest.raises(NotImplementedError, match="update"):
_translate_command_input(Command(update={"a": 1}))
def test_translate_command_input_passes_through_non_command():
assert _translate_command_input({"a": 1}) == {"a": 1}
assert _translate_command_input(None) is None
def test_v3_supported_kwargs_known_set():
assert _V3_SUPPORTED_KWARGS == frozenset({"metadata", "headers"})
def test_stream_events_v3_constructs_sdk_thread_with_sanitized_args():
sync_client = MagicMock()
sdk_thread = MagicMock()
sync_client.threads.stream.return_value = sdk_thread
rg = RemoteGraph(
"agent",
client=MagicMock(),
sync_client=sync_client,
)
result = rg.stream_events(
{"input_key": 1},
config={"configurable": {"thread_id": "t1", "user": "u"}},
version="v3",
)
assert isinstance(result, _RemoteGraphRunStream)
sync_client.threads.stream.assert_called_once()
call = sync_client.threads.stream.call_args
assert call.kwargs["thread_id"] == "t1"
assert call.kwargs["assistant_id"] == "agent"
assert call.kwargs["headers"] is None
def test_stream_events_v3_passes_none_thread_id_when_absent():
sync_client = MagicMock()
sync_client.threads.stream.return_value = MagicMock()
rg = RemoteGraph("agent", client=MagicMock(), sync_client=sync_client)
rg.stream_events({"x": 1}, version="v3")
call = sync_client.threads.stream.call_args
assert call.kwargs["thread_id"] is None
def test_stream_events_v3_rejects_unsupported_kwargs_before_sdk_call():
sync_client = MagicMock()
rg = RemoteGraph("agent", client=MagicMock(), sync_client=sync_client)
with pytest.raises(NotImplementedError, match="control"):
rg.stream_events({"x": 1}, version="v3", control=object())
sync_client.threads.stream.assert_not_called()
def test_stream_events_v3_translates_command_input():
sync_client = MagicMock()
sync_client.threads.stream.return_value = MagicMock()
rg = RemoteGraph("agent", client=MagicMock(), sync_client=sync_client)
# Resume Command surfaces its raw resume value as the wire `input`; the v3
# server wraps it as {"resume": input} once it detects the interrupt.
adapter = rg.stream_events(Command(resume="go"), version="v3")
assert adapter._start_kwargs["input"] == "go"
def test_stream_events_v3_rejects_goto_update_command():
rg = RemoteGraph("agent", client=MagicMock(), sync_client=MagicMock())
with pytest.raises(NotImplementedError, match="goto"):
rg.stream_events(Command(goto="node_b"), version="v3")
def test_stream_events_v3_strips_checkpoint_keys_from_configurable():
sync_client = MagicMock()
sync_client.threads.stream.return_value = MagicMock()
rg = RemoteGraph("agent", client=MagicMock(), sync_client=sync_client)
adapter = rg.stream_events(
{"x": 1},
config={
"configurable": {
"thread_id": "t1",
"checkpoint_id": "c1",
"checkpoint_ns": "ns",
"user": "u",
}
},
version="v3",
)
sent_config = adapter._start_kwargs["config"]
assert "checkpoint_id" not in sent_config["configurable"]
assert "checkpoint_ns" not in sent_config["configurable"]
assert sent_config["configurable"]["user"] == "u"
def test_stream_events_v3_merges_tracing_headers_when_distributed_tracing(
monkeypatch,
):
from langgraph.pregel import remote as remote_mod
sync_client = MagicMock()
sync_client.threads.stream.return_value = MagicMock()
rg = RemoteGraph(
"agent",
client=MagicMock(),
sync_client=sync_client,
distributed_tracing=True,
)
captured = {}
def fake_merge(headers):
captured["arg"] = headers
return {"x-ls-trace": "1", **(headers or {})}
monkeypatch.setattr(remote_mod, "_merge_tracing_headers", fake_merge)
rg.stream_events({"x": 1}, version="v3", headers={"X-Custom": "y"})
assert captured["arg"] == {"X-Custom": "y"}
sent_headers = sync_client.threads.stream.call_args.kwargs["headers"]
assert sent_headers["x-ls-trace"] == "1"
assert sent_headers["X-Custom"] == "y"
def test_stream_events_v3_passes_headers_unchanged_without_tracing():
sync_client = MagicMock()
sync_client.threads.stream.return_value = MagicMock()
rg = RemoteGraph(
"agent",
client=MagicMock(),
sync_client=sync_client,
distributed_tracing=False,
)
rg.stream_events({"x": 1}, version="v3", headers={"X-Custom": "y"})
sent_headers = sync_client.threads.stream.call_args.kwargs["headers"]
assert sent_headers == {"X-Custom": "y"}
def test_stream_events_non_v3_delegates_to_super():
rg = RemoteGraph("agent", client=MagicMock(), sync_client=MagicMock())
sync_client_attr = rg.sync_client
try:
rg.stream_events({"x": 1}, version="v2")
except Exception:
pass
sync_client_attr.threads.stream.assert_not_called()
@pytest.mark.anyio
async def test_astream_events_v3_constructs_sdk_thread():
client = MagicMock()
sdk_thread = MagicMock()
client.threads.stream.return_value = sdk_thread
rg = RemoteGraph("agent", client=client, sync_client=MagicMock())
result = await rg.astream_events(
{"x": 1},
config={"configurable": {"thread_id": "t1"}},
version="v3",
)
assert isinstance(result, _AsyncRemoteGraphRunStream)
call = client.threads.stream.call_args
assert call.kwargs["thread_id"] == "t1"
assert call.kwargs["assistant_id"] == "agent"
@pytest.mark.anyio
async def test_astream_events_v3_rejects_unsupported_kwargs():
client = MagicMock()
rg = RemoteGraph("agent", client=client, sync_client=MagicMock())
with pytest.raises(NotImplementedError, match="transformers"):
await rg.astream_events({"x": 1}, version="v3", transformers=[object()])
client.threads.stream.assert_not_called()
@pytest.mark.anyio
async def test_astream_events_non_v3_raises_not_implemented():
rg = RemoteGraph("agent", client=MagicMock(), sync_client=MagicMock())
with pytest.raises(NotImplementedError, match="not implemented"):
await rg.astream_events({"x": 1}, version="v2")
+140 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import contextlib
import operator
import sys
import threading
@@ -35,7 +36,13 @@ from langgraph._internal._runnable import RunnableCallable
from langgraph._internal._timeout import coerce_timeout_policy
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.errors import GraphInterrupt, NodeError, NodeTimeoutError, ParentCommand
from langgraph.errors import (
GraphInterrupt,
NodeCancelledError,
NodeError,
NodeTimeoutError,
ParentCommand,
)
from langgraph.func import entrypoint, task
from langgraph.graph import END, START, StateGraph, add_messages
from langgraph.pregel import NodeBuilder, Pregel
@@ -63,6 +70,18 @@ NEEDS_CONTEXTVARS = pytest.mark.skipif(
reason="Python 3.11+ is required for async contextvars support",
)
# `asyncio.Task.cancelling()` is Python 3.11+. The LSD-1507 fix in
# `langgraph/pregel/_retry.py` falls back to a no-op on 3.10 (preserves the
# existing CancelledError-as-silent-tear-down behaviour) because there is no
# reliable way to distinguish user-raised from framework-initiated
# cancellation without that API. Tests for the converted behaviour gate on
# the same Python version boundary.
NEEDS_TASK_CANCELLING = pytest.mark.skipif(
sys.version_info < (3, 11),
reason="LSD-1507 user-cancellation conversion requires Python 3.11+ "
"(asyncio.Task.cancelling)",
)
def test_should_retry_on_single_exception():
"""Test retry with a single exception type."""
@@ -2802,3 +2821,123 @@ def test_error_handler_resumes_after_crash_multiple_nodes():
assert call_count["handler_b"] == 2 # ran again on resume
assert "recovered_a:a" in result["results"]
assert "recovered_b:b" in result["results"]
@NEEDS_TASK_CANCELLING
@pytest.mark.anyio
async def test_arun_with_retry_user_raised_cancelled_becomes_node_cancelled():
class UserCancelsProc:
async def ainvoke(self, input, config):
raise asyncio.CancelledError("nope")
task = _make_task(UserCancelsProc(), name="user-cancel")
with pytest.raises(NodeCancelledError) as excinfo:
await arun_with_retry(task, retry_policy=None)
assert excinfo.value.node == "user-cancel"
# original CancelledError chained for debugging
assert isinstance(excinfo.value.__cause__, asyncio.CancelledError)
@NEEDS_TASK_CANCELLING
@pytest.mark.anyio
async def test_arun_with_retry_user_raised_cancelled_with_timeout_policy():
"""The timeout path runs the node in a child task; the conversion must
still trigger for user-raised ``CancelledError``."""
class UserCancelsProc:
async def ainvoke(self, input, config):
raise asyncio.CancelledError
task = _make_task(
UserCancelsProc(), timeout=_idle_timeout(1.0), name="user-cancel-timed"
)
with pytest.raises(NodeCancelledError) as excinfo:
await arun_with_retry(task, retry_policy=None)
assert excinfo.value.node == "user-cancel-timed"
def test_run_with_retry_sync_node_raising_cancelled_becomes_node_cancelled():
class SyncUserCancelsProc:
def invoke(self, input, config):
raise asyncio.CancelledError("sync nope")
task = _make_task(SyncUserCancelsProc(), timeout=None, name="sync-user-cancel")
with pytest.raises(NodeCancelledError) as excinfo:
run_with_retry(task, retry_policy=None)
assert excinfo.value.node == "sync-user-cancel"
assert isinstance(excinfo.value.__cause__, asyncio.CancelledError)
@NEEDS_TASK_CANCELLING
@pytest.mark.anyio
async def test_arun_with_retry_external_cancel_propagates_as_cancelled():
"""When the asyncio task running ``arun_with_retry`` is cancelled from the
outside, the cancellation must still propagate as
``asyncio.CancelledError``. Converting it to ``NodeCancelledError`` would
break the runner's ability to cancel sibling tasks during cleanup."""
started = asyncio.Event()
observed: list[BaseException] = []
class SlowProc:
async def ainvoke(self, input, config):
started.set()
await asyncio.sleep(10.0)
return "never"
task = _make_task(SlowProc(), timeout=None, name="external-cancel")
async def runner():
try:
await arun_with_retry(task, retry_policy=None)
except BaseException as exc:
observed.append(exc)
raise
bg = asyncio.create_task(runner())
await started.wait()
bg.cancel()
# We expect the cancellation to surface to us as well; swallow it here so
# the test runner's own task isn't poisoned by the cancel.
with contextlib.suppress(asyncio.CancelledError):
await bg
assert observed, "runner did not observe any exception"
# Framework cancellation must remain a CancelledError, not be rewritten as
# NodeCancelledError.
assert isinstance(observed[0], asyncio.CancelledError)
assert not isinstance(observed[0], NodeCancelledError)
@NEEDS_TASK_CANCELLING
@pytest.mark.anyio
async def test_pregel_user_raised_cancellederror_fails_run():
"""End-to-end: a two-branch graph where one branch raises
``asyncio.CancelledError`` must fail the run instead of returning
a partial-success state. This is the LSD-1507 customer scenario."""
class _S(TypedDict, total=False):
vals: Annotated[list[str], operator.add]
async def ok(state: _S) -> _S:
return {"vals": ["ok"]}
async def boom(state: _S) -> _S:
raise asyncio.CancelledError("user-raised in node body")
graph = (
StateGraph(_S)
.add_node("ok", ok)
.add_node("boom", boom)
.add_edge(START, "ok")
.add_edge(START, "boom")
.add_edge("ok", END)
.add_edge("boom", END)
.compile()
)
with pytest.raises(NodeCancelledError) as excinfo:
await graph.ainvoke({"vals": []})
assert excinfo.value.node == "boom"
@@ -0,0 +1,296 @@
"""Tests for `StreamTransformer.before_builtins` lane ordering.
`before_builtins = True` transformers are registered ahead of the
rest, preserving relative order within each lane. This lets
content-mutating transformers run before built-ins like
`MessagesTransformer` that eagerly snapshot text fields into their
projections.
"""
from __future__ import annotations
import time
from typing import Any, ClassVar
from langgraph.stream._mux import StreamMux
from langgraph.stream._types import StreamTransformer
from langgraph.stream.stream_channel import StreamChannel
from langgraph.stream.transformers import (
LifecycleTransformer,
MessagesTransformer,
TasksTransformer,
)
TS = int(time.time() * 1000)
def _messages_event(namespace: list[str], data: Any) -> dict[str, Any]:
return {
"type": "event",
"method": "messages",
"params": {"namespace": namespace, "timestamp": TS, "data": data},
}
class _Tap(StreamTransformer):
"""Records the order it observed each event."""
required_stream_modes: ClassVar[tuple[str, ...]] = ()
def __init__(self, scope: tuple[str, ...] = (), *, label: str = "tap") -> None:
super().__init__(scope)
self.label = label
self.log: list[str] = []
self._channel: StreamChannel[str] = StreamChannel()
def init(self) -> dict[str, Any]:
return {f"tap_{self.label}": self._channel}
def process(self, event: dict[str, Any]) -> bool:
self.log.append(self.label)
return True
class _PreTap(_Tap):
before_builtins: ClassVar[bool] = True
class _TextRedactor(StreamTransformer):
"""Mutates `text-delta` events in place to a fixed redacted string."""
before_builtins: ClassVar[bool] = True
required_stream_modes: ClassVar[tuple[str, ...]] = ("messages",)
def __init__(self, scope: tuple[str, ...] = ()) -> None:
super().__init__(scope)
self._channel: StreamChannel[str] = StreamChannel()
def init(self) -> dict[str, Any]:
return {"redactor": self._channel}
def process(self, event: dict[str, Any]) -> bool:
if event.get("method") != "messages":
return True
payload, _meta = event["params"]["data"]
if isinstance(payload, dict) and payload.get("event") == "content-block-delta":
delta = payload.get("delta") or {}
if delta.get("type") == "text-delta":
delta["text"] = "[REDACTED]"
return True
def test_before_builtins_factories_run_before_others() -> None:
"""A `before_builtins=True` factory is registered ahead of the rest."""
seen: list[type[StreamTransformer]] = []
class _PostTap(_Tap):
def __init__(self, scope: tuple[str, ...] = ()) -> None:
super().__init__(scope, label="post")
def process(self, event: dict[str, Any]) -> bool:
seen.append(_PostTap)
return True
class _EagerTap(_Tap):
before_builtins: ClassVar[bool] = True
def __init__(self, scope: tuple[str, ...] = ()) -> None:
super().__init__(scope, label="eager")
def process(self, event: dict[str, Any]) -> bool:
seen.append(_EagerTap)
return True
mux = StreamMux(
factories=[_PostTap, _EagerTap],
scope=(),
is_async=False,
)
# `_EagerTap` was supplied second but should be registered first.
types_in_order = [type(t) for t in mux._transformers]
assert types_in_order.index(_EagerTap) < types_in_order.index(_PostTap)
mux.push(
_messages_event([], ({"event": "message-start", "role": "ai", "id": "m1"}, {}))
)
# And it ran first when the event was dispatched.
assert seen == [_EagerTap, _PostTap]
def test_within_lane_order_preserved() -> None:
"""Within each lane, the supplied order is the registration order."""
class _A(_PreTap):
pass
class _B(_PreTap):
pass
class _C(_Tap):
pass
class _D(_Tap):
pass
mux = StreamMux(
factories=[
lambda scope: _C(scope, label="c"),
lambda scope: _A(scope, label="a"),
lambda scope: _D(scope, label="d"),
lambda scope: _B(scope, label="b"),
],
scope=(),
is_async=False,
)
order = [t.label for t in mux._transformers] # type: ignore[attr-defined]
# Pre lane (a, b) ahead of default lane (c, d). Within each, supplied order kept.
assert order == ["a", "b", "c", "d"]
def test_redactor_runs_before_messages_transformer() -> None:
"""Content mutated by a pre-lane transformer reaches `MessagesTransformer`."""
# Order supplied: built-ins first (as in pregel/main.py), then the
# opt-in pre-lane redactor. Partitioning should still register the
# redactor first.
mux = StreamMux(
factories=[MessagesTransformer, _TextRedactor],
scope=(),
is_async=False,
)
types_in_order = [type(t) for t in mux._transformers]
assert types_in_order.index(_TextRedactor) < types_in_order.index(
MessagesTransformer
)
def test_lifecycle_unaffected_by_pre_lane_observer() -> None:
"""An observer-only pre-lane transformer doesn't break lifecycle bookkeeping."""
class _NoopPreObserver(StreamTransformer):
before_builtins: ClassVar[bool] = True
required_stream_modes: ClassVar[tuple[str, ...]] = ("tasks",)
def __init__(self, scope: tuple[str, ...] = ()) -> None:
super().__init__(scope)
self._channel: StreamChannel[str] = StreamChannel()
self.seen: list[tuple[str, ...]] = []
def init(self) -> dict[str, Any]:
return {"noop_observer": self._channel}
def process(self, event: dict[str, Any]) -> bool:
if event.get("method") == "tasks":
self.seen.append(tuple(event["params"]["namespace"]))
return True
mux = StreamMux(
factories=[LifecycleTransformer, TasksTransformer, _NoopPreObserver],
scope=(),
is_async=False,
)
types_in_order = [type(t) for t in mux._transformers]
assert types_in_order.index(_NoopPreObserver) < types_in_order.index(
LifecycleTransformer
)
observer = next(t for t in mux._transformers if isinstance(t, _NoopPreObserver))
lifecycle = next(
t for t in mux._transformers if isinstance(t, LifecycleTransformer)
)
# Push a synthetic `tasks` event that lifecycle would normally track.
mux.push(
{
"type": "event",
"method": "tasks",
"params": {
"namespace": ["child:abc"],
"timestamp": TS,
"data": {"name": "child"},
},
}
)
# Pre-lane observer saw the event, AND lifecycle's bookkeeping still
# registered the new namespace (the observer didn't mutate anything).
assert observer.seen == [("child:abc",)]
assert ("child:abc",) in lifecycle._seen # type: ignore[attr-defined]
def test_default_is_false() -> None:
"""`StreamTransformer.before_builtins` defaults to False."""
assert StreamTransformer.before_builtins is False
assert MessagesTransformer.before_builtins is False
assert LifecycleTransformer.before_builtins is False
def test_pre_lane_mutation_lands_in_messages_projection() -> None:
"""End-to-end: text mutated by a pre-lane transformer is what
`MessagesTransformer` snapshots into its `ChatModelStream` projection.
Without `before_builtins`, the redactor would run after
MessagesTransformer's eager extraction and the projection would
contain the raw, un-redacted text.
"""
mux = StreamMux(
factories=[MessagesTransformer, _TextRedactor],
scope=(),
is_async=False,
)
messages_transformer = next(
t for t in mux._transformers if isinstance(t, MessagesTransformer)
)
# Unblock both the mux's main log and the messages projection log so
# synthetic pushes are accepted without a real consumer attached.
mux._events._subscribed = True
messages_transformer._log._subscribed = True
meta = {"langgraph_node": "model", "run_id": "run-1"}
# message-start → MessagesTransformer creates a ChatModelStream.
mux.push(
_messages_event(
[],
({"event": "message-start", "role": "ai", "id": "msg-1"}, meta),
)
)
# content-block-delta carrying the secret. The redactor (pre-lane)
# mutates `delta.text` BEFORE MessagesTransformer snapshots it.
mux.push(
_messages_event(
[],
(
{
"event": "content-block-delta",
"index": 0,
"delta": {"type": "text-delta", "text": "secret@example.com"},
},
meta,
),
)
)
# Capture the still-open stream before message-finish removes it from
# MessagesTransformer's `_by_run` dict.
chat_stream = messages_transformer._by_run["run-1"] # type: ignore[attr-defined]
# message-finish → closes the stream.
mux.push(
_messages_event(
[],
({"event": "message-finish"}, meta),
)
)
# The redactor mutated `delta.text` to "[REDACTED]" before
# MessagesTransformer snapshotted the string into the text
# accumulator. Without `before_builtins`, the accumulator would hold
# the raw "secret@example.com".
assert chat_stream._text_acc == "[REDACTED]", ( # type: ignore[attr-defined]
f"expected redacted text in projection, got {chat_stream._text_acc!r}"
)
@@ -35,20 +35,25 @@ def _tasks_start(
*,
task_id: str,
name: str,
metadata: dict[str, Any] | None = None,
input: Any = None,
) -> dict[str, Any]:
"""Build a `tasks` ProtocolEvent carrying a TaskPayload (start)."""
data: dict[str, Any] = {
"id": task_id,
"name": name,
"input": input,
"triggers": [],
}
if metadata is not None:
data["metadata"] = metadata
return {
"type": "event",
"method": "tasks",
"params": {
"namespace": namespace,
"timestamp": TS,
"data": {
"id": task_id,
"name": name,
"input": None,
"triggers": [],
},
"data": data,
},
}
@@ -404,3 +409,272 @@ def test_stream_events_v3_with_nested_parent_ns_scopes_lifecycle() -> None:
assert ns[:1] == ("outer:abc",), (
f"namespace {ns} not within scoped prefix ('outer:abc',)"
)
# ---------------------------------------------------------------------------
# Parsed-segment fallback (no subagent boundary)
# ---------------------------------------------------------------------------
def test_no_metadata_falls_through_to_existing_behavior() -> None:
"""Tasks events without metadata produce the same output as before T4."""
mux = _build_lifecycle_mux()
mux.push(_tasks_start(["agent:abc"], task_id="t1", name="tool"))
[payload] = _drain_lifecycle(mux)
assert payload["graph_name"] == "agent"
assert payload["trigger_call_id"] == "abc"
assert "cause" not in payload
def test_empty_metadata_dict_falls_through() -> None:
"""An explicit empty metadata dict is treated the same as no metadata."""
mux = _build_lifecycle_mux()
mux.push(_tasks_start(["agent:abc"], task_id="t1", name="tool", metadata={}))
[payload] = _drain_lifecycle(mux)
assert payload["graph_name"] == "agent"
assert "cause" not in payload
# ---------------------------------------------------------------------------
# Subagent discrimination via lc_agent_name transition
# ---------------------------------------------------------------------------
#
# A nested task is a subagent iff its metadata["lc_agent_name"] is present and
# differs from its PARENT namespace's lc_agent_name. These tests replicate the
# empirically-verified `create_agent` stream shape synthetically:
#
# - A supervisor created via `create_agent(name="supervisor")` emits its own
# node tasks (model, tools) at ns=(), each with
# metadata["lc_agent_name"] == "supervisor".
# - The parent `tools` task at ns=() carries a `tool_call_with_context`
# dict as its `input` (with the LLM tool_call_id at
# input["tool_call"]["id"]) and a task `id`. (A legacy / batched shape
# passes a list of tool-call dicts instead; both are exercised below.)
# - When a tool body invokes an inner `create_agent(name="weather_agent")`,
# the inner agent's node tasks stream at ns=("tools:<taskid>",) with
# metadata["lc_agent_name"] == "weather_agent", sharing the SAME <taskid>
# as the parent `tools` task.
# - A plain StateGraph (no name) inherits the parent's lc_agent_name, so its
# child lc == parent lc -> NOT a subagent.
def test_lifecycle_uses_lc_agent_name_for_subagent() -> None:
"""A nested run whose lc_agent_name differs from its parent's is a subagent.
graph_name becomes the child's lc_agent_name; cause is recovered by joining
the child segment's task-id to the parent push task's tool call. This uses
the production `tool_call_with_context` dict input shape current langgraph
emits (tool_call_id at input["tool_call"]["id"]).
"""
mux = _build_lifecycle_mux()
# Supervisor's `tools` push task at scope ns: carries its own lc_agent_name
# and a `tool_call_with_context` dict as `input`. Each tool call is its own
# push task, and the task id seeds the child segment.
mux.push(
_tasks_start(
[],
task_id="tools_task_1",
name="tools",
metadata={"lc_agent_name": "supervisor"},
input={
"__type": "tool_call_with_context",
"tool_call": {
"name": "call_weather",
"args": {"city": "Boston"},
"id": "call_w",
"type": "tool_call",
},
"state": {},
},
)
)
# Inner weather_agent's first node task streams under the parent `tools`
# task's namespace segment (shared task id) with its own lc_agent_name.
mux.push(
_tasks_start(
["tools:tools_task_1"],
task_id="inner_model_1",
name="model",
metadata={"lc_agent_name": "weather_agent"},
)
)
payloads = _drain_lifecycle(mux)
started = [p for p in payloads if p["event"] == "started"]
[subagent] = [p for p in started if p["namespace"] == ["tools:tools_task_1"]]
assert subagent["graph_name"] == "weather_agent", (
"graph_name should be the child's lc_agent_name, not the parsed segment"
)
assert subagent["cause"] == {"type": "toolCall", "tool_call_id": "call_w"}, (
"cause should recover the triggering tool_call_id from the parent push "
"task's tool_call_with_context input via the shared task id"
)
def test_lifecycle_subagent_cause_from_legacy_list_input() -> None:
"""cause recovery also handles the legacy / batched list input shape.
A parent task whose `input` is a list of tool-call dicts (rather than a
`tool_call_with_context` dict) still seeds the tool_call_id join.
"""
mux = _build_lifecycle_mux()
mux.push(
_tasks_start(
[],
task_id="tools_task_1",
name="tools",
metadata={"lc_agent_name": "supervisor"},
input=[{"name": "call_weather", "args": {"city": "SF"}, "id": "call_w"}],
)
)
mux.push(
_tasks_start(
["tools:tools_task_1"],
task_id="inner_model_1",
name="model",
metadata={"lc_agent_name": "weather_agent"},
)
)
payloads = _drain_lifecycle(mux)
started = [p for p in payloads if p["event"] == "started"]
[subagent] = [p for p in started if p["namespace"] == ["tools:tools_task_1"]]
assert subagent["graph_name"] == "weather_agent"
assert subagent["cause"] == {"type": "toolCall", "tool_call_id": "call_w"}
def test_lifecycle_same_name_nested_run_is_surfaced() -> None:
"""A nested run whose lc_agent_name matches the parent's is still surfaced.
A subagent that invokes itself re-asserts its own lc_agent_name, so child
lc == parent lc. The discriminator surfaces any nested run carrying an
lc_agent_name, so the recursive call is reported (named after the agent,
with the triggering tool call as cause).
Trade-off: a non-agent subgraph that merely inherited the parent's
lc_agent_name would also surface here. That is accepted; a caller can null
lc_agent_name in the config it invokes such a graph with to exclude it.
"""
mux = _build_lifecycle_mux()
mux.push(
_tasks_start(
[],
task_id="tools_task_1",
name="tools",
metadata={"lc_agent_name": "weather_agent"},
input={
"__type": "tool_call_with_context",
"tool_call": {
"name": "recurse",
"args": {},
"id": "call_x",
"type": "tool_call",
},
"state": {},
},
)
)
# The agent invokes itself: the nested run re-asserts the SAME lc_agent_name.
mux.push(
_tasks_start(
["tools:tools_task_1"],
task_id="inner_node_1",
name="model",
metadata={"lc_agent_name": "weather_agent"},
)
)
payloads = _drain_lifecycle(mux)
started = [p for p in payloads if p["event"] == "started"]
[nested] = [p for p in started if p["namespace"] == ["tools:tools_task_1"]]
assert nested["graph_name"] == "weather_agent", (
"a same-named nested run (e.g. self-recursion) must still be surfaced"
)
assert nested["cause"] == {"type": "toolCall", "tool_call_id": "call_x"}
def test_lifecycle_unnamed_nested_agent_is_not_subagent() -> None:
"""A nested run with lc_agent_name None is excluded (not a subagent)."""
mux = _build_lifecycle_mux()
mux.push(
_tasks_start(
[],
task_id="tools_task_1",
name="tools",
metadata={"lc_agent_name": "supervisor"},
input={
"__type": "tool_call_with_context",
"tool_call": {
"name": "lookup",
"args": {},
"id": "call_x",
"type": "tool_call",
},
"state": {},
},
)
)
mux.push(
_tasks_start(
["plain:tools_task_1"],
task_id="inner_node_1",
name="inner_node",
metadata={"lc_agent_name": None},
)
)
payloads = _drain_lifecycle(mux)
started = [p for p in payloads if p["event"] == "started"]
[nested] = [p for p in started if p["namespace"] == ["plain:tools_task_1"]]
assert nested["graph_name"] == "plain"
assert "cause" not in nested
def test_lifecycle_subagent_terminal_roundtrip() -> None:
"""A detected subagent closes with `completed` when its parent task results.
Pushes the subagent's `started` (via the `tool_call_with_context` parent
plus the child task event) and then the parent push task's terminal
result, asserting the namespace is closed and the `started` payload's
projected graph_name / cause survive the roundtrip.
"""
mux = _build_lifecycle_mux()
mux.push(
_tasks_start(
[],
task_id="tools_task_1",
name="tools",
metadata={"lc_agent_name": "supervisor"},
input={
"__type": "tool_call_with_context",
"tool_call": {
"name": "call_weather",
"args": {"city": "Boston"},
"id": "call_w",
"type": "tool_call",
},
"state": {},
},
)
)
mux.push(
_tasks_start(
["tools:tools_task_1"],
task_id="inner_model_1",
name="model",
metadata={"lc_agent_name": "weather_agent"},
)
)
# The parent push task (id=tools_task_1, at scope ns) finishes, closing
# the subagent subgraph that streamed under `tools:tools_task_1`.
mux.push(_tasks_result([], task_id="tools_task_1", name="tools"))
payloads = _drain_lifecycle(mux)
ns = ["tools:tools_task_1"]
subagent = [p for p in payloads if p["namespace"] == ns]
assert [p["event"] for p in subagent] == ["started", "completed"]
started, _completed = subagent
assert started["graph_name"] == "weather_agent"
assert started["cause"] == {"type": "toolCall", "tool_call_id": "call_w"}
@@ -12,7 +12,7 @@ from langchain_core.language_models.chat_model_stream import (
AsyncChatModelStream,
ChatModelStream,
)
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import AIMessage, AIMessageChunk, ToolMessage
from langchain_core.runnables import RunnableConfig
from typing_extensions import TypedDict
@@ -213,6 +213,23 @@ class TestProtocolEventRouting:
log.close()
assert _unstamped(log._items) == []
def test_tool_role_protocol_events_are_ignored(self) -> None:
t, log = _make_sync_transformer()
for evt in [
{"event": "message-start", "role": "tool", "message_id": "tool-msg-1"},
{
"event": "content-block-delta",
"index": 0,
"content_block": {"type": "text", "text": "[]"},
},
{"event": "message-finish", "reason": "stop"},
]:
t.process(_proto_event(evt, run_id="tool-run"))
log.close()
assert _unstamped(log._items) == []
assert t._ignored_runs == set()
def test_concurrent_streams_routed_by_run_id(self) -> None:
t, log = _make_sync_transformer()
life_a = _lifecycle(text="aaaa", message_id="run-a")
@@ -273,6 +290,29 @@ class TestWholeMessageFallback:
assert stream.done
assert stream.output.text == "the full answer"
def test_whole_tool_message_is_ignored(self) -> None:
t, log = _make_sync_transformer()
t.process(
{
"type": "event",
"method": "messages",
"params": {
"namespace": [],
"timestamp": TS,
"data": (
ToolMessage(
content="[]",
id="tool-msg-1",
tool_call_id="call_1",
),
{"langgraph_node": "tools"},
),
},
}
)
log.close()
assert _unstamped(log._items) == []
def test_whole_message_has_full_lifecycle(self) -> None:
t, log = _make_sync_transformer()
t.process(_whole_msg("full"))
@@ -849,6 +889,23 @@ class TestStreamMessagesHandlerV2Unit:
assert emitted == []
def test_on_chain_end_does_not_emit_tool_messages(self) -> None:
from uuid import uuid4
from langgraph.pregel._messages import StreamMessagesHandlerV2
emitted: list[Any] = []
handler = StreamMessagesHandlerV2(emitted.append, subgraphs=False)
run_id = uuid4()
handler.metadata[run_id] = ((), {"langgraph_node": "tools"})
handler.on_chain_end(
{"messages": [ToolMessage(content="[]", tool_call_id="call_1")]},
run_id=run_id,
)
assert emitted == []
def test_on_llm_end_dedupes_when_final_message_id_differs(self) -> None:
"""A streamed v2 message should not be emitted again from the final
AIMessage fallback when its final id does not match `message-start`."""
+127
View File
@@ -15,12 +15,14 @@ from unittest.mock import MagicMock, patch
import langsmith
import pytest
from langchain_core.callbacks import BaseCallbackHandler, CallbackManager
from langchain_core.runnables import RunnableConfig
from langchain_core.tracers import LangChainTracer
from typing_extensions import NotRequired, Required, TypedDict
from langgraph._internal._config import (
_is_not_empty,
_merge_callbacks,
ensure_config,
get_callback_manager_for_config,
)
@@ -427,3 +429,128 @@ def test_callback_manager_copies_configurable_ids_to_tracing_metadata() -> None:
"thread_id": "th-123",
"user_id": "uid-1",
}
class _TrackingCB(BaseCallbackHandler):
"""Minimal callback handler used only as a sentinel for merge tests."""
def __init__(self, tag: str) -> None:
self.tag = tag
def __eq__(self, other: object) -> bool:
return isinstance(other, _TrackingCB) and self.tag == other.tag
def __hash__(self) -> int:
return hash(self.tag)
def test_merge_callbacks_none_base_list_new() -> None:
cb = _TrackingCB("a")
merged = _merge_callbacks(None, [cb])
assert merged == [cb]
def test_merge_callbacks_list_base_list_new() -> None:
a, b = _TrackingCB("a"), _TrackingCB("b")
merged = _merge_callbacks([a], [b])
assert merged == [a, b]
def test_merge_callbacks_list_base_manager_new() -> None:
a = _TrackingCB("a")
mgr = CallbackManager(handlers=[_TrackingCB("b")])
merged = _merge_callbacks([a], mgr)
assert isinstance(merged, CallbackManager)
assert _TrackingCB("a") in merged.handlers
assert _TrackingCB("b") in merged.handlers
def test_merge_callbacks_manager_base_list_new() -> None:
mgr = CallbackManager(handlers=[_TrackingCB("a")])
b = _TrackingCB("b")
merged = _merge_callbacks(mgr, [b])
assert isinstance(merged, CallbackManager)
assert _TrackingCB("a") in merged.handlers
assert _TrackingCB("b") in merged.handlers
def test_merge_callbacks_manager_base_manager_new() -> None:
mgr_a = CallbackManager(handlers=[_TrackingCB("a")])
mgr_b = CallbackManager(handlers=[_TrackingCB("b")])
merged = _merge_callbacks(mgr_a, mgr_b)
assert isinstance(merged, CallbackManager)
assert _TrackingCB("a") in merged.handlers
assert _TrackingCB("b") in merged.handlers
def test_merge_callbacks_none_base_none_new() -> None:
merged = _merge_callbacks(None, None)
assert merged is None
def test_ensure_config_merges_configurable_across_configs() -> None:
a = {"configurable": {"ls_agent_type": "root"}}
b = {"configurable": {"thread_id": "T1"}}
merged = ensure_config(a, b)
assert merged["configurable"]["ls_agent_type"] == "root"
assert merged["configurable"]["thread_id"] == "T1"
def test_ensure_config_configurable_later_wins_per_key() -> None:
a = {"configurable": {"shared": "from_a", "only_a": "A"}}
b = {"configurable": {"shared": "from_b", "only_b": "B"}}
merged = ensure_config(a, b)
assert merged["configurable"]["shared"] == "from_b" # later wins per key
assert merged["configurable"]["only_a"] == "A"
assert merged["configurable"]["only_b"] == "B"
def test_ensure_config_merges_metadata_across_configs() -> None:
a = {"metadata": {"user_id": "U1"}}
b = {"metadata": {"correlation_id": "C1"}}
merged = ensure_config(a, b)
assert merged["metadata"]["user_id"] == "U1"
assert merged["metadata"]["correlation_id"] == "C1"
def test_ensure_config_metadata_later_wins_per_key() -> None:
a = {"metadata": {"shared": "from_a"}}
b = {"metadata": {"shared": "from_b"}}
merged = ensure_config(a, b)
assert merged["metadata"]["shared"] == "from_b"
def test_ensure_config_merges_tags_across_configs() -> None:
a = {"tags": ["alpha"]}
b = {"tags": ["beta"]}
merged = ensure_config(a, b)
assert merged["tags"] == ["alpha", "beta"]
def test_ensure_config_tags_concat_preserves_order_and_duplicates() -> None:
# Plain concat (matches merge_configs in this file — no dedup, no sort).
a = {"tags": ["shared", "alpha"]}
b = {"tags": ["shared", "beta"]}
merged = ensure_config(a, b)
assert merged["tags"] == ["shared", "alpha", "shared", "beta"]
def test_ensure_config_merges_callbacks_across_configs() -> None:
a_cb = _TrackingCB("a")
b_cb = _TrackingCB("b")
merged = ensure_config({"callbacks": [a_cb]}, {"callbacks": [b_cb]})
assert merged["callbacks"] == [a_cb, b_cb]
def test_ensure_config_none_inputs_ignored() -> None:
# mixed with None should not raise
merged = ensure_config(None, {"tags": ["t"]}, None)
assert merged["tags"] == ["t"]
def test_ensure_config_empty_inputs() -> None:
# everything empty -> defaults
merged = ensure_config()
assert merged["tags"] == []
assert merged["configurable"] == {}
assert merged["callbacks"] is None
+77 -12
View File
@@ -871,11 +871,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -1370,19 +1370,19 @@ wheels = [
[[package]]
name = "langchain-protocol"
version = "0.0.14"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/bf/efb5e2ed832e4d6d45590e25a9e5191986b291b543bc6a807b48bee070b0/langchain_protocol-0.0.14.tar.gz", hash = "sha256:bc1e8553122e6ede310280462d5813023a172ff2785ccbbdec54d43f3a15e5f2", size = 5862, upload-time = "2026-04-29T16:40:18.657Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e9/06c47ecb2aff08f83dfa30058da3bf86be64862c19569043ed5331bbeecd/langchain_protocol-0.0.14-py3-none-any.whl", hash = "sha256:ffc35089779bd8ca217015180cef5e660fc3b074efdaa0f2e95df73583f1a047", size = 6984, upload-time = "2026-04-29T16:40:17.841Z" },
{ url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" },
]
[[package]]
name = "langgraph"
version = "1.2.0"
version = "1.2.3"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -1563,7 +1563,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -1721,7 +1721,7 @@ inmem = [
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "httpx", specifier = ">=0.24.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.9.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.10.0" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.7" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
{ name = "pathspec", specifier = ">=0.11.0" },
@@ -1828,13 +1828,19 @@ name = "langgraph-sdk"
source = { editable = "../sdk-py" }
dependencies = [
{ name = "httpx" },
{ name = "langchain-core" },
{ name = "langchain-protocol" },
{ name = "orjson" },
{ name = "websockets" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.25.2" },
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
{ name = "langchain-protocol", specifier = ">=0.0.15" },
{ name = "orjson", specifier = ">=3.11.5" },
{ name = "websockets", specifier = ">=14,<16" },
]
[package.metadata.requires-dev]
@@ -1867,7 +1873,7 @@ test = [
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -1880,9 +1886,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[package.optional-dependencies]
@@ -4007,6 +4013,65 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
[[package]]
name = "widgetsnbextension"
version = "4.0.15"
@@ -5,12 +5,42 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Any
from langchain_core.messages import ToolMessage
from langgraph.stream._types import ProtocolEvent, StreamTransformer
from langgraph.stream.stream_channel import StreamChannel
from langgraph.prebuilt._tool_call_stream import ToolCallStream
def _is_serialized_tool_message(value: Any) -> bool:
"""Detect a serialized LangChain `ToolMessage` payload.
Example:
{
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "ToolMessage"],
"kwargs": {"content": "raw tool result", "tool_call_id": "call_1"},
}
"""
return (
isinstance(value, dict)
and value.get("type") == "constructor"
and isinstance(value.get("id"), list)
and value["id"][-1] == "ToolMessage"
)
def _normalize_tool_output(output: Any) -> Any:
if isinstance(output, ToolMessage):
return output.content
if _is_serialized_tool_message(output):
kwargs = output.get("kwargs")
if isinstance(kwargs, dict):
return kwargs.get("content")
return output
class ToolCallTransformer(StreamTransformer):
"""Project `tools` channel events into `ToolCallStream` handles.
@@ -109,7 +139,7 @@ class ToolCallTransformer(StreamTransformer):
elif event_type == "tool-finished":
stream = self._active.pop(tool_call_id, None)
if stream is not None:
stream._finish(data.get("output"))
stream._finish(_normalize_tool_output(data.get("output")))
elif event_type == "tool-error":
stream = self._active.pop(tool_call_id, None)
if stream is not None:
@@ -6,7 +6,7 @@ import time
from typing import Annotated, Any
import pytest
from langchain_core.messages import AIMessage
from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.constants import END, START
from langgraph.graph import StateGraph
@@ -128,6 +128,42 @@ class TestToolCallTransformerUnit:
assert stream.error is None
assert "tc1" not in transformer._active
def test_finish_unwraps_tool_message_output(self) -> None:
mux, transformer = _mux()
mux.push(_tool_event("tool-started", "tc1", tool_name="echo"))
stream = transformer._active["tc1"]
mux.push(
_tool_event(
"tool-finished",
"tc1",
output=ToolMessage(content="done", tool_call_id="tc1"),
)
)
assert stream.completed is True
assert stream.output == "done"
def test_finish_unwraps_serialized_tool_message_output(self) -> None:
mux, transformer = _mux()
mux.push(_tool_event("tool-started", "tc1", tool_name="echo"))
stream = transformer._active["tc1"]
mux.push(
_tool_event(
"tool-finished",
"tc1",
output={
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "ToolMessage"],
"kwargs": {
"content": "serialized done",
"tool_call_id": "tc1",
},
},
)
)
assert stream.completed is True
assert stream.output == "serialized done"
def test_error_closes_stream(self) -> None:
mux, transformer = _mux()
mux.push(_tool_event("tool-started", "tc1", tool_name="boom"))
+76 -11
View File
@@ -214,11 +214,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -273,19 +273,19 @@ wheels = [
[[package]]
name = "langchain-protocol"
version = "0.0.14"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/bf/efb5e2ed832e4d6d45590e25a9e5191986b291b543bc6a807b48bee070b0/langchain_protocol-0.0.14.tar.gz", hash = "sha256:bc1e8553122e6ede310280462d5813023a172ff2785ccbbdec54d43f3a15e5f2", size = 5862, upload-time = "2026-04-29T16:40:18.657Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e9/06c47ecb2aff08f83dfa30058da3bf86be64862c19569043ed5331bbeecd/langchain_protocol-0.0.14-py3-none-any.whl", hash = "sha256:ffc35089779bd8ca217015180cef5e660fc3b074efdaa0f2e95df73583f1a047", size = 6984, upload-time = "2026-04-29T16:40:17.841Z" },
{ url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" },
]
[[package]]
name = "langgraph"
version = "1.2.0"
version = "1.2.3"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -369,7 +369,7 @@ test = [
[[package]]
name = "langgraph-checkpoint"
version = "4.1.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -597,13 +597,19 @@ name = "langgraph-sdk"
source = { editable = "../sdk-py" }
dependencies = [
{ name = "httpx" },
{ name = "langchain-core" },
{ name = "langchain-protocol" },
{ name = "orjson" },
{ name = "websockets" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.25.2" },
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
{ name = "langchain-protocol", specifier = ">=0.0.15" },
{ name = "orjson", specifier = ">=3.11.5" },
{ name = "websockets", specifier = ">=14,<16" },
]
[package.metadata.requires-dev]
@@ -636,7 +642,7 @@ test = [
[[package]]
name = "langsmith"
version = "0.7.31"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -649,9 +655,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" },
]
[[package]]
@@ -1558,6 +1564,65 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
[[package]]
name = "xxhash"
version = "3.6.0"
+48
View File
@@ -0,0 +1,48 @@
# Changelog
## Unreleased
### Added
- **Thread-centric streaming (v3)**`client.threads.stream()` returns an
`AsyncThreadStream` (or `SyncThreadStream`) context manager that owns one
SSE or WebSocket connection for the lifetime of a thread session.
- **Typed projections**`thread.messages`, `thread.tool_calls`,
`thread.values`, and `thread.extensions[name]` all share the same underlying
transport connection. Iterating multiple projections concurrently expands the
server-side filter union without opening additional connections.
- **Scoped subgraph handles**`thread.subgraphs` (alias `thread.subagents`)
yields one `ScopedStreamHandle` per direct child invocation, each exposing
`.messages`, `.tool_calls`, and `.subgraphs` scoped to that namespace.
- **WebSocket transport** — pass `transport="websocket"` to
`client.threads.stream()` to use a WebSocket connection instead of SSE
(async client only).
- **Automatic reconnect** — the shared SSE fan-out and the lifecycle watcher
both reconnect on transport drops, replaying missed events via a `since`
cursor and deduplicating by `event_id`.
- **`thread.agent.get_tree()`** — fetches the assistant graph definition for
the current session's `assistant_id` with optional `xray` depth control.
- **`thread.run.respond()`** — resumes a run after a server-side interrupt,
resolving the outstanding `InterruptPayload` by `interrupt_id`.
- **`thread.output`** — awaitable that resolves to the terminal thread state
`values` dict after the run lifecycle completes.
### Changed
- `client.threads.stream()` now accepts `transport="sse"` (default) or
`transport="websocket"` in place of the previous transport-agnostic default.
### Notes
- The v3 streaming surface (`AsyncThreadStream`, `SyncThreadStream`, and all
projection classes) is **new** in this release. The existing
`client.runs.stream()` (v2) surface is unchanged and remains fully supported.
- `thread_id` is minted client-side (UUIDv4) when not provided; the server
creates the thread row lazily on the first `run.start`.
+118
View File
@@ -0,0 +1,118 @@
# Migration Guide: v2 → v3 Streaming
`client.runs.stream()` (v2) remains fully supported. This guide covers how to
adopt the new `client.threads.stream()` (v3) surface when you want typed
projections, shared SSE fan-out, or WebSocket transport.
## Minimal before/after
**v2 — `client.runs.stream()`**
```python
from langgraph_sdk import get_client
client = get_client()
thread = await client.threads.create()
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input={"messages": [{"role": "user", "content": "hello"}]},
stream_mode="messages",
):
print(chunk.event, chunk.data)
```
**v3 — `client.threads.stream()`**
```python
from langgraph_sdk import get_client
import asyncio
client = get_client()
async with client.threads.stream(assistant_id="agent") as thread:
await thread.run.start(input={"messages": [{"role": "user", "content": "hello"}]})
async for stream in thread.messages:
print(await stream.text)
```
## Key differences
| | v2 `client.runs.stream()` | v3 `client.threads.stream()` |
|---|---|---|
| Thread creation | Explicit `client.threads.create()` | Lazy (minted client-side if omitted) |
| Connection per run | Yes | No — shared SSE for the session |
| Typed projections | No (raw `StreamPart`) | Yes (`messages`, `tool_calls`, `values`, …) |
| Subgraph streaming | Not supported | `thread.subgraphs` / `thread.subagents` |
| WebSocket transport | No | Yes (`transport="websocket"`, async only) |
| Interrupt handling | Manual polling | `thread.interrupted` / `thread.run.respond()` |
| Terminal state | Included in stream | `await thread.output` |
## Reattaching to an existing thread
```python
async with client.threads.stream(
thread_id="existing-thread-id",
assistant_id="agent",
) as thread:
# If the run already completed, thread.output resolves immediately.
result = await thread.output
```
## Consuming multiple projections concurrently
All projections share one SSE connection. Use `asyncio.gather` (or
`asyncio.TaskGroup`) to start multiple consumers before any single projection
has finished — the fan-out task routes events to all subscribers in parallel.
```python
async with client.threads.stream(assistant_id="agent") as thread:
await thread.run.start(input={"messages": [{"role": "user", "content": "hi"}]})
async def collect_messages():
return [s async for s in thread.messages]
async def collect_tool_calls():
return [c async for c in thread.tool_calls]
messages, tool_calls = await asyncio.gather(
collect_messages(),
collect_tool_calls(),
)
```
## Human-in-the-loop (interrupts)
```python
async with client.threads.stream(assistant_id="agent") as thread:
await thread.run.start(input={"messages": [{"role": "user", "content": "book a flight"}]})
# Wait for the run to pause at an interrupt node.
# thread.interrupted becomes True when input.requested arrives.
while not thread.interrupted:
await asyncio.sleep(0.1)
# Resume with a human response (unambiguous when only one interrupt is outstanding).
await thread.run.respond("yes, confirm booking")
result = await thread.output
```
## Sync client
The sync client mirrors the async API without `async`/`await`:
```python
from langgraph_sdk import get_sync_client
client = get_sync_client()
with client.threads.stream(assistant_id="agent") as thread:
thread.run.start(input={"messages": [{"role": "user", "content": "hello"}]})
for stream in thread.messages:
print(stream.text)
```
The sync client uses SSE only (`transport="websocket"` is not supported).
+40
View File
@@ -33,3 +33,43 @@ input = {"messages": [{"role": "human", "content": "what's the weather in la"}]}
async for chunk in client.runs.stream(thread['thread_id'], agent['assistant_id'], input=input):
print(chunk)
```
## Known Limitations
- **WebSocket transport** requires `websockets>=14` and is only available on the async client (`AsyncThreadStream`). The sync client (`SyncThreadStream`) uses SSE exclusively.
- **`thread.extensions[name]`** opens a new subscription each time the same name is accessed. Assign the projection to a variable and reuse it within a single session rather than re-indexing across multiple iterations.
- **Sync streaming** drives the lifecycle watcher in a background thread. Long-lived sync sessions will hold that thread open until the context manager exits.
- **Reconnect attempts** are limited to 5 by default for both the shared SSE fan-out and the lifecycle watcher. Persistent network partitions will surface as `RuntimeError` on in-flight projections.
## Thread-Centric Streaming (v3)
`client.threads.stream()` returns a context manager that owns the SSE session for one
thread. Typed projections — values snapshots, message streams, tool calls, custom
events — all share the same underlying connection.
```python
from langgraph_sdk import get_client
import asyncio
client = get_client()
async with client.threads.stream(
thread_id="my-thread",
assistant_id="agent",
) as thread:
await thread.run.start(input={"messages": [{"role": "user", "content": "hi"}]})
# Start all consumers concurrently so they share one SSE connection.
async def get_messages():
return [s async for s in thread.messages]
async def get_tool_calls():
return [c async for c in thread.tool_calls]
messages, tool_calls = await asyncio.gather(get_messages(), get_tool_calls())
for stream in messages:
print(await stream.text) # accumulated text
final = await thread.output # terminal state values
```
+27
View File
@@ -0,0 +1,27 @@
# Thin layer on top of the latest published langgraph-api image.
#
# The base image bundles `langgraph_api`, `langgraph_runtime_postgres`,
# `langgraph_license`, `langgraph_grpc_common`, the Go `core-api-grpc`
# binary, and an entrypoint that starts both the gRPC server and uvicorn
# (`/storage/entrypoint.sh`). It also ships langgraph + langchain-core.
#
# We track the `latest-py3.12` tag rather than pinning a specific revision
# so CI surfaces upstream regressions early. If the base shifts under us,
# `docker compose build` will pick up the new digest on the next run.
#
# The image is the `licensed` variant, so it requires either a real
# `LANGSMITH_API_KEY` or a `LANGGRAPH_CLOUD_LICENSE_KEY` at runtime
# (passed through from the host shell / CI secrets, see docker-compose.yml).
FROM langchain/langgraph-api:latest-py3.12
# Graph dependencies not in the base image. `deepagents` is required for
# the deep_agent graph; the supervisor and researcher use a fake chat
# model (no `langchain-anthropic`) so no LLM API key is needed.
RUN pip install --no-cache-dir \
"langchain>=1.3.0" \
"deepagents>=0.6.2"
# Project graphs + registration config.
COPY graph/ /app/graph/
COPY langgraph.json /app/langgraph.json
@@ -0,0 +1,91 @@
name: langgraph-v3-integration
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
command: ["postgres", "-c", "shared_preload_libraries=vector", "-c", "max_connections=150"]
ports:
- "5443:5432"
healthcheck:
test: pg_isready -U postgres
interval: 5s
timeout: 1s
retries: 5
start_period: 10s
tmpfs:
- /var/lib/postgresql/data
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6380:6379"
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 1s
retries: 5
start_period: 2s
tmpfs:
- /data
api:
# Thin layer on top of langchain/langgraph-api:latest-py3.12 —
# see ./Dockerfile. The base image bundles langgraph-api +
# langgraph_runtime_postgres + langgraph_license + the Go core-server,
# so we only add graph deps (deepagents) and the graph files on top.
build:
context: .
dockerfile: Dockerfile
image: langgraph-v3-integration-api:local
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
# Database + cache (the production-shape postgres+redis runtime).
# Recent langgraph-api reads DATABASE_URI first and falls back to
# POSTGRES_URI; set both so older code paths also work.
DATABASE_URI: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
POSTGRES_URI: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
REDIS_URI: redis://redis:6379
LANGGRAPH_RUNTIME_EDITION: postgres
LANGGRAPH_AUTH_TYPE: noop
LANGSMITH_TRACING: "false"
PORT: "8000"
# Required for the v3 thread-centric streaming protocol. Without this
# flag the API falls back to the legacy v2 streaming surface and the
# client's v3 endpoints (POST /threads/{id}/stream/events, /commands,
# etc.) won't be exposed.
FF_OPTIMIZED_STREAMING: "true"
# Tell langgraph-api which graphs to register. The langgraph CLI sets
# this from langgraph.json; we're bypassing the CLI (running uvicorn
# directly) so we set it manually.
LANGSERVE_GRAPHS: '{"agent":"/app/graph/streaming_graph.py:graph","tools_agent":"/app/graph/tools_agent.py:graph","deep_agent":"/app/graph/deep_agent.py:graph"}'
# The published langgraph-api image is the `licensed` variant and
# requires a real LANGSMITH_API_KEY (or LANGGRAPH_CLOUD_LICENSE_KEY)
# at runtime. Passed through from the host shell / CI secrets.
LANGSMITH_API_KEY: ${LANGSMITH_API_KEY:-}
LANGGRAPH_CLOUD_LICENSE_KEY: ${LANGGRAPH_CLOUD_LICENSE_KEY:-}
# Mount the graph + config so edits don't require a rebuild.
volumes:
- ./graph:/app/graph:ro
- ./langgraph.json:/app/langgraph.json:ro
ports:
- "2024:8000"
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://localhost:8000/ok || exit 1"]
interval: 5s
timeout: 3s
retries: 30
start_period: 30s
@@ -0,0 +1,6 @@
"""Example graphs for v3 streaming integration tests."""
from .deep_agent import graph as deep_agent
from .streaming_graph import graph as streaming_graph
__all__ = ["deep_agent", "streaming_graph"]
@@ -0,0 +1,96 @@
"""Deep-agent variant exercising v3 `thread.subgraphs` properly.
`create_deep_agent` builds a graph whose `task` tool dispatches to one
of its configured `SubAgent`s. When the supervisor's model issues a
`task(subagent_type="researcher", description=...)` tool call, the
sub-agent runs as a nested invocation and the v3 streaming server
emits the subagent's lifecycle, messages, and tool events under a
scoped namespace. That namespace is what `thread.subgraphs` surfaces
as a direct-child `ScopedStreamHandle`.
Both the supervisor and the researcher use `FakeMessagesListChatModel`
with pre-scripted responses so this graph is hermetic. No LLM API keys
are required, and the test is deterministic.
"""
from __future__ import annotations
from typing import Any
from deepagents import create_deep_agent
from deepagents.middleware.subagents import SubAgent
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
from langchain_core.messages import AIMessage
class _FakeChatModelWithTools(FakeMessagesListChatModel):
"""`FakeMessagesListChatModel` that accepts `bind_tools(...)` as a no-op.
`create_deep_agent` calls `model.bind_tools(tools)` to expose the `task`
tool to the supervisor. The base `BaseChatModel.bind_tools` raises
`NotImplementedError`. Pre-baked responses in `responses` already carry
the desired `tool_calls`, so we ignore the tools list and return self.
"""
def bind_tools(self, tools: Any, **kwargs: Any) -> _FakeChatModelWithTools:
return self
# Supervisor turn 1: dispatch to the researcher via the `task` tool.
# Supervisor turn 2: emit a final assistant message (no more tool calls),
# which closes the agent loop.
_supervisor_model = _FakeChatModelWithTools(
responses=[
AIMessage(
content="",
id="sup-1",
tool_calls=[
{
"id": "tc-task-1",
"name": "task",
"args": {
"subagent_type": "researcher",
"description": "research v3 streaming",
},
}
],
),
AIMessage(content="Research complete.", id="sup-2"),
]
)
# Researcher turn 1: final message, no tool calls. Closes the subagent loop.
_researcher_model = _FakeChatModelWithTools(
responses=[
AIMessage(
content="v3 streaming is event-typed and thread-centric.", id="res-1"
),
]
)
_researcher: SubAgent = {
"name": "researcher",
"description": (
"Looks up notes on a topic and returns a short summary. "
"Use this when the user wants to research something."
),
"system_prompt": (
"You are a research assistant. Reply with one or two sentences "
"summarising what the user asked about. Do not call any tools."
),
"model": _researcher_model,
}
graph = create_deep_agent(
model=_supervisor_model,
system_prompt=(
"You are a supervisor coordinating a researcher subagent. "
"When the user asks to research anything, call the `task` tool "
"with subagent_type='researcher'."
),
subagents=[_researcher],
name="v3_deep_agent",
)
@@ -0,0 +1,300 @@
"""Example graph exercising the full v3 streaming surface.
Topology:
__start__ -> stream_message -> call_tool -> ask_human -> subgraph -> __end__
Each node is designed to surface a specific v3 channel:
- `stream_message` yields token-by-token AI message chunks (`messages`).
- `call_tool` invokes a tool and emits a tool-call lifecycle (`tools`).
- `ask_human` raises an `interrupt(...)` to test `thread.interrupted` /
`thread.run.respond(...)` (`lifecycle` / `input`).
- `subgraph` is a nested `StateGraph` invoked once so `thread.subgraphs` has
exactly one direct child (`tasks` + `messages` under a namespace).
Extensions: every node calls `get_stream_writer()("progress", {...})` so
`thread.extensions["progress"]` produces deterministic events.
No real LLM is used message streaming is simulated by yielding a list of
`AIMessageChunk`s from the node. This keeps the integration suite
hermetic.
"""
from __future__ import annotations
import operator
from collections.abc import AsyncIterator, Iterator
from typing import Annotated, Any, TypedDict
from langchain_core.callbacks import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, ToolMessage
from langchain_core.outputs import ChatGenerationChunk
from langchain_core.tools import tool
from langgraph.config import get_stream_writer
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.stream.transformers import CustomTransformer, UpdatesTransformer
from langgraph.types import interrupt
class _StreamingFakeChatModel(BaseChatModel):
"""Fake ``BaseChatModel`` that streams ``AIMessageChunk``s.
Implements ``_stream`` / ``_astream`` so the v3 chat-model
callback chain (``_aiter_v2_events`` in
``langchain_core/language_models/chat_models.py``) fires
``run_manager.on_stream_event(...)`` per normalized protocol
event. ``StreamMessagesHandlerV2`` -- attached by the langgraph
runtime when ``"messages"`` is in stream_modes -- catches those
callbacks and surfaces them on the v3 wire ``messages`` channel
at root namespace.
The base ``FakeMessagesListChatModel`` would have worked for
``ainvoke`` but raises ``NotImplementedError`` from ``_stream``,
so it can't drive the streaming-callback path. ``GenericFakeChatModel``
implements ``_stream`` but takes an ``Iterator`` that gets
exhausted across invocations.
"""
text: str = "Hello, world!"
message_id: str = "ai-msg-1"
@property
def _llm_type(self) -> str:
return "streaming-fake-chat-model"
def _generate(self, messages, stop=None, run_manager=None, **kwargs):
from langchain_core.outputs import ChatGeneration, ChatResult
return ChatResult(
generations=[
ChatGeneration(message=AIMessage(content=self.text, id=self.message_id))
]
)
def _stream(
self,
messages: list[BaseMessage],
stop: list[str] | None = None,
run_manager: CallbackManagerForLLMRun | None = None,
**kwargs: object,
) -> Iterator[ChatGenerationChunk]:
# Yield content as space-separated word chunks so deltas are
# observable. The final chunk's ``chunk_position="last"`` tells
# the callback chain to emit ``message-finish``.
parts = self.text.split(" ")
for i, part in enumerate(parts):
content = part if i == 0 else " " + part
chunk = AIMessageChunk(content=content, id=self.message_id)
if i == len(parts) - 1:
chunk.chunk_position = "last"
yield ChatGenerationChunk(message=chunk)
async def _astream(
self,
messages: list[BaseMessage],
stop: list[str] | None = None,
run_manager: AsyncCallbackManagerForLLMRun | None = None,
**kwargs: object,
) -> AsyncIterator[ChatGenerationChunk]:
for chunk in self._stream(messages, stop=stop, **kwargs):
yield chunk
_stream_model = _StreamingFakeChatModel()
class AgentState(TypedDict):
"""Top-level state for the agent.
`messages` accumulates AI/tool/user messages via the standard `add_messages`
reducer. `value` is a simple scalar to test the `values` channel.
`items` accumulates list-append updates via `operator.add` so each node
contributes a marker and the terminal state reflects the full path
rather than only the last node's return.
"""
messages: Annotated[list[BaseMessage], add_messages]
value: str
items: Annotated[list[str], operator.add]
@tool
def search(query: str) -> str:
"""Look up `query` in a fake search index."""
return f"result for {query!r}"
# ---------------------------------------------------------------------------
# Nodes
# ---------------------------------------------------------------------------
async def stream_message(state: AgentState) -> dict[str, Any]:
"""Stream an AI message via a fake chat model.
Awaiting ``model.ainvoke(...)`` drives langgraph's chat-model
streaming callbacks (``StreamMessagesHandlerV2`` ->
``MessagesTransformer``), so the v3 ``messages`` channel emits the
normalized delta lifecycle (``message-start`` ->
``content-block-start`` -> ``content-block-delta`` ->
``content-block-finish`` -> ``message-finish``) at root namespace.
Returning the resolved ``AIMessage`` via the messages reducer also
keeps the existing ``values`` snapshots intact.
"""
writer = get_stream_writer()
writer({"name": "progress", "step": "stream_message", "phase": "start"})
# ``astream_events(version="v3")`` drives the chat model's
# ``_aiter_v2_events`` path (``BaseChatModel`` in
# ``langchain_core/language_models/chat_models.py``), which fires
# ``run_manager.on_stream_event(...)`` per normalized protocol
# event (``message-start`` / ``content-block-delta`` /
# ``message-finish``). ``StreamMessagesHandlerV2`` -- attached by
# the langgraph runtime when ``"messages"`` is in stream_modes --
# catches those callbacks and surfaces them on the v3 wire
# ``messages`` channel at root namespace. Plain ``astream(...)``
# does NOT route through this handler.
text_parts: list[str] = []
message_id = "ai-msg-1"
# ``astream_events(version="v3")`` returns an awaitable that resolves
# to the async iterator.
stream = await _stream_model.astream_events([], version="v3")
async for event in stream:
if event.get("event") == "content-block-delta":
delta = event.get("delta") or {}
t = delta.get("text") if isinstance(delta, dict) else None
if isinstance(t, str):
text_parts.append(t)
elif event.get("event") == "message-start":
mid = event.get("id")
if isinstance(mid, str):
message_id = mid
final = AIMessage(content="".join(text_parts), id=message_id)
writer({"name": "progress", "step": "stream_message", "phase": "end"})
return {"messages": [final], "value": "x", "items": ["streamed"]}
def call_tool(state: AgentState) -> dict[str, Any]:
"""Invoke a tool and emit its result as a tool message.
A tool call here exercises the `tools` channel in v3.
"""
writer = get_stream_writer()
writer({"name": "progress", "step": "call_tool", "phase": "start"})
# Hand-roll a tool call so we don't need a model to issue it.
tool_call_id = "tc-1"
ai_with_tool = AIMessage(
content="",
id="ai-msg-2",
tool_calls=[
{
"id": tool_call_id,
"name": "search",
"args": {"query": "v3"},
}
],
)
result = search.invoke({"query": "v3"})
tool_msg = ToolMessage(content=result, tool_call_id=tool_call_id)
writer({"name": "progress", "step": "call_tool", "phase": "end"})
return {
"messages": [ai_with_tool, tool_msg],
"items": ["tool"],
}
def ask_human(state: AgentState) -> dict[str, Any]:
"""Pause the graph and wait for a `thread.run.respond(...)`.
`interrupt(value)` raises a special exception that the runtime catches;
the v3 lifecycle emits `input.requested` with this `value` and the
client must call `thread.run.respond(answer)` to continue.
"""
writer = get_stream_writer()
writer({"name": "progress", "step": "ask_human", "phase": "start"})
answer = interrupt("Are we good?")
writer(
{"name": "progress", "step": "ask_human", "phase": "end", "answer": str(answer)}
)
return {
"messages": [AIMessage(content=f"Human said: {answer}", id="ai-msg-3")],
"items": ["asked"],
}
# ---------------------------------------------------------------------------
# Subgraph (exercises `thread.subgraphs`)
# ---------------------------------------------------------------------------
class SubState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
note: str
def sub_node(state: SubState) -> dict[str, Any]:
"""Single node in the subgraph; emits a message and a custom event."""
writer = get_stream_writer()
writer({"name": "progress", "step": "sub_node", "phase": "start"})
msg = AIMessage(content="from subgraph", id="sub-msg-1")
writer({"name": "progress", "step": "sub_node", "phase": "end"})
return {"messages": [msg], "note": "ran"}
_sub_builder = StateGraph(SubState)
_sub_builder.add_node("sub", sub_node)
_sub_builder.set_entry_point("sub")
_sub_builder.set_finish_point("sub")
subgraph = _sub_builder.compile()
def run_subgraph(state: AgentState) -> dict[str, Any]:
"""Invoke the subgraph once so it appears as a direct child handle."""
writer = get_stream_writer()
writer({"name": "progress", "step": "run_subgraph", "phase": "start"})
sub_state = subgraph.invoke({"messages": [], "note": ""})
writer({"name": "progress", "step": "run_subgraph", "phase": "end"})
return {
"messages": sub_state["messages"],
"items": ["sub"],
}
# ---------------------------------------------------------------------------
# Top-level graph
# ---------------------------------------------------------------------------
_builder: StateGraph[AgentState, Any, Any, Any] = StateGraph(AgentState)
_builder.add_node("stream_message", stream_message)
_builder.add_node("call_tool", call_tool)
_builder.add_node("ask_human", ask_human)
_builder.add_node("run_subgraph", run_subgraph)
_builder.set_entry_point("stream_message")
_builder.add_edge("stream_message", "call_tool")
_builder.add_edge("call_tool", "ask_human")
_builder.add_edge("ask_human", "run_subgraph")
_builder.set_finish_point("run_subgraph")
graph = _builder.compile(
name="v3_integration_agent",
# Register transformers so ``custom`` (``get_stream_writer()``) and
# ``updates`` channels emit on the wire. ``MessagesTransformer`` is
# auto-registered by the v3 mux for any graph that streams a chat
# model. ``ValuesTransformer`` / ``LifecycleTransformer`` are also
# always-on natives.
transformers=[CustomTransformer, UpdatesTransformer],
)
@@ -0,0 +1,97 @@
"""create_agent-based example exercising the v3 `tools` channel.
`thread.tool_calls` and the underlying `tools` channel only emit
events when an actual model issues a tool call through langchain's
agent stack. The synthetic `streaming_graph.py` hand-builds
`AIMessage(tool_calls=[...])` and a `ToolMessage` via the messages
reducer that gets persisted in state but never produces tool-call
telemetry on the wire. This graph fixes that by going through
`create_agent` with a real tool, driven by a hermetic fake chat model
(no `ANTHROPIC_API_KEY` required).
Flow on `run.start`:
1. Supervisor model returns an `AIMessage(tool_calls=[search(query="v3")])`.
2. langchain's tool node executes `search` and produces a `ToolMessage`.
3. Supervisor model returns a final `AIMessage("done.")` to terminate.
The v3 streaming layer surfaces this as `messages` + `tools` channel
events at root namespace.
"""
from __future__ import annotations
from typing import Any
from langchain.agents import create_agent
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
from langchain_core.messages import AIMessage, BaseMessage, ToolMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Look up `query` in a fake search index."""
return f"result for {query!r}"
class _ToolBindingFakeChatModel(FakeMessagesListChatModel):
"""Stateless fake chat model driving a single `search` tool call.
`create_agent` calls `model.bind_tools(tools)` to attach the tool
schema (`langchain/agents/factory.py:1284`). The base
`FakeMessagesListChatModel` inherits `BaseChatModel.bind_tools`,
which raises `NotImplementedError`, so `bind_tools` is overridden as
a no-op (the reply is hand-built and already carries `tool_calls`).
The reply is derived from conversation state rather than a cycling
response list: the `search` tool call is issued until a `ToolMessage`
appears, then a terminating `AIMessage`. This avoids the response-index
parity flake where `FakeMessagesListChatModel.responses` is shared
process-wide and cycles `0 -> 1 -> 0`; a run that started mid-cycle
(e.g. on a reused server worker) would reply `"done."` first and emit
no tool call. Being order-independent, every run emits exactly one
tool call regardless of how many times the model was previously called.
`FakeMessagesListChatModel` is subclassed (rather than
`GenericFakeChatModel`) because the latter's `_stream` breaks the
message into content chunks and drops `tool_calls` when content is
empty, causing the v2 streaming path inside `create_agent` to raise
`RuntimeError("v2 stream finished without producing a message")`.
The inherited `_stream` yields the whole message in one chunk,
preserving `tool_calls`.
"""
def bind_tools(self, tools: Any, **kwargs: Any) -> _ToolBindingFakeChatModel:
return self
def _generate(
self,
messages: list[BaseMessage],
stop: list[str] | None = None,
run_manager: Any = None,
**kwargs: Any,
) -> ChatResult:
if any(isinstance(m, ToolMessage) for m in messages):
response = AIMessage(content="done.", id="ai-tools-done")
else:
response = AIMessage(
content="",
id="ai-tools-call",
tool_calls=[{"id": "tc-1", "name": "search", "args": {"query": "v3"}}],
)
return ChatResult(generations=[ChatGeneration(message=response)])
# `responses` is a required field on `FakeMessagesListChatModel`, but the
# overridden `_generate` derives its reply from state and never reads it.
_supervisor_model = _ToolBindingFakeChatModel(responses=[])
graph = create_agent(
model=_supervisor_model,
tools=[search],
system_prompt="You are a research assistant. Use the search tool when asked.",
name="v3_tools_agent",
)
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://langgra.ph/schema.json",
"dependencies": ["./graph"],
"graphs": {
"agent": "./graph/streaming_graph.py:graph",
"tools_agent": "./graph/tools_agent.py:graph",
"deep_agent": "./graph/deep_agent.py:graph"
},
"env": {}
}
+124
View File
@@ -0,0 +1,124 @@
"""Shared helpers for the v3 streaming integration scripts.
All scripts share the same expectations:
- A `langgraph-api` server is reachable at `BASE_URL` (default
http://localhost:2024 set by `docker-compose.yml`, which builds
on `langchain/langgraph-api:latest-py3.12`).
- The example graph (`integration/graph/streaming_graph.py:graph`) is
registered under the assistant id `agent` (see `integration/langgraph.json`).
Each script imports `make_async_client()` / `make_sync_client()` from here
to construct the v3 SDK client. Override `BASE_URL` via the
`LANGGRAPH_INTEGRATION_URL` env var if you're running the API elsewhere.
"""
from __future__ import annotations
import asyncio
import contextlib
import os
import threading
from typing import TYPE_CHECKING, Any
import httpx
if TYPE_CHECKING:
from langgraph_sdk._async.threads import ThreadsClient as AsyncThreadsClient
from langgraph_sdk._sync.threads import SyncThreadsClient
BASE_URL = os.environ.get("LANGGRAPH_INTEGRATION_URL", "http://localhost:2024")
ASSISTANT_ID = "agent"
def make_async_client() -> tuple[AsyncThreadsClient, httpx.AsyncClient]:
"""Build an async ThreadsClient pointing at the integration API.
Returns the client and the underlying httpx client so callers can close
it. Typical usage:
```python
threads, raw = make_async_client()
try:
async with threads.stream(...) as thread:
...
finally:
await raw.aclose()
```
"""
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._async.threads import ThreadsClient
raw = httpx.AsyncClient(base_url=BASE_URL, timeout=30.0)
return ThreadsClient(HttpClient(raw)), raw
def make_sync_client() -> tuple[SyncThreadsClient, httpx.Client]:
"""Build a sync ThreadsClient pointing at the integration API."""
from langgraph_sdk._sync.http import SyncHttpClient
from langgraph_sdk._sync.threads import SyncThreadsClient
raw = httpx.Client(base_url=BASE_URL, timeout=30.0)
return SyncThreadsClient(SyncHttpClient(raw)), raw
def header(title: str) -> None:
"""Print a section header for script output."""
bar = "=" * (len(title) + 4)
print(f"\n{bar}\n {title}\n{bar}")
def auto_respond_async(thread: Any, response: Any = "yes") -> asyncio.Task[None]:
"""Spawn a background task that responds to the first interrupt and exits.
Opens a private `thread.values` subscription and drains it; the SDK's
`_signal_paused` mechanism pushes the terminal sentinel into the
iterator when `interrupted` flips True, so the loop exits at the
interrupt. The auto-responder then calls `run.respond(...)` and the
foreground iteration sees the rest of the run.
Returns the task so callers can `await` it before tearing down the
stream (recommended) or cancel it.
"""
async def _runner() -> None:
async for _ in thread.values:
if thread.interrupted:
break
if thread.interrupted:
with contextlib.suppress(Exception):
await thread.run.respond(response)
return asyncio.create_task(_runner())
def auto_respond_sync(thread: Any, response: Any = "yes") -> threading.Thread:
"""Sync analogue of `auto_respond_async`."""
def _runner() -> None:
for _ in thread.values:
if thread.interrupted:
break
if thread.interrupted:
with contextlib.suppress(Exception):
thread.run.respond(response)
t = threading.Thread(target=_runner, daemon=True, name="auto-respond")
t.start()
return t
def check_api_reachable() -> None:
"""Fail fast with a helpful message if the API isn't reachable.
Call this at the top of `main()` in each script.
"""
try:
resp = httpx.get(f"{BASE_URL}/ok", timeout=2.0)
resp.raise_for_status()
except Exception as err:
raise SystemExit(
f"\nCannot reach the integration API at {BASE_URL}: {err!r}\n"
f"Did you run `docker compose up -d` from `libs/sdk-py/integration/`?\n"
f"Or set LANGGRAPH_INTEGRATION_URL=... to point elsewhere.\n"
) from err
@@ -0,0 +1,185 @@
"""Exercise mid-run cancellation against the integration API.
Strategy: start a run on a fresh thread, capture the run id, then
cancel via the runs REST client while events are still flowing. The
projection iterator must terminate without hanging, no exception
should escape, and the thread's persisted status must reflect a
non-success terminal state.
The graph normally interrupts at `ask_human`; cancel must take effect
before or after that interrupt, and either way the run must end up in
a non-success state from the server's perspective.
"""
from __future__ import annotations
import asyncio
import contextlib
import threading
import time
from typing import Any
from _common import (
ASSISTANT_ID,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
_CANCEL_GRACE_SECONDS = 10.0
async def _cancel_after_first_event(
runs_client: Any,
thread_id: str,
run_id_future: asyncio.Future[str],
) -> None:
"""Wait for the run id, briefly let events flow, then cancel."""
run_id = await run_id_future
# Allow a beat of events to flow so cancel hits mid-stream rather
# than racing with the run.start handshake.
await asyncio.sleep(0.1)
with contextlib.suppress(Exception):
await runs_client.cancel(thread_id, run_id, wait=False)
async def run_async() -> None:
header("async mid-run cancel")
threads, raw = make_async_client()
# Cancel goes through the runs REST surface, not the stream proxy.
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._async.runs import RunsClient
runs_client = RunsClient(HttpClient(raw))
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
run_id_future: asyncio.Future[str] = (
asyncio.get_running_loop().create_future()
)
start_result = await thread.run.start(
input={"messages": [], "value": "init", "items": []}
)
run_id = start_result.get("run_id")
assert run_id, f"run.start returned no run_id: {start_result!r}"
run_id_future.set_result(run_id)
canceller = asyncio.create_task(
_cancel_after_first_event(runs_client, thread.thread_id, run_id_future)
)
snapshots: list[dict] = []
started = time.monotonic()
iteration_error: BaseException | None = None
try:
async for snap in thread.values:
snapshots.append(snap)
if time.monotonic() - started > _CANCEL_GRACE_SECONDS:
raise AssertionError(
f"values iterator did not terminate within "
f"{_CANCEL_GRACE_SECONDS}s of cancel"
)
except BaseException as err:
iteration_error = err
await canceller
persisted = await threads.get(thread.thread_id)
status = persisted.get("status")
print(f" snapshots before cancel: {len(snapshots)}")
print(f" thread.thread_id={thread.thread_id}")
print(f" iteration_error={iteration_error!r}")
print(f" persisted status={status!r}")
assert iteration_error is None, (
f"values iterator raised after cancel: {iteration_error!r}"
)
assert status != "success", (
f"expected non-success terminal status after cancel, got {status!r}"
)
finally:
await raw.aclose()
def _cancel_after_first_event_sync(
runs_client: Any,
thread_id: str,
run_id_event: threading.Event,
run_id_holder: dict[str, str],
) -> None:
run_id_event.wait(timeout=10.0)
run_id = run_id_holder.get("run_id")
if not run_id:
return
time.sleep(0.1)
with contextlib.suppress(Exception):
runs_client.cancel(thread_id, run_id, wait=False)
def run_sync() -> None:
header("sync mid-run cancel")
threads, raw = make_sync_client()
from langgraph_sdk._sync.http import SyncHttpClient
from langgraph_sdk._sync.runs import SyncRunsClient
runs_client = SyncRunsClient(SyncHttpClient(raw))
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
run_id_event = threading.Event()
run_id_holder: dict[str, str] = {}
start_result = thread.run.start(
input={"messages": [], "value": "init", "items": []}
)
run_id = start_result.get("run_id")
assert run_id, f"run.start returned no run_id: {start_result!r}"
run_id_holder["run_id"] = run_id
run_id_event.set()
canceller = threading.Thread(
target=_cancel_after_first_event_sync,
args=(runs_client, thread.thread_id, run_id_event, run_id_holder),
daemon=True,
name="cancel-worker",
)
canceller.start()
snapshots: list[dict] = []
started = time.monotonic()
iteration_error: BaseException | None = None
try:
for snap in thread.values:
snapshots.append(snap)
if time.monotonic() - started > _CANCEL_GRACE_SECONDS:
raise AssertionError(
f"values iterator did not terminate within "
f"{_CANCEL_GRACE_SECONDS}s of cancel"
)
except BaseException as err:
iteration_error = err
canceller.join(timeout=5)
persisted = threads.get(thread.thread_id)
status = persisted.get("status")
print(f" snapshots before cancel: {len(snapshots)}")
print(f" thread.thread_id={thread.thread_id}")
print(f" iteration_error={iteration_error!r}")
print(f" persisted status={status!r}")
assert iteration_error is None, (
f"values iterator raised after cancel: {iteration_error!r}"
)
assert status != "success", (
f"expected non-success terminal status after cancel, got {status!r}"
)
finally:
with contextlib.suppress(Exception):
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,128 @@
"""Exercise concurrent `threads.stream()` against the integration API.
Two distinct threads.stream() contexts run in parallel against the same
client. Each context is independent (different thread_id minted by the
SDK, separate controller, separate auto-responder). Invariants:
1. Both runs reach the canonical terminal state independently
(`items == ['streamed','tool','asked','sub']`).
2. Their thread_ids differ (no thread-id collision when minting client-side).
3. Neither raises during iteration.
This catches regressions where the two streams might share controller
state or where minted ids could collide under concurrent ``__aenter__``.
"""
from __future__ import annotations
import asyncio
import contextlib
import threading
from typing import Any
from _common import (
ASSISTANT_ID,
auto_respond_async,
auto_respond_sync,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
_EXPECTED_TERMINAL_ITEMS = ["streamed", "tool", "asked", "sub"]
async def _drive_one_async(threads: Any, label: str) -> dict[str, Any]:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_async(thread)
# Just drain values until terminal; we only care about the final state.
async for _ in thread.values:
pass
await responder
final = await thread.output
print(f" [{label}] thread_id={thread.thread_id} items={final.get('items')!r}")
return {"thread_id": thread.thread_id, "items": final.get("items")}
async def run_async() -> None:
header("async concurrent threads.stream (x2)")
threads, raw = make_async_client()
try:
results = await asyncio.gather(
_drive_one_async(threads, "A"),
_drive_one_async(threads, "B"),
)
a, b = results
assert a["items"] == _EXPECTED_TERMINAL_ITEMS, (
f"stream A failed to reach terminal: {a!r}"
)
assert b["items"] == _EXPECTED_TERMINAL_ITEMS, (
f"stream B failed to reach terminal: {b!r}"
)
assert a["thread_id"] != b["thread_id"], (
f"concurrent streams collided on thread_id {a['thread_id']!r}"
)
finally:
await raw.aclose()
def _drive_one_sync(
threads: Any, label: str, results: dict[str, dict[str, Any]]
) -> None:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_sync(thread)
for _ in thread.values:
pass
responder.join(timeout=10)
final = thread.output
print(f" [{label}] thread_id={thread.thread_id} items={final.get('items')!r}")
results[label] = {"thread_id": thread.thread_id, "items": final.get("items")}
def run_sync() -> None:
header("sync concurrent threads.stream (x2)")
threads, raw = make_sync_client()
try:
results: dict[str, dict[str, Any]] = {}
workers = [
threading.Thread(
target=_drive_one_sync,
args=(threads, label, results),
daemon=True,
name=f"sync-stream-{label}",
)
for label in ("A", "B")
]
for w in workers:
w.start()
for w in workers:
w.join(timeout=60)
assert not w.is_alive(), f"worker {w.name} did not finish within 60s"
a = results.get("A")
b = results.get("B")
assert a is not None and a["items"] == _EXPECTED_TERMINAL_ITEMS, (
f"stream A failed to reach terminal: {a!r}"
)
assert b is not None and b["items"] == _EXPECTED_TERMINAL_ITEMS, (
f"stream B failed to reach terminal: {b!r}"
)
assert a["thread_id"] != b["thread_id"], (
f"concurrent streams collided on thread_id {a['thread_id']!r}"
)
finally:
with contextlib.suppress(Exception):
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,67 @@
"""Exercise `thread.extensions[name]` against the integration API.
Every node in the example graph writes `("progress", {...})` via
`get_stream_writer`. This script verifies the `extensions["progress"]`
projection yields each progress event in order.
"""
from __future__ import annotations
import asyncio
from _common import (
ASSISTANT_ID,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
async def run_async() -> None:
header("async extensions[progress]")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
events: list[dict] = []
async for event in thread.extensions["progress"]:
print(f" progress: {event!r}")
events.append(event)
print(f" total progress events: {len(events)}")
assert events, "expected at least one progress event"
# Verify ordering covers the node sequence.
steps = [e.get("step") for e in events]
print(f" step sequence: {steps}")
finally:
await raw.aclose()
def run_sync() -> None:
header("sync extensions[progress]")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
events: list[dict] = []
for event in thread.extensions["progress"]:
print(f" progress: {event!r}")
events.append(event)
print(f" total progress events: {len(events)}")
assert events, "expected at least one progress event"
steps = [e.get("step") for e in events]
print(f" step sequence: {steps}")
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,67 @@
"""Exercise helper methods on `thread` against the integration API.
Covers `thread.agent.get_tree(xray=...)` and the extensions cache.
"""
from __future__ import annotations
import asyncio
from _common import (
ASSISTANT_ID,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
async def run_async() -> None:
header("async helpers (agent.get_tree, extensions cache)")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
tree = await thread.agent.get_tree()
print(f" get_tree() returned: nodes={list(tree.get('nodes', []))[:5]} ...")
assert tree, "expected non-empty tree"
tree_xray = await thread.agent.get_tree(xray=True)
print(f" get_tree(xray=True) returned keys: {list(tree_xray)[:5]}")
# Extensions cache: same name returns same projection instance.
a = thread.extensions["progress"]
b = thread.extensions["progress"]
assert a is b, "expected cached _ExtensionProjection on repeated access"
print(" extensions cache: OK (same projection instance reused)")
finally:
await raw.aclose()
def run_sync() -> None:
header("sync helpers (agent.get_tree, extensions cache)")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
tree = thread.agent.get_tree()
print(f" get_tree() returned: nodes={list(tree.get('nodes', []))[:5]} ...")
assert tree, "expected non-empty tree"
tree_xray = thread.agent.get_tree(xray=True)
print(f" get_tree(xray=True) returned keys: {list(tree_xray)[:5]}")
a = thread.extensions["progress"]
b = thread.extensions["progress"]
assert a is b, "expected cached _ExtensionProjection on repeated access"
print(" extensions cache: OK (same projection instance reused)")
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,97 @@
"""Exercise lifecycle state + `thread.run.respond(...)` against the integration API.
The example graph's `ask_human` node calls `interrupt("Are we good?")`. This
script:
1. Starts a run.
2. Waits until `thread.interrupted` becomes True (an `input.requested`
lifecycle event lands).
3. Inspects `thread.interrupts` to see the outstanding payload.
4. Calls `thread.run.respond("yes")` to resume.
5. Awaits `thread.output` for the final state.
"""
from __future__ import annotations
import asyncio
from _common import (
ASSISTANT_ID,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
async def run_async() -> None:
header("async lifecycle + respond")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
# Drain values until interrupt fires. (`thread.values` ends when
# the run terminates OR when the run is paused on an interrupt;
# the latter sets `thread.interrupted` mid-iteration.)
saw_interrupt = False
async for _snap in thread.values:
if thread.interrupted:
saw_interrupt = True
break
print(f" thread.interrupted = {thread.interrupted}")
print(f" thread.interrupts = {thread.interrupts!r}")
assert thread.interrupted, "expected an interrupt before the run completed"
assert thread.interrupts, "expected interrupts list to be populated"
await thread.run.respond("yes")
final = await thread.output
print(f" final output items: {final.get('items')!r}")
assert "asked" in final.get("items", []), (
"expected ask_human to have run after respond"
)
print(f" saw_interrupt before respond = {saw_interrupt}")
finally:
await raw.aclose()
def run_sync() -> None:
header("sync lifecycle + respond")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
saw_interrupt = False
for _snap in thread.values:
if thread.interrupted:
saw_interrupt = True
break
print(f" thread.interrupted = {thread.interrupted}")
print(f" thread.interrupts = {thread.interrupts!r}")
assert thread.interrupted, "expected an interrupt before the run completed"
assert thread.interrupts, "expected interrupts list to be populated"
thread.run.respond("yes")
final = thread.output
print(f" final output items: {final.get('items')!r}")
assert "asked" in final.get("items", []), (
"expected ask_human to have run after respond"
)
print(f" saw_interrupt before respond = {saw_interrupt}")
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,83 @@
"""Exercise `thread.messages` against the integration API.
The ``stream_message`` node in ``streaming_graph`` invokes a fake
``FakeMessagesListChatModel`` whose ``_stream`` callbacks drive
langgraph's ``StreamMessagesHandlerV2`` -> ``MessagesTransformer``,
so the v3 ``messages`` channel emits the normalized delta lifecycle
(``message-start`` -> ``content-block-start`` ->
``content-block-delta`` -> ``content-block-finish`` ->
``message-finish``) at root namespace.
Pattern note: drain the outer iterator first (list comprehension)
before consuming each handle's chunks -- the outer iterator yields
on ``message-start`` but the inner ``chunk`` stream only completes
when ``message-finish`` is processed by the outer iter. Iterating
chunks while the outer is suspended at ``yield`` deadlocks.
"""
from __future__ import annotations
import asyncio
from _common import (
ASSISTANT_ID,
auto_respond_async,
auto_respond_sync,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
async def run_async() -> None:
header("async messages")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
# Graph interrupts at `ask_human`; background responder
# unblocks so terminal lifecycle fires and the messages
# iterator exits cleanly.
responder = auto_respond_async(thread)
streams = [s async for s in thread.messages]
await responder
print(f" total streams: {len(streams)}")
for stream in streams:
text = "".join([t async for t in stream.text])
msg_id = getattr(stream, "message_id", None) or "?"
print(f" message {msg_id}: {text!r}")
assert streams, "expected at least one streamed message"
finally:
await raw.aclose()
def run_sync() -> None:
header("sync messages")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_sync(thread)
streams = list(thread.messages)
responder.join(timeout=5)
print(f" total streams: {len(streams)}")
for stream in streams:
text = "".join(list(stream.text))
msg_id = getattr(stream, "message_id", None) or "?"
print(f" message {msg_id}: {text!r}")
assert streams, "expected at least one streamed message"
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,197 @@
"""Exercise stream-handle close + recovery against the integration API.
The SDK's "reconnect on transport drop" code path (controller
`_reconnect_shared_stream`) only fires when `shared.done` resolves to a
non-cancelled error i.e. genuine network/server failures, not graceful
client-initiated closes. Reliably faking such an error against a real
server is brittle, so this script asserts the next-strongest invariant:
**a client-initiated stream close mid-iteration must not corrupt durable
state**.
Concretely:
1. Start the run; let the auto-responder unblock the interrupt.
2. Drop the shared SSE handle after the first snapshot.
3. The values projection iterator may end early (the close drains the
sub queue with `None`), but `thread.output` must still resolve to the
canonical terminal state via the REST fallback path.
4. No exception escapes the iteration.
We also instrument `_dedup_iter` to count any duplicate event_ids and
print the counter for visibility. A future regression that
double-delivers events through the controller would surface here.
"""
from __future__ import annotations
import asyncio
import contextlib
import functools
from typing import Any
from _common import (
ASSISTANT_ID,
auto_respond_async,
auto_respond_sync,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
_EXPECTED_TERMINAL_ITEMS = ["streamed", "tool", "asked", "sub"]
def _instrument_dedup_async(controller: Any) -> dict[str, int]:
"""Wrap `_dedup_iter` so duplicate event_ids are counted."""
counter = {"drops": 0, "yields": 0}
original = controller._dedup_iter.__func__ # type: ignore[attr-defined]
@functools.wraps(original)
async def _counted(self, source): # type: ignore[no-untyped-def]
async for event in source:
event_id = event.get("event_id")
if event_id is not None:
if event_id in self._seen_event_ids:
counter["drops"] += 1
continue
self._seen_event_ids.add(event_id)
counter["yields"] += 1
yield event
controller._dedup_iter = _counted.__get__(controller, type(controller))
return counter
def _instrument_dedup_sync(controller: Any) -> dict[str, int]:
counter = {"drops": 0, "yields": 0}
original = controller._dedup_iter.__func__ # type: ignore[attr-defined]
@functools.wraps(original)
def _counted(self, source): # type: ignore[no-untyped-def]
for event in source:
event_id = event.get("event_id")
if event_id is not None:
if event_id in self._seen_event_ids:
counter["drops"] += 1
continue
self._seen_event_ids.add(event_id)
counter["yields"] += 1
yield event
controller._dedup_iter = _counted.__get__(controller, type(controller))
return counter
async def run_async() -> None:
header("async stream-close mid-iteration (terminal state via REST)")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
counter = _instrument_dedup_async(thread)
await thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_async(thread)
snapshots: list[dict] = []
dropped = False
iteration_error: BaseException | None = None
try:
async for snap in thread.values:
snapshots.append(snap)
if not dropped and thread._shared_stream is not None:
print(f" dropping shared stream (cursor={thread._cursor})...")
await thread._shared_stream.close()
dropped = True
except BaseException as err:
iteration_error = err
await responder
final = await thread.output
print(f" snapshots seen before drop: {len(snapshots)}")
print(f" final items={final.get('items')!r}")
print(f" dedup drops={counter['drops']} yields={counter['yields']}")
print(f" iteration_error={iteration_error!r}")
assert dropped, "expected to drop the shared stream during iteration"
assert snapshots, "expected at least one snapshot before the drop"
assert iteration_error is None, (
f"values iterator raised on stream close: {iteration_error!r}"
)
assert final.get("items") == _EXPECTED_TERMINAL_ITEMS, (
f"terminal state not reached via REST after drop: "
f"items={final.get('items')!r}"
)
assert counter["drops"] == 0, (
f"unexpected dedup activity (drops={counter['drops']}); "
"no rotation occurred so no overlap was expected"
)
finally:
await raw.aclose()
def run_sync() -> None:
header("sync stream-close mid-iteration (terminal state via REST)")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
controller = thread._controller
counter = _instrument_dedup_sync(controller)
thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_sync(thread)
snapshots: list[dict] = []
dropped = False
iteration_error: BaseException | None = None
try:
for snap in thread.values:
snapshots.append(snap)
if (
not dropped
and controller is not None
and controller._shared_stream is not None
):
print(
f" dropping shared stream (cursor={controller._cursor})..."
)
controller._shared_stream.close()
dropped = True
except BaseException as err:
iteration_error = err
responder.join(timeout=10)
final = thread.output
print(f" snapshots seen before drop: {len(snapshots)}")
print(f" final items={final.get('items')!r}")
print(f" dedup drops={counter['drops']} yields={counter['yields']}")
print(f" iteration_error={iteration_error!r}")
assert dropped, "expected to drop the shared stream during iteration"
assert snapshots, "expected at least one snapshot before the drop"
assert iteration_error is None, (
f"values iterator raised on stream close: {iteration_error!r}"
)
assert final.get("items") == _EXPECTED_TERMINAL_ITEMS, (
f"terminal state not reached via REST after drop: "
f"items={final.get('items')!r}"
)
assert counter["drops"] == 0, (
f"unexpected dedup activity (drops={counter['drops']}); "
"no rotation occurred so no overlap was expected"
)
finally:
with contextlib.suppress(Exception):
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,107 @@
"""Exercise `thread.subgraphs` against both example graphs.
Two passes:
1. `agent` (plain `StateGraph`): the parent calls a nested subgraph via
`subgraph.invoke(...)` from a node. This may or may not surface as a
v3 scoped child handle depending on how the server emits namespaces
for nested invokes included so we can compare behavior.
2. `deep_agent`: built with `create_deep_agent` + one `SubAgent`. The
supervisor's model is scripted to issue a `task(researcher, ...)`
call, which IS the path that produces a proper scoped child handle
on `thread.subgraphs`. This is the canonical exercise for the v3
scoped-subgraph surface.
"""
from __future__ import annotations
import asyncio
from _common import check_api_reachable, header, make_async_client, make_sync_client
async def _drain_subgraphs(thread) -> list:
"""Drain ``thread.subgraphs`` to a list of {path, messages} dicts.
The outer subgraphs iterator must complete before we deep-iterate
each handle's ``messages`` projection -- the same nested-iteration
deadlock pattern as ``thread.tool_calls`` (see
``test_tools.py``). So we first collect handles, then drain
each handle's messages serially.
"""
handles: list = [h async for h in thread.subgraphs]
children: list = []
for child in handles:
print(f" child handle path={child.path}")
# Just count handle paths; deep message iteration on scoped
# handles has its own draining pattern and isn't the goal of
# this test (which exercises subgraph discovery via
# child-namespace ``lifecycle: started``).
children.append({"path": child.path})
return children
def _drain_subgraphs_sync(thread) -> list:
handles = list(thread.subgraphs)
children: list = []
for child in handles:
print(f" child handle path={child.path}")
children.append({"path": child.path})
return children
async def run_async() -> None:
threads, raw = make_async_client()
try:
header("async subgraphs / agent (plain StateGraph)")
async with threads.stream(assistant_id="agent") as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
children = await _drain_subgraphs(thread)
print(f" agent: total subgraph handles: {len(children)}")
header("async subgraphs / deep_agent (create_deep_agent + SubAgent)")
async with threads.stream(assistant_id="deep_agent") as thread:
await thread.run.start(
input={
"messages": [{"role": "user", "content": "research the v3 spec"}]
},
)
children = await _drain_subgraphs(thread)
print(f" deep_agent: total subgraph handles: {len(children)}")
assert children, "deep_agent should produce at least one direct-child handle"
finally:
await raw.aclose()
def run_sync() -> None:
threads, raw = make_sync_client()
try:
header("sync subgraphs / agent")
with threads.stream(assistant_id="agent") as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
children = _drain_subgraphs_sync(thread)
print(f" agent: total subgraph handles: {len(children)}")
header("sync subgraphs / deep_agent")
with threads.stream(assistant_id="deep_agent") as thread:
thread.run.start(
input={
"messages": [{"role": "user", "content": "research the v3 spec"}]
},
)
children = _drain_subgraphs_sync(thread)
print(f" deep_agent: total subgraph handles: {len(children)}")
assert children, "deep_agent should produce at least one direct-child handle"
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,96 @@
"""Exercise `thread.tool_calls` against the `tools_agent` graph.
`tools_agent` (`graph/tools_agent.py`) wraps a
`FakeMessagesListChatModel` in `create_agent` with a real `search`
tool. The first scripted model turn returns an `AIMessage` with a
`tool_calls=[search(query="v3")]`; langchain's tool node then executes
`search` and surfaces a `ToolMessage`; the second turn returns a final
`AIMessage("done.")` that terminates the agent.
Compared to `test_tool_calls.py` (which targets the synthetic
`streaming_graph` and never produces real tool-call telemetry), this
test verifies the v3 ``tools`` channel actually fires when the
canonical langchain-agent surface is in play.
Pattern note: `thread.tool_calls` yields handles incrementally, but
each handle's `deltas` and `output` are only completed when the
*outer* iterator processes the matching `tool-finished` event. Drain
the outer iterator to completion FIRST (via a list comprehension),
then inspect handles -- this is the same pattern used in
`tests/streaming/test_tool_calls_projection.py`. Iterating
`handle.deltas` while the outer iterator is still suspended at its
`yield` deadlocks.
"""
from __future__ import annotations
import asyncio
from _common import check_api_reachable, header, make_async_client, make_sync_client
TOOLS_ASSISTANT_ID = "tools_agent"
async def run_async() -> None:
header("async tools_agent tool_calls")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=TOOLS_ASSISTANT_ID) as thread:
await thread.run.start(
input={"messages": [{"role": "human", "content": "search for v3"}]}
)
# Drain the outer iterator first; lifecycle-terminal triggers
# the None sentinel via the shared SSE fanout once the run
# completes naturally.
handles = [h async for h in thread.tool_calls]
print(f" total handles: {len(handles)}")
for handle in handles:
deltas = [d async for d in handle.deltas]
output = await handle.output
joined = "".join(deltas)
print(
f" tool {handle.name}({handle.tool_call_id}): "
f"args_stream={joined!r} output={output!r}"
)
assert any(h.name == "search" for h in handles), (
"expected `search` tool call"
)
finally:
await raw.aclose()
def run_sync() -> None:
header("sync tools_agent tool_calls")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=TOOLS_ASSISTANT_ID) as thread:
thread.run.start(
input={"messages": [{"role": "human", "content": "search for v3"}]}
)
handles = list(thread.tool_calls)
print(f" total handles: {len(handles)}")
for handle in handles:
deltas = list(handle.deltas)
output = handle.output
joined = "".join(deltas)
print(
f" tool {handle.name}({handle.tool_call_id}): "
f"args_stream={joined!r} output={output!r}"
)
assert any(h.name == "search" for h in handles), (
"expected `search` tool call"
)
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,158 @@
"""Exercise `threads.update_state(...)` mid-run against the integration API.
Flow:
1. Stream the canonical graph; `run.start` with `value="init"`.
2. Drain values until the interrupt fires at `ask_human`.
3. Call `threads.update_state(thread_id, {"value": "patched"})` to mutate
the persisted state while the run is paused.
4. Read state back via `threads.get_state(thread_id)` and assert
`state["values"]["value"] == "patched"`.
Why no respond afterwards: in langgraph-api, `update_state` against an
interrupted thread commits a new checkpoint that consumes the
outstanding interrupt. A subsequent `run.respond(...)` then fails with
`no_such_interrupt`. The meaningful integration invariant here is just
that the REST mutation lands on the same persisted thread the streaming
proxy was driving (no thread_id drift between client and server).
"""
from __future__ import annotations
import asyncio
import contextlib
import time
from _common import (
ASSISTANT_ID,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
from langgraph_sdk.errors import ConflictError
_PATCHED_VALUE = "patched"
_UPDATE_STATE_RETRY_BUDGET = 5.0
async def _update_state_with_retry_async(threads, thread_id: str, values: dict) -> None:
"""Retry update_state on ConflictError until the server's run row settles.
`thread.interrupted` flips when the client sees the `input.requested`
lifecycle event, which can land before the server commits the run row
to a non-busy state. Retry with backoff for a few seconds.
"""
delay = 0.05
deadline = asyncio.get_running_loop().time() + _UPDATE_STATE_RETRY_BUDGET
last_err: Exception | None = None
while asyncio.get_running_loop().time() < deadline:
try:
await threads.update_state(thread_id, values)
return
except ConflictError as err:
last_err = err
await asyncio.sleep(delay)
delay = min(delay * 2, 0.5)
raise AssertionError(
f"update_state never accepted within {_UPDATE_STATE_RETRY_BUDGET}s: {last_err!r}"
)
def _update_state_with_retry_sync(threads, thread_id: str, values: dict) -> None:
delay = 0.05
deadline = time.monotonic() + _UPDATE_STATE_RETRY_BUDGET
last_err: Exception | None = None
while time.monotonic() < deadline:
try:
threads.update_state(thread_id, values)
return
except ConflictError as err:
last_err = err
time.sleep(delay)
delay = min(delay * 2, 0.5)
raise AssertionError(
f"update_state never accepted within {_UPDATE_STATE_RETRY_BUDGET}s: {last_err!r}"
)
async def run_async() -> None:
header("async update_state during interrupt")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
async for _ in thread.values:
if thread.interrupted:
break
assert thread.interrupted, "expected interrupt before update_state"
pre_state = await threads.get_state(thread.thread_id)
pre_value = (pre_state.get("values") or {}).get("value")
print(f" pre-update value={pre_value!r}")
# `stream_message` overwrites value="init" with value="x" before the
# interrupt; verify we're starting from the expected pre-update state.
assert pre_value == "x", f"unexpected pre-update value: {pre_value!r}"
await _update_state_with_retry_async(
threads, thread.thread_id, {"value": _PATCHED_VALUE}
)
post_state = await threads.get_state(thread.thread_id)
post_values = post_state.get("values") or {}
post_value = post_values.get("value")
print(f" thread_id={thread.thread_id}")
print(f" post-update value={post_value!r}")
assert post_value == _PATCHED_VALUE, (
f"update_state did not persist: value={post_value!r}"
)
finally:
await raw.aclose()
def run_sync() -> None:
header("sync update_state during interrupt")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
for _ in thread.values:
if thread.interrupted:
break
assert thread.interrupted, "expected interrupt before update_state"
pre_state = threads.get_state(thread.thread_id)
pre_value = (pre_state.get("values") or {}).get("value")
print(f" pre-update value={pre_value!r}")
# `stream_message` overwrites value="init" with value="x" before the
# interrupt; verify we're starting from the expected pre-update state.
assert pre_value == "x", f"unexpected pre-update value: {pre_value!r}"
_update_state_with_retry_sync(
threads, thread.thread_id, {"value": _PATCHED_VALUE}
)
post_state = threads.get_state(thread.thread_id)
post_values = post_state.get("values") or {}
post_value = post_values.get("value")
print(f" thread_id={thread.thread_id}")
print(f" post-update value={post_value!r}")
assert post_value == _PATCHED_VALUE, (
f"update_state did not persist: value={post_value!r}"
)
finally:
with contextlib.suppress(Exception):
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,89 @@
"""Exercise `thread.values` against the integration API.
The integration graph's `ask_human` node interrupts mid-run. The
projection iterators (`thread.values`, `.messages`, `.tool_calls`,
`.subgraphs`) do not terminate on interrupt they're paused, waiting
for more events. To drain the full run end-to-end we use a background
auto-responder that watches `thread.interrupted` and calls
`thread.run.respond(...)` so the run continues to the terminal.
Run after `docker compose up -d` from `libs/sdk-py/integration/`:
uv run python integration/scripts/test_values.py
"""
from __future__ import annotations
import asyncio
from _common import (
ASSISTANT_ID,
auto_respond_async,
auto_respond_sync,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
async def run_async() -> None:
header("async values")
threads, raw = make_async_client()
try:
async with threads.stream(assistant_id=ASSISTANT_ID) as thread:
await thread.run.start(input={"messages": [], "value": "init", "items": []})
# Background task: respond to the interrupt so the iterator
# eventually sees terminal-completion events.
responder = auto_respond_async(thread)
snapshots: list[dict] = []
async for snap in thread.values:
snapshots.append(snap)
print(
f" values snapshot: items={snap.get('items')!r} value={snap.get('value')!r}"
)
await responder
final = await thread.output
print(f" final output items={final.get('items')!r}")
assert "sub" in final.get("items", []), "expected subgraph to have run"
finally:
await raw.aclose()
def run_sync() -> None:
header("sync values")
threads, raw = make_sync_client()
try:
with threads.stream(assistant_id=ASSISTANT_ID) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_sync(thread)
snapshots: list[dict] = []
for snap in thread.values:
snapshots.append(snap)
print(
f" values snapshot: items={snap.get('items')!r} value={snap.get('value')!r}"
)
responder.join(timeout=5)
final = thread.output
print(f" final output items={final.get('items')!r}")
assert "sub" in final.get("items", []), "expected subgraph to have run"
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
@@ -0,0 +1,92 @@
"""Exercise the WebSocket transport against the integration API.
Equivalent to `test_values.py` but with `transport="websocket"`.
"""
from __future__ import annotations
import asyncio
from _common import (
ASSISTANT_ID,
auto_respond_async,
auto_respond_sync,
check_api_reachable,
header,
make_async_client,
make_sync_client,
)
async def run_async() -> None:
header("async websocket transport")
threads, raw = make_async_client()
try:
async with threads.stream(
assistant_id=ASSISTANT_ID,
transport="websocket",
) as thread:
from langgraph_sdk.stream.transport import ProtocolWebSocketTransport
assert isinstance(thread._transport, ProtocolWebSocketTransport), (
f"expected ws transport, got {type(thread._transport).__name__}"
)
await thread.run.start(input={"messages": [], "value": "init", "items": []})
# The graph interrupts at `ask_human`; without a background
# responder the values iterator would pause indefinitely.
responder = auto_respond_async(thread)
snapshots: list[dict] = []
async for snap in thread.values:
snapshots.append(snap)
print(f" ws values snapshot items={snap.get('items')!r}")
await responder
final = await thread.output
print(f" final via ws: items={final.get('items')!r}")
assert "sub" in final.get("items", []), (
"expected subgraph to have run via ws transport"
)
finally:
await raw.aclose()
def run_sync() -> None:
header("sync websocket transport")
threads, raw = make_sync_client()
try:
with threads.stream(
assistant_id=ASSISTANT_ID,
transport="websocket",
) as thread:
thread.run.start(input={"messages": [], "value": "init", "items": []})
responder = auto_respond_sync(thread)
snapshots: list[dict] = []
for snap in thread.values:
snapshots.append(snap)
print(f" ws values snapshot items={snap.get('items')!r}")
responder.join(timeout=5)
final = thread.output
print(f" final via ws: items={final.get('items')!r}")
assert "sub" in final.get("items", []), (
"expected subgraph to have run via ws transport"
)
finally:
raw.close()
def main() -> None:
check_api_reachable()
asyncio.run(run_async())
run_sync()
if __name__ == "__main__":
main()
+1 -1
View File
@@ -3,6 +3,6 @@ from langgraph_sdk.client import get_client, get_sync_client
from langgraph_sdk.encryption import Encryption
from langgraph_sdk.encryption.types import EncryptionContext
__version__ = "0.3.14"
__version__ = "0.4.2"
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
+16 -9
View File
@@ -8,6 +8,7 @@ from typing import Any, Literal, cast, overload
import httpx
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.schema import (
Assistant,
AssistantSelectField,
@@ -84,7 +85,9 @@ class AssistantsClient:
```
"""
return await self.http.get(
f"/assistants/{assistant_id}", headers=headers, params=params
f"/assistants/{_quote_path_param(assistant_id)}",
headers=headers,
params=params,
)
async def get_graph(
@@ -142,7 +145,9 @@ class AssistantsClient:
query_params.update(params)
return await self.http.get(
f"/assistants/{assistant_id}/graph", params=query_params, headers=headers
f"/assistants/{_quote_path_param(assistant_id)}/graph",
params=query_params,
headers=headers,
)
async def get_schemas(
@@ -263,7 +268,9 @@ class AssistantsClient:
"""
return await self.http.get(
f"/assistants/{assistant_id}/schemas", headers=headers, params=params
f"/assistants/{_quote_path_param(assistant_id)}/schemas",
headers=headers,
params=params,
)
async def get_subgraphs(
@@ -293,13 +300,13 @@ class AssistantsClient:
get_params = {**get_params, **dict(params)}
if namespace is not None:
return await self.http.get(
f"/assistants/{assistant_id}/subgraphs/{namespace}",
f"/assistants/{_quote_path_param(assistant_id)}/subgraphs/{_quote_path_param(namespace)}",
params=get_params,
headers=headers,
)
else:
return await self.http.get(
f"/assistants/{assistant_id}/subgraphs",
f"/assistants/{_quote_path_param(assistant_id)}/subgraphs",
params=get_params,
headers=headers,
)
@@ -436,7 +443,7 @@ class AssistantsClient:
if description:
payload["description"] = description
return await self.http.patch(
f"/assistants/{assistant_id}",
f"/assistants/{_quote_path_param(assistant_id)}",
json=payload,
headers=headers,
params=params,
@@ -479,7 +486,7 @@ class AssistantsClient:
if params:
query_params.update(params)
await self.http.delete(
f"/assistants/{assistant_id}",
f"/assistants/{_quote_path_param(assistant_id)}",
headers=headers,
params=query_params or None,
)
@@ -686,7 +693,7 @@ class AssistantsClient:
if metadata:
payload["metadata"] = metadata
return await self.http.post(
f"/assistants/{assistant_id}/versions",
f"/assistants/{_quote_path_param(assistant_id)}/versions",
json=payload,
headers=headers,
params=params,
@@ -726,7 +733,7 @@ class AssistantsClient:
payload: dict[str, Any] = {"version": version}
return await self.http.post(
f"/assistants/{assistant_id}/latest",
f"/assistants/{_quote_path_param(assistant_id)}/latest",
json=payload,
headers=headers,
params=params,
+6 -4
View File
@@ -8,7 +8,7 @@ from datetime import datetime, tzinfo
from typing import Any
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._shared.utilities import _resolve_timezone
from langgraph_sdk._shared.utilities import _quote_path_param, _resolve_timezone
from langgraph_sdk.schema import (
All,
Config,
@@ -166,7 +166,7 @@ class CronClient:
payload["multitask_strategy"] = multitask_strategy
payload = {k: v for k, v in payload.items() if v is not None}
return await self.http.post(
f"/threads/{thread_id}/runs/crons",
f"/threads/{_quote_path_param(thread_id)}/runs/crons",
json=payload,
headers=headers,
params=params,
@@ -315,7 +315,9 @@ class CronClient:
```
"""
await self.http.delete(f"/runs/crons/{cron_id}", headers=headers, params=params)
await self.http.delete(
f"/runs/crons/{_quote_path_param(cron_id)}", headers=headers, params=params
)
async def update(
self,
@@ -402,7 +404,7 @@ class CronClient:
}
payload = {k: v for k, v in payload.items() if v is not None}
return await self.http.patch(
f"/runs/crons/{cron_id}",
f"/runs/crons/{_quote_path_param(cron_id)}",
json=payload,
headers=headers,
params=params,
+19 -10
View File
@@ -12,6 +12,7 @@ import httpx
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._shared.utilities import (
_get_run_metadata_from_response,
_quote_path_param,
_sse_to_v2_dict,
)
from langgraph_sdk.schema import (
@@ -337,7 +338,7 @@ class RunsClient:
"langsmith_tracer": langsmith_tracing,
}
endpoint = (
f"/threads/{thread_id}/runs/stream"
f"/threads/{_quote_path_param(thread_id)}/runs/stream"
if thread_id is not None
else "/runs/stream"
)
@@ -596,7 +597,7 @@ class RunsClient:
on_run_created(metadata)
return await self.http.post(
f"/threads/{thread_id}/runs" if thread_id else "/runs",
f"/threads/{_quote_path_param(thread_id)}/runs" if thread_id else "/runs",
json=payload,
params=params,
headers=headers,
@@ -821,7 +822,9 @@ class RunsClient:
"langsmith_tracer": langsmith_tracing,
}
endpoint = (
f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait"
f"/threads/{_quote_path_param(thread_id)}/runs/wait"
if thread_id is not None
else "/runs/wait"
)
def on_response(res: httpx.Response):
@@ -895,7 +898,9 @@ class RunsClient:
if params:
query_params.update(params)
return await self.http.get(
f"/threads/{thread_id}/runs", params=query_params, headers=headers
f"/threads/{_quote_path_param(thread_id)}/runs",
params=query_params,
headers=headers,
)
async def get(
@@ -930,7 +935,9 @@ class RunsClient:
"""
return await self.http.get(
f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}",
headers=headers,
params=params,
)
async def cancel(
@@ -978,14 +985,14 @@ class RunsClient:
query_params.update(params)
if wait:
return await self.http.request_reconnect(
f"/threads/{thread_id}/runs/{run_id}/cancel",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/cancel",
"POST",
params=query_params,
headers=headers,
)
else:
return await self.http.post(
f"/threads/{thread_id}/runs/{run_id}/cancel",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/cancel",
json=None,
params=query_params,
headers=headers,
@@ -1081,7 +1088,7 @@ class RunsClient:
"""
return await self.http.request_reconnect(
f"/threads/{thread_id}/runs/{run_id}/join",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/join",
"GET",
headers=headers,
params=params,
@@ -1136,7 +1143,7 @@ class RunsClient:
if params:
query_params.update(params)
return self.http.stream(
f"/threads/{thread_id}/runs/{run_id}/stream",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/stream",
"GET",
params=query_params,
headers={
@@ -1177,5 +1184,7 @@ class RunsClient:
"""
await self.http.delete(
f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}",
headers=headers,
params=params,
)
File diff suppressed because it is too large Load Diff
+67 -10
View File
@@ -2,10 +2,13 @@
from __future__ import annotations
import uuid
from collections.abc import AsyncIterator, Mapping, Sequence
from typing import Any, Literal, overload
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._async.stream import AsyncThreadStream
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.schema import (
Checkpoint,
Json,
@@ -90,7 +93,7 @@ class ThreadsClient:
if params:
query_params.update(params)
return await self.http.get(
f"/threads/{thread_id}",
f"/threads/{_quote_path_param(thread_id)}",
headers=headers,
params=query_params or None,
)
@@ -254,7 +257,7 @@ class ThreadsClient:
if return_minimal:
request_headers["Prefer"] = "return=minimal"
return await self.http.patch(
f"/threads/{thread_id}",
f"/threads/{_quote_path_param(thread_id)}",
json=payload,
headers=request_headers or None,
params=params,
@@ -287,7 +290,9 @@ class ThreadsClient:
```
"""
await self.http.delete(f"/threads/{thread_id}", headers=headers, params=params)
await self.http.delete(
f"/threads/{_quote_path_param(thread_id)}", headers=headers, params=params
)
async def search(
self,
@@ -430,7 +435,10 @@ class ThreadsClient:
"""
return await self.http.post(
f"/threads/{thread_id}/copy", json=None, headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/copy",
json=None,
headers=headers,
params=params,
)
async def prune(
@@ -586,7 +594,7 @@ class ThreadsClient:
"""
if checkpoint:
return await self.http.post(
f"/threads/{thread_id}/state/checkpoint",
f"/threads/{_quote_path_param(thread_id)}/state/checkpoint",
json={"checkpoint": checkpoint, "subgraphs": subgraphs},
headers=headers,
params=params,
@@ -596,7 +604,7 @@ class ThreadsClient:
if params:
get_params = {**get_params, **dict(params)}
return await self.http.get(
f"/threads/{thread_id}/state/{checkpoint_id}",
f"/threads/{_quote_path_param(thread_id)}/state/{_quote_path_param(checkpoint_id)}",
params=get_params,
headers=headers,
)
@@ -605,7 +613,7 @@ class ThreadsClient:
if params:
get_params = {**get_params, **dict(params)}
return await self.http.get(
f"/threads/{thread_id}/state",
f"/threads/{_quote_path_param(thread_id)}/state",
params=get_params,
headers=headers,
)
@@ -670,7 +678,10 @@ class ThreadsClient:
if as_node:
payload["as_node"] = as_node
return await self.http.post(
f"/threads/{thread_id}/state", json=payload, headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/state",
json=payload,
headers=headers,
params=params,
)
async def get_history(
@@ -719,12 +730,58 @@ class ThreadsClient:
if checkpoint:
payload["checkpoint"] = checkpoint
return await self.http.post(
f"/threads/{thread_id}/history",
f"/threads/{_quote_path_param(thread_id)}/history",
json=payload,
headers=headers,
params=params,
)
def stream(
self,
thread_id: str | None = None,
*,
assistant_id: str,
headers: Mapping[str, str] | None = None,
run_start_timeout: float | None = None,
transport: Literal["sse", "websocket"] = "sse",
) -> AsyncThreadStream:
"""Open a v3 thread-centric streaming session.
When `thread_id` is None, a fresh UUIDv4 is minted client-side and
included in the URL of subsequent `POST /threads/{thread_id}/...`
calls. The server creates the thread row lazily on the first
`run.start` (internal server detail the SDK does not send any
`if_not_exists` flag). The v3 protocol response carries only
`run_id`, never `thread_id` that's why the SDK mints the id
client-side.
Args:
thread_id: optional explicit thread identifier. Defaults to a
fresh UUIDv4.
assistant_id: assistant the run will use. Required.
headers: optional headers forwarded on every command and event
request for this stream session.
run_start_timeout: optional seconds to wait for an in-flight
`run.start` before subscribing operations raise
`asyncio.TimeoutError`. Defaults to `None` (wait forever).
transport: event transport to use `"sse"` (default) or
`"websocket"`.
Returns:
An `AsyncThreadStream` to use as an async context manager.
"""
if transport not in ("sse", "websocket"):
raise ValueError("transport must be 'sse' or 'websocket'.")
return AsyncThreadStream(
http=self.http,
thread_id=thread_id if thread_id is not None else str(uuid.uuid4()),
assistant_id=assistant_id,
headers=headers,
run_start_timeout=run_start_timeout,
explicit_thread_id=thread_id is not None,
transport_kind=transport,
)
async def join_stream(
self,
thread_id: str,
@@ -763,7 +820,7 @@ class ThreadsClient:
if params:
query_params.update(params)
return self.http.stream(
f"/threads/{thread_id}/stream",
f"/threads/{_quote_path_param(thread_id)}/stream",
"GET",
headers={
**({"Last-Event-ID": last_event_id} if last_event_id else {}),
+34 -1
View File
@@ -8,7 +8,7 @@ import re
from collections.abc import Mapping
from datetime import tzinfo
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import urlparse
from urllib.parse import quote, urlparse
import httpx
@@ -198,6 +198,39 @@ def _provided_vals(d: Mapping[str, Any]) -> dict[str, Any]:
return {k: v for k, v in d.items() if v is not None}
def _quote_path_param(value: Any) -> str:
"""Encode a value for safe interpolation into a request path segment.
Path segments are encoded with ``safe=""`` so that ``/`` and other reserved
characters are escaped. Standalone dot-segments (``.`` and ``..``) are also
encoded because some URL-handling stacks (including ``httpx``) collapse
them client-side as relative-path traversal before transmission. The value
is coerced to ``str`` so callers can pass ``uuid.UUID`` and similar types
directly without changing call sites.
A properly formed identifier (for example, a standard UUID, which contains
no dots or reserved characters) round-trips through this function
unchanged.
Raises:
TypeError: If `value` is `None` or a `bytes`/`bytearray` instance.
Coercing those would produce misleading paths (e.g. `/threads/None`),
so surface the caller bug instead.
"""
if value is None:
raise TypeError("path parameter must not be None")
if isinstance(value, (bytes, bytearray)):
raise TypeError("path parameter must not be bytes; pass a str or uuid.UUID")
quoted = quote(str(value), safe="")
# Bare "." or ".." (or any all-dot string) acts as a relative-path segment
# that some HTTP stacks (including ``httpx``) collapse client-side before
# transmission. Encode the dots so the segment becomes opaque to that
# logic. Mixed values like "agent.v1" are unaffected.
if quoted and all(c == "." for c in quoted):
quoted = "%2E" * len(quoted)
return quoted
_registered_transports: list[httpx.ASGITransport] = []
+16 -9
View File
@@ -7,6 +7,7 @@ from typing import Any, Literal, cast, overload
import httpx
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk._sync.http import SyncHttpClient
from langgraph_sdk.schema import (
Assistant,
@@ -83,7 +84,9 @@ class SyncAssistantsClient:
"""
return self.http.get(
f"/assistants/{assistant_id}", headers=headers, params=params
f"/assistants/{_quote_path_param(assistant_id)}",
headers=headers,
params=params,
)
def get_graph(
@@ -136,7 +139,9 @@ class SyncAssistantsClient:
if params:
query_params.update(params)
return self.http.get(
f"/assistants/{assistant_id}/graph", params=query_params, headers=headers
f"/assistants/{_quote_path_param(assistant_id)}/graph",
params=query_params,
headers=headers,
)
def get_schemas(
@@ -269,7 +274,9 @@ class SyncAssistantsClient:
"""
return self.http.get(
f"/assistants/{assistant_id}/schemas", headers=headers, params=params
f"/assistants/{_quote_path_param(assistant_id)}/schemas",
headers=headers,
params=params,
)
def get_subgraphs(
@@ -297,13 +304,13 @@ class SyncAssistantsClient:
get_params = {**get_params, **dict(params)}
if namespace is not None:
return self.http.get(
f"/assistants/{assistant_id}/subgraphs/{namespace}",
f"/assistants/{_quote_path_param(assistant_id)}/subgraphs/{_quote_path_param(namespace)}",
params=get_params,
headers=headers,
)
else:
return self.http.get(
f"/assistants/{assistant_id}/subgraphs",
f"/assistants/{_quote_path_param(assistant_id)}/subgraphs",
params=get_params,
headers=headers,
)
@@ -438,7 +445,7 @@ class SyncAssistantsClient:
if description:
payload["description"] = description
return self.http.patch(
f"/assistants/{assistant_id}",
f"/assistants/{_quote_path_param(assistant_id)}",
json=payload,
headers=headers,
params=params,
@@ -481,7 +488,7 @@ class SyncAssistantsClient:
if params:
query_params.update(params)
self.http.delete(
f"/assistants/{assistant_id}",
f"/assistants/{_quote_path_param(assistant_id)}",
headers=headers,
params=query_params or None,
)
@@ -685,7 +692,7 @@ class SyncAssistantsClient:
if metadata:
payload["metadata"] = metadata
return self.http.post(
f"/assistants/{assistant_id}/versions",
f"/assistants/{_quote_path_param(assistant_id)}/versions",
json=payload,
headers=headers,
params=params,
@@ -724,7 +731,7 @@ class SyncAssistantsClient:
payload: dict[str, Any] = {"version": version}
return self.http.post(
f"/assistants/{assistant_id}/latest",
f"/assistants/{_quote_path_param(assistant_id)}/latest",
json=payload,
headers=headers,
params=params,
+6 -4
View File
@@ -7,7 +7,7 @@ from collections.abc import Mapping, Sequence
from datetime import datetime, tzinfo
from typing import Any
from langgraph_sdk._shared.utilities import _resolve_timezone
from langgraph_sdk._shared.utilities import _quote_path_param, _resolve_timezone
from langgraph_sdk._sync.http import SyncHttpClient
from langgraph_sdk.schema import (
All,
@@ -156,7 +156,7 @@ class SyncCronClient:
}
payload = {k: v for k, v in payload.items() if v is not None}
return self.http.post(
f"/threads/{thread_id}/runs/crons",
f"/threads/{_quote_path_param(thread_id)}/runs/crons",
json=payload,
headers=headers,
params=params,
@@ -304,7 +304,9 @@ class SyncCronClient:
```
"""
self.http.delete(f"/runs/crons/{cron_id}", headers=headers, params=params)
self.http.delete(
f"/runs/crons/{_quote_path_param(cron_id)}", headers=headers, params=params
)
def update(
self,
@@ -391,7 +393,7 @@ class SyncCronClient:
}
payload = {k: v for k, v in payload.items() if v is not None}
return self.http.patch(
f"/runs/crons/{cron_id}",
f"/runs/crons/{_quote_path_param(cron_id)}",
json=payload,
headers=headers,
params=params,
+19 -10
View File
@@ -11,6 +11,7 @@ import httpx
from langgraph_sdk._shared.utilities import (
_get_run_metadata_from_response,
_quote_path_param,
_sse_to_v2_dict,
)
from langgraph_sdk._sync.http import SyncHttpClient
@@ -332,7 +333,7 @@ class SyncRunsClient:
"langsmith_tracer": langsmith_tracing,
}
endpoint = (
f"/threads/{thread_id}/runs/stream"
f"/threads/{_quote_path_param(thread_id)}/runs/stream"
if thread_id is not None
else "/runs/stream"
)
@@ -591,7 +592,7 @@ class SyncRunsClient:
on_run_created(metadata)
return self.http.post(
f"/threads/{thread_id}/runs" if thread_id else "/runs",
f"/threads/{_quote_path_param(thread_id)}/runs" if thread_id else "/runs",
json=payload,
params=params,
headers=headers,
@@ -825,7 +826,9 @@ class SyncRunsClient:
on_run_created(metadata)
endpoint = (
f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait"
f"/threads/{_quote_path_param(thread_id)}/runs/wait"
if thread_id is not None
else "/runs/wait"
)
return self.http.request_reconnect(
endpoint,
@@ -879,7 +882,9 @@ class SyncRunsClient:
if params:
query_params.update(params)
return self.http.get(
f"/threads/{thread_id}/runs", params=query_params, headers=headers
f"/threads/{_quote_path_param(thread_id)}/runs",
params=query_params,
headers=headers,
)
def get(
@@ -912,7 +917,9 @@ class SyncRunsClient:
"""
return self.http.get(
f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}",
headers=headers,
params=params,
)
def cancel(
@@ -960,14 +967,14 @@ class SyncRunsClient:
query_params.update(params)
if wait:
return self.http.request_reconnect(
f"/threads/{thread_id}/runs/{run_id}/cancel",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/cancel",
"POST",
json=None,
params=query_params,
headers=headers,
)
return self.http.post(
f"/threads/{thread_id}/runs/{run_id}/cancel",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/cancel",
json=None,
params=query_params,
headers=headers,
@@ -1063,7 +1070,7 @@ class SyncRunsClient:
"""
return self.http.request_reconnect(
f"/threads/{thread_id}/runs/{run_id}/join",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/join",
"GET",
headers=headers,
params=params,
@@ -1117,7 +1124,7 @@ class SyncRunsClient:
if params:
query_params.update(params)
return self.http.stream(
f"/threads/{thread_id}/runs/{run_id}/stream",
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}/stream",
"GET",
params=query_params,
headers={
@@ -1158,5 +1165,7 @@ class SyncRunsClient:
"""
self.http.delete(
f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/runs/{_quote_path_param(run_id)}",
headers=headers,
params=params,
)
File diff suppressed because it is too large Load Diff
+56 -10
View File
@@ -2,10 +2,13 @@
from __future__ import annotations
import uuid
from collections.abc import Iterator, Mapping, Sequence
from typing import Any, Literal, overload
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk._sync.http import SyncHttpClient
from langgraph_sdk._sync.stream import SyncThreadStream
from langgraph_sdk.schema import (
Checkpoint,
Json,
@@ -88,7 +91,7 @@ class SyncThreadsClient:
if params:
query_params.update(params)
return self.http.get(
f"/threads/{thread_id}",
f"/threads/{_quote_path_param(thread_id)}",
headers=headers,
params=query_params or None,
)
@@ -250,7 +253,7 @@ class SyncThreadsClient:
if return_minimal:
request_headers["Prefer"] = "return=minimal"
return self.http.patch(
f"/threads/{thread_id}",
f"/threads/{_quote_path_param(thread_id)}",
json=payload,
headers=request_headers or None,
params=params,
@@ -282,7 +285,9 @@ class SyncThreadsClient:
```
"""
self.http.delete(f"/threads/{thread_id}", headers=headers, params=params)
self.http.delete(
f"/threads/{_quote_path_param(thread_id)}", headers=headers, params=params
)
def search(
self,
@@ -421,7 +426,10 @@ class SyncThreadsClient:
"""
return self.http.post(
f"/threads/{thread_id}/copy", json=None, headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/copy",
json=None,
headers=headers,
params=params,
)
def prune(
@@ -576,7 +584,7 @@ class SyncThreadsClient:
"""
if checkpoint:
return self.http.post(
f"/threads/{thread_id}/state/checkpoint",
f"/threads/{_quote_path_param(thread_id)}/state/checkpoint",
json={"checkpoint": checkpoint, "subgraphs": subgraphs},
headers=headers,
params=params,
@@ -586,7 +594,7 @@ class SyncThreadsClient:
if params:
get_params = {**get_params, **dict(params)}
return self.http.get(
f"/threads/{thread_id}/state/{checkpoint_id}",
f"/threads/{_quote_path_param(thread_id)}/state/{_quote_path_param(checkpoint_id)}",
params=get_params,
headers=headers,
)
@@ -595,7 +603,7 @@ class SyncThreadsClient:
if params:
get_params = {**get_params, **dict(params)}
return self.http.get(
f"/threads/{thread_id}/state",
f"/threads/{_quote_path_param(thread_id)}/state",
params=get_params,
headers=headers,
)
@@ -657,7 +665,10 @@ class SyncThreadsClient:
if as_node:
payload["as_node"] = as_node
return self.http.post(
f"/threads/{thread_id}/state", json=payload, headers=headers, params=params
f"/threads/{_quote_path_param(thread_id)}/state",
json=payload,
headers=headers,
params=params,
)
def get_history(
@@ -707,12 +718,47 @@ class SyncThreadsClient:
if checkpoint:
payload["checkpoint"] = checkpoint
return self.http.post(
f"/threads/{thread_id}/history",
f"/threads/{_quote_path_param(thread_id)}/history",
json=payload,
headers=headers,
params=params,
)
def stream(
self,
thread_id: str | None = None,
*,
assistant_id: str,
headers: Mapping[str, str] | None = None,
run_start_timeout: float | None = None,
transport: Literal["sse", "websocket"] = "sse",
) -> SyncThreadStream:
"""Open a v3 thread-centric streaming session.
Args:
thread_id: optional explicit thread identifier. Defaults to a
fresh UUIDv4.
assistant_id: assistant the run will use. Required.
headers: optional headers forwarded on every command and SSE
request for this stream session.
transport: event transport to use, `"sse"` (default) or
`"websocket"`.
Returns:
A `SyncThreadStream` to use as a context manager.
"""
if transport not in ("sse", "websocket"):
raise ValueError("transport must be 'sse' or 'websocket'.")
return SyncThreadStream(
http=self.http,
thread_id=thread_id if thread_id is not None else str(uuid.uuid4()),
assistant_id=assistant_id,
headers=headers,
run_start_timeout=run_start_timeout,
explicit_thread_id=thread_id is not None,
transport_kind=transport,
)
def join_stream(
self,
thread_id: str,
@@ -752,7 +798,7 @@ class SyncThreadsClient:
if params:
query_params.update(params)
return self.http.stream(
f"/threads/{thread_id}/stream",
f"/threads/{_quote_path_param(thread_id)}/stream",
"GET",
headers={
**({"Last-Event-ID": last_event_id} if last_event_id else {}),
@@ -0,0 +1,15 @@
"""Stream module for LangGraph SDK v3."""
from langchain_protocol import (
Channel,
Event,
Namespace,
SubscribeParams,
)
__all__ = [
"Channel",
"Event",
"Namespace",
"SubscribeParams",
]
@@ -0,0 +1,398 @@
"""Stream controller: subscription registry and fan-out for AsyncThreadStream.
`StreamController` manages the set of active subscriptions against one shared
SSE connection, routing events from the shared stream to per-subscription
queues. It is the centralised place for:
- subscription registration / teardown
- shared-stream lifecycle (open, rotate, close)
- dedup of replayed events across rotations
- fan-out from the shared stream to subscriber queues
"""
from __future__ import annotations
import asyncio
import contextlib
import logging
import random
from collections import OrderedDict
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any
from langchain_protocol import Event, SubscribeParams
from langgraph_sdk.stream.transport import AsyncProtocolTransport, EventStreamHandle
# ---------------------------------------------------------------------------
# Bounded LRU set for event-id dedup
# ---------------------------------------------------------------------------
class _SeenEventIds:
"""LRU set of event ids with bounded memory."""
__slots__ = ("_data", "_maxsize")
def __init__(self, maxsize: int = 10_000) -> None:
self._data: OrderedDict[str, None] = OrderedDict()
self._maxsize = maxsize
def add(self, event_id: str) -> None:
if event_id in self._data:
self._data.move_to_end(event_id)
return
self._data[event_id] = None
if len(self._data) > self._maxsize:
self._data.popitem(last=False)
def __contains__(self, event_id: object) -> bool:
return event_id in self._data
def __iter__(self):
return iter(self._data)
# ---------------------------------------------------------------------------
# Per-subscription record
# ---------------------------------------------------------------------------
_logger = logging.getLogger(__name__)
@dataclass
class _Subscription:
"""Internal record for one active subscription on a `StreamController`."""
id: int
params: SubscribeParams
queue: asyncio.Queue = field(default_factory=asyncio.Queue)
# Why: asyncio.Queue[Event | None] as a subscript in the field annotation
# causes a type error with ty; bare asyncio.Queue is accepted.
# ---------------------------------------------------------------------------
# Rotation close helper
# ---------------------------------------------------------------------------
async def _close_after(handle: EventStreamHandle, *, delay: float = 0.0) -> None:
"""Close a handle, optionally after a brief delay.
Used to detach closing the old stream from the synchronous rotation step
so the new stream can absorb server-side replayed events first.
"""
if delay:
await asyncio.sleep(delay)
await handle.close()
# ---------------------------------------------------------------------------
# StreamController
# ---------------------------------------------------------------------------
class StreamController:
"""Manages subscriptions and fan-out against one shared SSE connection.
Responsibilities:
- subscription registry (register / unregister)
- shared-stream lifecycle (open on first subscribe, rotate on filter widen)
- dedup of replayed events via a bounded LRU `_SeenEventIds`
- fan-out from the shared stream to per-subscription queues
Args:
transport: the `AsyncProtocolTransport` bound to this thread session.
run_start_gate: zero-argument async callable that resolves once the
current `run.start` has committed server-side (no-op when no
run is in flight).
max_queue_size: per-subscription queue bound (default 1024).
seen_event_ids_max: LRU cap for the dedup set (default 10_000).
"""
def __init__(
self,
*,
transport: AsyncProtocolTransport,
run_start_gate: Callable[[], Awaitable[None]] | None = None, # noqa: ARG002
max_queue_size: int = 1024,
seen_event_ids_max: int = 10_000,
max_reconnect_attempts: int = 5,
reconnect_backoff_base: float = 0.1,
reconnect_backoff_cap: float = 2.0,
) -> None:
self._transport = transport
self._max_queue_size = max_queue_size
self._seen_event_ids = _SeenEventIds(maxsize=seen_event_ids_max)
self._next_subscription_id = 1
self._subscriptions: dict[int, _Subscription] = {}
self._shared_stream: EventStreamHandle | None = None
self._shared_stream_filter: dict[str, Any] | None = None
self._fanout_task: asyncio.Task[None] | None = None
self._rotation_close_tasks: set[asyncio.Task[None]] = set()
self._closed = False
self._cursor: int | None = None
self._max_reconnect_attempts = max_reconnect_attempts
self._reconnect_backoff_base = reconnect_backoff_base
self._reconnect_backoff_cap = reconnect_backoff_cap
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def subscribe(
self,
channels: list[str],
*,
namespaces: list[list[str]] | None = None,
depth: int | None = None,
) -> AsyncIterator[Event]:
"""Open a typed subscription against the shared SSE.
Returns an async iterator that yields raw `Event` dicts matching the
given filter. Multiple concurrent subscribes share one HTTP connection
whose union expands or rotates as subscriptions come and go.
"""
params: SubscribeParams = {"channels": list(channels)}
if namespaces is not None:
params["namespaces"] = namespaces
if depth is not None:
params["depth"] = depth
return self._subscription_iter(params)
async def close(self) -> None:
"""Tear down the controller, awaiting any pending rotation closes."""
if self._closed:
return
self._closed = True
if self._fanout_task is not None:
self._fanout_task.cancel()
with contextlib.suppress(Exception, asyncio.CancelledError):
await self._fanout_task
if self._shared_stream is not None:
await self._shared_stream.close()
if self._rotation_close_tasks:
await asyncio.gather(*self._rotation_close_tasks, return_exceptions=True)
# ------------------------------------------------------------------
# Subscription internals
# ------------------------------------------------------------------
def _register_subscription(self, params: SubscribeParams) -> _Subscription:
"""Allocate a subscription id, create a bounded queue, add to registry."""
sub = _Subscription(
id=self._next_subscription_id,
params=params,
queue=asyncio.Queue(maxsize=self._max_queue_size),
)
self._next_subscription_id += 1
self._subscriptions[sub.id] = sub
return sub
def _unregister_subscription(self, subscription_id: int) -> None:
"""Remove a subscription from the registry. No-op if already absent."""
self._subscriptions.pop(subscription_id, None)
# Public aliases used by tests and external callers.
register_subscription = _register_subscription
unregister_subscription = _unregister_subscription
async def _subscription_iter(
self, params: SubscribeParams
) -> AsyncGenerator[Event, None]:
sub = self._register_subscription(params)
try:
if self._closed:
return
await self._reconcile_stream(params)
self._ensure_fanout_running()
while True:
item = await sub.queue.get()
if item is None:
return
yield item
finally:
self._unregister_subscription(sub.id)
# ------------------------------------------------------------------
# Fan-out
# ------------------------------------------------------------------
def _ensure_fanout_running(self) -> None:
if self._fanout_task is None or self._fanout_task.done():
self._fanout_task = asyncio.create_task(self._fanout())
# Public alias.
ensure_fanout_running = _ensure_fanout_running
async def _fanout(self) -> None:
"""Single consumer of the shared SSE; routes events to subscriptions.
Why: rotation in `_reconcile_stream` replaces `_shared_stream` mid-loop.
Re-read `self._shared_stream` on each outer iteration so we always
consume from the current handle. The old handle's iterator exhausts
naturally after `_close_after` closes it.
On a post-ready transport drop (non-cancelled error in `shared.done`),
attempts to reconnect up to `_max_reconnect_attempts` times before
giving up and closing subscriber queues.
"""
from langgraph_sdk.stream.subscription import matches_subscription
while not self._closed:
shared = self._shared_stream
if shared is None:
return
try:
async for event in self._dedup_iter(shared.events):
if self._closed:
break
for sub in list(self._subscriptions.values()):
if matches_subscription(event, sub.params):
sub.queue.put_nowait(event)
except Exception as drop_err:
_logger.debug("transport drop in fanout: %r", drop_err)
if self._shared_stream is shared:
err = await shared.done
if (
err is not None
and not isinstance(err, asyncio.CancelledError)
and not self._closed
):
with contextlib.suppress(Exception):
await self._shared_stream.close()
reconnected = await self._reconnect_shared_stream()
if reconnected:
continue
break
# Rotation: loop again to pick up the new _shared_stream.
# Terminate consumers cleanly on shutdown / stream-end.
for sub in self._subscriptions.values():
sub.queue.put_nowait(None)
async def _reconnect_sleep(self, attempt: int) -> None:
"""Sleep with exponential backoff and jitter for reconnect attempt *attempt*."""
base = self._reconnect_backoff_base
cap = self._reconnect_backoff_cap
delay = min(cap, base * (2**attempt))
jitter = random.uniform(0, delay * 0.25)
await asyncio.sleep(delay + jitter)
async def _reconnect_shared_stream(self) -> bool:
"""Attempt to reopen the shared stream after a transport drop.
Returns True if a new stream was successfully opened, False if all
reconnect attempts were exhausted or the controller was closed.
"""
# We intentionally use the *current* shared_stream_filter (the latest
# computed union of all live subscriptions), not the filter that was
# active when this stream was originally opened. If subscriptions were
# added or removed during the drop window, the reconnect picks up the
# new shape.
base_filter = self._shared_stream_filter
if base_filter is None:
return False
for attempt in range(self._max_reconnect_attempts):
if self._closed:
return False
try:
new_stream = self._transport.open_event_stream(
self._filter_with_since(base_filter)
)
await new_stream.ready
except asyncio.CancelledError:
raise
except Exception:
await self._reconnect_sleep(attempt)
continue
self._shared_stream = new_stream
return True
return False
# ------------------------------------------------------------------
# Stream rotation
# ------------------------------------------------------------------
async def _reconcile_stream(self, candidate_filter: SubscribeParams) -> None:
"""Ensure the shared SSE covers `candidate_filter`. Rotate if not.
Open-new-before-close-old: any events buffered server-side between
the two opens are replayed on the new SSE, and `_seen_event_ids`
dedupes the overlap. Awaits `new_stream.ready` so the HTTP connection
is established before returning.
"""
from langgraph_sdk.stream.subscription import filter_covers
if (
self._shared_stream is not None
and self._shared_stream_filter is not None
and filter_covers(self._shared_stream_filter, dict(candidate_filter))
):
return # Existing stream is sufficient.
new_filter = self._compute_current_union(extra=candidate_filter)
new_stream = self._transport.open_event_stream(
self._filter_with_since(new_filter)
)
old_stream = self._shared_stream
self._shared_stream = new_stream
self._shared_stream_filter = new_filter
await new_stream.ready
if old_stream is not None:
task = asyncio.create_task(_close_after(old_stream))
self._rotation_close_tasks.add(task)
task.add_done_callback(self._rotation_close_tasks.discard)
async def reconcile_stream(self, candidate_filter: SubscribeParams) -> None:
"""Public alias for `_reconcile_stream`."""
return await self._reconcile_stream(candidate_filter)
def _compute_current_union(
self, extra: SubscribeParams | None = None
) -> dict[str, Any]:
from langgraph_sdk.stream.subscription import compute_union_filter
filters: list[dict[str, Any]] = [
dict(sub.params) for sub in self._subscriptions.values()
]
if extra is not None:
filters.append(dict(extra))
return compute_union_filter(filters)
# ------------------------------------------------------------------
# Cursor tracking
# ------------------------------------------------------------------
def observe_applied_through_seq(self, seq: Any) -> None:
"""Advance the reconnect cursor from a command response meta sequence."""
self._observe_seq(seq)
def _observe_event(self, event: Event) -> None:
self._observe_seq(event.get("seq"))
def _observe_seq(self, seq: Any) -> None:
if isinstance(seq, int) and (self._cursor is None or seq > self._cursor):
self._cursor = seq
def _filter_with_since(self, params: dict[str, Any]) -> dict[str, Any]:
out = dict(params)
if self._cursor is not None:
out["since"] = self._cursor
return out
# ------------------------------------------------------------------
# Dedup iterator
# ------------------------------------------------------------------
async def _dedup_iter(self, source: AsyncIterator[Event]) -> AsyncIterator[Event]:
async for event in source:
event_id = event.get("event_id")
if event_id is not None:
if event_id in self._seen_event_ids:
continue
self._seen_event_ids.add(event_id)
self._observe_event(event)
yield event
@@ -0,0 +1,359 @@
"""Per-channel event → items state machines.
Used both by the projection iterators (`_ValuesProjection`,
`_MessagesProjection`, `_ToolCallsProjection`, `_SubgraphsProjection`) on
`AsyncThreadStream` / `SyncThreadStream`, and by `interleave_projections`,
which drives multiple decoders from one shared subscription.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable, Mapping
from typing import Any, Literal, Protocol
#: Channel names the public ``interleave_projections`` API accepts as built-ins.
SUPPORTED_INTERLEAVE_CHANNELS = (
"values",
"messages",
"tool_calls",
"subgraphs",
"updates",
"checkpoints",
"tasks",
)
#: Channel names that ``infer_channel`` recognizes as first-class protocol
#: methods but that ``interleave_projections`` has no decoder for. Routing them
#: to the extension/``custom:`` fallback would subscribe to a channel that never
#: matches and silently yield nothing, so they are rejected up front (fail
#: closed). ``lifecycle`` is control-plane (drives run output/interrupt); ``tools``
#: is the wire alias for the public ``tool_calls`` channel.
RESERVED_INTERLEAVE_CHANNELS = frozenset({"lifecycle", "tools", "input"})
def validate_interleave_channels(channels: list[str]) -> None:
"""Reject reserved protocol channel names before they hit the fallback.
Genuine extension names pass through untouched; only names that
``infer_channel`` treats as built-in methods without an interleave decoder
are rejected, so a typo'd or unsupported protocol channel surfaces an error
instead of an empty stream.
"""
for ch in channels:
if ch in RESERVED_INTERLEAVE_CHANNELS:
hint = ' (use "tool_calls")' if ch == "tools" else ""
raise ValueError(
f"{ch!r} is not a valid interleave_projections channel{hint}. "
f"Supported channels: {', '.join(SUPPORTED_INTERLEAVE_CHANNELS)}, "
"or an extension name."
)
def _event_namespace(params_field: Any) -> list[str]:
if not isinstance(params_field, dict):
return []
namespace = params_field.get("namespace") or []
return list(namespace) if isinstance(namespace, list) else []
def _message_event_id(data: dict[str, Any]) -> str | None:
message_id = data.get("id") or data.get("message_id")
return str(message_id) if message_id is not None else None
def _message_route_key(data: dict[str, Any], fallback: str | None = None) -> str:
"""Return the routing key for a message-channel event in `active`.
Keys on `message_id` when available so concurrent messages that share the
same `run_id` (two AI turns in one agent step) route to independent streams
rather than colliding on a shared `run:<run_id>` slot.
"""
message_id = _message_event_id(data)
if message_id is not None:
return f"message:{message_id}"
if fallback is not None:
return f"message:{fallback}"
return "__single__"
SubgraphStatus = Literal["started", "completed", "failed", "interrupted"]
def _parse_namespace_segment(segment: str) -> tuple[str, str | None]:
name, sep, task_id = segment.partition(":")
return name, task_id if sep else None
def _terminal_from_tasks_result(
data: dict[str, Any],
) -> tuple[SubgraphStatus, str | None]:
if data.get("interrupts"):
return "interrupted", None
error = data.get("error")
if error:
return "failed", str(error)
return "completed", None
def _is_direct_child(namespace: list[str], scope: tuple[str, ...]) -> bool:
return len(namespace) == len(scope) + 1 and tuple(namespace[: len(scope)]) == scope
class Decoder(Protocol):
def feed(self, event: Mapping[str, Any]) -> Iterable[Any]: ...
class DataDecoder:
"""Yields `params.data` from events of a single `method`.
Covers the channels whose projection is just "emit the payload": `values`,
`updates`, `checkpoints`, `tasks` the SDK analog of local's
`Values`/`Updates`/`Checkpoints`/`TasksTransformer`, all of which push
`params["data"]` unchanged. The REST-state seeding for `values` stays at
the projection layer; it is a one-shot pre-stream fetch, not part of the
event state machine.
Args:
method: The protocol `method` this decoder consumes.
namespace: When not `None`, events whose namespace differs are ignored
(scope filter, mirroring the local transformers' `namespace != scope`
check). `None` consumes every namespace the historical `values`
projection behavior, where subscription scoping is handled upstream.
"""
def __init__(self, method: str, namespace: list[str] | None = None):
self._method = method
self._namespace = list(namespace) if namespace is not None else None
def feed(self, event: Mapping[str, Any]) -> Iterable[Any]:
if event.get("method") != self._method:
return
params = event.get("params") or {}
if self._namespace is not None and _event_namespace(params) != self._namespace:
return
data = params.get("data")
if data is not None:
yield data
class MessagesDecoder:
"""Yields one chat-model stream per `message-start` event.
Subsequent events route to the matching stream via `stream.dispatch(data)`.
Mirrors the per-event body of `_MessagesProjection._messages_iter`
(`_async/stream.py:404-458`). The subscription open/close and the
`_root_messages_inbox` drain branch stay at the projection layer.
Args:
namespace: Events whose namespace differs are ignored (scope filter).
stream_factory: Keyword-only `(namespace, node, message_id) -> stream`.
Sync binds `ChatModelStream`; async binds `AsyncChatModelStream`.
"""
def __init__(
self,
namespace: list[str],
stream_factory: Callable[..., Any],
):
self._namespace = list(namespace)
self._stream_factory = stream_factory
self._active: dict[str, Any] = {} # route_key -> stream
def feed(self, event: Mapping[str, Any]) -> Iterable[Any]:
if event.get("method") != "messages":
return
params = event.get("params") or {}
if _event_namespace(params) != self._namespace:
return
data = params.get("data")
if not isinstance(data, dict):
return
if data.get("event") == "message-start":
message_id = _message_event_id(data)
key = _message_route_key(data, fallback=message_id)
metadata = (
data.get("metadata") if isinstance(data.get("metadata"), dict) else {}
)
stream = self._stream_factory(
namespace=list(self._namespace),
node=metadata.get("langgraph_node") if metadata else None,
message_id=message_id,
)
self._active[key] = stream
stream.dispatch(data)
yield stream
else:
key = _message_route_key(data)
stream = self._active.get(key)
if stream is None and key == "__single__" and len(self._active) == 1:
stream = next(iter(self._active.values()))
if stream is None:
return
stream.dispatch(data)
if data.get("event") in ("message-finish", "error"):
for route_key, candidate in list(self._active.items()):
if candidate is stream:
del self._active[route_key]
class ToolCallsDecoder:
"""Yields one tool-call handle per `tool-started` event.
Mirrors the per-event body of `_ToolCallsProjection._tool_calls_iter`
(`_async/stream.py:1168-1217`). The thread register/unregister and the
terminal-error-on-close finally stay at the projection / wrapper layer.
Args:
namespace: Events whose namespace differs are ignored.
handle_factory: Keyword-only `(tool_call_id, name, input, namespace) -> handle`.
"""
def __init__(self, namespace: list[str], handle_factory: Callable[..., Any]):
self._namespace = list(namespace)
self._handle_factory = handle_factory
self._active: dict[str, Any] = {}
def feed(self, event: Mapping[str, Any]) -> Iterable[Any]:
if event.get("method") != "tools":
return
params = event.get("params") or {}
if _event_namespace(params) != self._namespace:
return
data = params.get("data")
if not isinstance(data, dict):
return
tool_call_id = data.get("tool_call_id")
if not isinstance(tool_call_id, str):
return
event_type = data.get("event")
if event_type == "tool-started":
name = data.get("tool_name")
handle = self._handle_factory(
tool_call_id=tool_call_id,
name=name if isinstance(name, str) else "",
input=data.get("input"),
namespace=list(self._namespace),
)
self._active[tool_call_id] = handle
yield handle
elif event_type == "tool-output-delta":
handle = self._active.get(tool_call_id)
delta = data.get("delta")
if handle is not None and isinstance(delta, str):
handle._push_delta(delta)
elif event_type == "tool-finished":
handle = self._active.pop(tool_call_id, None)
if handle is not None:
handle._finish(data.get("output"))
elif event_type == "tool-error":
handle = self._active.pop(tool_call_id, None)
if handle is not None:
message = data.get("message")
handle._fail(
RuntimeError(str(message) if message else "Tool call errored")
)
class SubgraphsDecoder:
"""Discovers child subgraph handles and fans out events to active ones.
Mirrors the per-event body of `_SubgraphsProjection._subgraphs_iter`
(`_async/stream.py:963-1041`) plus `_apply_tasks_result`. Root-inbox
forwarding and terminal-status-on-close stay at the projection / wrapper
layer.
Args:
scope: Tuple-form namespace of this decoder's parent. `()` for root.
handle_factory: Keyword-only `(path, graph_name, trigger_call_id) -> handle`.
"""
def __init__(self, scope: tuple[str, ...], handle_factory: Callable[..., Any]):
self._scope = scope
self._handle_factory = handle_factory
self._active: dict[tuple[str, ...], Any] = {}
self._seen: set[tuple[str, ...]] = set()
def feed(self, event: Mapping[str, Any]) -> Iterable[Any]:
params = event.get("params") or {}
namespace = _event_namespace(params)
data = params.get("data")
if not isinstance(data, dict):
return
method = event.get("method")
# 1. Fanout: first active child whose path prefixes this namespace.
ns_tuple = tuple(namespace)
for child_path, child_handle in self._active.items():
child_len = len(child_path)
if len(ns_tuple) >= child_len and ns_tuple[:child_len] == child_path:
child_handle._push_event(event)
break
# 2 + 3. Discovery / status from tasks; discovery from lifecycle.
if method == "tasks":
if "result" in data:
self._apply_tasks_result(namespace, data)
elif _is_direct_child(namespace, self._scope):
yield from self._discover(namespace)
elif (
method == "lifecycle"
and data.get("event") == "started"
and _is_direct_child(namespace, self._scope)
):
yield from self._discover(namespace)
def _discover(self, namespace: list[str]) -> Iterable[Any]:
path = tuple(namespace)
if path in self._seen:
return
self._seen.add(path)
graph_name, trigger_call_id = _parse_namespace_segment(path[-1])
handle = self._handle_factory(
path=path,
graph_name=graph_name or None,
trigger_call_id=trigger_call_id,
)
self._active[path] = handle
yield handle
def _apply_tasks_result(self, namespace: list[str], data: dict[str, Any]) -> None:
result_id = data.get("id")
if not result_id:
return
parent_path = tuple(namespace)
for child_path, handle in list(self._active.items()):
if child_path[:-1] != parent_path:
continue
if handle.trigger_call_id != result_id:
continue
status, error = _terminal_from_tasks_result(data)
handle._finish(status, error)
del self._active[child_path]
class ExtensionsDecoder:
"""Yields `params.data` from one named custom channel.
Mirrors `_ExtensionProjection._iter` (`_async/stream.py:1278-1299`), with
an added name filter so it can share one subscription in interleave.
Args:
name: The extension name. Only `custom` events whose `data["name"]`
matches are consumed.
"""
def __init__(self, name: str):
if not name:
raise ValueError("extension name must be non-empty.")
self._name = name
def feed(self, event: Mapping[str, Any]) -> Iterable[Any]:
if event.get("method") != "custom":
return
params = event.get("params") or {}
data = params.get("data")
if not isinstance(data, dict):
return
if data.get("name") != self._name:
return
yield data
@@ -0,0 +1,71 @@
"""Unbounded async-iterable append-only log with per-iterator cursors.
Direct port of `libs/sdk/src/client/stream/multi-cursor-buffer.ts`. Each
`async for` loop gets its own cursor starting at position 0, so late
consumers still see all previously buffered items. Lifetime is bounded by
the owning projection / handle; there is no eviction policy.
"""
from __future__ import annotations
import asyncio
from collections.abc import AsyncIterable, AsyncIterator
from typing import Generic, TypeVar
T = TypeVar("T")
class MultiCursorBuffer(AsyncIterable[T], Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
self._wakeups: set[asyncio.Future[None]] = set()
self._closed = False
def push(self, item: T) -> None:
# Post-close pushes are accepted: cursors already terminated miss the item,
# but new cursors started later see the full log including it. Matches JS.
self._items.append(item)
self._wake_all()
def close(self) -> None:
if self._closed:
return
self._closed = True
self._wake_all()
def __len__(self) -> int:
return len(self._items)
def __aiter__(self) -> AsyncIterator[T]:
return _Cursor(self)
def _wake_all(self) -> None:
for fut in self._wakeups:
if not fut.done():
fut.set_result(None)
self._wakeups.clear()
class _Cursor(Generic[T]):
def __init__(self, buffer: MultiCursorBuffer[T]) -> None:
self._buffer = buffer
self._idx = 0
def __aiter__(self) -> _Cursor[T]:
return self
async def __anext__(self) -> T:
while True:
if self._idx < len(self._buffer._items):
item = self._buffer._items[self._idx]
self._idx += 1
return item
if self._buffer._closed:
raise StopAsyncIteration
loop = asyncio.get_running_loop()
fut: asyncio.Future[None] = loop.create_future()
self._buffer._wakeups.add(fut)
try:
await fut
finally:
self._buffer._wakeups.discard(fut)
@@ -0,0 +1,208 @@
"""Subscription matching: channel inference + namespace prefix filtering.
Direct port of `libs/sdk/src/client/stream/subscription.ts` from the JS SDK.
"""
from __future__ import annotations
from typing import Any
from langchain_protocol import Channel, Event, Namespace, SubscribeParams
def normalize_segment(segment: str) -> str:
"""Strip the dynamic suffix after `:` from a namespace segment."""
idx = segment.find(":")
return segment if idx == -1 else segment[:idx]
def is_prefix_match(event_namespace: Namespace, prefix: Namespace) -> bool:
"""Whether `event_namespace` starts with `prefix`.
Segments compare literally first; if the prefix segment contains no `:`,
the candidate is also compared after its dynamic suffix is stripped.
Mirrors `is_prefix_match` in `api/langgraph_api/protocol/namespace.py`.
"""
if len(prefix) > len(event_namespace):
return False
for seg, candidate in zip(prefix, event_namespace, strict=False):
if candidate == seg:
continue
if ":" in seg:
return False
if normalize_segment(candidate) == seg:
continue
return False
return True
def namespace_matches(
event_namespace: Namespace,
prefixes: list[Namespace] | None,
depth: int | None,
) -> bool:
"""Whether `event_namespace` matches any of `prefixes` within `depth`."""
if not prefixes:
return True
for prefix in prefixes:
if not is_prefix_match(event_namespace, prefix):
continue
if depth is None:
return True
if len(event_namespace) - len(prefix) <= depth:
return True
return False
_DIRECT_METHODS = {
"values",
"checkpoints",
"updates",
"messages",
"tools",
"lifecycle",
"tasks",
}
def infer_channel(event: Event) -> Channel | None:
"""Map a protocol event's `method` to its subscription channel.
Returns `None` for unrecognized methods so new server-side channels (e.g.
from extension transformers) don't break existing clients.
"""
method = event.get("method")
if method in _DIRECT_METHODS:
return method # type: ignore[return-value]
if method == "custom":
params = event.get("params") or {}
data = params.get("data") if isinstance(params, dict) else None
name = data.get("name") if isinstance(data, dict) else None
# JS uses != null; truthiness here treats name="" the same as missing.
return f"custom:{name}" if name else "custom"
if method == "input.requested":
return "input"
return None
def matches_subscription(event: Event, definition: SubscribeParams) -> bool:
"""Whether `event` should be delivered for `definition`."""
channel = infer_channel(event)
if channel is None:
return False
channels = definition.get("channels", [])
if channel not in channels and not (
channel.startswith("custom:") and "custom" in channels
):
return False
params = event.get("params") or {}
namespace = params.get("namespace", []) if isinstance(params, dict) else []
return namespace_matches(
namespace,
definition.get("namespaces"),
definition.get("depth"),
)
def compute_union_filter(
subscriptions: list[dict[str, Any]],
) -> dict[str, Any]:
"""Aggregate a set of subscription filters into one covering filter.
Direct port of `client/stream/index.ts:#computeUnionFilter`.
- Channels are unioned.
- Namespaces: if any subscription omits `namespaces` (wildcard), the union
is unscoped (omits the key). Otherwise, deduplicated union.
- Depth: if any subscription omits `depth` (unbounded), the union is
unbounded (omits the key). Otherwise, take the max. `depth=0` is a
valid bounded value never omit when all subscriptions provide it.
Args:
subscriptions: list of `SubscribeParams`-shaped dicts.
Returns:
A `SubscribeParams`-shaped dict covering every input.
"""
if not subscriptions:
return {"channels": []}
channels: set[str] = set()
wildcard_namespaces = False
namespace_map: dict[tuple[str, ...], list[str]] = {}
unbounded_depth = False
max_depth = 0
for sub in subscriptions:
for ch in sub.get("channels", []):
channels.add(ch)
sub_namespaces = sub.get("namespaces")
if sub_namespaces is None:
wildcard_namespaces = True
elif not wildcard_namespaces:
for ns in sub_namespaces:
namespace_map[tuple(ns)] = ns
sub_depth = sub.get("depth")
if sub_depth is None:
unbounded_depth = True
elif not unbounded_depth and sub_depth > max_depth:
max_depth = sub_depth
result: dict[str, Any] = {"channels": sorted(channels)}
if not wildcard_namespaces and namespace_map:
result["namespaces"] = list(namespace_map.values())
if not unbounded_depth:
result["depth"] = max_depth
return result
def filter_covers(coverer: dict[str, Any], target: dict[str, Any]) -> bool:
"""Whether `coverer` is a superset of `target`.
Direct port of `client/stream/index.ts:filterCovers`. Depth coverage
accounts for namespace-prefix offset: a scoped coverer needs enough depth
to absorb the extra levels of any deeper target namespace prefix.
"""
coverer_channels = set(coverer.get("channels", []))
for ch in target.get("channels", []):
if ch not in coverer_channels:
return False
coverer_depth = coverer.get("depth")
target_depth = target.get("depth")
coverer_namespaces = coverer.get("namespaces")
target_namespaces = target.get("namespaces")
# Unscoped coverer covers any namespace; depth is a simple scalar check.
if coverer_namespaces is None:
if coverer_depth is None:
return True
if target_depth is None:
return False
return target_depth <= coverer_depth
# Scoped coverer cannot cover an unscoped target.
if target_namespaces is None:
return False
# Each target namespace must be covered by SOME coverer namespace,
# AND the depth-with-offset must fit.
for tp in target_namespaces:
covered = False
for cp in coverer_namespaces:
if not is_prefix_match(tp, cp):
continue
if coverer_depth is None:
covered = True
break
if target_depth is None:
# target wants unbounded depth — coverer bounded can't cover.
continue
if len(tp) - len(cp) + target_depth <= coverer_depth:
covered = True
break
if not covered:
return False
return True
@@ -0,0 +1,342 @@
"""Synchronous shared-stream fan-out controller for v3 thread streaming."""
from __future__ import annotations
import contextlib
import logging
import random
import threading
import time
from dataclasses import dataclass, field
from queue import Queue as _Queue
from typing import Any
from langchain_protocol import Event, SubscribeParams
from langgraph_sdk.stream.subscription import compute_union_filter, filter_covers
from langgraph_sdk.stream.transport import (
SyncEventStreamHandle,
SyncProtocolTransport,
)
_logger = logging.getLogger(__name__)
_ROOT_TERMINAL_LIFECYCLE_EVENTS = frozenset({"completed", "failed"})
def _is_root_terminal_lifecycle(event: Any) -> bool:
"""Return True for a root-namespace lifecycle event marking run end.
Matches the wire shape ``{method: "lifecycle", params: {namespace: [],
data: {event: "completed" | "failed"}}}``. Subgraph lifecycle events
(non-empty namespace) do not terminate the parent run.
"""
if not isinstance(event, dict):
return False
if event.get("method") != "lifecycle":
return False
params = event.get("params") or {}
if not isinstance(params, dict):
return False
if params.get("namespace") or []:
return False
data = params.get("data") or {}
if not isinstance(data, dict):
return False
return data.get("event") in _ROOT_TERMINAL_LIFECYCLE_EVENTS
@dataclass
class _SyncSubscription:
id: int
params: SubscribeParams
queue: _Queue[Event | None] = field(default_factory=_Queue)
# Why: using `queue.Queue` in the annotation causes ty to resolve `queue`
# as the field being defined (name shadowing), not the stdlib module.
_DEFAULT_RUN_START_TIMEOUT: float = 30.0
class SyncStreamController:
"""Owns the sync shared SSE handle, subscription registry, and fan-out thread."""
def __init__(
self,
transport: SyncProtocolTransport,
*,
run_start_gate: threading.Event | None = None,
run_start_timeout: float = _DEFAULT_RUN_START_TIMEOUT,
max_reconnect_attempts: int = 5,
reconnect_backoff_base: float = 0.1,
reconnect_backoff_cap: float = 10.0,
) -> None:
self._transport = transport
self._next_subscription_id = 1
self._subscriptions: dict[int, _SyncSubscription] = {}
self._seen_event_ids: set[str] = set()
self._shared_stream: SyncEventStreamHandle | None = None
self._shared_stream_filter: dict[str, Any] | None = None
self._fanout_thread: threading.Thread | None = None
self._closed = False
self._lock = threading.RLock()
self._cursor: int | None = None
# When None, no gate is applied and reconcile_stream proceeds immediately.
# SyncThreadStream passes an un-set Event so subscriptions wait until
# run.start completes.
self._run_start_gate = run_start_gate
self._run_start_timeout = run_start_timeout
self._max_reconnect_attempts = max_reconnect_attempts
self._reconnect_backoff_base = reconnect_backoff_base
self._reconnect_backoff_cap = reconnect_backoff_cap
self._drain_threads: set[threading.Thread] = set()
def register_subscription(self, params: SubscribeParams) -> _SyncSubscription:
with self._lock:
sub = _SyncSubscription(id=self._next_subscription_id, params=params)
self._next_subscription_id += 1
self._subscriptions[sub.id] = sub
return sub
def unregister_subscription(self, subscription_id: int) -> None:
with self._lock:
self._subscriptions.pop(subscription_id, None)
def signal_paused(self) -> None:
"""Wake every active subscription iterator on interrupt (run pause).
Pushes the terminal sentinel (`None`) into every subscription queue.
Iterators see `None` and return; the shared SSE keeps running so
re-iteration after `run.respond(...)` registers a fresh subscription
and resumes.
"""
with self._lock:
subs = list(self._subscriptions.values())
for sub in subs:
sub.queue.put(None)
def reconcile_stream(self, candidate_filter: SubscribeParams) -> None:
if self._run_start_gate is not None and not self._run_start_gate.wait(
timeout=self._run_start_timeout
):
raise TimeoutError("Sync run.start gate timeout.")
with self._lock:
if (
self._shared_stream is not None
and self._shared_stream_filter is not None
and filter_covers(self._shared_stream_filter, dict(candidate_filter))
):
return
new_filter = self._compute_current_union(extra=candidate_filter)
old_stream = self._shared_stream
self._shared_stream = self._transport.open_event_stream(
self._filter_with_since(new_filter)
)
self._shared_stream_filter = new_filter
if old_stream is not None:
drain_thread = threading.Thread(
target=self._drain_and_close,
args=(old_stream,),
daemon=True,
name="langgraph-sdk-sync-rotation-drain",
)
self._drain_threads.add(drain_thread)
drain_thread.start()
def ensure_fanout_running(self) -> None:
with self._lock:
if self._fanout_thread is not None and self._fanout_thread.is_alive():
return
self._fanout_thread = threading.Thread(
target=self._fanout,
name="langgraph-sdk-sync-stream-fanout",
daemon=True,
)
self._fanout_thread.start()
def _fanout(self) -> None:
from langgraph_sdk.stream.subscription import matches_subscription
while True:
with self._lock:
if self._closed:
return
shared = self._shared_stream
if shared is None:
return
try:
for event in self._dedup_iter(shared.events):
with self._lock:
if self._closed:
break
subscriptions = list(self._subscriptions.values())
for sub in subscriptions:
if matches_subscription(event, sub.params):
sub.queue.put(event)
# Root-terminal lifecycle: push `None` into all sub
# queues so projection iterators exit when the run
# ends naturally. Terminal is processed in seq order
# on the shared SSE, so in-flight values/tools/
# messages events for this run are already queued
# before None.
if _is_root_terminal_lifecycle(event):
self.signal_paused()
except Exception:
pass # transport drop — attempt reconnect below
with self._lock:
if self._shared_stream is not shared:
continue # rotation happened; pick up new stream
# No rotation — check if this was a transport drop
err = shared.error()
if err is not None and not self._closed:
if self._reconnect_shared_stream():
continue
break
with self._lock:
for sub in self._subscriptions.values():
sub.queue.put(None)
def _reconnect_shared_stream(self) -> bool:
with self._lock:
base_filter = self._shared_stream_filter
if base_filter is None:
return False
for _ in range(self._max_reconnect_attempts):
with self._lock:
if self._closed:
return False
params = self._filter_with_since(base_filter)
try:
new_stream = self._transport.open_event_stream(params)
except Exception:
continue
with self._lock:
self._shared_stream = new_stream
return True
return False
def _compute_current_union(
self, extra: SubscribeParams | None = None
) -> dict[str, Any]:
filters = [dict(sub.params) for sub in self._subscriptions.values()]
if extra is not None:
filters.append(dict(extra))
# Always include lifecycle in the shared SSE filter so `_fanout`
# sees root-terminal events in seq order with the projection
# events. See `_is_root_terminal_lifecycle`.
filters.append({"channels": ["lifecycle"]})
return compute_union_filter(filters)
def observe_applied_through_seq(self, seq: Any) -> None:
"""Advance the reconnect cursor from a command response meta sequence."""
with self._lock:
self._observe_seq(seq)
def _observe_event(self, event: Event) -> None:
with self._lock:
self._observe_seq(event.get("seq"))
def _observe_seq(self, seq: Any) -> None:
if isinstance(seq, int) and (self._cursor is None or seq > self._cursor):
self._cursor = seq
def _filter_with_since(self, params: dict[str, Any]) -> dict[str, Any]:
out = dict(params)
if self._cursor is not None:
out["since"] = self._cursor
return out
def _dedup_iter(self, source: Any) -> Any:
for event in source:
event_id = event.get("event_id")
if event_id is not None:
if event_id in self._seen_event_ids:
continue
self._seen_event_ids.add(event_id)
self._observe_event(event)
yield event
def _drain_and_close(self, handle: SyncEventStreamHandle) -> None:
"""Drain remaining events from an old handle before closing it.
Runs in a background thread spawned by `reconcile_stream` on rotation
so buffered events are not lost when the shared stream is replaced.
Events are dispatched to subscribers regardless of `_closed` so that
already-buffered events reach consumers before the handle is closed.
"""
from langgraph_sdk.stream.subscription import matches_subscription
try:
for event in self._dedup_iter(handle.events):
with self._lock:
subscriptions = list(self._subscriptions.values())
for sub in subscriptions:
if matches_subscription(event, sub.params):
sub.queue.put(event)
except Exception as err:
_logger.debug("rotation drain exception: %r", err)
finally:
with contextlib.suppress(Exception):
handle.close()
with self._lock:
self._drain_threads.discard(threading.current_thread())
def _reconnect_sleep(self, attempt: int) -> None:
"""Sleep with exponential backoff + jitter before a reconnect attempt."""
base = self._reconnect_backoff_base
cap = self._reconnect_backoff_cap
delay = min(cap, base * (2**attempt))
jitter = random.uniform(0, delay * 0.25)
time.sleep(delay + jitter)
def _reconnect_shared_stream(self) -> bool:
"""Attempt to reopen the shared stream after a transport drop.
Returns True if a new stream was successfully opened, False if all
reconnect attempts were exhausted or the controller was closed.
"""
base_filter = self._shared_stream_filter
if base_filter is None:
return False
for attempt in range(self._max_reconnect_attempts):
if self._closed:
return False
if attempt > 0:
self._reconnect_sleep(attempt - 1)
try:
new_handle = self._transport.open_event_stream(
self._filter_with_since(base_filter)
)
old = self._shared_stream
self._shared_stream = new_handle
if old is not None:
with contextlib.suppress(Exception):
old.close()
return True
except Exception as err:
_logger.debug("sync reconnect attempt %d failed: %r", attempt, err)
return False
def close(self) -> None:
with self._lock:
if self._closed:
return
self._closed = True
shared = self._shared_stream
for sub in self._subscriptions.values():
sub.queue.put(None)
if shared is not None:
shared.close()
thread = self._fanout_thread
if thread is not None and thread.is_alive():
with contextlib.suppress(RuntimeError):
thread.join(timeout=1.0)
with self._lock:
drain_threads = set(self._drain_threads)
for drain in drain_threads:
if drain.is_alive():
with contextlib.suppress(RuntimeError):
drain.join(timeout=1.0)
@@ -0,0 +1,27 @@
"""Public exports for the v3 streaming transport layer."""
from langgraph_sdk.stream.transport.base import (
AsyncProtocolTransport,
EventStreamHandle,
SyncEventStreamHandle,
SyncProtocolTransport,
build_event_stream_body,
build_websocket_url,
)
from langgraph_sdk.stream.transport.http import ProtocolSseTransport
from langgraph_sdk.stream.transport.sync_http import SyncProtocolSseTransport
from langgraph_sdk.stream.transport.sync_ws import SyncProtocolWebSocketTransport
from langgraph_sdk.stream.transport.ws import ProtocolWebSocketTransport
__all__ = [
"AsyncProtocolTransport",
"EventStreamHandle",
"ProtocolSseTransport",
"ProtocolWebSocketTransport",
"SyncEventStreamHandle",
"SyncProtocolSseTransport",
"SyncProtocolTransport",
"SyncProtocolWebSocketTransport",
"build_event_stream_body",
"build_websocket_url",
]
@@ -0,0 +1,79 @@
"""Shared transport contracts for v3 thread-centric streaming."""
from __future__ import annotations
import asyncio
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
from dataclasses import dataclass
from typing import Any, Protocol
import httpx
from langchain_protocol import Event
@dataclass
class EventStreamHandle:
"""Handle for one async filtered event stream."""
events: AsyncIterator[Event]
ready: asyncio.Future[None]
done: asyncio.Future[BaseException | None]
close: Callable[[], Awaitable[None]]
@dataclass
class SyncEventStreamHandle:
"""Handle for one sync filtered event stream."""
events: Iterator[Event]
error: Callable[[], BaseException | None]
close: Callable[[], None]
class AsyncProtocolTransport(Protocol):
"""Protocol implemented by async SSE and WebSocket transports."""
thread_id: str
async def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None: ...
def open_event_stream(self, params: dict[str, Any]) -> EventStreamHandle: ...
async def close(self) -> None: ...
class SyncProtocolTransport(Protocol):
"""Protocol implemented by sync SSE and WebSocket transports."""
thread_id: str
def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None: ...
def open_event_stream(self, params: dict[str, Any]) -> SyncEventStreamHandle: ...
def close(self) -> None: ...
def build_event_stream_body(params: dict[str, Any]) -> dict[str, Any]:
body: dict[str, Any] = {"channels": params["channels"]}
if params.get("namespaces") is not None:
body["namespaces"] = params["namespaces"]
if params.get("depth") is not None:
body["depth"] = params["depth"]
since = params.get("since")
if isinstance(since, int):
body["since"] = since
return body
def build_websocket_url(base_url: httpx.URL, path: str) -> str:
"""Convert an HTTP base URL plus API path into a WebSocket URL."""
scheme = "wss" if base_url.scheme == "https" else "ws"
base_path = base_url.path.rstrip("/")
stream_path = path if path.startswith("/") else f"/{path}"
full_path = f"{base_path}{stream_path}" if base_path else stream_path
return str(base_url.copy_with(scheme=scheme, path=full_path, query=None))
def websocket_headers(headers: Mapping[str, str] | None) -> list[tuple[str, str]]:
return list(dict(headers or {}).items())
@@ -0,0 +1,199 @@
"""HTTP/SSE transport for the v3 thread-centric protocol.
Direct port of `libs/sdk/src/client/stream/transport/http.ts`.
`ProtocolSseTransport` is bound to a single `thread_id` at construction. Commands
go to `POST /threads/{thread_id}/commands` (JSON in, JSON out). Each
`open_event_stream(params)` opens an independent filtered SSE connection at
`POST /threads/{thread_id}/stream/events` with the `SubscribeParams` in the
request body.
"""
from __future__ import annotations
import asyncio
import contextlib
from collections.abc import AsyncIterator, Mapping
from typing import Any, cast
import httpx
import orjson
from langchain_protocol import Event
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.sse import BytesLineDecoder, SSEDecoder
from langgraph_sdk.stream.transport.base import (
EventStreamHandle,
build_event_stream_body,
)
_build_event_stream_body = build_event_stream_body
class ProtocolSseTransport:
"""v3 protocol transport bound to a single `thread_id`.
Commands go to `POST /threads/{thread_id}/commands` (JSON in, JSON out).
`open_event_stream` opens filtered SSE streams against
`POST /threads/{thread_id}/stream/events`.
"""
def __init__(
self,
*,
client: httpx.AsyncClient,
thread_id: str,
commands_path: str | None = None,
stream_path: str | None = None,
headers: Mapping[str, str] | None = None,
max_queue_size: int = 1024,
) -> None:
self._client = client
self.thread_id = thread_id
self._commands_url = (
commands_path or f"/threads/{_quote_path_param(thread_id)}/commands"
)
self._stream_url = (
stream_path or f"/threads/{_quote_path_param(thread_id)}/stream/events"
)
self._default_headers: dict[str, str] = dict(headers or {})
self._max_queue_size = max_queue_size
self._closed = False
self._event_streams: set[asyncio.Task[None]] = set()
async def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
"""POST a command. Returns the response JSON, or `None` for 202/204.
Raises:
httpx.HTTPStatusError: server returned >= 400.
RuntimeError: the transport has been closed via `close()`.
RuntimeError: server returned a response missing the protocol envelope.
"""
if self._closed:
raise RuntimeError("Protocol transport is closed.")
# Merge default headers first so content-type always wins.
merged_headers = {**self._default_headers, "content-type": "application/json"}
response = await self._client.post(
self._commands_url,
content=orjson.dumps(command),
headers=merged_headers,
)
response.raise_for_status()
if response.status_code in (202, 204):
return None
if not response.content:
raise RuntimeError("Protocol command did not return a valid response.")
try:
payload = orjson.loads(response.content)
except orjson.JSONDecodeError as err:
raise RuntimeError(
"Protocol command did not return a valid response."
) from err
if not isinstance(payload, dict) or "id" not in payload:
raise RuntimeError("Protocol command did not return a valid response.")
return payload
def open_event_stream(self, params: dict[str, Any]) -> EventStreamHandle:
"""Open an independent filtered SSE event stream.
Posts `params` as a SubscribeParams body to `/threads/{thread_id}/stream/events`.
Returns an `EventStreamHandle` whose `events` async iterator yields typed
`Event` dicts as the server emits them. `handle.ready` resolves on a 2xx
response (rejects on HTTP error or transport failure before headers).
Reconnect: pass `params["since"]` to filter outbound seqs server-side. The
cursor goes in the request body, not as a `Last-Event-ID` header.
"""
if self._closed:
raise RuntimeError("Protocol transport is closed.")
loop = asyncio.get_running_loop()
ready: asyncio.Future[None] = loop.create_future()
done: asyncio.Future[BaseException | None] = loop.create_future()
queue: asyncio.Queue[Event | None] = asyncio.Queue(maxsize=self._max_queue_size)
cancel_event = asyncio.Event()
async def pump() -> None:
try:
# Merge default headers first so fixed SSE headers always win.
sse_headers = {
**self._default_headers,
"content-type": "application/json",
"accept": "text/event-stream",
"cache-control": "no-store",
}
async with self._client.stream(
"POST",
self._stream_url,
content=orjson.dumps(build_event_stream_body(params)),
headers=sse_headers,
) as response:
response.raise_for_status()
if not ready.done():
ready.set_result(None)
line_decoder = BytesLineDecoder()
sse_decoder = SSEDecoder()
async for chunk in response.aiter_bytes():
if cancel_event.is_set():
break
for line in line_decoder.decode(chunk):
part = sse_decoder.decode(bytes(line))
if part is None:
continue
if isinstance(part.data, dict):
await queue.put(cast("Event", part.data))
# Drain any trailing buffered line, then fire any pending event.
if not cancel_event.is_set():
for line in line_decoder.flush():
part = sse_decoder.decode(bytes(line))
if part is not None and isinstance(part.data, dict):
await queue.put(cast("Event", part.data))
part = sse_decoder.decode(b"")
if part is not None and isinstance(part.data, dict):
await queue.put(cast("Event", part.data))
except asyncio.CancelledError as err:
if not done.done():
done.set_result(err)
raise
except BaseException as err:
if not ready.done():
ready.set_exception(err)
if not done.done():
done.set_result(err)
finally:
if not done.done():
done.set_result(None)
await queue.put(None) # sentinel: end of stream
task = asyncio.create_task(pump())
self._event_streams.add(task)
task.add_done_callback(self._event_streams.discard)
async def aiter() -> AsyncIterator[Event]:
while True:
item = await queue.get()
if item is None or cancel_event.is_set():
return
yield item
async def close() -> None:
cancel_event.set()
# Why: pump may be mid-`finally`; ensure consumer unblocks.
queue.put_nowait(None)
task.cancel()
with contextlib.suppress(asyncio.CancelledError, Exception):
await task
return EventStreamHandle(events=aiter(), ready=ready, done=done, close=close)
async def close(self) -> None:
"""Cancel any open event streams and mark the transport closed. Idempotent."""
if self._closed:
return
self._closed = True
tasks = list(self._event_streams)
for task in tasks:
task.cancel()
if tasks:
with contextlib.suppress(Exception, asyncio.CancelledError):
await asyncio.gather(*tasks, return_exceptions=True)
@@ -0,0 +1,136 @@
"""Synchronous HTTP/SSE transport for the v3 thread-centric protocol."""
from __future__ import annotations
import contextlib
from collections.abc import Iterator, Mapping
from typing import Any, cast
import httpx
import orjson
from langchain_protocol import Event
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.sse import BytesLineDecoder, SSEDecoder
from langgraph_sdk.stream.transport.base import (
SyncEventStreamHandle,
build_event_stream_body,
)
class SyncProtocolSseTransport:
"""Sync v3 protocol transport bound to one thread id."""
def __init__(
self,
*,
client: httpx.Client,
thread_id: str,
commands_path: str | None = None,
stream_path: str | None = None,
headers: Mapping[str, str] | None = None,
) -> None:
self._client = client
self.thread_id = thread_id
self._commands_url = (
commands_path or f"/threads/{_quote_path_param(thread_id)}/commands"
)
self._stream_url = (
stream_path or f"/threads/{_quote_path_param(thread_id)}/stream/events"
)
self._default_headers: dict[str, str] = dict(headers or {})
self._closed = False
self._open_responses: list[httpx.Response] = []
def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
if self._closed:
raise RuntimeError("Protocol transport is closed.")
merged_headers = {**self._default_headers, "content-type": "application/json"}
response = self._client.post(
self._commands_url,
content=orjson.dumps(command),
headers=merged_headers,
)
response.raise_for_status()
if response.status_code in (202, 204):
return None
payload = orjson.loads(response.content)
if not isinstance(payload, dict) or "id" not in payload:
raise RuntimeError("Protocol command did not return a valid response.")
return payload
def open_event_stream(self, params: dict[str, Any]) -> SyncEventStreamHandle:
if self._closed:
raise RuntimeError("Protocol transport is closed.")
sse_headers = {
**self._default_headers,
"content-type": "application/json",
"accept": "text/event-stream",
"cache-control": "no-store",
}
request = self._client.build_request(
"POST",
self._stream_url,
content=orjson.dumps(build_event_stream_body(params)),
headers=sse_headers,
)
stream_cm = self._client.send(request, stream=True)
stream_cm.raise_for_status()
content_type = stream_cm.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
stream_cm.close()
raise httpx.TransportError(
"Expected response header Content-Type to contain "
f"'text/event-stream', got {content_type!r}"
)
self._open_responses.append(stream_cm)
closed = False
stream_error: BaseException | None = None
def events() -> Iterator[Event]:
nonlocal stream_error
line_decoder = BytesLineDecoder()
sse_decoder = SSEDecoder()
try:
for chunk in stream_cm.iter_bytes():
if closed:
return
for line in line_decoder.decode(chunk):
part = sse_decoder.decode(bytes(line))
if part is not None and isinstance(part.data, dict):
yield cast("Event", part.data)
for line in line_decoder.flush():
part = sse_decoder.decode(bytes(line))
if part is not None and isinstance(part.data, dict):
yield cast("Event", part.data)
part = sse_decoder.decode(b"")
if part is not None and isinstance(part.data, dict):
yield cast("Event", part.data)
except BaseException as exc:
if not closed:
stream_error = exc
raise
finally:
with contextlib.suppress(ValueError):
self._open_responses.remove(stream_cm)
stream_cm.close()
def error() -> BaseException | None:
return stream_error
def close() -> None:
nonlocal closed
closed = True
with contextlib.suppress(Exception):
stream_cm.close()
return SyncEventStreamHandle(events=events(), error=error, close=close)
def close(self) -> None:
if self._closed:
return
self._closed = True
for response in list(self._open_responses):
with contextlib.suppress(Exception):
response.close()
self._open_responses.clear()
@@ -0,0 +1,153 @@
"""Sync WebSocket transport for the v3 thread-centric protocol."""
from __future__ import annotations
import contextlib
from collections.abc import Callable, Iterator, Mapping
from typing import Any, cast
import httpx
import orjson
from langchain_protocol import Event
from websockets.sync.client import connect as websocket_connect
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.stream.transport.base import (
SyncEventStreamHandle,
build_event_stream_body,
build_websocket_url,
websocket_headers,
)
class SyncProtocolWebSocketTransport:
"""Sync v3 protocol transport using HTTP commands and WebSocket events."""
def __init__(
self,
*,
client: httpx.Client,
thread_id: str,
commands_path: str | None = None,
stream_path: str | None = None,
headers: Mapping[str, str] | None = None,
connect: Callable[..., Any] = websocket_connect,
ping_interval: float | None = 20.0,
ping_timeout: float | None = 20.0,
) -> None:
self._client = client
self.thread_id = thread_id
self._commands_url = (
commands_path or f"/threads/{_quote_path_param(thread_id)}/commands"
)
self._stream_path = (
stream_path or f"/threads/{_quote_path_param(thread_id)}/stream/events"
)
self._default_headers: dict[str, str] = dict(headers or {})
self._connect = connect
self._ping_interval = ping_interval
self._ping_timeout = ping_timeout
self._closed = False
def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
if self._closed:
raise RuntimeError("Protocol transport is closed.")
merged_headers = {**self._default_headers, "content-type": "application/json"}
response = self._client.post(
self._commands_url,
content=orjson.dumps(command),
headers=merged_headers,
)
response.raise_for_status()
if response.status_code in (202, 204):
return None
payload = orjson.loads(response.content)
if not isinstance(payload, dict) or "id" not in payload:
raise RuntimeError("Protocol command did not return a valid response.")
return payload
def open_event_stream(self, params: dict[str, Any]) -> SyncEventStreamHandle:
if self._closed:
raise RuntimeError("Protocol transport is closed.")
closed = False
stream_error: BaseException | None = None
url = build_websocket_url(self._client.base_url, self._stream_path)
handshake_headers = list(websocket_headers(self._default_headers))
cookie_header = _cookie_header(self._client, self._stream_path)
if cookie_header:
handshake_headers.append(("Cookie", cookie_header))
# Pre-enter the WebSocket context manager so close() can reach the socket
# immediately, even before the caller has started iterating events().
ws_cm = self._connect(
url,
additional_headers=handshake_headers,
ping_interval=self._ping_interval,
ping_timeout=self._ping_timeout,
)
websocket = ws_cm.__enter__()
def events() -> Iterator[Event]:
nonlocal stream_error
try:
# Wrap the initial subscribe in a ``subscription.subscribe``
# Protocol command envelope so the server's WS endpoint
# (see ``langgraph-api`` ``api/event_streaming.py``
# ``_thread_websocket``) accepts it. Bare subscribe bodies
# are rejected with ``invalid_argument``.
subscribe_command = {
"id": 1,
"method": "subscription.subscribe",
"params": build_event_stream_body(params),
}
websocket.send(orjson.dumps(subscribe_command).decode())
for raw in websocket:
if closed:
return
payload = _decode_frame(raw)
if isinstance(payload, dict):
yield cast("Event", payload)
except BaseException as exc:
if not closed:
stream_error = exc
raise
finally:
with contextlib.suppress(Exception):
ws_cm.__exit__(None, None, None)
def error() -> BaseException | None:
return stream_error
def close() -> None:
nonlocal closed
closed = True
with contextlib.suppress(Exception):
websocket.close()
return SyncEventStreamHandle(events=events(), error=error, close=close)
def close(self) -> None:
self._closed = True
def _decode_frame(raw: str | bytes | bytearray | memoryview) -> Any:
if isinstance(raw, str):
return orjson.loads(raw.encode())
return orjson.loads(bytes(raw))
def _cookie_header(client: httpx.Client, path: str) -> str | None:
"""Build a `Cookie` header for the WebSocket handshake.
Why pass `path`: `dict(client.cookies)` flattens the entire jar without
domain/path filtering, so cookies set by responses from other origins would
leak to the WS server. We delegate to `httpx.Cookies.set_cookie_header`,
which applies the same `CookieJar` rules httpx uses for regular HTTP
requests, scoping the result to `client.base_url` + `path`.
"""
if not list(client.cookies.jar):
return None
target = client.base_url.copy_with(path=path)
request = httpx.Request("GET", target)
client.cookies.set_cookie_header(request)
return request.headers.get("Cookie")
@@ -0,0 +1,223 @@
"""Async WebSocket transport for the v3 thread-centric protocol."""
from __future__ import annotations
import asyncio
import contextlib
from collections.abc import AsyncIterator, Callable, Mapping
from typing import Any, cast
import httpx
import orjson
from langchain_protocol import Event
from websockets.asyncio.client import connect as websocket_connect
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.stream.transport.base import (
EventStreamHandle,
build_event_stream_body,
build_websocket_url,
websocket_headers,
)
class ProtocolWebSocketTransport:
"""v3 protocol transport using HTTP commands and WebSocket events."""
def __init__(
self,
*,
client: httpx.AsyncClient,
thread_id: str,
commands_path: str | None = None,
stream_path: str | None = None,
headers: Mapping[str, str] | None = None,
connect: Callable[..., Any] = websocket_connect,
max_queue_size: int = 1024,
ping_interval: float | None = 20.0,
ping_timeout: float | None = 20.0,
) -> None:
self._client = client
self.thread_id = thread_id
self._commands_url = (
commands_path or f"/threads/{_quote_path_param(thread_id)}/commands"
)
self._stream_path = (
stream_path or f"/threads/{_quote_path_param(thread_id)}/stream/events"
)
self._default_headers: dict[str, str] = dict(headers or {})
self._connect = connect
self._max_queue_size = max_queue_size
self._ping_interval = ping_interval
self._ping_timeout = ping_timeout
self._closed = False
self._event_streams: set[asyncio.Task[None]] = set()
async def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
if self._closed:
raise RuntimeError("Protocol transport is closed.")
merged_headers = {**self._default_headers, "content-type": "application/json"}
response = await self._client.post(
self._commands_url,
content=orjson.dumps(command),
headers=merged_headers,
)
response.raise_for_status()
if response.status_code in (202, 204):
return None
payload = orjson.loads(response.content)
if not isinstance(payload, dict) or "id" not in payload:
raise RuntimeError("Protocol command did not return a valid response.")
return payload
def open_event_stream(self, params: dict[str, Any]) -> EventStreamHandle:
if self._closed:
raise RuntimeError("Protocol transport is closed.")
loop = asyncio.get_running_loop()
ready: asyncio.Future[None] = loop.create_future()
done: asyncio.Future[BaseException | None] = loop.create_future()
queue: asyncio.Queue[Event | None] = asyncio.Queue(maxsize=self._max_queue_size)
cancel_event = asyncio.Event()
ws_holder: dict[str, Any] = {"ws": None}
async def pump() -> None:
try:
url = build_websocket_url(self._client.base_url, self._stream_path)
handshake_headers = list(websocket_headers(self._default_headers))
cookie_header = _cookie_header(self._client, self._stream_path)
if cookie_header:
handshake_headers.append(("Cookie", cookie_header))
async with self._connect(
url,
additional_headers=handshake_headers,
ping_interval=self._ping_interval,
ping_timeout=self._ping_timeout,
) as websocket:
ws_holder["ws"] = websocket
try:
# The server's WS endpoint (``ApiWebSocketRoute`` in
# ``langgraph-api`` ``api/event_streaming.py``) treats
# every inbound frame as a Protocol command and
# rejects bare subscribe bodies with
# ``invalid_argument``. Wrap the initial subscribe
# in a ``subscription.subscribe`` command envelope.
# The id is constant (one auto-subscribe per WS
# connection); the resulting success response is
# delivered to the event queue and ignored by the
# SDK fanout (no ``method`` field).
subscribe_command = {
"id": 1,
"method": "subscription.subscribe",
"params": build_event_stream_body(params),
}
await websocket.send(orjson.dumps(subscribe_command).decode())
if not ready.done():
ready.set_result(None)
async for raw in websocket:
if cancel_event.is_set():
break
payload = _decode_frame(raw, done)
if payload is not None:
await queue.put(cast("Event", payload))
finally:
ws_holder["ws"] = None
except asyncio.CancelledError as err:
if not done.done():
done.set_result(err)
raise
except ConnectionClosedOK:
# Server sent close code 1000 — clean end, not an error.
if not done.done():
done.set_result(None)
except ConnectionClosedError as err:
# Abnormal close (1006) or application error (4xxx).
if not ready.done():
ready.set_exception(err)
if not done.done():
done.set_result(err)
except Exception as err:
if not ready.done():
ready.set_exception(err)
if not done.done():
done.set_result(err)
finally:
if not done.done():
done.set_result(None)
await queue.put(None)
task = asyncio.create_task(pump())
self._event_streams.add(task)
task.add_done_callback(self._event_streams.discard)
async def aiter() -> AsyncIterator[Event]:
while True:
item = await queue.get()
if item is None or cancel_event.is_set():
return
yield item
async def close() -> None:
cancel_event.set()
ws = ws_holder.get("ws")
if ws is not None:
with contextlib.suppress(Exception):
await ws.close(code=1000, reason="client close")
queue.put_nowait(None)
task.cancel()
with contextlib.suppress(asyncio.CancelledError, Exception):
await task
return EventStreamHandle(events=aiter(), ready=ready, done=done, close=close)
async def close(self) -> None:
if self._closed:
return
self._closed = True
tasks = list(self._event_streams)
for task in tasks:
task.cancel()
if tasks:
with contextlib.suppress(Exception, asyncio.CancelledError):
await asyncio.gather(*tasks, return_exceptions=True)
def _decode_frame(
raw: str | bytes | bytearray | memoryview,
done: asyncio.Future[BaseException | None],
) -> dict[str, Any] | None:
"""Decode a raw WS frame into an Event dict.
Returns None and sets `done` if the frame is invalid JSON or not a JSON object.
"""
try:
payload = orjson.loads(raw.encode() if isinstance(raw, str) else bytes(raw))
except orjson.JSONDecodeError as err:
if not done.done():
done.set_result(RuntimeError(f"WS frame is not valid JSON: {err!r}"))
return None
if not isinstance(payload, dict):
if not done.done():
done.set_result(
RuntimeError(f"WS frame is not a JSON object: {type(payload).__name__}")
)
return None
return payload
def _cookie_header(client: httpx.AsyncClient, path: str) -> str | None:
"""Build a `Cookie` header for the WebSocket handshake.
Why pass `path`: `dict(client.cookies)` flattens the entire jar without
domain/path filtering, so cookies set by responses from other origins would
leak to the WS server. We delegate to `httpx.Cookies.set_cookie_header`,
which applies the same `CookieJar` rules httpx uses for regular HTTP
requests, scoping the result to `client.base_url` + `path`.
"""
if not list(client.cookies.jar):
return None
target = client.base_url.copy_with(path=path)
request = httpx.Request("GET", target)
client.cookies.set_cookie_header(request)
return request.headers.get("Cookie")

Some files were not shown because too many files have changed in this diff Show More