From eb6acf9c8be9bde289a8030db1d448fc11766ae9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:25:32 -0700 Subject: [PATCH 01/12] chore(deps): bump soupsieve from 2.8.1 to 2.8.4 in /libs/langgraph (#8318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [soupsieve](https://github.com/facelessuser/soupsieve) from 2.8.1 to 2.8.4.
Release notes

Sourced from soupsieve's releases.

2.8.4

2.8.3

2.8.2

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=soupsieve&package-manager=uv&previous-version=2.8.1&new-version=2.8.4)](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) ---
Dependabot commands and options
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 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).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/langgraph/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 9b7a74776..23785f4f2 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -3401,11 +3401,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.1" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] From 80633b2c53901d56a5c81e9b92360594709ea124 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:40:50 -0700 Subject: [PATCH 02/12] chore(deps): bump mistune from 3.2.1 to 3.3.0 in /libs/langgraph (#8317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mistune](https://github.com/lepture/mistune) from 3.2.1 to 3.3.0.
Release notes

Sourced from mistune's releases.

v3.3.0

   🐞 Bug Fixes

   🏎 Performance

    View changes on GitHub
Changelog

Sourced from mistune's changelog.

Version 3.3.0

Released on Jun 21, 2026

  • Improve CommonMark compatibility and parser performance.
  • Add command line entrypoint with UTF-8 output.
  • Support display and backtick math.
  • Render plugin list and table nodes in Markdown renderer.
  • Escape leading block markers in Markdown renderer.
  • Fix RST renderer for block quotes nested in lists.
  • Avoid generated heading ID collisions in TOC.
  • Harden URL, image, figure, and include directive handling.
  • Fix quadratic scans in inline links, reference links, and formatting markers.
  • Fix math escaping, currency pattern matching, and cross-line matching.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mistune&package-manager=uv&previous-version=3.2.1&new-version=3.3.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) ---
Dependabot commands and options
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 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).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/langgraph/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 23785f4f2..338f806ee 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -2067,14 +2067,14 @@ wheels = [ [[package]] name = "mistune" -version = "3.2.1" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/9c/1939635275ec7258e2b43b00dafabc36d89ad11aa7838d375dc1b0e561cb/mistune-3.3.0.tar.gz", hash = "sha256:3074ec4c61b384abe725128e4dcbb483f5a09cc4632012505cdee655d3a113b9", size = 110936, upload-time = "2026-06-21T13:11:39.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/b7/76/b90f9d48d43fbd80a79a20d3eab2e5109859c7a56dc663b23187385898f3/mistune-3.3.0-py3-none-any.whl", hash = "sha256:a758e578acda49d8195f9a860b132dae2cf7bf409381393b1c4e6e489a65397b", size = 61250, upload-time = "2026-06-21T13:11:37.938Z" }, ] [[package]] From 49ae27c2ae983cfb92091b0dea9f7bc37a716479 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:55:09 +0000 Subject: [PATCH 03/12] chore(deps): bump websockets from 15.0.1 to 16.0 in /libs/sdk-py in the major group (#8253) Bumps the major group in /libs/sdk-py with 1 update: [websockets](https://github.com/python-websockets/websockets). Updates `websockets` from 15.0.1 to 16.0
Release notes

Sourced from websockets's releases.

16.0

See https://websockets.readthedocs.io/en/stable/project/changelog.html for details.

Commits
  • d4303a5 Release version 16.0.
  • 851bcd7 Bump pypa/cibuildwheel from 3.3.0 to 3.3.1
  • 740c8d3 Temporarily remove the trio implementation.
  • 92ea055 Add missing changelog entry.
  • ba74244 Document bug fix.
  • 9410483 Pin sphinx to avoid error in sphinxcontrib-trio.
  • 8e4d408 Document asyncio's TLS read buffer.
  • cb3500b Stop referring to the asyncio implementation as new.
  • 6563a9c The threading implementation supports max_queue.
  • 9f17e92 Clarify that protocol_mutex protects pending_pings.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=websockets&package-manager=uv&previous-version=15.0.1&new-version=16.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) ---
Dependabot commands and options
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 ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore 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 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 ` 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 ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Co-authored-by: open-swe[bot] --- libs/sdk-py/uv.lock | 115 ++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/libs/sdk-py/uv.lock b/libs/sdk-py/uv.lock index 786728ae4..bc15e1e7c 100644 --- a/libs/sdk-py/uv.lock +++ b/libs/sdk-py/uv.lock @@ -1233,61 +1233,70 @@ wheels = [ [[package]] name = "websockets" -version = "15.0.1" +version = "16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, - { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, - { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, - { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] From 9578140336b01a748d16ea154ae6278e155983f3 Mon Sep 17 00:00:00 2001 From: Saad Farooq Date: Mon, 20 Jul 2026 16:57:23 -0400 Subject: [PATCH 04/12] feat(checkpoint,checkpoint-postgres): add opt-in omit_expired to skip expired rows on read (#8354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Postgres store removes expired items only via the background TTL sweeper, so between sweeps a read can still return a logically expired row. Adds an opt-in flag so reads can filter expired rows at query time, closing the window without depending on sweep cadence. ## Changes - Add `omit_expired: bool` to `TTLConfig` (default `False`). When unset or `False`, behavior is unchanged - When enabled, inject `(expires_at IS NULL OR expires_at > NOW())` into the read query builders on `BasePostgresStore`, so `get`/`search`/`list_namespaces` do not surface an expired row: - **GET** (`_get_batch_GET_ops_queries`): predicate on both the final SELECT *and* the refresh `UPDATE` — the update is driven from an unfiltered key list, so gating only the SELECT would hide an expired row yet still refresh it back to life. - **SEARCH** (`_prepare_batch_search_queries`): inside the inner scans (before `LIMIT`/`OFFSET`), which also gates the refresh `UPDATE` fed by `search_results` and keeps pagination correct - **list_namespaces** (`_get_batch_list_namespaces_queries`): predicate appended to the existing scan conditions. ## Testing - Four behaviors covered sync + async, across the `default`/`pipe`/`pool` fixtures: expired-unswept row omitted from all read paths (with a raw SQL check proving the row is still physically present), default/explicit-`False` still returns it, `refresh_ttl=True` doesn't resurrect an expired row while still extending live ones, and search pagination stays correct when an expired row falls inside the page window. - Full `checkpoint-postgres` suite green on PG16 (210 passed); `ruff format`/`check` clean on both packages. --- .../langgraph/store/postgres/base.py | 29 +++- .../tests/test_async_store.py | 132 ++++++++++++++++++ libs/checkpoint-postgres/tests/test_store.py | 127 +++++++++++++++++ .../langgraph/store/base/__init__.py | 8 ++ 4 files changed, 293 insertions(+), 3 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py index 0d1cdc8dc..60e10eda5 100644 --- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py @@ -238,6 +238,12 @@ class BasePostgresStore(Generic[C]): conn: C _deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None index_config: PostgresIndexConfig | None + ttl_config: TTLConfig | None + + @property + def _omit_expired(self) -> bool: + """Whether expired-but-unswept rows should be filtered from reads.""" + return bool(self.ttl_config and self.ttl_config.get("omit_expired")) def _get_batch_GET_ops_queries( self, @@ -258,12 +264,17 @@ class BasePostgresStore(Generic[C]): namespace_groups[op.namespace].append((idx, op.key)) refresh_ttls[op.namespace].append(op.refresh_ttl) + omit_expired = self._omit_expired + expiry_clause = ( + "AND (s.expires_at IS NULL OR s.expires_at > NOW())" if omit_expired else "" + ) + results = [] for namespace, items in namespace_groups.items(): _, keys = zip(*items, strict=False) this_refresh_ttls = refresh_ttls[namespace] - query = """ + query = f""" WITH passed_in AS ( SELECT unnest(%s::text[]) AS key, unnest(%s::bool[]) AS do_refresh @@ -276,12 +287,14 @@ class BasePostgresStore(Generic[C]): AND s.key = p.key AND p.do_refresh = TRUE AND s.ttl_minutes IS NOT NULL + {expiry_clause} RETURNING s.key ) SELECT s.key, s.value, s.created_at, s.updated_at FROM store s JOIN passed_in p ON s.key = p.key WHERE s.prefix = %s + {expiry_clause} """ ns_text = _namespace_to_text(namespace) params = ( @@ -422,6 +435,13 @@ class BasePostgresStore(Generic[C]): - embedding_requests: list of (original_index_in_search_ops, text_query) """ + omit_expired = self._omit_expired + search_expiry_clause = ( + "AND (store.expires_at IS NULL OR store.expires_at > NOW())" + if omit_expired + else "" + ) + queries = [] embedding_requests = [] for idx, (_, op) in enumerate(search_ops): @@ -491,7 +511,7 @@ class BasePostgresStore(Generic[C]): {score_operator} AS neg_score FROM store JOIN store_vectors sv ON store.prefix = sv.prefix AND store.key = sv.key - WHERE {ns_condition} {extra_filters} + WHERE {ns_condition} {extra_filters} {search_expiry_clause} ORDER BY {score_operator} ASC LIMIT %s """ @@ -527,7 +547,7 @@ class BasePostgresStore(Generic[C]): base_query = f""" SELECT store.prefix, store.key, store.value, store.created_at, store.updated_at, NULL AS score FROM store - WHERE {ns_condition} {extra_filters} + WHERE {ns_condition} {extra_filters} {search_expiry_clause} ORDER BY store.updated_at DESC LIMIT %s OFFSET %s @@ -591,7 +611,10 @@ class BasePostgresStore(Generic[C]): """ params: list[Any] = [op.max_depth, op.max_depth] + omit_expired = self._omit_expired conditions = [] + if omit_expired: + conditions.append("(expires_at IS NULL OR expires_at > NOW())") if op.match_conditions: for condition in op.match_conditions: if condition.match_type == "prefix": diff --git a/libs/checkpoint-postgres/tests/test_async_store.py b/libs/checkpoint-postgres/tests/test_async_store.py index 97d3dfda3..080a78852 100644 --- a/libs/checkpoint-postgres/tests/test_async_store.py +++ b/libs/checkpoint-postgres/tests/test_async_store.py @@ -739,3 +739,135 @@ async def test_store_ttl(store): # Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2 results = await store.asearch(ns, query="bar", refresh_ttl=False) assert len(results) == 0 + + +async def _aexpire_now( + store: AsyncPostgresStore, ns: tuple[str, ...], key: str +) -> None: + """Backdate a row's expires_at into the past without deleting it (unswept).""" + async with store._cursor() as cur: + await cur.execute( + "UPDATE store SET expires_at = NOW() - INTERVAL '1 minute' " + "WHERE prefix = %s AND key = %s", + (".".join(ns), key), + ) + + +async def _arow_exists( + store: AsyncPostgresStore, ns: tuple[str, ...], key: str +) -> bool: + async with store._cursor() as cur: + await cur.execute( + "SELECT COUNT(*) AS n FROM store WHERE prefix = %s AND key = %s", + (".".join(ns), key), + ) + return (await cur.fetchone())["n"] == 1 + + +async def _astored_expires_at(store: AsyncPostgresStore, ns: tuple[str, ...], key: str): + async with store._cursor() as cur: + await cur.execute( + "SELECT expires_at FROM store WHERE prefix = %s AND key = %s", + (".".join(ns), key), + ) + return (await cur.fetchone())["expires_at"] + + +async def test_omit_expired_filters_read_paths(store: AsyncPostgresStore) -> None: + await store.stop_ttl_sweeper() # deterministic: no background deletion + store.ttl_config["omit_expired"] = True + + expired_ns = ("omit", "expired") + control_ns = ("omit", "control") + await store.aput(expired_ns, "e", {"data": "gone"}, ttl=TTL_MINUTES) + await store.aput(control_ns, "c", {"data": "keep"}, ttl=None) + await _aexpire_now(store, expired_ns, "e") + + # The row is expired but physically still present (unswept). + assert await _arow_exists(store, expired_ns, "e") + + # aget omits it; the never-expiring control is still returned. + assert await store.aget(expired_ns, "e") is None + assert await store.aget(control_ns, "c") is not None + + # asearch omits it but returns the control. + assert await store.asearch(expired_ns) == [] + assert [i.key for i in await store.asearch(control_ns)] == ["c"] + + # alist_namespaces drops the expired-only namespace, keeps the control. + namespaces = await store.alist_namespaces(prefix=("omit",)) + assert expired_ns not in namespaces + assert control_ns in namespaces + + +@pytest.mark.parametrize("omit", [None, False], ids=["default", "explicit-false"]) +async def test_omit_expired_disabled_preserves_expired_rows( + store: AsyncPostgresStore, omit +) -> None: + await store.stop_ttl_sweeper() + if omit is not None: + store.ttl_config["omit_expired"] = omit + + ns = ("keep",) + await store.aput(ns, "k", {"data": "still-here"}, ttl=TTL_MINUTES) + await _aexpire_now(store, ns, "k") + + assert await store.aget(ns, "k", refresh_ttl=False) is not None + assert [i.key for i in await store.asearch(ns, refresh_ttl=False)] == ["k"] + assert ns in await store.alist_namespaces(prefix=("keep",)) + + +async def test_omit_expired_refresh_ttl_only_refreshes_live_rows( + store: AsyncPostgresStore, +) -> None: + await store.stop_ttl_sweeper() + store.ttl_config["omit_expired"] = True + + ns = ("refresh",) + await store.aput(ns, "expired", {"n": 0}, ttl=TTL_MINUTES) + await store.aput(ns, "live_get", {"n": 1}, ttl=TTL_MINUTES) + await store.aput(ns, "live_search", {"n": 2}, ttl=TTL_MINUTES) + await _aexpire_now(store, ns, "expired") + + expired_before = await _astored_expires_at(store, ns, "expired") + get_before = await _astored_expires_at(store, ns, "live_get") + search_before = await _astored_expires_at(store, ns, "live_search") + + # refresh_ttl=True must NOT resurrect the expired row (via aget or asearch)... + assert await store.aget(ns, "expired", refresh_ttl=True) is None + live_keys = [i.key for i in await store.asearch(ns, refresh_ttl=True)] + assert "expired" not in live_keys + assert await _astored_expires_at(store, ns, "expired") == expired_before + + # ...but must still extend the live rows that were read. + assert await store.aget(ns, "live_get", refresh_ttl=True) is not None + assert await _astored_expires_at(store, ns, "live_get") > get_before + assert await _astored_expires_at(store, ns, "live_search") > search_before + + +async def test_omit_expired_search_pagination(store: AsyncPostgresStore) -> None: + await store.stop_ttl_sweeper() + store.ttl_config["omit_expired"] = True + + ns = ("page",) + for k in ("a", "b", "c"): + await store.aput(ns, k, {"k": k}, ttl=TTL_MINUTES) + await store.aput(ns, "expired", {"k": "x"}, ttl=TTL_MINUTES) + await _aexpire_now(store, ns, "expired") + + seconds_ago = {"a": 1, "expired": 2, "b": 3, "c": 4} + # updated_at DESC orders these a, expired, b, c, so the expired row sits inside + # the first limit=2 window. Correct (pre-LIMIT) filtering yields live pages + # [a, b] then [c]; post-LIMIT filtering would underfill page 1 to just [a]. + async with store._cursor() as cur: + for key, secs in seconds_ago.items(): + await cur.execute( + "UPDATE store SET updated_at = NOW() - (%s * INTERVAL '1 second') " + "WHERE prefix = %s AND key = %s", + (secs, ".".join(ns), key), + ) + + page1 = await store.asearch(ns, limit=2, offset=0) + page2 = await store.asearch(ns, limit=2, offset=2) + assert [i.key for i in page1] == ["a", "b"] + assert [i.key for i in page2] == ["c"] diff --git a/libs/checkpoint-postgres/tests/test_store.py b/libs/checkpoint-postgres/tests/test_store.py index ac8d16558..ad5c07421 100644 --- a/libs/checkpoint-postgres/tests/test_store.py +++ b/libs/checkpoint-postgres/tests/test_store.py @@ -863,6 +863,133 @@ def test_store_ttl(store): assert len(res) == 0 +def _expire_now(store: PostgresStore, ns: tuple[str, ...], key: str) -> None: + """Backdate a row's expires_at into the past without deleting it (unswept).""" + with store._cursor() as cur: + cur.execute( + "UPDATE store SET expires_at = NOW() - INTERVAL '1 minute' " + "WHERE prefix = %s AND key = %s", + (".".join(ns), key), + ) + + +def _row_exists(store: PostgresStore, ns: tuple[str, ...], key: str) -> bool: + with store._cursor() as cur: + cur.execute( + "SELECT COUNT(*) AS n FROM store WHERE prefix = %s AND key = %s", + (".".join(ns), key), + ) + return cur.fetchone()["n"] == 1 + + +def _stored_expires_at(store: PostgresStore, ns: tuple[str, ...], key: str): + with store._cursor() as cur: + cur.execute( + "SELECT expires_at FROM store WHERE prefix = %s AND key = %s", + (".".join(ns), key), + ) + return cur.fetchone()["expires_at"] + + +def test_omit_expired_filters_read_paths(store: PostgresStore) -> None: + store.stop_ttl_sweeper() # deterministic: no background deletion + store.ttl_config["omit_expired"] = True + + expired_ns = ("omit", "expired") + control_ns = ("omit", "control") + store.put(expired_ns, "e", {"data": "gone"}, ttl=TTL_MINUTES) + store.put(control_ns, "c", {"data": "keep"}, ttl=None) + _expire_now(store, expired_ns, "e") + + # The row is expired but physically still present (unswept). + assert _row_exists(store, expired_ns, "e") + + # get omits it; the never-expiring control is still returned. + assert store.get(expired_ns, "e") is None + assert store.get(control_ns, "c") is not None + + # search omits it but returns the control. + assert store.search(expired_ns) == [] + assert [i.key for i in store.search(control_ns)] == ["c"] + + # list_namespaces drops the expired-only namespace, keeps the control. + namespaces = store.list_namespaces(prefix=("omit",)) + assert expired_ns not in namespaces + assert control_ns in namespaces + + +@pytest.mark.parametrize("omit", [None, False], ids=["default", "explicit-false"]) +def test_omit_expired_disabled_preserves_expired_rows( + store: PostgresStore, omit +) -> None: + store.stop_ttl_sweeper() + if omit is not None: + store.ttl_config["omit_expired"] = omit + + ns = ("keep",) + store.put(ns, "k", {"data": "still-here"}, ttl=TTL_MINUTES) + _expire_now(store, ns, "k") + + assert store.get(ns, "k", refresh_ttl=False) is not None + assert [i.key for i in store.search(ns, refresh_ttl=False)] == ["k"] + assert ns in store.list_namespaces(prefix=("keep",)) + + +def test_omit_expired_refresh_ttl_only_refreshes_live_rows( + store: PostgresStore, +) -> None: + store.stop_ttl_sweeper() + store.ttl_config["omit_expired"] = True + + ns = ("refresh",) + store.put(ns, "expired", {"n": 0}, ttl=TTL_MINUTES) + store.put(ns, "live_get", {"n": 1}, ttl=TTL_MINUTES) + store.put(ns, "live_search", {"n": 2}, ttl=TTL_MINUTES) + _expire_now(store, ns, "expired") + + expired_before = _stored_expires_at(store, ns, "expired") + get_before = _stored_expires_at(store, ns, "live_get") + search_before = _stored_expires_at(store, ns, "live_search") + + # refresh_ttl=True must NOT resurrect the expired row (via get or search)... + assert store.get(ns, "expired", refresh_ttl=True) is None + assert "expired" not in [i.key for i in store.search(ns, refresh_ttl=True)] + assert _stored_expires_at(store, ns, "expired") == expired_before + + # ...but must still extend the live rows that were read. + assert store.get(ns, "live_get", refresh_ttl=True) is not None + assert _stored_expires_at(store, ns, "live_get") > get_before + assert _stored_expires_at(store, ns, "live_search") > search_before + + +def test_omit_expired_search_pagination(store: PostgresStore) -> None: + store.stop_ttl_sweeper() + store.ttl_config["omit_expired"] = True + + ns = ("page",) + for k in ("a", "b", "c"): + store.put(ns, k, {"k": k}, ttl=TTL_MINUTES) + store.put(ns, "expired", {"k": "x"}, ttl=TTL_MINUTES) + _expire_now(store, ns, "expired") + + seconds_ago = {"a": 1, "expired": 2, "b": 3, "c": 4} + # updated_at DESC orders these a, expired, b, c, so the expired row sits inside + # the first limit=2 window. Correct (pre-LIMIT) filtering yields live pages + # [a, b] then [c]; post-LIMIT filtering would underfill page 1 to just [a]. + with store._cursor() as cur: + for key, secs in seconds_ago.items(): + cur.execute( + "UPDATE store SET updated_at = NOW() - (%s * INTERVAL '1 second') " + "WHERE prefix = %s AND key = %s", + (secs, ".".join(ns), key), + ) + + page1 = store.search(ns, limit=2, offset=0) + page2 = store.search(ns, limit=2, offset=2) + assert [i.key for i in page1] == ["a", "b"] + assert [i.key for i in page2] == ["c"] + + @pytest.mark.parametrize( "vector_type,distance_type", [ diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py index 5b04fb51d..08fe74525 100644 --- a/libs/checkpoint/langgraph/store/base/__init__.py +++ b/libs/checkpoint/langgraph/store/base/__init__.py @@ -552,6 +552,14 @@ class TTLConfig(TypedDict, total=False): This can be overridden per-operation by explicitly setting `refresh_ttl`. Defaults to `True` if not configured. """ + omit_expired: bool + """Whether to omit expired items from read operations. + + If `True`, and if the store supports this option, expired items will not be + returned by `GET` or `SEARCH`, or included in namespace listings, even before + a TTL sweep deletes them. + Defaults to `False` if not configured. + """ default_ttl: float | None """Default TTL (time-to-live) in minutes for new items. From 8b39db3875f24151286dee9f21f937db1756da09 Mon Sep 17 00:00:00 2001 From: ccurme Date: Tue, 21 Jul 2026 15:04:04 -0400 Subject: [PATCH 05/12] feat(langgraph): expose `trace_policy` on `add_node` (#8362) --- libs/langgraph/langgraph/graph/_node.py | 10 +- libs/langgraph/langgraph/graph/state.py | 17 ++++ libs/langgraph/langgraph/pregel/_read.py | 12 ++- libs/langgraph/langgraph/types.py | 26 +++++- libs/langgraph/tests/test_trace_policy.py | 109 ++++++++++++++++++++++ 5 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 libs/langgraph/tests/test_trace_policy.py diff --git a/libs/langgraph/langgraph/graph/_node.py b/libs/langgraph/langgraph/graph/_node.py index d8238c2b3..4cb2676a9 100644 --- a/libs/langgraph/langgraph/graph/_node.py +++ b/libs/langgraph/langgraph/graph/_node.py @@ -9,7 +9,13 @@ from langgraph.store.base import BaseStore from langgraph._internal._typing import EMPTY_SEQ from langgraph.runtime import Runtime -from langgraph.types import CachePolicy, RetryPolicy, StreamWriter, TimeoutPolicy +from langgraph.types import ( + CachePolicy, + RetryPolicy, + StreamWriter, + TimeoutPolicy, + TracePolicy, +) from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra @@ -93,3 +99,5 @@ class StateNodeSpec(Generic[NodeInputT, ContextT]): ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ defer: bool = False timeout: TimeoutPolicy | None = None + trace_policy: TracePolicy | None = None + """Optional policy controlling what this node records on its trace run.""" diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index deb0e9e94..297db4989 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -85,6 +85,7 @@ from langgraph.types import ( RetryPolicy, Send, TimeoutPolicy, + TracePolicy, ensure_valid_checkpointer, ) from langgraph.typing import ContextT, InputT, NodeInputT, OutputT, StateT @@ -384,6 +385,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, + trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`, input schema is inferred as the state schema. @@ -453,6 +455,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, + trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph` where input schema is specified. @@ -527,6 +530,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, + trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`, input schema is inferred as the state schema. @@ -596,6 +600,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, + trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`, input schema is specified. @@ -672,6 +677,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, + trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`. @@ -691,6 +697,11 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): If a sequence is provided, the first matching policy will be applied. cache_policy: The cache policy for the node. error_handler: Optional node-level error handler callable for this node. + trace_policy: Optional policy controlling how this node's run is traced. Its + `process_inputs` callable transforms the node's input before it is + recorded (e.g. to omit or summarize large message history) without + changing the value passed to the node, and `tags` attaches tags to the + run (e.g. to hide it from the trace tree). Does not affect execution. destinations: Destinations that indicate where a node can route to. Useful for edgeless graphs with nodes that return `Command` objects. @@ -880,6 +891,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): ends=ends, defer=defer, timeout=timeout, + trace_policy=trace_policy, ) elif inferred_input_schema is not None: self.nodes[node] = StateNodeSpec( @@ -892,6 +904,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): ends=ends, defer=defer, timeout=timeout, + trace_policy=trace_policy, ) else: self.nodes[node] = StateNodeSpec[StateT, ContextT]( @@ -904,6 +917,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): ends=ends, defer=defer, timeout=timeout, + trace_policy=trace_policy, ) input_schema = input_schema or inferred_input_schema @@ -1515,6 +1529,7 @@ class CompiledStateGraph( if node.defer else EphemeralValue(Any, guard=False) ) + trace_tags = node.trace_policy.tags if node.trace_policy else None self.nodes[key] = PregelNode( triggers=[branch_channel], # read state keys and managed values @@ -1523,6 +1538,7 @@ class CompiledStateGraph( mapper=mapper, # publish to state keys writers=[ChannelWrite(write_entries)], + tags=list(trace_tags) if trace_tags else None, metadata=node.metadata, retry_policy=node.retry_policy, cache_policy=node.cache_policy, @@ -1530,6 +1546,7 @@ class CompiledStateGraph( error_handler_node=node.error_handler_node, bound=node.runnable, # type: ignore[arg-type] timeout=node.timeout, + trace_policy=node.trace_policy, ) else: raise RuntimeError diff --git a/libs/langgraph/langgraph/pregel/_read.py b/libs/langgraph/langgraph/pregel/_read.py index 7d49fce85..d5c30adf6 100644 --- a/libs/langgraph/langgraph/pregel/_read.py +++ b/libs/langgraph/langgraph/pregel/_read.py @@ -16,7 +16,7 @@ from langgraph._internal._timeout import coerce_timeout_policy from langgraph.pregel._utils import find_subgraph_pregel from langgraph.pregel._write import ChannelWrite from langgraph.pregel.protocol import PregelProtocol -from langgraph.types import CachePolicy, RetryPolicy, TimeoutPolicy +from langgraph.types import CachePolicy, RetryPolicy, TimeoutPolicy, TracePolicy READ_TYPE = Callable[[str | Sequence[str], bool], Any | dict[str, Any]] INPUT_CACHE_KEY_TYPE = tuple[Callable[..., Any], tuple[str, ...]] @@ -138,6 +138,9 @@ class PregelNode: metadata: Mapping[str, Any] | None """Metadata to attach to the node for tracing.""" + trace_policy: TracePolicy | None + """Optional policy controlling what this node records on its trace run.""" + is_error_handler: bool """Whether this node is registered as an error handler node.""" @@ -156,6 +159,7 @@ class PregelNode: writers: list[Runnable] | None = None, tags: list[str] | None = None, metadata: Mapping[str, Any] | None = None, + trace_policy: TracePolicy | None = None, bound: Runnable[Any, Any] | None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, cache_policy: CachePolicy | None = None, @@ -177,6 +181,7 @@ class PregelNode: self.timeout = coerce_timeout_policy(timeout) self.tags = tags self.metadata = metadata + self.trace_policy = trace_policy self.is_error_handler = is_error_handler self.error_handler_node = error_handler_node if subgraphs is not None: @@ -222,14 +227,15 @@ class PregelNode: def node(self) -> Runnable[Any, Any] | None: """Get a runnable that combines `bound` and `writers`.""" writers = self.flat_writers + trace_inputs = self.trace_policy.process_inputs if self.trace_policy else None if self.bound is DEFAULT_BOUND and not writers: return None elif self.bound is DEFAULT_BOUND and len(writers) == 1: return writers[0] elif self.bound is DEFAULT_BOUND: - return RunnableSeq(*writers) + return RunnableSeq(*writers, trace_inputs=trace_inputs) elif writers: - return RunnableSeq(self.bound, *writers) + return RunnableSeq(self.bound, *writers, trace_inputs=trace_inputs) else: return self.bound diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index ac9aa9b00..e79ecfb3d 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys from collections import deque from collections.abc import Callable, Hashable, Sequence -from dataclasses import asdict, dataclass +from dataclasses import asdict, dataclass, field from datetime import timedelta from typing import ( TYPE_CHECKING, @@ -70,6 +70,7 @@ __all__ = ( "RetryPolicy", "TimeoutPolicy", "CachePolicy", + "TracePolicy", "Interrupt", "StateUpdate", "PregelTask", @@ -527,6 +528,29 @@ class CachePolicy(Generic[KeyFuncT]): """Time to live for the cache entry in seconds. If `None`, the entry never expires.""" +@dataclass(**_DC_KWARGS) +class TracePolicy: + """Configuration for how a node's run is traced. + + Scope: this only transforms what the node's *own* run records. Child runs created + by a traced `bound` runnable (e.g. a `RunnableLambda`, a chain, or a `prompt | model`) + and the root graph run are not affected. Plain function nodes are traced with + `trace=False`, so they have no such child runs. + + This is a trace-payload/latency tool, not a secret-redaction control. To redact + inputs/outputs across all runs (children included), use the LangSmith client's + `hide_inputs`/`hide_outputs`/`anonymizer` instead. + """ + + process_inputs: Callable[[Any], Any] | None = None + """Optional callable to transform the node's input before it is recorded on the + node's trace run. Use to omit or summarize large payloads (e.g. message history). + Does not affect the value passed to the node.""" + + tags: list[str] = field(default_factory=list) + """Tags to attach to this node's trace run.""" + + _DEFAULT_INTERRUPT_ID = "placeholder-id" diff --git a/libs/langgraph/tests/test_trace_policy.py b/libs/langgraph/tests/test_trace_policy.py new file mode 100644 index 000000000..e04dcc9e5 --- /dev/null +++ b/libs/langgraph/tests/test_trace_policy.py @@ -0,0 +1,109 @@ +"""End-to-end tests for `TracePolicy` (input processing + hidden tag) on node runs.""" + +from typing import Any + +import pytest +from typing_extensions import TypedDict + +from langgraph.graph import END, START, StateGraph +from langgraph.types import TracePolicy +from tests.fake_tracer import FakeTracer, Run + + +class State(TypedDict): + value: int + + +def _node_run(tracer: FakeTracer, name: str) -> Run: + return next(r for r in tracer.flattened_runs() if r.name == name) + + +def _incr(state: State) -> State: + return {"value": state["value"] + 1} + + +def test_trace_policy_transforms_recorded_inputs() -> None: + seen: dict[str, Any] = {} + + def process_inputs(inp: Any) -> Any: + seen["inputs"] = inp + return {"scrubbed_in": True} + + graph = ( + StateGraph(State) + .add_node("n", _incr, trace_policy=TracePolicy(process_inputs=process_inputs)) + .add_edge(START, "n") + .add_edge("n", END) + .compile() + ) + + tracer = FakeTracer() + # the real graph output is unaffected by the trace policy + assert graph.invoke({"value": 1}, {"callbacks": [tracer]}) == {"value": 2} + + run = _node_run(tracer, "n") + # the recorded input is transformed; the output is recorded as-is + assert run.inputs == {"scrubbed_in": True} + assert run.outputs == {"value": 2} + # process_inputs observed the real, untransformed input + assert seen["inputs"] == {"value": 1} + + +def test_trace_policy_none_records_real_payloads() -> None: + graph = ( + StateGraph(State) + .add_node("n", _incr) + .add_edge(START, "n") + .add_edge("n", END) + .compile() + ) + + tracer = FakeTracer() + assert graph.invoke({"value": 1}, {"callbacks": [tracer]}) == {"value": 2} + + run = _node_run(tracer, "n") + assert run.inputs == {"value": 1} + assert run.outputs == {"value": 2} + + +@pytest.mark.anyio +async def test_trace_policy_transforms_recorded_inputs_async() -> None: + graph = ( + StateGraph(State) + .add_node( + "n", + _incr, + trace_policy=TracePolicy(process_inputs=lambda _: {"scrubbed_in": True}), + ) + .add_edge(START, "n") + .add_edge("n", END) + .compile() + ) + + tracer = FakeTracer() + assert await graph.ainvoke({"value": 5}, {"callbacks": [tracer]}) == {"value": 6} + + run = _node_run(tracer, "n") + assert run.inputs == {"scrubbed_in": True} + assert run.outputs == {"value": 6} + + +def test_trace_policy_tags_applied_to_run() -> None: + graph = ( + StateGraph(State) + .add_node("shown", _incr) + .add_node( + "hush", _incr, trace_policy=TracePolicy(tags=["langsmith:hidden_middleware"]) + ) + .add_edge(START, "shown") + .add_edge("shown", "hush") + .add_edge("hush", END) + .compile() + ) + + tracer = FakeTracer() + graph.invoke({"value": 0}, {"callbacks": [tracer]}) + + # the policy's tags land on the node's run; other nodes are unaffected + assert "langsmith:hidden_middleware" in _node_run(tracer, "hush").tags + assert "langsmith:hidden_middleware" not in _node_run(tracer, "shown").tags From f02c0f0ce575ce1d3879b09ee3c534446c31a983 Mon Sep 17 00:00:00 2001 From: ccurme Date: Tue, 21 Jul 2026 15:57:18 -0400 Subject: [PATCH 06/12] feat(langgraph): drop tags from TracePolicy (#8402) --- libs/langgraph/langgraph/graph/state.py | 5 +---- libs/langgraph/langgraph/types.py | 5 +---- libs/langgraph/tests/test_trace_policy.py | 23 +---------------------- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 297db4989..0e0df9157 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -700,8 +700,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): trace_policy: Optional policy controlling how this node's run is traced. Its `process_inputs` callable transforms the node's input before it is recorded (e.g. to omit or summarize large message history) without - changing the value passed to the node, and `tags` attaches tags to the - run (e.g. to hide it from the trace tree). Does not affect execution. + changing the value passed to the node. Does not affect execution. destinations: Destinations that indicate where a node can route to. Useful for edgeless graphs with nodes that return `Command` objects. @@ -1529,7 +1528,6 @@ class CompiledStateGraph( if node.defer else EphemeralValue(Any, guard=False) ) - trace_tags = node.trace_policy.tags if node.trace_policy else None self.nodes[key] = PregelNode( triggers=[branch_channel], # read state keys and managed values @@ -1538,7 +1536,6 @@ class CompiledStateGraph( mapper=mapper, # publish to state keys writers=[ChannelWrite(write_entries)], - tags=list(trace_tags) if trace_tags else None, metadata=node.metadata, retry_policy=node.retry_policy, cache_policy=node.cache_policy, diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index e79ecfb3d..8175e47e9 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys from collections import deque from collections.abc import Callable, Hashable, Sequence -from dataclasses import asdict, dataclass, field +from dataclasses import asdict, dataclass from datetime import timedelta from typing import ( TYPE_CHECKING, @@ -547,9 +547,6 @@ class TracePolicy: node's trace run. Use to omit or summarize large payloads (e.g. message history). Does not affect the value passed to the node.""" - tags: list[str] = field(default_factory=list) - """Tags to attach to this node's trace run.""" - _DEFAULT_INTERRUPT_ID = "placeholder-id" diff --git a/libs/langgraph/tests/test_trace_policy.py b/libs/langgraph/tests/test_trace_policy.py index e04dcc9e5..e72d0299c 100644 --- a/libs/langgraph/tests/test_trace_policy.py +++ b/libs/langgraph/tests/test_trace_policy.py @@ -1,4 +1,4 @@ -"""End-to-end tests for `TracePolicy` (input processing + hidden tag) on node runs.""" +"""End-to-end tests for `TracePolicy` input processing on node trace runs.""" from typing import Any @@ -86,24 +86,3 @@ async def test_trace_policy_transforms_recorded_inputs_async() -> None: run = _node_run(tracer, "n") assert run.inputs == {"scrubbed_in": True} assert run.outputs == {"value": 6} - - -def test_trace_policy_tags_applied_to_run() -> None: - graph = ( - StateGraph(State) - .add_node("shown", _incr) - .add_node( - "hush", _incr, trace_policy=TracePolicy(tags=["langsmith:hidden_middleware"]) - ) - .add_edge(START, "shown") - .add_edge("shown", "hush") - .add_edge("hush", END) - .compile() - ) - - tracer = FakeTracer() - graph.invoke({"value": 0}, {"callbacks": [tracer]}) - - # the policy's tags land on the node's run; other nodes are unaffected - assert "langsmith:hidden_middleware" in _node_run(tracer, "hush").tags - assert "langsmith:hidden_middleware" not in _node_run(tracer, "shown").tags From 31f90df3e6b0268fa77fd2d118a917d420b84a68 Mon Sep 17 00:00:00 2001 From: ccurme Date: Tue, 21 Jul 2026 17:09:16 -0400 Subject: [PATCH 07/12] revert(langgraph): delete TracePolicy (#8403) --- libs/langgraph/langgraph/graph/_node.py | 10 +-- libs/langgraph/langgraph/graph/state.py | 14 ---- libs/langgraph/langgraph/pregel/_read.py | 12 +--- libs/langgraph/langgraph/types.py | 21 ------ libs/langgraph/tests/test_trace_policy.py | 88 ----------------------- 5 files changed, 4 insertions(+), 141 deletions(-) delete mode 100644 libs/langgraph/tests/test_trace_policy.py diff --git a/libs/langgraph/langgraph/graph/_node.py b/libs/langgraph/langgraph/graph/_node.py index 4cb2676a9..d8238c2b3 100644 --- a/libs/langgraph/langgraph/graph/_node.py +++ b/libs/langgraph/langgraph/graph/_node.py @@ -9,13 +9,7 @@ from langgraph.store.base import BaseStore from langgraph._internal._typing import EMPTY_SEQ from langgraph.runtime import Runtime -from langgraph.types import ( - CachePolicy, - RetryPolicy, - StreamWriter, - TimeoutPolicy, - TracePolicy, -) +from langgraph.types import CachePolicy, RetryPolicy, StreamWriter, TimeoutPolicy from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra @@ -99,5 +93,3 @@ class StateNodeSpec(Generic[NodeInputT, ContextT]): ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ defer: bool = False timeout: TimeoutPolicy | None = None - trace_policy: TracePolicy | None = None - """Optional policy controlling what this node records on its trace run.""" diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 0e0df9157..deb0e9e94 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -85,7 +85,6 @@ from langgraph.types import ( RetryPolicy, Send, TimeoutPolicy, - TracePolicy, ensure_valid_checkpointer, ) from langgraph.typing import ContextT, InputT, NodeInputT, OutputT, StateT @@ -385,7 +384,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, - trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`, input schema is inferred as the state schema. @@ -455,7 +453,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, - trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph` where input schema is specified. @@ -530,7 +527,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, - trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`, input schema is inferred as the state schema. @@ -600,7 +596,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, - trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`, input schema is specified. @@ -677,7 +672,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): error_handler: StateNode[Any, ContextT] | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, timeout: float | timedelta | TimeoutPolicy | None = None, - trace_policy: TracePolicy | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: """Add a new node to the `StateGraph`. @@ -697,10 +691,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): If a sequence is provided, the first matching policy will be applied. cache_policy: The cache policy for the node. error_handler: Optional node-level error handler callable for this node. - trace_policy: Optional policy controlling how this node's run is traced. Its - `process_inputs` callable transforms the node's input before it is - recorded (e.g. to omit or summarize large message history) without - changing the value passed to the node. Does not affect execution. destinations: Destinations that indicate where a node can route to. Useful for edgeless graphs with nodes that return `Command` objects. @@ -890,7 +880,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): ends=ends, defer=defer, timeout=timeout, - trace_policy=trace_policy, ) elif inferred_input_schema is not None: self.nodes[node] = StateNodeSpec( @@ -903,7 +892,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): ends=ends, defer=defer, timeout=timeout, - trace_policy=trace_policy, ) else: self.nodes[node] = StateNodeSpec[StateT, ContextT]( @@ -916,7 +904,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): ends=ends, defer=defer, timeout=timeout, - trace_policy=trace_policy, ) input_schema = input_schema or inferred_input_schema @@ -1543,7 +1530,6 @@ class CompiledStateGraph( error_handler_node=node.error_handler_node, bound=node.runnable, # type: ignore[arg-type] timeout=node.timeout, - trace_policy=node.trace_policy, ) else: raise RuntimeError diff --git a/libs/langgraph/langgraph/pregel/_read.py b/libs/langgraph/langgraph/pregel/_read.py index d5c30adf6..7d49fce85 100644 --- a/libs/langgraph/langgraph/pregel/_read.py +++ b/libs/langgraph/langgraph/pregel/_read.py @@ -16,7 +16,7 @@ from langgraph._internal._timeout import coerce_timeout_policy from langgraph.pregel._utils import find_subgraph_pregel from langgraph.pregel._write import ChannelWrite from langgraph.pregel.protocol import PregelProtocol -from langgraph.types import CachePolicy, RetryPolicy, TimeoutPolicy, TracePolicy +from langgraph.types import CachePolicy, RetryPolicy, TimeoutPolicy READ_TYPE = Callable[[str | Sequence[str], bool], Any | dict[str, Any]] INPUT_CACHE_KEY_TYPE = tuple[Callable[..., Any], tuple[str, ...]] @@ -138,9 +138,6 @@ class PregelNode: metadata: Mapping[str, Any] | None """Metadata to attach to the node for tracing.""" - trace_policy: TracePolicy | None - """Optional policy controlling what this node records on its trace run.""" - is_error_handler: bool """Whether this node is registered as an error handler node.""" @@ -159,7 +156,6 @@ class PregelNode: writers: list[Runnable] | None = None, tags: list[str] | None = None, metadata: Mapping[str, Any] | None = None, - trace_policy: TracePolicy | None = None, bound: Runnable[Any, Any] | None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, cache_policy: CachePolicy | None = None, @@ -181,7 +177,6 @@ class PregelNode: self.timeout = coerce_timeout_policy(timeout) self.tags = tags self.metadata = metadata - self.trace_policy = trace_policy self.is_error_handler = is_error_handler self.error_handler_node = error_handler_node if subgraphs is not None: @@ -227,15 +222,14 @@ class PregelNode: def node(self) -> Runnable[Any, Any] | None: """Get a runnable that combines `bound` and `writers`.""" writers = self.flat_writers - trace_inputs = self.trace_policy.process_inputs if self.trace_policy else None if self.bound is DEFAULT_BOUND and not writers: return None elif self.bound is DEFAULT_BOUND and len(writers) == 1: return writers[0] elif self.bound is DEFAULT_BOUND: - return RunnableSeq(*writers, trace_inputs=trace_inputs) + return RunnableSeq(*writers) elif writers: - return RunnableSeq(self.bound, *writers, trace_inputs=trace_inputs) + return RunnableSeq(self.bound, *writers) else: return self.bound diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index 8175e47e9..ac9aa9b00 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -70,7 +70,6 @@ __all__ = ( "RetryPolicy", "TimeoutPolicy", "CachePolicy", - "TracePolicy", "Interrupt", "StateUpdate", "PregelTask", @@ -528,26 +527,6 @@ class CachePolicy(Generic[KeyFuncT]): """Time to live for the cache entry in seconds. If `None`, the entry never expires.""" -@dataclass(**_DC_KWARGS) -class TracePolicy: - """Configuration for how a node's run is traced. - - Scope: this only transforms what the node's *own* run records. Child runs created - by a traced `bound` runnable (e.g. a `RunnableLambda`, a chain, or a `prompt | model`) - and the root graph run are not affected. Plain function nodes are traced with - `trace=False`, so they have no such child runs. - - This is a trace-payload/latency tool, not a secret-redaction control. To redact - inputs/outputs across all runs (children included), use the LangSmith client's - `hide_inputs`/`hide_outputs`/`anonymizer` instead. - """ - - process_inputs: Callable[[Any], Any] | None = None - """Optional callable to transform the node's input before it is recorded on the - node's trace run. Use to omit or summarize large payloads (e.g. message history). - Does not affect the value passed to the node.""" - - _DEFAULT_INTERRUPT_ID = "placeholder-id" diff --git a/libs/langgraph/tests/test_trace_policy.py b/libs/langgraph/tests/test_trace_policy.py deleted file mode 100644 index e72d0299c..000000000 --- a/libs/langgraph/tests/test_trace_policy.py +++ /dev/null @@ -1,88 +0,0 @@ -"""End-to-end tests for `TracePolicy` input processing on node trace runs.""" - -from typing import Any - -import pytest -from typing_extensions import TypedDict - -from langgraph.graph import END, START, StateGraph -from langgraph.types import TracePolicy -from tests.fake_tracer import FakeTracer, Run - - -class State(TypedDict): - value: int - - -def _node_run(tracer: FakeTracer, name: str) -> Run: - return next(r for r in tracer.flattened_runs() if r.name == name) - - -def _incr(state: State) -> State: - return {"value": state["value"] + 1} - - -def test_trace_policy_transforms_recorded_inputs() -> None: - seen: dict[str, Any] = {} - - def process_inputs(inp: Any) -> Any: - seen["inputs"] = inp - return {"scrubbed_in": True} - - graph = ( - StateGraph(State) - .add_node("n", _incr, trace_policy=TracePolicy(process_inputs=process_inputs)) - .add_edge(START, "n") - .add_edge("n", END) - .compile() - ) - - tracer = FakeTracer() - # the real graph output is unaffected by the trace policy - assert graph.invoke({"value": 1}, {"callbacks": [tracer]}) == {"value": 2} - - run = _node_run(tracer, "n") - # the recorded input is transformed; the output is recorded as-is - assert run.inputs == {"scrubbed_in": True} - assert run.outputs == {"value": 2} - # process_inputs observed the real, untransformed input - assert seen["inputs"] == {"value": 1} - - -def test_trace_policy_none_records_real_payloads() -> None: - graph = ( - StateGraph(State) - .add_node("n", _incr) - .add_edge(START, "n") - .add_edge("n", END) - .compile() - ) - - tracer = FakeTracer() - assert graph.invoke({"value": 1}, {"callbacks": [tracer]}) == {"value": 2} - - run = _node_run(tracer, "n") - assert run.inputs == {"value": 1} - assert run.outputs == {"value": 2} - - -@pytest.mark.anyio -async def test_trace_policy_transforms_recorded_inputs_async() -> None: - graph = ( - StateGraph(State) - .add_node( - "n", - _incr, - trace_policy=TracePolicy(process_inputs=lambda _: {"scrubbed_in": True}), - ) - .add_edge(START, "n") - .add_edge("n", END) - .compile() - ) - - tracer = FakeTracer() - assert await graph.ainvoke({"value": 5}, {"callbacks": [tracer]}) == {"value": 6} - - run = _node_run(tracer, "n") - assert run.inputs == {"scrubbed_in": True} - assert run.outputs == {"value": 6} From 1e1ca88dad9c7e708263257fa9fc27a3fbdfff68 Mon Sep 17 00:00:00 2001 From: Nick Hollon Date: Fri, 24 Jul 2026 13:16:41 -0400 Subject: [PATCH 08/12] feat(langgraph): type v3 stream_events return and native projections (#8389) The `version="v3"` overloads of `stream_events`/`astream_events` returned `Any`, and `GraphRunStream`/`AsyncGraphRunStream` attached native projections via a runtime `setattr` loop invisible to type checkers. - Return `GraphRunStream` / `Awaitable[AsyncGraphRunStream]` from the v3 overloads. - Declare the always-registered native projections (`values`, `messages`, `lifecycle`, `subgraphs`) as typed class attributes on both run streams. - Add `assert_type` checks in `test_stream_events_v3.py`. Opt-in native projections (`updates`, `custom`, `checkpoints`, `debug`, `tasks`) are only present when their transformer is registered, so they remain reached via `extensions[...]` rather than annotated as always-present. --- libs/langgraph/langgraph/pregel/main.py | 8 ++-- libs/langgraph/langgraph/stream/run_stream.py | 28 ++++++++++++- libs/langgraph/tests/test_stream_events_v3.py | 41 +++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/main.py b/libs/langgraph/langgraph/pregel/main.py index 4e098ba5b..f4ca48024 100644 --- a/libs/langgraph/langgraph/pregel/main.py +++ b/libs/langgraph/langgraph/pregel/main.py @@ -3512,7 +3512,7 @@ class Pregel( control: RunControl | None = None, transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None, **kwargs: Any, - ) -> Any: + ) -> GraphRunStream: """Internal v3 sync streaming implementation. Public entry: stream_events(version='v3'). Extra keyword arguments are forwarded to the underlying ``stream(...)`` @@ -3568,7 +3568,7 @@ class Pregel( control: RunControl | None = None, transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None, **kwargs: Any, - ) -> Any: + ) -> AsyncGraphRunStream: """Internal v3 async streaming implementation. Public entry: astream_events(version='v3'). Extra keyword arguments are forwarded to the underlying ``astream(...)`` @@ -3633,7 +3633,7 @@ class Pregel( control: RunControl | None = None, transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None, **kwargs: Any, - ) -> Any: ... + ) -> GraphRunStream: ... def stream_events( self, @@ -3738,7 +3738,7 @@ class Pregel( control: RunControl | None = None, transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None, **kwargs: Any, - ) -> Awaitable[Any]: ... + ) -> Awaitable[AsyncGraphRunStream]: ... def astream_events( self, diff --git a/libs/langgraph/langgraph/stream/run_stream.py b/libs/langgraph/langgraph/stream/run_stream.py index 5eb14a6fa..37062c64d 100644 --- a/libs/langgraph/langgraph/stream/run_stream.py +++ b/libs/langgraph/langgraph/stream/run_stream.py @@ -12,7 +12,13 @@ from langgraph.stream._mux import StreamMux from langgraph.stream._types import ProtocolEvent if TYPE_CHECKING: - from langgraph.stream.transformers import SubgraphStatus + from langchain_core.language_models.chat_model_stream import ( + AsyncChatModelStream, + ChatModelStream, + ) + + from langgraph.stream.stream_channel import StreamChannel + from langgraph.stream.transformers import LifecyclePayload, SubgraphStatus def _drive_until_done(pump: Callable[[], bool]) -> None: @@ -48,6 +54,16 @@ class GraphRunStream: experimental and may change. """ + # Native projections always registered by `stream_events(version="v3")`. + # Attached dynamically by the `setattr` loop in `__init__`; declared here + # so type checkers see them. Opt-in native projections (`updates`, + # `custom`, `checkpoints`, `debug`, `tasks`) are only present when their + # transformer is registered, so they are reached via `extensions[...]`. + values: StreamChannel[dict[str, Any]] + messages: StreamChannel[ChatModelStream] + lifecycle: StreamChannel[LifecyclePayload] + subgraphs: StreamChannel[SubgraphRunStream] + def __init__( self, graph_iter: Iterator[Any] | None, @@ -329,6 +345,16 @@ class AsyncGraphRunStream: experimental and may change. """ + # Native projections always registered by `astream_events(version="v3")`. + # Attached dynamically by the `setattr` loop in `__init__`; declared here + # so type checkers see them. Opt-in native projections (`updates`, + # `custom`, `checkpoints`, `debug`, `tasks`) are only present when their + # transformer is registered, so they are reached via `extensions[...]`. + values: StreamChannel[dict[str, Any]] + messages: StreamChannel[AsyncChatModelStream] + lifecycle: StreamChannel[LifecyclePayload] + subgraphs: StreamChannel[AsyncSubgraphRunStream] + def __init__( self, graph_aiter: AsyncIterator[Any] | None, diff --git a/libs/langgraph/tests/test_stream_events_v3.py b/libs/langgraph/tests/test_stream_events_v3.py index 6399b3084..71d81325e 100644 --- a/libs/langgraph/tests/test_stream_events_v3.py +++ b/libs/langgraph/tests/test_stream_events_v3.py @@ -12,6 +12,10 @@ from dataclasses import dataclass from typing import Annotated, Any, TypeVar import pytest +from langchain_core.language_models.chat_model_stream import ( + AsyncChatModelStream, + ChatModelStream, +) from langchain_core.messages import AIMessage, BaseMessage from langgraph.checkpoint.memory import InMemorySaver from pydantic import BaseModel, ValidationError @@ -24,6 +28,14 @@ from langgraph.func import entrypoint from langgraph.graph import StateGraph from langgraph.graph.message import MessagesState from langgraph.runtime import RunControl +from langgraph.stream import ( + AsyncGraphRunStream, + AsyncSubgraphRunStream, + GraphRunStream, + LifecyclePayload, + StreamChannel, + SubgraphRunStream, +) from langgraph.types import ( CheckpointPayload, CheckpointStreamPart, @@ -1178,3 +1190,32 @@ def _check_type_narrowing(part: StreamPart[_StateT, _OutputT]) -> None: assert_type(part, DebugStreamPart[_StateT]) assert_type(part["data"], DebugPayload[_StateT]) assert_type(part["ns"], tuple[str, ...]) + + +# --- v3 stream_events return / projection typing checks --- +# These functions are never called at runtime; `ty` validates the +# assert_type calls. They pin the public typing surface of +# stream_events(version="v3") / astream_events(version="v3"): the handle +# type and the always-registered native projections. + + +def _check_stream_events_v3_typing() -> None: + """Compile-time checks for sync v3 typing — never called at runtime.""" + graph = _make_simple_graph().compile() + run = graph.stream_events(_SIMPLE_INPUT, version="v3") + assert_type(run, GraphRunStream) + assert_type(run.values, StreamChannel[dict[str, Any]]) + assert_type(run.messages, StreamChannel[ChatModelStream]) + assert_type(run.lifecycle, StreamChannel[LifecyclePayload]) + assert_type(run.subgraphs, StreamChannel[SubgraphRunStream]) + + +async def _check_astream_events_v3_typing() -> None: + """Compile-time checks for async v3 typing — never called at runtime.""" + graph = _make_simple_graph().compile() + run = await graph.astream_events(_SIMPLE_INPUT, version="v3") + assert_type(run, AsyncGraphRunStream) + assert_type(run.values, StreamChannel[dict[str, Any]]) + assert_type(run.messages, StreamChannel[AsyncChatModelStream]) + assert_type(run.lifecycle, StreamChannel[LifecyclePayload]) + assert_type(run.subgraphs, StreamChannel[AsyncSubgraphRunStream]) From 9f1e40bfeea28dee2155b4a57f5babd0a53534cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:33:43 -0700 Subject: [PATCH 09/12] chore(deps): bump setuptools from 80.9.0 to 83.0.0 in /libs/langgraph (#8435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [setuptools](https://github.com/pypa/setuptools) from 80.9.0 to 83.0.0.
Changelog

Sourced from setuptools's changelog.

v83.0.0

Features

  • Require Python 3.10 or later.

Bugfixes

  • MANIFEST.in matching (via FileList) is now insensitive to Unicode normalization form. A pattern authored in one form (e.g. NFC, as typically saved by editors) now matches a file whose name is stored on disk in another (e.g. NFD, as produced by macOS APFS/HFS+). Previously an exclude, global-exclude, recursive-exclude, or prune rule could silently fail to drop a non-ASCII-named file from the source distribution, publishing it despite the exclusion -- see GHSA-h35f-9h28-mq5c.

Deprecations and Removals

  • pypa/distutils#334

v82.0.1

Bugfixes

  • Fix the loading of launcher manifest.xml file. (#5047)
  • Replaced deprecated json.__version__ with fixture in tests. (#5186)

Improved Documentation

  • Add advice about how to improve predictability when installing sdists. (#5168)

Misc

v82.0.0

... (truncated)

Commits
  • 6519f72 Bump version: 82.0.1 → 83.0.0
  • d1151b1 Merge pull request #5250 from pypa/feature/distutils-d7633fbed
  • a2df31e Capture removal of dry_run parameter in changelog.
  • 00144dc Moved newsfragment to the release where it occurred.
  • a4a5a2b Add news fragment.
  • 77470c2 Merge https://github.com/pypa/distutils into feature/distutils-d7633fbed
  • 3c43897 Merge pull request #5247 from pypa/copilot/fix-pypy-version-issue
  • bb6ea66 Bump PyPy from 3.10 to 3.11 in CI workflow
  • a2bc3ac Fix broken intersphinx reference to build's installation docs
  • 2d6a739 Use stacked parametrize decorators instead of itertools.product
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=uv&previous-version=80.9.0&new-version=83.0.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) ---
Dependabot commands and options
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 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).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/langgraph/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 338f806ee..e45bc3969 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -3383,11 +3383,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.9.0" +version = "83.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { 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" }, ] [[package]] From 270820363d56cf8a8e594d2d3b19d543230337fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:34:37 -0700 Subject: [PATCH 10/12] chore(deps): bump setuptools from 82.0.1 to 83.0.0 in /libs/cli (#8434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [setuptools](https://github.com/pypa/setuptools) from 82.0.1 to 83.0.0.
Changelog

Sourced from setuptools's changelog.

v83.0.0

Features

  • Require Python 3.10 or later.

Bugfixes

  • MANIFEST.in matching (via FileList) is now insensitive to Unicode normalization form. A pattern authored in one form (e.g. NFC, as typically saved by editors) now matches a file whose name is stored on disk in another (e.g. NFD, as produced by macOS APFS/HFS+). Previously an exclude, global-exclude, recursive-exclude, or prune rule could silently fail to drop a non-ASCII-named file from the source distribution, publishing it despite the exclusion -- see GHSA-h35f-9h28-mq5c.

Deprecations and Removals

  • pypa/distutils#334
Commits
  • 6519f72 Bump version: 82.0.1 → 83.0.0
  • d1151b1 Merge pull request #5250 from pypa/feature/distutils-d7633fbed
  • a2df31e Capture removal of dry_run parameter in changelog.
  • 00144dc Moved newsfragment to the release where it occurred.
  • a4a5a2b Add news fragment.
  • 77470c2 Merge https://github.com/pypa/distutils into feature/distutils-d7633fbed
  • 3c43897 Merge pull request #5247 from pypa/copilot/fix-pypy-version-issue
  • bb6ea66 Bump PyPy from 3.10 to 3.11 in CI workflow
  • a2bc3ac Fix broken intersphinx reference to build's installation docs
  • 2d6a739 Use stacked parametrize decorators instead of itertools.product
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=uv&previous-version=82.0.1&new-version=83.0.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) ---
Dependabot commands and options
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 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).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/cli/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/cli/uv.lock b/libs/cli/uv.lock index d8775a19b..83b254118 100644 --- a/libs/cli/uv.lock +++ b/libs/cli/uv.lock @@ -2031,11 +2031,11 @@ wheels = [ [[package]] name = "setuptools" -version = "82.0.1" +version = "83.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, + { 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" }, ] [[package]] From 1f2f88b2b74aa4e26c6a8be877dcb95bad2cef48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:00:25 -0700 Subject: [PATCH 11/12] chore(deps): bump js-yaml from 4.2.0 to 4.3.0 in /libs/cli/js-monorepo-example (#8438) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
Changelog

Sourced from js-yaml's changelog.

4.3.0, 3.15.0 - 2026-06-27

Security

  • Backported maxTotalMergeKeys option.

[5.2.0] - 2026-06-26

Added

  • Added maxTotalMergeKeys (10000) loader option to limit the total number of keys processed by YAML merge (<<) across one load() / loadAll() call.
  • Added maxAliases (-1) loader option to limit the number of YAML aliases per document.

Removed

  • maxMergeSeqLength replaced with maxTotalMergeKeys for limiting YAML merge processing.

Fixed

  • Round-trip of integers with exponential form (>= 1e21)

[5.1.0] - 2026-06-23

Added

  • Collection tags can finalize an incrementally populated carrier into a different result value.

Changed

  • [breaking] quoteStyle now selects the preferred quote style; use the restored forceQuotes option to force quoting non-key strings.

[5.0.0] - 2026-06-20

Added

  • Added named exports for schemas, tags, parser events and AST utilities.
  • Reworked JSON_SCHEMA and CORE_SCHEMA with spec-compliant scalar resolution rules, and added YAML11_SCHEMA.
  • Added realMapTag for lossless mappings with non-string and complex keys. Object-based mappings now reject complex keys instead of stringifying them.
  • Added dump() transform option for changing the generated AST before rendering.
  • Added dump() options seqInlineFirst, flowBracketPadding, flowSkipCommaSpace, flowSkipColonSpace, quoteFlowKeys, quoteStyle and tagBeforeAnchor.
  • Added formal data layers (events and AST) for modular data pipelines.
    • Added low-level parser (to events), presenter and visitor APIs.
  • Added the YAML Test Suite to the test set.

Changed

  • See the migration guide for upgrade notes.
  • Rewritten in TypeScript and reorganized the public API around flat named exports.

... (truncated)

Commits
  • 33d05b5 4.3.0 released
  • 663bfab Drop demo publish, to not override new v5 one.
  • 1cb8c7b Add v4-legacy tag for publish
  • 02f27af Restore umd builds back to es5
  • 8be84ed Fix es5 compatibility
  • 59423c6 Replace maxMergeSeqLength option with maxTotalMergeKeys (more robust). Ba...
  • 6842ef6 doc polish
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=4.2.0&new-version=4.3.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) ---
Dependabot commands and options
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 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).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/cli/js-monorepo-example/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/cli/js-monorepo-example/yarn.lock b/libs/cli/js-monorepo-example/yarn.lock index 85a044f87..437f5feb3 100644 --- a/libs/cli/js-monorepo-example/yarn.lock +++ b/libs/cli/js-monorepo-example/yarn.lock @@ -1276,9 +1276,9 @@ js-tiktoken@^1.0.12: base64-js "^1.5.1" js-yaml@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524" - integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw== + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592" + integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q== dependencies: argparse "^2.0.1" From 30c4d58db86455128e42ddec96b1ba53c553ba22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:00:34 -0700 Subject: [PATCH 12/12] chore(deps): bump jupyterlab from 4.5.9 to 4.5.10 in /libs/langgraph (#8440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 4.5.9 to 4.5.10.
Release notes

Sourced from jupyterlab's releases.

v4.5.10

4.5.10

(Full Changelog)

Security patches

  • GHSA-gx64-gj6p-pc4c
  • GHSA-89vp-jrxv-24w8
  • GHSA-h5v5-8746-g7mm
  • GHSA-pppj-hq3g-57pj
  • GHSA-whvh-wf3x-g77j

Bugs fixed

Maintenance and upkeep improvements

Contributors to this release

The following people contributed discussions, new ideas, code and documentation contributions, and review. See our definition of contributors.

(GitHub contributors page for this release)

@​krassowski (activity) | @​MUFFANUJ (activity)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jupyterlab&package-manager=uv&previous-version=4.5.9&new-version=4.5.10)](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) ---
Dependabot commands and options
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 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).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/langgraph/uv.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index e45bc3969..08415c897 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1345,7 +1345,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.9" +version = "4.5.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1362,10 +1362,11 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tornado" }, { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/52/a8d4895bef501ffeb6af448e8bf7079541c7772978211963aa653518c2d9/jupyterlab-4.5.9.tar.gz", hash = "sha256:dd79a073fecae7a39066ea99e4627ed6c76269ac926e95a810e1e1df6358d865", size = 23994445, upload-time = "2026-06-17T15:42:16.406Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/24/621aa20ec0d2fe72f52095bda0fc1be7738ac21aabe4129ff623140d5cdf/jupyterlab-4.5.10.tar.gz", hash = "sha256:77e8d80b78be59b2eaba2154562e21caa6e79c2f1281d6f486584f7144ee2f47", size = 23998879, upload-time = "2026-07-21T12:43:27.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/bb/2f9b425062416fba58f580c9b89c3b07277ccdf0a292501fedbca8ea00ea/jupyterlab-4.5.9-py3-none-any.whl", hash = "sha256:5ff0f908e8ac0afbed32b106fdef360f101c0a6654d1bf4a81e98a293ae1b336", size = 12449803, upload-time = "2026-06-17T15:42:12.18Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c9/940f95f17ee4e413ad252bf8d4f2ee9a341f18cfeda87775fef3d7847321/jupyterlab-4.5.10-py3-none-any.whl", hash = "sha256:5967ca61e692e67a2f30b5a2b901c941dc6ce56c0b0e357bc6d34fed5ec095f6", size = 12452502, upload-time = "2026-07-21T12:43:23.542Z" }, ] [[package]]