Compare commits

..
Author SHA1 Message Date
Hugo DURAND dc1741ce96 release(cli): 0.4.39 (#9442)
Bumps the CLI to 0.4.39 to cut a release including #9395, the only CLI
change merged since 0.4.38:

- `langgraph deploy list --json` prints `[{"id": "...", "name":
"..."}]`, so a CI job can check whether a deployment exists before it
deploys.
- `deploy list` and `deploy listeners list` now read every page, instead
of stopping at 20 deployments or 100 listeners.

Merging doesn't publish by itself: after it merges, run the `release`
workflow with `working-directory: libs/cli`.
2026-10-26 10:14:16 -04:00
Igor Soarez 244ca657e4 docs: add "Direction: LangGraph is a runtime" section to AGENTS.md (#9439)
## What

Adds a "Direction" section to `AGENTS.md`, placed before the library
list so it frames how the rest of the file is read. It states the
project's strategic direction for contributors — human and coding-agent
alike:

- LangGraph is consolidating as the low-level **runtime** for agents and
workflows (graph execution, checkpointing, streaming, interrupt/resume),
and is moving away from being a friendly SDK for building agents
directly.
- Agent development belongs on a harness; Deep Agents (separate repo) is
the recommended one.
- Contributions are optimized for runtime correctness, reliability,
performance, and simplicity — not for the shortest agent example.

## Why

LangGraph today is both a runtime and an agent-building SDK. As agent
harnesses have consolidated, keeping both roles blurs the contribution
criteria: "should I add this convenience helper?" and "is this the right
place for this feature?" have no written answer. `AGENTS.md` is the
first thing a coding agent reads before touching this repo; giving it an
explicit direction makes that judgment mechanical and aligns
agent-generated contributions with where the project is going.

## Notes for reviewers

- **The ratchet is stated inline, not linked.** The section embeds the
stabilization rule ("one way to do each thing, and every change leaves
fewer ways than it found") directly, because that rule currently has no
home in this repo. If the full stabilization policy lands in
`CONTRIBUTING.md`, the bullet can shrink to a link and the "deprecation
process" reference can point at it.
- **Scope is explicit.** The consolidation mandate applies to the
runtime core (`langgraph`, `checkpoint*`); the client SDKs and CLI
remain ergonomic, as thin clients.
- **`prebuilt` is marked maintenance-only**, matching its state in code
(`create_react_agent` is already `@deprecated` in favour of
`langchain.agents`). The library list itself is unchanged in this PR;
rewording the `langgraph` and `prebuilt` descriptions could be a
follow-up.
- **Removal discipline is part of the section**: breaking changes must
be a PR's stated purpose and go through deprecation — never a side
effect of an unrelated change. The intent is to prevent agents from
over-reading the direction as license for opportunistic removals.
- **Companion PR:** a matching section adapted for
langchain-ai/langgraphjs follows.

Nothing here changes code or CI; this is a documentation-only change to
contributor guidance.
2026-10-26 15:56:56 +02:00
Hugo DURAND c696d4e79f feat(cli): add --json to deploy list (#9395)
This PR adds a `--json` flag to the `langgraph deploy list` CLI command.
It solves the issue of CI/CD pipelines having to parse complex ASCII
tables by providing a structured JSON output (e.g., `[{"id": "...",
"name": "..."}]`) to check for existing deployments. It also fixes a
limitation where the CLI previously only fetched the first 20
deployments by implementing proper pagination.

## Changes

* **Added `--json` flag:** Outputs a clean, allow-listed JSON array of
deployments.
* **Stripped secrets:** Explicitly excludes environment variables from
the JSON output to prevent leaking secrets in CI logs.
* **Full pagination:** Updates the backend to fetch all limit/offset
pages, ensuring all deployments (not just the first 20) are retrieved.
* **Cleaned standard output:** Moved beta warnings to `stderr` and
disabled interactive prompts when using `--json` to guarantee valid JSON
on `stdout`.
* **Centralized formatting:** Refactored three separate table rendering
functions into a single shared `Table` and `JsonOutput` logic.

## Test

* **Automated checks:** Passed `make format`, `make lint`, and `make
test` (580 tests total).
* **New unit tests:** Added extensive testing for JSON shape, secret
omission, empty lists, missing IDs, stderr routing, and multi-page
retrieval logic.
* **Manual verification:** Tested against a local mock control plane
with 101 deployments, verifying that `jq` can successfully parse the
output and check for specific deployment names.

Release Note: Added `--json` flag to `langgraph deploy list` to support
scriptable, structured deployment lookups in CI/CD environments.
2026-10-26 09:45:57 -04:00
Elior Nataf Lackritz 34c7f58655 fix(sdk-py): retry runs.wait with a bodyless GET and raise run errors in sync wait (#9399)
Two `runs.wait` bugs in the Python SDK.

- When the response of `runs.wait` broke off mid-read, the SDK retried
with a GET to the `Location` header, but that GET reused the POST's
`Content-Length` and `Content-Type`, so the only retry always failed
with `h11.LocalProtocolError`. The sync client also sent a JSON `null`
body on every body-less request made this way (`runs.join`,
`runs.cancel(wait=True)`). Fixes #7858.
- Sync `runs.wait(raise_error=True)` returned a failed run's
`{"__error__": ...}` as a normal result, and sent `raise_error` to the
server, which ignores it. The async client raises. Fixes #8383.

## Fix

The retry GET carries only the caller's headers, and the sync client
sends no body for `json=None`, as the async one already did. Sync
`runs.wait` checks `__error__` like the async client and no longer sends
`raise_error`.

The new tests fail on `main`.

## Release note

Sync `runs.wait` now raises when the run fails, as `raise_error=True`
(the default) always documented. Code that reads `__error__` from the
result should pass `raise_error=False`.

Ideas from #7869 by @pragnyanramtha, #8385 by @Solaris-star and #9377 by
@ShallAbby.
2026-10-26 09:25:00 -04:00
Elior Nataf Lackritz 1b1aee689d fix(langgraph): stream the last checkpoint of a run stopped by interrupt_after or the recursion limit (#9409)
When a run stopped at an `interrupt_after` pause or at the recursion
limit, the checkpoint it stopped on was never sent on the `checkpoints`
or `debug` stream. A checkpoint is streamed at the start of the next
tick, which works out its next tasks; `interrupt_after` raises right
after saving it, and the recursion limit stops the run at the start of
the next tick before that point. A client that resumes from the last
checkpoint it saw ran the last node again. Fixes #9180.

The block that prepares the next tasks and streams the checkpoint moves
into one method, called by `tick` and on both of those exits.

The new tests fail on `main` with every checkpointer in the suite.

## Release note

The `checkpoints` and `debug` streams send one more event when a run
stops at an `interrupt_after` pause or at the recursion limit: the
checkpoint it stopped on.

Same approach as @jayhemnani9910's branch on the issue.
2026-10-26 09:18:02 -04:00
Elior Nataf Lackritz 27aeb75efd fix(langgraph): stop duplicating tags at every subgraph level (#9400)
Tags passed at invoke time are repeated at every subgraph level: a node
one subgraph deep sees each tag twice, three levels deep 8 times. The
copies show up in the node's `config["tags"]`, in `messages` stream
metadata and in traces. Fixes #9084.

## Cause

`ensure_config` appends the tags of explicit configs to the ambient
config's tags, so tags bound with `with_config` survive. A subgraph is
called with its parent's config, which the ambient config also holds, so
the parent's tags are appended a second time at each level.

## Fix

`ensure_config` skips tags the ambient config already has. Tags repeated
across explicit configs are still concatenated, as before.

The new tests fail on `main`.

Same approach as #9110 by @mudscode.
2026-10-26 08:56:49 -04:00
Elior Nataf Lackritz c18a42fcba fix(sdk-py): follow the SSE spec for multi-line data, empty ids and a leading BOM (#9402)
The Python SDK's SSE decoder broke three rules of the SSE spec.

- Repeated `data:` lines were concatenated without a newline, so a value
split across lines could decode to a different one: `[1` then `0]`
became `[10]`. They are now joined with a newline. Fixes #7915.
- An empty `id:` field did not reset the reconnect cursor, so a
reconnect after it resumed from the previous id. It now clears the
cursor, and the reconnect sends no `Last-Event-ID`. Fixes #9009.
- A UTF-8 BOM at the start of a stream became part of the first field
name, so the first event lost its type. It is now stripped. Fixes #9225.

A server that writes each event's data on one line, with no BOM and no
empty ids, sees no change.

The new tests fail on `main`.

## Release note

Multi-line `data:` fields are joined with newlines, so data that only
parsed because its lines were glued together now raises a decode error.
`SSEDecoder.last_event_id` returns `""` after an empty `id:` field,
instead of `None`.

Ideas from #7936 by @NishchayMahor, #7947 by @tayyub-ai, #9014 by
@daleselaji-dev and #9236 by @Kayvan-Zahiri.
2026-10-26 08:56:37 -04:00
Elior Nataf Lackritz b54566b620 fix(sdk-py): accept durability, stream_resumable and context in the runs overloads (#9403)
The `@overload` stubs of `runs.stream`, `runs.create` and `runs.wait`
left out parameters their implementations accept, so type checkers
rejected valid calls, for example `client.runs.wait(thread_id,
assistant_id, durability="sync")` (mypy: "Unexpected keyword argument").
Fixes #8442.

Added `durability` to all 16 stubs, `stream_resumable` to the two sync
stateful `stream` stubs, and `context` to the two async stateless
`stream` stubs. A new test parses both runs clients and checks that
every overload accepts the implementation's keyword parameters, except
the ones a run kind leaves out on purpose (thread-only parameters on
stateless runs, `on_completion` on stateful ones). It fails on `main`,
and mypy accepts the calls above with this change.

Covers the gap #7101 by @KartikPawade fixed for `stream_resumable`.

Co-authored-by: Kartik Pawade
<81703361+KartikPawade@users.noreply.github.com>
Co-authored-by: Anurag Paul
<231525521+anuragpaul602-netizen@users.noreply.github.com>
2026-10-26 08:56:26 -04:00
dependabot[bot] a25bfafe48 chore(deps): bump zod from 4.3.6 to 4.6.7 in /libs/cli/js-examples (#9432)
Bumps [zod](https://github.com/colinhacks/zod) from 4.3.6 to 4.6.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/colinhacks/zod/releases">zod's
releases</a>.</em></p>
<blockquote>
<h2>v4.6.7</h2>
<h2>Commits:</h2>
<ul>
<li>b500a2ee95134f81d74da7911d7378dd2853d042 4.6.7</li>
</ul>
<h2>v4.6.6</h2>
<blockquote>
<p>Six malicious commits pushed to <code>main</code> on September 21
with a compromised git credential were reverted the same day in <a
href="https://redirect.github.com/colinhacks/zod/issues/6620">#6620</a>
and have since been removed from the history of <code>main</code>. None
were ever published to npm. The npm provenance for 4.6.6 names 284243fb,
the pre-rewrite copy of 9c219833 with identical contents.</p>
</blockquote>
<h2>Commits:</h2>
<ul>
<li>0cd3af5e9f50ad11bbcd4407a46a21b8848ba574 docs: load the en locale
directly in Zod Mini</li>
<li>9d5e60f162997b764b40d45aa02481888db50dea docs: import locales from
zod/locales, not z.locales</li>
<li>34a626f307871ecbb0123947559328dc52ed5e3e docs: tighten the z.locales
tree-shaking note</li>
<li>74304ce5b5796b5edb9a88ceb8e170b6ea3e82df release: stage npm versions
instead of publishing them (<a
href="https://redirect.github.com/colinhacks/zod/issues/6622">#6622</a>)</li>
<li>b2ef983e0711ff6517d6020b021071e2e5653efa docs: add Code for Japan to
bronze sponsors</li>
<li>8fb2faf0584828a6a027d8279db36f6f52784462 docs: crop the Code for
Japan logo and list it first in bronze</li>
<li>bacad518c41610d654658172d325874bf0ee6290 docs: square the Code for
Japan logo to match the other bronze tiles</li>
<li>c42596bd1c87e941d7241d10e012c5c196adcc10 release: hold the run on
the release-approval environment until the staged versions are approved
(<a
href="https://redirect.github.com/colinhacks/zod/issues/6638">#6638</a>)</li>
<li>a2ba4a67986a3e6b2b131be30084a2f460d9ea67 fix(types): declare record
output and input as members, not base type arguments (<a
href="https://redirect.github.com/colinhacks/zod/issues/6645">#6645</a>)</li>
<li>a0ebf970f2d752e5f58527cd2942ff48be4f46bd fix(v4): check array, set
and map size bounds before parsing elements (<a
href="https://redirect.github.com/colinhacks/zod/issues/6652">#6652</a>)</li>
<li>529f78dd9431b07f49647fee3aa0819b3f8b66b7 4.6.6</li>
<li>9c219833c3a3764fa1f1ea9c5ecec8254a63c0b1 ci: stage at verbose so a
failed OIDC token exchange says why</li>
</ul>
<h2>v4.6.5</h2>
<h2>Commits:</h2>
<ul>
<li>d2b135cfb7a3582b9eb515756b9166bcb9521f4a docs: add the 4.6.x patch
highlights to the 4.6 post</li>
<li>f1448f7cee00df9fe1e9ad84a000aa1828cc8bc1 docs: fold the 4.6.x patch
highlights into the 4.6 post's own sections</li>
<li>de65a5cb39ed22a507fac935788f718fa88d104f docs: lead the properties
section with the check and add a Zod Mini tab (<a
href="https://redirect.github.com/colinhacks/zod/issues/6598">#6598</a>)</li>
<li>56222cd1532c07bcb91b67df529cab4c0a215330 feat(instanceof): key the
.properties() shape off the instance type (<a
href="https://redirect.github.com/colinhacks/zod/issues/6600">#6600</a>)</li>
<li>ca0229a404818290e6cdcfefcd7eb2d04bcbb543 Revert &quot;feat: add
z.currencyCode() over a vendored ISO 4217 list, refreshed weekly by CI
(<a
href="https://redirect.github.com/colinhacks/zod/issues/6595">#6595</a>)&quot;</li>
<li>cc4cd4ee9c52fcaa10964e48cc144541e41a5ed9 Revert &quot;Revert
&quot;feat: add z.currencyCode() over a vendored ISO 4217 list,
refreshed weekly by CI (<a
href="https://redirect.github.com/colinhacks/zod/issues/6595">#6595</a>)&quot;&quot;</li>
<li>0f3f5ee3ca56c7574bf849e54f79e9a6e02562ee 4.6.5</li>
<li>59bbc03e10c636b9eb3c393dfeb552819774ec21 chore: re-pin the
integration peers to the workspace zod after the 4.6.5 bump</li>
</ul>
<h2>v4.6.4</h2>
<p>A patch on top of <a
href="https://github.com/colinhacks/zod/releases/tag/v4.6.3">4.6.3</a>.</p>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/d6bc1e30"><code>d6bc1e30</code></a>
feat: add <code>z.currencyCode()</code> over a vendored ISO 4217 list,
refreshed weekly by CI (<a
href="https://redirect.github.com/colinhacks/zod/pull/6595">#6595</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/ad32d751"><code>ad32d751</code></a>
perf: <code>z.url()</code> rejects an invalid URL with
<code>URL.canParse()</code> instead of a throwing constructor, about 50x
faster; fewer allocations on the validation path (<a
href="https://redirect.github.com/colinhacks/zod/pull/6588">#6588</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/2bb08717"><code>2bb08717</code></a>
chore: re-pin the integration peers to the workspace zod after the 4.6.4
bump</li>
<li><a
href="https://github.com/colinhacks/zod/commit/f6e1701a"><code>f6e1701a</code></a>
chore(deps): bump next to 15.5.25 and vite to 7.3.6 (<a
href="https://redirect.github.com/colinhacks/zod/pull/6153">#6153</a>)</li>
</ul>
<h2>v4.6.3</h2>
<p>A patch on top of <a
href="https://github.com/colinhacks/zod/releases/tag/v4.6.2">4.6.2</a>.</p>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/413cce9a"><code>413cce9a</code></a>
fix(v4): make z.properties() a check again (<a
href="https://redirect.github.com/colinhacks/zod/pull/6594">#6594</a>) —
removes the standalone <code>z.properties()</code> schema from 4.6.0;
<code>z.instanceof().properties()</code> and
<code>.check(...z.properties())</code> are unchanged</li>
<li><a
href="https://github.com/colinhacks/zod/commit/75d63ee1"><code>75d63ee1</code></a>
docs: show only the <code>.properties()</code> method form in the 4.6
post</li>
<li><a
href="https://github.com/colinhacks/zod/commit/46da9572"><code>46da9572</code></a>
docs: match the error-message examples to what the parsers emit</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/b500a2ee95134f81d74da7911d7378dd2853d042"><code>b500a2e</code></a>
4.6.7</li>
<li><a
href="https://github.com/colinhacks/zod/commit/9c219833c3a3764fa1f1ea9c5ecec8254a63c0b1"><code>9c21983</code></a>
ci: stage at verbose so a failed OIDC token exchange says why</li>
<li><a
href="https://github.com/colinhacks/zod/commit/529f78dd9431b07f49647fee3aa0819b3f8b66b7"><code>529f78d</code></a>
4.6.6</li>
<li><a
href="https://github.com/colinhacks/zod/commit/a0ebf970f2d752e5f58527cd2942ff48be4f46bd"><code>a0ebf97</code></a>
fix(v4): check array, set and map size bounds before parsing elements
(<a
href="https://redirect.github.com/colinhacks/zod/issues/6652">#6652</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/a2ba4a67986a3e6b2b131be30084a2f460d9ea67"><code>a2ba4a6</code></a>
fix(types): declare record output and input as members, not base type
argumen...</li>
<li><a
href="https://github.com/colinhacks/zod/commit/c42596bd1c87e941d7241d10e012c5c196adcc10"><code>c42596b</code></a>
release: hold the run on the release-approval environment until the
staged ve...</li>
<li><a
href="https://github.com/colinhacks/zod/commit/bacad518c41610d654658172d325874bf0ee6290"><code>bacad51</code></a>
docs: square the Code for Japan logo to match the other bronze
tiles</li>
<li><a
href="https://github.com/colinhacks/zod/commit/8fb2faf0584828a6a027d8279db36f6f52784462"><code>8fb2faf</code></a>
docs: crop the Code for Japan logo and list it first in bronze</li>
<li><a
href="https://github.com/colinhacks/zod/commit/b2ef983e0711ff6517d6020b021071e2e5653efa"><code>b2ef983</code></a>
docs: add Code for Japan to bronze sponsors</li>
<li><a
href="https://github.com/colinhacks/zod/commit/74304ce5b5796b5edb9a88ceb8e170b6ea3e82df"><code>74304ce</code></a>
release: stage npm versions instead of publishing them (<a
href="https://redirect.github.com/colinhacks/zod/issues/6622">#6622</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/colinhacks/zod/compare/v4.3.6...v4.6.7">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for zod since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=zod&package-manager=npm_and_yarn&previous-version=4.3.6&new-version=4.6.7)](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-10-25 15:27:13 +00:00
dependabot[bot] 471fee3dff chore(deps): bump langgraph-checkpoint-postgres from 3.1.1 to 3.1.3 in /libs/cli/uv-examples/monorepo in the uv group across 1 directory (#9422)
Bumps the uv group with 1 update in the /libs/cli/uv-examples/monorepo
directory:
[langgraph-checkpoint-postgres](https://github.com/langchain-ai/langgraph).

Updates `langgraph-checkpoint-postgres` from 3.1.1 to 3.1.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/langchain-ai/langgraph/releases">langgraph-checkpoint-postgres's
releases</a>.</em></p>
<blockquote>
<h2>langgraph-checkpoint-postgres==3.1.3</h2>
<p>Changes since checkpointpostgres==3.1.2</p>
<ul>
<li>release(checkpoint-postgres): 3.1.3 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9287">#9287</a>)</li>
<li>fix(checkpoint-postgres,checkpoint-sqlite): treat an empty
checkpoint_id as the latest in delta history (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9262">#9262</a>)</li>
<li>perf(checkpoint-postgres): start the delta walk at the target (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9248">#9248</a>)</li>
<li>fix(checkpoint,checkpoint-postgres,checkpoint-sqlite): validate
namespace labels before store dispatch (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9217">#9217</a>)</li>
<li>fix: order delta channel replay by task path (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8544">#8544</a>)</li>
<li>chore(deps): bump the minor-and-patch group in
/libs/checkpoint-postgres with 6 updates (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9147">#9147</a>)</li>
<li>chore(deps): bump urllib3 from 2.7.0 to 2.8.0 in
/libs/checkpoint-postgres (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9132">#9132</a>)</li>
<li>fix(checkpoint-postgres): derive the delta walk cursor once the
target loads (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8556">#8556</a>)</li>
<li>chore(deps): bump the minor-and-patch group in
/libs/checkpoint-postgres with 3 updates (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8773">#8773</a>)</li>
<li>fix(checkpoint): widen <code>Store</code> <code>put</code> value
type to <code>Mapping[str, Any]</code> (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8617">#8617</a>)</li>
</ul>
<h2>langgraph-checkpoint-postgres==3.1.2</h2>
<p>Changes since checkpointpostgres==3.1.1</p>
<ul>
<li>release(checkpoint-postgres): 3.1.2 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8565">#8565</a>)</li>
<li>release(checkpoint): 4.2.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8563">#8563</a>)</li>
<li>test(checkpoint-postgres,checkpoint-sqlite): run the conformance
suite (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8537">#8537</a>)</li>
<li>fix(checkpoint-postgres): find plain-value seeds when walking delta
history (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8535">#8535</a>)</li>
<li>chore: enable RUF100 and clear unused noqa directives (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8546">#8546</a>)</li>
<li>chore(checkpoint-postgres,checkpoint-sqlite): enable PLC0415 lint
rule (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8540">#8540</a>)</li>
<li>chore(deps): bump the minor-and-patch group in
/libs/checkpoint-postgres with 5 updates (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8497">#8497</a>)</li>
</ul>
<h2>langgraph-checkpoint-sqlite==3.1.2</h2>
<p>Changes since checkpointsqlite==3.1.1</p>
<ul>
<li>release(checkpoint-sqlite): 3.1.2 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9288">#9288</a>)</li>
<li>fix(checkpoint-postgres,checkpoint-sqlite): treat an empty
checkpoint_id as the latest in delta history (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9262">#9262</a>)</li>
<li>fix(checkpoint,checkpoint-postgres,checkpoint-sqlite): validate
namespace labels before store dispatch (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9217">#9217</a>)</li>
<li>fix: order delta channel replay by task path (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8544">#8544</a>)</li>
<li>chore(deps-dev): bump the minor-and-patch group in
/libs/checkpoint-sqlite with 3 updates (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9146">#9146</a>)</li>
<li>chore(deps): bump urllib3 from 2.7.0 to 2.8.0 in
/libs/checkpoint-sqlite (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9135">#9135</a>)</li>
<li>fix(checkpoint-sqlite): walk delta ancestors by parent pointer (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8557">#8557</a>)</li>
<li>chore(deps): bump anyio from 4.12.1 to 4.14.2 in
/libs/checkpoint-sqlite (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8993">#8993</a>)</li>
<li>chore(deps-dev): bump the minor-and-patch group in
/libs/checkpoint-sqlite with 2 updates (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8772">#8772</a>)</li>
<li>fix(checkpoint): widen <code>Store</code> <code>put</code> value
type to <code>Mapping[str, Any]</code> (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8617">#8617</a>)</li>
<li>release(checkpoint): 4.2.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8563">#8563</a>)</li>
<li>test(checkpoint-postgres,checkpoint-sqlite): run the conformance
suite (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8537">#8537</a>)</li>
<li>chore: enable RUF100 and clear unused noqa directives (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8546">#8546</a>)</li>
<li>chore(checkpoint-postgres,checkpoint-sqlite): enable PLC0415 lint
rule (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8540">#8540</a>)</li>
<li>chore(deps): bump the minor-and-patch group in
/libs/checkpoint-sqlite with 4 updates (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/8494">#8494</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/afbe7e48b17f62e139b5c6337ab934549e6dc73b"><code>afbe7e4</code></a>
release(checkpoint-postgres): 3.1.3 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9287">#9287</a>)</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/1074714c260486900d9bec9b5dd48e13883dd5df"><code>1074714</code></a>
release(checkpoint-sqlite): 3.1.2 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9288">#9288</a>)</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/740461ed50f89f867b0c3a1e46fa6d2a4e339cf4"><code>740461e</code></a>
release(sdk-py): 0.4.7 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9289">#9289</a>)</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/08a345637fb2bd499f69c90056a22c478a468817"><code>08a3456</code></a>
release(langgraph): 1.2.15 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9283">#9283</a>)</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/b7d053b256157ac6b8e8ff5a0783f8a446a9c89e"><code>b7d053b</code></a>
fix(langgraph): give every message written to a messages DeltaChannel an
id (...</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/395c145c047aaf962a4f599582bbc8d717bd9fa1"><code>395c145</code></a>
fix(langgraph): save a cached node's writes like a node that ran (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/9259">#9259</a>)</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/10f1fabb3b2f2d550b8133fba1217391c27694c4"><code>10f1fab</code></a>
fix(langgraph): stop at an interrupt_before breakpoint when a drain is
reques...</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/799c2f04fd3a56186e24e9fb8bbf3568e543cfbe"><code>799c2f0</code></a>
fix(langgraph): keep an exit run from a checkpoint the thread moved past
out ...</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/b4991f1ba36a2449ab59c38ab71828ee865bff84"><code>b4991f1</code></a>
fix(langgraph): store an update_state input's writes on the checkpoint
it upd...</li>
<li><a
href="https://github.com/langchain-ai/langgraph/commit/6aa0afba682b0308fbcace31f66fb532978d4dca"><code>6aa0afb</code></a>
fix(checkpoint): default InMemorySaver delta history to the latest
checkpoint...</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langgraph/compare/checkpointsqlite==3.1.1...checkpointpostgres==3.1.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langgraph-checkpoint-postgres&package-manager=uv&previous-version=3.1.1&new-version=3.1.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 <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-10-26 00:09:10 +09:00
dependabot[bot] 7bf29e9798 chore(deps): bump virtualenv from 21.7.13 to 21.14.13 in /libs/cli (#9427)
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.7.13 to
21.14.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/virtualenv/releases">virtualenv's
releases</a>.</em></p>
<blockquote>
<h2>21.14.13</h2>
<h1>Bugfixes - 21.14.13</h1>
<ul>
<li>
<p>Stop bash running commands from <code>--prompt</code> or an
environment name containing <code>$(...)</code>, backticks or a
backslash
after <code>shopt -s promptvars</code>, <code>set -o posix</code>, or in
a child shell that inherits an exported <code>PS1</code>
(GHSA-cx6f-vjhg-g3cc); reported by rozzkml. zsh now draws the prompt
from <code>psvar[9]</code>, which is never parsed
as shell syntax.</p>
<p>Write the <code>.venv</code> redirect and the wheel
<code>RECORD</code> with exclusive create, so a link planted after the
existence
check cannot redirect the write. On POSIX, a read-only
<code>.venv</code> redirect that points at a virtualenv is replaced.
(<a
href="https://redirect.github.com/pypa/virtualenv/issues/3466">#3466</a>)</p>
</li>
</ul>
<p><a href="https://pypi.org/project/virtualenv/21.14.13/">PyPI
release</a></p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/pypa/virtualenv/compare/21.14.12...21.14.13">https://github.com/pypa/virtualenv/compare/21.14.12...21.14.13</a></p>
<h2>21.14.12</h2>
<h1>Bugfixes - 21.14.12</h1>
<ul>
<li>
<p>Upgrade embedded wheels:</p>
<ul>
<li>setuptools to <code>84.2.1</code> from <code>84.0.0</code></li>
<li>pip to <code>26.2.2</code> from <code>26.2.1</code> (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3452">#3452</a>)</li>
</ul>
</li>
</ul>
<h1>Improved Documentation - 21.14.12</h1>
<ul>
<li>Require a pull request news fragment and a matching changelog
section before publishing. (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3445">#3445</a>)</li>
</ul>
<p><a href="https://pypi.org/project/virtualenv/21.14.12/">PyPI
release</a></p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/pypa/virtualenv/compare/21.14.11...21.14.12">https://github.com/pypa/virtualenv/compare/21.14.11...21.14.12</a></p>
<h2>21.14.11</h2>
<h1>Bugfixes - 21.14.11</h1>
<ul>
<li>Search all directories supplied by repeated
<code>--extra-search-dir</code> options instead of keeping only the last
group.
(<a
href="https://redirect.github.com/pypa/virtualenv/issues/3440">#3440</a>)</li>
<li>On Windows, the app data is now migrated into the nested cache
directory instead of the migration failing with a
warning on every run. (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3441">#3441</a>)</li>
</ul>
<p><a href="https://pypi.org/project/virtualenv/21.14.11/">PyPI
release</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst">virtualenv's
changelog</a>.</em></p>
<blockquote>
<h1>Bugfixes - 21.14.13</h1>
<ul>
<li>
<p>Stop bash running commands from <code>--prompt</code> or an
environment name containing <code>$(...)</code>, backticks or a
backslash
after <code>shopt -s promptvars</code>, <code>set -o posix</code>, or in
a child shell that inherits an exported <code>PS1</code>
(GHSA-cx6f-vjhg-g3cc); reported by :user:<code>rozzkml</code>. zsh now
draws the prompt from <code>psvar[9]</code>, which is never parsed
as shell syntax.</p>
<p>Write the <code>.venv</code> redirect and the wheel
<code>RECORD</code> with exclusive create, so a link planted after the
existence
check cannot redirect the write. On POSIX, a read-only
<code>.venv</code> redirect that points at a virtualenv is replaced.
(:issue:<code>3466</code>)</p>
</li>
</ul>
<hr />
<p>v21.14.12 (2026-10-21)</p>
<hr />
<h1>Bugfixes - 21.14.12</h1>
<ul>
<li>
<p>Upgrade embedded wheels:</p>
<ul>
<li>setuptools to <code>84.2.1</code> from <code>84.0.0</code></li>
<li>pip to <code>26.2.2</code> from <code>26.2.1</code>
(:issue:<code>3452</code>)</li>
</ul>
</li>
</ul>
<h1>Improved Documentation - 21.14.12</h1>
<ul>
<li>Require a pull request news fragment and a matching changelog
section before publishing. (:issue:<code>3445</code>)</li>
</ul>
<hr />
<p>v21.14.11 (2026-10-18)</p>
<hr />
<h1>Bugfixes - 21.14.11</h1>
<ul>
<li>Search all directories supplied by repeated
<code>--extra-search-dir</code> options instead of keeping only the last
group.
(:issue:<code>3440</code>)</li>
<li>On Windows, the app data is now migrated into the nested cache
directory instead of the migration failing with a
warning on every run. (:issue:<code>3441</code>)</li>
</ul>
<hr />
<p>v21.14.10 (2026-10-17)</p>
<hr />
<h1>Bugfixes - 21.14.10</h1>
<ul>
<li>Always copy seed packages when app-data is temporary, including when
symlink seeding is enabled through an environment
variable or configuration file. Seeded packages remain available after
temporary cache cleanup. (:issue:<code>3437</code>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pypa/virtualenv/commit/6accbd5ee857f6d7a5a59b1bd3db25cd5f04174a"><code>6accbd5</code></a>
release 21.14.13</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/3598e201f6f044e389136b2347a8a919ac03c5f4"><code>3598e20</code></a>
🐛 fix(bash): escape the prompt for every PS1 state (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3466">#3466</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/f6512a559dc5a4561b8364881aee0f8ea7341e9b"><code>f6512a5</code></a>
release 21.14.12</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/b5357267d81fcb95672fa2fe8f33108de4b04a33"><code>b535726</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3455">#3455</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/7e32366ab551514994a7d455ffb203208ecdc08b"><code>7e32366</code></a>
Fix broken pip and code review links in the docs (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3460">#3460</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/632e56fc92d8f64da72922f3e9da569491bc52c7"><code>632e56f</code></a>
Upgrade embedded pip/setuptools/wheel and CI test tools (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3452">#3452</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/a8bd1a916a5629859307c153bfc1924e56ba969c"><code>a8bd1a9</code></a>
chore(deps): bump certifi from 2026.7.22 to 2026.10.19 in /tasks (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3462">#3462</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/c82323bb7140ed678b2485e00c3785525f4e91af"><code>c82323b</code></a>
chore(deps): bump uv from 0.13.0 to 0.13.4 in /tasks (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3461">#3461</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/863ae6ccfe1878e77b6f00074f3fd91664c8c0aa"><code>863ae6c</code></a>
chore(deps): bump the runtime-dependencies group with 3 updates (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3456">#3456</a>)</li>
<li><a
href="https://github.com/pypa/virtualenv/commit/79537b3461bf150d3eb439347f2858b8ecd504f8"><code>79537b3</code></a>
👷 ci: pin App token action v3.3.0 (<a
href="https://redirect.github.com/pypa/virtualenv/issues/3449">#3449</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pypa/virtualenv/compare/21.7.13...21.14.13">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=uv&previous-version=21.7.13&new-version=21.14.13)](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-10-25 10:03:59 +00:00
dependabot[bot]andJohn Kennedy 80133010ac chore(deps): bump opentelemetry-api from 1.39.1 to 1.45.1 in /libs/langgraph (#9424)
Bumps
[opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python)
from 1.39.1 to 1.45.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-python/releases">opentelemetry-api's
releases</a>.</em></p>
<blockquote>
<h2>Version 1.45.1/0.66b1</h2>
<p>This is a patch release on the previous 1.45.0/0.66b0 release, fixing
the issue(s) below.</p>
<h3>Fixed</h3>
<ul>
<li><code>opentelemetry-api</code>: make <code>AnyValue</code> and
<code>AttributeValue</code> explicit type aliases (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5703">#5703</a>)</li>
<li><code>opentelemetry-sdk</code>: fix thread leak bug in resource
initialization logic for long running resource detectors (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5706">#5706</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: restore
<code>requests</code> as the default HTTP transport so proxy environment
variables such as <code>HTTPS_PROXY</code> are honored again (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5714">#5714</a>)</li>
<li><code>opentelemetry-api</code>: fix quadratic time parsing of the
<code>tracestate</code> header and discard <code>tracestate</code>
headers longer than 8192 characters (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5746">#5746</a>)</li>
</ul>
<h2>Version 1.45.0/0.66b0</h2>
<h3>Added</h3>
<ul>
<li><code>opentelemetry-exporter-prometheus</code>: add support to
configure Resource attributes as metric labels (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5122">#5122</a>)</li>
<li>infra: add renovate (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5202">#5202</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
support for extended attribute values everywhere. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5266">#5266</a>)</li>
<li><code>opentelemetry-sdk</code>: wire the top-level
<code>log_level</code> field in declarative configuration — when set,
maps the OTel <code>SeverityNumber</code> value to a Python logging
level and applies it to the <code>opentelemetry</code> logger so SDK
internal diagnostics respect the configured severity. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5351">#5351</a>)</li>
<li><code>opentelemetry-sdk</code>: add the new stable
<code>AlwaysRecordSampler</code> (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5354">#5354</a>)</li>
<li><code>opentelemetry-configuration</code>,
<code>opentelemetry-sdk</code>: wire top-level
<code>attribute_limits</code> into per-signal providers via declarative
config; add <code>log_record_limits</code> support to
<code>LoggerProvider</code> (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5365">#5365</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-http</code>: add OTLP JSON
HTTP exporter package (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5374">#5374</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
<code>enabled()</code> support to the Logger API, SDK, and
<code>LogRecordProcessor</code> to let instrumentation skip expensive
work when logging is disabled (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5380">#5380</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-file</code>: add OTLP JSON
file Docker tests (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5412">#5412</a>)</li>
<li><code>opentelemetry-configuration</code>: wire the experimental
<code>tracer_configurator/development</code>,
<code>meter_configurator/development</code> and
<code>logger_configurator/development</code> fields into
<code>create_tracer_provider</code>, <code>create_meter_provider</code>
and <code>create_logger_provider</code>, so per-instrumentation-scope
<code>enabled</code> overrides declared in the config file are applied
to the provider (previously these fields were parsed but silently
discarded). The logger
<code>minimum_severity</code>/<code>trace_based</code> fields are not
supported by the Python SDK and are ignored with a warning. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5418">#5418</a>)</li>
<li><code>docs/examples</code>: add example on how to manually setup the
SDK to get SDK metrics (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5449">#5449</a>)</li>
<li><code>opentelemetry-docker-tests</code>: add Prometheus exporter
docker tests (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5457">#5457</a>)</li>
<li><code>opentelemetry-sdk</code>: count records dropped after shutdown
on <code>otel.sdk.processor.{span,log}.processed</code> with
<code>error.type=already_shutdown</code> (batch span/log and simple log
processors), which the semantic conventions define as a valid value for
this metric. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5509">#5509</a>)</li>
<li><code>opentelemetry-semantic-conventions</code>: update semantic
conventions to v1.44.0 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5511">#5511</a>)</li>
<li><code>opentelemetry-sdk</code>: add <code>host.id</code> to the host
resource detector (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5653">#5653</a>)</li>
<li><code>opentelemetry-test-utils</code>: add
<code>CapturingSampler</code> to record what samplers receive in
instrumentation tests (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5681">#5681</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Enable <code>PIE</code> (flake8-pie) ruff rule and fix all
violations (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5150">#5150</a>)</li>
<li>The public <code>opentelemetry.util.types.AttributeValue</code> type
in package <code>opentelemetry-api</code> is being expanded to include
<code>None</code>, heterogeneous sequences of primitive types (and
nested sequences) as opposed to only homogeneous primitive sequences,
and Mappings of strings to any primitive types or sequences/mappings
(which themselves must only contain primitive types or
sequences/mappings validated the same way). If a <code>bytes</code> type
is set as an attribute value in the SDK, it will no longer be utf-8
decoded to a string, instead it will be passed along as is in accordance
with the OTEL spec, since <code>bytes</code> is a valid type in the OTLP
proto. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5266">#5266</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: add a
<code>max_request_size</code> argument to the OTLP HTTP exporters
(traces, logs, metrics); serialized requests larger than the limit are
dropped before sending, measured before compression. Defaults to 64 MiB
(enabled); set to 0 to disable. Mirrors opentelemetry-go#8157. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5369">#5369</a>)</li>
<li>[BREAKING] <code>opentelemetry-api</code>: subclasses of
<code>Logger</code> need to implement the <code>enabled</code> method
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5380">#5380</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: refactor to use
shared opentelemetry-exporter-otlp-common and
opentelemetry-exporter-http-transport packages and switch default HTTP
backend to urllib3 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5389">#5389</a>)</li>
<li><code>opentelemetry-sdk</code>: unify logging force_flush timeout
defaults to 30000ms (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5438">#5438</a>)</li>
<li><code>opentelemetry-python</code>: enable Ruff default ruleset and
fix auto-fixable lint issues (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5491">#5491</a>)</li>
<li><code>opentelemetry-sdk</code>: <code>SimpleSpanProcessor</code> now
drops spans ended after <code>shutdown()</code> instead of passing them
to the exporter, and counts them on
<code>otel.sdk.processor.span.processed</code> with
<code>error.type=already_shutdown</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5512">#5512</a>)</li>
<li>Bump pytest to 9.0.3 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5518">#5518</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: clarify that
the <code>endpoint=</code> kwarg requires the full signal path (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5633">#5633</a>)</li>
<li><code>opentelemetry-sdk</code>: fix typos in SpanLimits docstring
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5658">#5658</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><code>opentelemetry-configuration</code>: perform environment
variable substitution on scalar values after parsing the configuration
file, so <code>${VAR}</code> references inside comments and mapping keys
are no longer substituted and undefined references in comments no longer
abort loading (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5407">#5407</a>)</li>
<li><code>opentelemetry-configuration</code>: declarative config
environment variable substitution now replaces an unset variable that
has no default with an empty value instead of raising an error, per the
configuration spec. Resource attributes whose value resolves to null (an
unset <code>${VAR}</code> with no default) are skipped with a warning
instead of being inserted as a null value. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5408">#5408</a>)</li>
<li>'scripts/build.sh<code>: add
</code>opentelemetry-configuration<code>and</code>opentelemetry-proto-json`
to the package to release (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5425">#5425</a>)</li>
<li><code>opentelemetry-sdk</code>: fix <code>View</code>
instrument-name matching so a view configured with an instrument's real
(mixed-case) name is applied; matching is now case-insensitive and
platform-independent instead of relying on <code>fnmatch</code>'s
OS-dependent case handling (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5430">#5430</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md">opentelemetry-api's
changelog</a>.</em></p>
<blockquote>
<h2>Version 1.45.1/0.66b1 (2026-10-06)</h2>
<h3>Fixed</h3>
<ul>
<li><code>opentelemetry-api</code>: make <code>AnyValue</code> and
<code>AttributeValue</code> explicit type
aliases
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5703">#5703</a>)</li>
<li><code>opentelemetry-sdk</code>: fix thread leak bug in resource
initialization logic for
long running resource detectors
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5706">#5706</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: restore
<code>requests</code> as the default
HTTP transport so proxy environment variables such as
<code>HTTPS_PROXY</code> are
honored again
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5714">#5714</a>)</li>
<li><code>opentelemetry-api</code>: fix quadratic time parsing of the
<code>tracestate</code> header
and discard <code>tracestate</code> headers longer than 8192 characters
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5746">#5746</a>)</li>
</ul>
<h2>Version 1.45.0/0.66b0 (2026-09-25)</h2>
<h3>Added</h3>
<ul>
<li><code>opentelemetry-exporter-prometheus</code>: add support to
configure Resource
attributes as metric labels
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5122">#5122</a>)</li>
<li>infra: add renovate
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5202">#5202</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
support for extended attribute
values everywhere.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5266">#5266</a>)</li>
<li><code>opentelemetry-sdk</code>: wire the top-level
<code>log_level</code> field in declarative
configuration — when set, maps the OTel <code>SeverityNumber</code>
value to a Python
logging level and applies it to the <code>opentelemetry</code> logger so
SDK internal
diagnostics respect the configured severity.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5351">#5351</a>)</li>
<li><code>opentelemetry-sdk</code>: add the new stable
<code>AlwaysRecordSampler</code>
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5354">#5354</a>)</li>
<li><code>opentelemetry-configuration</code>,
<code>opentelemetry-sdk</code>: wire top-level
<code>attribute_limits</code> into per-signal providers via declarative
config; add
<code>log_record_limits</code> support to <code>LoggerProvider</code>
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5365">#5365</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-http</code>: add OTLP JSON
HTTP exporter package
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5374">#5374</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
<code>enabled()</code> support to the
Logger API, SDK, and <code>LogRecordProcessor</code> to let
instrumentation skip
expensive work when logging is disabled
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5380">#5380</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-file</code>: add OTLP JSON
file Docker tests
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5412">#5412</a>)</li>
<li><code>opentelemetry-configuration</code>: wire the experimental</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/14971cef9c84c2354cbaf6871090896fad0f9a1a"><code>14971ce</code></a>
Prepare release 1.45.1/0.66b1 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5752">#5752</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/44ac26edcee5cc99e03a65d6a1a9978775703e28"><code>44ac26e</code></a>
opentelemetry-sdk: fix thread leak bug in resource initialization logic
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/570">#570</a>...</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/a2ec3f91f1c2b735ccba177eaa77d6019a4428ae"><code>a2ec3f9</code></a>
[release/v1.45.x-0.66bx] opentelemetry-exporter-otlp-proto-http: restore
requ...</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/d5224bf4ffdf2aee1b592ad238dcc32ca957f26b"><code>d5224bf</code></a>
opentelemetry-api: improve performance of tracestate parsing (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5746">#5746</a>)
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5749">#5749</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/db61173a56475d4fa1276d16182e46b43db83b9e"><code>db61173</code></a>
Declare AnyValue and AttributeValue as explicit type aliases (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5703">#5703</a>)
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5750">#5750</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/4f0fcfa53e53a8c654a34df0d0a1fd74483ebca2"><code>4f0fcfa</code></a>
Prepare release 1.45.0/0.66b0 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5690">#5690</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/9406f34ca29d78ad760658ccb9f37b7b6c8e58e9"><code>9406f34</code></a>
opentelemetry-test-utils: add CapturingSampler for instrumentation tests
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5681">#5681</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/1fe31a9bd00c88c824060506aed49b0611fe6e9c"><code>1fe31a9</code></a>
fix: update W3CBaggagePropagator to properly handle whitespace (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5680">#5680</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/008b5b0a06c521203b43842c2604acfce9a2b534"><code>008b5b0</code></a>
feat(config): wire top-level attribute_limits into per-signal providers
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5365">#5365</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/edfad0cafec718ff0f635232390e7f37e2460ab1"><code>edfad0c</code></a>
[opentelemetry-sdk] Fix overwriting of the service.instance.id which has
been...</li>
<li>Additional commits viewable in <a
href="https://github.com/open-telemetry/opentelemetry-python/compare/v1.39.1...v1.45.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=opentelemetry-api&package-manager=uv&previous-version=1.39.1&new-version=1.45.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>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
2026-10-25 10:03:11 +00:00
dependabot[bot] 4136a6b763 chore(deps): bump setuptools from 83.0.0 to 84.1.0 in /libs/cli (#9425)
Bumps [setuptools](https://github.com/pypa/setuptools) from 83.0.0 to
84.1.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/setuptools/blob/main/NEWS.rst">setuptools's
changelog</a>.</em></p>
<blockquote>
<h1>v84.1.0</h1>
<h2>Features</h2>
<ul>
<li><code>setuptools.archive_util</code> now raises the new
<code>UnsafeMember</code> exception when
an archive member would be extracted outside of the extraction
directory,
where previously such a member was silently skipped. Aborting makes a
malicious or malformed archive visible to the caller instead of yielding
a
quietly incomplete extraction, and matches the behavior of the standard
library's <code>tarfile</code> extraction filters.</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>
<p><code>Wheel.install_as_egg</code> no longer creates namespace package
directories
outside of the destination egg directory. Entries in a wheel's
<code>namespace_packages.txt</code> were split on <code>.</code> and
passed straight to
<code>os.path.join</code>, so an entry that was absolute,
drive-qualified, or UNC
discarded the destination and placed an <code>__init__.py</code> at a
path chosen by
the wheel. Namespace entries are now validated as dotted module names on
every platform -- see GHSA-xmj3-9gf7-h52w.</p>
<p>This code path is reached only by the deprecated
<code>setup_requires</code>
mechanism when <code>setup()</code> is invoked outside a PEP 517
frontend; the
setuptools build backend never reaches it.</p>
</li>
<li>
<p><code>setuptools.archive_util</code> no longer extracts archive
members outside of the
requested extraction directory. Both the tar and zip formats specify
<code>/</code> as
the only path separator, but the guard against <code>..</code>
components split member
names on <code>/</code> alone, so a name such as
<code>..\escaped.txt</code> was treated as a
single component and then resolved as a traversal by the filesystem on
Windows. Member names containing a backslash, a drive letter, or a UNC
prefix
are now rejected on every platform, as are members whose resolved
destination
falls outside the extraction directory -- see GHSA-grgh-hr87-3jpw.</p>
</li>
</ul>
<h1>v84.0.0</h1>
<h2>Features</h2>
<ul>
<li>Newline-separated <code>keywords</code> and
<code>platforms</code><code>pypa/setuptools#4887</code><code>old
specification &lt;https://peps.python.org/pep-0345/&gt;</code>_
separated items with spaces and the current one uses commas. (<a
href="https://redirect.github.com/pypa/setuptools/issues/4887">#4887</a>)</li>
<li><code>Extension</code><code>pypa/distutils#373</code><a
href="https://redirect.github.com/pypa/setuptools/issues/5022">#5022</a>)</li>
<li>The C compiler modules now emit log messages through their own
<code>compilers.C.*</code> loggers instead of the distutils root logger,
part of decoupling the compilers package from distutils. The logger
names are normalized to a stable <code>compilers.C.*</code> prefix so
they remain constant as the package migrates toward a standalone
<code>compilers.C</code> distribution. (<a
href="https://redirect.github.com/pypa/setuptools/issues/5266">#5266</a>)</li>
<li>The C compilers gained a <code>Compiler.call</code> method -- a thin
wrapper over <code>subprocess.check_call</code> (with macOS
deployment-target env injection) that is the modern replacement for
<code>Compiler.spawn</code>. The compilers no longer depend on
<code>distutils.spawn</code>, <code>distutils.dir_util</code>,
<code>distutils.file_util</code>, <code>distutils._modified</code>, or
<code>distutils.util.execute</code>/<code>split_quoted</code>: the
generic <code>newer</code>/<code>newer_group</code> and
<code>split_quoted</code> helpers are vendored into the
<code>compilers</code> package, and
<code>Compiler.mkpath</code>/<code>move_file</code>/<code>execute</code>
are implemented directly on the standard library
(<code>os.makedirs</code>/<code>shutil.move</code>). The methods are
retained for backward compatibility. (<a
href="https://redirect.github.com/pypa/setuptools/issues/5267">#5267</a>)</li>
<li>The compilers no longer depend on <code>distutils.util</code>,
<code>distutils.version</code>, <code>distutils.compat</code>, or
<code>distutils._macos_compat</code>. The platform-identification
helpers
(<code>get_platform</code>/<code>get_host_platform</code>/<code>is_mingw</code>)
now live in <code>distutils.compilers.platform.detect</code> and the
macOS deployment-target logic and <code>compiler_fixup</code> in
<code>distutils.compilers.platform.macos</code>;
<code>CygwinCCompiler.gcc_version</code> returns a
<code>packaging.version.Version</code>. <code>distutils.util</code>
re-exports the platform/macOS helpers from their new homes for backward
compatibility rather than keeping duplicate copies. (sysconfig lookups
still route through distutils pending its own decoupling.) (<a
href="https://redirect.github.com/pypa/setuptools/issues/5268">#5268</a>)</li>
<li>The compilers now read their build configuration from the standard
library's <code>sysconfig</code> instead of
<code>distutils.sysconfig</code>. Per-compiler customization --
previously <code>distutils.sysconfig.customize_compiler</code> -- has
moved into <code>Compiler.configure_system()</code>: a no-op on the base
class, with <code>UnixCCompiler</code> applying the
compiler/flag/archiver settings CPython recorded in
<code>sysconfig</code> (and the usual
<code>CC</code>/<code>CFLAGS</code>/<code>LDSHARED</code>/… environment
overrides). <code>distutils.sysconfig.customize_compiler</code> is
retained as a thin wrapper that calls
<code>compiler.configure_system()</code>. (<a
href="https://redirect.github.com/pypa/setuptools/issues/5269">#5269</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pypa/setuptools/commit/77b7ace0aaf660beeb9b3f9da4aabf21cbcf3fa9"><code>77b7ace</code></a>
Bump version: 84.0.0 → 84.1.0</li>
<li><a
href="https://github.com/pypa/setuptools/commit/e66004c64eaf6cf2f8c53ccdfac75cf7abce7ccf"><code>e66004c</code></a>
Merge pull request <a
href="https://redirect.github.com/pypa/setuptools/issues/5329">#5329</a>
from pypa/fix/wheel-namespace-path-escape</li>
<li><a
href="https://github.com/pypa/setuptools/commit/54b5dcadc1bd938a5a66919e8ab7bcd59805e7fd"><code>54b5dca</code></a>
Cover the containment check with a symlink escape test</li>
<li><a
href="https://github.com/pypa/setuptools/commit/6d07b8d17890dda58d944ac51e58f54af92d52af"><code>6d07b8d</code></a>
Validate namespace package names during legacy wheel installation</li>
<li><a
href="https://github.com/pypa/setuptools/commit/8419f006d4e34c4ecfc7a0bd1bc6993855adc3cc"><code>8419f00</code></a>
Merge pull request <a
href="https://redirect.github.com/pypa/setuptools/issues/5356">#5356</a>
from pypa/skeleton-ruff-0.16</li>
<li><a
href="https://github.com/pypa/setuptools/commit/6dadc86a3f7dbe1cf49c6301548ffde61d4e74e1"><code>6dadc86</code></a>
Remove DTZ011 suppressions that Ruff 0.17 reports as unused.</li>
<li><a
href="https://github.com/pypa/setuptools/commit/715ec38b46424533806195a96983e3dd70fdf3b9"><code>715ec38</code></a>
Merge <a
href="https://github.com/jaraco/skeleton">https://github.com/jaraco/skeleton</a></li>
<li><a
href="https://github.com/pypa/setuptools/commit/9695460ebfa879ada16545d62b26b9a489a6d5ed"><code>9695460</code></a><code>jaraco/skeleton#219</code></li>
<li><a
href="https://github.com/pypa/setuptools/commit/9e0812976b1154a7aece47188814385c887bc6c9"><code>9e08129</code></a><code>jaraco/skeleton#216</code></li>
<li><a
href="https://github.com/pypa/setuptools/commit/4e612f9e44ecfad130d0b2a27a2dce0740599b4b"><code>4e612f9</code></a>
Ignore PyPy's deprecated check_home warning in tests</li>
<li>Additional commits viewable in <a
href="https://github.com/pypa/setuptools/compare/v83.0.0...v84.1.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=uv&previous-version=83.0.0&new-version=84.1.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-10-25 02:56:36 -07:00
dependabot[bot] 4f5740e53e chore(deps): bump nbconvert from 7.17.1 to 7.17.2 in /libs/langgraph (#9426)
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.17.1 to
7.17.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jupyter/nbconvert/releases">nbconvert's
releases</a>.</em></p>
<blockquote>
<h2>v7.17.2</h2>
<h2>7.17.2</h2>
<p>(<a
href="https://github.com/jupyter/nbconvert/compare/v7.17.1...8685e15e2d924fa39be38aeafb6ac859fc254ccc">Full
Changelog</a>)</p>
<h3>Maintenance and upkeep improvements</h3>
<ul>
<li>Fix CI: fetch the classic CSS from PyPI, update dead links <a
href="https://redirect.github.com/jupyter/nbconvert/pull/2327">#2327</a>
(<a href="https://github.com/Yann-P"><code>@​Yann-P</code></a>, <a
href="https://github.com/Carreau"><code>@​Carreau</code></a>, <a
href="https://github.com/claude"><code>@​claude</code></a>)</li>
</ul>
<h3>Other merged PRs</h3>
<ul>
<li>Use org-level security policy (jupyter/.github/SECURITY.md) <a
href="https://redirect.github.com/jupyter/nbconvert/pull/2318">#2318</a>
(<a href="https://github.com/Yann-P"><code>@​Yann-P</code></a>, <a
href="https://github.com/minrk"><code>@​minrk</code></a>)</li>
</ul>
<h3>Contributors to this release</h3>
<p>The following people contributed discussions, new ideas, code and
documentation contributions, and review.
See <a
href="https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports">our
definition of contributors</a>.</p>
<p>(<a
href="https://github.com/jupyter/nbconvert/graphs/contributors?from=2026-04-08&amp;to=2026-10-12&amp;type=c">GitHub
contributors page for this release</a>)</p>
<p><a href="https://github.com/Carreau"><code>@​Carreau</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3ACarreau+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a href="https://github.com/claude"><code>@​claude</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3Aclaude+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a
href="https://github.com/GruffElixir"><code>@​GruffElixir</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3AGruffElixir+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a href="https://github.com/minrk"><code>@​minrk</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3Aminrk+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a href="https://github.com/Yann-P"><code>@​Yann-P</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3AYann-P+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md">nbconvert's
changelog</a>.</em></p>
<blockquote>
<h2>7.17.2</h2>
<p>(<a
href="https://github.com/jupyter/nbconvert/compare/v7.17.1...8685e15e2d924fa39be38aeafb6ac859fc254ccc">Full
Changelog</a>)</p>
<h3>Maintenance and upkeep improvements</h3>
<ul>
<li>Fix CI: fetch the classic CSS from PyPI, update dead links <a
href="https://redirect.github.com/jupyter/nbconvert/pull/2327">#2327</a>
(<a href="https://github.com/Yann-P"><code>@​Yann-P</code></a>, <a
href="https://github.com/Carreau"><code>@​Carreau</code></a>, <a
href="https://github.com/claude"><code>@​claude</code></a>)</li>
</ul>
<h3>Other merged PRs</h3>
<ul>
<li>Use org-level security policy (jupyter/.github/SECURITY.md) <a
href="https://redirect.github.com/jupyter/nbconvert/pull/2318">#2318</a>
(<a href="https://github.com/Yann-P"><code>@​Yann-P</code></a>, <a
href="https://github.com/minrk"><code>@​minrk</code></a>)</li>
</ul>
<h3>Contributors to this release</h3>
<p>The following people contributed discussions, new ideas, code and
documentation contributions, and review.
See <a
href="https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports">our
definition of contributors</a>.</p>
<p>(<a
href="https://github.com/jupyter/nbconvert/graphs/contributors?from=2026-04-08&amp;to=2026-10-12&amp;type=c">GitHub
contributors page for this release</a>)</p>
<p><a href="https://github.com/Carreau"><code>@​Carreau</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3ACarreau+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a href="https://github.com/claude"><code>@​claude</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3Aclaude+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a
href="https://github.com/GruffElixir"><code>@​GruffElixir</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3AGruffElixir+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a href="https://github.com/minrk"><code>@​minrk</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3Aminrk+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)
| <a href="https://github.com/Yann-P"><code>@​Yann-P</code></a> (<a
href="https://github.com/search?q=repo%3Ajupyter%2Fnbconvert+involves%3AYann-P+updated%3A2026-04-08..2026-10-12&amp;type=Issues">activity</a>)</p>
<!-- raw HTML omitted -->
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/jupyter/nbconvert/commit/e681152f214b65387f875aad96a4726994533a0b"><code>e681152</code></a>
Publish 7.17.2</li>
<li><a
href="https://github.com/jupyter/nbconvert/commit/8685e15e2d924fa39be38aeafb6ac859fc254ccc"><code>8685e15</code></a>
Fix CI: fetch the classic CSS from PyPI, update dead links</li>
<li><a
href="https://github.com/jupyter/nbconvert/commit/ce4bd99aec31d81d759fa8eeb8643179392ec66f"><code>ce4bd99</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/jupyter/nbconvert/commit/1ff189e1b30f0820238e95427c035a9150a5e43f"><code>1ff189e</code></a>
Use org-level security policy (jupyter/.github/SECURITY.md) (<a
href="https://redirect.github.com/jupyter/nbconvert/issues/2318">#2318</a>)</li>
<li>See full diff in <a
href="https://github.com/jupyter/nbconvert/compare/v7.17.1...v7.17.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nbconvert&package-manager=uv&previous-version=7.17.1&new-version=7.17.2)](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-10-25 02:55:35 -07:00
dependabot[bot] 225328f2f9 chore(deps): bump opentelemetry-api from 1.41.0 to 1.45.1 in /libs/cli (#9423)
Bumps
[opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python)
from 1.41.0 to 1.45.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-python/releases">opentelemetry-api's
releases</a>.</em></p>
<blockquote>
<h2>Version 1.45.1/0.66b1</h2>
<p>This is a patch release on the previous 1.45.0/0.66b0 release, fixing
the issue(s) below.</p>
<h3>Fixed</h3>
<ul>
<li><code>opentelemetry-api</code>: make <code>AnyValue</code> and
<code>AttributeValue</code> explicit type aliases (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5703">#5703</a>)</li>
<li><code>opentelemetry-sdk</code>: fix thread leak bug in resource
initialization logic for long running resource detectors (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5706">#5706</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: restore
<code>requests</code> as the default HTTP transport so proxy environment
variables such as <code>HTTPS_PROXY</code> are honored again (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5714">#5714</a>)</li>
<li><code>opentelemetry-api</code>: fix quadratic time parsing of the
<code>tracestate</code> header and discard <code>tracestate</code>
headers longer than 8192 characters (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5746">#5746</a>)</li>
</ul>
<h2>Version 1.45.0/0.66b0</h2>
<h3>Added</h3>
<ul>
<li><code>opentelemetry-exporter-prometheus</code>: add support to
configure Resource attributes as metric labels (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5122">#5122</a>)</li>
<li>infra: add renovate (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5202">#5202</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
support for extended attribute values everywhere. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5266">#5266</a>)</li>
<li><code>opentelemetry-sdk</code>: wire the top-level
<code>log_level</code> field in declarative configuration — when set,
maps the OTel <code>SeverityNumber</code> value to a Python logging
level and applies it to the <code>opentelemetry</code> logger so SDK
internal diagnostics respect the configured severity. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5351">#5351</a>)</li>
<li><code>opentelemetry-sdk</code>: add the new stable
<code>AlwaysRecordSampler</code> (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5354">#5354</a>)</li>
<li><code>opentelemetry-configuration</code>,
<code>opentelemetry-sdk</code>: wire top-level
<code>attribute_limits</code> into per-signal providers via declarative
config; add <code>log_record_limits</code> support to
<code>LoggerProvider</code> (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5365">#5365</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-http</code>: add OTLP JSON
HTTP exporter package (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5374">#5374</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
<code>enabled()</code> support to the Logger API, SDK, and
<code>LogRecordProcessor</code> to let instrumentation skip expensive
work when logging is disabled (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5380">#5380</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-file</code>: add OTLP JSON
file Docker tests (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5412">#5412</a>)</li>
<li><code>opentelemetry-configuration</code>: wire the experimental
<code>tracer_configurator/development</code>,
<code>meter_configurator/development</code> and
<code>logger_configurator/development</code> fields into
<code>create_tracer_provider</code>, <code>create_meter_provider</code>
and <code>create_logger_provider</code>, so per-instrumentation-scope
<code>enabled</code> overrides declared in the config file are applied
to the provider (previously these fields were parsed but silently
discarded). The logger
<code>minimum_severity</code>/<code>trace_based</code> fields are not
supported by the Python SDK and are ignored with a warning. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5418">#5418</a>)</li>
<li><code>docs/examples</code>: add example on how to manually setup the
SDK to get SDK metrics (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5449">#5449</a>)</li>
<li><code>opentelemetry-docker-tests</code>: add Prometheus exporter
docker tests (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5457">#5457</a>)</li>
<li><code>opentelemetry-sdk</code>: count records dropped after shutdown
on <code>otel.sdk.processor.{span,log}.processed</code> with
<code>error.type=already_shutdown</code> (batch span/log and simple log
processors), which the semantic conventions define as a valid value for
this metric. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5509">#5509</a>)</li>
<li><code>opentelemetry-semantic-conventions</code>: update semantic
conventions to v1.44.0 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5511">#5511</a>)</li>
<li><code>opentelemetry-sdk</code>: add <code>host.id</code> to the host
resource detector (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5653">#5653</a>)</li>
<li><code>opentelemetry-test-utils</code>: add
<code>CapturingSampler</code> to record what samplers receive in
instrumentation tests (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5681">#5681</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Enable <code>PIE</code> (flake8-pie) ruff rule and fix all
violations (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5150">#5150</a>)</li>
<li>The public <code>opentelemetry.util.types.AttributeValue</code> type
in package <code>opentelemetry-api</code> is being expanded to include
<code>None</code>, heterogeneous sequences of primitive types (and
nested sequences) as opposed to only homogeneous primitive sequences,
and Mappings of strings to any primitive types or sequences/mappings
(which themselves must only contain primitive types or
sequences/mappings validated the same way). If a <code>bytes</code> type
is set as an attribute value in the SDK, it will no longer be utf-8
decoded to a string, instead it will be passed along as is in accordance
with the OTEL spec, since <code>bytes</code> is a valid type in the OTLP
proto. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5266">#5266</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: add a
<code>max_request_size</code> argument to the OTLP HTTP exporters
(traces, logs, metrics); serialized requests larger than the limit are
dropped before sending, measured before compression. Defaults to 64 MiB
(enabled); set to 0 to disable. Mirrors opentelemetry-go#8157. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5369">#5369</a>)</li>
<li>[BREAKING] <code>opentelemetry-api</code>: subclasses of
<code>Logger</code> need to implement the <code>enabled</code> method
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5380">#5380</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: refactor to use
shared opentelemetry-exporter-otlp-common and
opentelemetry-exporter-http-transport packages and switch default HTTP
backend to urllib3 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5389">#5389</a>)</li>
<li><code>opentelemetry-sdk</code>: unify logging force_flush timeout
defaults to 30000ms (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5438">#5438</a>)</li>
<li><code>opentelemetry-python</code>: enable Ruff default ruleset and
fix auto-fixable lint issues (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5491">#5491</a>)</li>
<li><code>opentelemetry-sdk</code>: <code>SimpleSpanProcessor</code> now
drops spans ended after <code>shutdown()</code> instead of passing them
to the exporter, and counts them on
<code>otel.sdk.processor.span.processed</code> with
<code>error.type=already_shutdown</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5512">#5512</a>)</li>
<li>Bump pytest to 9.0.3 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5518">#5518</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: clarify that
the <code>endpoint=</code> kwarg requires the full signal path (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5633">#5633</a>)</li>
<li><code>opentelemetry-sdk</code>: fix typos in SpanLimits docstring
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5658">#5658</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><code>opentelemetry-configuration</code>: perform environment
variable substitution on scalar values after parsing the configuration
file, so <code>${VAR}</code> references inside comments and mapping keys
are no longer substituted and undefined references in comments no longer
abort loading (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5407">#5407</a>)</li>
<li><code>opentelemetry-configuration</code>: declarative config
environment variable substitution now replaces an unset variable that
has no default with an empty value instead of raising an error, per the
configuration spec. Resource attributes whose value resolves to null (an
unset <code>${VAR}</code> with no default) are skipped with a warning
instead of being inserted as a null value. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5408">#5408</a>)</li>
<li>'scripts/build.sh<code>: add
</code>opentelemetry-configuration<code>and</code>opentelemetry-proto-json`
to the package to release (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5425">#5425</a>)</li>
<li><code>opentelemetry-sdk</code>: fix <code>View</code>
instrument-name matching so a view configured with an instrument's real
(mixed-case) name is applied; matching is now case-insensitive and
platform-independent instead of relying on <code>fnmatch</code>'s
OS-dependent case handling (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5430">#5430</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md">opentelemetry-api's
changelog</a>.</em></p>
<blockquote>
<h2>Version 1.45.1/0.66b1 (2026-10-06)</h2>
<h3>Fixed</h3>
<ul>
<li><code>opentelemetry-api</code>: make <code>AnyValue</code> and
<code>AttributeValue</code> explicit type
aliases
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5703">#5703</a>)</li>
<li><code>opentelemetry-sdk</code>: fix thread leak bug in resource
initialization logic for
long running resource detectors
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5706">#5706</a>)</li>
<li><code>opentelemetry-exporter-otlp-proto-http</code>: restore
<code>requests</code> as the default
HTTP transport so proxy environment variables such as
<code>HTTPS_PROXY</code> are
honored again
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5714">#5714</a>)</li>
<li><code>opentelemetry-api</code>: fix quadratic time parsing of the
<code>tracestate</code> header
and discard <code>tracestate</code> headers longer than 8192 characters
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5746">#5746</a>)</li>
</ul>
<h2>Version 1.45.0/0.66b0 (2026-09-25)</h2>
<h3>Added</h3>
<ul>
<li><code>opentelemetry-exporter-prometheus</code>: add support to
configure Resource
attributes as metric labels
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5122">#5122</a>)</li>
<li>infra: add renovate
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5202">#5202</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
support for extended attribute
values everywhere.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5266">#5266</a>)</li>
<li><code>opentelemetry-sdk</code>: wire the top-level
<code>log_level</code> field in declarative
configuration — when set, maps the OTel <code>SeverityNumber</code>
value to a Python
logging level and applies it to the <code>opentelemetry</code> logger so
SDK internal
diagnostics respect the configured severity.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5351">#5351</a>)</li>
<li><code>opentelemetry-sdk</code>: add the new stable
<code>AlwaysRecordSampler</code>
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5354">#5354</a>)</li>
<li><code>opentelemetry-configuration</code>,
<code>opentelemetry-sdk</code>: wire top-level
<code>attribute_limits</code> into per-signal providers via declarative
config; add
<code>log_record_limits</code> support to <code>LoggerProvider</code>
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5365">#5365</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-http</code>: add OTLP JSON
HTTP exporter package
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5374">#5374</a>)</li>
<li><code>opentelemetry-api</code>, <code>opentelemetry-sdk</code>: add
<code>enabled()</code> support to the
Logger API, SDK, and <code>LogRecordProcessor</code> to let
instrumentation skip
expensive work when logging is disabled
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5380">#5380</a>)</li>
<li><code>opentelemetry-exporter-otlp-json-file</code>: add OTLP JSON
file Docker tests
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/pull/5412">#5412</a>)</li>
<li><code>opentelemetry-configuration</code>: wire the experimental</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/14971cef9c84c2354cbaf6871090896fad0f9a1a"><code>14971ce</code></a>
Prepare release 1.45.1/0.66b1 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5752">#5752</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/44ac26edcee5cc99e03a65d6a1a9978775703e28"><code>44ac26e</code></a>
opentelemetry-sdk: fix thread leak bug in resource initialization logic
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/570">#570</a>...</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/a2ec3f91f1c2b735ccba177eaa77d6019a4428ae"><code>a2ec3f9</code></a>
[release/v1.45.x-0.66bx] opentelemetry-exporter-otlp-proto-http: restore
requ...</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/d5224bf4ffdf2aee1b592ad238dcc32ca957f26b"><code>d5224bf</code></a>
opentelemetry-api: improve performance of tracestate parsing (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5746">#5746</a>)
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5749">#5749</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/db61173a56475d4fa1276d16182e46b43db83b9e"><code>db61173</code></a>
Declare AnyValue and AttributeValue as explicit type aliases (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5703">#5703</a>)
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5750">#5750</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/4f0fcfa53e53a8c654a34df0d0a1fd74483ebca2"><code>4f0fcfa</code></a>
Prepare release 1.45.0/0.66b0 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5690">#5690</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/9406f34ca29d78ad760658ccb9f37b7b6c8e58e9"><code>9406f34</code></a>
opentelemetry-test-utils: add CapturingSampler for instrumentation tests
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5681">#5681</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/1fe31a9bd00c88c824060506aed49b0611fe6e9c"><code>1fe31a9</code></a>
fix: update W3CBaggagePropagator to properly handle whitespace (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5680">#5680</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/008b5b0a06c521203b43842c2604acfce9a2b534"><code>008b5b0</code></a>
feat(config): wire top-level attribute_limits into per-signal providers
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-python/issues/5365">#5365</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-python/commit/edfad0cafec718ff0f635232390e7f37e2460ab1"><code>edfad0c</code></a>
[opentelemetry-sdk] Fix overwriting of the service.instance.id which has
been...</li>
<li>Additional commits viewable in <a
href="https://github.com/open-telemetry/opentelemetry-python/compare/v1.41.0...v1.45.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=opentelemetry-api&package-manager=uv&previous-version=1.41.0&new-version=1.45.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-10-25 02:55:05 -07:00
Elior Nataf Lackritz 4c52e129eb release(langgraph): 1.2.17 (#9418)
Bumps langgraph from 1.2.16 to 1.2.17.

Includes:

- Apply an explicit `recursion_limit` equal to the default (#9417)
- Keep the caller's namespace in `RemoteGraph.stream(subgraphs=True)`
(#9401)
2026-10-24 01:02:32 +00:00
Elior Nataf Lackritz 6c4ceb1332 fix(langgraph): apply an explicit recursion_limit equal to the default (#9417)
Fixes #9416.

`merge_configs` decided whether a `recursion_limit` was set by comparing
it with `DEFAULT_RECURSION_LIMIT`, so an explicit limit equal to the
default was dropped. `LANGGRAPH_DEFAULT_RECURSION_LIMIT` sets that
default, so with it at 1000, `agent.with_config(recursion_limit=1000)`
silently kept the 9999 that `create_agent` and `create_deep_agent` bind,
and a looping agent ran to 9999 steps.

The default is now a sentinel, as #7313 proposed: `ensure_config` fills
in one `int` subclass instance, and `merge_configs` skips that exact
object instead of any equal value. The filled-in default still never
overrides a bound limit (a subgraph bound to 50 keeps 50 when its parent
runs at the default), while an explicit limit equal to the default now
applies wherever configs are merged: `with_config`, `RemoteGraph` calls,
and the limit a parent passes to its subgraphs.

langchain-core has the same check in its own `merge_configs`
(langchain-ai/langchain#39994); this PR does not touch it.

Thanks @awg66 for the report and the repro.
2026-10-24 00:56:19 +00:00
Elior Nataf Lackritz 4897c51365 fix(langgraph): keep the caller's namespace in RemoteGraph.stream(subgraphs=True) (#9401)
`RemoteGraph.stream(..., subgraphs=True)` and `astream` dropped the
caller's namespace prefix from the events they yield. Called inside a
node of a graph running under `outer:123`, a remote subgraph event came
back as `("sub:abc",)` instead of `("outer:123", "sub:abc")`, while the
events RemoteGraph pushes to the parent graph's stream, and an in-memory
subgraph streamed the same way, keep the prefix. Fixes #6604.

The `subgraphs` branch parsed the namespace from the event name a second
time, after the caller prefix was added. This removes that second parse
in `stream` and `astream`.

The new tests fail on `main`.

Same fix as #6605 by @Rakshit-gen.

Co-authored-by: Rakshit Sisodiya
<64471316+rakshit-gen@users.noreply.github.com>
2026-10-23 19:24:04 -04:00
33 changed files with 1366 additions and 428 deletions

No files matched your search

+33
View File
@@ -24,6 +24,39 @@ TEST=path/to/test.py make test
Other pytest arguments can also be supplied inside the `TEST` variable.
## Direction: LangGraph is a runtime
LangGraph is consolidating as the low-level runtime for agents and workflows:
graph execution, checkpointing, streaming, and interrupt/resume. Critical
production systems and agent harnesses run on it, so correctness and stability
come first. It is moving away from being a friendly SDK for building agents
directly; agent development belongs one layer up, on a harness — Deep Agents
(github.com/langchain-ai/deepagents, a separate repo) is the recommended one.
Optimize contributions for runtime correctness, reliability, performance, and
simplicity — not for the shortest agent example.
- One way to do each thing. A change never adds a way; removals are always
deliberate (see below). The number of ways only goes down. No aliases,
overloads, extra accepted types, implicit defaults, or customization hooks
for convenience: ergonomics alone never justify new public surface.
- The mandate applies to everything that implements runtime behavior, in
whichever repo it lives: the graph engine (`langgraph`, `langgraph-core`),
the checkpointers (`checkpoint*`), and the server (`langgraph-api`). The
client SDKs and CLI stay ergonomic but are thin, faithful clients, never a
second implementation of runtime behavior.
- Agent-level packages (`prebuilt`, `langgraph-supervisor`, `langgraph-swarm`,
`langgraph-cua`) are maintenance-only: fix bugs, don't add agent features.
New examples and docs that build agents should use Deep Agents.
- Remove deliberately, not opportunistically. Removing or changing public
behavior must be the stated purpose of the PR and go through the deprecation
process — replacement in place, removal version recorded, migration note.
Never drop a default or overload as a side effect of an unrelated change.
- Behavior changes land in Python and JS together (the implementations live in
separate repos; note in the PR when the other one needs a matching change).
- If a task asks for something this section rules out, say so and propose the
runtime-level or harness-level alternative instead of quietly implementing it.
## Libraries
The repository contains several Python and JavaScript/TypeScript libraries.
+3 -3
View File
@@ -5217,6 +5217,6 @@ yocto-queue@^0.1.0:
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
"zod@^3.25.76 || ^4":
version "4.3.6"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a"
integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==
version "4.6.7"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.6.7.tgz#e806cf64a7f4e37635ce9f2f52c04c3766f75e5b"
integrity sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.38"
__version__ = "0.4.39"
+116 -120
View File
@@ -12,7 +12,7 @@ from collections.abc import Callable, Mapping, Sequence
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Protocol, TypeVar
from typing import Generic, Protocol, TypeVar
import click
import click.exceptions
@@ -26,7 +26,6 @@ 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 CommandRunner, Runner, subp_exec
from langgraph_cli.host_backend import (
MAX_PAGE_SIZE,
ControlPlaneEndpoints,
HostBackendClient,
HostBackendError,
@@ -183,12 +182,15 @@ class Listener:
)
return cls(
identifier,
str(resource.get("compute_id", "")),
str(resource.get("compute_id") or ""),
tuple(str(namespace) for namespace in namespaces)
if isinstance(namespaces, list)
else (),
)
def cells(self) -> tuple[str | None, ...]:
return (self.id, self.compute_id, ", ".join(self.namespaces))
@dataclass(frozen=True, slots=True)
class Unplaced:
@@ -276,8 +278,6 @@ def _describe_listeners(listeners: Sequence[Listener]) -> str:
]
if len(listeners) > len(shown):
lines.append(f" ... and {len(listeners) - len(shown)} more")
if len(listeners) == MAX_PAGE_SIZE:
lines.append(f" (only the first {MAX_PAGE_SIZE} listeners were read)")
return "\n".join(lines)
@@ -499,16 +499,9 @@ def _source_of(resource: object) -> str | None:
def find_deployment_by_name(
client: HostBackendClient, name: str
) -> ExistingDeployment | None:
listed = client.list_deployments(name=name, name_contains=name, limit=MAX_PAGE_SIZE)
for resource in listed:
for resource in client.list_deployments(name=name, name_contains=name):
if resource.get("name") == name and resource.get("id"):
return ExistingDeployment(str(resource["id"]), _source_of(resource))
if len(listed) >= MAX_PAGE_SIZE:
raise click.ClickException(
"This workspace has more deployments than the CLI can search, so it "
f"cannot tell whether '{name}' already exists. Pass --deployment-id to "
"update an existing deployment."
)
return None
@@ -583,103 +576,96 @@ def _validate_prebuilt_image(
_get_emitter().info(f"Image is available for {_DEPLOYMENT_PLATFORM}")
def _extract_deployment_url(deployment: dict[str, object]) -> str:
source_config = deployment.get("source_config")
if isinstance(source_config, dict):
custom_url = source_config.get("custom_url")
if isinstance(custom_url, str) and custom_url:
return custom_url
return "-"
Resource = Mapping[str, object]
RowT = TypeVar("RowT")
def format_deployments_table(deployments: Sequence[dict[str, object]]) -> str:
headers = ("Deployment ID", "Deployment Name", "Deployment URL")
rows = [
(
str(deployment.get("id", "-") or "-"),
str(deployment.get("name", "-") or "-"),
_extract_deployment_url(deployment),
)
for deployment in deployments
]
widths = [
max(len(headers[index]), *(len(row[index]) for row in rows))
for index in range(len(headers))
]
@dataclass(frozen=True, slots=True)
class Table(Generic[RowT]):
headers: tuple[str, ...]
cells: Callable[[RowT], tuple[str | None, ...]]
def format_row(row: Sequence[str]) -> str:
return " ".join(value.ljust(widths[index]) for index, value in enumerate(row))
lines = [format_row(headers), format_row(tuple("-" * width for width in widths))]
lines.extend(format_row(row) for row in rows)
return "\n".join(lines)
def _extract_listener_namespaces(listener: dict[str, object]) -> str:
compute_config = listener.get("compute_config")
namespaces = (
compute_config.get("k8s_namespaces")
if isinstance(compute_config, dict)
else None
)
if isinstance(namespaces, list) and namespaces:
return ", ".join(str(namespace) for namespace in namespaces)
return "-"
def format_listeners_table(listeners: Sequence[dict[str, object]]) -> str:
headers = ("Listener ID", "Compute ID", "Namespaces")
rows = [
(
str(listener.get("id", "-") or "-"),
str(listener.get("compute_id", "-") or "-"),
_extract_listener_namespaces(listener),
)
for listener in listeners
]
widths = [
max(len(headers[index]), *(len(row[index]) for row in rows))
for index in range(len(headers))
]
def format_row(row: Sequence[str]) -> str:
return " ".join(value.ljust(widths[index]) for index, value in enumerate(row))
lines = [format_row(headers), format_row(tuple("-" * width for width in widths))]
lines.extend(format_row(row) for row in rows)
return "\n".join(lines)
def format_revisions_table(revisions: Sequence[dict[str, object]]) -> str:
headers = ("Revision ID", "Status", "Created At")
latest_deployed_seen = False
rows = []
for revision in revisions:
status = str(revision.get("status", "-") or "-")
if status == "DEPLOYED":
if latest_deployed_seen:
status = "REPLACED"
else:
latest_deployed_seen = True
rows.append(
(
str(revision.get("id", "-") or "-"),
status,
str(revision.get("created_at", "-") or "-"),
def render(self, rows: Sequence[RowT]) -> str:
grid = [
self.headers,
*(tuple(cell or "-" for cell in self.cells(row)) for row in rows),
]
widths = [max(map(len, column)) for column in zip(*grid, strict=True)]
rule = tuple("-" * width for width in widths)
return "\n".join(
" ".join(
cell.ljust(width) for cell, width in zip(line, widths, strict=True)
)
for line in (grid[0], rule, *grid[1:])
)
widths = [
max(len(headers[index]), *(len(row[index]) for row in rows))
for index in range(len(headers))
def _text(resource: Resource, key: str) -> str | None:
value = resource.get(key)
return str(value) if value else None
def _deployment_url(deployment: Resource) -> str | None:
source_config = deployment.get("source_config")
custom_url = (
source_config.get("custom_url") if isinstance(source_config, Mapping) else None
)
return custom_url if isinstance(custom_url, str) and custom_url else None
def _with_replaced_status(revisions: Sequence[Resource]) -> list[Resource]:
live_index = next(
(
index
for index, revision in enumerate(revisions)
if revision.get("status") == "DEPLOYED"
),
None,
)
return [
{**revision, "status": "REPLACED"}
if revision.get("status") == "DEPLOYED" and index != live_index
else revision
for index, revision in enumerate(revisions)
]
def format_row(row: Sequence[str]) -> str:
return " ".join(value.ljust(widths[index]) for index, value in enumerate(row))
lines = [format_row(headers), format_row(tuple("-" * width for width in widths))]
lines.extend(format_row(row) for row in rows)
return "\n".join(lines)
@dataclass(frozen=True, slots=True)
class DeploymentRow:
id: str
name: str | None
url: str | None
@classmethod
def from_resource(cls, deployment: Resource) -> "DeploymentRow":
identifier = _text(deployment, "id")
if identifier is None:
raise HostBackendError(
"The control plane returned a deployment without an id."
)
return cls(identifier, _text(deployment, "name"), _deployment_url(deployment))
def cells(self) -> tuple[str | None, ...]:
return (self.id, self.name, self.url)
def as_json(self) -> dict[str, str | None]:
return {"id": self.id, "name": self.name}
DEPLOYMENTS_TABLE = Table(
("Deployment ID", "Deployment Name", "Deployment URL"), DeploymentRow.cells
)
LISTENERS_TABLE = Table(("Listener ID", "Compute ID", "Namespaces"), Listener.cells)
REVISIONS_TABLE = Table(
("Revision ID", "Status", "Created At"),
lambda revision: (
_text(revision, "id"),
_text(revision, "status"),
_text(revision, "created_at"),
),
)
def format_timestamp(ts) -> str:
@@ -841,7 +827,6 @@ def _find_deployment(
lambda c: c.list_deployments(
agent_id=selector.agent_id,
agent_environment=selector.environment,
limit=MAX_PAGE_SIZE,
),
)
if len(existing) > 1:
@@ -1007,20 +992,15 @@ def _print_deployment_result(
dashboard_label: str,
status_url: str | None = None,
) -> None:
"""Print final deployment status and raise on failure."""
em = _get_emitter()
dep_info = client.get_deployment(deployment_id)
custom_url = None
if isinstance(dep_info, dict):
sc = dep_info.get("source_config")
if isinstance(sc, dict):
custom_url = sc.get("custom_url")
deployment = client.get_deployment(deployment_id)
url = _deployment_url(deployment) if isinstance(deployment, Mapping) else None
if last_status == "DEPLOYED":
em.result(
"succeeded",
deployment_id=deployment_id,
url=custom_url,
url=url,
status_url=status_url,
)
elif last_status in ("BUILD_FAILED", "DEPLOY_FAILED", "CREATE_FAILED"):
@@ -2594,6 +2574,13 @@ def _deploy_cmd(
default="",
help="Only show deployments whose names contain this value.",
)
@click.option(
"--json",
"json_output",
is_flag=True,
default=False,
help="Print the deployments as a JSON array of {id, name} instead of a table.",
)
@deploy.command("list", help="[Beta] List LangSmith Deployments.")
def deploy_list(
api_key: str | None,
@@ -2601,11 +2588,13 @@ def deploy_list(
name_contains: str,
agent_id: str | None,
environment: str | None,
json_output: bool,
) -> None:
if agent_id is not None or environment is not None:
click.secho(
"Note: --agent-id and --agent-environment flags are in private beta",
fg="yellow",
err=True,
)
if agent_id is not None and not agent_id.strip():
raise click.UsageError("--agent-id must not be empty.")
@@ -2614,15 +2603,19 @@ def deploy_list(
filters["agent_id"] = agent_id
if environment is not None:
filters["agent_environment"] = environment
client = _create_host_backend_client(host_url, api_key)
deployments = _call_host_backend_with_optional_tenant(
client = _create_host_backend_client(host_url, api_key, no_input=json_output)
resources = _call_host_backend_with_optional_tenant(
client,
lambda c: c.list_deployments(name_contains=name_contains, **filters),
no_input=json_output,
)
if not deployments:
deployments = [DeploymentRow.from_resource(resource) for resource in resources]
if json_output:
click.echo(json_mod.dumps([deployment.as_json() for deployment in deployments]))
elif not deployments:
click.echo("No deployments found.")
return
click.echo(format_deployments_table(deployments))
else:
click.echo(DEPLOYMENTS_TABLE.render(deployments))
# ---------------------------------------------------------------------------
@@ -2665,11 +2658,14 @@ def deploy_listeners_list(
)
if json_output:
click.echo(json_mod.dumps(listeners))
return
if not listeners:
elif not listeners:
click.echo("No listeners found for this workspace.")
return
click.echo(format_listeners_table(listeners))
else:
click.echo(
LISTENERS_TABLE.render(
[Listener.from_resource(listener) for listener in listeners]
)
)
# ---------------------------------------------------------------------------
@@ -2712,7 +2708,7 @@ def deploy_revisions_list(
if not revisions:
click.echo(f"No revisions found for deployment {deployment_id}.")
return
click.echo(format_revisions_table(revisions))
click.echo(REVISIONS_TABLE.render(_with_replaced_status(revisions)))
# ---------------------------------------------------------------------------
+34 -9
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Literal
from urllib.parse import urlparse
@@ -220,27 +221,51 @@ class HostBackendClient:
*,
name: str | None = None,
name_contains: str | None = None,
limit: int | None = None,
agent_id: str | None = None,
agent_environment: str | None = None,
) -> list[dict[str, Any]]:
) -> list[dict[str, object]]:
given = (
("name", name),
("name_contains", name_contains),
("limit", limit),
("agent_id", agent_id),
("agent_environment", agent_environment),
)
params = {key: value for key, value in given if value is not None}
return _resources(self._request("GET", "/v2/deployments", params=params))
filters = {key: value for key, value in given if value is not None}
return self._every_page("/v2/deployments", filters)
def _every_page(
self, path: str, filters: Mapping[str, object]
) -> list[dict[str, object]]:
resources: list[dict[str, object]] = []
seen_ids: set[object] = set()
while True:
page = _resources(
self._request(
"GET",
path,
params={
**filters,
"limit": MAX_PAGE_SIZE,
"offset": len(resources),
},
)
)
page_ids = {resource.get("id") for resource in page}
if page_ids and page_ids <= seen_ids:
raise HostBackendError(
f"GET {path} returned the same resources again: this control "
"plane ignores 'offset', so the CLI cannot read past the first page."
)
seen_ids |= page_ids
resources.extend(page)
if len(page) < MAX_PAGE_SIZE:
return resources
def get_listener(self, listener_id: str) -> dict[str, Any]:
return self._request("GET", f"/v2/listeners/{listener_id}")
def list_listeners(self) -> list[dict[str, Any]]:
return _resources(
self._request("GET", "/v2/listeners", params={"limit": MAX_PAGE_SIZE})
)
def list_listeners(self) -> list[dict[str, object]]:
return self._every_page("/v2/listeners", {})
def get_deployment(self, deployment_id: str) -> dict[str, Any]:
return self._request("GET", f"/v2/deployments/{deployment_id}")
+297
View File
@@ -482,6 +482,177 @@ def test_deploy_list_command_no_results(monkeypatch) -> None:
assert result.output.strip() == "No deployments found."
def test_deploy_list_prints_an_aligned_table(monkeypatch) -> None:
class FakeClient:
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
pass
def list_deployments(self, name_contains: str = ""):
return [
{
"id": "dep-123",
"name": "alpha",
"source_config": {"custom_url": "https://alpha.example.com"},
},
{"id": "dep-4567", "name": "beta-agent", "source_config": {}},
]
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
result = CliRunner().invoke(
cli,
[
"deploy",
"list",
"--api-key",
"test-key",
"--host-url",
"https://api.example.com",
],
)
assert result.exit_code == 0, result.output
assert result.output.splitlines() == [
"Deployment ID Deployment Name Deployment URL ",
"------------- --------------- -------------------------",
"dep-123 alpha https://alpha.example.com",
"dep-4567 beta-agent - ",
]
def _deploy_list_json(
monkeypatch, deployments: list[dict], *extra_args: str
) -> click.testing.Result:
class FakeClient:
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
pass
def list_deployments(
self,
name_contains: str = "",
agent_id: str | None = None,
agent_environment: str | None = None,
):
return deployments
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
return CliRunner().invoke(
cli,
[
"deploy",
"list",
"--json",
"--api-key",
"test-key",
"--host-url",
"https://api.example.com",
*extra_args,
],
)
def test_deploy_list_command_json(monkeypatch) -> None:
result = _deploy_list_json(
monkeypatch,
[
{
"id": "dep-123",
"name": "alpha",
"source_config": {"custom_url": "https://alpha.example.com"},
},
{"id": "dep-456", "name": "beta", "source_config": {}},
],
)
assert result.exit_code == 0, result.output
assert json.loads(result.stdout) == [
{"id": "dep-123", "name": "alpha"},
{"id": "dep-456", "name": "beta"},
]
def test_deploy_list_json_never_prints_deployment_secrets(monkeypatch) -> None:
result = _deploy_list_json(
monkeypatch,
[
{
"id": "dep-123",
"name": "alpha",
"source_config": {"custom_url": None},
"secrets": [{"name": "OPENAI_API_KEY", "value": "sk-plain-value"}],
}
],
)
assert result.exit_code == 0, result.output
assert "sk-plain-value" not in result.output
assert json.loads(result.stdout) == [{"id": "dep-123", "name": "alpha"}]
def test_deploy_list_command_json_no_results(monkeypatch) -> None:
result = _deploy_list_json(monkeypatch, [])
assert result.exit_code == 0, result.output
assert json.loads(result.stdout) == []
def test_deploy_list_rejects_a_deployment_without_an_id(monkeypatch) -> None:
result = _deploy_list_json(monkeypatch, [{"name": "alpha"}])
assert result.exit_code != 0
assert "deployment without an id" in result.output
def test_deploy_list_json_keeps_the_agent_note_off_stdout(monkeypatch) -> None:
result = _deploy_list_json(
monkeypatch,
[{"id": "dep-123", "name": "alpha"}],
"--agent-id",
"customer-support",
"--agent-environment",
"staging",
)
assert result.exit_code == 0, result.output
assert json.loads(result.stdout) == [{"id": "dep-123", "name": "alpha"}]
assert "private beta" in result.stderr
def test_deploy_list_json_never_prompts_for_a_workspace(monkeypatch) -> None:
class FakeClient:
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
pass
def list_deployments(self, name_contains: str = ""):
raise HostBackendError("requires workspace specification", status_code=403)
def set_tenant(self, tenant_id: str) -> None:
raise AssertionError("must not retry with a prompted tenant with --json")
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
prompt_calls = []
monkeypatch.setattr(
click, "prompt", lambda *a, **k: prompt_calls.append((a, k)) or "unreachable"
)
result = CliRunner().invoke(
cli,
[
"deploy",
"list",
"--json",
"--api-key",
"test-key",
"--host-url",
"https://api.example.com",
],
)
assert prompt_calls == []
assert result.exit_code != 0
assert "LANGSMITH_TENANT_ID" in result.output
def test_deploy_listeners_list_command(monkeypatch) -> None:
runner = CliRunner()
captured: dict[str, str] = {}
@@ -564,6 +735,84 @@ def test_deploy_listeners_list_command_no_results(monkeypatch) -> None:
assert result.output.strip() == "No listeners found for this workspace."
def test_deploy_listeners_list_prints_an_aligned_table(monkeypatch) -> None:
class FakeClient:
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
pass
def list_listeners(self):
return [
{
"id": "listener-1",
"compute_id": "prod-cluster",
"compute_config": {"k8s_namespaces": ["agents"]},
},
{
"id": "listener-22",
"compute_id": "multi",
"compute_config": {"k8s_namespaces": ["agents", "agents-staging"]},
},
{"id": "listener-3", "compute_id": "", "compute_config": {}},
{
"id": "listener-4",
"compute_id": None,
"compute_config": {"k8s_namespaces": ["agents"]},
},
]
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
result = CliRunner().invoke(
cli,
[
"deploy",
"listeners",
"list",
"--api-key",
"test-key",
"--host-url",
"https://api.example.com",
],
)
assert result.exit_code == 0, result.output
assert result.output.splitlines() == [
"Listener ID Compute ID Namespaces ",
"----------- ------------ ----------------------",
"listener-1 prod-cluster agents ",
"listener-22 multi agents, agents-staging",
"listener-3 - - ",
"listener-4 - agents ",
]
def test_deploy_listeners_list_rejects_a_listener_without_an_id(monkeypatch) -> None:
class FakeClient:
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
pass
def list_listeners(self):
return [{"compute_id": "prod-cluster"}]
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
result = CliRunner().invoke(
cli,
[
"deploy",
"listeners",
"list",
"--api-key",
"test-key",
"--host-url",
"https://api.example.com",
],
)
assert result.exit_code != 0
assert "listener without an id" in result.output
def test_deploy_listeners_list_command_json(monkeypatch) -> None:
runner = CliRunner()
@@ -766,6 +1015,54 @@ def test_deploy_revisions_list_command_no_results(monkeypatch) -> None:
assert result.output.strip() == "No revisions found for deployment dep-123."
def test_deploy_revisions_list_marks_older_deployed_revisions_as_replaced(
monkeypatch,
) -> None:
class FakeClient:
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
pass
def list_revisions(self, deployment_id: str, limit: int = 1):
return [
{
"id": "rev-3",
"status": "DEPLOYING",
"created_at": "2026-10-20T10:00:00Z",
},
{
"id": "rev-2",
"status": "DEPLOYED",
"created_at": "2026-10-19T10:00:00Z",
},
{"id": "rev-1", "status": "DEPLOYED", "created_at": None},
]
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
result = CliRunner().invoke(
cli,
[
"deploy",
"revisions",
"list",
"--api-key",
"test-key",
"--host-url",
"https://api.example.com",
"dep-123",
],
)
assert result.exit_code == 0, result.output
assert result.output.splitlines() == [
"Revision ID Status Created At ",
"----------- --------- --------------------",
"rev-3 DEPLOYING 2026-10-20T10:00:00Z",
"rev-2 DEPLOYED 2026-10-19T10:00:00Z",
"rev-1 REPLACED - ",
]
def test_deploy_revisions_list_command_with_explicit_limit(monkeypatch) -> None:
runner = CliRunner()
captured: dict[str, str] = {}
@@ -111,7 +111,11 @@ class ControlPlaneDouble:
def _respond(self, request: httpx.Request) -> httpx.Response:
method, path = request.method, request.url.path
if (method, path) == ("GET", "/v2/listeners"):
return httpx.Response(200, json={"resources": self.listeners})
offset = int(request.url.params.get("offset", 0))
limit = int(request.url.params.get("limit", 20))
return httpx.Response(
200, json={"resources": self.listeners[offset : offset + limit]}
)
if method == "GET" and path.startswith("/v2/listeners/"):
listener_id = path.rsplit("/", 1)[-1]
if not _looks_like_a_uuid(listener_id):
@@ -1288,14 +1292,16 @@ def test_listener_flags_without_push_to_make_no_call_at_all(
assert deploy_project.timeline == []
def test_a_truncated_listener_page_says_so(deploy_project: DeployProject) -> None:
def test_listeners_beyond_the_first_page_are_counted(
deploy_project: DeployProject,
) -> None:
deploy_project.control_plane.listeners = [
{
"id": str(uuid.UUID(int=index)),
"compute_id": "cluster",
"compute_config": {"k8s_namespaces": ["agents"]},
}
for index in range(100)
for index in range(101)
]
result = deploy_project.run(
@@ -1303,7 +1309,8 @@ def test_a_truncated_listener_page_says_so(deploy_project: DeployProject) -> Non
)
assert result.exit_code != 0
assert "first 100" in result.output
assert "... and 91 more" in result.output
assert "first 100" not in result.output
def test_a_managed_build_in_a_listener_workspace_points_at_push_to(
@@ -75,6 +75,7 @@ def test_agent_create(deployment_api, tmp_path, monkeypatch):
"agent_id": "customer-support",
"agent_environment": "staging",
"limit": "100",
"offset": "0",
}
payload = json.loads(requests[1].content)
assert payload["agent"] == {
@@ -33,6 +33,7 @@ from langgraph_cli.deploy import (
_Emitter,
_env_without_deployment_name,
_parse_env_from_config,
_print_deployment_result,
_resolve_env_path,
_resolve_pushed_image_digest,
_resource_spec,
@@ -536,6 +537,39 @@ class TestEmitterTextMode:
assert "URL: https://app.test" in lines
@pytest.mark.parametrize(
("deployment", "expected_url_lines"),
[
pytest.param(
{
"id": "dep-1",
"source_config": {"custom_url": "https://agent.example.com"},
},
["URL: https://agent.example.com"],
id="custom_url_is_printed",
),
pytest.param(
{"id": "dep-1", "source_config": {}}, [], id="no_custom_url_no_line"
),
],
)
def test_a_deployed_result_prints_the_deployment_url(
capsys, monkeypatch, deployment, expected_url_lines
):
class FakeClient:
def get_deployment(self, deployment_id: str) -> dict:
return deployment
monkeypatch.setattr(deploy_mod, "_emitter", None)
_print_deployment_result(
FakeClient(), "dep-1", "DEPLOYED", dashboard_label="Deployment dashboard"
)
lines = [line.strip() for line in capsys.readouterr().out.splitlines()]
assert [line for line in lines if line.startswith("URL:")] == expected_url_lines
# ---------------------------------------------------------------------------
# --no-input guard on _create_host_backend_client
# ---------------------------------------------------------------------------
@@ -1210,6 +1244,7 @@ def test_finding_a_deployment_by_name_narrows_the_search_for_every_server_versio
"name": "agent",
"name_contains": "agent",
"limit": "100",
"offset": "0",
}
assert found == ExistingDeployment("dep-1", "github")
@@ -1237,20 +1272,29 @@ def test_a_server_that_ignores_the_exact_name_filter_never_matches_another_deplo
assert find_deployment_by_name(client, "brand-new-agent") is None
def test_a_full_page_without_a_match_refuses_to_claim_the_name_is_free():
page = [
{"id": f"dep-{index}", "name": f"other-agent-{index}"} for index in range(100)
]
client = HostBackendClient(
"https://api.example.com",
"key",
transport=httpx.MockTransport(
lambda req: httpx.Response(200, json={"resources": page})
def test_a_deployment_beyond_the_first_page_is_found_by_name():
deployments = [
*(
{"id": f"dep-{index}", "name": f"other-agent-{index}"}
for index in range(100)
),
{"id": "dep-target", "name": "agent", "source": "external_docker"},
]
def handler(req: httpx.Request) -> httpx.Response:
offset = int(req.url.params["offset"])
limit = int(req.url.params["limit"])
return httpx.Response(
200, json={"resources": deployments[offset : offset + limit]}
)
client = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
with pytest.raises(click.ClickException, match="--deployment-id"):
find_deployment_by_name(client, "brand-new-agent")
assert find_deployment_by_name(client, "agent") == ExistingDeployment(
"dep-target", "external_docker"
)
def test_a_partial_page_without_a_match_means_the_name_is_free():
+72 -9
View File
@@ -575,7 +575,7 @@ def test_list_listeners_asks_for_a_full_page():
)
assert c.list_listeners() == [{"id": "listener-1"}]
assert seen["url"] == "https://api.example.com/v2/listeners?limit=100"
assert seen["url"] == "https://api.example.com/v2/listeners?limit=100&offset=0"
@pytest.mark.parametrize(
@@ -602,29 +602,30 @@ def test_is_cloud_recognises_the_managed_control_plane(control_plane_url, expect
assert endpoints.is_cloud is expected
FIRST_PAGE = {"limit": "100", "offset": "0"}
@pytest.mark.parametrize(
("call", "expected_params"),
[
pytest.param(
lambda c: c.list_deployments(name="agent"),
{"name": "agent"},
{"name": "agent", **FIRST_PAGE},
id="exact_name_filters_server_side",
),
pytest.param(
lambda c: c.list_deployments(name_contains="age"),
{"name_contains": "age"},
{"name_contains": "age", **FIRST_PAGE},
id="substring_search_keeps_its_own_parameter",
),
pytest.param(
lambda c: c.list_deployments(),
{},
id="no_filter_sends_no_parameters",
FIRST_PAGE,
id="no_filter_sends_only_the_page",
),
pytest.param(
lambda c: c.list_deployments(
name="agent", name_contains="agent", limit=100
),
{"name": "agent", "name_contains": "agent", "limit": "100"},
lambda c: c.list_deployments(name="agent", name_contains="agent"),
{"name": "agent", "name_contains": "agent", **FIRST_PAGE},
id="both_filters_travel_together_for_older_servers",
),
],
@@ -645,6 +646,68 @@ def test_list_deployments_sends_one_name_filter(call, expected_params):
assert seen == expected_params
@pytest.mark.parametrize(
"list_every",
[
pytest.param(HostBackendClient.list_deployments, id="deployments"),
pytest.param(HostBackendClient.list_listeners, id="listeners"),
],
)
def test_list_endpoints_read_every_page(list_every):
resources = [{"id": f"resource-{index}"} for index in range(101)]
offsets: list[str] = []
def handler(req: httpx.Request) -> httpx.Response:
offset = int(req.url.params["offset"])
limit = int(req.url.params["limit"])
offsets.append(req.url.params["offset"])
return httpx.Response(
200, json={"resources": resources[offset : offset + limit]}
)
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
assert list_every(c) == resources
assert offsets == ["0", "100"]
FIRST_PAGE_OF_DEPLOYMENTS = [
{"id": f"dep-{index}", "status": "DEPLOYED"} for index in range(100)
]
@pytest.mark.parametrize(
"repeated_page",
[
pytest.param(FIRST_PAGE_OF_DEPLOYMENTS, id="same_page_again"),
pytest.param(
[
{"id": "dep-99", "status": "DEPLOYING"},
*FIRST_PAGE_OF_DEPLOYMENTS[:99],
],
id="same_deployments_reordered_and_updated",
),
],
)
def test_list_deployments_fails_when_the_server_ignores_the_offset(repeated_page):
pages = iter([FIRST_PAGE_OF_DEPLOYMENTS, repeated_page])
def handler(req: httpx.Request) -> httpx.Response:
page = next(pages, None)
if page is None:
return httpx.Response(500, json={"detail": "the guard let paging run on"})
return httpx.Response(200, json={"resources": page})
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
with pytest.raises(HostBackendError, match="ignores 'offset'"):
c.list_deployments()
@pytest.mark.parametrize(
("body", "expected"),
[
+4 -98
View File
@@ -1,11 +1,6 @@
from unittest.mock import patch
from langgraph_cli.deploy import (
_extract_deployment_url,
format_deployments_table,
format_listeners_table,
format_revisions_table,
)
from langgraph_cli.deploy import Table
from langgraph_cli.util import clean_empty_lines, warn_non_wolfi_distro
@@ -194,96 +189,7 @@ def test_warn_non_wolfi_distro_does_not_modify_config():
assert config_copy == original_config # Config should remain unchanged
def test_extract_deployment_url_uses_custom_url():
deployment = {"source_config": {"custom_url": "https://example.com/custom"}}
assert _extract_deployment_url(deployment) == "https://example.com/custom"
def test_a_table_without_rows_still_renders_its_headers():
table = Table(("Name", "Status"), lambda resource: (None, None))
def test_extract_deployment_url_defaults_to_dash():
assert _extract_deployment_url({"id": "dep-123"}) == "-"
def test_format_deployments_table():
output = format_deployments_table(
[
{
"id": "dep-123",
"name": "alpha",
"source_config": {"custom_url": "https://alpha.example.com"},
},
{
"id": "dep-456",
"name": "beta",
"url": "https://beta.example.com",
},
]
)
assert "Deployment ID" in output
assert "Deployment Name" in output
assert "Deployment URL" in output
assert "dep-123" in output
assert "alpha" in output
assert "https://alpha.example.com" in output
assert "dep-456" in output
def test_format_revisions_table():
output = format_revisions_table(
[
{
"id": "rev-123",
"status": "DEPLOYED",
"created_at": "2023-11-09T10:00:00Z",
},
{
"id": "rev-456",
"status": "CREATING",
"created_at": "2023-11-07T05:31:56Z",
},
{
"id": "rev-789",
"status": "DEPLOYED",
"created_at": "2023-11-08T10:00:00Z",
},
]
)
assert "Revision ID" in output
assert "Status" in output
assert "Created At" in output
assert "rev-123" in output
assert "CREATING" in output
assert "2023-11-07T05:31:56Z" in output
assert "rev-456" in output
assert "rev-789" in output
assert "REPLACED" in output
def test_format_listeners_table():
output = format_listeners_table(
[
{
"id": "listener-1",
"compute_id": "prod-cluster",
"compute_config": {"k8s_namespaces": ["agents"]},
},
{
"id": "listener-2",
"compute_id": "multi-cluster",
"compute_config": {"k8s_namespaces": ["agents", "agents-staging"]},
},
{
"id": "listener-3",
"compute_id": "broken-cluster",
},
]
)
assert "Listener ID" in output
assert "Compute ID" in output
assert "Namespaces" in output
assert "listener-1" in output
assert "prod-cluster" in output
assert "agents" in output
assert "listener-2" in output
assert "agents, agents-staging" in output
assert "listener-3" in output
assert "broken-cluster" in output
assert table.render([]).splitlines() == ["Name Status", "---- ------"]
+7 -7
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 5
requires-python = ">=3.11"
[manifest]
@@ -267,20 +267,20 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.2.0"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "ormsgpack" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/e1/089c4c9e0a2fec7f883f82ae8e6a727138d50074cfeb6644bc2d13b1019b/langgraph_checkpoint-4.2.0.tar.gz", hash = "sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7", size = 180239, upload-time = "2026-08-07T20:05:03.756Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/69/31fdbdc65a85bbd6178afa193c772bb926620f47b4869638bc2bc80afaaa/langgraph_checkpoint-4.3.0.tar.gz", hash = "sha256:c75965d84cc2c1d549163e910a15bcb577758001b141619d05297c463280b018", size = 182652, upload-time = "2026-10-12T22:26:31.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/71/3b475f09bd57d3a5649792c66353312b4432afd843f301739dfcebd157f0/langgraph_checkpoint-4.2.0-py3-none-any.whl", hash = "sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942", size = 56833, upload-time = "2026-08-07T20:05:02.655Z" },
{ url = "https://files.pythonhosted.org/packages/1f/0c/84747e340bf4f29291c84cdd5733fc8d0a822f3d33bb24e664a18afa4a7c/langgraph_checkpoint-4.3.0-py3-none-any.whl", hash = "sha256:bedfafe2f997ded60e4fa593e79f56f436a6e45586392dc382aa810d0c751c64", size = 58063, upload-time = "2026-10-12T22:26:30.429Z" },
]
[[package]]
name = "langgraph-checkpoint-postgres"
version = "3.1.1"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langgraph-checkpoint" },
@@ -288,9 +288,9 @@ dependencies = [
{ name = "psycopg" },
{ name = "psycopg-pool" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/92/1e8959f8cd1b56e672fde3227f6fd642be85af6c5fd662d73921074aa39d/langgraph_checkpoint_postgres-3.1.1.tar.gz", hash = "sha256:d320e147ddad8c374cd546df0b52b532dd54d0541dd9fd23fc738cbd5de76f41", size = 150413, upload-time = "2026-07-30T19:15:39.014Z" }
sdist = { url = "https://files.pythonhosted.org/packages/78/bf/d0ab4d6e4d61952de2f77044d7407b7ce09e07d53e7bb448cf9df55c35e5/langgraph_checkpoint_postgres-3.1.3.tar.gz", hash = "sha256:a152a9c0c3d5931bc949b64e01e8c7da20be57a32aa754626446318e90a07650", size = 158122, upload-time = "2026-10-12T23:05:19.759Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/32/ba457698a48a0e18d786caa770033067049fbe36d6846f8e50f13b594b51/langgraph_checkpoint_postgres-3.1.1-py3-none-any.whl", hash = "sha256:6e353aecd8150de144fef8e51a49076f58b7d6830d4cf51392b7ad4d79832ba7", size = 50778, upload-time = "2026-07-30T19:15:37.405Z" },
{ url = "https://files.pythonhosted.org/packages/41/42/659106ed829ee026144e32ddd589735f978d2ed09681020e5965cdfca04c/langgraph_checkpoint_postgres-3.1.3-py3-none-any.whl", hash = "sha256:050ae583223e24d97747f27b9e06e7345bf13c972f1fb6ed33bb3d1f9c11cee4", size = 52048, upload-time = "2026-10-12T23:05:18.854Z" },
]
[[package]]
+101 -50
View File
@@ -1,10 +1,10 @@
version = 1
revision = 3
revision = 5
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'emscripten'",
"(python_full_version >= '3.11' and python_full_version < '3.15' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform != 'emscripten')",
"python_full_version < '3.11'",
]
@@ -24,7 +24,8 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
wheels = [
@@ -416,7 +417,7 @@ version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
@@ -469,11 +470,11 @@ wheels = [
[[package]]
name = "distlib"
version = "0.4.0"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
{ url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" },
]
[[package]]
@@ -496,7 +497,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -535,7 +536,7 @@ name = "grpcio"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
wheels = [
@@ -811,7 +812,8 @@ dependencies = [
{ name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
{ name = "idna" },
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
wheels = [
@@ -1000,7 +1002,7 @@ dependencies = [
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
{ name = "uuid-utils" },
]
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" }
@@ -1013,7 +1015,7 @@ name = "langchain-protocol"
version = "0.0.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/b3/4e2429876c7a35585618caa2b9f9089f7162a6b50562b614ad82ac11c17e/langchain_protocol-0.0.17.tar.gz", hash = "sha256:e7cbe58c205df4b4fd87dc6d5bb23f10e13b236d0e2e1b0b9d05bc2b648f3eea", size = 6026, upload-time = "2026-06-12T18:39:51.923Z" }
wheels = [
@@ -1337,84 +1339,114 @@ wheels = [
[[package]]
name = "opentelemetry-api"
version = "1.41.0"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" },
{ url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
]
[[package]]
name = "opentelemetry-exporter-http-transport"
version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
]
[[package]]
name = "opentelemetry-exporter-otlp-common"
version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-sdk" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.41.0"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/28/e8eca94966fe9a1465f6094dc5ddc5398473682180279c94020bc23b4906/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee", size = 20411, upload-time = "2026-04-09T14:38:36.572Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/c4/78b9bf2d9c1d5e494f44932988d9d91c51a66b9a7b48adf99b62f7c65318/opentelemetry_exporter_otlp_proto_common-1.41.0-py3-none-any.whl", hash = "sha256:7a99177bf61f85f4f9ed2072f54d676364719c066f6d11f515acc6c745c7acf0", size = 18366, upload-time = "2026-04-09T14:38:15.135Z" },
{ url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.41.0"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-http-transport", extra = ["requests"] },
{ name = "opentelemetry-exporter-otlp-common" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/63/d9f43cd75f3fabb7e01148c89cfa9491fc18f6580a6764c554ff7c953c46/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5", size = 24139, upload-time = "2026-04-09T14:38:38.128Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b5/a214cd907eedc17699d1c2d602288ae17cb775526df04db3a3b3585329d2/opentelemetry_exporter_otlp_proto_http-1.41.0-py3-none-any.whl", hash = "sha256:a9c4ee69cce9c3f4d7ee736ad1b44e3c9654002c0816900abbafd9f3cf289751", size = 22673, upload-time = "2026-04-09T14:38:18.349Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.41.0"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/08e3dc6156878713e8c811682bc76151f5fe1a3cb7f3abda3966fd56e71e/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6", size = 45669, upload-time = "2026-04-09T14:38:45.978Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/8c/65ef7a9383a363864772022e822b5d5c6988e6f9dabeebb9278f5b86ebc3/opentelemetry_proto-1.41.0-py3-none-any.whl", hash = "sha256:b970ab537309f9eed296be482c3e7cca05d8aca8165346e929f658dbe153b247", size = 72074, upload-time = "2026-04-09T14:38:29.38Z" },
{ url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.41.0"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload-time = "2026-04-09T14:38:47.225Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/13/a7825118208cb32e6a4edcd0a99f925cbef81e77b3b0aedfd9125583c543/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd", size = 180214, upload-time = "2026-04-09T14:38:30.657Z" },
{ url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.62b0"
version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload-time = "2026-04-09T14:38:48.274Z" }
sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload-time = "2026-04-09T14:38:32.394Z" },
{ url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" },
]
[[package]]
@@ -1556,11 +1588,11 @@ wheels = [
[[package]]
name = "packaging"
version = "26.1"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
@@ -1642,7 +1674,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/e5/06d23afac9973109d1e3c8ad38e1547a12e860610e327c05ee686827dc37/pydantic-2.13.2.tar.gz", hash = "sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1", size = 843836, upload-time = "2026-04-17T09:31:59.636Z" }
@@ -1655,7 +1687,7 @@ name = "pydantic-core"
version = "2.46.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/bb/4742f05b739b2478459bb16fa8470549518c802e06ddcf3f106c5081315e/pydantic_core-2.46.2.tar.gz", hash = "sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596", size = 471269, upload-time = "2026-04-17T09:10:07.017Z" }
wheels = [
@@ -1818,7 +1850,8 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
wheels = [
@@ -2035,11 +2068,11 @@ wheels = [
[[package]]
name = "setuptools"
version = "83.0.0"
version = "84.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
sdist = { url = "https://files.pythonhosted.org/packages/73/eb/7e5f628ee2eb168fecbf8026de7856b2543d6d609cc85f086b9a91ac93d6/setuptools-84.1.0.tar.gz", hash = "sha256:23657205b63d4d31395ab917815247f694aafad28cd21316bf5fd45a51048ea0", size = 1171783, upload-time = "2026-10-19T20:11:43.489Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
{ url = "https://files.pythonhosted.org/packages/18/02/b4bbb90f8f29e334898ff45c6d9b1fffaa6f378017052b3023a06262b5f7/setuptools-84.1.0-py3-none-any.whl", hash = "sha256:5231689c6ac8156cd155cfb98c3fd6fb252c3e7e9bd3d78b64d3c55f132624c9", size = 819380, upload-time = "2026-10-19T20:11:41.684Z" },
]
[[package]]
@@ -2079,7 +2112,7 @@ version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
wheels = [
@@ -2223,17 +2256,34 @@ wheels = [
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform != 'emscripten')",
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
@@ -2375,18 +2425,19 @@ wheels = [
[[package]]
name = "virtualenv"
version = "21.7.13"
version = "21.14.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "packaging" },
{ name = "platformdirs" },
{ name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/50/c9b84eb106d0db420b9878dac0a386c5791726f127ce20b06897f6e3a1e9/virtualenv-21.7.13.tar.gz", hash = "sha256:0355558b6f33619aab31347e43643b0ebc97f61ea3acf617b2b69e1f8a843d11", size = 5360306, upload-time = "2026-09-18T04:35:49.354Z" }
sdist = { url = "https://files.pythonhosted.org/packages/70/aa/bff50daee0488e16d611258a9b6c5a1d0c9e128f8ca424e9f4e1c0444ef2/virtualenv-21.14.13.tar.gz", hash = "sha256:de5307ba4b9a0001d9c8ea02832d38f63ca432880f416c1bface61c4323fd905", size = 5570301, upload-time = "2026-10-22T20:10:16.952Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/ce/e74453531b0c49a58d0e8a4f9bab4495705859fee4a4f7de27d58f4a791a/virtualenv-21.7.13-py3-none-any.whl", hash = "sha256:1bea5af7463f59c4719db48fe739579a2a4f569c96f26c086edda85c96da9f59", size = 5328680, upload-time = "2026-09-18T04:35:47.194Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a9/c10d8a44a29fb94a18b43fdf11f38caca53ae7c9406f11a72b1cb8f624f1/virtualenv-21.14.13-py3-none-any.whl", hash = "sha256:5fbb04138a4861580b78ba9110d529a26bc48fcc93c8ec36bc0202eeed96a788", size = 5557094, upload-time = "2026-10-22T20:10:15.096Z" },
]
[[package]]
+22 -6
View File
@@ -29,7 +29,19 @@ from langgraph._internal._constants import (
NS_SEP,
)
DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10007"))
class _DefaultRecursionLimit(int):
"""The limit `ensure_config` fills in when none is given.
`merge_configs` skips values of this type rather than any equal value, so an
explicit limit that happens to equal the default still applies. It checks the
type, not identity, because copying or pickling a config rebuilds the value.
"""
DEFAULT_RECURSION_LIMIT = _DefaultRecursionLimit(
getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10007")
)
DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT = int(
getenv("LANGGRAPH_DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT", "5000")
)
@@ -182,7 +194,7 @@ def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
base.get("callbacks"), cast(Callbacks, value)
)
elif key == "recursion_limit":
if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT:
if not isinstance(config["recursion_limit"], _DefaultRecursionLimit):
base["recursion_limit"] = config["recursion_limit"]
else:
base[key] = config[key] # type: ignore[literal-required]
@@ -335,6 +347,7 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
recursion_limit=DEFAULT_RECURSION_LIMIT,
configurable={},
)
inherited_tags: set[str] = set()
if var_config := var_child_runnable_config.get():
empty.update(
{
@@ -343,6 +356,7 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
if _is_not_empty(v)
},
)
inherited_tags.update(empty["tags"])
# An explicit config that supplies its own checkpoint coordinate (a
# thread_id, or any checkpoint_ns/checkpoint_id/checkpoint_map) is addressing
# its own checkpoint lineage, so drop the inherited ambient configurable
@@ -395,12 +409,14 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
elif k == "tags":
# Concatenate tags across configs so values bound via
# with_config(...) are preserved when later configs
# supply additional tags. Matches merge_configs.
# supply additional tags. Matches merge_configs. Tags the
# ambient config already has are skipped: a subgraph gets
# its parent's config both explicitly and as the ambient
# one, so they would double at every nesting level.
existing_tags: list[str] | None = empty.get("tags")
new_tags = [t for t in cast(list, v) if t not in inherited_tags]
empty["tags"] = (
[*existing_tags, *cast(list, v)]
if existing_tags
else list(cast(list, v))
[*existing_tags, *new_tags] if existing_tags else new_tags
)
else:
empty[k] = v # type: ignore[literal-required]
+46 -40
View File
@@ -657,48 +657,11 @@ class PregelLoop:
# check if iteration limit is reached
if self.step > self.stop:
self.status = "out_of_steps"
# stream the checkpoint the last step saved, as a tick that runs would
self._prepare_tasks_and_emit_checkpoint()
return False
# prepare next tasks
self.tasks = prepare_next_tasks(
self.checkpoint,
self.checkpoint_pending_writes,
self.nodes,
self.channels,
self.managed,
self.config,
self.step,
self.stop,
for_execution=True,
manager=self.manager,
store=self.store,
checkpointer=self.checkpointer,
trigger_to_nodes=self.trigger_to_nodes,
updated_channels=self.updated_channels,
retry_policy=self.retry_policy,
cache_policy=self.cache_policy,
)
# produce debug output
if self._checkpointer_put_after_previous is not None:
self._emit(
"checkpoints",
map_debug_checkpoint,
{
**self.checkpoint_config,
CONF: {
**self.checkpoint_config[CONF],
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
},
},
self.channels,
self.stream_keys,
self.checkpoint_metadata,
self.tasks.values(),
self.checkpoint_pending_writes,
self.prev_checkpoint_config,
self.output_keys,
)
self._prepare_tasks_and_emit_checkpoint()
# if no more tasks, we're done
if not self.tasks:
@@ -806,10 +769,53 @@ class PregelLoop:
self.checkpoint, self.interrupt_after, self.tasks.values()
):
self.status = "interrupt_after"
# the next tick, which streams this checkpoint, never runs
self._prepare_tasks_and_emit_checkpoint()
raise GraphInterrupt()
# unset resuming flag
self.config[CONF].pop(CONFIG_KEY_RESUMING, None)
def _prepare_tasks_and_emit_checkpoint(self) -> None:
self.tasks = prepare_next_tasks(
self.checkpoint,
self.checkpoint_pending_writes,
self.nodes,
self.channels,
self.managed,
self.config,
self.step,
self.stop,
for_execution=True,
manager=self.manager,
store=self.store,
checkpointer=self.checkpointer,
trigger_to_nodes=self.trigger_to_nodes,
updated_channels=self.updated_channels,
retry_policy=self.retry_policy,
cache_policy=self.cache_policy,
)
# produce debug output
if self._checkpointer_put_after_previous is not None:
self._emit(
"checkpoints",
map_debug_checkpoint,
{
**self.checkpoint_config,
CONF: {
**self.checkpoint_config[CONF],
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
},
},
self.channels,
self.stream_keys,
self.checkpoint_metadata,
self.tasks.values(),
self.checkpoint_pending_writes,
self.prev_checkpoint_config,
self.output_keys,
)
def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
raise NotImplementedError
-10
View File
@@ -861,11 +861,6 @@ class RemoteGraph(PregelProtocol):
)
yield {"type": mode, "ns": ns, "data": chunk.data, "interrupts": ints}
elif subgraphs:
if NS_SEP in chunk.event:
mode, ns_ = chunk.event.split(NS_SEP, 1)
ns = tuple(ns_.split(NS_SEP))
else:
mode, ns = chunk.event, ()
if req_single:
yield ns, chunk.data
else:
@@ -1016,11 +1011,6 @@ class RemoteGraph(PregelProtocol):
)
yield {"type": mode, "ns": ns, "data": chunk.data, "interrupts": ints}
elif subgraphs:
if NS_SEP in chunk.event:
mode, ns_ = chunk.event.split(NS_SEP, 1)
ns = tuple(ns_.split(NS_SEP))
else:
mode, ns = chunk.event, ()
if req_single:
yield ns, chunk.data
else:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.2.16"
version = "1.2.17"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
+33 -1
View File
@@ -1,11 +1,19 @@
from collections.abc import Callable
import pytest
from langchain_core.callbacks import AsyncCallbackManager, BaseCallbackHandler
from langgraph._internal._config import get_async_callback_manager_for_config
from langgraph._internal._config import (
DEFAULT_RECURSION_LIMIT,
get_async_callback_manager_for_config,
)
from langgraph.graph import StateGraph
from langgraph.pregel import Pregel
pytestmark = pytest.mark.anyio
EXPLICIT_DEFAULT_LIMIT = int(DEFAULT_RECURSION_LIMIT)
def test_new_async_manager_includes_tags() -> None:
config = {"callbacks": None}
@@ -114,3 +122,27 @@ async def test_with_config_tags_preserved_on_invoke() -> None:
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"
@pytest.mark.parametrize(
"rebind",
[
lambda g: g.with_config(recursion_limit=EXPLICIT_DEFAULT_LIMIT),
lambda g: g.with_config({"recursion_limit": EXPLICIT_DEFAULT_LIMIT}),
lambda g: g.with_config(
{"recursion_limit": 1}, recursion_limit=EXPLICIT_DEFAULT_LIMIT
),
lambda g: g.with_config(recursion_limit=EXPLICIT_DEFAULT_LIMIT).with_config(
tags=["t"]
),
],
ids=["kwarg", "config", "kwarg_over_config", "kept_by_later_with_config"],
)
def test_with_config_applies_recursion_limit_equal_to_default(
rebind: Callable[[Pregel], Pregel],
) -> None:
builder = StateGraph(dict)
builder.add_node("node", lambda state: state)
builder.add_edge("__start__", "node")
graph = builder.compile().with_config(recursion_limit=EXPLICIT_DEFAULT_LIMIT + 1)
assert rebind(graph).config["recursion_limit"] == EXPLICIT_DEFAULT_LIMIT
+62
View File
@@ -4410,6 +4410,52 @@ def test_debug_retry(sync_checkpointer: BaseCheckpointSaver):
assert stream_parent_conf == history_parent_conf
def test_interrupt_after_streams_the_checkpoint_it_pauses_on(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
class State(TypedDict):
x: int
graph = (
StateGraph(State)
.add_node("one", lambda state: {"x": 1})
.add_node("two", lambda state: {"x": 2})
.add_edge(START, "one")
.add_edge("one", "two")
.compile(checkpointer=sync_checkpointer, interrupt_after=["one"])
)
config = {"configurable": {"thread_id": "1"}}
checkpoints = list(graph.stream({"x": 0}, config, stream_mode="checkpoints"))
state = graph.get_state(config)
assert checkpoints[-1]["config"] == state.config
assert checkpoints[-1]["next"] == ["two"]
def test_recursion_limit_streams_the_last_checkpoint_saved(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
class State(TypedDict):
x: int
graph = (
StateGraph(State)
.add_node("loop", lambda state: {"x": state["x"] + 1})
.add_edge(START, "loop")
.add_edge("loop", "loop")
.compile(checkpointer=sync_checkpointer)
)
config = {"configurable": {"thread_id": "1"}, "recursion_limit": 3}
checkpoints = []
with pytest.raises(GraphRecursionError):
for chunk in graph.stream({"x": 0}, config, stream_mode="checkpoints"):
checkpoints.append(chunk)
assert checkpoints[-1]["config"] == graph.get_state(config).config
def test_debug_subgraphs(
sync_checkpointer: BaseCheckpointSaver, durability: Durability
):
@@ -6880,6 +6926,22 @@ def test_named_tasks_functional() -> None:
]
def test_invoke_tags_reach_a_node_three_subgraphs_deep_once() -> None:
class State(TypedDict):
copies: int
def leaf(state: State, config: RunnableConfig) -> State:
return {"copies": config["tags"].count("my-tag")}
graph = StateGraph(State).add_node("leaf", leaf).add_edge(START, "leaf").compile()
for _ in range(3):
graph = (
StateGraph(State).add_node("sub", graph).add_edge(START, "sub").compile()
)
assert graph.invoke({"copies": 0}, {"tags": ["my-tag"]}) == {"copies": 1}
def test_tags_stream_mode_messages() -> None:
model = GenericFakeChatModel(messages=iter(["foo"]), tags=["meow"])
graph = (
+49
View File
@@ -5875,6 +5875,55 @@ async def test_debug_retry(async_checkpointer: BaseCheckpointSaver):
assert stream_parent_conf == history_parent_conf
async def test_interrupt_after_streams_the_checkpoint_it_pauses_on(
async_checkpointer: BaseCheckpointSaver,
) -> None:
class State(TypedDict):
x: int
graph = (
StateGraph(State)
.add_node("one", lambda state: {"x": 1})
.add_node("two", lambda state: {"x": 2})
.add_edge(START, "one")
.add_edge("one", "two")
.compile(checkpointer=async_checkpointer, interrupt_after=["one"])
)
config = {"configurable": {"thread_id": "1"}}
checkpoints = [
chunk
async for chunk in graph.astream({"x": 0}, config, stream_mode="checkpoints")
]
state = await graph.aget_state(config)
assert checkpoints[-1]["config"] == state.config
assert checkpoints[-1]["next"] == ["two"]
async def test_recursion_limit_streams_the_last_checkpoint_saved(
async_checkpointer: BaseCheckpointSaver,
) -> None:
class State(TypedDict):
x: int
graph = (
StateGraph(State)
.add_node("loop", lambda state: {"x": state["x"] + 1})
.add_edge(START, "loop")
.add_edge("loop", "loop")
.compile(checkpointer=async_checkpointer)
)
config = {"configurable": {"thread_id": "1"}, "recursion_limit": 3}
checkpoints = []
with pytest.raises(GraphRecursionError):
async for chunk in graph.astream({"x": 0}, config, stream_mode="checkpoints"):
checkpoints.append(chunk)
assert checkpoints[-1]["config"] == (await graph.aget_state(config)).config
async def test_debug_subgraphs(
async_checkpointer: BaseCheckpointSaver, durability: Durability
):
+44
View File
@@ -819,6 +819,50 @@ async def test_astream():
]
_CALLER_CONFIG: RunnableConfig = {
"configurable": {"thread_id": "thread_1", "checkpoint_ns": "outer:123"}
}
_SUBGRAPH_PARTS = [
StreamPart(event="values|sub:abc", data={"chunk": "inner"}),
StreamPart(event="values", data={"chunk": "root"}),
]
_CALLER_PREFIXED = [
(("outer:123", "sub:abc"), {"chunk": "inner"}),
(("outer:123",), {"chunk": "root"}),
]
def test_stream_subgraphs_keeps_caller_namespace():
mock_sync_client = MagicMock()
mock_sync_client.runs.stream.return_value = _SUBGRAPH_PARTS
remote_pregel = RemoteGraph("test_graph_id", sync_client=mock_sync_client)
stream_parts = list(
remote_pregel.stream(
{"input": "data"}, _CALLER_CONFIG, stream_mode="values", subgraphs=True
)
)
assert stream_parts == _CALLER_PREFIXED
async def test_astream_subgraphs_keeps_caller_namespace():
mock_async_client = MagicMock()
async_iter = MagicMock()
async_iter.__aiter__.return_value = _SUBGRAPH_PARTS
mock_async_client.runs.stream.return_value = async_iter
remote_pregel = RemoteGraph("test_graph_id", client=mock_async_client)
stream_parts = [
part
async for part in remote_pregel.astream(
{"input": "data"}, _CALLER_CONFIG, stream_mode="values", subgraphs=True
)
]
assert stream_parts == _CALLER_PREFIXED
def test_invoke():
# set up test
mock_sync_client = MagicMock()
+36
View File
@@ -1,4 +1,6 @@
import copy
import functools
import pickle
import sys
import uuid
from collections.abc import Callable
@@ -25,6 +27,7 @@ from pydantic import BaseModel, Field
from typing_extensions import NotRequired, Required, TypedDict
from langgraph._internal._config import (
DEFAULT_RECURSION_LIMIT,
_is_not_empty,
_merge_callbacks,
ensure_config,
@@ -620,6 +623,30 @@ def test_merge_configs_merges_metadata_lc_versions() -> None:
assert merged["metadata"]["lc_agent_name"] == "agent"
@pytest.mark.parametrize(
"rebuild",
[
lambda config: config,
copy.deepcopy,
lambda config: pickle.loads(pickle.dumps(config)),
],
ids=["as_is", "deepcopy", "pickle"],
)
def test_merge_configs_skips_the_injected_default_recursion_limit(
rebuild: Callable[[RunnableConfig], RunnableConfig],
) -> None:
merged = merge_configs({"recursion_limit": 50}, rebuild(ensure_config()))
assert merged["recursion_limit"] == 50
def test_merge_configs_applies_an_explicit_recursion_limit_equal_to_default() -> None:
explicit_default = int(DEFAULT_RECURSION_LIMIT)
merged = merge_configs(
{"recursion_limit": 50}, {"recursion_limit": explicit_default}
)
assert merged["recursion_limit"] == explicit_default
def test_ensure_config_merges_metadata_lc_versions() -> None:
a = {
"metadata": {
@@ -772,6 +799,15 @@ def test_ensure_config_tags_concat_preserves_order_and_duplicates() -> None:
assert merged["tags"] == ["shared", "alpha", "shared", "beta"]
def test_ensure_config_skips_tags_inherited_from_ambient() -> None:
token = var_child_runnable_config.set({"tags": ["inherited"]})
try:
merged = ensure_config({"tags": ["inherited", "own"]})
finally:
var_child_runnable_config.reset(token)
assert merged["tags"] == ["inherited", "own"]
def test_ensure_config_merges_callbacks_across_configs() -> None:
a_cb = _TrackingCB("a")
b_cb = _TrackingCB("b")
+54 -45
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 5
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.15'",
@@ -931,18 +931,6 @@ wheels = [
{ 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]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
@@ -1451,7 +1439,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.16"
version = "1.2.17"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -2111,7 +2099,7 @@ wheels = [
[[package]]
name = "nbconvert"
version = "7.17.1"
version = "7.17.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
@@ -2129,9 +2117,9 @@ dependencies = [
{ name = "pygments" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/6a/252c7620305a9661205aed7a2a4b3592f5207bfca56ad11b7dea2c5ec2f6/nbconvert-7.17.2.tar.gz", hash = "sha256:344a218e8595117ce37396ed025d994f146d076226ebac59255a2fa085ab24d8", size = 865989, upload-time = "2026-10-12T09:11:19.583Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" },
{ url = "https://files.pythonhosted.org/packages/81/fd/a8d457af68848a8e5c12dd1d84ff35c415db471af78280e0703db4efbb12/nbconvert-7.17.2-py3-none-any.whl", hash = "sha256:6a143c11c00bf831604da4b1be4347d36ea52367057eb920135d69bc2862fed2", size = 262094, upload-time = "2026-10-12T09:11:17.215Z" },
]
[[package]]
@@ -2189,84 +2177,114 @@ wheels = [
[[package]]
name = "opentelemetry-api"
version = "1.39.1"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
{ url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
]
[[package]]
name = "opentelemetry-exporter-http-transport"
version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
]
[[package]]
name = "opentelemetry-exporter-otlp-common"
version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-sdk" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.39.1"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" },
{ url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.39.1"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-http-transport", extra = ["requests"] },
{ name = "opentelemetry-exporter-otlp-common" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.39.1"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" },
{ url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.39.1"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
{ url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.60b1"
version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
{ url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" },
]
[[package]]
@@ -4239,15 +4257,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/f3/1ac078fc8fceadcf066469acecacb35d2821cbfaf7d6fc5ac2107c7a314d/xxhash-4.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58", size = 37252, upload-time = "2026-08-17T08:24:06.652Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]
[[package]]
name = "zstandard"
version = "0.25.0"
+1 -1
View File
@@ -286,7 +286,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.16"
version = "1.2.17"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
+2 -2
View File
@@ -179,7 +179,7 @@ class HttpClient:
return await self.request_reconnect(
loc,
"GET",
headers=request_headers,
headers=headers,
# don't pass on_response so it's only called once
reconnect_limit=reconnect_limit - 1,
)
@@ -217,7 +217,7 @@ class HttpClient:
current_headers = dict(
request_headers if reconnect_path is None else reconnect_headers
)
if last_event_id is not None:
if last_event_id:
current_headers["Last-Event-ID"] = last_event_id
current_method = method if reconnect_path is None else "GET"
+10
View File
@@ -99,6 +99,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v1"] = "v1",
) -> AsyncIterator[StreamPart]: ...
@@ -131,6 +132,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v2"],
) -> AsyncIterator[StreamPartV2]: ...
@@ -147,6 +149,7 @@ class RunsClient:
stream_resumable: bool = False,
metadata: Mapping[str, Any] | None = None,
config: Config | None = None,
context: Context | None = None,
checkpoint_during: bool | None = None,
interrupt_before: All | Sequence[str] | None = None,
interrupt_after: All | Sequence[str] | None = None,
@@ -160,6 +163,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v1"] = "v1",
) -> AsyncIterator[StreamPart]: ...
@@ -176,6 +180,7 @@ class RunsClient:
stream_resumable: bool = False,
metadata: Mapping[str, Any] | None = None,
config: Config | None = None,
context: Context | None = None,
checkpoint_during: bool | None = None,
interrupt_before: All | Sequence[str] | None = None,
interrupt_after: All | Sequence[str] | None = None,
@@ -189,6 +194,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v2"],
) -> AsyncIterator[StreamPartV2]: ...
@@ -385,6 +391,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> Run: ...
@overload
@@ -414,6 +421,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> Run: ...
async def create(
@@ -647,6 +655,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> builtins.list[dict] | dict[str, Any]: ...
@overload
@@ -673,6 +682,7 @@ class RunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> builtins.list[dict] | dict[str, Any]: ...
async def wait(
+5 -3
View File
@@ -179,7 +179,7 @@ class SyncHttpClient:
return self.request_reconnect(
loc,
"GET",
headers=request_headers,
headers=headers,
# don't pass on_response so it's only called once
reconnect_limit=reconnect_limit - 1,
)
@@ -219,7 +219,7 @@ class SyncHttpClient:
current_headers = dict(
request_headers if reconnect_path is None else reconnect_headers
)
if last_event_id is not None:
if last_event_id:
current_headers["Last-Event-ID"] = last_event_id
current_method = method if reconnect_path is None else "GET"
@@ -286,7 +286,9 @@ class SyncHttpClient:
break
def _encode_json(json: Any) -> tuple[dict[str, str], bytes]:
def _encode_json(json: Any) -> tuple[dict[str, str], bytes | None]:
if json is None:
return {}, None
body = orjson.dumps(
json,
_orjson_default,
+21 -2
View File
@@ -80,6 +80,7 @@ class SyncRunsClient:
command: Command | None = None,
stream_mode: StreamMode | Sequence[StreamMode] = "values",
stream_subgraphs: bool = False,
stream_resumable: bool = False,
metadata: Mapping[str, Any] | None = None,
config: Config | None = None,
context: Context | None = None,
@@ -98,6 +99,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v1"] = "v1",
) -> Iterator[StreamPart]: ...
@@ -111,6 +113,7 @@ class SyncRunsClient:
command: Command | None = None,
stream_mode: StreamMode | Sequence[StreamMode] = "values",
stream_subgraphs: bool = False,
stream_resumable: bool = False,
metadata: Mapping[str, Any] | None = None,
config: Config | None = None,
context: Context | None = None,
@@ -129,6 +132,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v2"],
) -> Iterator[StreamPartV2]: ...
@@ -159,6 +163,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v1"] = "v1",
) -> Iterator[StreamPart]: ...
@@ -189,6 +194,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
version: Literal["v2"],
) -> Iterator[StreamPartV2]: ...
@@ -380,6 +386,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> Run: ...
@overload
@@ -409,6 +416,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> Run: ...
def create(
@@ -642,6 +650,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> builtins.list[dict] | dict[str, Any]: ...
@overload
@@ -668,6 +677,7 @@ class SyncRunsClient:
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
durability: Durability | None = None,
) -> builtins.list[dict] | dict[str, Any]: ...
def wait(
@@ -815,7 +825,6 @@ class SyncRunsClient:
"checkpoint_during": checkpoint_during,
"on_completion": on_completion,
"after_seconds": after_seconds,
"raise_error": raise_error,
"durability": durability,
"langsmith_tracer": langsmith_tracing,
}
@@ -830,7 +839,7 @@ class SyncRunsClient:
if thread_id is not None
else "/runs/wait"
)
return self.http.request_reconnect(
response = self.http.request_reconnect(
endpoint,
"POST",
json={k: v for k, v in payload.items() if v is not None},
@@ -838,6 +847,16 @@ class SyncRunsClient:
headers=headers,
on_response=on_response if on_run_created else None,
)
if (
raise_error
and isinstance(response, dict)
and "__error__" in response
and isinstance(response["__error__"], dict)
):
raise Exception(
f"{response['__error__'].get('error')}: {response['__error__'].get('message')}"
)
return response
def list(
self,
+15 -4
View File
@@ -79,18 +79,27 @@ class SSEDecoder:
def __init__(self) -> None:
self._event = ""
self._data = bytearray()
self._last_event_id = ""
self._last_event_id: str | None = None
self._retry: int | None = None
self._at_stream_start = True
@property
def last_event_id(self) -> str | None:
"""Return the last event identifier that was seen."""
"""Return the last event identifier that was seen.
return self._last_event_id or None
`None` until the stream sends an `id` field, and `""` after an empty
one, which resets the reconnect cursor.
"""
return self._last_event_id
def decode(self, line: bytes) -> StreamPart | None:
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
if self._at_stream_start:
self._at_stream_start = False
line = line.removeprefix(b"\xef\xbb\xbf")
if not line:
if (
not self._event
@@ -103,7 +112,7 @@ class SSEDecoder:
sse = StreamPart(
event=self._event,
data=orjson.loads(self._data) if self._data else None, # ty: ignore[invalid-argument-type]
id=self.last_event_id,
id=self._last_event_id or None,
)
# NOTE: as per the SSE spec, do not reset last_event_id.
@@ -124,6 +133,8 @@ class SSEDecoder:
if fieldname == b"event":
self._event = value.decode()
elif fieldname == b"data":
if self._data:
self._data.extend(b"\n")
self._data.extend(value)
elif fieldname == b"id":
if b"\0" in value:
+44
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
import ast
import inspect
import re
from pathlib import Path
import pytest
@@ -128,3 +130,45 @@ def test_sync_api_matches_async(async_cls, sync_cls):
f"Return annotation mismatch for {async_cls.__name__}.{name}: "
f"async={a_ret}, sync={s_ret}"
)
# Stateless overloads (`thread_id: None`) leave out the thread-only parameters,
# and stateful ones (`thread_id: str`) leave out `on_completion`, which only
# applies to stateless runs.
_RUN_OVERLOAD_OMISSIONS = {
"None": {"checkpoint", "checkpoint_id", "multitask_strategy"},
"str": {"on_completion"},
}
@pytest.mark.parametrize("client_cls", [RunsClient, SyncRunsClient])
def test_run_overloads_accept_implementation_params(client_cls) -> None:
tree = ast.parse(Path(inspect.getfile(client_cls)).read_text())
cls = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == client_cls.__name__
)
defs = [
node
for node in cls.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name in {"stream", "create", "wait"}
]
impl_params = {
node.name: {arg.arg for arg in node.args.kwonlyargs}
for node in defs
if not node.decorator_list
}
gaps = []
for stub in defs:
if not stub.decorator_list:
continue
thread_id = next(arg for arg in stub.args.args if arg.arg == "thread_id")
assert thread_id.annotation is not None
kind = ast.unparse(thread_id.annotation)
expected = impl_params[stub.name] - _RUN_OVERLOAD_OMISSIONS[kind]
if missing := expected - {arg.arg for arg in stub.args.kwonlyargs}:
gaps.append(f"{stub.name}(thread_id: {kind}) line {stub.lineno}: {missing}")
assert not gaps, gaps
+66
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from typing import Any
import httpx
import orjson
import pytest
from typing_extensions import assert_type
@@ -110,6 +111,25 @@ def test_stream_sse():
assert len(parts) == 79
def test_sse_decoder_joins_data_lines_with_a_newline():
decoder = SSEDecoder()
for line in (b"data: [1,", b"data: 2]"):
decoder.decode(line)
assert decoder.decode(b"") == StreamPart(event="", data=[1, 2], id=None) # ty: ignore[invalid-argument-type]
for line in (b"data: [1", b"data: 0]"):
decoder.decode(line)
with pytest.raises(orjson.JSONDecodeError):
decoder.decode(b"")
def test_sse_decoder_strips_a_leading_bom():
decoder = SSEDecoder()
for line in (b"\xef\xbb\xbfevent: values", b"data: 1"):
decoder.decode(line)
assert decoder.decode(b"") == StreamPart(event="values", data=1, id=None) # ty: ignore[invalid-argument-type]
# --- HTTP client streaming ---
@@ -292,6 +312,52 @@ async def test_http_client_stream_recovers_after_disconnect():
]
def _drop_after_an_empty_id(stream_cls: type, reconnects: list[httpx.Request]):
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "POST":
return httpx.Response(
200,
headers={"Content-Type": "text/event-stream", "Location": "/reconnect"},
stream=stream_cls(
[
b"id: 1\nevent: values\ndata: 1\n\n",
b"id:\nevent: values\ndata: 2\n\n",
],
httpx.RemoteProtocolError("incomplete chunked read"),
),
)
reconnects.append(request)
return httpx.Response(
200,
headers={"Content-Type": "text/event-stream"},
stream=stream_cls([b"event: end\ndata: null\n\n"]),
)
return handler
def test_sync_stream_reconnects_without_a_cursor_after_an_empty_id():
reconnects: list[httpx.Request] = []
transport = httpx.MockTransport(_drop_after_an_empty_id(ListByteStream, reconnects))
with httpx.Client(transport=transport, base_url="https://example.com") as client:
list(SyncHttpClient(client).stream("/stream", "POST", json={}))
assert "last-event-id" not in reconnects[0].headers
async def test_async_stream_reconnects_without_a_cursor_after_an_empty_id():
reconnects: list[httpx.Request] = []
transport = httpx.MockTransport(
_drop_after_an_empty_id(AsyncListByteStream, reconnects)
)
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
[part async for part in HttpClient(client).stream("/stream", "POST", json={})]
assert "last-event-id" not in reconnects[0].headers
# --- _sse_to_v2_dict conversion ---
+119
View File
@@ -0,0 +1,119 @@
from __future__ import annotations
import json
from collections.abc import AsyncIterator, Iterator
import httpx
import pytest
from langgraph_sdk.client import (
HttpClient,
RunsClient,
SyncHttpClient,
SyncRunsClient,
)
class _BodyDropsMidRead(httpx.SyncByteStream, httpx.AsyncByteStream):
def __iter__(self) -> Iterator[bytes]:
yield b'{"partial'
raise httpx.ReadError("connection dropped")
async def __aiter__(self) -> AsyncIterator[bytes]:
yield b'{"partial'
raise httpx.ReadError("connection dropped")
def _wait_handler(requests: list[httpx.Request]):
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
if request.method == "POST":
return httpx.Response(
200,
headers={"Location": "/threads/t1/runs/r1/join"},
stream=_BodyDropsMidRead(),
)
return httpx.Response(200, json={"done": True})
return handler
def _assert_bodyless_get(request: httpx.Request) -> None:
assert request.method == "GET"
assert request.url.path == "/threads/t1/runs/r1/join"
assert "content-length" not in request.headers, request.headers
assert "content-type" not in request.headers, request.headers
assert request.read() == b""
def test_sync_wait_reconnects_with_a_bodyless_get():
requests: list[httpx.Request] = []
transport = httpx.MockTransport(_wait_handler(requests))
with httpx.Client(transport=transport, base_url="https://example.com") as client:
runs = SyncRunsClient(SyncHttpClient(client))
with pytest.warns(UserWarning, match="attempting reconnect"):
result = runs.wait("t1", "agent", input={"question": "hi"})
assert result == {"done": True}
_assert_bodyless_get(requests[1])
async def test_async_wait_reconnects_with_a_bodyless_get():
requests: list[httpx.Request] = []
transport = httpx.MockTransport(_wait_handler(requests))
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
runs = RunsClient(HttpClient(client))
with pytest.warns(UserWarning, match="attempting reconnect"):
result = await runs.wait("t1", "agent", input={"question": "hi"})
assert result == {"done": True}
_assert_bodyless_get(requests[1])
def test_sync_wait_raises_the_run_error():
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(
200, json={"__error__": {"error": "RuntimeError", "message": "boom"}}
)
transport = httpx.MockTransport(handler)
with httpx.Client(transport=transport, base_url="https://example.com") as client:
runs = SyncRunsClient(SyncHttpClient(client))
with pytest.raises(Exception, match="RuntimeError: boom"):
runs.wait("t1", "agent", input={}, raise_error=True)
assert "raise_error" not in json.loads(requests[0].content)
@pytest.mark.parametrize("asynchronous", [False, True])
async def test_wait_returns_the_run_error_without_raise_error(asynchronous):
requests: list[httpx.Request] = []
run_error = {"__error__": {"error": "RuntimeError", "message": "boom"}}
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, json=run_error)
transport = httpx.MockTransport(handler)
if asynchronous:
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
result = await RunsClient(HttpClient(client)).wait(
"t1", "agent", input={}, raise_error=False
)
else:
with httpx.Client(
transport=transport, base_url="https://example.com"
) as client:
result = SyncRunsClient(SyncHttpClient(client)).wait(
"t1", "agent", input={}, raise_error=False
)
assert result == run_error
assert "raise_error" not in json.loads(requests[0].content)
+1 -1
View File
@@ -301,7 +301,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.16"
version = "1.2.17"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },