Compare commits

..
Author SHA1 Message Date
David Asamu bd5f14101c fix(cli): include build environment hash in Docker cache keys 2026-10-26 19:03:47 +00:00
85 changed files with 1077 additions and 5532 deletions

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
working-directory: libs/sdk-py
- name: Login to Docker Hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
cache-suffix: test-${{ inputs.working-directory }}
working-directory: ${{ inputs.working-directory }}
- name: Login to Docker Hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
cache-suffix: "test-langgraph"
working-directory: libs/langgraph
- name: Login to Docker Hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
+3 -3
View File
@@ -48,7 +48,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
@@ -76,13 +76,13 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
- name: Publish to test PyPI
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: ${{ inputs.working-directory }}/dist/
verbose: true
+1 -1
View File
@@ -114,7 +114,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Run check_sdk_methods script
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
+5 -5
View File
@@ -51,7 +51,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -167,7 +167,7 @@ jobs:
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -259,13 +259,13 @@ jobs:
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: ${{ inputs.working-directory }}/dist/
verbose: true
@@ -301,7 +301,7 @@ jobs:
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
@@ -190,7 +190,7 @@ jobs:
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
+46 -46
View File
@@ -1,5 +1,5 @@
version = 1
revision = 5
revision = 3
requires-python = ">=3.10"
[[package]]
@@ -279,7 +279,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -686,7 +686,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.2.0"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -697,23 +697,23 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" },
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.4.1"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/9a/887535e477847f5159377ab35303923644840b13c0ca03c0a467f84d4680/pytest_asyncio-1.4.1.tar.gz", hash = "sha256:e8266eb094e5ee9ffdb2068c9836f33e209ea77a970eb846933d416b5602669b", size = 145237, upload-time = "2026-10-25T21:05:37.135Z" }
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/d7/cd9e2708afdbf059658adaf08bdb0792ca52489871051d6ce7d498b51277/pytest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:507afa78ec77c6661164ef1a2368bc7820383fd1a14b2a7f813de8b5a92949cc", size = 17061, upload-time = "2026-10-25T21:05:35.473Z" },
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
]
[[package]]
@@ -809,27 +809,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.17.3"
version = "0.16.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" }
sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" },
{ url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" },
{ url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" },
{ url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" },
{ url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" },
{ url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" },
{ url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" },
{ url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" },
{ url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" },
{ url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" },
{ url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" },
{ url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" },
{ url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
{ url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
{ url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
{ url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
{ url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
{ url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
{ url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
{ url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
{ url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
{ url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
{ url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
{ url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
{ url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
{ url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
{ url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
{ url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
{ url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
]
[[package]]
@@ -897,27 +897,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.94"
version = "0.0.84"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/c7/1257c8c70e46274664db1eaf2b8aba99a1277470daaa39c65e7decd67c8f/ty-0.0.94.tar.gz", hash = "sha256:933d2feca6057742d04af00c090621c139e7be2c059aa38b040a70532fd5104b", size = 8106769, upload-time = "2026-10-28T18:06:09.045Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/bd/5145d91cf196cb20fc3b005515b21266a419ea7336d3d7891a1769439259/ty-0.0.94-py3-none-linux_armv6l.whl", hash = "sha256:c6463c045880ad17df911a53d367eb2554b17538ca54911bea6ccb20ec95ee11", size = 14528574, upload-time = "2026-10-28T18:05:19.423Z" },
{ url = "https://files.pythonhosted.org/packages/29/0e/2b563132429b5fc6044095061fde4584548cb55abf0d6d9f2829c41ab226/ty-0.0.94-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec3a3892f4290306c674586149668097cf44f13e8b381ba388662f5a440b459b", size = 14067146, upload-time = "2026-10-28T18:05:23.014Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/3a458362ea33ca9756cd6b914f905b3dec007d329b21d83e859ecee65a63/ty-0.0.94-py3-none-macosx_11_0_arm64.whl", hash = "sha256:74ddc6eeecd3251cf8980c9abb960d7eeab649dac5cdb7504910d99c479f84ab", size = 13148266, upload-time = "2026-10-28T18:05:25.769Z" },
{ url = "https://files.pythonhosted.org/packages/cd/38/5380bdaad2745a4e191deacce96403af74d1992ed8782d5766c301c32b50/ty-0.0.94-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31bdc3aace142400d7e22b4d32676c063ec42f0437d77940012c3dc60587ecdd", size = 13973583, upload-time = "2026-10-28T18:05:28.731Z" },
{ url = "https://files.pythonhosted.org/packages/1d/bc/7e1dc4f2594010aff6d1e2e34d0ff9003736210983be5720a04d69c27e03/ty-0.0.94-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fecacfe9c41ed1109aa503ed172d84b1b0fc283ee49aaf078d298dbe1c62e75f", size = 14269235, upload-time = "2026-10-28T18:05:31.697Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/43041f1c53164d6be21ace55047feffeaa8a9709782b01b06c951412b900/ty-0.0.94-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a49762bbc97a732847da8350ad0227c99ce2675fa32e3f58aa9f9ff4a81e4c14", size = 15191720, upload-time = "2026-10-28T18:05:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/22/72/f425d48b13cbf12b92e4f837a5866e9788e9ef4d5a071e2b12f54ee94da4/ty-0.0.94-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76a7b3aea839dfafb4bc068af918e0292b06ac26ea2a43f7ab3dd65548c0ed50", size = 15596604, upload-time = "2026-10-28T18:05:37.377Z" },
{ url = "https://files.pythonhosted.org/packages/13/ea/bd2da666ad4af596886833b095098fa9ff93685285f48bebc4d3865126e0/ty-0.0.94-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f032ce5a9c0c7033a5a8e748f952ece2e22afe2b148d211d05dca2cba8b28000", size = 15572711, upload-time = "2026-10-28T18:05:40.499Z" },
{ url = "https://files.pythonhosted.org/packages/44/f8/4f07eac57e9b60f49cc222d5048d9a61ac7f0902964ec8ac8d4ea8bc521d/ty-0.0.94-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc5e780c2107c1b83a21a394076da328e05a293ce06298df40ecab1ddc31d371", size = 16309102, upload-time = "2026-10-28T18:05:43.539Z" },
{ url = "https://files.pythonhosted.org/packages/22/39/c2ac7e14eb8ca59bd3c71b8c3055294524df84c476ba88545f59c11ceaf9/ty-0.0.94-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5bc969cdabf85aa66987c9b4818072e3af17ce5f2b7bc93550ab896c904c6c40", size = 15073645, upload-time = "2026-10-28T18:05:46.65Z" },
{ url = "https://files.pythonhosted.org/packages/d1/7d/dd4935abf50a97ccc1f46cf6c921a7acdab30f102c3c56aa4e8d6cbc3fa9/ty-0.0.94-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:27bbaff0d7bbcb3e92ed7cab07d6b26dc7ed7fa28ff69910324e5cc347c7b256", size = 13936242, upload-time = "2026-10-28T18:05:49.726Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f0/b65fa6ccde2d629e25031bf2cc2d21ac7b50ee6e0e3d29a5f471c4686c54/ty-0.0.94-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d0960eb71220300655d67db91073fd180f385186b1d186e76faa2fd3ae5bd10f", size = 14280351, upload-time = "2026-10-28T18:05:52.521Z" },
{ url = "https://files.pythonhosted.org/packages/d6/d2/5c901ad5e4f5444852b002b1f4dd3a0b470039305c688facc0483bfccd1a/ty-0.0.94-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab04c14bca92ab92106ceaa19a962a80998389700cf7938576ba3936a304355d", size = 14611163, upload-time = "2026-10-28T18:05:55.208Z" },
{ url = "https://files.pythonhosted.org/packages/6d/99/37d8633e9b5d1dcf1b06f12466825ff0c4028664994e573c352546ee9cbd/ty-0.0.94-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a92ebefa092e6bd58cb5719626858df744575473e2de0cd1c44abd44b9b84b57", size = 14938978, upload-time = "2026-10-28T18:05:58.17Z" },
{ url = "https://files.pythonhosted.org/packages/51/13/804b1f993bf84a819385f8b440fbc4b894499e2c20d865cd74d69327e7b8/ty-0.0.94-py3-none-win32.whl", hash = "sha256:3212dcf30b7567f2bb65a83cdfc4818a5385f82e7afddd83548b0d84e759057e", size = 13448009, upload-time = "2026-10-28T18:06:01.111Z" },
{ url = "https://files.pythonhosted.org/packages/a1/23/81850750cda0ea781ba9d6e08cea1a1039361cb3474c7dd03765bbc4b51f/ty-0.0.94-py3-none-win_amd64.whl", hash = "sha256:daf5bafe8e7d21b8a99a56fc8518f2342a2dbcaa370a5c25cfaedc684088d158", size = 14010576, upload-time = "2026-10-28T18:06:03.773Z" },
{ url = "https://files.pythonhosted.org/packages/d7/d4/ac3a759556493230823abd31e149c95401f96b9cac36567bd403a8454ea3/ty-0.0.94-py3-none-win_arm64.whl", hash = "sha256:ce344353ccdc542cfcfd6abe1481cf23c005f483411ea79652b78ce0beae36f2", size = 13547398, upload-time = "2026-10-28T18:06:06.379Z" },
{ url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" },
{ url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" },
{ url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" },
{ url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" },
{ url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" },
{ url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" },
{ url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" },
{ url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" },
{ url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" },
{ url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" },
{ url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" },
]
[[package]]
+1 -1
View File
@@ -278,7 +278,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-sqlite"
version = "3.1.3"
version = "3.1.2"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.10"
@@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=4.3.1,<5.0.0",
"langgraph-checkpoint>=4.3.0,<5.0.0",
"aiosqlite>=0.20",
"sqlite-vec>=0.1.6",
]
+2 -2
View File
@@ -285,7 +285,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -360,7 +360,7 @@ test = [
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "3.1.3"
version = "3.1.2"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -1,7 +1,6 @@
from __future__ import annotations
import copy
import json
import logging
from collections.abc import AsyncIterator, Collection, Iterator, Mapping, Sequence
from typing import (
@@ -27,8 +26,6 @@ from langgraph.checkpoint.serde.types import (
RESUME,
SCHEDULED,
ChannelProtocol,
LogAppend,
LogStep,
)
V = TypeVar("V", int, float, str)
@@ -185,93 +182,6 @@ class DeltaChannelHistory(TypedDict):
seed: NotRequired[Any]
class LogPage(NamedTuple):
"""A page of a log channel, as returned by `BaseCheckpointSaver.read_log`.
!!! warning "Prototype"
Part of the transcript log prototype. Shape and semantics may change.
"""
entries: list[tuple[int, Any]]
"""`(position, entry)` pairs in path order. Entries are JSON values."""
length: int
"""Number of committed entries on the path to the checkpoint's leaf."""
pending: list[Any]
"""Entries written by finished tasks of the step in progress at the head,
not committed yet. Empty unless the checkpoint read is the thread's latest."""
def log_range(
length: int, after: int | None, before: int | None, limit: int | None
) -> tuple[int, int]:
"""The position range `[lo, hi)` a `read_log` call asks for.
`after=p` starts after position `p`; `before=p` stops before it. With a
`limit`, a read with only `after` takes the first `limit` entries of the
range; any other read takes the last `limit` (chat UIs page from the end).
"""
lo = 0 if after is None else max(after + 1, 0)
hi = length if before is None else min(before, length)
if limit is not None:
if after is not None and before is None:
hi = min(hi, lo + limit)
else:
lo = max(lo, hi - limit)
return lo, max(lo, hi)
def _without_checkpoint_id(config: RunnableConfig) -> RunnableConfig:
conf = {k: v for k, v in config["configurable"].items() if k != "checkpoint_id"}
return {**config, "configurable": conf}
def _pending_log_entries(tup: CheckpointTuple, channel: str) -> list[Any]:
return [
json.loads(text)
for _, ch, value in tup.pending_writes or ()
if ch == channel and isinstance(value, LogAppend)
for text in value.entries
]
def _step_entries(
step: LogStep, parent: CheckpointTuple | None, channel: str
) -> list[str]:
"""The JSON texts of one step: inline, or the `LogAppend` writes stored on
the checkpoint the step started from."""
if step.entries is not None:
texts = list(step.entries)
else:
texts = [
text
for _, ch, value in (parent.pending_writes or () if parent else ())
if ch == channel
and isinstance(value, LogAppend)
and value.start == step.start
for text in value.entries
]
if len(texts) != step.count:
raise ValueError(
f"log channel {channel!r}: the step at position {step.start} "
f"committed {step.count} entries, found {len(texts)}"
)
return texts
def _page_entries(
steps: Sequence[tuple[int, Sequence[str]]], lo: int, hi: int
) -> list[tuple[int, Any]]:
"""Positions `[lo, hi)` from steps given as `(start, texts)`, any order."""
out: list[tuple[int, Any]] = []
for start, texts in sorted(steps, key=lambda s: s[0]):
for i, text in enumerate(texts):
pos = start + i
if lo <= pos < hi:
out.append((pos, json.loads(text)))
return out
def writes_sort_key(
task_path: str, task_id: str = "", idx: int = 0
) -> tuple[str, str, int]:
@@ -321,14 +231,6 @@ class BaseCheckpointSaver(Generic[V]):
serde: SerializerProtocol = JsonPlusSerializer()
supports_log: bool = False
"""Whether this saver stores log channels natively (prototype).
A saver without native support still works: it stores `LogAppend` writes
and `LogStep` values like any other, and the default `read_log` walks the
parent chain to read them back.
"""
def __init__(
self,
*,
@@ -702,109 +604,6 @@ class BaseCheckpointSaver(Generic[V]):
"""
raise NotImplementedError
def read_log(
self,
config: RunnableConfig,
channel: str,
*,
after: int | None = None,
before: int | None = None,
limit: int | None = None,
) -> LogPage:
"""Read a log channel as of the checkpoint `config` names (or the latest).
!!! warning "Prototype"
Part of the transcript log prototype. Shape and semantics may change.
The log as of a checkpoint is the path from the root to the leaf its
channel version points at. See `log_range` for `after`, `before` and
`limit`.
The default implementation walks the parent chain through `get_tuple`:
a step's entries are inline in its `LogStep`, or are the `LogAppend`
writes stored on the checkpoint the step started from. It is correct
and costs one `get_tuple` per checkpoint walked. Savers with direct
storage access override it with range reads.
"""
tup = self.get_tuple(config)
if tup is None:
return LogPage([], 0, [])
latest = self.get_tuple(_without_checkpoint_id(tup.config))
is_head = latest is not None and latest.checkpoint["id"] == tup.checkpoint["id"]
pending = _pending_log_entries(tup, channel) if is_head else []
marker = tup.checkpoint["channel_values"].get(channel)
if not isinstance(marker, LogStep):
return LogPage([], 0, pending)
lo, hi = log_range(marker.end, after, before, limit)
steps: list[tuple[int, list[str]]] = []
cur: CheckpointTuple | None = tup
while cur is not None:
version = cur.checkpoint["channel_versions"].get(channel)
step = cur.checkpoint["channel_values"].get(channel)
if version is None or not isinstance(step, LogStep) or step.end <= lo:
break
# climb to the checkpoint that committed this version: later ones
# carry its marker without inline entries
parent = self.get_tuple(cur.parent_config) if cur.parent_config else None
while (
parent is not None
and parent.checkpoint["channel_versions"].get(channel) == version
):
step = parent.checkpoint["channel_values"].get(channel, step)
parent = (
self.get_tuple(parent.parent_config)
if parent.parent_config
else None
)
steps.append((step.start, _step_entries(step, parent, channel)))
cur = parent
return LogPage(_page_entries(steps, lo, hi), marker.end, pending)
async def aread_log(
self,
config: RunnableConfig,
channel: str,
*,
after: int | None = None,
before: int | None = None,
limit: int | None = None,
) -> LogPage:
"""Async version of `read_log`."""
tup = await self.aget_tuple(config)
if tup is None:
return LogPage([], 0, [])
latest = await self.aget_tuple(_without_checkpoint_id(tup.config))
is_head = latest is not None and latest.checkpoint["id"] == tup.checkpoint["id"]
pending = _pending_log_entries(tup, channel) if is_head else []
marker = tup.checkpoint["channel_values"].get(channel)
if not isinstance(marker, LogStep):
return LogPage([], 0, pending)
lo, hi = log_range(marker.end, after, before, limit)
steps: list[tuple[int, list[str]]] = []
cur: CheckpointTuple | None = tup
while cur is not None:
version = cur.checkpoint["channel_versions"].get(channel)
step = cur.checkpoint["channel_values"].get(channel)
if version is None or not isinstance(step, LogStep) or step.end <= lo:
break
parent = (
await self.aget_tuple(cur.parent_config) if cur.parent_config else None
)
while (
parent is not None
and parent.checkpoint["channel_versions"].get(channel) == version
):
step = parent.checkpoint["channel_values"].get(channel, step)
parent = (
await self.aget_tuple(parent.parent_config)
if parent.parent_config
else None
)
steps.append((step.start, _step_entries(step, parent, channel)))
cur = parent
return LogPage(_page_entries(steps, lo, hi), marker.end, pending)
def get_delta_channel_history(
self, *, config: RunnableConfig, channels: Sequence[str]
) -> Mapping[str, DeltaChannelHistory]:
@@ -21,15 +21,12 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
DeltaChannelHistory,
LogPage,
PendingWrite,
SerializerProtocol,
get_checkpoint_id,
get_checkpoint_metadata,
writes_sort_key,
)
from langgraph.checkpoint.memory._log import InMemoryLog
from langgraph.checkpoint.serde.types import LogAppend, LogStep
logger = logging.getLogger(__name__)
@@ -69,8 +66,6 @@ class InMemorySaver(
```
"""
supports_log = True
# thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping
storage: defaultdict[
str,
@@ -98,7 +93,6 @@ class InMemorySaver(
self.storage = factory(lambda: defaultdict(dict))
self.writes = factory(dict)
self.blobs = factory()
self.log = InMemoryLog()
self.stack = ExitStack()
if factory is not defaultdict:
self.stack.enter_context(self.storage) # type: ignore[arg-type]
@@ -137,11 +131,6 @@ class InMemorySaver(
) -> dict[str, Any]:
result: dict[str, Any] = {}
for k, ver in versions.items():
if (
marker := self.log.marker(thread_id, checkpoint_ns, k, ver)
) is not None:
result[k] = marker
continue
kk = (thread_id, checkpoint_ns, k, ver)
if kk not in self.blobs:
continue
@@ -160,87 +149,6 @@ class InMemorySaver(
for k in sorted(stored, key=lambda k: writes_sort_key(stored[k][3], *k))
]
def _pending_writes(
self, thread_id: str, checkpoint_ns: str, checkpoint_id: str
) -> list[PendingWrite]:
"""A checkpoint's writes, including its uncommitted log writes, in
`writes_sort_key` order."""
stored = self.writes.get((thread_id, checkpoint_ns, checkpoint_id), {})
rows: list[tuple[tuple[str, str, int], PendingWrite]] = [
(
writes_sort_key(v[3], *k),
(v[0], v[1], self.serde.loads_typed(v[2])),
)
for k, v in stored.items()
]
for task_path, task_id, channel, idx, value in self.log.pending_writes(
thread_id, checkpoint_ns, checkpoint_id
):
rows.append(
(writes_sort_key(task_path, task_id, idx), (task_id, channel, value))
)
rows.sort(key=lambda r: r[0])
return [w for _, w in rows]
def _version(
self,
thread_id: str,
checkpoint_ns: str,
checkpoint_id: str | None,
channel: str,
) -> Any:
if checkpoint_id is None:
return None
saved = (
self.storage.get(thread_id, {}).get(checkpoint_ns, {}).get(checkpoint_id)
)
if saved is None:
return None
version = self.serde.loads_typed(saved[0])["channel_versions"].get(channel)
if (thread_id, checkpoint_ns, channel, version) in self.blobs:
# stored before the field became a log channel: its log is empty
return None
return version
def read_log(
self,
config: RunnableConfig,
channel: str,
*,
after: int | None = None,
before: int | None = None,
limit: int | None = None,
) -> LogPage:
"""Read a log channel with range reads over its segment chain."""
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
ns_storage = self.storage.get(thread_id, {}).get(checkpoint_ns, {})
latest = max(ns_storage, default=None)
checkpoint_id = get_checkpoint_id(config) or latest
if checkpoint_id is None or checkpoint_id not in ns_storage:
return LogPage([], 0, [])
return self.log.read(
thread_id,
checkpoint_ns,
channel,
self._version(thread_id, checkpoint_ns, checkpoint_id, channel),
checkpoint_id if checkpoint_id == latest else None,
after=after,
before=before,
limit=limit,
)
async def aread_log(
self,
config: RunnableConfig,
channel: str,
*,
after: int | None = None,
before: int | None = None,
limit: int | None = None,
) -> LogPage:
return self.read_log(config, channel, after=after, before=before, limit=limit)
def get_delta_channel_history(
self, *, config: RunnableConfig, channels: Sequence[str]
) -> Mapping[str, DeltaChannelHistory]:
@@ -347,6 +255,7 @@ class InMemorySaver(
if checkpoint_id := get_checkpoint_id(config):
if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id):
checkpoint, metadata, parent_checkpoint_id = saved
writes = self._ordered_writes(thread_id, checkpoint_ns, checkpoint_id)
checkpoint_: Checkpoint = self.serde.loads_typed(checkpoint)
return CheckpointTuple(
config=config,
@@ -357,9 +266,9 @@ class InMemorySaver(
),
},
metadata=self.serde.loads_typed(metadata),
pending_writes=self._pending_writes(
thread_id, checkpoint_ns, checkpoint_id
),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
],
parent_config=(
{
"configurable": {
@@ -376,6 +285,7 @@ class InMemorySaver(
if checkpoints := self.storage[thread_id][checkpoint_ns]:
checkpoint_id = max(checkpoints.keys())
checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id]
writes = self._ordered_writes(thread_id, checkpoint_ns, checkpoint_id)
checkpoint_ = self.serde.loads_typed(checkpoint)
return CheckpointTuple(
config={
@@ -392,9 +302,9 @@ class InMemorySaver(
),
},
metadata=self.serde.loads_typed(metadata),
pending_writes=self._pending_writes(
thread_id, checkpoint_ns, checkpoint_id
),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
],
parent_config=(
{
"configurable": {
@@ -478,6 +388,10 @@ class InMemorySaver(
elif limit is not None:
limit -= 1
writes = self._ordered_writes(
thread_id, checkpoint_ns, checkpoint_id
)
checkpoint_: Checkpoint = self.serde.loads_typed(checkpoint)
yield CheckpointTuple(
@@ -508,9 +422,9 @@ class InMemorySaver(
if parent_checkpoint_id
else None
),
pending_writes=self._pending_writes(
thread_id, checkpoint_ns, checkpoint_id
),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
],
)
def put(
@@ -537,22 +451,8 @@ class InMemorySaver(
c = checkpoint.copy()
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
parent_id = config["configurable"].get("checkpoint_id")
values: dict[str, Any] = c.pop("channel_values") # type: ignore[misc]
for k, v in new_versions.items():
if isinstance(values.get(k), LogStep):
# a log channel: the step row is its "blob" at this version
self.log.put_step(
thread_id,
checkpoint_ns,
k,
self._version(thread_id, checkpoint_ns, parent_id, k),
parent_id,
checkpoint["id"],
v,
values[k],
)
continue
self.blobs[(thread_id, checkpoint_ns, k, v)] = (
self.serde.dumps_typed(values[k]) if k in values else ("empty", b"")
)
@@ -600,19 +500,6 @@ class InMemorySaver(
outer_key = (thread_id, checkpoint_ns, checkpoint_id)
outer_writes_ = self.writes.get(outer_key)
for idx, (c, v) in enumerate(writes):
if isinstance(v, LogAppend):
self.log.put_append(
thread_id,
checkpoint_ns,
c,
self._version(thread_id, checkpoint_ns, checkpoint_id, c),
checkpoint_id,
task_id,
task_path,
idx,
v,
)
continue
inner_key = (task_id, WRITES_IDX_MAP.get(c, idx))
if inner_key[1] >= 0 and outer_writes_ and inner_key in outer_writes_:
continue
@@ -641,72 +528,6 @@ class InMemorySaver(
for k in list(self.blobs.keys()):
if k[0] == thread_id:
del self.blobs[k]
self.log.delete_thread(thread_id)
def copy_thread(self, source_thread_id: str, target_thread_id: str) -> None:
"""Copy all checkpoints, writes, blobs and log rows to another thread."""
for ns, checkpoints in self.storage.get(source_thread_id, {}).items():
self.storage[target_thread_id][ns] = dict(checkpoints)
for (t, ns, cid), rows in list(self.writes.items()):
if t == source_thread_id:
self.writes[(target_thread_id, ns, cid)] = dict(rows)
for (t, ns, ch, ver), blob in list(self.blobs.items()):
if t == source_thread_id:
self.blobs[(target_thread_id, ns, ch, ver)] = blob
self.log.copy_thread(source_thread_id, target_thread_id)
def prune(
self, thread_ids: Sequence[str], *, strategy: str = "keep_latest"
) -> None:
"""Prune checkpoints; the log keeps what remaining checkpoints reach.
`"keep_latest"` keeps the latest checkpoint per namespace, `"delete"`
deletes every checkpoint. Writes and blobs no remaining checkpoint
uses are deleted with them.
"""
if strategy not in ("keep_latest", "delete"):
raise ValueError(f"Unknown prune strategy: {strategy}")
for thread_id in thread_ids:
keep = {
ns: ({max(cps)} if strategy == "keep_latest" and cps else set())
for ns, cps in self.storage.get(thread_id, {}).items()
}
self._retain(thread_id, keep)
def delete_for_runs(self, run_ids: Sequence[str]) -> None:
"""Delete the checkpoints the given runs created; the log keeps what
the remaining checkpoints reach."""
runs = set(run_ids)
for thread_id, namespaces in list(self.storage.items()):
keep = {
ns: {
cid
for cid, saved in cps.items()
if self.serde.loads_typed(saved[1]).get("run_id") not in runs
}
for ns, cps in namespaces.items()
}
self._retain(thread_id, keep)
def _retain(self, thread_id: str, keep: dict[str, set[str]]) -> None:
namespaces = self.storage.get(thread_id, {})
for ns, cps in list(namespaces.items()):
for cid in [c for c in cps if c not in keep.get(ns, set())]:
del cps[cid]
self.writes.pop((thread_id, ns, cid), None)
used = {
(thread_id, ns, ch, ver)
for ns, cps in namespaces.items()
for saved in cps.values()
for ch, ver in self.serde.loads_typed(saved[0])["channel_versions"].items()
}
for k in [k for k in self.blobs if k[0] == thread_id and k not in used]:
del self.blobs[k]
self.log.collect(
thread_id,
{ns: dict.fromkeys(cps) for ns, cps in namespaces.items()},
lambda ns, cid, ch: self._version(thread_id, ns, cid, ch),
)
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Asynchronous version of `get_tuple`.
@@ -798,17 +619,6 @@ class InMemorySaver(
"""
return self.delete_thread(thread_id)
async def acopy_thread(self, source_thread_id: str, target_thread_id: str) -> None:
return self.copy_thread(source_thread_id, target_thread_id)
async def aprune(
self, thread_ids: Sequence[str], *, strategy: str = "keep_latest"
) -> None:
return self.prune(thread_ids, strategy=strategy)
async def adelete_for_runs(self, run_ids: Sequence[str]) -> None:
return self.delete_for_runs(run_ids)
def get_next_version(self, current: str | None, channel: None) -> str:
if current is None:
current_v = 0
@@ -1,401 +0,0 @@
"""Log channel storage for `InMemorySaver` (transcript log prototype).
The reference implementation of the log storage design: entries stored once,
keyed in path order; one step row per committing channel version; segments
for linear runs, with parent pointers between them. Reads are range reads
over a segment chain, and lifecycle is reachability from the checkpoints that
remain.
Rows, per `(thread_id, checkpoint_ns, channel)`:
- segment: `seg -> (parent_seg | None, fork_pos)`
- step: `version -> StepRow(seg, start, count, batch)`, unique on `(seg, start)`
- entry: `seg -> {(start, batch, task_path, task_id, idx, j): json_text}`
`batch` names the execution that wrote a step's entries: the checkpoint the
step started from for entries that came through `put_writes`, the committing
checkpoint for entries a `LogStep` carried inline.
"""
from __future__ import annotations
import json
import threading
import uuid
from collections import defaultdict
from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from typing import Any
from langgraph.checkpoint.base import LogPage, log_range, writes_sort_key
from langgraph.checkpoint.serde.types import LogAppend, LogStep
_SEGMENT_NS = uuid.UUID("6f6c6f67-0000-4000-8000-6c6f67736567")
EntryKey = tuple[int, str, str, str, int, int]
@dataclass
class StepRow:
seg: str
start: int
count: int
batch: str
exec: str
@property
def end(self) -> int:
return self.start + self.count
@dataclass
class ChannelLog:
segments: dict[str, tuple[str | None, int]] = field(default_factory=dict)
steps: dict[str, StepRow] = field(default_factory=dict)
step_at: dict[tuple[str, int], str] = field(default_factory=dict)
entries: defaultdict[str, dict[EntryKey, str]] = field(
default_factory=lambda: defaultdict(dict)
)
batch_segments: defaultdict[str, list[str]] = field(
default_factory=lambda: defaultdict(list)
)
def copy(self) -> ChannelLog:
new = ChannelLog()
new.segments = dict(self.segments)
new.steps = {
v: StepRow(r.seg, r.start, r.count, r.batch, r.exec)
for v, r in self.steps.items()
}
new.step_at = dict(self.step_at)
for seg, rows in self.entries.items():
new.entries[seg] = dict(rows)
for batch, segs in self.batch_segments.items():
new.batch_segments[batch] = list(segs)
return new
# -- structure ---------------------------------------------------------
def committed(self, seg: str) -> bool:
return any(s == seg for s, _ in self.step_at)
def tip(self, seg: str) -> int:
ends = [self.steps[v].end for (s, _), v in self.step_at.items() if s == seg]
return max(ends) if ends else self.segments[seg][1]
def chain(self, seg: str, upto: int) -> list[tuple[str, int]]:
"""`(segment, upto)` pairs from the leaf's segment to the root."""
out: list[tuple[str, int]] = []
cur: str | None = seg
while cur is not None:
out.append((cur, upto))
cur, upto = self.segments[cur]
return out
def segment_for(self, leaf: tuple[str | None, int], batch: str, exec_: str) -> str:
"""The fork rule: continue the leaf's segment if the leaf is its tip;
otherwise use a new segment forking at the leaf, reusing one this
batch already started and nothing committed to."""
seg, end = leaf
if seg is not None and self.tip(seg) == end:
return seg
for cand in self.batch_segments.get(batch, ()):
if self.segments[cand] == leaf and not self.committed(cand):
return cand
new = str(
uuid.uuid5(
_SEGMENT_NS, f"{batch}:{exec_}:{len(self.batch_segments[batch])}"
)
)
self.segments[new] = leaf
self.batch_segments[batch].append(new)
return new
def step_texts(self, seg: str, start: int, batch: str) -> list[str]:
rows = self.entries.get(seg, {})
keys = sorted(
(k for k in rows if k[0] == start and k[1] == batch),
key=lambda k: (writes_sort_key(k[2], k[3], k[4]), k[5]),
)
return [rows[k] for k in keys]
def replace_write(
self,
seg: str,
start: int,
batch: str,
task_path: str,
task_id: str,
idx: int,
texts: Iterable[str],
) -> None:
rows = self.entries[seg]
for k in [k for k in rows if k[:5] == (start, batch, task_path, task_id, idx)]:
del rows[k]
for j, text in enumerate(texts):
rows[(start, batch, task_path, task_id, idx, j)] = text
def is_committed_step(self, seg: str, start: int, batch: str) -> bool:
v = self.step_at.get((seg, start))
return v is not None and self.steps[v].batch == batch
def pending(self, batch: str) -> list[tuple[str, str, int, LogAppend, str]]:
"""Uncommitted writes stored under `batch`, as
`(task_path, task_id, idx, LogAppend, exec)`, in write order."""
groups: dict[tuple[str, int, str, str, int], list[tuple[int, str]]] = (
defaultdict(list)
)
for seg, rows in self.entries.items():
for (start, b, task_path, task_id, idx, j), text in rows.items():
if b != batch or self.is_committed_step(seg, start, b):
continue
groups[(seg, start, task_path, task_id, idx)].append((j, text))
out = []
for (_, start, task_path, task_id, idx), items in groups.items():
texts = tuple(t for _, t in sorted(items))
out.append((task_path, task_id, idx, LogAppend(start, texts), ""))
out.sort(key=lambda w: writes_sort_key(w[0], w[1], w[2]))
return out
class InMemoryLog:
"""Log storage for one `InMemorySaver`."""
def __init__(self) -> None:
self.logs: dict[tuple[str, str, str], ChannelLog] = {}
self.lock = threading.RLock()
def _log(self, thread_id: str, ns: str, channel: str) -> ChannelLog:
key = (thread_id, ns, channel)
if key not in self.logs:
self.logs[key] = ChannelLog()
return self.logs[key]
def is_log_version(
self, thread_id: str, ns: str, channel: str, version: Any
) -> bool:
log = self.logs.get((thread_id, ns, channel))
return log is not None and version in log.steps
def marker(
self, thread_id: str, ns: str, channel: str, version: Any
) -> LogStep | None:
log = self.logs.get((thread_id, ns, channel))
if log is None or version not in log.steps:
return None
row = log.steps[version]
return LogStep(row.start, row.count, row.exec)
def leaf(
self, thread_id: str, ns: str, channel: str, version: Any
) -> tuple[str | None, int]:
if version is None:
return (None, 0)
log = self.logs.get((thread_id, ns, channel))
if log is None or version not in log.steps:
raise ValueError(
f"log channel {channel!r}: version {version} has no step row"
)
row = log.steps[version]
return (row.seg, row.end)
# -- writes ------------------------------------------------------------
def put_append(
self,
thread_id: str,
ns: str,
channel: str,
parent_version: Any,
batch: str,
task_id: str,
task_path: str,
idx: int,
value: LogAppend,
) -> None:
with self.lock:
log = self._log(thread_id, ns, channel)
leaf = self.leaf(thread_id, ns, channel, parent_version)
if value.start != leaf[1]:
raise ValueError(
f"log channel {channel!r}: append at {value.start}, but the "
f"checkpoint's log has {leaf[1]} entries"
)
seg = log.segment_for(leaf, batch, value.exec)
if log.is_committed_step(seg, value.start, batch):
return # already committed: a reused write, nothing to do
log.replace_write(
seg, value.start, batch, task_path, task_id, idx, value.entries
)
def put_step(
self,
thread_id: str,
ns: str,
channel: str,
parent_version: Any,
parent_id: str | None,
checkpoint_id: str,
version: Any,
step: LogStep,
) -> None:
with self.lock:
log = self._log(thread_id, ns, channel)
leaf = self.leaf(thread_id, ns, channel, parent_version)
if step.start != leaf[1]:
raise ValueError(
f"log channel {channel!r}: step starts at {step.start}, but "
f"the parent checkpoint's log has {leaf[1]} entries"
)
inline = step.entries is not None
batch = checkpoint_id if inline or parent_id is None else parent_id
seg = log.segment_for(leaf, batch, step.exec)
if inline:
log.replace_write(seg, step.start, batch, "", "", 0, step.entries)
texts = log.step_texts(seg, step.start, batch)
if len(texts) != step.count:
raise ValueError(
f"log channel {channel!r}: the step at {step.start} commits "
f"{step.count} entries, but {len(texts)} were written"
)
existing = log.step_at.get((seg, step.start))
if existing is not None and existing != version:
raise ValueError(
f"log channel {channel!r}: position {step.start} of segment "
f"{seg} was already committed by another step (concurrent "
"writers on one checkpoint)"
)
log.steps[version] = StepRow(seg, step.start, step.count, batch, step.exec)
log.step_at[(seg, step.start)] = version
# -- reads -------------------------------------------------------------
def read(
self,
thread_id: str,
ns: str,
channel: str,
version: Any,
batch: str | None,
*,
after: int | None,
before: int | None,
limit: int | None,
) -> LogPage:
with self.lock:
log = self.logs.get((thread_id, ns, channel))
pending: list[Any] = []
if log is not None and batch is not None:
pending = [
json.loads(text)
for *_, value, _ in log.pending(batch)
for text in value.entries
]
if log is None or version is None or version not in log.steps:
return LogPage([], 0, pending)
row = log.steps[version]
lo, hi = log_range(row.end, after, before, limit)
out: list[tuple[int, Any]] = []
for seg, upto in log.chain(row.seg, row.end):
if upto <= lo:
break
starts = sorted(s for (sg, s) in log.step_at if sg == seg and s < upto)
for start in starts:
srow = log.steps[log.step_at[(seg, start)]]
if srow.end <= lo or srow.start >= hi:
continue
texts = log.step_texts(seg, start, srow.batch)
if len(texts) != srow.count:
raise ValueError(
f"log channel {channel!r}: step at {start} has "
f"{len(texts)} entries, expected {srow.count}"
)
for i, text in enumerate(texts):
if lo <= start + i < hi:
out.append((start + i, json.loads(text)))
out.sort(key=lambda e: e[0])
return LogPage(out, row.end, pending)
def pending_writes(
self, thread_id: str, ns: str, checkpoint_id: str
) -> list[tuple[str, str, str, int, LogAppend]]:
"""Uncommitted log writes stored on a checkpoint, as
`(task_path, task_id, channel, idx, LogAppend)`."""
with self.lock:
out = []
for (t, n, channel), log in self.logs.items():
if t != thread_id or n != ns:
continue
for task_path, task_id, idx, value, _ in log.pending(checkpoint_id):
out.append((task_path, task_id, channel, idx, value))
return out
# -- lifecycle ---------------------------------------------------------
def delete_thread(self, thread_id: str) -> None:
with self.lock:
for key in [k for k in self.logs if k[0] == thread_id]:
del self.logs[key]
def copy_thread(self, source: str, target: str) -> None:
with self.lock:
for (t, ns, channel), log in list(self.logs.items()):
if t == source:
self.logs[(target, ns, channel)] = log.copy()
def collect(
self,
thread_id: str,
remaining: dict[str, dict[str, Any]],
version_of: Callable[[str, str, str], Any],
) -> None:
"""Keep exactly what remaining checkpoints reach.
`remaining` maps each namespace to its remaining checkpoint ids;
`version_of(ns, checkpoint_id, channel)` returns a checkpoint's
version of a channel.
"""
with self.lock:
for (t, ns, channel), log in list(self.logs.items()):
if t != thread_id:
continue
cids = remaining.get(ns, {})
reach: dict[str, int] = {}
for cid in cids:
version = version_of(ns, cid, channel)
if version is None or version not in log.steps:
continue
row = log.steps[version]
for seg, upto in log.chain(row.seg, row.end):
reach[seg] = max(reach.get(seg, -1), upto)
kept_steps = {
v: r
for v, r in log.steps.items()
if r.seg in reach and r.start < reach[r.seg]
}
kept_keys = {(r.seg, r.start, r.batch) for r in kept_steps.values()}
for seg in list(log.entries):
rows = log.entries[seg]
for k in list(rows):
start, batch = k[0], k[1]
if (seg, start, batch) in kept_keys:
continue
if batch in cids and not log.is_committed_step(
seg, start, batch
):
continue # pending at a remaining checkpoint
del rows[k]
if not rows:
del log.entries[seg]
log.steps = kept_steps
log.step_at = {(r.seg, r.start): v for v, r in kept_steps.items()}
live = set(reach) | set(log.entries)
log.segments = {s: p for s, p in log.segments.items() if s in live}
for batch in list(log.batch_segments):
segs = [s for s in log.batch_segments[batch] if s in live]
if segs:
log.batch_segments[batch] = segs
else:
del log.batch_segments[batch]
if not log.steps and not log.entries:
del self.logs[(t, ns, channel)]
@@ -34,8 +34,6 @@ from langgraph.checkpoint.serde import _msgpack as _lg_msgpack
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.event_hooks import emit_serde_event
from langgraph.checkpoint.serde.types import (
LogAppend,
LogStep,
SendProtocol,
_DeltaSnapshot,
)
@@ -324,29 +322,11 @@ EXT_PYDANTIC_V1 = 4
EXT_PYDANTIC_V2 = 5
EXT_NUMPY_ARRAY = 6
EXT_DELTA_SNAPSHOT = 7
EXT_LOG_APPEND = 8
EXT_LOG_STEP = 9
def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
if isinstance(obj, _DeltaSnapshot):
return ormsgpack.Ext(EXT_DELTA_SNAPSHOT, _msgpack_enc(obj.value))
elif isinstance(obj, LogAppend):
return ormsgpack.Ext(
EXT_LOG_APPEND, _msgpack_enc([obj.start, list(obj.entries), obj.exec])
)
elif isinstance(obj, LogStep):
return ormsgpack.Ext(
EXT_LOG_STEP,
_msgpack_enc(
[
obj.start,
obj.count,
obj.exec,
None if obj.entries is None else list(obj.entries),
]
),
)
elif hasattr(obj, "model_dump") and callable(obj.model_dump): # pydantic v2
return ormsgpack.Ext(
EXT_PYDANTIC_V2,
@@ -679,14 +659,6 @@ def _create_msgpack_ext_hook(
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
)
)
elif code == EXT_LOG_APPEND:
start, entries, exec_ = ormsgpack.unpackb(data)
return LogAppend(start, tuple(entries), exec_)
elif code == EXT_LOG_STEP:
start, count, exec_, entries = ormsgpack.unpackb(data)
return LogStep(
start, count, exec_, None if entries is None else tuple(entries)
)
elif code == EXT_CONSTRUCTOR_SINGLE_ARG:
tup = None
try:
@@ -31,52 +31,6 @@ class _DeltaSnapshot(NamedTuple):
value: Any
class LogAppend(NamedTuple):
"""One task write to a log channel, as handed to `put_writes`.
!!! warning "Prototype"
Part of the transcript log prototype. Shape and semantics may change.
`start` is the log's length at the checkpoint the step started from (the
position the step's entries begin at). `entries` are the appended entries
as JSON text, exactly as written. `exec` identifies the execution that
wrote them, so a log-aware saver can keep two executions from one
checkpoint apart.
"""
start: int
entries: tuple[str, ...]
exec: str = ""
class LogStep(NamedTuple):
"""A log channel's value in a checkpoint: the step that produced its version.
!!! warning "Prototype"
Part of the transcript log prototype. Shape and semantics may change.
The step appended `count` entries starting at position `start`, so the
log as of the checkpoint has `start + count` entries. `entries` carries
the step's entries inline when they did not reach the saver through
`put_writes` (graph input, `update_state`, `durability="exit"`); it is
`None` when they did.
"""
start: int
count: int
exec: str = ""
entries: tuple[str, ...] | None = None
@property
def end(self) -> int:
return self.start + self.count
def stripped(self) -> "LogStep":
return LogStep(self.start, self.count, self.exec, None)
Value = TypeVar("Value", covariant=True)
Update = TypeVar("Update", contravariant=True)
C = TypeVar("C")
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.10"
+91 -206
View File
@@ -1,5 +1,5 @@
version = 1
revision = 5
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.15'",
@@ -178,15 +178,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
@@ -221,40 +212,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpcore2"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "h11" },
{ name = "truststore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/ff/b53af1b49bb2cb5fa97c92215614b173ca5c1a99ded80e2d52f15a94fa6d/httpcore2-2.14.0.tar.gz", hash = "sha256:4f57a1a8249fe6af589d935cfd456f1d13bbdcb0f0fc72af9142368fc94811d7", size = 69096, upload-time = "2026-10-18T16:29:21.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/06/a8d6a8f90d11b76d9086242b6bb0ee4b503473bfc8be34c2d5b067275c22/httpcore2-2.14.0-py3-none-any.whl", hash = "sha256:4c4f64f4b544afeb778c42c0ffb5941b29e671157e90ed3f6dc787c4bca90cd1", size = 84291, upload-time = "2026-10-18T16:29:19.743Z" },
]
[[package]]
name = "httpcore2"
version = "2.16.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
]
dependencies = [
{ name = "h11" },
{ name = "truststore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
@@ -270,63 +227,13 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx2"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
{ name = "httpcore2", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" },
{ name = "idna" },
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/67/58353f1848b4ba568e1ec22ea1e0d8e039f0bca5fde7e2d1f07739fd83e7/httpx2-2.14.0.tar.gz", hash = "sha256:47ad61a3c40282cd8d0dc96311531f00f5bfc71a066c6fe3b36d8f63fb9e4f6e", size = 102036, upload-time = "2026-10-18T16:29:22.627Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/5a/469616aa8c2cbb0bb9dd67a3c18d07c01ab604cc0b236ee1c47d260c7f6e/httpx2-2.14.0-py3-none-any.whl", hash = "sha256:9d20c13a10bae1773a0be3b0f1e5beeb4bdd549bc3a2ba18efa32897c65403e4", size = 96901, upload-time = "2026-10-18T16:29:20.819Z" },
]
[[package]]
name = "httpx2"
version = "2.16.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
]
dependencies = [
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
{ name = "httpcore2", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" },
{ name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
{ name = "idna" },
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" },
]
[[package]]
name = "httpx2-jsfetch"
version = "1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
]
[[package]]
name = "idna"
version = "3.20"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -361,7 +268,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.7.1"
version = "1.6.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -375,26 +282,26 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/ec/55c77c3d53c65fbf58d41bd78567ed34b5c0e9dcacd00e4568a9067314ab/langchain_core-1.7.1.tar.gz", hash = "sha256:5dc2a357e1aa40e84c7f29755182121b3acf04410e55fa3723915f645033fede", size = 1026919, upload-time = "2026-10-28T14:59:47.314Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/ed/ceecd3bfcfc77a94cf30a9f1506e907fdea447458825e01914a72d2d1a6f/langchain_core-1.6.5.tar.gz", hash = "sha256:bdb059fef65473fd444558dbb18f50901657402c43ceed9dc0fdc7b76a393c6b", size = 1007041, upload-time = "2026-09-24T18:11:06.487Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/87/50fa7f000065502ce76ec6b701a13adec62c32171785d5bebb0345219a0f/langchain_core-1.7.1-py3-none-any.whl", hash = "sha256:2d668788945e824832e19bca05788a25dbb864197d63ec48b583057cf8d975fa", size = 575541, upload-time = "2026-10-28T14:59:45.887Z" },
{ url = "https://files.pythonhosted.org/packages/59/68/447ac7e734990b7ed9dcfc3b6dee9a4cd4d4169e296429fa71a9a47c1c7b/langchain_core-1.6.5-py3-none-any.whl", hash = "sha256:54c7b0e9314b9084fb04405bb33dc5d32986d512d92b7b8863899cd5f6267556", size = 572136, upload-time = "2026-09-24T18:11:04.837Z" },
]
[[package]]
name = "langchain-protocol"
version = "0.0.20"
version = "0.0.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/8b/d908d082c3cf5368ddd1816ed114de79c873f979ba7f98b95fab0e841643/langchain_protocol-0.0.20.tar.gz", hash = "sha256:0f5bd002474265648e91161771a9e0438fe9480bd55a416e51af0f60070637ec", size = 6280, upload-time = "2026-10-20T11:13:50.84Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/4b/f1c99a0b27c5ff8b4b131c0d6e62012c17cc424089efe213767db927afd2/langchain_protocol-0.0.20-py3-none-any.whl", hash = "sha256:3c935e5e46b0f6f9cb52f45ba7cfa53d1b9fa80c82c4a277d47f03e07f7385b1", size = 7347, upload-time = "2026-10-20T11:13:49.566Z" },
{ url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -477,28 +384,23 @@ test = [
[[package]]
name = "langsmith"
version = "0.14.16"
version = "0.8.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx2", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "httpx2", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "httpx" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "sniffio" },
{ name = "typing-extensions" },
{ name = "uuid-utils" },
{ name = "websockets" },
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/45/576803a72da96d9c0866b3815f08f447ed16c14acb52174129daca6847d6/langsmith-0.14.16.tar.gz", hash = "sha256:4e5816cbe1541f4d04d71f652eb8f4137889861bebcf7137bcba3553ae9e335e", size = 5096529, upload-time = "2026-10-29T17:53:27.747Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/3a/9a072c02a22089ddf20f0f33ea95bee5a5951b31839f118b04f27b94be6e/langsmith-0.14.16-py3-none-any.whl", hash = "sha256:b9ea1a57622b0788b181d78605f974fb206c5f6f75793076ffec23cef4d904c8", size = 933680, upload-time = "2026-10-29T17:53:25.465Z" },
{ url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" },
]
[[package]]
@@ -904,36 +806,37 @@ wheels = [
[[package]]
name = "pycryptodome"
version = "4.2.0"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/33/c7/f9374c8939560ab22db1dd9dbe215b62d20f32634aeb7f259dc03c807b44/pycryptodome-4.2.0.tar.gz", hash = "sha256:21d7fdba239d6ef863d3d08388801c683453a3bb4897c2b27f27847aad89bdf7", size = 1750332, upload-time = "2026-10-24T10:24:42.016Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/05/61028b65913438a091ed3a2ec55bb50843e674b6ae14bccf196bbed652a2/pycryptodome-4.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:4b58c4fd5615a788e0df6793d578e565b19d75a348c0ba02d469b0c38bbf0332", size = 1684620, upload-time = "2026-10-24T10:23:50.473Z" },
{ url = "https://files.pythonhosted.org/packages/cd/73/0930fd56dd5eb856ecc33ec48845def52a1d25a9cd6b810a4c3ef3633cf4/pycryptodome-4.2.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:42ab554efcc14928f1db42deac9c84989fd49e289a16aacbbee709fe2eb838cc", size = 1520423, upload-time = "2026-10-24T10:23:52.505Z" },
{ url = "https://files.pythonhosted.org/packages/19/ba/4b931de42881053a6d818b9ee70f6dde3dfe1cfcf78527d45db3f99d8b47/pycryptodome-4.2.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b4ed1a34d2d667ec165beb15313a759bcd8e704e2743f9942491c5a88a2d6ca5", size = 2296340, upload-time = "2026-10-24T10:23:54.294Z" },
{ url = "https://files.pythonhosted.org/packages/d4/63/68fc2a01d621c2ff389ea261e731e8f1abe6aba89992681bbcdb19fcca43/pycryptodome-4.2.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1482a7d9ceed7beadbb0853f83d4d44406a9f9fa8ee596d637618f095869e14", size = 2987589, upload-time = "2026-10-24T10:23:56.547Z" },
{ url = "https://files.pythonhosted.org/packages/f5/63/69e211459f4625c89bb47a7ba4527a6cc3276c9bbd79ae871a2d0b05c021/pycryptodome-4.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:dceadb6f478b0a687d129f1b58220e0587fb17a31b2502ae7a1a4234bc304ee1", size = 2322482, upload-time = "2026-10-24T10:23:58.538Z" },
{ url = "https://files.pythonhosted.org/packages/4a/65/21a4ed3ae5af153c57861d7f987c81fca5bf02d49b76f34c545f8eb6b95f/pycryptodome-4.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8217736d5ac9b7fef49d1d2b292d5af4e8300298ef4926742aaec49b2bf6505b", size = 3548352, upload-time = "2026-10-24T10:24:01.185Z" },
{ url = "https://files.pythonhosted.org/packages/4a/a4/9a7e4e8896cfd8131978d31e39c930fb114f6a63751f39d2069b965dad45/pycryptodome-4.2.0-cp315-cp315t-win32.whl", hash = "sha256:b717e36cf14cffe463ff4e7d423a0c9fd6f83b40d207388e0f40dd0a3d4018bc", size = 1306386, upload-time = "2026-10-24T10:24:03.047Z" },
{ url = "https://files.pythonhosted.org/packages/af/6c/e1a78019b7db83e2cfc1c98cea4a7a0afbb6b233514b9dfee2bad7443bd9/pycryptodome-4.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:585f0a792cdc7fb381ca0d1fac4e20f03fd99bb78cf2733eaab59f62c77ccf88", size = 1688319, upload-time = "2026-10-24T10:24:04.963Z" },
{ url = "https://files.pythonhosted.org/packages/c6/96/7cb051ba325141b33bc7e6ef6152499cf74b74606c5bb4c8c72411e8afdc/pycryptodome-4.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:831ae8ce070e990d4fcb913c49c7fb0181ea7ab2691fc58dcb9d41fdb5384d85", size = 1330914, upload-time = "2026-10-24T10:24:06.934Z" },
{ url = "https://files.pythonhosted.org/packages/41/4c/954a9a1451f7bcc89d66db39bf6499ef5701e8ff35b6c898c659a7ec8546/pycryptodome-4.2.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e430168f767ecec60c1f850df4abbd0b9f94786b9cc8041a00e976f6d5190c6", size = 1680663, upload-time = "2026-10-24T10:24:08.982Z" },
{ url = "https://files.pythonhosted.org/packages/6e/84/23f8013dce07cbc8c8b9ed878972294230a1edc49a6b64d271c77c878c29/pycryptodome-4.2.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aea22120791b4534a27e39fc01d91265a249ba72dbd682a8a5c1adee475d5483", size = 1513323, upload-time = "2026-10-24T10:24:11.076Z" },
{ url = "https://files.pythonhosted.org/packages/1a/82/693940a8e4a20c3db91050480429daf39c818be3c7ec59f992ded4567ce6/pycryptodome-4.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6f30a457bd83a3074747d11bcb95375397a2e9b9a6b9179ab001e0cdd8ed0c5", size = 2292885, upload-time = "2026-10-24T10:24:13.486Z" },
{ url = "https://files.pythonhosted.org/packages/18/88/33df6b1176b63aed427745696d88ed38f7877104cf0f6004ee9ad600f897/pycryptodome-4.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76777686de3a354c41710727988c2284448d5db9bd82e5ddb9a9b6710d17d9df", size = 2976488, upload-time = "2026-10-24T10:24:16.139Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c6/c1c137d6e3284409ca5b2cfd7db3621167c316f9c314a96274af69a66fbb/pycryptodome-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c5bc41a418451616cd0ec3d765ba8539416d7429d7cd633ffb603a5a22355e70", size = 2313324, upload-time = "2026-10-24T10:24:18.302Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/b7752cffc8200040835ad24dc17c9de6376ac27612c8bf943766cb1b3c36/pycryptodome-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2c5b1e9058687daca3aaaff23eeb1c1853c8251986f0de7a950235e8bc0cc8b7", size = 3537773, upload-time = "2026-10-24T10:24:20.482Z" },
{ url = "https://files.pythonhosted.org/packages/df/1b/34bec1416507cda23b5dfa33c0bca936fd4ac8ed4d3269803477b897649d/pycryptodome-4.2.0-cp39-abi3-win32.whl", hash = "sha256:bb7659f87d7aa69105e1be12764f9cf1908287eb424b242186f5ac3c192ed612", size = 1292974, upload-time = "2026-10-24T10:24:22.422Z" },
{ url = "https://files.pythonhosted.org/packages/03/5b/a4a31e6e1aabd5a26da77c7960e7fcce4fca9132e51868f1bba10bf1e932/pycryptodome-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:3282c9fd744a16594e51e1a9fbfe4d658b6b0aaa1f050926e6d591ac7494f78b", size = 1658684, upload-time = "2026-10-24T10:24:24.009Z" },
{ url = "https://files.pythonhosted.org/packages/2b/81/bddb0637fa85a03d00eea2f3514c5aa8d7b9c4921ef5e0c125b4ce662a8c/pycryptodome-4.2.0-cp39-abi3-win_arm64.whl", hash = "sha256:34ec11258967efec61d92a83a63a050ab7a7c31dc3806412f6334b8aef47e2d9", size = 1305591, upload-time = "2026-10-24T10:24:25.993Z" },
{ url = "https://files.pythonhosted.org/packages/df/a1/5460bbb750121f71abacaaf25f0cb1df67be7fe13c70cf6d7c2b8ad54fc0/pycryptodome-4.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ea7dbf6b8ce0f0d2c0e17c1226c74d07c01499db7704797bdbb6cb9ec511ccb", size = 1472158, upload-time = "2026-10-24T10:24:27.92Z" },
{ url = "https://files.pythonhosted.org/packages/e2/65/27ac8fb15f0ffb70b72a0413291637de46d29b075804221703273c3ca6d1/pycryptodome-4.2.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0106af8684e1d73c93bc39a2f1cb4ae2f2636d1f80e6291caddcc37b4efc5dc1", size = 1247107, upload-time = "2026-10-24T10:24:29.672Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7d/6b3ed6cd8626007f6a6b13616b08150bc0bb936a0b84c310e59558ed5136/pycryptodome-4.2.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4deb46e0d0a540b1f047cb6f430249692da15c21edd75f5e6278314089fb859", size = 1426092, upload-time = "2026-10-24T10:24:31.627Z" },
{ url = "https://files.pythonhosted.org/packages/73/45/5adf4dbb1eab1da6291d25f777d1d34f745c7f2993bf2d4d3b0d9abb87c6/pycryptodome-4.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e094873fe87ae4d546ef393cec7ea1e1cf8e0684f649612b19352afa67eb8bdb", size = 1662257, upload-time = "2026-10-24T10:24:33.373Z" },
{ url = "https://files.pythonhosted.org/packages/57/88/fef93a9ecbda783a312b15a8823fa1a9a4ec3474685e756584af595457aa/pycryptodome-4.2.0-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:30f418cd1f4e776bde6c5df1fcd90ddfe74dfc8f679286a82f17ec1d4018cce0", size = 1467319, upload-time = "2026-10-24T10:24:35.108Z" },
{ url = "https://files.pythonhosted.org/packages/78/65/cb53934c9f8d5c41dfcfaddb0d98103f4dd6670af1ffdbc2582bcdd99382/pycryptodome-4.2.0-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20cb4a489f7a5150a882901884b84ee0df77a47b9a6eb6e2dd8e98997b7b826f", size = 1247105, upload-time = "2026-10-24T10:24:36.859Z" },
{ url = "https://files.pythonhosted.org/packages/11/38/5bba7fd24beb6bc7a82fdd155f2df39f51c8ad16143fc99d3a1ccc7c76b4/pycryptodome-4.2.0-pp311-pypy311_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f104dc1a38aa1172a166ebe3716f2666f785bfa015b7ca13bb808c12c902e455", size = 1426092, upload-time = "2026-10-24T10:24:38.584Z" },
{ url = "https://files.pythonhosted.org/packages/38/cf/e70a8cb1ab184b59b2d02dfd403965a0c6419571d8520463944bcb0ca8a0/pycryptodome-4.2.0-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:a7c6857077255f5f4e6c34e6325802ba1bb59960133c0a5e584d9371dbdf3dea", size = 1662214, upload-time = "2026-10-24T10:24:40.139Z" },
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
{ url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
{ url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
{ url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
{ url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
{ url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
]
[[package]]
@@ -1080,7 +983,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.2.0"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1091,35 +994,35 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" },
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.4.1"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/9a/887535e477847f5159377ab35303923644840b13c0ca03c0a467f84d4680/pytest_asyncio-1.4.1.tar.gz", hash = "sha256:e8266eb094e5ee9ffdb2068c9836f33e209ea77a970eb846933d416b5602669b", size = 145237, upload-time = "2026-10-25T21:05:37.135Z" }
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/d7/cd9e2708afdbf059658adaf08bdb0792ca52489871051d6ce7d498b51277/pytest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:507afa78ec77c6661164ef1a2368bc7820383fd1a14b2a7f813de8b5a92949cc", size = 17061, upload-time = "2026-10-25T21:05:35.473Z" },
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
]
[[package]]
name = "pytest-mock"
version = "3.17.0"
version = "3.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/c2/28b9f5b955ff2bcd4727a6f768e2ca9b586cd7c37b95ea5f7b9b873ac7b3/pytest_mock-3.17.0.tar.gz", hash = "sha256:e36576e0985e01027e999891e7a966c67822d40c89ef1b4c8b6e22737e3590b4", size = 43064, upload-time = "2026-10-17T16:21:42.435Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7a/7f/6ed29931d5c8cd396e7c0a55412e6cc88020373365c8685985dea53d26d7/pytest_mock-3.16.0.tar.gz", hash = "sha256:5a8395528b8f498205f3718f575228d0edaed7425fff638f87d1a6c3e0383636", size = 35362, upload-time = "2026-09-27T14:57:55.46Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/ea/47fbdc8f37410956fd445720e927589b1e2a47772c5f28109f0ce2fa26d9/pytest_mock-3.17.0-py3-none-any.whl", hash = "sha256:1b82aa4b37137248bd687055e109723debeb07b489de1565297dc9589c5f2d6b", size = 11248, upload-time = "2026-10-17T16:21:41.265Z" },
{ url = "https://files.pythonhosted.org/packages/db/5b/b83a9bf1a3b4ec222f9fa083147ff6816245223da0ab92370e7e056f113f/pytest_mock-3.16.0-py3-none-any.whl", hash = "sha256:007cfeb257801d88d9c0b2a7b5a15a15e73b71968dfd72e7bf8c4a2f8393aec8", size = 10016, upload-time = "2026-09-27T14:57:54.283Z" },
]
[[package]]
@@ -1261,27 +1164,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.17.3"
version = "0.16.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" }
sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" },
{ url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" },
{ url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" },
{ url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" },
{ url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" },
{ url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" },
{ url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" },
{ url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" },
{ url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" },
{ url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" },
{ url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" },
{ url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" },
{ url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
{ url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
{ url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
{ url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
{ url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
{ url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
{ url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
{ url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
{ url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
{ url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
{ url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
{ url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
{ url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
{ url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
{ url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
{ url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
{ url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
]
[[package]]
@@ -1293,15 +1196,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "tenacity"
version = "9.1.2"
@@ -1365,38 +1259,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "truststore"
version = "0.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd", size = 28091, upload-time = "2026-10-12T22:27:31.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c", size = 19017, upload-time = "2026-10-12T22:27:30.377Z" },
]
[[package]]
name = "ty"
version = "0.0.94"
version = "0.0.84"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/c7/1257c8c70e46274664db1eaf2b8aba99a1277470daaa39c65e7decd67c8f/ty-0.0.94.tar.gz", hash = "sha256:933d2feca6057742d04af00c090621c139e7be2c059aa38b040a70532fd5104b", size = 8106769, upload-time = "2026-10-28T18:06:09.045Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/bd/5145d91cf196cb20fc3b005515b21266a419ea7336d3d7891a1769439259/ty-0.0.94-py3-none-linux_armv6l.whl", hash = "sha256:c6463c045880ad17df911a53d367eb2554b17538ca54911bea6ccb20ec95ee11", size = 14528574, upload-time = "2026-10-28T18:05:19.423Z" },
{ url = "https://files.pythonhosted.org/packages/29/0e/2b563132429b5fc6044095061fde4584548cb55abf0d6d9f2829c41ab226/ty-0.0.94-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec3a3892f4290306c674586149668097cf44f13e8b381ba388662f5a440b459b", size = 14067146, upload-time = "2026-10-28T18:05:23.014Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/3a458362ea33ca9756cd6b914f905b3dec007d329b21d83e859ecee65a63/ty-0.0.94-py3-none-macosx_11_0_arm64.whl", hash = "sha256:74ddc6eeecd3251cf8980c9abb960d7eeab649dac5cdb7504910d99c479f84ab", size = 13148266, upload-time = "2026-10-28T18:05:25.769Z" },
{ url = "https://files.pythonhosted.org/packages/cd/38/5380bdaad2745a4e191deacce96403af74d1992ed8782d5766c301c32b50/ty-0.0.94-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31bdc3aace142400d7e22b4d32676c063ec42f0437d77940012c3dc60587ecdd", size = 13973583, upload-time = "2026-10-28T18:05:28.731Z" },
{ url = "https://files.pythonhosted.org/packages/1d/bc/7e1dc4f2594010aff6d1e2e34d0ff9003736210983be5720a04d69c27e03/ty-0.0.94-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fecacfe9c41ed1109aa503ed172d84b1b0fc283ee49aaf078d298dbe1c62e75f", size = 14269235, upload-time = "2026-10-28T18:05:31.697Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/43041f1c53164d6be21ace55047feffeaa8a9709782b01b06c951412b900/ty-0.0.94-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a49762bbc97a732847da8350ad0227c99ce2675fa32e3f58aa9f9ff4a81e4c14", size = 15191720, upload-time = "2026-10-28T18:05:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/22/72/f425d48b13cbf12b92e4f837a5866e9788e9ef4d5a071e2b12f54ee94da4/ty-0.0.94-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76a7b3aea839dfafb4bc068af918e0292b06ac26ea2a43f7ab3dd65548c0ed50", size = 15596604, upload-time = "2026-10-28T18:05:37.377Z" },
{ url = "https://files.pythonhosted.org/packages/13/ea/bd2da666ad4af596886833b095098fa9ff93685285f48bebc4d3865126e0/ty-0.0.94-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f032ce5a9c0c7033a5a8e748f952ece2e22afe2b148d211d05dca2cba8b28000", size = 15572711, upload-time = "2026-10-28T18:05:40.499Z" },
{ url = "https://files.pythonhosted.org/packages/44/f8/4f07eac57e9b60f49cc222d5048d9a61ac7f0902964ec8ac8d4ea8bc521d/ty-0.0.94-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc5e780c2107c1b83a21a394076da328e05a293ce06298df40ecab1ddc31d371", size = 16309102, upload-time = "2026-10-28T18:05:43.539Z" },
{ url = "https://files.pythonhosted.org/packages/22/39/c2ac7e14eb8ca59bd3c71b8c3055294524df84c476ba88545f59c11ceaf9/ty-0.0.94-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5bc969cdabf85aa66987c9b4818072e3af17ce5f2b7bc93550ab896c904c6c40", size = 15073645, upload-time = "2026-10-28T18:05:46.65Z" },
{ url = "https://files.pythonhosted.org/packages/d1/7d/dd4935abf50a97ccc1f46cf6c921a7acdab30f102c3c56aa4e8d6cbc3fa9/ty-0.0.94-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:27bbaff0d7bbcb3e92ed7cab07d6b26dc7ed7fa28ff69910324e5cc347c7b256", size = 13936242, upload-time = "2026-10-28T18:05:49.726Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f0/b65fa6ccde2d629e25031bf2cc2d21ac7b50ee6e0e3d29a5f471c4686c54/ty-0.0.94-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d0960eb71220300655d67db91073fd180f385186b1d186e76faa2fd3ae5bd10f", size = 14280351, upload-time = "2026-10-28T18:05:52.521Z" },
{ url = "https://files.pythonhosted.org/packages/d6/d2/5c901ad5e4f5444852b002b1f4dd3a0b470039305c688facc0483bfccd1a/ty-0.0.94-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab04c14bca92ab92106ceaa19a962a80998389700cf7938576ba3936a304355d", size = 14611163, upload-time = "2026-10-28T18:05:55.208Z" },
{ url = "https://files.pythonhosted.org/packages/6d/99/37d8633e9b5d1dcf1b06f12466825ff0c4028664994e573c352546ee9cbd/ty-0.0.94-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a92ebefa092e6bd58cb5719626858df744575473e2de0cd1c44abd44b9b84b57", size = 14938978, upload-time = "2026-10-28T18:05:58.17Z" },
{ url = "https://files.pythonhosted.org/packages/51/13/804b1f993bf84a819385f8b440fbc4b894499e2c20d865cd74d69327e7b8/ty-0.0.94-py3-none-win32.whl", hash = "sha256:3212dcf30b7567f2bb65a83cdfc4818a5385f82e7afddd83548b0d84e759057e", size = 13448009, upload-time = "2026-10-28T18:06:01.111Z" },
{ url = "https://files.pythonhosted.org/packages/a1/23/81850750cda0ea781ba9d6e08cea1a1039361cb3474c7dd03765bbc4b51f/ty-0.0.94-py3-none-win_amd64.whl", hash = "sha256:daf5bafe8e7d21b8a99a56fc8518f2342a2dbcaa370a5c25cfaedc684088d158", size = 14010576, upload-time = "2026-10-28T18:06:03.773Z" },
{ url = "https://files.pythonhosted.org/packages/d7/d4/ac3a759556493230823abd31e149c95401f96b9cac36567bd403a8454ea3/ty-0.0.94-py3-none-win_arm64.whl", hash = "sha256:ce344353ccdc542cfcfd6abe1481cf23c005f483411ea79652b78ce0beae36f2", size = 13547398, upload-time = "2026-10-28T18:06:06.379Z" },
{ url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" },
{ url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" },
{ url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" },
{ url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" },
{ url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" },
{ url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" },
{ url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" },
{ url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" },
{ url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" },
{ url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" },
{ url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" },
]
[[package]]
@@ -5,5 +5,5 @@ description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langgraph==1.2.19"
"langgraph==1.2.6"
]
@@ -7,7 +7,7 @@ requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.1.14",
"langchain-anthropic==1.4.6",
"langgraph==1.2.19"
"langgraph==1.2.6"
]
[tool.uv]
+9 -9
View File
@@ -21,8 +21,8 @@
"test:all": "yarn test && yarn test:int && yarn lint:langgraph"
},
"dependencies": {
"@langchain/core": "^1.3.1",
"@langchain/langgraph": "^1.5.3"
"@langchain/core": "^1.2.13",
"@langchain/langgraph": "^1.4.18"
},
"resolutions": {
"@langchain/langgraph-checkpoint": "1.0.4",
@@ -34,17 +34,17 @@
"@eslint/js": "^10.0.1",
"@tsconfig/recommended": "^1.0.13",
"@types/jest": "^30.0.0",
"@typescript-eslint/eslint-plugin": "^8.72.1",
"@typescript-eslint/parser": "^8.72.1",
"dotenv": "^18.0.9",
"eslint": "^10.13.0",
"@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.70.1",
"dotenv": "^18.0.4",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-prettier": "^5.5.6",
"jest": "^30.5.2",
"prettier": "^3.9.10",
"ts-jest": "^29.4.15",
"prettier": "^3.9.9",
"ts-jest": "^29.4.14",
"typescript": "^7.0.2"
}
}
+123 -126
View File
@@ -387,11 +387,6 @@
resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
"@dual-bundle/import-meta-resolve@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz#cd0b25b3808cd9e684cd6cd549bbf8e1dcf05ee7"
integrity sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==
"@emnapi/core@1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467"
@@ -895,50 +890,50 @@
resolved "https://registry.yarnpkg.com/@keyv/serialize/-/serialize-1.1.1.tgz#0c01dd3a3483882af7cf3878d4e71d505c81fc4a"
integrity sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==
"@langchain/core@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.3.1.tgz#f6cdb6d200def5c2e69d25ff4fef92612d1b7601"
integrity sha512-jt0miXWxNi+rbZLeSeZkc7BBYKnWopksoJSpB+P3zaYgzMMNwkAPWqnd1zSClfYGwLZPXQbbTtaqv8ZFXmE3FA==
"@langchain/core@^1.2.13":
version "1.2.13"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.2.13.tgz#4ea4eb0308a25af350f75de2fbf34aecb819177e"
integrity sha512-ADGTxZ84n3civruUX1LlTOdua/PDGoni2U6TmKTX7hvRU2Jlepu8vLJI4Wnwj45KUKhUCrW94Il12Q2bxE3e8A==
dependencies:
"@cfworker/json-schema" "^4.0.2"
"@standard-schema/spec" "^1.1.0"
js-tiktoken "^1.0.12"
langsmith ">=0.11.0 <1.0.0"
langsmith ">=0.5.0 <1.0.0"
mustache "^4.2.0"
p-queue "^6.6.2"
zod "^3.25.76 || ^4"
"@langchain/langgraph-checkpoint@1.0.4", "@langchain/langgraph-checkpoint@^1.2.1":
"@langchain/langgraph-checkpoint@1.0.4", "@langchain/langgraph-checkpoint@^1.1.5":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.4.tgz#893aa7255b2fe810fdeb50281f6750a3b0a6fef7"
integrity sha512-1y5MgZ0gXXrtmoy56e3kaBChI3GwFPIKl27xkrHwN+VE/3iUsyr9gO3Jtp7kdKAe6diZGbcas5bdC/r0yUwTZA==
dependencies:
uuid "^14.0.0"
"@langchain/langgraph-sdk@~1.12.8":
version "1.12.9"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.12.9.tgz#b8e66e00af207f18e56eb6cd916c1ee8e7608745"
integrity sha512-A1LkLPVu00dj+9aErXPBLhmFURHtnnRO9KRFST+xAwurqQTa+LxC9ixxF7p+ZMo5P70qMFoaP8Ud2wuZmYD4xw==
"@langchain/langgraph-sdk@~1.12.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.12.0.tgz#b063a7eba0a66f51cbc841a824626a47eca1eec9"
integrity sha512-F3AOZjKZRUGmE3FzY+RaVZI6WzXOkwnuF7jqgto6rDPSnO9OdVdYGvwGDi3jjRGf5xLoDtX2dpsR9z5KptU+5A==
dependencies:
"@langchain/protocol" "^0.0.20"
"@langchain/protocol" "^0.0.19"
"@types/json-schema" "^7.0.15"
p-queue "^9.0.1"
p-retry "^7.1.1"
"@langchain/langgraph@^1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.5.3.tgz#9a921c9e099c2e3009f34987e0acb2645636bb74"
integrity sha512-cuqswNgfoF/J3/r9K5Qm7EnQc2f0mz7uWlAAs751KWhoK8sKfmuLABghF4b1LYq1NpK72XG1x8lWPyg45M8BOA==
"@langchain/langgraph@^1.4.18":
version "1.4.18"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.4.18.tgz#c8cc4ea734834262fdb00ae6a45a0849620e4fd2"
integrity sha512-yrMMJ9hk2NVMD2xU2WoVrgFawAU6s/RzEl/dX9BhlyxZHazo1wHY35z4K2BIBzTUh4z3giCzrUoqRAqW2CWpWg==
dependencies:
"@langchain/langgraph-checkpoint" "^1.2.1"
"@langchain/langgraph-sdk" "~1.12.8"
"@langchain/protocol" "^0.0.20"
"@langchain/langgraph-checkpoint" "^1.1.5"
"@langchain/langgraph-sdk" "~1.12.0"
"@langchain/protocol" "^0.0.19"
"@standard-schema/spec" "1.1.0"
"@langchain/protocol@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.20.tgz#441a57a6176f634306d9214da65d35033355d5e9"
integrity sha512-sxlkDG2/HGyjDFLTJ8B/o3oglRjOv8iLCFgrV0550/Vym3V6RZ0SUjAQFe8jrLQw8ZXjERy3swJp5OODBSrUYA==
"@langchain/protocol@^0.0.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.19.tgz#7fe43fed115dc34b3b4c246e8d5283fbcbfab7b0"
integrity sha512-9hKcRrH7cBX6gfutdfXPoft1OCchHe4FEpALoDJMl5Qu+n/YG5ynZmyu8+8cxORlPwHBoKTxggvXz+76M1yX1Q==
"@napi-rs/wasm-runtime@^1.1.4":
version "1.2.3"
@@ -1185,100 +1180,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz#95e29de63ce7ed793eb1c6efd9f8b802ce808262"
integrity sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==
"@typescript-eslint/eslint-plugin@^8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz#d404bac4cb04e4dcd20086e46e13c952bd0771e0"
integrity sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.72.1"
"@typescript-eslint/type-utils" "8.72.1"
"@typescript-eslint/utils" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/scope-manager" "8.70.1"
"@typescript-eslint/type-utils" "8.70.1"
"@typescript-eslint/utils" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.72.1.tgz#7370b68dadc3819b9aac68ea340bd8b43ee62379"
integrity sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==
"@typescript-eslint/parser@^8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.70.1.tgz#6c99b7b954efc05dae179001c8b477d1a8948dcf"
integrity sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==
dependencies:
"@typescript-eslint/scope-manager" "8.72.1"
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/typescript-estree" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/scope-manager" "8.70.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/typescript-estree" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.72.1.tgz#1d09902a9c23f6cfab779e5acc78a45abf540add"
integrity sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==
"@typescript-eslint/project-service@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.70.1.tgz#d5635594cc8a573f0e15a772b1704e93a00134c1"
integrity sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.72.1"
"@typescript-eslint/types" "^8.72.1"
"@typescript-eslint/tsconfig-utils" "^8.70.1"
"@typescript-eslint/types" "^8.70.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz#a0fb4dfe5aa2b0149b7a2d1fd00d947992b13067"
integrity sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==
"@typescript-eslint/scope-manager@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz#0feeae4da2d279b960cdbfe5e0a02fbf1195e93f"
integrity sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==
dependencies:
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
"@typescript-eslint/tsconfig-utils@8.72.1", "@typescript-eslint/tsconfig-utils@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz#07394c85095ee38455e76bc221ba23650af73e44"
integrity sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==
"@typescript-eslint/tsconfig-utils@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz#e9353a6e5f3498a29b40946ca3e11f6166fef09e"
integrity sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==
"@typescript-eslint/type-utils@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz#f1f592f63bd9726de21191aae3ccf64a813914b8"
integrity sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==
"@typescript-eslint/tsconfig-utils@^8.70.1":
version "8.71.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz#83a69c9a514af8f5ff099cc6c0d609c273e8f039"
integrity sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==
"@typescript-eslint/type-utils@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz#6ea36f2c7dc0776c1c8e1c380b4896f00bc4c27f"
integrity sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==
dependencies:
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/typescript-estree" "8.72.1"
"@typescript-eslint/utils" "8.72.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/typescript-estree" "8.70.1"
"@typescript-eslint/utils" "8.70.1"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.72.1", "@typescript-eslint/types@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.72.1.tgz#067d8f738c294611e33a1425ae43ac9b9c2f24e3"
integrity sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==
"@typescript-eslint/types@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.70.1.tgz#2022b9d07d057174eba25b9bbd289641d262a196"
integrity sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==
"@typescript-eslint/typescript-estree@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz#e93b9cc3a56bf12f5fed857bb8f4963cce7924ee"
integrity sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==
"@typescript-eslint/types@^8.70.1":
version "8.71.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.71.0.tgz#a230a121e71665060afe27d1f1ad110371f9451e"
integrity sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==
"@typescript-eslint/typescript-estree@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz#ced48fee6f7043835d2edfed9786ae74813a3c99"
integrity sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==
dependencies:
"@typescript-eslint/project-service" "8.72.1"
"@typescript-eslint/tsconfig-utils" "8.72.1"
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/project-service" "8.70.1"
"@typescript-eslint/tsconfig-utils" "8.70.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.72.1.tgz#f210c1bf4a25aa1306df80c702ed30bbbc9b4d3d"
integrity sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==
"@typescript-eslint/utils@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.70.1.tgz#6a97887cee702df513692f5cdc7f79baeef48e02"
integrity sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.72.1"
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/typescript-estree" "8.72.1"
"@typescript-eslint/scope-manager" "8.70.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/typescript-estree" "8.70.1"
"@typescript-eslint/visitor-keys@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz#c7aec9883b44ef795f1b8200df81d9d44d355de7"
integrity sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==
"@typescript-eslint/visitor-keys@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz#b4cde0ed16982bd963a6310fcae554f59fd14124"
integrity sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==
dependencies:
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/types" "8.70.1"
eslint-visitor-keys "^5.0.0"
"@typescript/typescript-aix-ppc64@7.0.2":
@@ -2104,10 +2109,10 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
dotenv@^18.0.9:
version "18.0.9"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-18.0.9.tgz#5c565760ce7e6fb69f9477ebcd1a3b64c766f9c8"
integrity sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==
dotenv@^18.0.4:
version "18.0.4"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-18.0.4.tgz#419bbe784f30915bfeb10e2d407e13ca5528504f"
integrity sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==
dunder-proto@^1.0.0, dunder-proto@^1.0.1:
version "1.0.1"
@@ -2425,13 +2430,13 @@ eslint-plugin-no-instanceof@^1.0.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-no-instanceof/-/eslint-plugin-no-instanceof-1.0.1.tgz#5d9fc86d160df6991b654b294a62390207f1bb97"
integrity sha512-zlqQ7EsfzbRO68uI+p8FIE7zYB4njs+nNbkNjSb5QmLi2et67zQLqSeaao5U9SpnlZTTJC87nS2oyHo2ACtajw==
eslint-plugin-prettier@^5.5.7:
version "5.5.7"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.7.tgz#ebef3acc2d664333b1356b717780198abc635398"
integrity sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==
eslint-plugin-prettier@^5.5.6:
version "5.5.6"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz#363ebe4d769bce157ccdd8129ce3efd91dc62564"
integrity sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==
dependencies:
prettier-linter-helpers "^1.0.1"
synckit "^0.12.3"
synckit "^0.11.13"
eslint-scope@^9.1.2:
version "9.1.2"
@@ -2458,10 +2463,10 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
eslint@^10.13.0:
version "10.13.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.13.0.tgz#a8a4742648c0fa9131f429b509de5407682d4864"
integrity sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==
eslint@^10.11.0:
version "10.11.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.11.0.tgz#304d1591b7c6a327e3f64b4f550f99fda160ae20"
integrity sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.2"
@@ -2861,7 +2866,7 @@ graceful-fs@^4.2.11:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
handlebars@^4.7.10:
handlebars@^4.7.9:
version "4.7.10"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.10.tgz#12419ea1b41391b3ffcf003f2164dcbce090c417"
integrity sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==
@@ -3849,10 +3854,10 @@ keyv@^5.6.0:
dependencies:
"@keyv/serialize" "^1.1.1"
"langsmith@>=0.11.0 <1.0.0":
version "0.11.6"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.11.6.tgz#cdc0f89a14632f8b172deea5daa1ee625f7442c3"
integrity sha512-5hfJM5DEZpXFxUS3U9nY2CMyBRmFnhkJj7znUODJ8vsr/Jx38tGJl4GIhV484xAY7yQFWrFOTDr33PB95kHUKg==
"langsmith@>=0.5.0 <1.0.0":
version "0.6.3"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.6.3.tgz#a3d8ad58d66a47d3697e3c69b2be3f6df5233190"
integrity sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==
dependencies:
p-queue "6.6.2"
@@ -4307,10 +4312,10 @@ prettier-linter-helpers@^1.0.1:
dependencies:
fast-diff "^1.1.2"
prettier@^3.9.10:
version "3.9.10"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.10.tgz#ac378e432f1a7381518f2e080816bbaefa38d585"
integrity sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==
prettier@^3.9.9:
version "3.9.9"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.9.tgz#09b826918c91cd4cbc80e0cbd1d2a922ff04f233"
integrity sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==
pretty-format@30.2.0, pretty-format@^30.0.0:
version "30.2.0"
@@ -4754,21 +4759,13 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
synckit@^0.11.8:
synckit@^0.11.13, synckit@^0.11.8:
version "0.11.13"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.13.tgz#062a5ea57d81befc35892f8254de5c567e97c80a"
integrity sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==
dependencies:
"@pkgr/core" "^0.3.6"
synckit@^0.12.3:
version "0.12.3"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.12.3.tgz#6e30a3441d095ed04c311077350ce205465512d6"
integrity sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==
dependencies:
"@dual-bundle/import-meta-resolve" "^4.2.1"
"@pkgr/core" "^0.3.6"
test-exclude@^7.0.1:
version "7.0.2"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.2.tgz#482392077630bc57d5630c13abe908bb910dfc65"
@@ -4803,14 +4800,14 @@ ts-api-utils@^2.5.0:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1"
integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==
ts-jest@^29.4.15:
version "29.4.15"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.15.tgz#238f2d495066a33ca973a54a36a5ef228262a9ad"
integrity sha512-s2qmEUYFRqbjz1wv0EmccNPVOb45HOFpFuhzQJLKShYVisuTbCFPIIAIV2EuK5rIxb4BejLDbH7dnBZfYjybUQ==
ts-jest@^29.4.14:
version "29.4.14"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.14.tgz#cb36c043df0fada861c8b604a5d514f953126180"
integrity sha512-+943G6wCX5E3VpzTQhBGBFntCmdT+QAkTvUlp03edTONFBvqskuXPhpyX16xdUotO4P5HQKyppldSWjocoltLQ==
dependencies:
bs-logger "^0.2.6"
fast-json-stable-stringify "^2.1.0"
handlebars "^4.7.10"
handlebars "^4.7.9"
json5 "^2.2.3"
lodash.memoize "^4.1.2"
make-error "^1.3.6"
@@ -9,8 +9,8 @@
},
"dependencies": {
"@js-monorepo-example/shared": "*",
"@langchain/core": "^1.3.1",
"@langchain/langgraph": "^1.5.3"
"@langchain/core": "^1.2.13",
"@langchain/langgraph": "^1.4.18"
},
"devDependencies": {
"typescript": "^7.0.2"
+6 -6
View File
@@ -17,18 +17,18 @@
"lint": "eslint 'apps/**/*.ts' 'libs/**/*.ts'"
},
"devDependencies": {
"turbo": "^2.11.10",
"turbo": "^2.11.5",
"typescript": "^7.0.2",
"@tsconfig/recommended": "^1.0.13",
"@eslint/eslintrc": "^3.3.7",
"@eslint/js": "^10.0.1",
"eslint": "^10.13.0",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^8.72.1",
"@typescript-eslint/parser": "^8.72.1",
"prettier": "^3.9.10"
"eslint-plugin-prettier": "^5.5.6",
"@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.70.1",
"prettier": "^3.9.9"
}
}
+156 -152
View File
@@ -25,11 +25,6 @@
resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
"@dual-bundle/import-meta-resolve@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz#cd0b25b3808cd9e684cd6cd549bbf8e1dcf05ee7"
integrity sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==
"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
@@ -139,48 +134,48 @@
resolved "https://registry.yarnpkg.com/@keyv/serialize/-/serialize-1.1.1.tgz#0c01dd3a3483882af7cf3878d4e71d505c81fc4a"
integrity sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==
"@langchain/core@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.3.1.tgz#f6cdb6d200def5c2e69d25ff4fef92612d1b7601"
integrity sha512-jt0miXWxNi+rbZLeSeZkc7BBYKnWopksoJSpB+P3zaYgzMMNwkAPWqnd1zSClfYGwLZPXQbbTtaqv8ZFXmE3FA==
"@langchain/core@^1.2.13":
version "1.2.13"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.2.13.tgz#4ea4eb0308a25af350f75de2fbf34aecb819177e"
integrity sha512-ADGTxZ84n3civruUX1LlTOdua/PDGoni2U6TmKTX7hvRU2Jlepu8vLJI4Wnwj45KUKhUCrW94Il12Q2bxE3e8A==
dependencies:
"@cfworker/json-schema" "^4.0.2"
"@standard-schema/spec" "^1.1.0"
js-tiktoken "^1.0.12"
langsmith ">=0.11.0 <1.0.0"
langsmith ">=0.5.0 <1.0.0"
mustache "^4.2.0"
p-queue "^6.6.2"
zod "^3.25.76 || ^4"
"@langchain/langgraph-checkpoint@^1.2.1":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.2.2.tgz#cbfdf0d461385769665e26f70e41f1291ed1d37e"
integrity sha512-KL3nzi7I61ll5nANCQe5RoJ5SqTFrE+si9IK6k7IIj9OrPkIKT/Dz0w8CyJF+K7OEuJfA33HvPrZqfzzW1DXDA==
"@langchain/langgraph-checkpoint@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.5.tgz#c793177f9afcce31a1e9922f317f88fec1254282"
integrity sha512-BwDwl5VeTOh6CVuiIPgsUgfK51vTJDMSbFcSCUfjJWsl8/DPdK/mbv+ejxJstkSk/BlSPMP4JfXWcN6jD2ea2Q==
"@langchain/langgraph-sdk@~1.12.8":
version "1.12.9"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.12.9.tgz#b8e66e00af207f18e56eb6cd916c1ee8e7608745"
integrity sha512-A1LkLPVu00dj+9aErXPBLhmFURHtnnRO9KRFST+xAwurqQTa+LxC9ixxF7p+ZMo5P70qMFoaP8Ud2wuZmYD4xw==
"@langchain/langgraph-sdk@~1.12.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.12.0.tgz#b063a7eba0a66f51cbc841a824626a47eca1eec9"
integrity sha512-F3AOZjKZRUGmE3FzY+RaVZI6WzXOkwnuF7jqgto6rDPSnO9OdVdYGvwGDi3jjRGf5xLoDtX2dpsR9z5KptU+5A==
dependencies:
"@langchain/protocol" "^0.0.20"
"@langchain/protocol" "^0.0.19"
"@types/json-schema" "^7.0.15"
p-queue "^9.0.1"
p-retry "^7.1.1"
"@langchain/langgraph@^1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.5.3.tgz#9a921c9e099c2e3009f34987e0acb2645636bb74"
integrity sha512-cuqswNgfoF/J3/r9K5Qm7EnQc2f0mz7uWlAAs751KWhoK8sKfmuLABghF4b1LYq1NpK72XG1x8lWPyg45M8BOA==
"@langchain/langgraph@^1.4.18":
version "1.4.18"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.4.18.tgz#c8cc4ea734834262fdb00ae6a45a0849620e4fd2"
integrity sha512-yrMMJ9hk2NVMD2xU2WoVrgFawAU6s/RzEl/dX9BhlyxZHazo1wHY35z4K2BIBzTUh4z3giCzrUoqRAqW2CWpWg==
dependencies:
"@langchain/langgraph-checkpoint" "^1.2.1"
"@langchain/langgraph-sdk" "~1.12.8"
"@langchain/protocol" "^0.0.20"
"@langchain/langgraph-checkpoint" "^1.1.5"
"@langchain/langgraph-sdk" "~1.12.0"
"@langchain/protocol" "^0.0.19"
"@standard-schema/spec" "1.1.0"
"@langchain/protocol@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.20.tgz#441a57a6176f634306d9214da65d35033355d5e9"
integrity sha512-sxlkDG2/HGyjDFLTJ8B/o3oglRjOv8iLCFgrV0550/Vym3V6RZ0SUjAQFe8jrLQw8ZXjERy3swJp5OODBSrUYA==
"@langchain/protocol@^0.0.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.19.tgz#7fe43fed115dc34b3b4c246e8d5283fbcbfab7b0"
integrity sha512-9hKcRrH7cBX6gfutdfXPoft1OCchHe4FEpALoDJMl5Qu+n/YG5ynZmyu8+8cxORlPwHBoKTxggvXz+76M1yX1Q==
"@pkgr/core@^0.3.6":
version "0.3.6"
@@ -202,35 +197,35 @@
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.13.tgz#269fce3ad04ca70b93269ff44cca81b950f542da"
integrity sha512-sySRuBfMKyKO/j2ZAhR8kSembhjuPEV4Ra3AHtmWLq51+iGaudr45crPSzNC5b7/Ctrh9dfUpBuTlYrH6rM58Q==
"@turbo/darwin-64@2.11.10":
version "2.11.10"
resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.11.10.tgz#824aa74de92806ef2a21932b4148feaea98e8887"
integrity sha512-By4idRap5+nLnMuRzLB4qkOPRlvNdyY1ne4VJYDyUoUB11H94k7uif9mbe3JT8JSNXs1D68uMcBj6hJ/jMQRvA==
"@turbo/darwin-64@2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.11.5.tgz#6ddc580c2b7d05e578ca1c06b3f61473f4b7b00e"
integrity sha512-KdY4y7BBjIisl+jAkb3FFaRjClGqHyaeHCcVcCT8LBzFFwUu6thgfxyRZAx0uL4KoxLmH+iOan8UhVf/aDDcaA==
"@turbo/darwin-arm64@2.11.10":
version "2.11.10"
resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.11.10.tgz#e9de7c9b9eb2a428cd4fb63fb9826696107339a4"
integrity sha512-pyEYlVcuexOPa92p0Wlwl9YT1Is5D60lxN3ItWm3g30YLvFaUq86TF4BBWg5ZAc0uMTnrOzCvuCsOBTdzUWnRQ==
"@turbo/darwin-arm64@2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.11.5.tgz#a756c67bb828cd00d03e909769dd39453c1f1e69"
integrity sha512-ToZDGi2u410+TfRtC/O1/LpujGgvNJoRBZnwVXob7oO0+3OSlwIO3zofXKUvwQvlba7a8ZXPyCZwufjAXd7f0w==
"@turbo/linux-64@2.11.10":
version "2.11.10"
resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.11.10.tgz#e9f48a6383e7c6926a488f329343792de3ee537c"
integrity sha512-1tYeR/IDZ5HEdsb0jNq2L2xam5nMQ+g80tlV2BZmv4OFw+pMcUM0mMmUcfc5wXPPNHVNPuDMjh9Y8G8HgXPLZA==
"@turbo/linux-64@2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.11.5.tgz#b95c05218aebb06c06e5f991d48c55b78dc5e005"
integrity sha512-Ub2d/nAbdWLaSytcEaPp1wm/YuIvHEIqNrCRMI/UgOGyFdOhgxm8VyWPAJilQWRYxYVrqEUoy2K5S5K8SG50rA==
"@turbo/linux-arm64@2.11.10":
version "2.11.10"
resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.11.10.tgz#d76831f850d30ef386b221b8ee72270a5ee7e2ba"
integrity sha512-HuekQxvVGhUd46g1Im/DKdOtglHr4/gguZAvzEpPUgtxNkztSa8lWhQEGQ+EmPQ0n7w19qjThFU+OfEEd0uvlw==
"@turbo/linux-arm64@2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.11.5.tgz#c7a1d1a44c8c8dcff4b72fbafe9a5059c8fb668a"
integrity sha512-JQjmGxp724Wuft9FAxPMtmBur1CEdUyxJ+FVoIvKkgj63rXHl9PYCWrsU2tMjVi609XTQ3M8vZD9Yc9qVAVYrw==
"@turbo/windows-64@2.11.10":
version "2.11.10"
resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.11.10.tgz#fb29a315a112afac9f01a4a1ea7601b28f140f31"
integrity sha512-UJOP3BkbreY3/JlMgJR6ZiimxYIBn+c+B4tnBIxD0E9c3Estmk+XoVNVBNBottZLNRXe3/ScgOU4+/k8tIWysg==
"@turbo/windows-64@2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.11.5.tgz#60bdaaa7e8ba32f32623b31a1c75f6c3db400a0c"
integrity sha512-AFkbPmXNd8XgCi/66YEHR2bVl3gtusXyDihTIVEPAuSB+7ulLmJePvz1unWPVKLHdbCTTIdKq2mpJ1B1e3lJXw==
"@turbo/windows-arm64@2.11.10":
version "2.11.10"
resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.11.10.tgz#bac3077b0a39643043ece360ff1f4f2af9272106"
integrity sha512-xOkEjF3ws/b/YNlc+bvJHMYBn+vL+QbEISZFVi0qS+ImmhA1X3/gmNmBrjjIG3dg6aeN+n6gQitzSkXQ286qJQ==
"@turbo/windows-arm64@2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.11.5.tgz#18bd5a0ad30c4e094995786bd78916b6de6b5c1c"
integrity sha512-52ziXYP3snGOxjv19xA3WL1WDKv9Q6uDAa/7EKIR5j4hocMlYURkrOHbJ2ZZI+VfPU5MwKpcGeRfHxHJu7Y/Pw==
"@types/esrecurse@^4.3.1":
version "4.3.1"
@@ -252,100 +247,110 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@typescript-eslint/eslint-plugin@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz#95e29de63ce7ed793eb1c6efd9f8b802ce808262"
integrity sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==
"@typescript-eslint/eslint-plugin@^8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz#d404bac4cb04e4dcd20086e46e13c952bd0771e0"
integrity sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.72.1"
"@typescript-eslint/type-utils" "8.72.1"
"@typescript-eslint/utils" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/scope-manager" "8.70.1"
"@typescript-eslint/type-utils" "8.70.1"
"@typescript-eslint/utils" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.72.1.tgz#7370b68dadc3819b9aac68ea340bd8b43ee62379"
integrity sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==
"@typescript-eslint/parser@^8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.70.1.tgz#6c99b7b954efc05dae179001c8b477d1a8948dcf"
integrity sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==
dependencies:
"@typescript-eslint/scope-manager" "8.72.1"
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/typescript-estree" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/scope-manager" "8.70.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/typescript-estree" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.72.1.tgz#1d09902a9c23f6cfab779e5acc78a45abf540add"
integrity sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==
"@typescript-eslint/project-service@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.70.1.tgz#d5635594cc8a573f0e15a772b1704e93a00134c1"
integrity sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.72.1"
"@typescript-eslint/types" "^8.72.1"
"@typescript-eslint/tsconfig-utils" "^8.70.1"
"@typescript-eslint/types" "^8.70.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz#a0fb4dfe5aa2b0149b7a2d1fd00d947992b13067"
integrity sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==
"@typescript-eslint/scope-manager@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz#0feeae4da2d279b960cdbfe5e0a02fbf1195e93f"
integrity sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==
dependencies:
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
"@typescript-eslint/tsconfig-utils@8.72.1", "@typescript-eslint/tsconfig-utils@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz#07394c85095ee38455e76bc221ba23650af73e44"
integrity sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==
"@typescript-eslint/tsconfig-utils@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz#e9353a6e5f3498a29b40946ca3e11f6166fef09e"
integrity sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==
"@typescript-eslint/type-utils@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz#f1f592f63bd9726de21191aae3ccf64a813914b8"
integrity sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==
"@typescript-eslint/tsconfig-utils@^8.70.1":
version "8.71.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz#83a69c9a514af8f5ff099cc6c0d609c273e8f039"
integrity sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==
"@typescript-eslint/type-utils@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz#6ea36f2c7dc0776c1c8e1c380b4896f00bc4c27f"
integrity sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==
dependencies:
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/typescript-estree" "8.72.1"
"@typescript-eslint/utils" "8.72.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/typescript-estree" "8.70.1"
"@typescript-eslint/utils" "8.70.1"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.72.1", "@typescript-eslint/types@^8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.72.1.tgz#067d8f738c294611e33a1425ae43ac9b9c2f24e3"
integrity sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==
"@typescript-eslint/types@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.70.1.tgz#2022b9d07d057174eba25b9bbd289641d262a196"
integrity sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==
"@typescript-eslint/typescript-estree@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz#e93b9cc3a56bf12f5fed857bb8f4963cce7924ee"
integrity sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==
"@typescript-eslint/types@^8.70.1":
version "8.71.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.71.0.tgz#a230a121e71665060afe27d1f1ad110371f9451e"
integrity sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==
"@typescript-eslint/typescript-estree@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz#ced48fee6f7043835d2edfed9786ae74813a3c99"
integrity sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==
dependencies:
"@typescript-eslint/project-service" "8.72.1"
"@typescript-eslint/tsconfig-utils" "8.72.1"
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/visitor-keys" "8.72.1"
"@typescript-eslint/project-service" "8.70.1"
"@typescript-eslint/tsconfig-utils" "8.70.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/visitor-keys" "8.70.1"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.72.1.tgz#f210c1bf4a25aa1306df80c702ed30bbbc9b4d3d"
integrity sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==
"@typescript-eslint/utils@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.70.1.tgz#6a97887cee702df513692f5cdc7f79baeef48e02"
integrity sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.72.1"
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/typescript-estree" "8.72.1"
"@typescript-eslint/scope-manager" "8.70.1"
"@typescript-eslint/types" "8.70.1"
"@typescript-eslint/typescript-estree" "8.70.1"
"@typescript-eslint/visitor-keys@8.72.1":
version "8.72.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz#c7aec9883b44ef795f1b8200df81d9d44d355de7"
integrity sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==
"@typescript-eslint/visitor-keys@8.70.1":
version "8.70.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz#b4cde0ed16982bd963a6310fcae554f59fd14124"
integrity sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==
dependencies:
"@typescript-eslint/types" "8.72.1"
"@typescript-eslint/types" "8.70.1"
eslint-visitor-keys "^5.0.0"
"@typescript/typescript-aix-ppc64@7.0.2":
@@ -885,13 +890,13 @@ eslint-plugin-no-instanceof@^1.0.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-no-instanceof/-/eslint-plugin-no-instanceof-1.0.1.tgz#5d9fc86d160df6991b654b294a62390207f1bb97"
integrity sha512-zlqQ7EsfzbRO68uI+p8FIE7zYB4njs+nNbkNjSb5QmLi2et67zQLqSeaao5U9SpnlZTTJC87nS2oyHo2ACtajw==
eslint-plugin-prettier@^5.5.7:
version "5.5.7"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.7.tgz#ebef3acc2d664333b1356b717780198abc635398"
integrity sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==
eslint-plugin-prettier@^5.5.6:
version "5.5.6"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz#363ebe4d769bce157ccdd8129ce3efd91dc62564"
integrity sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==
dependencies:
prettier-linter-helpers "^1.0.1"
synckit "^0.12.3"
synckit "^0.11.13"
eslint-scope@^9.1.2:
version "9.1.2"
@@ -918,10 +923,10 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
eslint@^10.13.0:
version "10.13.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.13.0.tgz#a8a4742648c0fa9131f429b509de5407682d4864"
integrity sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==
eslint@^10.11.0:
version "10.11.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.11.0.tgz#304d1591b7c6a327e3f64b4f550f99fda160ae20"
integrity sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.2"
@@ -1471,10 +1476,10 @@ keyv@^5.6.0:
dependencies:
"@keyv/serialize" "^1.1.1"
"langsmith@>=0.11.0 <1.0.0":
version "0.11.6"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.11.6.tgz#cdc0f89a14632f8b172deea5daa1ee625f7442c3"
integrity sha512-5hfJM5DEZpXFxUS3U9nY2CMyBRmFnhkJj7znUODJ8vsr/Jx38tGJl4GIhV484xAY7yQFWrFOTDr33PB95kHUKg==
"langsmith@>=0.5.0 <1.0.0":
version "0.7.1"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.7.1.tgz#d721ad2e5211f9a5373e8a84eb4fed36b5621971"
integrity sha512-Wjk90UjNoY5cBHMlNAC/eZx5clI8jnjBOBW8uJu8+MWBtx0QesNjsUiLtjI+I3UnrpxFFpDqGXcnhBjH654Mqg==
dependencies:
p-queue "6.6.2"
@@ -1702,10 +1707,10 @@ prettier-linter-helpers@^1.0.1:
dependencies:
fast-diff "^1.1.2"
prettier@^3.9.10:
version "3.9.10"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.10.tgz#ac378e432f1a7381518f2e080816bbaefa38d585"
integrity sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==
prettier@^3.9.9:
version "3.9.9"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.9.tgz#09b826918c91cd4cbc80e0cbd1d2a922ff04f233"
integrity sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==
punycode@^2.1.0:
version "2.3.1"
@@ -1935,12 +1940,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
synckit@^0.12.3:
version "0.12.3"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.12.3.tgz#6e30a3441d095ed04c311077350ce205465512d6"
integrity sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==
synckit@^0.11.13:
version "0.11.13"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.13.tgz#062a5ea57d81befc35892f8254de5c567e97c80a"
integrity sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==
dependencies:
"@dual-bundle/import-meta-resolve" "^4.2.1"
"@pkgr/core" "^0.3.6"
tinyglobby@^0.2.15:
@@ -1966,17 +1970,17 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
turbo@^2.11.10:
version "2.11.10"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.11.10.tgz#8b1d40debab2b871ff9e908d6f3018aeda04879d"
integrity sha512-3GJYwig8ygEI89oaOZdduJX8/aw6O9eRPYHoIt6T3FZl9hkKjiyIspufJoVgqXihPoJu8M5CSV0DlZvzfohBCw==
turbo@^2.11.5:
version "2.11.5"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.11.5.tgz#e7223abd0555b978d3efe1b00ea9a98f43afb7ab"
integrity sha512-qLXheqz3TUvJpHIV6MUBwkNkvwKxMVG/QsimyiKT9DfNWPHJ294G/i5nDaH+fbHJt3UH7orubNNxAqYkbUNGAQ==
optionalDependencies:
"@turbo/darwin-64" "2.11.10"
"@turbo/darwin-arm64" "2.11.10"
"@turbo/linux-64" "2.11.10"
"@turbo/linux-arm64" "2.11.10"
"@turbo/windows-64" "2.11.10"
"@turbo/windows-arm64" "2.11.10"
"@turbo/darwin-64" "2.11.5"
"@turbo/darwin-arm64" "2.11.5"
"@turbo/linux-64" "2.11.5"
"@turbo/linux-arm64" "2.11.5"
"@turbo/windows-64" "2.11.5"
"@turbo/windows-arm64" "2.11.5"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
@@ -2144,6 +2148,6 @@ yocto-queue@^0.1.0:
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
"zod@^3.25.76 || ^4":
version "4.6.7"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.6.7.tgz#e806cf64a7f4e37635ce9f2f52c04c3766f75e5b"
integrity sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==
version "4.3.6"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a"
integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==
+4
View File
@@ -1589,6 +1589,8 @@ ADD {relpath} /deps/{name}
docker_file_contents.extend(
[
f"FROM {image_str}",
# BuildKit secrets do not invalidate RUN caches; hosted builds supply this hash.
"ARG USER_ENV_VARS_HASH",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
@@ -1679,6 +1681,8 @@ def node_config_to_docker(
docker_file_contents = [
f"FROM {image_str}",
# BuildKit secrets do not invalidate RUN caches; hosted builds supply this hash.
"ARG USER_ENV_VARS_HASH",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
+1
View File
@@ -2431,6 +2431,7 @@ def _deploy_cmd(
validate_deploy_commands(install_command, build_command)
agent = None
if agent_id is not None or environment is not None:
em.note("Note: --agent-id and --agent-environment flags are in private beta")
if not agent_id or not agent_id.strip() or not environment:
raise click.UsageError(
"--agent-id and --agent-environment are required together."
+2
View File
@@ -955,6 +955,8 @@ def python_config_to_docker_uv_lock(
docker_plan.add_blank()
docker_plan.add_instruction("FROM", image_str)
# BuildKit secrets do not invalidate RUN caches; hosted builds supply this hash.
docker_plan.add_instruction("ARG", "USER_ENV_VARS_HASH")
docker_plan.add_blank()
docker_plan.extend_nonempty(config["dockerfile_lines"])
if config["dockerfile_lines"]:
@@ -148,6 +148,7 @@ services:
dockerfile_inline: |
# syntax=docker/dockerfile:1.4
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding local package . --
ADD . /deps/cli
# -- End of local package . --
+15
View File
@@ -910,6 +910,7 @@ def test_config_to_docker_simple():
expected_docker_stdin = f"""\
# syntax=docker/dockerfile:1.4
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Installing local requirements --
COPY --from=outer-requirements.txt requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
@@ -974,6 +975,7 @@ def test_config_to_docker_outside_path():
"""\
# syntax=docker/dockerfile:1.4
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
RUN set -ex && \\
@@ -1034,6 +1036,7 @@ def test_config_to_docker_pipconfig():
expected_docker_stdin = (
"""\
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
ADD pipconfig.txt /pipconfig.txt
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
@@ -1097,6 +1100,7 @@ def test_config_to_docker_local_deps():
)
expected_docker_stdin = f"""\
FROM langchain/langgraph-api-custom:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency graphs --
ADD ./graphs /deps/outer-graphs/src
RUN set -ex && \\
@@ -1144,6 +1148,7 @@ dependencies = ["langchain"]"""
os.remove(pyproject_path)
expected_docker_stdin = (
"""FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding local package . --
ADD . /deps/unit_tests
# -- End of local package . --
@@ -1177,6 +1182,7 @@ def test_config_to_docker_end_to_end():
base_image="langchain/langgraph-api",
)
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.12
ARG USER_ENV_VARS_HASH
ARG meow
ARG foo
ADD pipconfig.txt /pipconfig.txt
@@ -1222,6 +1228,7 @@ def test_config_to_docker_nodejs():
base_image="langchain/langgraphjs-api",
)
expected_docker_stdin = """FROM langchain/langgraphjs-api:20
ARG USER_ENV_VARS_HASH
ARG meow
ARG foo
ADD . /deps/unit_tests
@@ -1413,6 +1420,7 @@ def test_config_to_docker_nodejs_internal_docker_tag():
base_image="langchain/langgraphjs-api",
)
expected_docker_stdin = """FROM langchain/langgraphjs-api:my-tag
ARG USER_ENV_VARS_HASH
ARG meow
ARG foo
ADD . /deps/unit_tests
@@ -1524,6 +1532,7 @@ def test_config_to_docker_gen_ui_python():
)
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
RUN /storage/install-node.sh
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
@@ -1571,6 +1580,7 @@ def test_config_to_docker_multiplatform():
)
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
RUN /storage/install-node.sh
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
@@ -2843,6 +2853,7 @@ def test_config_to_compose_simple_config():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
RUN set -ex && \\
@@ -2884,6 +2895,7 @@ def test_config_to_compose_env_vars():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api-custom:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
RUN set -ex && \\
@@ -2929,6 +2941,7 @@ def test_config_to_compose_env_file():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
RUN set -ex && \\
@@ -2967,6 +2980,7 @@ def test_config_to_compose_watch():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
RUN set -ex && \\
@@ -3014,6 +3028,7 @@ def test_config_to_compose_end_to_end():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ARG USER_ENV_VARS_HASH
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
RUN set -ex && \\
@@ -85,8 +85,6 @@ def test_agent_create(deployment_api, tmp_path, monkeypatch):
assert "name" not in payload
assert build.call_args.kwargs["deployment_id"] == "runtime-id"
assert "server-name" in result.output
assert "'langgraph deploy' is in beta" in result.output
assert "private beta" not in result.output
assert (tmp_path / ".env").read_text() == "LANGSMITH_DEPLOYMENT_NAME=legacy\n"
+142
View File
@@ -0,0 +1,142 @@
"""Generated Dockerfile cache keys; opt into the real BuildKit test locally."""
import hashlib
import os
import pathlib
import subprocess
import uuid
import pytest
from langgraph_cli.config import config_to_docker, validate_config
def generated_prefix(tmp_path: pathlib.Path, runtime: str) -> str:
config = {
"graphs": {"agent": "agent:graph"},
"dockerfile_lines": [
f"RUN --mount=type=secret,id=build_marker echo {tmp_path.name} >/dev/null; "
"sha256sum /run/secrets/build_marker | cut -d ' ' -f 1 > /result"
],
}
if runtime == "node":
config["node_version"] = "20"
config["graphs"] = {"agent": "./agent.js:graph"}
else:
config.update(python_version="3.11", dependencies=["requests"])
if runtime == "uv":
config.pop("dependencies")
config["source"] = {"kind": "uv", "root": "."}
(tmp_path / "uv.lock").write_text("version = 1\n")
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "agent"\nversion = "0.1.0"\n'
'[build-system]\nrequires = ["setuptools"]\n'
'build-backend = "setuptools.build_meta"\n'
)
dockerfile, _ = config_to_docker(
tmp_path / "langgraph.json",
validate_config(config),
base_image="langchain/langgraph-api",
)
# Keep the generated stage header and custom RUN exactly as emitted. Avoid
# installing real graph dependencies: this test exercises the cache boundary.
return dockerfile.split(" > /result", 1)[0] + " > /result\n"
@pytest.mark.parametrize("runtime", ["python", "uv", "node"])
def test_env_hash_in_build_stage_before_custom_run(tmp_path, runtime):
prefix = generated_prefix(tmp_path, runtime)
assert prefix.index("FROM ") < prefix.index("ARG USER_ENV_VARS_HASH")
assert prefix.index("ARG USER_ENV_VARS_HASH") < prefix.index("RUN ")
@pytest.mark.skipif(
os.environ.get("LANGGRAPH_TEST_BUILDKIT") != "1",
reason="Set LANGGRAPH_TEST_BUILDKIT=1 with a local Docker BuildKit daemon",
)
@pytest.mark.parametrize("runtime", ["python", "uv", "node"])
def test_secret_hash_invalidates_real_buildkit_cache(tmp_path, runtime, request):
prefix = generated_prefix(tmp_path, runtime)
lines = prefix.splitlines()
lines[0] = "FROM alpine:3.21"
prefix = "\n".join(lines) + "\n"
dockerfile = tmp_path / "Dockerfile"
marker = tmp_path / "build-marker"
(tmp_path / ".dockerignore").write_text("*\n!Dockerfile\n")
output_counter = 0
cache_image = "langgraph-cli-cache-test:" + uuid.uuid4().hex
request.addfinalizer(
lambda: subprocess.run(
["docker", "image", "rm", cache_image], capture_output=True, check=False
)
)
def build(contents, value, *, pass_hash=True, export_image=False):
nonlocal output_counter
output_counter += 1
marker.write_text(value)
dockerfile.write_text(
contents + "FROM scratch\nCOPY --from=0 /result /result\n"
)
output = tmp_path / f"output-{output_counter}"
args = [
"docker",
"buildx",
"build",
"--progress=plain",
"--secret",
f"id=build_marker,src={marker}",
"--output",
f"type=local,dest={output}",
"-f",
str(dockerfile),
str(tmp_path),
]
if pass_hash:
args.extend(
[
"--build-arg",
f"USER_ENV_VARS_HASH={hashlib.sha256(value.encode()).hexdigest()}",
]
)
if output_counter > 1:
args.extend(["--cache-from", cache_image])
if export_image:
# Export a legacy image with inline cache, then explicitly import it.
image_args = args.copy()
output_index = image_args.index("--output")
del image_args[output_index : output_index + 2]
image_args.extend(
["--load", "--tag", cache_image, "--cache-to", "type=inline"]
)
subprocess.run(image_args, capture_output=True, text=True, check=True)
# List arguments only; all inputs are local public test markers.
result = subprocess.run(args, capture_output=True, text=True, check=True)
return (output / "result").read_text().strip(), result.stderr
first = f"{runtime}-{tmp_path.name}-first"
second = f"{runtime}-{tmp_path.name}-second"
legacy = prefix.replace("ARG USER_ENV_VARS_HASH\n", "")
legacy_output, _ = build(legacy, first, export_image=True)
assert legacy_output == hashlib.sha256(first.encode()).hexdigest()
stale_output, _ = build(legacy, second)
assert stale_output == legacy_output # Secret contents alone do not bust cache.
fixed_output, _ = build(prefix, second)
assert fixed_output == hashlib.sha256(second.encode()).hexdigest()
assert fixed_output != stale_output # Old cache is safe when adopting the ARG.
repeated_output, repeated_log = build(prefix, second)
assert repeated_output == fixed_output
run_vertex = next(
line.split()[0]
for line in repeated_log.splitlines()
if "] RUN " in line and "sha256sum" in line
)
assert f"{run_vertex} CACHED" in repeated_log
changed_output, _ = build(prefix, first)
assert changed_output == hashlib.sha256(first.encode()).hexdigest()
assert changed_output != repeated_output
# Ordinary CLI callers can still build without supplying the optional hash.
unset_output, _ = build(prefix, first, pass_hash=False)
assert unset_output == hashlib.sha256(first.encode()).hexdigest()
+6 -6
View File
@@ -250,7 +250,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.19"
version = "1.2.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -260,9 +260,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ba/10/329dc49ce3fc40e36faafd407f08607fe3fab75613e709a66758b53faac2/langgraph-1.2.19.tar.gz", hash = "sha256:4a00fa1aad2b5fc2d3a789a8e9eaf993f76218ca924b434902ecf1bbaaf300f6", size = 782618, upload-time = "2026-10-30T22:23:32.962Z" }
sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/9e/bd29e368fb16feea8d24270d377d56c34b49afea371717124f19cdfa890c/langgraph-1.2.19-py3-none-any.whl", hash = "sha256:6094a5cbed52a77d8c74c276fcd2056abde7b2836eab1c7a6b74bacbd1a269b3", size = 262285, upload-time = "2026-10-30T22:23:31.334Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" },
]
[[package]]
@@ -308,7 +308,7 @@ wheels = [
[[package]]
name = "langgraph-sdk"
version = "0.4.11"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -317,9 +317,9 @@ dependencies = [
{ name = "orjson" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/a8/d5807df5976f2b8e181baddbddf857cc0ba501bba274e81711b10385c65b/langgraph_sdk-0.4.11.tar.gz", hash = "sha256:2550af3bc7cb78ac4facb97d141ec1a76568fa3eb3d29040cff70b45bda84740", size = 366656, upload-time = "2026-10-30T22:17:49.202Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/88/69899bb20b96b80468e98b90377e605c36658125e7e7fba401868a86138e/langgraph_sdk-0.4.11-py3-none-any.whl", hash = "sha256:6a65be4a8f825966a07317b8e34e3337cd227b9b58b185f0123fd13c3d0116fa", size = 169311, upload-time = "2026-10-30T22:17:47.96Z" },
{ url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" },
]
[[package]]
+10 -10
View File
@@ -1,5 +1,5 @@
version = 1
revision = 5
revision = 3
requires-python = ">=3.11"
[[package]]
@@ -224,7 +224,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.19"
version = "1.2.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -234,22 +234,22 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ba/10/329dc49ce3fc40e36faafd407f08607fe3fab75613e709a66758b53faac2/langgraph-1.2.19.tar.gz", hash = "sha256:4a00fa1aad2b5fc2d3a789a8e9eaf993f76218ca924b434902ecf1bbaaf300f6", size = 782618, upload-time = "2026-10-30T22:23:32.962Z" }
sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/9e/bd29e368fb16feea8d24270d377d56c34b49afea371717124f19cdfa890c/langgraph-1.2.19-py3-none-any.whl", hash = "sha256:6094a5cbed52a77d8c74c276fcd2056abde7b2836eab1c7a6b74bacbd1a269b3", size = 262285, upload-time = "2026-10-30T22:23:31.334Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "ormsgpack" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/c4/f4107f50dd07169d08d95766cd69104231e694f44a1b25b68ad8b393712d/langgraph_checkpoint-4.3.1.tar.gz", hash = "sha256:625e7c78a72b33e7c47f7e87ca051060fb90aa66f5fd551c472c08376fb669cb", size = 182679, upload-time = "2026-10-26T19:57:31.416Z" }
sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/40/4251d676b5c585d42175c901175d4bff56e22939625e18bcef781fc3e893/langgraph_checkpoint-4.3.1-py3-none-any.whl", hash = "sha256:215a826935c8db4d9676e79fd95943e5b3847d78604390898e1d98581cc594e5", size = 58093, upload-time = "2026-10-26T19:57:30.361Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" },
]
[[package]]
@@ -267,7 +267,7 @@ wheels = [
[[package]]
name = "langgraph-sdk"
version = "0.4.11"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -276,9 +276,9 @@ dependencies = [
{ name = "orjson" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/a8/d5807df5976f2b8e181baddbddf857cc0ba501bba274e81711b10385c65b/langgraph_sdk-0.4.11.tar.gz", hash = "sha256:2550af3bc7cb78ac4facb97d141ec1a76568fa3eb3d29040cff70b45bda84740", size = 366656, upload-time = "2026-10-30T22:17:49.202Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/88/69899bb20b96b80468e98b90377e605c36658125e7e7fba401868a86138e/langgraph_sdk-0.4.11-py3-none-any.whl", hash = "sha256:6a65be4a8f825966a07317b8e34e3337cd227b9b58b185f0123fd13c3d0116fa", size = 169311, upload-time = "2026-10-30T22:17:47.96Z" },
{ url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" },
]
[[package]]
+16 -31
View File
@@ -992,11 +992,9 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.7.3"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "httpx" },
{ name = "jsonpatch" },
{ name = "langchain-protocol" },
{ name = "langsmith" },
@@ -1007,26 +1005,26 @@ dependencies = [
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/90/f932d9c82d15033ae9676fb3670993e10e1b4d9c59fe1b7ae024dee11955/langchain_core-1.7.3.tar.gz", hash = "sha256:77f70b81e238c981bfd8405840b03fdf143735ac93c0729c2bdb39d6f0648024", size = 1029116, upload-time = "2026-10-29T20:55:56.42Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/c6/88bcc77899601e09a428d9c764e41ac3e7ede161384f85b9a2a8a828f81d/langchain_core-1.7.3-py3-none-any.whl", hash = "sha256:460c214bf31495d7b7e0a879e408dbfda677fd53fd879da786c1b966a4f6bff6", size = 577074, upload-time = "2026-10-29T20:55:54.722Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
]
[[package]]
name = "langchain-protocol"
version = "0.0.20"
version = "0.0.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/8b/d908d082c3cf5368ddd1816ed114de79c873f979ba7f98b95fab0e841643/langchain_protocol-0.0.20.tar.gz", hash = "sha256:0f5bd002474265648e91161771a9e0438fe9480bd55a416e51af0f60070637ec", size = 6280, upload-time = "2026-10-20T11:13:50.84Z" }
sdist = { url = "https://files.pythonhosted.org/packages/44/b3/4e2429876c7a35585618caa2b9f9089f7162a6b50562b614ad82ac11c17e/langchain_protocol-0.0.17.tar.gz", hash = "sha256:e7cbe58c205df4b4fd87dc6d5bb23f10e13b236d0e2e1b0b9d05bc2b648f3eea", size = 6026, upload-time = "2026-06-12T18:39:51.923Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/4b/f1c99a0b27c5ff8b4b131c0d6e62012c17cc424089efe213767db927afd2/langchain_protocol-0.0.20-py3-none-any.whl", hash = "sha256:3c935e5e46b0f6f9cb52f45ba7cfa53d1b9fa80c82c4a277d47f03e07f7385b1", size = 7347, upload-time = "2026-10-20T11:13:49.566Z" },
{ url = "https://files.pythonhosted.org/packages/13/0a/a1bfe72c6ec856e99773bbd96c8086421e554b3693d0142b9ea009c6ac92/langchain_protocol-0.0.17-py3-none-any.whl", hash = "sha256:982a08fe152586ed10d4ff3d538c2e0b5766e5f307cdea325e10be3f2c17cae6", size = 7096, upload-time = "2026-06-12T18:39:50.973Z" },
]
[[package]]
name = "langgraph"
version = "1.2.14"
version = "1.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -1036,9 +1034,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/3f/6e60789eab2c5ceaf34ba2c626ac345df93b026bbe7385018aa4b9c30397/langgraph-1.2.14.tar.gz", hash = "sha256:5b1a72b7a7f994ac6947b7751fbbe00e949f8bb44d9ab78881604b703247ae55", size = 752793, upload-time = "2026-10-06T14:41:19.789Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/55/e42f59b5e4cce75085f68abb77114c4d1152628157bd60c2587881bca559/langgraph-1.2.3.tar.gz", hash = "sha256:7f89cd5f0946fe29bd7ca048e2a84d3c14e7f652e38bb98e00f0ba8b7004b9d0", size = 718812, upload-time = "2026-06-01T18:55:54.458Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/4a/f976df0d3cf7fb7986dc39f656c6b7b5655834430e9bdfaef1e1e4fffaba/langgraph-1.2.14-py3-none-any.whl", hash = "sha256:e3f5f8ff4259f9281b73ff50fc9ef5cc3b025b8936b8283ecdca7cc0c4128690", size = 255980, upload-time = "2026-10-06T14:41:18.129Z" },
{ url = "https://files.pythonhosted.org/packages/39/9b/287f11be799f2f4dd289ab19d23788df22ac20ce884a010d0696f79016db/langgraph-1.2.3-py3-none-any.whl", hash = "sha256:22e6c89084adb3edb7855f6ffa692af3d75925f9c70deb43b86da14db1b02c78", size = 244841, upload-time = "2026-06-01T18:55:52.945Z" },
]
[[package]]
@@ -1211,7 +1209,7 @@ wheels = [
[[package]]
name = "langgraph-sdk"
version = "0.4.6"
version = "0.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -1220,34 +1218,30 @@ dependencies = [
{ name = "orjson" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/85/54edf4902c0378510f09a009f64e3d3f5644aeeb51d13cf67ec380a5245b/langgraph_sdk-0.4.6.tar.gz", hash = "sha256:ea5b1749d70987d741c9b9fe2b7e1c5f7da7e4d102f64e390333c1890c9bbd5b", size = 350632, upload-time = "2026-10-06T14:17:53.641Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9c/7ff305b366ce986052a27dc5946839bef2823b3567664cbad22e294dff5a/langgraph_sdk-0.4.5.tar.gz", hash = "sha256:d49a98a2ee8e0c494b101a7caf8061c8b4b94d3ee5ada0ea2dbe50903a5487b1", size = 349783, upload-time = "2026-09-21T14:42:55.732Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/c7/69d13026db3577c157877dea47a9dc5fffd3dcde2d1ab8009fa8f7e5f6a6/langgraph_sdk-0.4.6-py3-none-any.whl", hash = "sha256:57122b02bda41d37a97a5e09eb018aa8c9b3dc85c433880073779bd9324b7eea", size = 162390, upload-time = "2026-10-06T14:17:52.134Z" },
{ url = "https://files.pythonhosted.org/packages/49/a9/71a6499f3481bc00bf4f42a17cb81caf19a02fce762db356626d7214e2c2/langgraph_sdk-0.4.5-py3-none-any.whl", hash = "sha256:e03ef033a20d21288af496e9b6c08ae3afcc1b58dc5abcd37575bfd1a93045de", size = 162304, upload-time = "2026-09-21T14:42:54.623Z" },
]
[[package]]
name = "langsmith"
version = "0.14.16"
version = "0.8.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx2" },
{ name = "httpx" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "sniffio" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" } },
{ name = "uuid-utils" },
{ name = "websockets" },
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/45/576803a72da96d9c0866b3815f08f447ed16c14acb52174129daca6847d6/langsmith-0.14.16.tar.gz", hash = "sha256:4e5816cbe1541f4d04d71f652eb8f4137889861bebcf7137bcba3553ae9e335e", size = 5096529, upload-time = "2026-10-29T17:53:27.747Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/3a/9a072c02a22089ddf20f0f33ea95bee5a5951b31839f118b04f27b94be6e/langsmith-0.14.16-py3-none-any.whl", hash = "sha256:b9ea1a57622b0788b181d78605f974fb206c5f6f75793076ffec23cef4d904c8", size = 933680, upload-time = "2026-10-29T17:53:25.465Z" },
{ url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" },
]
[package.optional-dependencies]
@@ -2099,15 +2093,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sse-starlette"
version = "3.3.4"
+60 -119
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
import asyncio
import copy
import enum
import inspect
import logging
@@ -24,16 +23,9 @@ from typing import (
Optional,
Protocol,
TypeGuard,
TypeVar,
cast,
)
from langchain_core.callbacks.base import BaseCallbackHandler, BaseCallbackManager
from langchain_core.callbacks.manager import (
AsyncCallbackManagerForChainRun,
BaseRunManager,
CallbackManagerForChainRun,
)
from langchain_core.runnables.base import (
Runnable,
RunnableConfig,
@@ -49,7 +41,6 @@ from langchain_core.runnables.config import (
var_child_runnable_config,
)
from langchain_core.runnables.utils import Input, Output
from langchain_core.tracers.base import BaseTracer
from langchain_core.tracers.langchain import LangChainTracer
from langgraph.store.base import BaseStore
@@ -66,7 +57,7 @@ from langgraph._internal._constants import (
)
from langgraph._internal._typing import MISSING
from langgraph.errors import NodeError
from langgraph.types import StreamWriter, TracePolicy
from langgraph.types import StreamWriter
try:
from langchain_core.tracers._streaming import _StreamingCallbackHandler
@@ -94,29 +85,6 @@ def _trace_payload(value: Any, transform: Callable[[Any], Any] | None) -> Any:
return value
def _is_stream_handler(handler: BaseCallbackHandler) -> bool:
# Inheritance, not isinstance on the runtime-checkable protocol, which also
# matches any exporter that merely has the tap methods: stream handlers get a
# node's real payload and its streamed output.
return _StreamingCallbackHandler in type(handler).__mro__
_M = TypeVar("_M", bound=BaseCallbackManager | BaseRunManager)
def _split_handlers(manager: _M) -> tuple[_M, _M]:
# The recorded payload exists to keep tracer ingest small, so only tracers get it.
# `astream_log`'s handler is a tracer too, but it feeds the caller's own stream.
real, tracers = copy.copy(manager), copy.copy(manager)
tracers.handlers = [
h
for h in manager.handlers
if isinstance(h, BaseTracer) and not _is_stream_handler(h)
]
real.handlers = [h for h in manager.handlers if h not in tracers.handlers]
return real, tracers
def _set_config_context(
config: RunnableConfig, run: Any = None
) -> Token[RunnableConfig | None]:
@@ -625,15 +593,13 @@ class RunnableSeq(Runnable):
*steps: RunnableLike,
name: str | None = None,
trace_inputs: Callable[[Any], Any] | None = None,
trace_policy: TracePolicy | None = None,
trace_outputs: Callable[[Any], Any] | None = None,
) -> None:
"""Create a new RunnableSeq.
Args:
steps: The steps to include in the sequence.
name: The name of the `Runnable`.
trace_inputs: Transforms the input every callback handler receives.
trace_policy: Processes what tracers record, after `trace_inputs`.
Raises:
ValueError: If the sequence has less than 2 steps.
@@ -653,67 +619,7 @@ class RunnableSeq(Runnable):
self.steps = steps_flat
self.name = name
self.trace_inputs = trace_inputs
self.trace_policy = trace_policy or TracePolicy()
def _start(self, input: Any, config: RunnableConfig) -> CallbackManagerForChainRun:
callback_manager = get_callback_manager_for_config(config)
payload = _trace_payload(input, self.trace_inputs)
recorded = _trace_payload(payload, self.trace_policy.process_inputs)
name = config.get("run_name") or self.get_name()
run_id = config.pop("run_id", None)
if recorded is payload:
return callback_manager.on_chain_start(
None, payload, name=name, run_id=run_id
)
real, tracers = _split_handlers(callback_manager)
run_manager = real.on_chain_start(None, payload, name=name, run_id=run_id)
tracers.on_chain_start(None, recorded, name=name, run_id=run_manager.run_id)
run_manager.handlers = callback_manager.handlers
return run_manager
async def _astart(
self, input: Any, config: RunnableConfig
) -> AsyncCallbackManagerForChainRun:
callback_manager = get_async_callback_manager_for_config(config)
payload = _trace_payload(input, self.trace_inputs)
recorded = _trace_payload(payload, self.trace_policy.process_inputs)
name = config.get("run_name") or self.get_name()
run_id = config.pop("run_id", None)
if recorded is payload:
return await callback_manager.on_chain_start(
None, payload, name=name, run_id=run_id
)
real, tracers = _split_handlers(callback_manager)
# Tracers start last, so a cancellation before then leaves no tracer run open.
run_manager = await real.on_chain_start(None, payload, name=name, run_id=run_id)
await tracers.on_chain_start(
None, recorded, name=name, run_id=run_manager.run_id
)
run_manager.handlers = callback_manager.handlers
return run_manager
def _end(self, run_manager: CallbackManagerForChainRun, output: Any) -> None:
recorded = _trace_payload(output, self.trace_policy.process_outputs)
if recorded is output:
run_manager.on_chain_end(output)
else:
real, tracers = _split_handlers(run_manager)
tracers.on_chain_end(recorded)
real.on_chain_end(output)
async def _aend(
self, run_manager: AsyncCallbackManagerForChainRun, output: Any
) -> None:
recorded = _trace_payload(output, self.trace_policy.process_outputs)
if recorded is output:
await run_manager.on_chain_end(output)
else:
real, tracers = _split_handlers(run_manager)
# Tracers end first: on_chain_end is shielded once it starts, so a
# cancellation can only skip the other handlers' end, never leave a
# tracer run open.
await tracers.on_chain_end(recorded)
await real.on_chain_end(output)
self.trace_outputs = trace_outputs
def __or__(
self,
@@ -770,8 +676,15 @@ class RunnableSeq(Runnable):
) -> Any:
if config is None:
config = ensure_config()
# setup callbacks and context
callback_manager = get_callback_manager_for_config(config)
# start the root run
run_manager = self._start(input, config)
run_manager = callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
# invoke all steps in sequence
try:
for i, step in enumerate(self.steps):
@@ -799,7 +712,7 @@ class RunnableSeq(Runnable):
run_manager.on_chain_error(e)
raise
else:
self._end(run_manager, input)
run_manager.on_chain_end(_trace_payload(input, self.trace_outputs))
return input
async def ainvoke(
@@ -810,8 +723,15 @@ class RunnableSeq(Runnable):
) -> Any:
if config is None:
config = ensure_config()
# setup callbacks
callback_manager = get_async_callback_manager_for_config(config)
# start the root run
run_manager = await self._astart(input, config)
run_manager = await callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
# invoke all steps in sequence
try:
@@ -845,7 +765,7 @@ class RunnableSeq(Runnable):
await run_manager.on_chain_error(e)
raise
else:
await self._aend(run_manager, input)
await run_manager.on_chain_end(_trace_payload(input, self.trace_outputs))
return input
def stream(
@@ -856,8 +776,15 @@ class RunnableSeq(Runnable):
) -> Iterator[Any]:
if config is None:
config = ensure_config()
# setup callbacks
callback_manager = get_callback_manager_for_config(config)
# start the root run
run_manager = self._start(input, config)
run_manager = callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
# get the run object
for h in run_manager.handlers:
if isinstance(h, LangChainTracer):
@@ -887,9 +814,10 @@ class RunnableSeq(Runnable):
)
iterator = step.transform(iterator, config)
# populates streamed_output in astream_log() output if needed
for h in run_manager.handlers:
if _is_stream_handler(h):
iterator = h.tap_output_iter(run_manager.run_id, iterator)
if _StreamingCallbackHandler is not None:
for h in run_manager.handlers:
if isinstance(h, _StreamingCallbackHandler):
iterator = h.tap_output_iter(run_manager.run_id, iterator)
# consume into final output
output = context.run(_consume_iter, iterator)
# sequence doesn't emit output, yield to mark as generator
@@ -898,7 +826,7 @@ class RunnableSeq(Runnable):
run_manager.on_chain_error(e)
raise
else:
self._end(run_manager, output)
run_manager.on_chain_end(_trace_payload(output, self.trace_outputs))
async def astream(
self,
@@ -908,8 +836,15 @@ class RunnableSeq(Runnable):
) -> AsyncIterator[Any]:
if config is None:
config = ensure_config()
# setup callbacks
callback_manager = get_async_callback_manager_for_config(config)
# start the root run
run_manager = await self._astart(input, config)
run_manager = await callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
# stream the last steps
# transform the input stream of each step with the next
# steps that don't natively support transforming an input stream will
@@ -945,11 +880,12 @@ class RunnableSeq(Runnable):
if hasattr(aiterator, "aclose"):
stack.push_async_callback(aiterator.aclose)
# populates streamed_output in astream_log() output if needed
for h in run_manager.handlers:
if _is_stream_handler(h):
aiterator = h.tap_output_aiter(
run_manager.run_id, aiterator
)
if _StreamingCallbackHandler is not None:
for h in run_manager.handlers:
if isinstance(h, _StreamingCallbackHandler):
aiterator = h.tap_output_aiter(
run_manager.run_id, aiterator
)
# consume into final output
output = await asyncio.create_task(
_consume_aiter(aiterator), context=context
@@ -960,7 +896,9 @@ class RunnableSeq(Runnable):
await run_manager.on_chain_error(e)
raise
else:
await self._aend(run_manager, output)
await run_manager.on_chain_end(
_trace_payload(output, self.trace_outputs)
)
else:
try:
async with AsyncExitStack() as stack:
@@ -976,11 +914,12 @@ class RunnableSeq(Runnable):
if hasattr(aiterator, "aclose"):
stack.push_async_callback(aiterator.aclose)
# populates streamed_output in astream_log() output if needed
for h in run_manager.handlers:
if _is_stream_handler(h):
aiterator = h.tap_output_aiter(
run_manager.run_id, aiterator
)
if _StreamingCallbackHandler is not None:
for h in run_manager.handlers:
if isinstance(h, _StreamingCallbackHandler):
aiterator = h.tap_output_aiter(
run_manager.run_id, aiterator
)
# consume into final output
output = await _consume_aiter(aiterator)
# sequence doesn't emit output, yield to mark as generator
@@ -989,7 +928,9 @@ class RunnableSeq(Runnable):
await run_manager.on_chain_error(e)
raise
else:
await self._aend(run_manager, output)
await run_manager.on_chain_end(
_trace_payload(output, self.trace_outputs)
)
def _consume_iter(it: Iterator[Any]) -> Any:
@@ -4,7 +4,6 @@ from langgraph.channels.binop import BinaryOperatorAggregate
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue, LastValueAfterFinish
from langgraph.channels.log import Log, LogRef, MessagesLog
from langgraph.channels.named_barrier_value import (
NamedBarrierValue,
NamedBarrierValueAfterFinish,
@@ -23,9 +22,6 @@ __all__ = (
"EphemeralValue",
"BinaryOperatorAggregate",
"DeltaChannel",
"Log",
"LogRef",
"MessagesLog",
"NamedBarrierValue",
"NamedBarrierValueAfterFinish",
# topics
@@ -1,5 +1,4 @@
import collections.abc
import warnings
from collections.abc import Callable, Sequence
from typing import Any, Generic
@@ -30,26 +29,6 @@ def _strip_extras(t): # type: ignore[no-untyped-def]
return t
def _is_add_messages(operator: Callable) -> bool:
"""Whether `operator` is `add_messages`, alone or with `format=`."""
return getattr(operator, "_is_add_messages", False) or getattr(
getattr(operator, "func", None), "_is_add_messages", False
)
def _warn_overwrite_messages() -> None:
from langgraph.warnings import LangGraphDeprecatedSinceV13
warnings.warn(
"Replacing all messages with `Overwrite` in an `add_messages` channel "
"is deprecated: stored history will be append-only. Build the messages "
"the model sees when calling it instead, and use the `MessagesLog` "
"channel, which stores messages once instead of in every checkpoint.",
category=LangGraphDeprecatedSinceV13,
stacklevel=4,
)
def _get_overwrite(value: Any) -> tuple[bool, Any]:
"""Inspects the given value and returns (is_overwrite, overwrite_value).
@@ -161,8 +140,6 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
)
raise InvalidUpdateError(msg)
if _is_add_messages(self.operator):
_warn_overwrite_messages()
self.value = overwrite_value
seen_overwrite = True
continue
-328
View File
@@ -1,328 +0,0 @@
"""Append-only log channels (transcript log prototype).
!!! warning "Prototype"
The transcript log is a prototype. Its API and storage may change.
A log channel is append-only and has no reducer: the only write is "append
these entries". Its entries are not stored in checkpoints. A checkpoint's
value for the channel is a marker (`LogStep`) for the step that produced its
version, and the entries live in the saver's log storage, read with
`BaseCheckpointSaver.read_log`.
Two channel types share that storage:
- `Log` holds JSON values. Anything that isn't JSON is rejected.
- `MessagesLog` holds LangChain messages, stored as canonical JSON. Edit ops
(`RemoveMessage`, `Overwrite`, re-adding an existing id) are rejected; a
graph that needs a different model context builds it as a projection.
A thread whose field held a plain list before it became a log channel (for
example `Annotated[list, add_messages]` changed to `MessagesLog()`) keeps
working: the stored list becomes the log's first entries, the "seed". The
seed is committed with the first checkpoint saved after the change, and
checkpoints from before the change still read as the list they stored.
"""
from __future__ import annotations
import json
import math
import uuid
from collections.abc import Sequence
from typing import Any, NamedTuple
from langgraph.checkpoint.serde.types import LogAppend, LogStep
from typing_extensions import Self
from langgraph._internal._typing import MISSING
from langgraph.channels.base import BaseChannel
from langgraph.channels.binop import _get_overwrite
from langgraph.errors import EmptyChannelError, InvalidUpdateError
__all__ = ("Log", "LogRef", "MessagesLog")
class LogRef(NamedTuple):
"""What state reads return for a log channel: the log's length at the
checkpoint. Read the entries with `read_log`."""
length: int
def _check_json(value: Any, path: str = "entry") -> None:
if value is None or isinstance(value, (str, bool, int)):
return
if isinstance(value, float):
if not math.isfinite(value):
raise InvalidUpdateError(f"Log {path} is not JSON: {value!r}")
return
if isinstance(value, list):
for i, item in enumerate(value):
_check_json(item, f"{path}[{i}]")
return
if isinstance(value, dict):
for k, item in value.items():
if not isinstance(k, str):
raise InvalidUpdateError(f"Log {path} has a non-string key: {k!r}")
_check_json(item, f"{path}.{k}")
return
raise InvalidUpdateError(
f"Log {path} is not JSON: {type(value).__name__} {value!r}"
)
def _dumps(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
class Log(BaseChannel[list, list, LogStep]):
"""An append-only log of JSON entries.
Write a list of entries to append them. Nodes read the whole log as a
list. State reads return a `LogRef` instead of the entries.
"""
__slots__ = ("entries", "base", "marker", "loaded", "extra", "seed", "seeding")
def __init__(self, typ: Any = list) -> None:
super().__init__(typ)
self.entries: list[Any] = []
# length covered by the last committed step (the checkpoint's leaf)
self.base = 0
self.marker: LogStep | None = None
# False when built from a checkpoint without reading the log
self.loaded = True
# entries appended to a channel that was not loaded
self.extra = 0
# True while the entries hold a plain list stored before the field
# became a log channel, not committed to the log yet
self.seed = False
# True from the commit that includes the seed until the loop has
# stamped that checkpoint's step
self.seeding = False
def __eq__(self, other: object) -> bool:
return type(self) is type(other)
@property
def ValueType(self) -> Any:
return list
@property
def UpdateType(self) -> Any:
return list
# -- encoding ----------------------------------------------------------
def encode(self, entry: Any) -> Any:
"""An entry as the JSON value stored for it."""
_check_json(entry)
return entry
def decode(self, data: Any) -> Any:
"""A stored JSON value as the entry nodes see."""
return data
def coerce(self, value: Any) -> list[Any]:
"""A write as the list of entries it appends."""
if not isinstance(value, list):
raise InvalidUpdateError(
f"Log channel {self.key!r} takes a list of entries, got "
f"{type(value).__name__}"
)
return value
def append_value(self, value: Any, exec_: str) -> LogAppend:
"""A write as the `LogAppend` handed to the saver."""
self._reject_overwrite(value)
texts = tuple(_dumps(self.encode(e)) for e in self.coerce(value))
return LogAppend(self.base, texts, exec_)
# -- channel protocol --------------------------------------------------
def _new(self) -> Self:
new = self.__class__.__new__(self.__class__)
BaseChannel.__init__(new, self.typ, self.key)
new.entries = []
new.base = 0
new.marker = None
new.loaded = True
new.extra = 0
new.seed = False
new.seeding = False
return new
def copy(self) -> Self:
new = self._new()
new.entries = list(self.entries)
new.base = self.base
new.marker = self.marker
new.loaded = self.loaded
new.extra = self.extra
new.seed = self.seed
new.seeding = self.seeding
return new
def from_checkpoint(self, checkpoint: Any) -> Self:
"""A channel that knows its marker but not its entries.
State reads build channels this way; `get` then returns a `LogRef`.
The loop builds log channels with `from_log` instead.
A plain list is what the field stored before it became a log channel.
The channel then holds it as its seed: uncommitted entries that the
next commit writes to the log, ahead of anything appended after.
"""
new = self._new()
if isinstance(checkpoint, LogStep):
new.marker = checkpoint.stripped()
new.base = checkpoint.end
new.loaded = False
elif isinstance(checkpoint, list):
new.entries = self._seed_entries(checkpoint)
new.seed = bool(new.entries)
elif checkpoint is not MISSING and checkpoint is not None:
raise InvalidUpdateError(
f"Log channel {self.key!r} can't start from {type(checkpoint).__name__}"
)
return new
def _seed_entries(self, stored: list[Any]) -> list[Any]:
"""The entries a stored plain list becomes."""
return list(stored)
def from_log(self, marker: Any, entries: Sequence[Any]) -> Self:
"""A loaded channel: the marker, and the entries read from the log."""
new = self._new()
if isinstance(marker, LogStep):
if len(entries) != marker.end:
raise ValueError(
f"Log channel {self.key!r}: read {len(entries)} entries, "
f"the checkpoint has {marker.end}"
)
new.marker = marker.stripped()
new.base = marker.end
new.entries = [self.decode(e) for e in entries]
return new
def get(self) -> Any:
if not self.loaded:
return LogRef(self.base + self.extra)
if not self.entries:
raise EmptyChannelError()
return self.entries
def is_available(self) -> bool:
return (not self.loaded and self.base + self.extra > 0) or bool(self.entries)
def update(self, values: Sequence[Any]) -> bool:
if not values:
return False
appended: list[Any] = []
for value in values:
if isinstance(value, LogAppend):
# a write read back from the saver (pending writes on resume)
appended.extend(self.decode(json.loads(t)) for t in value.entries)
continue
self._reject_overwrite(value)
for entry in self.coerce(value):
self.encode(entry) # validate before anything is applied
appended.append(entry)
if not appended:
return False
self._check_appends(appended)
if not self.loaded:
self.extra += len(appended)
else:
self.entries.extend(appended)
return True
def _check_appends(self, appended: list[Any]) -> None:
pass
def _reject_overwrite(self, value: Any) -> None:
if _get_overwrite(value)[0]:
raise InvalidUpdateError(
f"Log channel {self.key!r} is append-only: Overwrite is not supported"
)
def checkpoint(self) -> Any:
return MISSING if self.marker is None else self.marker
def commit(self) -> LogStep | None:
"""The marker for a checkpoint about to be saved.
Entries appended since the last commit become a new `LogStep`
(carrying them inline; the loop strips them when they already reached
the saver through `put_writes`), and the channel's base moves to its
end.
"""
if self.loaded and len(self.entries) > self.base:
texts = tuple(_dumps(self.encode(e)) for e in self.entries[self.base :])
step = LogStep(self.base, len(self.entries) - self.base, "", texts)
self.base = len(self.entries)
self.marker = step.stripped()
self.seeding, self.seed = self.seed, False
return step
return self.marker
class MessagesLog(Log):
"""An append-only log of LangChain messages.
Messages are stored as canonical JSON (`message_to_dict`). Every message
has an id; writing an id that is already in the log is an error, as are
`RemoveMessage` and `Overwrite`.
"""
__slots__ = ()
def encode(self, entry: Any) -> Any:
from langchain_core.messages import BaseMessage, RemoveMessage, message_to_dict
if isinstance(entry, RemoveMessage):
raise InvalidUpdateError(
f"MessagesLog {self.key!r} is append-only: RemoveMessage is not "
"supported. To give the model a different set of messages, build "
"them when calling the model instead of changing stored history."
)
if not isinstance(entry, BaseMessage):
raise InvalidUpdateError(
f"MessagesLog {self.key!r} takes messages, got {type(entry).__name__}"
)
data = message_to_dict(entry)
_check_json(data, "message")
return data
def decode(self, data: Any) -> Any:
from langchain_core.messages import messages_from_dict
return messages_from_dict([data])[0]
def _seed_entries(self, stored: list[Any]) -> list[Any]:
# the list `add_messages` stored: already merged, so ids are unique
# and there are no `RemoveMessage`s
return self.coerce(stored)
def coerce(self, value: Any) -> list[Any]:
from langchain_core.messages import convert_to_messages
items = value if isinstance(value, list) else [value]
messages = convert_to_messages(items)
for m in messages:
if not m.id:
m.id = str(uuid.uuid4())
return messages
def _check_appends(self, appended: list[Any]) -> None:
seen = {m.id for m in self.entries} if self.loaded else set()
for m in appended:
if m.id in seen:
raise InvalidUpdateError(
f"MessagesLog {self.key!r} already has a message with id "
f"{m.id!r}: the log is append-only, so a message can't be "
"replaced. Return only new messages from a node, not the "
"whole list."
)
seen.add(m.id)
+12 -36
View File
@@ -22,12 +22,12 @@ from langchain_core.messages import (
)
from typing_extensions import TypedDict, deprecated
from langgraph._internal._constants import CONF, CONFIG_KEY_SEND
from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, NS_SEP
from langgraph.graph.state import StateGraph
from langgraph.pregel._messages import (
_messages_delta_reducer as _messages_delta_reducer,
)
from langgraph.warnings import LangGraphDeprecatedSinceV10, LangGraphDeprecatedSinceV13
from langgraph.warnings import LangGraphDeprecatedSinceV10
__all__ = (
"add_messages",
@@ -41,27 +41,7 @@ Messages = list[MessageLikeRepresentation] | MessageLikeRepresentation
REMOVE_ALL_MESSAGES = "__remove_all__"
_EDIT_DEPRECATION = (
"{what} through `add_messages` is deprecated: stored history will be "
"append-only. Build the messages the model sees when calling it instead "
"(for example from a summary kept in state), and use the `MessagesLog` "
"channel, which stores messages once instead of in every checkpoint."
)
def _warn_edit(what: str) -> None:
warnings.warn(
_EDIT_DEPRECATION.format(what=what),
category=LangGraphDeprecatedSinceV13,
stacklevel=3,
)
def _add_messages_wrapper(func: Callable) -> Callable[[Messages, Messages], Messages]:
# lets `BinaryOperatorAggregate` recognize the reducer, alone or with
# `format=`, without importing this module
func._is_add_messages = True # type: ignore[attr-defined]
def _add_messages(
left: Messages | None = None, right: Messages | None = None, **kwargs: Any
) -> Messages | Callable[[Messages, Messages], Messages]:
@@ -77,7 +57,6 @@ def _add_messages_wrapper(func: Callable) -> Callable[[Messages, Messages], Mess
return partial(func, **kwargs)
_add_messages.__doc__ = func.__doc__
_add_messages._is_add_messages = True # type: ignore[attr-defined]
return cast(Callable[[Messages, Messages], Messages], _add_messages)
@@ -234,11 +213,9 @@ def add_messages(
remove_all_idx = idx
if remove_all_idx is not None:
_warn_edit("Removing all messages with `REMOVE_ALL_MESSAGES`")
return right[remove_all_idx + 1 :]
# merge
left_ids = {m.id for m in left}
merged = left.copy()
merged_by_id = {m.id: i for i, m in enumerate(merged)}
ids_to_remove = set()
@@ -258,10 +235,6 @@ def add_messages(
merged_by_id[m.id] = len(merged)
merged.append(m)
merged = [m for m in merged if m.id not in ids_to_remove]
if any(isinstance(m, RemoveMessage) for m in right):
_warn_edit("Removing messages with `RemoveMessage`")
elif any(m.id in left_ids for m in right):
_warn_edit("Replacing a message by id")
if format == "langchain-openai":
merged = _format_messages(merged)
@@ -370,7 +343,7 @@ def push_message(
)
from langgraph.config import get_config
from langgraph.pregel._messages import StreamMessagesHandler, graph_namespace
from langgraph.pregel._messages import StreamMessagesHandler
config = get_config()
message = next(x for x in convert_to_messages([message]))
@@ -386,12 +359,15 @@ def push_message(
):
handlers = config["callbacks"]
metadata = config["metadata"]
for handler in handlers:
if isinstance(handler, StreamMessagesHandler):
ns = graph_namespace(metadata)
if handler.subgraphs or not ns or ns == handler.parent_ns:
handler._emit((ns, metadata), message, dedupe=False)
if stream_handler := next(
(x for x in handlers if isinstance(x, StreamMessagesHandler)), None
):
metadata = config["metadata"]
message_meta = (
tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP)),
metadata,
)
stream_handler._emit(message_meta, message, dedupe=False)
if state_key:
config[CONF][CONFIG_KEY_SEND]([(state_key, message)])
+2 -51
View File
@@ -4,7 +4,7 @@ import uuid
from collections.abc import Callable, Iterable, Mapping, Sequence
from datetime import datetime, timezone
from inspect import signature
from typing import Any, Literal, cast
from typing import Any, Literal
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
@@ -16,7 +16,7 @@ from langgraph.checkpoint.base import (
PendingWrite,
)
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.serde.types import LogStep, _DeltaSnapshot
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph._internal._config import (
DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT,
@@ -35,7 +35,6 @@ from langgraph._internal._typing import MISSING
from langgraph.channels.base import BaseChannel
from langgraph.channels.binop import BinaryOperatorAggregate, _get_overwrite
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.log import Log
from langgraph.managed.base import ManagedValueMapping, ManagedValueSpec
LATEST_VERSION = 4
@@ -326,10 +325,6 @@ def create_checkpoint(
bumped[k] = (old, channel_versions[k])
values[k] = _DeltaSnapshot(ch.get())
else:
if isinstance(ch, Log):
if (log_step := ch.commit()) is not None:
values[k] = log_step
continue
v = ch.checkpoint()
if v is not MISSING:
values[k] = v
@@ -460,39 +455,12 @@ def _require_saver_for_history(
)
def _log_specs(
specs: Mapping[str, BaseChannel], checkpoint: Checkpoint, load_logs: bool
) -> list[str]:
"""Log channels whose entries must be read from the saver."""
if not load_logs:
return []
return [
k
for k, spec in specs.items()
if isinstance(spec, Log)
and isinstance(checkpoint["channel_values"].get(k), LogStep)
]
def _require_saver_for_log(
log_channels: list[str],
saver: BaseCheckpointSaver | None,
config: RunnableConfig | None,
) -> None:
if log_channels and (saver is None or config is None):
raise ValueError(
f"Log channels {log_channels} have entries to read but no "
"checkpointer or config was passed to read them"
)
def channels_from_checkpoint(
specs: Mapping[str, BaseChannel | ManagedValueSpec],
checkpoint: Checkpoint,
*,
saver: BaseCheckpointSaver | None = None,
config: RunnableConfig | None = None,
load_logs: bool = True,
) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
"""Hydrate channels from a checkpoint.
@@ -519,20 +487,12 @@ def channels_from_checkpoint(
histories = saver.get_delta_channel_history(
config=config, channels=delta_channels
)
log_channels = _log_specs(channel_specs, checkpoint, load_logs)
_require_saver_for_log(log_channels, saver, config)
logs: dict[str, list[Any]] = {}
if log_channels and saver is not None and config is not None:
for k in log_channels:
logs[k] = [e for _, e in saver.read_log(config, k).entries]
channels: dict[str, BaseChannel] = {}
for k, spec in channel_specs.items():
ch: BaseChannel
if k in histories:
ch = _replay(spec, histories[k])
elif k in logs:
ch = cast(Log, spec).from_log(checkpoint["channel_values"][k], logs[k])
else:
ch = spec.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
channels[k] = ch
@@ -545,7 +505,6 @@ async def achannels_from_checkpoint(
*,
saver: BaseCheckpointSaver | None = None,
config: RunnableConfig | None = None,
load_logs: bool = True,
) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
"""Async version of `channels_from_checkpoint`. See docstring there."""
channel_specs: dict[str, BaseChannel] = {}
@@ -563,20 +522,12 @@ async def achannels_from_checkpoint(
histories = await saver.aget_delta_channel_history(
config=config, channels=delta_channels
)
log_channels = _log_specs(channel_specs, checkpoint, load_logs)
_require_saver_for_log(log_channels, saver, config)
logs: dict[str, list[Any]] = {}
if log_channels and saver is not None and config is not None:
for k in log_channels:
logs[k] = [e for _, e in (await saver.aread_log(config, k)).entries]
channels: dict[str, BaseChannel] = {}
for k, spec in channel_specs.items():
ch: BaseChannel
if k in histories:
ch = _replay(spec, histories[k])
elif k in logs:
ch = cast(Log, spec).from_log(checkpoint["channel_values"][k], logs[k])
else:
ch = spec.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
channels[k] = ch
-4
View File
@@ -228,10 +228,6 @@ def draw_graph(
metadata["__interrupt"] = "before"
elif name in interrupt_after_nodes:
metadata["__interrupt"] = "after"
# Duck-typed so core doesn't import ToolNode from langgraph-prebuilt.
tools_by_name = getattr(node.bound, "tools_by_name", None)
if isinstance(tools_by_name, Mapping) and tools_by_name:
metadata["__tools"] = list(tools_by_name)
graph.add_node(node.bound, name, metadata=metadata or None)
# add start node
if START not in nodes:
+6 -119
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import binascii
import concurrent.futures
import json
from collections import defaultdict, deque
from collections.abc import Callable, Iterator, Mapping, Sequence
from contextlib import (
@@ -13,7 +12,6 @@ from contextlib import (
ExitStack,
)
from datetime import datetime, timezone
from functools import partial
from types import TracebackType
from typing import (
Any,
@@ -21,7 +19,7 @@ from typing import (
TypeVar,
cast,
)
from uuid import UUID, uuid4, uuid5
from uuid import UUID, uuid5
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
from langchain_core.runnables import RunnableConfig
@@ -35,7 +33,6 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
PendingWrite,
)
from langgraph.checkpoint.serde.types import LogAppend, LogStep
from langgraph.store.base import BaseStore
from typing_extensions import ParamSpec, Self
@@ -75,7 +72,6 @@ from langgraph.callbacks import (
from langgraph.channels.base import BaseChannel
from langgraph.channels.binop import _get_overwrite
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.log import Log
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.constants import TAG_HIDDEN
from langgraph.errors import (
@@ -360,9 +356,6 @@ class PregelLoop:
self.retry_policy = retry_policy
self.cache_policy = cache_policy
self.durability = durability
# identifies this execution's log writes (transcript log prototype)
self._log_exec = str(uuid4())
self._log_inline_first_step = False
self._has_graph_lifecycle_callbacks = has_graph_lifecycle_callbacks
self._graph_lifecycle_events = deque()
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
@@ -520,7 +513,6 @@ class PregelLoop:
# save writes
self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes)
if self.durability != "exit" and self.checkpointer_put_writes is not None:
writes_to_save = self._log_writes_for_saver(writes_to_save)
config = patch_configurable(
self.checkpoint_config,
{
@@ -530,21 +522,13 @@ class PregelLoop:
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
},
)
# A log write is placed relative to the log of the checkpoint it is
# stored on, so that checkpoint must be saved first.
put_writes_fn = self.checkpointer_put_writes
if any(isinstance(self.specs.get(c), Log) for c, _ in writes_to_save):
put_writes_fn = partial(
self._put_writes_after_checkpoint,
getattr(self, "_put_checkpoint_fut", None),
)
if self.checkpointer_put_writes_accepts_task_path:
if hasattr(self, "tasks"):
task = self.tasks.get(task_id)
else:
task = None
fut = self.submit(
put_writes_fn,
self.checkpointer_put_writes,
config,
writes_to_save,
task_id,
@@ -552,16 +536,13 @@ class PregelLoop:
)
else:
fut = self.submit(
put_writes_fn,
self.checkpointer_put_writes,
config,
writes_to_save,
task_id,
)
# the checkpoint that commits these writes must not be saved before
# them: a delta replay or a log step reads them
if self._delta_write_futs is not None and any(
isinstance(self.specs.get(c), (DeltaChannel, Log))
for c, _ in writes_to_save
isinstance(self.specs.get(c), DeltaChannel) for c, _ in writes_to_save
):
self._delta_write_futs.append(fut)
# ERROR_SOURCE_NODE is only appended by commit() when the task
@@ -575,24 +556,6 @@ class PregelLoop:
if hasattr(self, "tasks"):
self.output_writes(task_id, writes, cached=cached)
def _log_write_from_saver(self, channel: str, value: Any) -> Any:
"""A saved `LogAppend` as the write it was: a list of entries."""
spec = self.specs.get(channel)
if isinstance(value, LogAppend) and isinstance(spec, Log):
return [spec.decode(json.loads(t)) for t in value.entries]
return value
def _log_writes_for_saver(self, writes: WritesT) -> WritesT:
"""Writes to log channels as the `LogAppend`s handed to the saver."""
if not any(isinstance(self.channels.get(c), Log) for c, _ in writes):
return writes
return [
(c, ch.append_value(v, self._log_exec))
if isinstance(ch := self.channels.get(c), Log)
else (c, v)
for c, v in writes
]
def _put_pending_writes(self) -> None:
if self.checkpointer_put_writes is None:
return
@@ -616,7 +579,6 @@ class PregelLoop:
by_task[task_id].append((channel, value))
# submit writes to checkpointer
for task_id, writes in by_task.items():
writes = self._log_writes_for_saver(writes)
if self.checkpointer_put_writes_accepts_task_path and hasattr(
self, "tasks"
):
@@ -1349,8 +1311,6 @@ class PregelLoop:
stored_versions=self.checkpoint_previous_versions,
trigger_to_nodes=self.trigger_to_nodes,
)
if do_checkpoint:
self._finish_log_steps(metadata)
for k in channels_to_snapshot:
new_counters[k] = (0, 0)
if do_checkpoint:
@@ -1423,51 +1383,6 @@ class PregelLoop:
# increment step
self.step += 1
def _finish_log_steps(self, metadata: CheckpointMetadata) -> None:
"""Stamp the log steps of the checkpoint about to be saved.
A step's entries reached the saver through `put_writes` when the
checkpoint comes from the loop and writes are saved as they happen;
then the step carries no entries. Otherwise (graph input, a fork,
`durability="exit"`) it carries them inline.
A resume at a checkpoint the thread moved past re-runs finished tasks
on that same checkpoint, and a saver without native log support keeps
the first run's writes under the same keys. So the first step of such
a resume carries its entries inline too.
So does a step that commits a seed (the list a field stored before it
became a log channel), which never went through `put_writes`. The
channel's version is bumped if the step didn't change it, so that the
saver stores the step and the checkpoint points at the log.
"""
inline = (
metadata.get("source") != "loop"
or self.durability == "exit"
or self._log_inline_first_step
)
if metadata.get("source") == "loop":
self._log_inline_first_step = False
values = self.checkpoint["channel_values"]
for k, ch in self.channels.items():
if not isinstance(ch, Log):
continue
step = values.get(k)
seeding, ch.seeding = ch.seeding, False
if seeding:
versions = self.checkpoint["channel_versions"]
if versions.get(k) == self.checkpoint_previous_versions.get(k):
versions[k] = self.checkpointer_get_next_version(
versions.get(k), None
)
if isinstance(step, LogStep) and step.entries is not None:
values[k] = LogStep(
step.start,
step.count,
self._log_exec,
step.entries if inline or seeding else None,
)
def _put_exit_delta_writes(self) -> None:
"""Stage stub + accumulated delta writes so final_checkpoint's put
waits on them (visibility invariant: both must be durable before
@@ -1794,13 +1709,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
self.checkpointer_put_writes = None
self.checkpointer_put_writes_accepts_task_path = False
def _put_writes_after_checkpoint(
self, prev: concurrent.futures.Future | None, *args: Any
) -> None:
if prev is not None:
prev.result()
cast(Callable[..., None], self.checkpointer_put_writes)(*args)
def _checkpointer_put_after_previous(
self,
prev: concurrent.futures.Future | None,
@@ -1913,10 +1821,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
self._addressed_checkpoint_superseded = checkpoint_superseded(
self.checkpointer, self.checkpoint_config, saved
)
self._log_inline_first_step = (
self._addressed_checkpoint_superseded
and not self.checkpointer.supports_log
)
elif replay_state := self.config[CONF].get(CONFIG_KEY_REPLAY_STATE):
# Subgraph replay: the parent is replaying and passed us a
# replay_state with its checkpoint_id. Look up our checkpoint
@@ -1961,10 +1865,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
self.checkpoint = saved.checkpoint
self.checkpoint_metadata = saved.metadata
self.checkpoint_pending_writes = (
[
(str(tid), k, self._log_write_from_saver(k, v))
for tid, k, v in saved.pending_writes
]
[(str(tid), k, v) for tid, k, v in saved.pending_writes]
if saved.pending_writes is not None
else []
)
@@ -2064,13 +1965,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
self.checkpointer_put_writes = None
self.checkpointer_put_writes_accepts_task_path = False
async def _put_writes_after_checkpoint(
self, prev: asyncio.Task | None, *args: Any
) -> None:
if prev is not None:
await prev
await cast(Callable[..., Any], self.checkpointer_put_writes)(*args)
async def _checkpointer_put_after_previous(
self,
prev: asyncio.Task | None,
@@ -2183,10 +2077,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
self._addressed_checkpoint_superseded = await acheckpoint_superseded(
self.checkpointer, self.checkpoint_config, saved
)
self._log_inline_first_step = (
self._addressed_checkpoint_superseded
and not self.checkpointer.supports_log
)
elif replay_state := self.config[CONF].get(CONFIG_KEY_REPLAY_STATE):
# Subgraph replay: the parent is replaying and passed us a
# replay_state with its checkpoint_id. Look up our checkpoint
@@ -2231,10 +2121,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
self.checkpoint = saved.checkpoint
self.checkpoint_metadata = saved.metadata
self.checkpoint_pending_writes = (
[
(str(tid), k, self._log_write_from_saver(k, v))
for tid, k, v in saved.pending_writes
]
[(str(tid), k, v) for tid, k, v in saved.pending_writes]
if saved.pending_writes is not None
else []
)
+8 -29
View File
@@ -23,10 +23,9 @@ from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResul
from pydantic import BaseModel
from langgraph._internal._config import filter_to_user_tags
from langgraph._internal._constants import NS_SEP, PUSH
from langgraph._internal._constants import NS_SEP
from langgraph.channels.binop import _get_overwrite
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.log import MessagesLog
from langgraph.constants import TAG_HIDDEN, TAG_NOSTREAM
from langgraph.pregel.protocol import StreamChunk
from langgraph.types import Command, Overwrite
@@ -56,23 +55,6 @@ def _state_values(obj: Any) -> Sequence[Any]:
return ()
def graph_namespace(metadata: Mapping[str, Any]) -> tuple[str, ...]:
"""Namespace of the graph a task runs in, from the task's run metadata.
Drops the task's own segment, plus one per enclosing `@task` call: a call
runs in its caller's graph but nests its segment under the caller's. A
call's `langgraph_path` is `(PUSH, <caller's path>, <index>, True)`, while
a `Send` task's ends in `False`.
"""
ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))
path = metadata.get("langgraph_path", ())
drop = 1
while len(path) == 4 and path[0] == PUSH and path[-1] is True:
drop += 1
path = path[1]
return ns[:-drop]
class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
"""A callback handler that implements stream_mode=messages.
@@ -166,7 +148,9 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
**kwargs: Any,
) -> Any:
if metadata and (not tags or (TAG_NOSTREAM not in tags)):
ns = graph_namespace(metadata)
ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))[
:-1
]
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
return
if (filtered_tags := filter_to_user_tags(tags)) is not None:
@@ -229,8 +213,10 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
and kwargs.get("name") == metadata.get("langgraph_node")
and (not tags or TAG_HIDDEN not in tags)
):
ns = graph_namespace(metadata)
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))[
:-1
]
if not self.subgraphs and len(ns) > 0:
return
self.metadata[run_id] = (ns, metadata)
for value in _state_values(inputs):
@@ -538,13 +524,6 @@ def ensure_message_ids(
"""
for i, (ch, value) in enumerate(writes):
spec = channels.get(ch)
if isinstance(spec, MessagesLog):
# ids must be set before the write is saved, so the saved
# entries and the run's in-memory messages agree
new_value = _with_message_ids(value)
if new_value is not value:
cast(MutableSequence[tuple[str, Any]], writes)[i] = (ch, new_value)
continue
if not isinstance(spec, DeltaChannel):
continue
if spec.reducer is _messages_delta_reducer:
+11 -2
View File
@@ -227,14 +227,23 @@ 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
trace_outputs = self.trace_policy.process_outputs 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_policy=self.trace_policy)
return RunnableSeq(
*writers, trace_inputs=trace_inputs, trace_outputs=trace_outputs
)
elif writers:
return RunnableSeq(self.bound, *writers, trace_policy=self.trace_policy)
return RunnableSeq(
self.bound,
*writers,
trace_inputs=trace_inputs,
trace_outputs=trace_outputs,
)
else:
return self.bound
+3 -9
View File
@@ -34,7 +34,6 @@ from langgraph._internal._constants import (
CONFIG_KEY_THREAD_ID,
CONFIG_KEY_TIMED_ATTEMPT_OBSERVER,
NS_SEP,
RETURN,
)
from langgraph._internal._runnable import create_task_in_config_context
from langgraph._internal._timeout import sync_timeout_unsupported
@@ -734,14 +733,15 @@ async def arun_with_retry(
if stream:
async for _ in task.proc.astream(task.input, config):
pass
return _return_value(task)
break
return await task.proc.ainvoke(task.input, config)
result = await _arun_with_timeout(
task, config, resolved_timeout, attempt_ctx, stream=stream
)
_finish_timed_attempt(config, attempt_ctx)
if stream:
return _return_value(task)
# if successful, end
break
return result
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
@@ -831,12 +831,6 @@ async def arun_with_retry(
config = patch_configurable(config, {CONFIG_KEY_RESUMING: True})
def _return_value(task: PregelExecutableTask) -> Any:
# Streaming drains the task instead of returning its result, so a caller
# awaiting a `@task` needs the value the task wrote.
return next((value for channel, value in task.writes if channel == RETURN), None)
def _should_retry_on(retry_policy: RetryPolicy, exc: Exception) -> bool:
"""Check if the given exception should be retried based on the retry policy."""
if isinstance(retry_policy.retry_on, Sequence):
+12 -12
View File
@@ -7,9 +7,8 @@ from uuid import UUID
from langchain_core.callbacks import BaseCallbackHandler
from langgraph._internal._constants import NS_SEP
from langgraph.constants import TAG_NOSTREAM
from langgraph.errors import GraphDrained, GraphInterrupt
from langgraph.pregel._messages import graph_namespace
from langgraph.pregel.protocol import StreamChunk
try:
@@ -92,9 +91,12 @@ class StreamToolCallHandler(BaseCallbackHandler, _StreamingCallbackHandler):
) -> tuple[str, ...] | None:
"""Resolve the namespace this tool call should emit at, or `None` to skip.
Mirrors `StreamMessagesHandler.on_chat_model_start`: the namespace
is that of the graph the tool runs in (`graph_namespace`). Returns
`None` when the call should be silently suppressed:
Mirrors `StreamMessagesHandler.on_chat_model_start`'s namespace
derivation: parses `langgraph_checkpoint_ns` (which ends with
the `node_name:task_id` of the calling node), drops that
trailing segment, and returns the containing subgraph's own
namespace. Returns `None` when the call should be silently
suppressed:
- `metadata` is missing — handler is attached to a context
without Pregel routing info.
@@ -107,9 +109,11 @@ class StreamToolCallHandler(BaseCallbackHandler, _StreamingCallbackHandler):
return None
if tags and TAG_NOSTREAM in tags:
return None
ns = (
graph_namespace(metadata) if metadata.get("langgraph_checkpoint_ns") else ()
)
nskey = metadata.get("langgraph_checkpoint_ns")
if not nskey:
ns: tuple[str, ...] = ()
else:
ns = tuple(cast(str, nskey).split(NS_SEP))[:-1]
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
return None
return ns
@@ -184,10 +188,6 @@ class StreamToolCallHandler(BaseCallbackHandler, _StreamingCallbackHandler):
return
ns, tool_call_id, token = info
self._reset_writer(token)
# An interrupted or drained tool is paused, not failed: it runs again
# on resume and reports its start and end then.
if isinstance(error, (GraphInterrupt, GraphDrained)):
return
self.stream(
(
ns,
+50 -149
View File
@@ -49,8 +49,6 @@ from langgraph.checkpoint.base import (
ChannelVersions,
Checkpoint,
CheckpointTuple,
LogPage,
log_range,
)
from langgraph.store.base import BaseStore
from pydantic import BaseModel, TypeAdapter
@@ -99,7 +97,6 @@ from langgraph._internal._runnable import (
Runnable,
RunnableLike,
RunnableSeq,
_is_stream_handler,
coerce_to_runnable,
)
from langgraph._internal._timeout import coerce_timeout_policy
@@ -112,7 +109,6 @@ from langgraph.callbacks import (
)
from langgraph.channels.base import BaseChannel
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.log import Log
from langgraph.channels.topic import Topic
from langgraph.config import get_config
from langgraph.constants import END
@@ -207,6 +203,11 @@ from langgraph.types import (
from langgraph.typing import ContextT, InputT, OutputT, StateT
from langgraph.warnings import LangGraphDeprecatedSinceV10
try:
from langchain_core.tracers._streaming import _StreamingCallbackHandler
except ImportError:
_StreamingCallbackHandler = None # type: ignore
__all__ = ("NodeBuilder", "Pregel")
_WriteValue = Callable[[Input], Output] | Any
@@ -1248,7 +1249,6 @@ class Pregel(
saved.checkpoint,
saver=saver,
config=saved.config,
load_logs=False,
)
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
@@ -1374,7 +1374,6 @@ class Pregel(
saved.checkpoint,
saver=saver,
config=saved.config,
load_logs=False,
)
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
@@ -1487,97 +1486,6 @@ class Pregel(
live=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
)
def read_log(
self,
config: RunnableConfig,
channel: str,
*,
after: int | None = None,
before: int | None = None,
limit: int | None = None,
) -> LogPage:
"""Read a log channel as of a checkpoint (prototype).
The same operation as `BaseCheckpointSaver.read_log`, with entries
decoded by the channel (messages for a `MessagesLog`).
"""
checkpointer = self._state_checkpointer(config)
if subgraph := self._subgraph_for_namespace(config, checkpointer):
pregel, subgraph_config = subgraph
return pregel.read_log( # type: ignore[attr-defined]
subgraph_config, channel, after=after, before=before, limit=limit
)
config = merge_configs(self.config, config) if self.config else config
config = self._own_checkpoint_config(config)
page = checkpointer.read_log(
config, channel, after=after, before=before, limit=limit
)
if page.length == 0 and (tup := checkpointer.get_tuple(config)) is not None:
if (
pre := self._pre_log_page(channel, tup, after, before, limit)
) is not None:
return pre
return self._decode_log_page(channel, page)
async def aread_log(
self,
config: RunnableConfig,
channel: str,
*,
after: int | None = None,
before: int | None = None,
limit: int | None = None,
) -> LogPage:
"""Async version of `read_log`."""
checkpointer = self._state_checkpointer(config)
if subgraph := self._subgraph_for_namespace(config, checkpointer):
pregel, subgraph_config = subgraph
return await pregel.aread_log( # type: ignore[attr-defined]
subgraph_config, channel, after=after, before=before, limit=limit
)
config = merge_configs(self.config, config) if self.config else config
config = self._own_checkpoint_config(config)
page = await checkpointer.aread_log(
config, channel, after=after, before=before, limit=limit
)
if (
page.length == 0
and (tup := await checkpointer.aget_tuple(config)) is not None
):
if (
pre := self._pre_log_page(channel, tup, after, before, limit)
) is not None:
return pre
return self._decode_log_page(channel, page)
def _pre_log_page(
self,
channel: str,
tup: CheckpointTuple,
after: int | None,
before: int | None,
limit: int | None,
) -> LogPage | None:
"""A page of the list a checkpoint stored before the field became a
log channel, or `None` if it stored none."""
spec = self.channels.get(channel)
stored = tup.checkpoint["channel_values"].get(channel)
if not isinstance(spec, Log) or not isinstance(stored, list):
return None
entries = spec.from_checkpoint(stored).entries
lo, hi = log_range(len(entries), after, before, limit)
return LogPage([(i, entries[i]) for i in range(lo, hi)], len(entries), [])
def _decode_log_page(self, channel: str, page: LogPage) -> LogPage:
spec = self.channels.get(channel)
if not isinstance(spec, Log):
raise ValueError(f"Channel {channel!r} is not a log channel")
return LogPage(
[(pos, spec.decode(e)) for pos, e in page.entries],
page.length,
[spec.decode(e) for e in page.pending],
)
async def aget_state(
self, config: RunnableConfig, *, subgraphs: bool = False
) -> StateSnapshot:
@@ -2972,20 +2880,6 @@ class Pregel(
for h in callback_manager.inheritable_handlers
if not isinstance(h, StreamMessagesHandlerV2)
]
if CONFIG_KEY_TASK_ID not in config[CONF]:
# Not run as a task (a child invoked with its own thread, say),
# this graph's namespace starts at the root, so a caller's
# handler would take this graph's events for its own graph's.
callback_manager.handlers = [
h
for h in callback_manager.handlers
if not _is_stream_mode_handler(h)
]
callback_manager.inheritable_handlers = [
h
for h in callback_manager.inheritable_handlers
if not _is_stream_mode_handler(h)
]
if "ls_integration" not in callback_manager.metadata:
callback_manager.add_metadata({"ls_integration": "langgraph"})
run_manager = callback_manager.on_chain_start(
@@ -3004,9 +2898,9 @@ class Pregel(
)
# set up subgraph checkpointing
config = self._own_checkpoint_config(config)
stream_handlers: list[StreamMessagesHandler | StreamToolCallHandler] = []
# set up messages stream mode
if "messages" in stream_modes:
ns_ = cast(str | None, config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
use_stream_messages_v2 = bool(
version == "v2" and config[CONF].get(CONFIG_KEY_STREAM_MESSAGES_V2)
)
@@ -3015,12 +2909,24 @@ class Pregel(
if use_stream_messages_v2
else StreamMessagesHandler
)
stream_handlers.append(messages_handler_cls(stream.put, subgraphs))
run_manager.inheritable_handlers.append(
messages_handler_cls(
stream.put,
subgraphs,
parent_ns=tuple(ns_.split(NS_SEP)) if ns_ else None,
)
)
# set up tools stream mode
if "tools" in stream_modes:
stream_handlers.append(StreamToolCallHandler(stream.put, subgraphs))
run_manager.inheritable_handlers.extend(stream_handlers)
ns_tools = cast(str | None, config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
run_manager.inheritable_handlers.append(
StreamToolCallHandler(
stream.put,
subgraphs,
parent_ns=tuple(ns_tools.split(NS_SEP)) if ns_tools else None,
)
)
# set up custom stream mode
if "custom" in stream_modes:
@@ -3103,10 +3009,6 @@ class Pregel(
cache_policy=self.cache_policy,
has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
) as loop:
# Known only now: the loop numbers the namespace of every
# subgraph a task runs after its first.
for handler in stream_handlers:
handler.parent_ns = loop.checkpoint_ns
emit_graph_lifecycle_events(loop)
# create runner
runner = PregelRunner(
@@ -3389,20 +3291,6 @@ class Pregel(
for h in callback_manager.inheritable_handlers
if not isinstance(h, StreamMessagesHandlerV2)
]
if CONFIG_KEY_TASK_ID not in config[CONF]:
# Not run as a task (a child invoked with its own thread, say),
# this graph's namespace starts at the root, so a caller's
# handler would take this graph's events for its own graph's.
callback_manager.handlers = [
h
for h in callback_manager.handlers
if not _is_stream_mode_handler(h)
]
callback_manager.inheritable_handlers = [
h
for h in callback_manager.inheritable_handlers
if not _is_stream_mode_handler(h)
]
if "ls_integration" not in callback_manager.metadata:
callback_manager.add_metadata({"ls_integration": "langgraph"})
run_manager = await callback_manager.on_chain_start(
@@ -3416,10 +3304,18 @@ class Pregel(
run_id=run_manager.run_id,
)
# if running from astream_log() run each proc with streaming
do_stream = any(
_is_stream_handler(h)
and not isinstance(h, (StreamMessagesHandler, StreamToolCallHandler))
for h in run_manager.handlers
do_stream = (
next(
(
True
for h in run_manager.handlers
if isinstance(h, _StreamingCallbackHandler)
and not isinstance(h, StreamMessagesHandler)
),
False,
)
if _StreamingCallbackHandler is not None
else False
)
if checkpointer is None and durability is not None:
warnings.warn(
@@ -3427,9 +3323,10 @@ class Pregel(
)
# set up subgraph checkpointing
config = self._own_checkpoint_config(config)
stream_handlers: list[StreamMessagesHandler | StreamToolCallHandler] = []
# set up messages stream mode
if "messages" in stream_modes:
# namespace can be None in a root level graph?
ns_ = cast(str | None, config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
use_stream_messages_v2 = bool(
version == "v2" and config[CONF].get(CONFIG_KEY_STREAM_MESSAGES_V2)
)
@@ -3438,12 +3335,24 @@ class Pregel(
if use_stream_messages_v2
else StreamMessagesHandler
)
stream_handlers.append(messages_handler_cls(stream_put, subgraphs))
run_manager.inheritable_handlers.append(
messages_handler_cls(
stream_put,
subgraphs,
parent_ns=tuple(ns_.split(NS_SEP)) if ns_ else None,
)
)
# set up tools stream mode
if "tools" in stream_modes:
stream_handlers.append(StreamToolCallHandler(stream_put, subgraphs))
run_manager.inheritable_handlers.extend(stream_handlers)
ns_tools = cast(str | None, config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
run_manager.inheritable_handlers.append(
StreamToolCallHandler(
stream_put,
subgraphs,
parent_ns=tuple(ns_tools.split(NS_SEP)) if ns_tools else None,
)
)
# set up custom stream mode
def stream_writer(c: Any) -> None:
@@ -3552,10 +3461,6 @@ class Pregel(
cache_policy=self.cache_policy,
has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
) as loop:
# Known only now: the loop numbers the namespace of every
# subgraph a task runs after its first.
for handler in stream_handlers:
handler.parent_ns = loop.checkpoint_ns
await aemit_graph_lifecycle_events(loop)
# create runner
runner = PregelRunner(
@@ -4553,10 +4458,6 @@ def _coerce_checkpoint_values(payload: Any, mapper: Callable[[Any], Any]) -> Non
payload["values"] = mapper(payload["values"])
def _is_stream_mode_handler(handler: object) -> bool:
return isinstance(handler, (StreamMessagesHandler, StreamToolCallHandler))
def _resolve_parent_ns(
graph_config: RunnableConfig | None, call_config: RunnableConfig | None
) -> tuple[str, ...]:
-10
View File
@@ -847,11 +847,6 @@ class RemoteGraph(PregelProtocol):
# filter for what was actually requested
if mode not in requested:
continue
# subgraph events may have been requested only for the parent stream.
# messages are exempt: the server dedupes them across namespaces, so
# dropping them here would also drop the root-level message
if not subgraphs and NS_SEP in chunk.event and mode != "messages":
continue
if chunk.event.startswith("messages"):
chunk = chunk._replace(data=tuple(chunk.data))
@@ -1002,11 +997,6 @@ class RemoteGraph(PregelProtocol):
# filter for what was actually requested
if mode not in requested:
continue
# subgraph events may have been requested only for the parent stream.
# messages are exempt: the server dedupes them across namespaces, so
# dropping them here would also drop the root-level message
if not subgraphs and NS_SEP in chunk.event and mode != "messages":
continue
if chunk.event.startswith("messages"):
chunk = chunk._replace(data=tuple(chunk.data))
-8
View File
@@ -7,7 +7,6 @@ __all__ = (
"LangGraphDeprecatedSinceV05",
"LangGraphDeprecatedSinceV10",
"LangGraphDeprecatedSinceV11",
"LangGraphDeprecatedSinceV13",
)
@@ -68,10 +67,3 @@ class LangGraphDeprecatedSinceV11(LangGraphDeprecationWarning):
def __init__(self, message: str, *args: object) -> None:
super().__init__(message, *args, since=(1, 1), expected_removal=(3, 0))
class LangGraphDeprecatedSinceV13(LangGraphDeprecationWarning):
"""A specific `LangGraphDeprecationWarning` subclass defining functionality deprecated since LangGraph v1.3.0"""
def __init__(self, message: str, *args: object) -> None:
super().__init__(message, *args, since=(1, 3), expected_removal=(2, 0))
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.2.19"
version = "1.2.17"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
@@ -133,11 +133,6 @@
"ToolNode"
],
"name": "tools"
},
"metadata": {
"__tools": [
"search_api"
]
}
},
{
@@ -226,11 +221,6 @@
"ToolNode"
],
"name": "tools"
},
"metadata": {
"__tools": [
"search_api"
]
}
},
{
-718
View File
@@ -1,718 +0,0 @@
"""Transcript log prototype: append-only `Log` / `MessagesLog` channels.
Every scenario runs against `InMemorySaver` (native log storage: segments,
step rows, range reads) and `SqliteSaver` (no native support: the default
`read_log` walks the parent chain), and both must read the same log.
"""
import operator
import sqlite3
import warnings
from collections.abc import Iterator
from typing import Annotated, Any
import pytest
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
from langchain_core.tools import tool
from langgraph.checkpoint.base import BaseCheckpointSaver, LogPage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
from langgraph.prebuilt import ToolNode, tools_condition
from typing_extensions import TypedDict
from langgraph.channels import Log, LogRef, MessagesLog
from langgraph.errors import InvalidUpdateError
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import REMOVE_ALL_MESSAGES, add_messages
from langgraph.types import Command, Overwrite, Send, interrupt
from langgraph.warnings import LangGraphDeprecatedSinceV13
pytestmark = pytest.mark.anyio
@pytest.fixture(params=["memory", "sqlite"])
def saver(request: pytest.FixtureRequest) -> Iterator[BaseCheckpointSaver]:
if request.param == "memory":
yield InMemorySaver()
else:
conn = sqlite3.connect(":memory:", check_same_thread=False)
yield SqliteSaver(conn)
conn.close()
def contents(page: LogPage) -> list[Any]:
return [e.content if hasattr(e, "content") else e for _, e in page.entries]
class ChatState(TypedDict):
messages: Annotated[list, MessagesLog()]
def chat_graph(saver: BaseCheckpointSaver, **compile_kw: Any):
def model(state: ChatState) -> dict:
return {"messages": [AIMessage(f"reply to {state['messages'][-1].content}")]}
builder = StateGraph(ChatState)
builder.add_node("model", model)
builder.add_edge(START, "model")
builder.add_edge("model", END)
return builder.compile(checkpointer=saver, **compile_kw)
def test_appends_reads_and_state_marker(saver: BaseCheckpointSaver) -> None:
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config)
out = graph.invoke({"messages": [HumanMessage("b")]}, config)
# nodes and invoke() see the list
assert [m.content for m in out["messages"]] == [
"a",
"reply to a",
"b",
"reply to b",
]
# state reads return the marker, not the transcript
assert graph.get_state(config).values == {"messages": LogRef(4)}
# one read operation, same arguments everywhere
page = graph.read_log(config, "messages")
assert page.length == 4
assert contents(page) == ["a", "reply to a", "b", "reply to b"]
assert [p for p, _ in page.entries] == [0, 1, 2, 3]
assert contents(graph.read_log(config, "messages", limit=2)) == ["b", "reply to b"]
assert contents(graph.read_log(config, "messages", before=2, limit=1)) == [
"reply to a"
]
assert contents(graph.read_log(config, "messages", after=1, limit=1)) == ["b"]
# the saver-level read returns JSON
raw = saver.read_log(config, "messages")
assert raw.entries[0][1]["type"] == "human"
# every checkpoint points at its own leaf
lengths = [
(s.metadata["source"], graph.read_log(s.config, "messages").length)
for s in graph.get_state_history(config)
]
assert lengths == [
("loop", 4),
("loop", 3),
("input", 2),
("loop", 2),
("loop", 1),
("input", 0),
]
async def test_async_appends_and_reads() -> None:
async with AsyncSqliteSaver.from_conn_string(":memory:") as fallback:
for saver in (InMemorySaver(), fallback):
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
await graph.ainvoke({"messages": [HumanMessage("a")]}, config)
await graph.ainvoke({"messages": [HumanMessage("b")]}, config)
state = await graph.aget_state(config)
assert state.values == {"messages": LogRef(4)}
page = await graph.aread_log(config, "messages")
assert contents(page) == ["a", "reply to a", "b", "reply to b"]
def test_log_entries_are_not_stored_in_checkpoints() -> None:
saver = InMemorySaver()
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
for i in range(20):
graph.invoke({"messages": [HumanMessage(f"m{i}")]}, config)
assert not [k for k in saver.blobs if k[2] == "messages"]
log = saver.log.logs[("1", "", "messages")]
stored = sum(len(rows) for rows in log.entries.values())
# each entry stored once: linear in the transcript
assert stored == 40 == graph.read_log(config, "messages").length
assert len(log.segments) == 1
def test_parallel_tasks_keep_one_order(saver: BaseCheckpointSaver) -> None:
class State(TypedDict):
events: Annotated[list, Log()]
seen: Annotated[list, operator.add]
def fan_out(state: State) -> list[Send]:
return [Send("worker", {"i": i}) for i in range(5)]
def worker(arg: dict) -> dict:
return {"events": [{"worker": arg["i"]}, {"worker": arg["i"], "n": 2}]}
def after(state: State) -> dict:
return {"seen": [list(state["events"])]}
builder = StateGraph(State)
builder.add_node("worker", worker)
builder.add_node("after", after)
builder.add_conditional_edges(START, fan_out, ["worker"])
builder.add_edge("worker", "after")
builder.add_edge("after", END)
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "1"}}
out = graph.invoke({"events": [], "seen": []}, config)
in_run = out["seen"][-1]
assert len(in_run) == 10
# read back after the run: the same order the run's nodes saw
assert contents(graph.read_log(config, "events")) == in_run
def test_interrupt_and_resume_reuse_finished_writes(
saver: BaseCheckpointSaver,
) -> None:
calls: list[str] = []
class State(TypedDict):
events: Annotated[list, Log()]
def fast(state: State) -> dict:
calls.append("fast")
return {"events": [{"from": "fast"}]}
def asks(state: State) -> dict:
answer = interrupt("question")
calls.append("asks")
return {"events": [{"from": "asks", "answer": answer}]}
builder = StateGraph(State)
builder.add_node("fast", fast)
builder.add_node("asks", asks)
builder.add_edge(START, "fast")
builder.add_edge(START, "asks")
builder.add_edge(["fast", "asks"], END)
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"events": [{"from": "input"}]}, config)
# fast finished; its entry is pending at the head, not committed
page = saver.read_log(config, "events")
assert page.length == 1
assert page.pending == [{"from": "fast"}]
out = graph.invoke(Command(resume="yes"), config)
assert calls == ["fast", "asks"] # fast was not re-run
# the reused write and the new one commit as one step, in path order,
# and the log reads back exactly what the run's state held
assert out["events"] == [
{"from": "input"},
{"from": "asks", "answer": "yes"},
{"from": "fast"},
]
assert contents(graph.read_log(config, "events")) == out["events"]
assert graph.read_log(config, "events").pending == []
def test_time_travel_forks_without_touching_the_original(
saver: BaseCheckpointSaver,
) -> None:
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config)
graph.invoke({"messages": [HumanMessage("b")]}, config)
original = graph.get_state(config).config
# the checkpoint before the second reply ("b" added, model not run)
before_reply = next(
s
for s in graph.get_state_history(config)
if s.next == ("model",) and graph.read_log(s.config, "messages").length == 3
)
graph.invoke(None, before_reply.config) # replay: a new branch
head = graph.get_state(config).config
assert (
head["configurable"]["checkpoint_id"]
!= original["configurable"]["checkpoint_id"]
)
assert contents(graph.read_log(head, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
assert contents(graph.read_log(original, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
a = graph.read_log(head, "messages").entries
b = graph.read_log(original, "messages").entries
# same prefix, different last message (a new run of the model node)
assert [m.id for _, m in a[:3]] == [m.id for _, m in b[:3]]
assert a[3][1].id != b[3][1].id
if isinstance(saver, InMemorySaver):
assert len(saver.log.logs[("1", "", "messages")].segments) == 2
def test_update_state_fork_starts_a_branch(saver: BaseCheckpointSaver) -> None:
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config)
graph.invoke({"messages": [HumanMessage("b")]}, config)
original = graph.get_state(config).config
first_turn = next(
s
for s in graph.get_state_history(config)
if graph.read_log(s.config, "messages").length == 2
and s.metadata["source"] == "loop"
)
# edit: fork from the end of the first turn with a different question
forked = graph.update_state(
first_turn.config, {"messages": [HumanMessage("c")]}, as_node="model"
)
assert contents(graph.read_log(forked, "messages")) == ["a", "reply to a", "c"]
graph.invoke(Command(goto="model"), forked)
head = graph.get_state(config).config
assert contents(graph.read_log(head, "messages")) == [
"a",
"reply to a",
"c",
"reply to c",
]
assert contents(graph.read_log(original, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
def test_subgraph_has_its_own_log(saver: BaseCheckpointSaver) -> None:
class Sub(TypedDict):
notes: Annotated[list, Log()]
def note(state: Sub) -> dict:
return {"notes": [{"n": len(state.get("notes", []))}]}
sub_builder = StateGraph(Sub)
sub_builder.add_node("note", note)
sub_builder.add_edge(START, "note")
sub = sub_builder.compile(checkpointer=True)
class Parent(TypedDict):
messages: Annotated[list, MessagesLog()]
def call_sub(state: Parent) -> dict:
sub.invoke({"notes": [{"from": "parent"}]})
return {"messages": [AIMessage("done")]}
builder = StateGraph(Parent)
builder.add_node("call_sub", call_sub)
builder.add_edge(START, "call_sub")
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("go")]}, config)
graph.invoke({"messages": [HumanMessage("again")]}, config)
assert contents(graph.read_log(config, "messages")) == [
"go",
"done",
"again",
"done",
]
sub_config = {"configurable": {"thread_id": "1", "checkpoint_ns": "call_sub"}}
assert contents(saver.read_log(sub_config, "notes")) == [
{"from": "parent"},
{"n": 1},
{"from": "parent"},
{"n": 3},
]
def test_exit_durability_commits_inline(saver: BaseCheckpointSaver) -> None:
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config, durability="exit")
graph.invoke({"messages": [HumanMessage("b")]}, config, durability="exit")
assert contents(graph.read_log(config, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
def test_prune_keeps_the_heads_transcript() -> None:
saver = InMemorySaver()
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config)
graph.invoke({"messages": [HumanMessage("b")]}, config)
original = graph.get_state(config).config
before_reply = next(
s
for s in graph.get_state_history(config)
if s.next == ("model",) and graph.read_log(s.config, "messages").length == 3
)
graph.invoke(None, before_reply.config)
log = saver.log.logs[("1", "", "messages")]
assert len(log.segments) == 2
saver.prune(["1"], strategy="keep_latest")
assert len(saver.storage["1"][""]) == 1
# the head's full transcript survives without its ancestors' checkpoints
assert contents(graph.read_log(config, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
# the abandoned branch is collected
log = saver.log.logs[("1", "", "messages")]
assert sum(len(r) for r in log.entries.values()) == 4
assert len(log.steps) == 4
assert graph.read_log(original, "messages").length == 0 # its checkpoint is gone
# the thread keeps working after prune
graph.invoke({"messages": [HumanMessage("c")]}, config)
assert graph.read_log(config, "messages").length == 6
saver.prune(["1"], strategy="delete")
assert ("1", "", "messages") not in saver.log.logs
def test_rollback_collects_the_runs_entries() -> None:
saver = InMemorySaver()
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke(
{"messages": [HumanMessage("a")]}, {**config, "metadata": {"run_id": "r1"}}
)
graph.invoke(
{"messages": [HumanMessage("b")]}, {**config, "metadata": {"run_id": "r2"}}
)
saver.delete_for_runs(["r2"])
assert contents(graph.read_log(config, "messages")) == ["a", "reply to a"]
log = saver.log.logs[("1", "", "messages")]
assert sum(len(r) for r in log.entries.values()) == 2
# the tip moved back: the next run continues the same segment
graph.invoke({"messages": [HumanMessage("c")]}, config)
assert contents(graph.read_log(config, "messages")) == [
"a",
"reply to a",
"c",
"reply to c",
]
assert len(saver.log.logs[("1", "", "messages")].segments) == 1
def test_copy_and_delete_thread() -> None:
saver = InMemorySaver()
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config)
saver.copy_thread("1", "2")
copy_config = {"configurable": {"thread_id": "2"}}
graph.invoke({"messages": [HumanMessage("b")]}, copy_config)
assert contents(graph.read_log(config, "messages")) == ["a", "reply to a"]
assert contents(graph.read_log(copy_config, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
saver.delete_thread("1")
assert ("1", "", "messages") not in saver.log.logs
assert graph.read_log(copy_config, "messages").length == 4
def test_log_takes_only_json(saver: BaseCheckpointSaver) -> None:
class State(TypedDict):
events: Annotated[list, Log()]
def bad(state: State) -> dict:
return {"events": [{"when": object()}]}
builder = StateGraph(State)
builder.add_node("bad", bad)
builder.add_edge(START, "bad")
graph = builder.compile(checkpointer=saver)
with pytest.raises(InvalidUpdateError, match="not JSON"):
graph.invoke({"events": [{"ok": 1}]}, {"configurable": {"thread_id": "1"}})
with pytest.raises(InvalidUpdateError, match="not JSON"):
graph.invoke({"events": [float("nan")]}, {"configurable": {"thread_id": "2"}})
@pytest.mark.parametrize(
"write",
[
pytest.param(lambda s: [RemoveMessage(id=s["messages"][0].id)], id="remove"),
pytest.param(lambda s: Overwrite([AIMessage("x")]), id="overwrite"),
pytest.param(lambda s: [s["messages"][0]], id="replace-by-id"),
pytest.param(lambda s: s["messages"] + [AIMessage("x")], id="whole-list"),
],
)
def test_messages_log_rejects_edits(saver: BaseCheckpointSaver, write: Any) -> None:
def edits(state: ChatState) -> dict:
return {"messages": write(state)}
builder = StateGraph(ChatState)
builder.add_node("edits", edits)
builder.add_edge(START, "edits")
graph = builder.compile(checkpointer=saver)
with pytest.raises(InvalidUpdateError):
graph.invoke(
{"messages": [HumanMessage("a")]}, {"configurable": {"thread_id": "1"}}
)
def test_fallback_and_native_read_the_same_log() -> None:
def run(saver: BaseCheckpointSaver) -> list[list[Any]]:
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
for q in "abc":
graph.invoke({"messages": [HumanMessage(q)]}, config)
return [
contents(graph.read_log(s.config, "messages"))
for s in graph.get_state_history(config)
]
conn = sqlite3.connect(":memory:", check_same_thread=False)
try:
assert run(InMemorySaver()) == run(SqliteSaver(conn))
finally:
conn.close()
def test_resume_at_a_superseded_checkpoint_branches(
saver: BaseCheckpointSaver,
) -> None:
"""A resume at a checkpoint the thread moved past re-runs finished tasks on
that same checkpoint, without a fork checkpoint. The log puts the re-run
in a new segment and leaves the original branch alone."""
calls: list[str] = []
class State(TypedDict):
events: Annotated[list, Log()]
def fast(state: State) -> dict:
calls.append("fast")
return {"events": [{"from": "fast", "run": len(calls)}]}
def asks(state: State) -> dict:
answer = interrupt("question")
return {"events": [{"from": "asks", "answer": answer}]}
builder = StateGraph(State)
builder.add_node("fast", fast)
builder.add_node("asks", asks)
builder.add_edge(START, "fast")
builder.add_edge(START, "asks")
builder.add_edge(["fast", "asks"], END)
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"events": [{"from": "input"}]}, config)
interrupted = graph.get_state(config).config
first = graph.invoke(Command(resume="yes"), config)["events"]
original_head = graph.get_state(config).config
second = graph.invoke(Command(resume="no"), interrupted)["events"]
assert calls == ["fast", "fast"] # the finished task re-ran
new_head = graph.get_state(config).config
assert contents(graph.read_log(new_head, "events")) == second
assert contents(graph.read_log(original_head, "events")) == first
assert first != second
if isinstance(saver, InMemorySaver):
assert len(saver.log.logs[("1", "", "events")].segments) == 2
def test_tool_calling_agent_runs_on_a_messages_log(
saver: BaseCheckpointSaver,
) -> None:
"""Prebuilt `ToolNode` and `tools_condition` work unchanged: they read the
list and append messages."""
@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
def model(state: ChatState) -> dict:
last = state["messages"][-1]
if isinstance(last, HumanMessage):
call = {
"name": "add",
"args": {"a": 2, "b": 3},
"id": f"call-{len(state['messages'])}",
}
return {"messages": [AIMessage("", tool_calls=[call])]}
return {"messages": [AIMessage(f"the answer is {last.content}")]}
builder = StateGraph(ChatState)
builder.add_node("model", model)
builder.add_node("tools", ToolNode([add]))
builder.add_edge(START, "model")
builder.add_conditional_edges("model", tools_condition)
builder.add_edge("tools", "model")
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "1"}}
out = graph.invoke({"messages": [HumanMessage("2+3?")]}, config)
assert [type(m).__name__ for m in out["messages"]] == [
"HumanMessage",
"AIMessage",
"ToolMessage",
"AIMessage",
]
page = graph.read_log(config, "messages")
assert [m.id for _, m in page.entries] == [m.id for m in out["messages"]]
assert page.entries[-1][1].content == "the answer is 5"
def test_input_after_update_state_reads_the_inline_step(
saver: BaseCheckpointSaver,
) -> None:
"""`update_state` commits its entries inline; the input checkpoint after
it keeps the version with a stripped marker, and reads must still find
the entries on the checkpoint that committed them."""
graph = chat_graph(saver)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("a")]}, config)
graph.update_state(config, {"messages": [HumanMessage("note")]})
graph.invoke({"messages": [HumanMessage("b")]}, config)
assert contents(graph.read_log(config, "messages")) == [
"a",
"reply to a",
"note",
"b",
"reply to b",
]
# -- existing `add_messages` threads ------------------------------------------
class OldChatState(TypedDict):
messages: Annotated[list, add_messages]
def _reply(state: dict) -> dict:
return {"messages": [AIMessage(f"reply to {state['messages'][-1].content}")]}
def _chat(schema: type, saver: BaseCheckpointSaver, **nodes: Any):
builder = StateGraph(schema)
builder.add_node("model", nodes.get("model", _reply))
builder.add_edge(START, "model")
if "review" in nodes:
builder.add_node("review", nodes["review"])
builder.add_edge("model", "review")
builder.add_edge("review", END)
else:
builder.add_edge("model", END)
return builder.compile(checkpointer=saver)
def test_add_messages_thread_switches_to_messages_log(
saver: BaseCheckpointSaver,
) -> None:
"""A thread written with `add_messages` keeps working on `MessagesLog`:
the stored list is the log's first entries."""
config = {"configurable": {"thread_id": "1"}}
old = _chat(OldChatState, saver)
old.invoke({"messages": [HumanMessage("a", id="h1")]}, config)
old.invoke({"messages": [HumanMessage("b", id="h2")]}, config)
before_switch = old.get_state(config).config
new = _chat(ChatState, saver)
out = new.invoke({"messages": [HumanMessage("c", id="h3")]}, config)
assert [m.content for m in out["messages"]] == [
"a",
"reply to a",
"b",
"reply to b",
"c",
"reply to c",
]
assert new.get_state(config).values == {"messages": LogRef(6)}
page = new.read_log(config, "messages")
assert [p for p, _ in page.entries] == list(range(6))
assert contents(page) == [m.content for m in out["messages"]]
assert [m.id for _, m in page.entries][:1] == ["h1"]
# checkpoints from before the switch still read as the list they stored
assert contents(new.read_log(before_switch, "messages")) == [
"a",
"reply to a",
"b",
"reply to b",
]
assert len(new.get_state(before_switch).values["messages"]) == 4
# the next turn appends after the seed
new.invoke({"messages": [HumanMessage("d")]}, config)
assert new.read_log(config, "messages").length == 8
if isinstance(saver, InMemorySaver):
log = saver.log.logs[("1", "", "messages")]
assert sum(len(rows) for rows in log.entries.values()) == 8
def test_switch_with_an_interrupted_add_messages_run(
saver: BaseCheckpointSaver,
) -> None:
"""The first checkpoint after the switch can come from the loop (a
resume), not graph input; the seed is committed with it."""
def review(state: dict) -> dict:
answer = interrupt("approve?")
return {"messages": [AIMessage(f"review: {answer}")]}
config = {"configurable": {"thread_id": "1"}}
old = _chat(OldChatState, saver, review=review)
old.invoke({"messages": [HumanMessage("a")]}, config)
assert old.get_state(config).next == ("review",)
new = _chat(ChatState, saver, review=review)
out = new.invoke(Command(resume="ok"), config)
assert [m.content for m in out["messages"]] == ["a", "reply to a", "review: ok"]
assert contents(new.read_log(config, "messages")) == [
"a",
"reply to a",
"review: ok",
]
def test_switch_then_update_state(saver: BaseCheckpointSaver) -> None:
config = {"configurable": {"thread_id": "1"}}
old = _chat(OldChatState, saver)
old.invoke({"messages": [HumanMessage("a")]}, config)
new = _chat(ChatState, saver)
new.update_state(config, {"messages": [HumanMessage("note")]})
assert contents(new.read_log(config, "messages")) == ["a", "reply to a", "note"]
out = new.invoke({"messages": [HumanMessage("b")]}, config)
assert [m.content for m in out["messages"]][-2:] == ["b", "reply to b"]
assert new.read_log(config, "messages").length == 5
# -- deprecation of message edit ops on `add_messages` -------------------------
@pytest.mark.parametrize(
"edit",
[
pytest.param(lambda s: [RemoveMessage(id=s["messages"][0].id)], id="remove"),
pytest.param(
lambda s: [RemoveMessage(id=REMOVE_ALL_MESSAGES), AIMessage("x")],
id="remove-all",
),
pytest.param(
lambda s: [HumanMessage("edited", id=s["messages"][0].id)],
id="replace-by-id",
),
pytest.param(lambda s: Overwrite([AIMessage("x")]), id="overwrite"),
],
)
def test_add_messages_edit_ops_are_deprecated(edit: Any) -> None:
def node(state: dict) -> dict:
return {"messages": edit(state)}
graph = _chat(OldChatState, InMemorySaver(), model=node)
with pytest.warns(LangGraphDeprecatedSinceV13, match="MessagesLog"):
graph.invoke(
{"messages": [HumanMessage("a")]}, {"configurable": {"thread_id": "1"}}
)
def test_add_messages_appends_are_not_deprecated() -> None:
graph = _chat(OldChatState, InMemorySaver())
with warnings.catch_warnings():
warnings.simplefilter("error", LangGraphDeprecatedSinceV13)
graph.invoke(
{"messages": [HumanMessage("a")]}, {"configurable": {"thread_id": "1"}}
)
-33
View File
@@ -8950,39 +8950,6 @@ def test_subgraph_streaming_sync() -> None:
assert result["num_chunks"] == 9
def test_subgraph_streaming_node_output_messages_sync() -> None:
"""Messages returned by a node (not streamed by an LLM) are emitted when a
graph is explicitly streamed with `stream_mode="messages"` inside a node."""
def respond(state: MessagesState) -> dict:
return {"messages": [AIMessage("hi there", id="ai1")]}
subgraph = (
StateGraph(MessagesState)
.add_node("respond", respond)
.add_edge(START, "respond")
.compile()
)
msgs = {"messages": [("user", "hello")]}
top_level = [msg for msg, _ in subgraph.stream(msgs, stream_mode="messages")]
def parent_node(state: MessagesState, config: RunnableConfig) -> dict:
nonlocal in_node
in_node = [
msg for msg, _ in subgraph.stream(msgs, config, stream_mode="messages")
]
return {}
in_node: list = []
StateGraph(MessagesState).add_node("parent", parent_node).add_edge(
START, "parent"
).compile().invoke(msgs)
assert [m.content for m in top_level] == ["hi there"]
assert [m.content for m in in_node] == ["hi there"]
def test_get_graph_nonterminal_last_step_source(snapshot: SnapshotAssertion) -> None:
class State(TypedDict):
messages: list[str]
-97
View File
@@ -6834,31 +6834,6 @@ async def test_multiple_updates() -> None:
]
@NEEDS_CONTEXTVARS
async def test_task_in_subgraph_node_returns_its_value_with_tools_mode() -> None:
class State(TypedDict):
out: int | None
@task
async def double(x: int) -> int:
return x * 2
async def node(state: State) -> State:
return {"out": await double(21)}
inner = StateGraph(State).add_node("node", node).add_edge(START, "node").compile()
graph = (
StateGraph(State).add_node("inner", inner).add_edge(START, "inner").compile()
)
chunks = [
chunk
async for chunk in graph.astream({"out": None}, stream_mode=["values", "tools"])
]
assert chunks[-1] == ("values", {"out": 42})
@NEEDS_CONTEXTVARS
async def test_falsy_return_from_task(async_checkpointer: BaseCheckpointSaver) -> None:
"""Test with a falsy return from a task."""
@@ -7623,40 +7598,6 @@ async def test_entrypoint_from_async_generator() -> None:
yield "b"
@NEEDS_CONTEXTVARS
@pytest.mark.parametrize("timeout", [None, 30])
@pytest.mark.parametrize("version", ["v1", "v2"])
async def test_awaited_task_returns_its_value_under_astream_events(
version: Literal["v1", "v2"], timeout: int | None
) -> None:
class State(TypedDict):
doubled: int
zeroed: int
@task(timeout=timeout)
async def double(x: int) -> int:
return x * 2
@task(timeout=timeout)
async def zero() -> int:
return 0
async def node(state: State) -> State:
return {"doubled": await double(21), "zeroed": await zero()}
graph = StateGraph(State).add_node("node", node).add_edge(START, "node").compile()
outputs = [
event["data"]["output"]
async for event in graph.astream_events(
{"doubled": 0, "zeroed": 1}, version=version
)
if event["event"] == "on_chain_end" and event["name"] == "node"
]
assert outputs == [{"doubled": 42, "zeroed": 0}]
@NEEDS_CONTEXTVARS
async def test_named_tasks_functional() -> None:
class Foo:
@@ -9503,44 +9444,6 @@ async def test_subgraph_streaming_async() -> None:
assert result["num_chunks"] == 9
async def test_subgraph_streaming_node_output_messages_async() -> None:
"""Messages returned by a node (not streamed by an LLM) are emitted when a
graph is explicitly streamed with `stream_mode="messages"` inside a node."""
async def respond(state: MessagesState) -> dict:
return {"messages": [AIMessage("hi there", id="ai1")]}
subgraph = (
StateGraph(MessagesState)
.add_node("respond", respond)
.add_edge(START, "respond")
.compile()
)
msgs = {"messages": [("user", "hello")]}
top_level = [msg async for msg, _ in subgraph.astream(msgs, stream_mode="messages")]
async def parent_node(state: MessagesState, config: RunnableConfig) -> dict:
nonlocal in_node
in_node = [
msg
async for msg, _ in subgraph.astream(msgs, config, stream_mode="messages")
]
return {}
in_node: list = []
await (
StateGraph(MessagesState)
.add_node("parent", parent_node)
.add_edge(START, "parent")
.compile()
.ainvoke(msgs)
)
assert [m.content for m in top_level] == ["hi there"]
assert [m.content for m in in_node] == ["hi there"]
@NEEDS_CONTEXTVARS
async def test_null_resume_disallowed_with_multiple_interrupts(
async_checkpointer: BaseCheckpointSaver,
-83
View File
@@ -16,11 +16,9 @@ from langgraph_sdk.schema import StreamPart
from pydantic import BaseModel
from typing_extensions import TypedDict
from langgraph._internal._constants import CONFIG_KEY_STREAM
from langgraph.errors import GraphInterrupt
from langgraph.graph import END, START, MessagesState, StateGraph, add_messages
from langgraph.pregel import Pregel
from langgraph.pregel.protocol import StreamProtocol
from langgraph.pregel.remote import RemoteGraph
from langgraph.types import Interrupt, StateSnapshot
from tests.any_str import AnyStr
@@ -865,87 +863,6 @@ async def test_astream_subgraphs_keeps_caller_namespace():
assert stream_parts == _CALLER_PREFIXED
# Subgraph events requested only on behalf of a streaming parent graph
_PARENT_STREAMED_PARTS = [
StreamPart(event="updates|sub:abc", data={"inner": {"chunk": "inner"}}),
StreamPart(event="messages|sub:abc", data=[{"content": "token"}, {}]),
StreamPart(event="updates", data={"sub": {"chunk": "root"}}),
]
def _parent_streamed_config(parent_parts: list) -> RunnableConfig:
return {
"configurable": {
"thread_id": "thread_1",
"checkpoint_ns": "outer:123",
CONFIG_KEY_STREAM: StreamProtocol(
parent_parts.append, {"updates", "messages"}
),
}
}
def test_stream_without_subgraphs_skips_parent_requested_subgraph_events():
mock_sync_client = MagicMock()
mock_sync_client.runs.stream.return_value = _PARENT_STREAMED_PARTS
remote_pregel = RemoteGraph("test_graph_id", sync_client=mock_sync_client)
for stream_mode, expected in [
("updates", [{"sub": {"chunk": "root"}}]),
# messages are not filtered: the server dedupes them across namespaces
("messages", [({"content": "token"}, {})]),
]:
parent_parts: list = []
stream_parts = list(
remote_pregel.stream(
{"input": "data"},
_parent_streamed_config(parent_parts),
stream_mode=stream_mode,
)
)
assert stream_parts == expected
# subgraph events are still requested and forwarded to the parent
assert mock_sync_client.runs.stream.call_args.kwargs["stream_subgraphs"]
assert [ns for ns, _, _ in parent_parts] == [
("outer:123", "sub:abc"),
("outer:123", "sub:abc"),
("outer:123",),
]
async def test_astream_without_subgraphs_skips_parent_requested_subgraph_events():
mock_async_client = MagicMock()
async_iter = MagicMock()
async_iter.__aiter__.return_value = _PARENT_STREAMED_PARTS
mock_async_client.runs.stream.return_value = async_iter
remote_pregel = RemoteGraph("test_graph_id", client=mock_async_client)
for stream_mode, expected in [
("updates", [{"sub": {"chunk": "root"}}]),
# messages are not filtered: the server dedupes them across namespaces
("messages", [({"content": "token"}, {})]),
]:
parent_parts: list = []
stream_parts = [
part
async for part in remote_pregel.astream(
{"input": "data"},
_parent_streamed_config(parent_parts),
stream_mode=stream_mode,
)
]
assert stream_parts == expected
# subgraph events are still requested and forwarded to the parent
assert mock_async_client.runs.stream.call_args.kwargs["stream_subgraphs"]
assert [ns for ns, _, _ in parent_parts] == [
("outer:123", "sub:abc"),
("outer:123", "sub:abc"),
("outer:123",),
]
def test_invoke():
# set up test
mock_sync_client = MagicMock()
@@ -1,308 +0,0 @@
import sys
from typing import Any
import pytest
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langgraph.func import entrypoint, task
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.graph.message import push_message
from langgraph.graph.state import CompiledStateGraph
pytestmark = pytest.mark.anyio
NEEDS_CONTEXTVARS = pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.11+ is required for async contextvars support",
)
@tool
def echo(text: str) -> str:
"""Echo the text."""
return text
def _one_node_graph(node: Any) -> CompiledStateGraph:
return (
StateGraph(MessagesState)
.add_node(node)
.add_edge(START, node.__name__)
.compile()
)
def _chat_and_echo(state: MessagesState) -> dict:
echo.invoke({"text": "done"})
return {"messages": [GenericFakeChatModel(messages=iter(["one two"])).invoke("hi")]}
async def _achat_and_echo(state: MessagesState) -> dict:
await echo.ainvoke({"text": "done"})
model = GenericFakeChatModel(messages=iter(["one two"]))
return {"messages": [await model.ainvoke("hi")]}
def _segment_names(ns: tuple[str, ...]) -> tuple[str, ...]:
return tuple(segment.split(":")[0] for segment in ns)
def _summary(part: dict) -> tuple[tuple[str, ...], str]:
if part["type"] == "messages":
return _segment_names(part["ns"]), part["data"][0].content
return _segment_names(part["ns"]), part["data"]["event"]
_CHAT_AND_ECHO_EVENTS = [
((), "tool-started"),
((), "tool-finished"),
((), "one"),
((), " "),
((), "two"),
]
_CHILD_CONFIGS = pytest.mark.parametrize(
"child_config",
[
None,
{"configurable": {"thread_id": "child"}},
{"configurable": {"thread_id": "parent"}},
],
ids=["no_config", "own_thread", "caller_thread"],
)
_TASK_CALLERS = pytest.mark.parametrize(
"nested", [False, True], ids=["from_entrypoint", "from_task_in_task"]
)
@_TASK_CALLERS
@pytest.mark.parametrize("subgraphs", [False, True])
def test_task_events_stream_under_the_graph_namespace(
nested: bool, subgraphs: bool
) -> None:
@task
def chat_and_echo() -> None:
_chat_and_echo({"messages": []})
@task
def outer() -> None:
chat_and_echo().result()
@entrypoint()
def workflow(inputs: dict) -> None:
(outer() if nested else chat_and_echo()).result()
parts = workflow.stream(
{}, stream_mode=["messages", "tools"], subgraphs=subgraphs, version="v2"
)
assert [_summary(part) for part in parts] == _CHAT_AND_ECHO_EVENTS
@NEEDS_CONTEXTVARS
@_TASK_CALLERS
@pytest.mark.parametrize("subgraphs", [False, True])
async def test_task_events_stream_under_the_graph_namespace_async(
nested: bool, subgraphs: bool
) -> None:
@task
async def chat_and_echo() -> None:
await _achat_and_echo({"messages": []})
@task
async def outer() -> None:
await chat_and_echo()
@entrypoint()
async def workflow(inputs: dict) -> None:
await (outer() if nested else chat_and_echo())
parts = workflow.astream(
{}, stream_mode=["messages", "tools"], subgraphs=subgraphs, version="v2"
)
assert [_summary(part) async for part in parts] == _CHAT_AND_ECHO_EVENTS
def test_task_events_stream_under_the_namespace_of_their_subgraph() -> None:
@task
def chat_and_echo() -> None:
_chat_and_echo({"messages": []})
def node(state: MessagesState) -> dict:
chat_and_echo().result()
return {}
sub = _one_node_graph(node)
parent = StateGraph(MessagesState).add_node("sub", sub).add_edge(START, "sub")
parts = parent.compile().stream(
{"messages": []},
stream_mode=["messages", "tools"],
subgraphs=True,
version="v2",
)
assert [_summary(part) for part in parts] == [
(("sub",), event) for _, event in _CHAT_AND_ECHO_EVENTS
]
@_CHILD_CONFIGS
def test_child_graph_streams_only_to_the_node_streaming_it(
child_config: dict | None,
) -> None:
child = _one_node_graph(_chat_and_echo)
received = []
def node(state: MessagesState) -> dict:
received.extend(
child.stream(
{"messages": []}, child_config, stream_mode=["messages", "tools"]
)
)
return {}
parts = _one_node_graph(node).stream(
{"messages": []},
{"configurable": {"thread_id": "parent"}},
stream_mode=["messages", "tools"],
)
assert list(parts) == []
assert len(received) == len(_CHAT_AND_ECHO_EVENTS)
@NEEDS_CONTEXTVARS
@_CHILD_CONFIGS
async def test_child_graph_streams_only_to_the_node_streaming_it_async(
child_config: dict | None,
) -> None:
child = _one_node_graph(_achat_and_echo)
received = []
async def node(state: MessagesState) -> dict:
stream = child.astream(
{"messages": []}, child_config, stream_mode=["messages", "tools"]
)
received.extend([part async for part in stream])
return {}
parts = _one_node_graph(node).astream(
{"messages": []},
{"configurable": {"thread_id": "parent"}},
stream_mode=["messages", "tools"],
)
assert [part async for part in parts] == []
assert len(received) == len(_CHAT_AND_ECHO_EVENTS)
@pytest.mark.parametrize(
"child_config",
[
{"configurable": {"thread_id": "child"}},
{"configurable": {"thread_id": "parent"}},
],
ids=["own_thread", "caller_thread"],
)
def test_child_graph_on_its_own_thread_skips_a_caller_streaming_subgraphs(
child_config: dict,
) -> None:
child = _one_node_graph(_chat_and_echo)
def node(state: MessagesState) -> dict:
child.invoke({"messages": []}, child_config)
return {}
parts = _one_node_graph(node).stream(
{"messages": []},
{"configurable": {"thread_id": "parent"}},
stream_mode=["messages", "tools"],
subgraphs=True,
)
assert list(parts) == []
def test_node_gets_the_events_of_every_subgraph_it_streams() -> None:
sub = _one_node_graph(_chat_and_echo)
counts = []
def node(state: MessagesState) -> dict:
for _ in range(2):
stream = sub.stream({"messages": []}, stream_mode=["messages", "tools"])
counts.append(len(list(stream)))
return {}
_one_node_graph(node).invoke({"messages": []})
assert counts == [len(_CHAT_AND_ECHO_EVENTS)] * 2
@NEEDS_CONTEXTVARS
async def test_node_gets_the_events_of_every_subgraph_it_streams_async() -> None:
sub = _one_node_graph(_achat_and_echo)
counts = []
async def node(state: MessagesState) -> dict:
for _ in range(2):
stream = sub.astream({"messages": []}, stream_mode=["messages", "tools"])
counts.append(len([part async for part in stream]))
return {}
await _one_node_graph(node).ainvoke({"messages": []})
assert counts == [len(_CHAT_AND_ECHO_EVENTS)] * 2
def _push(state: MessagesState) -> dict:
push_message(AIMessage(id="pushed", content="hi"), state_key=None)
return {}
def _namespaces(graph: CompiledStateGraph, subgraphs: bool) -> list[tuple[str, ...]]:
parts = graph.stream(
{"messages": []}, stream_mode="messages", subgraphs=subgraphs, version="v2"
)
return [_segment_names(part["ns"]) for part in parts]
def _streams_pushing_graph(received: list) -> Any:
pushing = _one_node_graph(_push)
def node(state: MessagesState) -> dict:
for message, _ in pushing.stream({"messages": []}, stream_mode="messages"):
received.append(message.id)
return {}
return node
def test_pushed_message_streams_under_the_namespace_of_its_graph() -> None:
pushing = _one_node_graph(_push)
parent = StateGraph(MessagesState).add_node("sub", pushing).add_edge(START, "sub")
assert _namespaces(pushing, subgraphs=True) == [()]
assert _namespaces(parent.compile(), subgraphs=False) == []
assert _namespaces(parent.compile(), subgraphs=True) == [("sub",)]
def test_pushed_message_streams_to_the_node_that_streams_its_graph() -> None:
received: list[str] = []
graph = _one_node_graph(_streams_pushing_graph(received))
assert _namespaces(graph, subgraphs=False) == []
assert received == ["pushed"]
def test_pushed_message_streams_to_every_stream_that_takes_it() -> None:
received: list[str] = []
graph = _one_node_graph(_streams_pushing_graph(received))
assert _namespaces(graph, subgraphs=True) == [("node",)]
assert received == ["pushed"]
@@ -13,17 +13,13 @@ from typing import Annotated, Any
import pytest
from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import ToolNode, ToolRuntime
from typing_extensions import TypedDict
from langgraph.constants import END, START
from langgraph.errors import GraphDrained
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.pregel._tools import _tool_call_writer
from langgraph.runtime import RunControl
from langgraph.types import Command, interrupt
class _State(TypedDict):
@@ -58,14 +54,14 @@ def _caller_async(tool_name: str, tool_args: dict[str, Any], tc_id: str = "tc1")
return caller
def _build_graph(caller, tools, checkpointer=None) -> Any:
def _build_graph(caller, tools) -> Any:
sg = StateGraph(_State)
sg.add_node("caller", caller)
sg.add_node("tools", ToolNode(tools))
sg.add_edge(START, "caller")
sg.add_edge("caller", "tools")
sg.add_edge("tools", END)
return sg.compile(checkpointer=checkpointer)
return sg.compile()
def _tool_events(stream) -> list[tuple[tuple[str, ...], dict]]:
@@ -150,105 +146,6 @@ class TestSyncGraphSyncTool:
assert kinds == ["tool-started", "tool-error"]
assert events[1][1]["message"] == "nope"
def test_interrupt_in_tool_is_not_a_tool_error(self) -> None:
@tool
def ask() -> str:
"""asks."""
return interrupt("ok?")
graph = _build_graph(_caller_sync("ask", {}), [ask], InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
paused = _tool_events(
graph.stream(
{"messages": []}, config, stream_mode=["tools"], subgraphs=True
)
)
resumed = _tool_events(
graph.stream(
Command(resume="yes"), config, stream_mode=["tools"], subgraphs=True
)
)
assert [p["event"] for _, p in paused] == ["tool-started"]
assert [(p["event"], p["tool_call_id"]) for _, p in resumed] == [
("tool-started", "tc1"),
("tool-finished", "tc1"),
]
def test_tool_handing_off_to_the_parent_still_ends_its_call(self) -> None:
class State(TypedDict):
messages: Annotated[list, add_messages]
done: bool
def hand_off(state: State) -> Command:
return Command(graph=Command.PARENT, goto="finish", update={"done": True})
inner = StateGraph(State).add_node("hand_off", hand_off)
inner = inner.add_edge(START, "hand_off").compile()
@tool
def handoff() -> str:
"""hands off."""
inner.invoke({"messages": [], "done": False})
return "unreachable"
graph = (
StateGraph(State)
.add_node("caller", _caller_sync("handoff", {}))
.add_node("tools", ToolNode([handoff]))
.add_node("finish", lambda state: {"done": True})
.add_edge(START, "caller")
.add_edge("caller", "tools")
.add_edge("tools", END)
.add_edge("finish", END)
.compile()
)
events = [
payload["event"]
for _, payload in _tool_events(
graph.stream(
{"messages": [], "done": False},
stream_mode=["tools"],
subgraphs=True,
)
)
]
assert events[0] == "tool-started"
assert events[-1] == "tool-error", events
def test_drained_tool_is_not_a_tool_error(self) -> None:
control = RunControl()
def request_drain(state: _State) -> dict:
control.request_drain()
return {}
inner = (
StateGraph(_State)
.add_node("request_drain", request_drain)
.add_node("after", lambda state: {})
.add_edge(START, "request_drain")
.add_edge("request_drain", "after")
.compile()
)
@tool
def drained() -> str:
"""drains."""
inner.invoke({"messages": []}, control=control)
return "unreachable"
graph = _build_graph(_caller_sync("drained", {}), [drained])
chunks: list = []
with pytest.raises(GraphDrained):
for chunk in graph.stream(
{"messages": []}, stream_mode=["tools"], subgraphs=True
):
chunks.append(chunk)
assert [p["event"] for _, p in _tool_events(chunks)] == ["tool-started"]
def test_writer_unset_outside_tool(self) -> None:
# Outside any tool body the ContextVar that ToolRuntime reads
# is unset — emitting from there would be a no-op.
@@ -299,25 +196,6 @@ class TestAsyncGraphAsyncTool:
assert kinds == ["tool-started", "tool-output-delta", "tool-finished"]
assert events[1][1]["delta"] == "hi"
@pytest.mark.anyio
async def test_interrupt_in_async_tool_is_not_a_tool_error(self) -> None:
@tool
async def aask() -> str:
"""asks."""
return interrupt("ok?")
graph = _build_graph(_caller_async("aask", {}), [aask], InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
events = []
for graph_input in ({"messages": []}, Command(resume="yes")):
async for ns, mode, payload in graph.astream(
graph_input, config, stream_mode=["tools"], subgraphs=True
):
if mode == "tools":
events.append(payload["event"])
assert events == ["tool-started", "tool-started", "tool-finished"]
class TestConcurrentToolCalls:
def test_parallel_tool_calls_do_not_bleed(self) -> None:
+1 -190
View File
@@ -1,24 +1,11 @@
"""End-to-end tests for `TracePolicy` input processing on node trace runs."""
import asyncio
from collections.abc import AsyncIterator, Iterator
from typing import Any
from uuid import UUID
import pytest
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.runnables import RunnableConfig
from typing_extensions import TypedDict
from langgraph._internal._runnable import (
ASYNCIO_ACCEPTS_CONTEXT,
RunnableCallable,
RunnableSeq,
)
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.graph import END, START, StateGraph
from langgraph.types import TracePolicy
from tests.fake_tracer import FakeTracer, Run
@@ -160,179 +147,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}
_RECORDING_POLICY = TracePolicy(
process_inputs=lambda _: {"recorded": "inputs"},
process_outputs=lambda _: {"recorded": "outputs"},
)
def _chat_graph() -> CompiledStateGraph:
model = FakeMessagesListChatModel(
responses=[AIMessage("model reply", id="ai-model")]
)
def chat(state: MessagesState) -> dict[str, Any]:
reply = model.invoke(state["messages"])
return {
"messages": [
*state["messages"],
reply,
AIMessage("from node", id="ai-node"),
]
}
return (
StateGraph(MessagesState)
.add_node("chat", chat, trace_policy=_RECORDING_POLICY)
.add_edge(START, "chat")
.compile()
)
def _assert_scrubbed_node_run(tracer: FakeTracer, *, model_is_child: bool) -> None:
run = _node_run(tracer, "chat")
assert run.inputs == {"recorded": "inputs"}
assert run.outputs == {"recorded": "outputs"}
if model_is_child:
assert [c.name for c in run.child_runs] == ["FakeMessagesListChatModel"]
def test_trace_policy_scrubs_tracer_not_messages_stream() -> None:
tracer = FakeTracer()
streamed = [
message.content
for message, _ in _chat_graph().stream(
{"messages": [HumanMessage("hello", id="h-1")]},
{"callbacks": [tracer]},
stream_mode="messages",
)
]
assert streamed == ["model reply", "from node"]
_assert_scrubbed_node_run(tracer, model_is_child=True)
@pytest.mark.anyio
async def test_trace_policy_scrubs_tracer_not_messages_stream_async() -> None:
tracer = FakeTracer()
streamed = [
message.content
async for message, _ in _chat_graph().astream(
{"messages": [HumanMessage("hello", id="h-1")]},
{"callbacks": [tracer]},
stream_mode="messages",
)
]
assert streamed == ["model reply", "from node"]
_assert_scrubbed_node_run(tracer, model_is_child=ASYNCIO_ACCEPTS_CONTEXT)
@pytest.mark.anyio
async def test_trace_policy_scrubs_tracer_not_astream_events() -> None:
tracer = FakeTracer()
chat_events = [
event
async for event in _chat_graph().astream_events(
{"messages": [HumanMessage("hello", id="h-1")]},
{"callbacks": [tracer]},
version="v2",
)
if event["name"] == "chat"
and event["event"] in ("on_chain_start", "on_chain_end")
]
assert [e["event"] for e in chat_events] == ["on_chain_start", "on_chain_end"]
start, end = (e["data"] for e in chat_events)
assert [m.content for m in start["input"]["messages"]] == ["hello"]
assert [m.content for m in end["output"]["messages"]] == [
"hello",
"model reply",
"from node",
]
_assert_scrubbed_node_run(tracer, model_is_child=ASYNCIO_ACCEPTS_CONTEXT)
class _HandlerWithTapMethods(BaseCallbackHandler):
def __init__(self) -> None:
self.node_payloads: list[Any] = []
self.node_run_ids: set[UUID] = set()
def _record(self, run_id: UUID, payload: Any) -> None:
if run_id in self.node_run_ids:
self.node_payloads.append(payload)
def tap_output_iter(self, run_id: UUID, output: Iterator[Any]) -> Iterator[Any]:
for chunk in output:
self._record(run_id, chunk)
yield chunk
async def tap_output_aiter(
self, run_id: UUID, output: AsyncIterator[Any]
) -> AsyncIterator[Any]:
async for chunk in output:
self._record(run_id, chunk)
yield chunk
def on_chain_start(
self, serialized: Any, inputs: Any, *, run_id: UUID, **kwargs: Any
) -> None:
if kwargs.get("name") == "n":
self.node_run_ids.add(run_id)
self._record(run_id, inputs)
def on_chain_end(self, outputs: Any, *, run_id: UUID, **kwargs: Any) -> None:
self._record(run_id, outputs)
@pytest.mark.anyio
@pytest.mark.parametrize("via_events", [False, True], ids=["ainvoke", "astream_events"])
async def test_trace_policy_reaches_tracers_but_not_custom_handlers(
via_events: bool,
) -> None:
graph = (
StateGraph(State)
.add_node("n", _incr, trace_policy=_RECORDING_POLICY)
.add_edge(START, "n")
.add_edge("n", END)
.compile()
)
handler = _HandlerWithTapMethods()
tracer = FakeTracer()
config: RunnableConfig = {"callbacks": [handler, tracer]}
if via_events:
async for _ in graph.astream_events({"value": 1}, config, version="v2"):
pass
else:
assert await graph.ainvoke({"value": 1}, config) == {"value": 2}
assert handler.node_payloads == [{"value": 1}, {"value": 2}]
run = _node_run(tracer, "n")
assert (run.inputs, run.outputs) == (
{"recorded": "inputs"},
{"recorded": "outputs"},
)
@pytest.mark.anyio
async def test_trace_policy_cancel_at_node_end_still_closes_tracer_run() -> None:
def cancel_own_task(value: Any) -> Any:
asyncio.current_task().cancel()
return value
tracer = FakeTracer()
seq = RunnableSeq(
RunnableCallable(lambda x: x, trace=False),
RunnableCallable(cancel_own_task, trace=False),
trace_policy=_RECORDING_POLICY,
)
with pytest.raises(asyncio.CancelledError):
await asyncio.create_task(seq.ainvoke({}, {"callbacks": [tracer]}))
for _ in range(100):
if tracer.runs:
break
await asyncio.sleep(0.01)
assert [r.outputs for r in tracer.runs] == [{"recorded": "outputs"}]
+38 -37
View File
@@ -1439,7 +1439,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.19"
version = "1.2.17"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -1625,7 +1625,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -1722,7 +1722,7 @@ test = [
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "3.1.3"
version = "3.1.2"
source = { editable = "../checkpoint-sqlite" }
dependencies = [
{ name = "aiosqlite" },
@@ -2699,36 +2699,37 @@ wheels = [
[[package]]
name = "pycryptodome"
version = "4.2.0"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/33/c7/f9374c8939560ab22db1dd9dbe215b62d20f32634aeb7f259dc03c807b44/pycryptodome-4.2.0.tar.gz", hash = "sha256:21d7fdba239d6ef863d3d08388801c683453a3bb4897c2b27f27847aad89bdf7", size = 1750332, upload-time = "2026-10-24T10:24:42.016Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/05/61028b65913438a091ed3a2ec55bb50843e674b6ae14bccf196bbed652a2/pycryptodome-4.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:4b58c4fd5615a788e0df6793d578e565b19d75a348c0ba02d469b0c38bbf0332", size = 1684620, upload-time = "2026-10-24T10:23:50.473Z" },
{ url = "https://files.pythonhosted.org/packages/cd/73/0930fd56dd5eb856ecc33ec48845def52a1d25a9cd6b810a4c3ef3633cf4/pycryptodome-4.2.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:42ab554efcc14928f1db42deac9c84989fd49e289a16aacbbee709fe2eb838cc", size = 1520423, upload-time = "2026-10-24T10:23:52.505Z" },
{ url = "https://files.pythonhosted.org/packages/19/ba/4b931de42881053a6d818b9ee70f6dde3dfe1cfcf78527d45db3f99d8b47/pycryptodome-4.2.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b4ed1a34d2d667ec165beb15313a759bcd8e704e2743f9942491c5a88a2d6ca5", size = 2296340, upload-time = "2026-10-24T10:23:54.294Z" },
{ url = "https://files.pythonhosted.org/packages/d4/63/68fc2a01d621c2ff389ea261e731e8f1abe6aba89992681bbcdb19fcca43/pycryptodome-4.2.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1482a7d9ceed7beadbb0853f83d4d44406a9f9fa8ee596d637618f095869e14", size = 2987589, upload-time = "2026-10-24T10:23:56.547Z" },
{ url = "https://files.pythonhosted.org/packages/f5/63/69e211459f4625c89bb47a7ba4527a6cc3276c9bbd79ae871a2d0b05c021/pycryptodome-4.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:dceadb6f478b0a687d129f1b58220e0587fb17a31b2502ae7a1a4234bc304ee1", size = 2322482, upload-time = "2026-10-24T10:23:58.538Z" },
{ url = "https://files.pythonhosted.org/packages/4a/65/21a4ed3ae5af153c57861d7f987c81fca5bf02d49b76f34c545f8eb6b95f/pycryptodome-4.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8217736d5ac9b7fef49d1d2b292d5af4e8300298ef4926742aaec49b2bf6505b", size = 3548352, upload-time = "2026-10-24T10:24:01.185Z" },
{ url = "https://files.pythonhosted.org/packages/4a/a4/9a7e4e8896cfd8131978d31e39c930fb114f6a63751f39d2069b965dad45/pycryptodome-4.2.0-cp315-cp315t-win32.whl", hash = "sha256:b717e36cf14cffe463ff4e7d423a0c9fd6f83b40d207388e0f40dd0a3d4018bc", size = 1306386, upload-time = "2026-10-24T10:24:03.047Z" },
{ url = "https://files.pythonhosted.org/packages/af/6c/e1a78019b7db83e2cfc1c98cea4a7a0afbb6b233514b9dfee2bad7443bd9/pycryptodome-4.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:585f0a792cdc7fb381ca0d1fac4e20f03fd99bb78cf2733eaab59f62c77ccf88", size = 1688319, upload-time = "2026-10-24T10:24:04.963Z" },
{ url = "https://files.pythonhosted.org/packages/c6/96/7cb051ba325141b33bc7e6ef6152499cf74b74606c5bb4c8c72411e8afdc/pycryptodome-4.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:831ae8ce070e990d4fcb913c49c7fb0181ea7ab2691fc58dcb9d41fdb5384d85", size = 1330914, upload-time = "2026-10-24T10:24:06.934Z" },
{ url = "https://files.pythonhosted.org/packages/41/4c/954a9a1451f7bcc89d66db39bf6499ef5701e8ff35b6c898c659a7ec8546/pycryptodome-4.2.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e430168f767ecec60c1f850df4abbd0b9f94786b9cc8041a00e976f6d5190c6", size = 1680663, upload-time = "2026-10-24T10:24:08.982Z" },
{ url = "https://files.pythonhosted.org/packages/6e/84/23f8013dce07cbc8c8b9ed878972294230a1edc49a6b64d271c77c878c29/pycryptodome-4.2.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aea22120791b4534a27e39fc01d91265a249ba72dbd682a8a5c1adee475d5483", size = 1513323, upload-time = "2026-10-24T10:24:11.076Z" },
{ url = "https://files.pythonhosted.org/packages/1a/82/693940a8e4a20c3db91050480429daf39c818be3c7ec59f992ded4567ce6/pycryptodome-4.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6f30a457bd83a3074747d11bcb95375397a2e9b9a6b9179ab001e0cdd8ed0c5", size = 2292885, upload-time = "2026-10-24T10:24:13.486Z" },
{ url = "https://files.pythonhosted.org/packages/18/88/33df6b1176b63aed427745696d88ed38f7877104cf0f6004ee9ad600f897/pycryptodome-4.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76777686de3a354c41710727988c2284448d5db9bd82e5ddb9a9b6710d17d9df", size = 2976488, upload-time = "2026-10-24T10:24:16.139Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c6/c1c137d6e3284409ca5b2cfd7db3621167c316f9c314a96274af69a66fbb/pycryptodome-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c5bc41a418451616cd0ec3d765ba8539416d7429d7cd633ffb603a5a22355e70", size = 2313324, upload-time = "2026-10-24T10:24:18.302Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/b7752cffc8200040835ad24dc17c9de6376ac27612c8bf943766cb1b3c36/pycryptodome-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2c5b1e9058687daca3aaaff23eeb1c1853c8251986f0de7a950235e8bc0cc8b7", size = 3537773, upload-time = "2026-10-24T10:24:20.482Z" },
{ url = "https://files.pythonhosted.org/packages/df/1b/34bec1416507cda23b5dfa33c0bca936fd4ac8ed4d3269803477b897649d/pycryptodome-4.2.0-cp39-abi3-win32.whl", hash = "sha256:bb7659f87d7aa69105e1be12764f9cf1908287eb424b242186f5ac3c192ed612", size = 1292974, upload-time = "2026-10-24T10:24:22.422Z" },
{ url = "https://files.pythonhosted.org/packages/03/5b/a4a31e6e1aabd5a26da77c7960e7fcce4fca9132e51868f1bba10bf1e932/pycryptodome-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:3282c9fd744a16594e51e1a9fbfe4d658b6b0aaa1f050926e6d591ac7494f78b", size = 1658684, upload-time = "2026-10-24T10:24:24.009Z" },
{ url = "https://files.pythonhosted.org/packages/2b/81/bddb0637fa85a03d00eea2f3514c5aa8d7b9c4921ef5e0c125b4ce662a8c/pycryptodome-4.2.0-cp39-abi3-win_arm64.whl", hash = "sha256:34ec11258967efec61d92a83a63a050ab7a7c31dc3806412f6334b8aef47e2d9", size = 1305591, upload-time = "2026-10-24T10:24:25.993Z" },
{ url = "https://files.pythonhosted.org/packages/df/a1/5460bbb750121f71abacaaf25f0cb1df67be7fe13c70cf6d7c2b8ad54fc0/pycryptodome-4.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ea7dbf6b8ce0f0d2c0e17c1226c74d07c01499db7704797bdbb6cb9ec511ccb", size = 1472158, upload-time = "2026-10-24T10:24:27.92Z" },
{ url = "https://files.pythonhosted.org/packages/e2/65/27ac8fb15f0ffb70b72a0413291637de46d29b075804221703273c3ca6d1/pycryptodome-4.2.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0106af8684e1d73c93bc39a2f1cb4ae2f2636d1f80e6291caddcc37b4efc5dc1", size = 1247107, upload-time = "2026-10-24T10:24:29.672Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7d/6b3ed6cd8626007f6a6b13616b08150bc0bb936a0b84c310e59558ed5136/pycryptodome-4.2.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4deb46e0d0a540b1f047cb6f430249692da15c21edd75f5e6278314089fb859", size = 1426092, upload-time = "2026-10-24T10:24:31.627Z" },
{ url = "https://files.pythonhosted.org/packages/73/45/5adf4dbb1eab1da6291d25f777d1d34f745c7f2993bf2d4d3b0d9abb87c6/pycryptodome-4.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e094873fe87ae4d546ef393cec7ea1e1cf8e0684f649612b19352afa67eb8bdb", size = 1662257, upload-time = "2026-10-24T10:24:33.373Z" },
{ url = "https://files.pythonhosted.org/packages/57/88/fef93a9ecbda783a312b15a8823fa1a9a4ec3474685e756584af595457aa/pycryptodome-4.2.0-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:30f418cd1f4e776bde6c5df1fcd90ddfe74dfc8f679286a82f17ec1d4018cce0", size = 1467319, upload-time = "2026-10-24T10:24:35.108Z" },
{ url = "https://files.pythonhosted.org/packages/78/65/cb53934c9f8d5c41dfcfaddb0d98103f4dd6670af1ffdbc2582bcdd99382/pycryptodome-4.2.0-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20cb4a489f7a5150a882901884b84ee0df77a47b9a6eb6e2dd8e98997b7b826f", size = 1247105, upload-time = "2026-10-24T10:24:36.859Z" },
{ url = "https://files.pythonhosted.org/packages/11/38/5bba7fd24beb6bc7a82fdd155f2df39f51c8ad16143fc99d3a1ccc7c76b4/pycryptodome-4.2.0-pp311-pypy311_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f104dc1a38aa1172a166ebe3716f2666f785bfa015b7ca13bb808c12c902e455", size = 1426092, upload-time = "2026-10-24T10:24:38.584Z" },
{ url = "https://files.pythonhosted.org/packages/38/cf/e70a8cb1ab184b59b2d02dfd403965a0c6419571d8520463944bcb0ca8a0/pycryptodome-4.2.0-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:a7c6857077255f5f4e6c34e6325802ba1bb59960133c0a5e584d9371dbdf3dea", size = 1662214, upload-time = "2026-10-24T10:24:40.139Z" },
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
{ url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
{ url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
{ url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
{ url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
{ url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
]
[[package]]
@@ -3418,11 +3419,11 @@ wheels = [
[[package]]
name = "setuptools"
version = "84.1.0"
version = "83.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/eb/7e5f628ee2eb168fecbf8026de7856b2543d6d609cc85f086b9a91ac93d6/setuptools-84.1.0.tar.gz", hash = "sha256:23657205b63d4d31395ab917815247f694aafad28cd21316bf5fd45a51048ea0", size = 1171783, upload-time = "2026-10-19T20:11:43.489Z" }
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/18/02/b4bbb90f8f29e334898ff45c6d9b1fffaa6f378017052b3023a06262b5f7/setuptools-84.1.0-py3-none-any.whl", hash = "sha256:5231689c6ac8156cd155cfb98c3fd6fb252c3e7e9bd3d78b64d3c55f132624c9", size = 819380, upload-time = "2026-10-19T20:11:41.684Z" },
{ 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]]
@@ -3668,14 +3669,14 @@ wheels = [
[[package]]
name = "types-requests"
version = "2.33.0.20261006"
version = "2.33.0.20260906"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/15/9b7e2e2e7c87d01366185b198b3febc6bc0c973f2bf21a62da4ab7d3495e/types_requests-2.33.0.20261006.tar.gz", hash = "sha256:0652999e9306aea345f40732d58fa49a7f6cade6a0d74d92119c5c8d82eddaf0", size = 25316, upload-time = "2026-10-06T08:15:57.782Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c0/18/4c2c0290953f8b3b9612adfcb07b57f144ade3ad32a76764fca42b77c5f3/types_requests-2.33.0.20260906.tar.gz", hash = "sha256:76ab8a0fb736744a0c3deee7aa57b2927e301f078d9e61f5391b3e92002416b9", size = 25263, upload-time = "2026-09-06T06:35:47.707Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/72/b82789207b3d360ce9f5a4372c790c52cc3dd928aeb7d4faeec652b651b2/types_requests-2.33.0.20261006-py3-none-any.whl", hash = "sha256:26cc8146505cab33cda9737991929e4144c559bebe05078ccc6998f27c4ca2c1", size = 21445, upload-time = "2026-10-06T08:15:56.658Z" },
{ url = "https://files.pythonhosted.org/packages/60/4c/51ec821d22a45b4162fa3f3e9e94ea4c0f8c49e82363b10955baa5438391/types_requests-2.33.0.20260906-py3-none-any.whl", hash = "sha256:9f53622652bd921ead7a54d665be1b8d518165c8b51cc9d68d944cd6b6bfa8fb", size = 21461, upload-time = "2026-09-06T06:35:45.856Z" },
]
[[package]]
+3 -3
View File
@@ -286,7 +286,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.19"
version = "1.2.17"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -370,7 +370,7 @@ test = [
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -467,7 +467,7 @@ test = [
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "3.1.3"
version = "3.1.2"
source = { editable = "../checkpoint-sqlite" }
dependencies = [
{ name = "aiosqlite" },
+2 -17
View File
@@ -1,21 +1,6 @@
# Changelog
## 0.4.12
### Removed
- **`PrioritySettings.fairness_weight`** — removed. The server stores only
`priority` and `fairness_group` on the thread; it ignores a
`fairness_weight` and logs a warning. Remove it from your callback.
### Changed
- **`PrioritySettings.priority`** — now says -5 runs first and 5 runs last,
matching how the server orders runs. 0.4.9 to 0.4.11 said the reverse, and
their example gave premium users 3, which runs them after everyone at the
default 0. Check any callback written from those docs.
## 0.4.11 and earlier
## Unreleased
### Added
@@ -53,7 +38,7 @@
and name the object in the `priority` section of `langgraph.json`. The
server calls the function with a `PriorityContext` at the first run on a
thread with no settings and stores the `PrioritySettings` it returns
(`priority`, `fairness_group`) on the thread. Requires
(`priority`, `fairness_group`, `fairness_weight`) on the thread. Requires
`langgraph-api` 0.18.0 or later and a `langgraph-cli` that supports the
section; older versions of either ignore it.
+1 -1
View File
@@ -5,7 +5,7 @@ from langgraph_sdk.encryption.types import DecryptResult, EncryptionContext
from langgraph_sdk.priority import Priority
from langgraph_sdk.priority.types import PriorityContext, PrioritySettings
__version__ = "0.4.12"
__version__ = "0.4.9"
__all__ = [
"Auth",
+59 -148
View File
@@ -14,16 +14,9 @@ import orjson
from langgraph_sdk._shared.utilities import (
_orjson_default,
_reconnect_delay,
_run_check_headers,
_run_url_from_stream_location,
_validate_reconnect_location,
)
from langgraph_sdk.errors import (
InternalServerError,
RateLimitError,
_araise_for_status_typed,
)
from langgraph_sdk.errors import _araise_for_status_typed
from langgraph_sdk.schema import QueryParamTypes, StreamPart
from langgraph_sdk.sse import SSEDecoder, aiter_lines_raw
@@ -153,8 +146,7 @@ class HttpClient:
on_response: Callable[[httpx.Response], None] | None = None,
reconnect_limit: int = 5,
) -> Any:
"""Send a request that reconnects to its Location header if the response is
cut off, with backoff between attempts."""
"""Send a request that automatically reconnects to Location header."""
request_headers, content = await _aencode_json(json)
if headers:
request_headers.update(headers)
@@ -184,27 +176,13 @@ class HttpClient:
stacklevel=2,
)
await r.aclose()
for attempt in range(reconnect_limit):
await asyncio.sleep(_reconnect_delay(attempt, base=1.0, cap=5.0))
# A restart or a deploy looks like a refused connection, a
# dropped body, a 429 or a 5xx; other errors won't go away.
try:
return await self.request_reconnect(
loc,
"GET",
headers=headers,
# don't pass on_response so it's only called once
reconnect_limit=0,
)
except httpx.HTTPStatusError as err:
status = err.response.status_code
if attempt == reconnect_limit - 1 or (
status != 429 and status < 500
):
raise
except httpx.TransportError:
if attempt == reconnect_limit - 1:
raise
return await self.request_reconnect(
loc,
"GET",
headers=headers,
# don't pass on_response so it's only called once
reconnect_limit=reconnect_limit - 1,
)
async def stream(
self,
@@ -234,119 +212,77 @@ class HttpClient:
reconnect_path: str | None = None
reconnect_attempts = 0
max_reconnect_attempts = 5
run_ended = False
while True:
current_headers = dict(
request_headers if reconnect_path is None else reconnect_headers
)
if last_event_id is not None:
current_headers = {
key: value
for key, value in current_headers.items()
if key.lower() != "last-event-id"
}
if last_event_id:
current_headers["Last-Event-ID"] = last_event_id
if reconnect_path is not None and not any(
key.lower() == "last-event-id" and value
for key, value in current_headers.items()
):
warnings.warn(
"Reconnecting without a Last-Event-ID, so events sent while "
"disconnected are not replayed. Only runs created with "
"stream_resumable=True send the event IDs a reconnect resumes "
"from.",
stacklevel=2,
)
current_method = method if reconnect_path is None else "GET"
current_content = content if reconnect_path is None else None
current_params = params if reconnect_path is None else None
reconnect_cause: httpx.HTTPError | None = None
try:
async with self.client.stream(
current_method,
reconnect_path or path,
headers=current_headers,
content=current_content,
params=current_params,
) as res:
if reconnect_path is None and on_response:
on_response(res)
# check status
await _araise_for_status_typed(res)
# check content type
content_type = res.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
raise httpx.TransportError(
"Expected response header Content-Type to contain 'text/event-stream', "
f"got {content_type!r}"
)
retry = False
async with self.client.stream(
current_method,
reconnect_path or path,
headers=current_headers,
content=current_content,
params=current_params,
) as res:
if reconnect_path is None and on_response:
on_response(res)
# check status
await _araise_for_status_typed(res)
# check content type
content_type = res.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
raise httpx.TransportError(
"Expected response header Content-Type to contain 'text/event-stream', "
f"got {content_type!r}"
)
reconnect_location = res.headers.get("location")
if reconnect_location:
_validate_reconnect_location(
self.client.base_url, reconnect_location
)
reconnect_path = reconnect_location
reconnect_location = res.headers.get("location")
if reconnect_location:
_validate_reconnect_location(
self.client.base_url, reconnect_location
)
reconnect_path = reconnect_location
# parse SSE
decoder = SSEDecoder(last_event_id=last_event_id)
try:
async for line in aiter_lines_raw(res):
sse = decoder.decode(line=cast("bytes", line).rstrip(b"\n"))
if sse is not None:
if decoder.last_event_id is not None:
last_event_id = decoder.last_event_id
if sse.event or decoder.has_data:
# Progress since the last drop earns a fresh
# set of reconnect attempts.
reconnect_attempts = 0
run_ended |= sse.event in ("end", "error")
yield sse
except httpx.HTTPError as err:
# httpx.TransportError inherits from HTTPError, so transient
# disconnects during streaming land here.
if reconnect_path is None:
raise
reconnect_cause = err
else:
if sse := decoder.decode(b""):
# parse SSE
decoder = SSEDecoder()
try:
async for line in aiter_lines_raw(res):
sse = decoder.decode(line=cast("bytes", line).rstrip(b"\n"))
if sse is not None:
if decoder.last_event_id is not None:
last_event_id = decoder.last_event_id
if sse.event or decoder.has_data:
# decoder.decode(b"") flushes the in-flight event and may
# return an empty placeholder when there is no pending
# message. Skip these no-op events so the stream doesn't
# emit a trailing blank item after reconnects.
run_ended |= sse.event in ("end", "error")
if sse.event or sse.data is not None:
yield sse
except (httpx.TransportError, RateLimitError, InternalServerError) as err:
# A reconnect that can't open (connection refused, 429, 5xx or a
# non-SSE body) usually means the server is restarting or
# mid-deploy, so retry it like a drop. Before the server sends a
# Location there is nothing to reconnect to, so raise.
if reconnect_path is None:
raise
reconnect_cause = err
# A run's stream closes cleanly both when the run ends and when a
# proxy timeout or a server restart cuts it. Without an `end` or
# `error` event the two look the same, so ask the run.
if reconnect_cause is None and reconnect_path is not None and not run_ended:
reconnect_cause = await _acheck_run(
self.client, reconnect_path, reconnect_headers
)
if reconnect_cause is not None:
except httpx.HTTPError:
# httpx.TransportError inherits from HTTPError, so transient
# disconnects during streaming land here.
if reconnect_path is None:
raise
retry = True
else:
if sse := decoder.decode(b""):
if decoder.last_event_id is not None:
last_event_id = decoder.last_event_id
if sse.event or sse.data is not None:
# decoder.decode(b"") flushes the in-flight event and may
# return an empty placeholder when there is no pending
# message. Skip these no-op events so the stream doesn't
# emit a trailing blank item after reconnects.
yield sse
if retry:
reconnect_attempts += 1
if reconnect_attempts > max_reconnect_attempts:
raise httpx.TransportError(
"Exceeded maximum SSE reconnection attempts"
) from reconnect_cause
await asyncio.sleep(
_reconnect_delay(reconnect_attempts - 1, base=1.0, cap=5.0)
)
)
continue
break
@@ -374,28 +310,3 @@ async def _adecode_json(r: httpx.Response) -> Any:
if body
else None
)
async def _acheck_run(
client: httpx.AsyncClient, stream_location: str, headers: Mapping[str, str]
) -> httpx.HTTPError | None:
run_url = _run_url_from_stream_location(stream_location)
if run_url is None:
return None
try:
r = await client.get(run_url, headers=_run_check_headers(headers))
if r.status_code == 429 or r.status_code >= 500:
await _araise_for_status_typed(r)
status = (await _adecode_json(r))["status"] if r.is_success else None
except (httpx.TransportError, RateLimitError, InternalServerError) as err:
# The restart that closed the stream can fail the check too, so retry
# it like a drop.
return err
except Exception:
# Anything else must not turn a normal end of stream into an error.
return None
if status in ("pending", "running"):
return httpx.TransportError(
"Stream closed while the run was still pending or running"
)
return None
+7 -8
View File
@@ -16,6 +16,7 @@ from __future__ import annotations
import asyncio
import contextlib
import logging
import random
from collections.abc import AsyncGenerator, AsyncIterator, Generator, Mapping
from dataclasses import dataclass, field
from typing import Any, Literal, TypedDict, cast
@@ -24,7 +25,7 @@ from langchain_core.language_models.chat_model_stream import AsyncChatModelStrea
from langchain_protocol import Event, SubscribeParams
from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._shared.utilities import _quote_path_param, _reconnect_delay
from langgraph_sdk._shared.utilities import _quote_path_param
from langgraph_sdk.schema import LangSmithTracing, QueryParamTypes
from langgraph_sdk.stream.controller import _SeenEventIds
from langgraph_sdk.stream.decoders import (
@@ -1690,13 +1691,11 @@ class AsyncThreadStream:
async def _reconnect_sleep(self, attempt: int) -> None:
"""Sleep with exponential backoff and jitter for reconnect attempt `attempt`."""
await asyncio.sleep(
_reconnect_delay(
attempt,
base=self._shared_reconnect_backoff_base,
cap=self._shared_reconnect_backoff_cap,
)
)
base = self._shared_reconnect_backoff_base
cap = self._shared_reconnect_backoff_cap
delay = min(cap, base * (2**attempt))
jitter = random.uniform(0, delay * 0.25)
await asyncio.sleep(delay + jitter)
async def _reconnect_shared_stream(self) -> bool:
"""Attempt to reopen the shared stream after a post-ready transport drop.
+3 -30
View File
@@ -21,7 +21,6 @@ from langgraph_sdk.schema import (
ThreadSelectField,
ThreadSortBy,
ThreadState,
ThreadStateSelectField,
ThreadStatus,
ThreadStreamMode,
ThreadUpdateStateResponse,
@@ -490,7 +489,6 @@ class ThreadsClient:
checkpoint_id: str | None = None, # deprecated
*,
subgraphs: bool = False,
select: list[ThreadStateSelectField] | None = None,
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
) -> ThreadState:
@@ -501,9 +499,6 @@ class ThreadsClient:
checkpoint: The checkpoint to get the state of.
checkpoint_id: (deprecated) The checkpoint ID to get the state of.
subgraphs: Include subgraphs states.
select: Fields to include in the response. Use `values.<key>` to
return a single state key (e.g. `values.messages`), which lets the
server skip loading the rest of the state. Defaults to all fields.
headers: Optional custom headers to include with the request.
params: Optional query parameters to include with the request.
@@ -596,30 +591,16 @@ class ThreadsClient:
}
}
```
Fetch only the `messages` state key:
```python
thread_state = await client.threads.get_state(
thread_id="my_thread_id",
select=["values.messages"],
)
```
"""
if checkpoint:
payload: dict[str, Any] = {"checkpoint": checkpoint, "subgraphs": subgraphs}
if select:
payload["select"] = select
return await self.http.post(
f"/threads/{_quote_path_param(thread_id)}/state/checkpoint",
json=payload,
json={"checkpoint": checkpoint, "subgraphs": subgraphs},
headers=headers,
params=params,
)
elif checkpoint_id:
get_params: dict[str, Any] = {"subgraphs": subgraphs}
if select:
get_params["select"] = select
get_params = {"subgraphs": subgraphs}
if params:
get_params = {**get_params, **dict(params)}
return await self.http.get(
@@ -628,9 +609,7 @@ class ThreadsClient:
headers=headers,
)
else:
get_params: dict[str, Any] = {"subgraphs": subgraphs}
if select:
get_params["select"] = select
get_params = {"subgraphs": subgraphs}
if params:
get_params = {**get_params, **dict(params)}
return await self.http.get(
@@ -713,7 +692,6 @@ class ThreadsClient:
before: str | Checkpoint | None = None,
metadata: Mapping[str, Any] | None = None,
checkpoint: Checkpoint | None = None,
select: list[ThreadStateSelectField] | None = None,
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
) -> list[ThreadState]:
@@ -725,9 +703,6 @@ class ThreadsClient:
limit: The maximum number of states to return.
before: Return states before this checkpoint.
metadata: Filter states by metadata key-value pairs.
select: Fields to include in the response. Use `values.<key>` to
return a single state key (e.g. `values.messages`), which lets the
server skip loading the rest of the state. Defaults to all fields.
headers: Optional custom headers to include with the request.
params: Optional query parameters to include with the request.
@@ -754,8 +729,6 @@ class ThreadsClient:
payload["metadata"] = metadata
if checkpoint:
payload["checkpoint"] = checkpoint
if select:
payload["select"] = select
return await self.http.post(
f"/threads/{_quote_path_param(thread_id)}/history",
json=payload,
+1 -30
View File
@@ -4,12 +4,11 @@ from __future__ import annotations
import functools
import os
import random
import re
from collections.abc import Mapping
from datetime import tzinfo
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import quote, urlparse, urlsplit, urlunsplit
from urllib.parse import quote, urlparse
import httpx
@@ -195,34 +194,6 @@ def _validate_reconnect_location(base_url: httpx.URL, location: str) -> str:
return location
_RUN_STREAM_PATH = re.compile(r"(.*/threads/[^/]+/runs/[^/]+)/stream")
def _run_url_from_stream_location(location: str) -> str | None:
url = urlsplit(location)
if match := _RUN_STREAM_PATH.fullmatch(url.path):
return urlunsplit((url.scheme, url.netloc, match[1], "", ""))
return None
def _run_check_headers(reconnect_headers: Mapping[str, str]) -> dict[str, str]:
headers = {
key: value
for key, value in reconnect_headers.items()
if key.lower() not in ("accept", "last-event-id")
}
return {**headers, "Accept": "application/json"}
def _reconnect_delay(attempt: int, *, base: float, cap: float) -> float:
"""Seconds to wait before reconnect attempt `attempt` (0-based).
Exponential backoff capped at `cap`, plus up to 25% jitter.
"""
delay = min(cap, base * (2**attempt))
return delay + random.uniform(0, delay * 0.25)
def _provided_vals(d: Mapping[str, Any]) -> dict[str, Any]:
return {k: v for k, v in d.items() if v is not None}
+56 -144
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
import sys
import time
import warnings
from collections.abc import Callable, Iterator, Mapping
from typing import Any, cast
@@ -14,16 +13,9 @@ import orjson
from langgraph_sdk._shared.utilities import (
_orjson_default,
_reconnect_delay,
_run_check_headers,
_run_url_from_stream_location,
_validate_reconnect_location,
)
from langgraph_sdk.errors import (
InternalServerError,
RateLimitError,
_raise_for_status_typed,
)
from langgraph_sdk.errors import _raise_for_status_typed
from langgraph_sdk.schema import QueryParamTypes, StreamPart
from langgraph_sdk.sse import SSEDecoder, iter_lines_raw
@@ -154,8 +146,7 @@ class SyncHttpClient:
on_response: Callable[[httpx.Response], None] | None = None,
reconnect_limit: int = 5,
) -> Any:
"""Send a request that reconnects to its Location header if the response is
cut off, with backoff between attempts."""
"""Send a request that automatically reconnects to Location header."""
request_headers, content = _encode_json(json)
if headers:
request_headers.update(headers)
@@ -185,27 +176,13 @@ class SyncHttpClient:
stacklevel=2,
)
r.close()
for attempt in range(reconnect_limit):
time.sleep(_reconnect_delay(attempt, base=1.0, cap=5.0))
# A restart or a deploy looks like a refused connection, a
# dropped body, a 429 or a 5xx; other errors won't go away.
try:
return self.request_reconnect(
loc,
"GET",
headers=headers,
# don't pass on_response so it's only called once
reconnect_limit=0,
)
except httpx.HTTPStatusError as err:
status = err.response.status_code
if attempt == reconnect_limit - 1 or (
status != 429 and status < 500
):
raise
except httpx.TransportError:
if attempt == reconnect_limit - 1:
raise
return self.request_reconnect(
loc,
"GET",
headers=headers,
# don't pass on_response so it's only called once
reconnect_limit=reconnect_limit - 1,
)
def stream(
self,
@@ -237,114 +214,74 @@ class SyncHttpClient:
reconnect_path: str | None = None
reconnect_attempts = 0
max_reconnect_attempts = 5
run_ended = False
while True:
current_headers = dict(
request_headers if reconnect_path is None else reconnect_headers
)
if last_event_id is not None:
current_headers = {
key: value
for key, value in current_headers.items()
if key.lower() != "last-event-id"
}
if last_event_id:
current_headers["Last-Event-ID"] = last_event_id
if reconnect_path is not None and not any(
key.lower() == "last-event-id" and value
for key, value in current_headers.items()
):
warnings.warn(
"Reconnecting without a Last-Event-ID, so events sent while "
"disconnected are not replayed. Only runs created with "
"stream_resumable=True send the event IDs a reconnect resumes "
"from.",
stacklevel=2,
)
current_method = method if reconnect_path is None else "GET"
current_content = content if reconnect_path is None else None
current_params = params if reconnect_path is None else None
reconnect_cause: httpx.HTTPError | None = None
try:
with self.client.stream(
current_method,
reconnect_path or path,
headers=current_headers,
content=current_content,
params=current_params,
) as res:
if reconnect_path is None and on_response:
on_response(res)
# check status
_raise_for_status_typed(res)
# check content type
content_type = res.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
raise httpx.TransportError(
"Expected response header Content-Type to contain 'text/event-stream', "
f"got {content_type!r}"
)
retry = False
with self.client.stream(
current_method,
reconnect_path or path,
headers=current_headers,
content=current_content,
params=current_params,
) as res:
if reconnect_path is None and on_response:
on_response(res)
# check status
_raise_for_status_typed(res)
# check content type
content_type = res.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
raise httpx.TransportError(
"Expected response header Content-Type to contain 'text/event-stream', "
f"got {content_type!r}"
)
reconnect_location = res.headers.get("location")
if reconnect_location:
_validate_reconnect_location(
self.client.base_url, reconnect_location
)
reconnect_path = reconnect_location
reconnect_location = res.headers.get("location")
if reconnect_location:
_validate_reconnect_location(
self.client.base_url, reconnect_location
)
reconnect_path = reconnect_location
decoder = SSEDecoder(last_event_id=last_event_id)
try:
for line in iter_lines_raw(res):
sse = decoder.decode(cast(bytes, line).rstrip(b"\n"))
if sse is not None:
if decoder.last_event_id is not None:
last_event_id = decoder.last_event_id
if sse.event or decoder.has_data:
# Progress since the last drop earns a fresh
# set of reconnect attempts.
reconnect_attempts = 0
run_ended |= sse.event in ("end", "error")
yield sse
except httpx.HTTPError as err:
# httpx.TransportError inherits from HTTPError, so transient
# disconnects during streaming land here.
if reconnect_path is None:
raise
reconnect_cause = err
else:
if sse := decoder.decode(b""):
decoder = SSEDecoder()
try:
for line in iter_lines_raw(res):
sse = decoder.decode(cast(bytes, line).rstrip(b"\n"))
if sse is not None:
if decoder.last_event_id is not None:
last_event_id = decoder.last_event_id
if sse.event or decoder.has_data:
# See async stream implementation for rationale on
# skipping empty flush events.
run_ended |= sse.event in ("end", "error")
if sse.event or sse.data is not None:
yield sse
except (httpx.TransportError, RateLimitError, InternalServerError) as err:
# A reconnect that can't open (connection refused, 429, 5xx or a
# non-SSE body) usually means the server is restarting or
# mid-deploy, so retry it like a drop. Before the server sends a
# Location there is nothing to reconnect to, so raise.
if reconnect_path is None:
raise
reconnect_cause = err
# A run's stream closes cleanly both when the run ends and when a
# proxy timeout or a server restart cuts it. Without an `end` or
# `error` event the two look the same, so ask the run.
if reconnect_cause is None and reconnect_path is not None and not run_ended:
reconnect_cause = _check_run(
self.client, reconnect_path, reconnect_headers
)
if reconnect_cause is not None:
except httpx.HTTPError:
# httpx.TransportError inherits from HTTPError, so transient
# disconnects during streaming land here.
if reconnect_path is None:
raise
retry = True
else:
if sse := decoder.decode(b""):
if decoder.last_event_id is not None:
last_event_id = decoder.last_event_id
if sse.event or sse.data is not None:
# See async stream implementation for rationale on
# skipping empty flush events.
yield sse
if retry:
reconnect_attempts += 1
if reconnect_attempts > max_reconnect_attempts:
raise httpx.TransportError(
"Exceeded maximum SSE reconnection attempts"
) from reconnect_cause
time.sleep(_reconnect_delay(reconnect_attempts - 1, base=1.0, cap=5.0))
)
continue
break
@@ -366,28 +303,3 @@ def _encode_json(json: Any) -> tuple[dict[str, str], bytes | None]:
def _decode_json(r: httpx.Response) -> Any:
body = r.read()
return orjson.loads(body) if body else None
def _check_run(
client: httpx.Client, stream_location: str, headers: Mapping[str, str]
) -> httpx.HTTPError | None:
run_url = _run_url_from_stream_location(stream_location)
if run_url is None:
return None
try:
r = client.get(run_url, headers=_run_check_headers(headers))
if r.status_code == 429 or r.status_code >= 500:
_raise_for_status_typed(r)
status = _decode_json(r)["status"] if r.is_success else None
except (httpx.TransportError, RateLimitError, InternalServerError) as err:
# The restart that closed the stream can fail the check too, so retry
# it like a drop.
return err
except Exception:
# Anything else must not turn a normal end of stream into an error.
return None
if status in ("pending", "running"):
return httpx.TransportError(
"Stream closed while the run was still pending or running"
)
return None
+3 -29
View File
@@ -21,7 +21,6 @@ from langgraph_sdk.schema import (
ThreadSelectField,
ThreadSortBy,
ThreadState,
ThreadStateSelectField,
ThreadStatus,
ThreadStreamMode,
ThreadUpdateStateResponse,
@@ -481,7 +480,6 @@ class SyncThreadsClient:
checkpoint_id: str | None = None, # deprecated
*,
subgraphs: bool = False,
select: list[ThreadStateSelectField] | None = None,
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
) -> ThreadState:
@@ -491,9 +489,6 @@ class SyncThreadsClient:
thread_id: The ID of the thread to get the state of.
checkpoint: The checkpoint to get the state of.
subgraphs: Include subgraphs states.
select: Fields to include in the response. Use `values.<key>` to
return a single state key (e.g. `values.messages`), which lets the
server skip loading the rest of the state. Defaults to all fields.
headers: Optional custom headers to include with the request.
Returns:
@@ -586,29 +581,16 @@ class SyncThreadsClient:
}
```
Fetch only the `messages` state key:
```python
thread_state = client.threads.get_state(
thread_id="my_thread_id",
select=["values.messages"],
)
```
"""
if checkpoint:
payload: dict[str, Any] = {"checkpoint": checkpoint, "subgraphs": subgraphs}
if select:
payload["select"] = select
return self.http.post(
f"/threads/{_quote_path_param(thread_id)}/state/checkpoint",
json=payload,
json={"checkpoint": checkpoint, "subgraphs": subgraphs},
headers=headers,
params=params,
)
elif checkpoint_id:
get_params: dict[str, Any] = {"subgraphs": subgraphs}
if select:
get_params["select"] = select
get_params = {"subgraphs": subgraphs}
if params:
get_params = {**get_params, **dict(params)}
return self.http.get(
@@ -617,9 +599,7 @@ class SyncThreadsClient:
headers=headers,
)
else:
get_params: dict[str, Any] = {"subgraphs": subgraphs}
if select:
get_params["select"] = select
get_params = {"subgraphs": subgraphs}
if params:
get_params = {**get_params, **dict(params)}
return self.http.get(
@@ -699,7 +679,6 @@ class SyncThreadsClient:
before: str | Checkpoint | None = None,
metadata: Mapping[str, Any] | None = None,
checkpoint: Checkpoint | None = None,
select: list[ThreadStateSelectField] | None = None,
headers: Mapping[str, str] | None = None,
params: QueryParamTypes | None = None,
) -> list[ThreadState]:
@@ -711,9 +690,6 @@ class SyncThreadsClient:
limit: The maximum number of states to return.
before: Return states before this checkpoint.
metadata: Filter states by metadata key-value pairs.
select: Fields to include in the response. Use `values.<key>` to
return a single state key (e.g. `values.messages`), which lets the
server skip loading the rest of the state. Defaults to all fields.
headers: Optional custom headers to include with the request.
Returns:
@@ -741,8 +717,6 @@ class SyncThreadsClient:
payload["metadata"] = metadata
if checkpoint:
payload["checkpoint"] = checkpoint
if select:
payload["select"] = select
return self.http.post(
f"/threads/{_quote_path_param(thread_id)}/history",
json=payload,
+1 -1
View File
@@ -811,7 +811,7 @@ class _On:
def __call__(
self,
*,
resources: str | Sequence[str] | None = None,
resources: str | Sequence[str],
actions: str | Sequence[str] | None = None,
) -> Callable[[AHO], AHO]: ...
@@ -102,7 +102,7 @@ class Priority:
@priority.assign
async def assign(ctx: PriorityContext) -> PrioritySettings | None:
return {
"priority": -3 if "premium" in ctx.permissions else 0,
"priority": 3 if "premium" in ctx.permissions else 0,
# Stored unencrypted, so an opaque ID rather than personal data.
"fairness_group": ctx.user["org_id"],
}
@@ -134,7 +134,7 @@ class Priority:
@priority.assign
async def assign(ctx: PriorityContext) -> PrioritySettings | None:
if ctx.cron_id is not None:
return {"priority": 2}
return {"priority": -2}
return None
```
+6 -8
View File
@@ -50,20 +50,18 @@ class PrioritySettings(typing.TypedDict, total=False):
Every key is optional: a missing key or a key set to `None` takes its
default, and returning `None` instead of a dict gives every key its default.
A `priority` outside -5 to 5, or a `fairness_group` longer than 256
characters, is an error.
A value outside its range, or an empty `fairness_group`, is an error.
"""
priority: int | None
"""An integer from -5 to 5, where -5 runs first and 5 runs last. Defaults to 0."""
"""An integer from -5 to 5, where 5 runs first and -5 runs last. Defaults to 0."""
fairness_group: str | None
"""Currently disabled.
The thread's fairness group, a string of at most 256 characters.
Defaults to `"default"`, as does an empty string. It has no effect on run
order for now.
"""The thread's fairness group, a non-empty string. Defaults to `"default"`.
The server stores it unencrypted, even when the deployment uses custom
encryption, so use an opaque ID rather than personal data.
"""
fairness_weight: int | None
"""The weight of the thread's fairness group, an integer from 1 to 100. Defaults to 1."""
-15
View File
@@ -480,21 +480,6 @@ ThreadSelectField = Literal[
"interrupts",
]
ThreadStateSelectField = (
Literal[
"values",
"next",
"tasks",
"checkpoint",
"metadata",
"created_at",
"parent_checkpoint",
"interrupts",
]
| str
)
"""Also accepts `values.<key>` for a single state key, e.g. `values.messages`."""
RunSelectField = Literal[
"run_id",
"thread_id",
+20 -33
View File
@@ -30,14 +30,18 @@ class BytesLineDecoder:
# See https://docs.python.org/3/glossary.html#term-universal-newlines
NEWLINE_CHARS = b"\n\r"
if not text:
return []
if self.trailing_cr and text.startswith(b"\n"):
text = text[1:]
self.trailing_cr = text.endswith(b"\r")
# We always push a trailing `\r` into the next decode iteration.
if self.trailing_cr:
text = b"\r" + text
self.trailing_cr = False
if text.endswith(b"\r"):
self.trailing_cr = True
text = text[:-1]
if not text:
return []
# NOTE: the edge case input of empty text doesn't occur in practice,
# because other httpx internals filter out this value
return [] # pragma: no cover
trailing_newline = text[-1] in NEWLINE_CHARS
lines = cast(list[BytesLike], text.splitlines())
@@ -62,7 +66,7 @@ class BytesLineDecoder:
return lines
def flush(self) -> list[BytesLike]:
if not self.buffer:
if not self.buffer and not self.trailing_cr:
return []
lines: list[BytesLike] = [self.buffer]
@@ -72,25 +76,18 @@ class BytesLineDecoder:
class SSEDecoder:
def __init__(self, *, last_event_id: str | None = None) -> None:
def __init__(self) -> None:
self._event = ""
self._data = bytearray()
self._last_event_id = last_event_id
self._last_event_id: str | None = None
self._retry: int | None = None
self._at_stream_start = True
self._has_data = False
self._id_changed = False
@property
def has_data(self) -> bool:
"""Whether the last decoded block contained a data field."""
return self._has_data
@property
def last_event_id(self) -> str | None:
"""Return the last event identifier that was seen.
`None` until an ID is supplied or received, and `""` after an empty
`None` until the stream sends an `id` field, and `""` after an empty
one, which resets the reconnect cursor.
"""
@@ -104,26 +101,17 @@ class SSEDecoder:
line = line.removeprefix(b"\xef\xbb\xbf")
if not line:
self._has_data = bool(self._data)
if (
not self._event
and not self._data
and not self._id_changed
and not self._last_event_id
and self._retry is None
):
return None
body = self._data[:-1]
data = None
if body and not body.isspace():
try:
data = orjson.loads(body)
except orjson.JSONDecodeError:
data = orjson.loads(body.decode(errors="replace"))
sse = StreamPart(
event=self._event,
data=data, # ty: ignore[invalid-argument-type]
data=orjson.loads(self._data) if self._data else None, # ty: ignore[invalid-argument-type]
id=self._last_event_id or None,
)
@@ -131,7 +119,6 @@ class SSEDecoder:
self._event = ""
self._data = bytearray()
self._retry = None
self._id_changed = False
return sse
@@ -144,16 +131,16 @@ class SSEDecoder:
value = value[1:]
if fieldname == b"event":
self._event = value.decode(errors="replace")
self._event = value.decode()
elif fieldname == b"data":
if self._data:
self._data.extend(b"\n")
self._data.extend(value)
self._data.extend(b"\n")
elif fieldname == b"id":
if b"\0" in value:
pass
else:
self._last_event_id = value.decode(errors="replace")
self._id_changed = True
self._last_event_id = value.decode()
elif fieldname == b"retry":
with contextlib.suppress(TypeError, ValueError):
self._retry = int(value)
@@ -15,6 +15,7 @@ from __future__ import annotations
import asyncio
import contextlib
import logging
import random
from collections import OrderedDict
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
from dataclasses import dataclass, field
@@ -22,7 +23,6 @@ from typing import Any
from langchain_protocol import Event, SubscribeParams
from langgraph_sdk._shared.utilities import _reconnect_delay
from langgraph_sdk.stream.transport import AsyncProtocolTransport, EventStreamHandle
# ---------------------------------------------------------------------------
@@ -275,13 +275,11 @@ class StreamController:
async def _reconnect_sleep(self, attempt: int) -> None:
"""Sleep with exponential backoff and jitter for reconnect attempt *attempt*."""
await asyncio.sleep(
_reconnect_delay(
attempt,
base=self._reconnect_backoff_base,
cap=self._reconnect_backoff_cap,
)
)
base = self._reconnect_backoff_base
cap = self._reconnect_backoff_cap
delay = min(cap, base * (2**attempt))
jitter = random.uniform(0, delay * 0.25)
await asyncio.sleep(delay + jitter)
async def _reconnect_shared_stream(self) -> bool:
"""Attempt to reopen the shared stream after a transport drop.
@@ -4,6 +4,7 @@ from __future__ import annotations
import contextlib
import logging
import random
import threading
import time
from dataclasses import dataclass, field
@@ -12,7 +13,6 @@ from typing import Any
from langchain_protocol import Event, SubscribeParams
from langgraph_sdk._shared.utilities import _reconnect_delay
from langgraph_sdk.stream.errors import (
is_non_retryable_stream_error,
stream_error_frame,
@@ -282,13 +282,11 @@ class SyncStreamController:
def _reconnect_sleep(self, attempt: int) -> None:
"""Sleep with exponential backoff + jitter before a reconnect attempt."""
time.sleep(
_reconnect_delay(
attempt,
base=self._reconnect_backoff_base,
cap=self._reconnect_backoff_cap,
)
)
base = self._reconnect_backoff_base
cap = self._reconnect_backoff_cap
delay = min(cap, base * (2**attempt))
jitter = random.uniform(0, delay * 0.25)
time.sleep(delay + jitter)
def _reconnect_shared_stream(self) -> bool:
"""Attempt to reopen the shared stream after a transport drop.
-17
View File
@@ -1,17 +0,0 @@
import asyncio
import time
import pytest
@pytest.fixture
def no_sleep(monkeypatch):
"""Record backoff sleeps without actually waiting."""
sleeps: list[float] = []
async def fake_async_sleep(d):
sleeps.append(d)
monkeypatch.setattr(asyncio, "sleep", fake_async_sleep)
monkeypatch.setattr(time, "sleep", lambda d: sleeps.append(d))
return sleeps
-41
View File
@@ -1,49 +1,8 @@
import ast
import inspect
import pytest
from langgraph_sdk import Auth
@pytest.mark.parametrize(
"kwargs",
[{}, {"actions": "read"}, {"resources": None}],
)
def test_global_handler_factory_accepts_optional_resources(kwargs) -> None:
auth = Auth()
tree = ast.parse(inspect.getsource(type(auth.on)))
cls = tree.body[0]
assert isinstance(cls, ast.ClassDef)
factory_overload = next(
node
for node in cls.body
if isinstance(node, ast.FunctionDef)
and node.name == "__call__"
and node.decorator_list
and any(arg.arg == "resources" for arg in node.args.kwonlyargs)
)
resources = next(
arg for arg in factory_overload.args.kwonlyargs if arg.arg == "resources"
)
assert resources.annotation is not None
assert ast.unparse(resources.annotation) == "str | Sequence[str] | None"
default = factory_overload.args.kw_defaults[0]
assert isinstance(default, ast.Constant) and default.value is None
@auth.on(**kwargs)
async def handler(ctx, value):
del ctx, value
return None
if kwargs.get("actions"):
assert auth._handlers == {("*", "read"): [handler]}
assert auth._global_handlers == []
else:
assert auth._global_handlers == [handler]
assert auth._handlers == {}
def test_handler_multiple_resources_and_actions() -> None:
auth = Auth()
+16 -757
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Callable, Iterator, Sequence
from collections.abc import Iterator, Sequence
from pathlib import Path
from typing import Any
@@ -13,7 +13,6 @@ from langgraph_sdk._async.runs import _wrap_stream_v2
from langgraph_sdk._shared.utilities import _sse_to_v2_dict
from langgraph_sdk._sync.runs import _wrap_stream_v2_sync
from langgraph_sdk.client import HttpClient, SyncHttpClient
from langgraph_sdk.errors import InternalServerError, NotFoundError
from langgraph_sdk.schema import (
CheckpointPayload,
CheckpointsStreamPart,
@@ -131,110 +130,6 @@ def test_sse_decoder_strips_a_leading_bom():
assert decoder.decode(b"") == StreamPart(event="values", data=1, id=None) # ty: ignore[invalid-argument-type]
def test_control_blocks_do_not_repeat_after_a_cursor_update():
decoder = SSEDecoder()
decoder.decode(b"id: cursor")
assert decoder.decode(b"") == StreamPart(event="", data=None, id="cursor") # ty: ignore[invalid-argument-type]
decoder.decode(b": heartbeat")
assert decoder.decode(b"") is None
assert decoder.decode(b"") is None
decoder.decode(b"id:")
assert decoder.decode(b"") == StreamPart(event="", data=None, id=None) # ty: ignore[invalid-argument-type]
assert decoder.last_event_id == ""
assert decoder.decode(b"") is None
def test_cr_lines_are_emitted_without_waiting_for_another_chunk():
decoder = BytesLineDecoder()
assert decoder.decode(b"data: 1\r\r") == [b"data: 1", b""]
assert decoder.decode(b"\n") == []
assert decoder.decode(b"data: 2\r") == [b"data: 2"]
assert decoder.decode(b"") == []
assert decoder.decode(b"\n\r") == [b""]
assert decoder.flush() == []
def test_empty_data_lines_preserve_their_newlines():
decoder = SSEDecoder()
for line in (b"data", b"data: 1", b"data:"):
decoder.decode(line)
assert decoder._data == b"\n1\n\n"
assert decoder.decode(b"") == StreamPart(event="", data=1, id=None) # ty: ignore[invalid-argument-type]
decoder.decode(b"data:")
decoder.decode(b"data:")
assert decoder.decode(b"") == StreamPart(event="", data=None, id=None) # ty: ignore[invalid-argument-type]
assert decoder.has_data
assert decoder.decode(b"") is None
assert not decoder.has_data
def test_malformed_utf8_is_replaced_in_fields_and_json_strings():
decoder = SSEDecoder()
for line in (b"event: \xff", b"id: \xff", b'data: "\xff"'):
decoder.decode(line)
assert decoder.decode(b"") == StreamPart(event="\ufffd", data="\ufffd", id="\ufffd") # ty: ignore[invalid-argument-type]
@pytest.mark.parametrize("asynchronous", [False, True])
async def test_null_and_empty_data_are_not_dropped(asynchronous: bool):
def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
headers={"Content-Type": "text/event-stream"},
content=b"data: null\n\ndata:\n\n",
)
transport = httpx.MockTransport(handler)
if asynchronous:
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
parts = [part async for part in HttpClient(client).stream("/stream", "GET")]
else:
with httpx.Client(
transport=transport, base_url="https://example.com"
) as client:
parts = list(SyncHttpClient(client).stream("/stream", "GET"))
assert parts == [StreamPart(event="", data=None), StreamPart(event="", data=None)] # ty: ignore[invalid-argument-type]
@pytest.mark.parametrize("asynchronous", [False, True])
async def test_cursor_is_inherited_after_reconnect(asynchronous: bool):
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
if len(requests) == 1:
stream_cls = AsyncListByteStream if asynchronous else ListByteStream
return httpx.Response(
200,
headers={"Content-Type": "text/event-stream", "Location": "/reconnect"},
stream=stream_cls(
[b"id: cursor\nevent: values\ndata: 1\n\n"],
httpx.RemoteProtocolError("incomplete chunked read"),
),
)
assert (b"Last-Event-ID", b"cursor") in request.headers.raw
return httpx.Response(
200,
headers={"Content-Type": "text/event-stream"},
content=b"event: values\ndata: 2\n\n",
)
transport = httpx.MockTransport(handler)
if asynchronous:
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
parts = [part async for part in HttpClient(client).stream("/stream", "GET")]
else:
with httpx.Client(
transport=transport, base_url="https://example.com"
) as client:
parts = list(SyncHttpClient(client).stream("/stream", "GET"))
assert [part.id for part in parts] == ["cursor", "cursor"]
# --- HTTP client streaming ---
@@ -281,7 +176,6 @@ def test_sync_http_client_stream_flushes_trailing_event():
assert parts == [StreamPart(event="foo", data={"bar": 1})]
@pytest.mark.usefixtures("no_sleep")
def test_sync_http_client_stream_recovers_after_disconnect():
reconnect_path = "/reconnect"
first_chunks = [
@@ -347,7 +241,6 @@ def test_sync_http_client_stream_recovers_after_disconnect():
@pytest.mark.asyncio
@pytest.mark.usefixtures("no_sleep")
async def test_http_client_stream_recovers_after_disconnect():
reconnect_path = "/reconnect"
first_chunks = [
@@ -419,14 +312,18 @@ async def test_http_client_stream_recovers_after_disconnect():
]
def _drop_after(chunks: list[bytes], stream_cls: type, reconnects: list[httpx.Request]):
def _drop_after_an_empty_id(stream_cls: type, reconnects: list[httpx.Request]):
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "POST":
return httpx.Response(
200,
headers={"Content-Type": "text/event-stream", "Location": "/reconnect"},
stream=stream_cls(
chunks, httpx.RemoteProtocolError("incomplete chunked read")
[
b"id: 1\nevent: values\ndata: 1\n\n",
b"id:\nevent: values\ndata: 2\n\n",
],
httpx.RemoteProtocolError("incomplete chunked read"),
),
)
reconnects.append(request)
@@ -439,664 +336,26 @@ def _drop_after(chunks: list[bytes], stream_cls: type, reconnects: list[httpx.Re
return handler
def _sync_reconnect(
chunks: list[bytes], headers: dict[str, str] | None
) -> httpx.Request:
def test_sync_stream_reconnects_without_a_cursor_after_an_empty_id():
reconnects: list[httpx.Request] = []
transport = httpx.MockTransport(_drop_after(chunks, ListByteStream, reconnects))
transport = httpx.MockTransport(_drop_after_an_empty_id(ListByteStream, reconnects))
with httpx.Client(transport=transport, base_url="https://example.com") as client:
list(SyncHttpClient(client).stream("/stream", "POST", json={}, headers=headers))
return reconnects[0]
list(SyncHttpClient(client).stream("/stream", "POST", json={}))
assert "last-event-id" not in reconnects[0].headers
async def _async_reconnect(
chunks: list[bytes], headers: dict[str, str] | None
) -> httpx.Request:
async def test_async_stream_reconnects_without_a_cursor_after_an_empty_id():
reconnects: list[httpx.Request] = []
transport = httpx.MockTransport(
_drop_after(chunks, AsyncListByteStream, reconnects)
_drop_after_an_empty_id(AsyncListByteStream, reconnects)
)
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
[
part
async for part in HttpClient(client).stream(
"/stream", "POST", json={}, headers=headers
)
]
return reconnects[0]
[part async for part in HttpClient(client).stream("/stream", "POST", json={})]
_EMPTY_IDS = pytest.mark.parametrize(
"empty_id",
[b"id:\nevent: values\ndata: 2\n\n", b"id:\n\n"],
ids=["with-an-event", "alone"],
)
_CALLER_CURSORS = pytest.mark.parametrize(
"headers",
[None, {"Last-Event-ID": "5"}, {"last-event-id": "5"}],
ids=["no-caller-cursor", "caller-cursor", "lowercase-caller-cursor"],
)
@_EMPTY_IDS
@_CALLER_CURSORS
@pytest.mark.usefixtures("no_sleep")
def test_sync_stream_reconnects_without_a_cursor_after_an_empty_id(
empty_id: bytes, headers: dict[str, str] | None
):
with pytest.warns(UserWarning, match="Last-Event-ID"):
reconnect = _sync_reconnect(
[b"id: 1\nevent: values\ndata: 1\n\n", empty_id], headers
)
assert "last-event-id" not in reconnect.headers
@_EMPTY_IDS
@_CALLER_CURSORS
@pytest.mark.usefixtures("no_sleep")
async def test_async_stream_reconnects_without_a_cursor_after_an_empty_id(
empty_id: bytes, headers: dict[str, str] | None
):
with pytest.warns(UserWarning, match="Last-Event-ID"):
reconnect = await _async_reconnect(
[b"id: 1\nevent: values\ndata: 1\n\n", empty_id], headers
)
assert "last-event-id" not in reconnect.headers
@pytest.mark.usefixtures("no_sleep")
def test_sync_stream_id_replaces_a_lowercase_caller_cursor():
reconnect = _sync_reconnect(
[b"id: 1\nevent: values\ndata: 1\n\n"], {"last-event-id": "5"}
)
assert reconnect.headers.get_list("last-event-id") == ["1"]
@pytest.mark.usefixtures("no_sleep")
async def test_async_stream_id_replaces_a_lowercase_caller_cursor():
reconnect = await _async_reconnect(
[b"id: 1\nevent: values\ndata: 1\n\n"], {"last-event-id": "5"}
)
assert reconnect.headers.get_list("last-event-id") == ["1"]
@pytest.mark.usefixtures("no_sleep")
def test_sync_stream_keeps_the_caller_cursor_until_the_stream_sends_an_id():
reconnect = _sync_reconnect([b"event: values\ndata: 1\n\n"], {"Last-Event-ID": "5"})
assert reconnect.headers.get_list("last-event-id") == ["5"]
@pytest.mark.usefixtures("no_sleep")
async def test_async_stream_keeps_the_caller_cursor_until_the_stream_sends_an_id():
reconnect = await _async_reconnect(
[b"event: values\ndata: 1\n\n"], {"Last-Event-ID": "5"}
)
assert reconnect.headers.get_list("last-event-id") == ["5"]
# A reconnect that sends no Last-Event-ID can't replay what was sent while it was
# disconnected, so it warns.
_RECONNECT_CURSORS = pytest.mark.parametrize(
("chunks", "headers", "warns"),
[
([b"event: values\ndata: 1\n\n"], None, True),
([b"id: 1\nevent: values\ndata: 1\n\n"], None, False),
([b"event: values\ndata: 1\n\n"], {"Last-Event-ID": "5"}, False),
],
ids=["no-cursor", "stream-cursor", "caller-cursor"],
)
@_RECONNECT_CURSORS
@pytest.mark.usefixtures("no_sleep")
def test_sync_reconnect_warns_only_without_a_cursor(
chunks: list[bytes],
headers: dict[str, str] | None,
warns: bool,
recwarn: pytest.WarningsRecorder,
):
_sync_reconnect(chunks, headers)
assert any("Last-Event-ID" in str(w.message) for w in recwarn) is warns
@_RECONNECT_CURSORS
@pytest.mark.usefixtures("no_sleep")
async def test_async_reconnect_warns_only_without_a_cursor(
chunks: list[bytes],
headers: dict[str, str] | None,
warns: bool,
recwarn: pytest.WarningsRecorder,
):
await _async_reconnect(chunks, headers)
assert any("Last-Event-ID" in str(w.message) for w in recwarn) is warns
# --- reconnects survive a server restart or a deploy ---
SSE = {"Content-Type": "text/event-stream"}
DROP = httpx.RemoteProtocolError("incomplete chunked read")
def _event(i: int) -> bytes:
return f"id: {i}\nevent: values\ndata: {i}\n\n".encode()
def _scripted(steps: list[Callable[[httpx.Request], httpx.Response]]):
"""Handler that plays one step per request and records each request."""
seen: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
seen.append(request)
if len(seen) > len(steps):
raise AssertionError("unexpected request")
return steps[len(seen) - 1](request)
return handler, seen
def _ok(stream_cls, chunks, *, drop=False, location=None):
headers = dict(SSE)
if location:
headers["Location"] = location
def step(_request):
return httpx.Response(
200, headers=headers, stream=stream_cls(chunks, DROP if drop else None)
)
return step
def _status(code):
return lambda request: httpx.Response(
code, json={"detail": "upstream"}, request=request
)
def _connect_error(request):
raise httpx.ConnectError("[Errno 61] Connection refused", request=request)
def _not_sse(request):
return httpx.Response(
200, headers={"Content-Type": "text/html"}, text="<html>", request=request
)
def _transient_steps(stream_cls, failure):
"""POST drops after event 1; first reconnect fails; second succeeds."""
return [
_ok(stream_cls, [_event(1)], drop=True, location="/reconnect"),
failure,
_ok(stream_cls, [_event(2), b"event: end\ndata: null\n\n"]),
]
def _long_stream_steps(stream_cls, drops):
"""POST + `drops` reconnects, each delivering one event then dropping."""
steps = [_ok(stream_cls, [_event(0)], drop=True, location="/reconnect")]
for i in range(1, drops):
steps.append(_ok(stream_cls, [_event(i)], drop=True))
steps.append(_ok(stream_cls, [_event(drops), b"event: end\ndata: null\n\n"]))
return steps
def _run_sync(steps, headers=None):
handler, seen = _scripted(steps)
with httpx.Client(
transport=httpx.MockTransport(handler), base_url="https://example.com"
) as client:
parts = list(
SyncHttpClient(client).stream("/stream", "POST", json={}, headers=headers)
)
return parts, seen
async def _run_async(steps, headers=None):
sync_handler, seen = _scripted(steps)
async def handler(request):
return sync_handler(request)
async with httpx.AsyncClient(
transport=httpx.MockTransport(handler), base_url="https://example.com"
) as client:
parts = [
p
async for p in HttpClient(client).stream(
"/stream", "POST", json={}, headers=headers
)
]
return parts, seen
FAILURES = {
"429": _status(429),
"502": _status(502),
"503": _status(503),
"connect_error": _connect_error,
"not_sse": _not_sse,
}
# A reconnect that hits a 429 / 502 / 503 / connect error / non-SSE body, then
# succeeds, should recover.
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("failure", FAILURES.values(), ids=FAILURES.keys())
def test_sync_reconnect_recovers_from_transient_failure(failure):
parts, seen = _run_sync(_transient_steps(ListByteStream, failure))
assert [p.event for p in parts] == ["values", "values", "end"]
assert len(seen) == 3
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("failure", FAILURES.values(), ids=FAILURES.keys())
async def test_async_reconnect_recovers_from_transient_failure(failure):
parts, seen = await _run_async(_transient_steps(AsyncListByteStream, failure))
assert [p.event for p in parts] == ["values", "values", "end"]
assert len(seen) == 3
# Any other 4xx on a reconnect won't go away by retrying, so it raises.
def _client_error_steps(stream_cls):
return [
_ok(stream_cls, [_event(1)], drop=True, location="/reconnect"),
_status(404),
]
@pytest.mark.usefixtures("no_sleep")
def test_sync_reconnect_does_not_retry_a_client_error():
with pytest.raises(NotFoundError):
_run_sync(_client_error_steps(ListByteStream))
@pytest.mark.usefixtures("no_sleep")
async def test_async_reconnect_does_not_retry_a_client_error():
with pytest.raises(NotFoundError):
await _run_async(_client_error_steps(AsyncListByteStream))
# The first request isn't a reconnect, so it raises even when it names a Location.
def _first_request_503(request):
return httpx.Response(
503,
headers={"Location": "/reconnect"},
json={"detail": "upstream"},
request=request,
)
@pytest.mark.usefixtures("no_sleep")
def test_sync_first_request_failure_is_not_retried():
with pytest.raises(InternalServerError):
_run_sync([_first_request_503])
@pytest.mark.usefixtures("no_sleep")
async def test_async_first_request_failure_is_not_retried():
with pytest.raises(InternalServerError):
await _run_async([_first_request_503])
# A server that stays down gets five reconnects, backing off 1, 2, 4, 5 and 5s
# (each plus up to 25% jitter), so about 20s, before the stream gives up. The
# error it gives up with is caused by the last failure, so callers see why.
GIVE_UP = "Exceeded maximum SSE reconnection attempts"
def _server_down_steps(stream_cls):
return [
_ok(stream_cls, [_event(1)], drop=True, location="/reconnect"),
*[_status(502)] * 5,
]
def _assert_backed_off_for_about_20s(sleeps: list[float]) -> None:
assert len(sleeps) == 5
for slept, delay in zip(sleeps, [1, 2, 4, 5, 5], strict=True):
assert delay <= slept <= delay * 1.25
def test_sync_reconnect_gives_up_after_five_backed_off_attempts(no_sleep):
with pytest.raises(httpx.TransportError, match=GIVE_UP) as exc_info:
_run_sync(_server_down_steps(ListByteStream))
assert isinstance(exc_info.value.__cause__, InternalServerError)
_assert_backed_off_for_about_20s(no_sleep)
async def test_async_reconnect_gives_up_after_five_backed_off_attempts(no_sleep):
with pytest.raises(httpx.TransportError, match=GIVE_UP) as exc_info:
await _run_async(_server_down_steps(AsyncListByteStream))
assert isinstance(exc_info.value.__cause__, InternalServerError)
_assert_backed_off_for_about_20s(no_sleep)
# A long stream that drops 6 times, each reconnect succeeding and making
# progress, should not run out of attempts.
@pytest.mark.usefixtures("no_sleep")
def test_sync_long_stream_survives_many_spaced_out_drops():
parts, _ = _run_sync(_long_stream_steps(ListByteStream, drops=6))
assert [p.data for p in parts] == [0, 1, 2, 3, 4, 5, 6, None]
@pytest.mark.usefixtures("no_sleep")
async def test_async_long_stream_survives_many_spaced_out_drops():
parts, _ = await _run_async(_long_stream_steps(AsyncListByteStream, drops=6))
assert [p.data for p in parts] == [0, 1, 2, 3, 4, 5, 6, None]
# Each reconnect resumes after the newest event the stream got, so a reconnect
# loop on a resumable run replays only what it missed. A failed reconnect keeps
# the cursor for the next one.
RECONNECT_LOOPS = {
"progress_between_drops": (
lambda stream_cls: _long_stream_steps(stream_cls, drops=3),
[None, "0", "1", "2"],
),
"failed_reconnect": (
lambda stream_cls: _transient_steps(stream_cls, _status(502)),
[None, "1", "1"],
),
}
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize(
("steps", "cursors"), RECONNECT_LOOPS.values(), ids=RECONNECT_LOOPS.keys()
)
def test_sync_reconnect_loop_resumes_after_the_newest_event(steps, cursors):
_, seen = _run_sync(steps(ListByteStream))
assert [r.headers.get("last-event-id") for r in seen] == cursors
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize(
("steps", "cursors"), RECONNECT_LOOPS.values(), ids=RECONNECT_LOOPS.keys()
)
async def test_async_reconnect_loop_resumes_after_the_newest_event(steps, cursors):
_, seen = await _run_async(steps(AsyncListByteStream))
assert [r.headers.get("last-event-id") for r in seen] == cursors
# Reconnects should back off rather than hammer the server.
def test_sync_reconnect_backs_off(no_sleep):
_run_sync(_long_stream_steps(ListByteStream, drops=2))
assert len(no_sleep) == 2 and all(d > 0 for d in no_sleep)
async def test_async_reconnect_backs_off(no_sleep):
await _run_async(_long_stream_steps(AsyncListByteStream, drops=2))
assert len(no_sleep) == 2 and all(d > 0 for d in no_sleep)
# --- a run stream that closes cleanly before the run ends ---
RUN_STREAM = "/threads/t1/runs/r1/stream"
RUN = "/threads/t1/runs/r1"
END = b"event: end\ndata: null\n\n"
Step = Callable[[httpx.Request], httpx.Response]
def _run_status(status: str) -> Step:
return lambda request: httpx.Response(200, json={"status": status}, request=request)
def _clean_close_steps(stream_cls: type, closes: int, check: Step) -> list[Step]:
steps = [_ok(stream_cls, [_event(0)], location=RUN_STREAM)]
for i in range(1, closes + 1):
steps += [check, _ok(stream_cls, [_event(i)])]
return [*steps, _run_status("success")]
def _stalled_steps(stream_cls: type) -> list[Step]:
return [
_ok(stream_cls, [_event(1)], location=RUN_STREAM),
*[_run_status("running"), _ok(stream_cls, [])] * 5,
_run_status("running"),
]
def _requests(seen: list[httpx.Request]) -> list[tuple[str, str, str | None]]:
return [(r.method, r.url.path, r.headers.get("last-event-id")) for r in seen]
RESUMED = [
("POST", "/stream", None),
("GET", RUN, None),
("GET", RUN_STREAM, "0"),
("GET", RUN, None),
]
CHECKS_THAT_RECONNECT = {
"pending": _run_status("pending"),
"running": _run_status("running"),
"429": _status(429),
"503": _status(503),
"connect_error": _connect_error,
}
@pytest.mark.parametrize(
"check", CHECKS_THAT_RECONNECT.values(), ids=CHECKS_THAT_RECONNECT.keys()
)
def test_sync_clean_close_reconnects_after_check(check: Step, no_sleep: list[float]):
parts, seen = _run_sync(_clean_close_steps(ListByteStream, 1, check))
assert [p.data for p in parts] == [0, 1]
assert _requests(seen) == RESUMED
assert len(no_sleep) == 1
@pytest.mark.parametrize(
"check", CHECKS_THAT_RECONNECT.values(), ids=CHECKS_THAT_RECONNECT.keys()
)
async def test_async_clean_close_reconnects_after_check(
check: Step, no_sleep: list[float]
):
parts, seen = await _run_async(_clean_close_steps(AsyncListByteStream, 1, check))
assert [p.data for p in parts] == [0, 1]
assert _requests(seen) == RESUMED
assert len(no_sleep) == 1
@pytest.mark.usefixtures("no_sleep")
def test_sync_clean_closes_with_progress_do_not_run_out_of_attempts():
parts, _ = _run_sync(_clean_close_steps(ListByteStream, 6, _run_status("running")))
assert [p.data for p in parts] == [0, 1, 2, 3, 4, 5, 6]
@pytest.mark.usefixtures("no_sleep")
async def test_async_clean_closes_with_progress_do_not_run_out_of_attempts():
parts, _ = await _run_async(
_clean_close_steps(AsyncListByteStream, 6, _run_status("running"))
)
assert [p.data for p in parts] == [0, 1, 2, 3, 4, 5, 6]
CHECKS_THAT_END = {
"success": _run_status("success"),
"error": _run_status("error"),
"timeout": _run_status("timeout"),
"interrupted": _run_status("interrupted"),
"403": _status(403),
"404": _status(404),
"not_json": _not_sse,
"json_without_status": lambda request: httpx.Response(
200, json={}, request=request
),
}
CHECKED = [("POST", "/stream", None), ("GET", RUN, None)]
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("check", CHECKS_THAT_END.values(), ids=CHECKS_THAT_END.keys())
def test_sync_clean_close_ends_the_stream_after_check(check: Step):
parts, seen = _run_sync(
[_ok(ListByteStream, [_event(1)], location=RUN_STREAM), check]
)
assert [p.data for p in parts] == [1]
assert _requests(seen) == CHECKED
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("check", CHECKS_THAT_END.values(), ids=CHECKS_THAT_END.keys())
async def test_async_clean_close_ends_the_stream_after_check(check: Step):
parts, seen = await _run_async(
[_ok(AsyncListByteStream, [_event(1)], location=RUN_STREAM), check]
)
assert [p.data for p in parts] == [1]
assert _requests(seen) == CHECKED
NO_CHECK = {
"end": lambda s: [_ok(s, [_event(1), END], location=RUN_STREAM)],
"error": lambda s: [
_ok(s, [_event(1), b"event: error\ndata: {}\n\n"], location=RUN_STREAM)
],
"unterminated_end": lambda s: [
_ok(s, [_event(1), b"event: end\ndata: null\n"], location=RUN_STREAM)
],
"end_on_an_earlier_connection": lambda s: [
_ok(s, [_event(1), END], drop=True, location=RUN_STREAM),
_ok(s, []),
],
"thread_stream": lambda s: [_ok(s, [_event(1)], location="/threads/t1/stream")],
"threadless_run_location": lambda s: [
_ok(s, [_event(1)], location="/runs/r1/stream")
],
}
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("steps", NO_CHECK.values(), ids=NO_CHECK.keys())
def test_sync_stream_ends_without_a_run_check(steps: Callable[[type], list[Step]]):
_, seen = _run_sync(steps(ListByteStream))
assert [r.url.path for r in seen if not r.url.path.endswith("/stream")] == []
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("steps", NO_CHECK.values(), ids=NO_CHECK.keys())
async def test_async_stream_ends_without_a_run_check(
steps: Callable[[type], list[Step]],
):
_, seen = await _run_async(steps(AsyncListByteStream))
assert [r.url.path for r in seen if not r.url.path.endswith("/stream")] == []
def test_sync_clean_closes_without_progress_give_up_after_five_attempts(
no_sleep: list[float],
):
with pytest.raises(httpx.TransportError, match=GIVE_UP) as exc_info:
_run_sync(_stalled_steps(ListByteStream))
assert "still pending or running" in str(exc_info.value.__cause__)
_assert_backed_off_for_about_20s(no_sleep)
async def test_async_clean_closes_without_progress_give_up_after_five_attempts(
no_sleep: list[float],
):
with pytest.raises(httpx.TransportError, match=GIVE_UP) as exc_info:
await _run_async(_stalled_steps(AsyncListByteStream))
assert "still pending or running" in str(exc_info.value.__cause__)
_assert_backed_off_for_about_20s(no_sleep)
CHECK_URLS = {
"path": ("/threads/t1/runs/r1/stream", "https://example.com/threads/t1/runs/r1"),
"prefix_and_query": (
"/api/threads/t1/runs/r1/stream?stream_mode=values",
"https://example.com/api/threads/t1/runs/r1",
),
"absolute": (
"https://example.com/threads/t1/runs/r1/stream",
"https://example.com/threads/t1/runs/r1",
),
}
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize(
("location", "url"), CHECK_URLS.values(), ids=CHECK_URLS.keys()
)
def test_sync_run_check_gets_the_run_behind_the_stream_location(
location: str, url: str
):
_, seen = _run_sync(
[_ok(ListByteStream, [_event(1)], location=location), _run_status("success")]
)
assert str(seen[-1].url) == url
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize(
("location", "url"), CHECK_URLS.values(), ids=CHECK_URLS.keys()
)
async def test_async_run_check_gets_the_run_behind_the_stream_location(
location: str, url: str
):
_, seen = await _run_async(
[
_ok(AsyncListByteStream, [_event(1)], location=location),
_run_status("success"),
]
)
assert str(seen[-1].url) == url
CALLER_HEADERS = {"Authorization": "Bearer token", "Last-Event-ID": "0"}
BODY_AND_CURSOR_HEADERS = {"content-type", "content-length", "last-event-id"}
def _assert_run_check_headers(seen: list[httpx.Request]) -> None:
check = seen[-1]
assert check.url.path == RUN
assert check.headers["authorization"] == "Bearer token"
assert check.headers["accept"] == "application/json"
assert check.headers.keys() & BODY_AND_CURSOR_HEADERS == set()
@pytest.mark.usefixtures("no_sleep")
def test_sync_run_check_sends_caller_headers_and_accepts_json_without_body_or_cursor():
_, seen = _run_sync(
[_ok(ListByteStream, [_event(1)], location=RUN_STREAM), _run_status("success")],
headers=CALLER_HEADERS,
)
_assert_run_check_headers(seen)
@pytest.mark.usefixtures("no_sleep")
async def test_async_run_check_sends_caller_headers_and_accepts_json_without_body_or_cursor():
_, seen = await _run_async(
[
_ok(AsyncListByteStream, [_event(1)], location=RUN_STREAM),
_run_status("success"),
],
headers=CALLER_HEADERS,
)
_assert_run_check_headers(seen)
assert "last-event-id" not in reconnects[0].headers
# --- _sse_to_v2_dict conversion ---
+5 -1
View File
@@ -120,5 +120,9 @@ def test_context_is_frozen():
def test_every_settings_key_is_optional():
assert PrioritySettings.__optional_keys__ == {"priority", "fairness_group"}
assert PrioritySettings.__optional_keys__ == {
"priority",
"fairness_group",
"fairness_weight",
}
assert PrioritySettings.__required_keys__ == frozenset()
+3 -98
View File
@@ -1,8 +1,7 @@
from __future__ import annotations
import json
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
from typing import Any
from collections.abc import AsyncIterator, Iterator
import httpx
import pytest
@@ -25,16 +24,7 @@ class _BodyDropsMidRead(httpx.SyncByteStream, httpx.AsyncByteStream):
raise httpx.ReadError("connection dropped")
def _done(_request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"done": True})
def _wait_handler(
requests: list[httpx.Request],
reconnects: Sequence[Callable[[httpx.Request], httpx.Response]] = (_done,),
):
"""POST drops mid-body; the reconnects answer from `reconnects` in order."""
def _wait_handler(requests: list[httpx.Request]):
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
if request.method == "POST":
@@ -43,7 +33,7 @@ def _wait_handler(
headers={"Location": "/threads/t1/runs/r1/join"},
stream=_BodyDropsMidRead(),
)
return reconnects[len(requests) - 2](request)
return httpx.Response(200, json={"done": True})
return handler
@@ -56,7 +46,6 @@ def _assert_bodyless_get(request: httpx.Request) -> None:
assert request.read() == b""
@pytest.mark.usefixtures("no_sleep")
def test_sync_wait_reconnects_with_a_bodyless_get():
requests: list[httpx.Request] = []
transport = httpx.MockTransport(_wait_handler(requests))
@@ -69,7 +58,6 @@ def test_sync_wait_reconnects_with_a_bodyless_get():
_assert_bodyless_get(requests[1])
@pytest.mark.usefixtures("no_sleep")
async def test_async_wait_reconnects_with_a_bodyless_get():
requests: list[httpx.Request] = []
transport = httpx.MockTransport(_wait_handler(requests))
@@ -84,89 +72,6 @@ async def test_async_wait_reconnects_with_a_bodyless_get():
_assert_bodyless_get(requests[1])
def _connect_error(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("[Errno 61] Connection refused", request=request)
async def _wait(
handler: Callable[[httpx.Request], httpx.Response], *, asynchronous: bool
) -> Any:
transport = httpx.MockTransport(handler)
if asynchronous:
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
return await RunsClient(HttpClient(client)).wait("t1", "agent", input={})
with httpx.Client(transport=transport, base_url="https://example.com") as client:
return SyncRunsClient(SyncHttpClient(client)).wait("t1", "agent", input={})
# What a server restart or a deploy looks like to the reconnect GET: each one is
# retried after a backoff.
RECONNECT_FAILURES = {
"429": lambda request: httpx.Response(429, request=request),
"503": lambda request: httpx.Response(503, request=request),
"connect_error": _connect_error,
"body_drops": lambda _request: httpx.Response(
200,
headers={"Location": "/threads/t1/runs/r1/join"},
stream=_BodyDropsMidRead(),
),
}
@pytest.mark.parametrize("asynchronous", [False, True])
@pytest.mark.parametrize(
"failure", RECONNECT_FAILURES.values(), ids=RECONNECT_FAILURES.keys()
)
async def test_wait_retries_a_failed_reconnect_after_a_backoff(
failure, asynchronous, no_sleep
):
requests: list[httpx.Request] = []
with pytest.warns(UserWarning, match="attempting reconnect"):
result = await _wait(
_wait_handler(requests, [failure, _done]), asynchronous=asynchronous
)
assert result == {"done": True}
assert len(requests) == 3
assert len(no_sleep) == 2 and min(no_sleep) >= 1, no_sleep
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("asynchronous", [False, True])
async def test_wait_raises_after_five_failed_reconnects(asynchronous):
requests: list[httpx.Request] = []
with (
pytest.warns(UserWarning, match="attempting reconnect"),
pytest.raises(httpx.HTTPStatusError, match="503"),
):
await _wait(
_wait_handler(requests, [RECONNECT_FAILURES["503"]] * 5),
asynchronous=asynchronous,
)
assert len(requests) == 6
@pytest.mark.usefixtures("no_sleep")
@pytest.mark.parametrize("asynchronous", [False, True])
async def test_wait_does_not_retry_a_client_error(asynchronous):
requests: list[httpx.Request] = []
with (
pytest.warns(UserWarning, match="attempting reconnect"),
pytest.raises(httpx.HTTPStatusError, match="404"),
):
await _wait(
_wait_handler(
requests, [lambda request: httpx.Response(404, request=request)]
),
asynchronous=asynchronous,
)
assert len(requests) == 2
def test_sync_wait_raises_the_run_error():
requests: list[httpx.Request] = []
-126
View File
@@ -11,14 +11,6 @@ from langgraph_sdk.client import (
SyncThreadsClient,
ThreadsClient,
)
from langgraph_sdk.schema import Checkpoint
CHECKPOINT: Checkpoint = {
"thread_id": "thread_123",
"checkpoint_ns": "",
"checkpoint_id": "cp_1",
"checkpoint_map": None,
}
@pytest.mark.asyncio
@@ -64,121 +56,3 @@ def test_sync_threads_update_return_minimal():
)
assert result is None
@pytest.mark.asyncio
async def test_async_threads_get_state_select():
requests: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, json={})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
threads_client = ThreadsClient(HttpClient(client))
await threads_client.get_state("thread_123", select=["values.messages", "next"])
await threads_client.get_state(
"thread_123", checkpoint_id="cp_1", select=["values.messages"]
)
await threads_client.get_state(
"thread_123",
checkpoint=CHECKPOINT,
select=["values.messages"],
)
await threads_client.get_state("thread_123")
latest, by_id, by_checkpoint, default = requests
assert latest.method == "GET"
assert latest.url.path == "/threads/thread_123/state"
assert latest.url.params.get_list("select") == ["values.messages", "next"]
assert by_id.method == "GET"
assert by_id.url.path == "/threads/thread_123/state/cp_1"
assert by_id.url.params.get_list("select") == ["values.messages"]
assert by_checkpoint.method == "POST"
assert by_checkpoint.url.path == "/threads/thread_123/state/checkpoint"
assert json.loads(by_checkpoint.content) == {
"checkpoint": CHECKPOINT,
"subgraphs": False,
"select": ["values.messages"],
}
assert "select" not in default.url.params
@pytest.mark.asyncio
async def test_async_threads_get_history_select():
bodies: list[dict] = []
async def handler(request: httpx.Request) -> httpx.Response:
assert request.method == "POST"
assert request.url.path == "/threads/thread_123/history"
bodies.append(json.loads(request.content))
return httpx.Response(200, json=[])
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(
transport=transport, base_url="https://example.com"
) as client:
threads_client = ThreadsClient(HttpClient(client))
await threads_client.get_history("thread_123", select=["values.messages"])
await threads_client.get_history("thread_123")
assert bodies == [{"limit": 10, "select": ["values.messages"]}, {"limit": 10}]
def test_sync_threads_get_state_select():
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, json={})
transport = httpx.MockTransport(handler)
with httpx.Client(transport=transport, base_url="https://example.com") as client:
threads_client = SyncThreadsClient(SyncHttpClient(client))
threads_client.get_state("thread_123", select=["values.messages", "next"])
threads_client.get_state(
"thread_123", checkpoint_id="cp_1", select=["values.messages"]
)
threads_client.get_state(
"thread_123",
checkpoint=CHECKPOINT,
select=["values.messages"],
)
threads_client.get_state("thread_123")
latest, by_id, by_checkpoint, default = requests
assert latest.method == "GET"
assert latest.url.path == "/threads/thread_123/state"
assert latest.url.params.get_list("select") == ["values.messages", "next"]
assert by_id.method == "GET"
assert by_id.url.path == "/threads/thread_123/state/cp_1"
assert by_id.url.params.get_list("select") == ["values.messages"]
assert by_checkpoint.method == "POST"
assert by_checkpoint.url.path == "/threads/thread_123/state/checkpoint"
assert json.loads(by_checkpoint.content) == {
"checkpoint": CHECKPOINT,
"subgraphs": False,
"select": ["values.messages"],
}
assert "select" not in default.url.params
def test_sync_threads_get_history_select():
bodies: list[dict] = []
def handler(request: httpx.Request) -> httpx.Response:
assert request.method == "POST"
assert request.url.path == "/threads/thread_123/history"
bodies.append(json.loads(request.content))
return httpx.Response(200, json=[])
transport = httpx.MockTransport(handler)
with httpx.Client(transport=transport, base_url="https://example.com") as client:
threads_client = SyncThreadsClient(SyncHttpClient(client))
threads_client.get_history("thread_123", select=["values.messages"])
threads_client.get_history("thread_123")
assert bodies == [{"limit": 10, "select": ["values.messages"]}, {"limit": 10}]
+2 -2
View File
@@ -301,7 +301,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.19"
version = "1.2.17"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -385,7 +385,7 @@ test = [
[[package]]
name = "langgraph-checkpoint"
version = "4.3.1"
version = "4.3.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },