mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-30 03:39:38 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8393182893 | ||
|
|
488c1af383 | ||
|
|
2c98c59fca | ||
|
|
d27d4b2d98 | ||
|
|
742d165acb | ||
|
|
fbcdbb5e0c | ||
|
|
7f225ef93e |
@@ -217,11 +217,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/uuid@^10.0.0":
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
|
||||
integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^8.58.0":
|
||||
version "8.58.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz#ad40e492f1931f46da1bd888e52b9e56df9063aa"
|
||||
@@ -343,13 +338,6 @@ ajv@^6.14.0:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||
@@ -508,38 +496,11 @@ camelcase@6:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||
|
||||
chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
console-table-printer@^2.12.1:
|
||||
version "2.14.6"
|
||||
resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.14.6.tgz#edfe0bf311fa2701922ed509443145ab51e06436"
|
||||
integrity sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==
|
||||
dependencies:
|
||||
simple-wcswidth "^1.0.1"
|
||||
|
||||
cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
@@ -1059,11 +1020,6 @@ has-bigints@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
|
||||
integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
|
||||
@@ -1372,16 +1328,12 @@ keyv@^4.5.4:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
"langsmith@>=0.5.0 <1.0.0":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.4.tgz#f75b82b08e30db72a7d1d595b341e9666bd525e5"
|
||||
integrity sha512-qYkNIoKpf0ZYt+cYzrDV+XI3FCexApmZmp8EMs3eDTMv0OvrHMLoxJ9IpkeoXJSX24+GPk0/jXjKx2hWerpy9w==
|
||||
version "0.5.18"
|
||||
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.18.tgz#c691ad23614f0b46eaf07d982e0ac988e1f43880"
|
||||
integrity sha512-3zuZUWffTHQ+73EAwnodADtf534VNEZUpXr9jC12qyG8/IQuJET7PRsCpTb9wX2lmBspakwLUpqpj3tNm/0bVA==
|
||||
dependencies:
|
||||
"@types/uuid" "^10.0.0"
|
||||
chalk "^4.1.2"
|
||||
console-table-printer "^2.12.1"
|
||||
p-queue "^6.6.2"
|
||||
semver "^7.6.3"
|
||||
uuid "^10.0.0"
|
||||
p-queue "6.6.2"
|
||||
uuid "10.0.0"
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
@@ -1528,7 +1480,7 @@ p-locate@^5.0.0:
|
||||
dependencies:
|
||||
p-limit "^3.0.2"
|
||||
|
||||
p-queue@^6.6.2:
|
||||
p-queue@6.6.2, p-queue@^6.6.2:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
|
||||
integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
|
||||
@@ -1690,11 +1642,6 @@ semver@^6.3.1:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.6.3:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
semver@^7.7.3:
|
||||
version "7.7.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||
@@ -1783,11 +1730,6 @@ side-channel@^1.1.0:
|
||||
side-channel-map "^1.0.1"
|
||||
side-channel-weakmap "^1.0.2"
|
||||
|
||||
simple-wcswidth@^1.0.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b"
|
||||
integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==
|
||||
|
||||
stop-iteration-iterator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad"
|
||||
@@ -1838,13 +1780,6 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-preserve-symlinks-flag@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
@@ -1966,7 +1901,7 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
uuid@^10.0.0:
|
||||
uuid@10.0.0, uuid@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||
|
||||
Generated
+20
-20
@@ -2318,28 +2318,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uv"
|
||||
version = "0.11.3"
|
||||
version = "0.11.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/ed/f11c558e8d2e02fba6057dacd9e92a71557359a80bd5355452310b89f40f/uv-0.11.3.tar.gz", hash = "sha256:6a6fcaf1fec28bbbdf0dfc5a0a6e34be4cea08c6287334b08c24cf187300f20d", size = 4027684, upload-time = "2026-04-01T21:47:22.096Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/93/4f04c49fd6046a18293de341d795ded3b9cbd95db261d687e26db0f11d1e/uv-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:deb533e780e8181e0859c68c84f546620072cd1bd827b38058cb86ebfba9bb7d", size = 23337334, upload-time = "2026-04-01T21:46:47.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/4b/c44fd3fbc80ac2f81e2ad025d235c820aac95b228076da85be3f5d509781/uv-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d2b3b0fa1693880ca354755c216ae1c65dd938a4f1a24374d0c3f4b9538e0ee6", size = 22940169, upload-time = "2026-04-01T21:47:32.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c7/7d01be259a47d42fa9e80adcb7a829d81e7c376aa8fa1b714f31d7dfc226/uv-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71f5d0b9e73daa5d8a7e2db3fa2e22a4537d24bb4fe78130db797280280d4edc", size = 21473579, upload-time = "2026-04-01T21:47:25.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/71/fffcd890290a4639a3799cf3f3e87947c10d1b0de19eba3cf837cb418dd8/uv-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:55ba578752f29a3f2b22879b22a162edad1454e3216f3ca4694fdbd4093a6822", size = 23132691, upload-time = "2026-04-01T21:47:44.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/7b/1ac9e1f753a19b6252434f0bbe96efdcc335cd74677f4c6f431a7c916114/uv-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3b1fe09d5e1d8e19459cd28d7825a3b66ef147b98328345bad6e17b87c4fea48", size = 22955764, upload-time = "2026-04-01T21:46:51.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/51/1a6010a681a3c3e0a8ec99737ba2d0452194dc372a5349a9267873261c02/uv-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:088165b9eed981d2c2a58566cc75dd052d613e47c65e2416842d07308f793a6f", size = 22966245, upload-time = "2026-04-01T21:47:07.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/74/1a1b0712daead7e85f56d620afe96fe166a04b615524c14027b4edd39b82/uv-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef0ae8ee2988928092616401ec7f473612b8e9589fe1567452c45dbc56840f85", size = 24623370, upload-time = "2026-04-01T21:47:03.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/62/5c3aa5e7bd2744810e50ad72a5951386ec84a513e109b1b5cb7ec442f3b6/uv-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6708827ecb846d00c5512a7e4dc751c2e27b92e9bd55a0be390561ac68930c32", size = 25142735, upload-time = "2026-04-01T21:46:55.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ab/6266a04980e0877af5518762adfe23a0c1ab0b801ae3099a2e7b74e34411/uv-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df030ea7563e99c09854e1bc82ab743dfa2d0ba18976e6861979cb40d04dba7", size = 24512083, upload-time = "2026-04-01T21:46:43.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/be/7c66d350f833eb437f9aa0875655cc05e07b441e3f4a770f8bced56133f7/uv-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fde893b5ab9f6997fe357138e794bac09d144328052519fbbe2e6f72145e457", size = 24589293, upload-time = "2026-04-01T21:47:11.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/4f/22ada41564a8c8c36653fc86f89faae4c54a4cdd5817bda53764a3eb352d/uv-0.11.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:45006bcd9e8718248a23ab81448a5beb46a72a9dd508e3212d6f3b8c63aeb88a", size = 23214854, upload-time = "2026-04-01T21:46:59.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/18/8669840657fea9fd668739dec89643afe1061c023c1488228b02f79a2399/uv-0.11.3-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:089b9d338a64463956b6fee456f03f73c9a916479bdb29009600781dc1e1d2a7", size = 23914434, upload-time = "2026-04-01T21:47:29.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/0d/c59f24b3a1ae5f377aa6fd9653562a0968ea6be946fe35761871a0072919/uv-0.11.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3ff461335888336467402cc5cb792c911df95dd0b52e369182cfa4c902bb21f4", size = 23971481, upload-time = "2026-04-01T21:47:48.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/7d/f83ed79921310ef216ed6d73fcd3822dff4b66749054fb97e09b7bd5901e/uv-0.11.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:a62e29277efd39c35caf4a0fe739c4ebeb14d4ce4f02271f3f74271d608061ff", size = 23784797, upload-time = "2026-04-01T21:47:40.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/19/3ff3539c44ca7dc2aa87b021d4a153ba6a72866daa19bf91c289e4318f95/uv-0.11.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ebccdcdebd2b288925f0f7c18c39705dc783175952eacaf94912b01d3b381b86", size = 24794606, upload-time = "2026-04-01T21:47:36.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/e5/e676454bb7cc5dcf5c4637ed3ef0ff97309d84a149b832a4dea53f04c0ab/uv-0.11.3-py3-none-win32.whl", hash = "sha256:794aae3bab141eafbe37c51dc5dd0139658a755a6fa9cc74d2dbd7c71dcc4826", size = 22573432, upload-time = "2026-04-01T21:47:15.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/a0/95d22d524bd3b4708043d65035f02fc9656e5fb6e0aaef73510313b1641b/uv-0.11.3-py3-none-win_amd64.whl", hash = "sha256:68fda574f2e5e7536a2b747dcea88329a71aad7222317e8f4717d0af8f99fbd4", size = 24969508, upload-time = "2026-04-01T21:47:19.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/6d/3f0b90a06e8c4594e11f813651756d6896de6dd4461f554fd7e4984a1c4f/uv-0.11.3-py3-none-win_arm64.whl", hash = "sha256:92ffc4d521ab2c4738ef05d8ef26f2750e26d31f3ad5611cdfefc52445be9ace", size = 23488911, upload-time = "2026-04-01T21:47:52.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.checkpoint.base import (
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
)
|
||||
|
||||
QUEUE_PUT_TIMEOUT = 0.05
|
||||
CHECKPOINT_BACKLOG_ENV_VAR = "LANGGRAPH_CHECKPOINT_BACKLOG"
|
||||
DEFAULT_CHECKPOINT_BACKLOG = 10
|
||||
|
||||
|
||||
def _resolve_checkpoint_backlog() -> int:
|
||||
if raw := os.getenv(CHECKPOINT_BACKLOG_ENV_VAR):
|
||||
try:
|
||||
backlog = int(raw)
|
||||
except ValueError:
|
||||
return DEFAULT_CHECKPOINT_BACKLOG
|
||||
if backlog > 0:
|
||||
return backlog
|
||||
return DEFAULT_CHECKPOINT_BACKLOG
|
||||
|
||||
|
||||
CHECKPOINT_BACKLOG = _resolve_checkpoint_backlog()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CheckpointRequest:
|
||||
config: RunnableConfig
|
||||
checkpoint: Checkpoint
|
||||
metadata: CheckpointMetadata
|
||||
new_versions: ChannelVersions
|
||||
|
||||
|
||||
def _raise(error: BaseException) -> None:
|
||||
raise error
|
||||
|
||||
|
||||
class SyncCheckpointWriter(AbstractContextManager):
|
||||
__slots__ = ("put", "queue", "error", "closed", "thread")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
put: Callable[
|
||||
[RunnableConfig, Checkpoint, CheckpointMetadata, ChannelVersions], Any
|
||||
],
|
||||
*,
|
||||
max_pending: int | None = None,
|
||||
) -> None:
|
||||
self.put = put
|
||||
max_pending = CHECKPOINT_BACKLOG if max_pending is None else max_pending
|
||||
self.queue: queue.Queue[CheckpointRequest | None] = queue.Queue(max_pending)
|
||||
self.error: BaseException | None = None
|
||||
self.closed = False
|
||||
self.thread = threading.Thread(
|
||||
target=self._run,
|
||||
name="langgraph-checkpoint-writer",
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
def __enter__(self) -> SyncCheckpointWriter:
|
||||
self.thread.start()
|
||||
return self
|
||||
|
||||
def submit(self, request: CheckpointRequest) -> None:
|
||||
self._ensure_open()
|
||||
while True:
|
||||
self._raise_if_broken()
|
||||
try:
|
||||
self.queue.put(request, timeout=QUEUE_PUT_TIMEOUT)
|
||||
except queue.Full:
|
||||
continue
|
||||
else:
|
||||
self._raise_if_broken()
|
||||
return
|
||||
|
||||
def _run(self) -> None:
|
||||
while True:
|
||||
item = self.queue.get()
|
||||
if item is None:
|
||||
return
|
||||
try:
|
||||
self.put(
|
||||
item.config,
|
||||
item.checkpoint,
|
||||
item.metadata,
|
||||
item.new_versions,
|
||||
)
|
||||
except BaseException as exc:
|
||||
self.error = exc
|
||||
return
|
||||
|
||||
def _ensure_open(self) -> None:
|
||||
if self.closed:
|
||||
raise RuntimeError("Checkpoint writer is closed")
|
||||
|
||||
def _raise_if_broken(self) -> None:
|
||||
if self.error is not None:
|
||||
_raise(self.error)
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
self.closed = True
|
||||
while self.thread.is_alive():
|
||||
if self.error is not None:
|
||||
break
|
||||
try:
|
||||
self.queue.put(None, timeout=QUEUE_PUT_TIMEOUT)
|
||||
except queue.Full:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
self.thread.join()
|
||||
if exc_type is None and self.error is not None:
|
||||
_raise(self.error)
|
||||
return None
|
||||
|
||||
|
||||
class AsyncCheckpointWriter(AbstractAsyncContextManager):
|
||||
__slots__ = ("put", "queue", "error", "closed", "task")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
put: Callable[
|
||||
[RunnableConfig, Checkpoint, CheckpointMetadata, ChannelVersions], Any
|
||||
],
|
||||
*,
|
||||
max_pending: int | None = None,
|
||||
) -> None:
|
||||
self.put = put
|
||||
max_pending = CHECKPOINT_BACKLOG if max_pending is None else max_pending
|
||||
self.queue: asyncio.Queue[CheckpointRequest | None] = asyncio.Queue(max_pending)
|
||||
self.error: BaseException | None = None
|
||||
self.closed = False
|
||||
self.task: asyncio.Task[None] | None = None
|
||||
|
||||
async def __aenter__(self) -> AsyncCheckpointWriter:
|
||||
self.task = asyncio.create_task(self._run(), name="langgraph-checkpoint-writer")
|
||||
return self
|
||||
|
||||
async def submit(self, request: CheckpointRequest) -> None:
|
||||
self._ensure_open()
|
||||
while True:
|
||||
self._raise_if_broken()
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.queue.put(request),
|
||||
timeout=QUEUE_PUT_TIMEOUT,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
else:
|
||||
self._raise_if_broken()
|
||||
return
|
||||
|
||||
async def _run(self) -> None:
|
||||
while True:
|
||||
item = await self.queue.get()
|
||||
if item is None:
|
||||
return
|
||||
try:
|
||||
await self.put(
|
||||
item.config,
|
||||
item.checkpoint,
|
||||
item.metadata,
|
||||
item.new_versions,
|
||||
)
|
||||
except BaseException as exc:
|
||||
self.error = exc
|
||||
return
|
||||
|
||||
def _ensure_open(self) -> None:
|
||||
if self.closed:
|
||||
raise RuntimeError("Checkpoint writer is closed")
|
||||
|
||||
def _raise_if_broken(self) -> None:
|
||||
if self.error is not None:
|
||||
_raise(self.error)
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.closed = True
|
||||
while self.task is not None and not self.task.done():
|
||||
if self.error is not None:
|
||||
break
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.queue.put(None),
|
||||
timeout=QUEUE_PUT_TIMEOUT,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
if self.task is not None:
|
||||
await self.task
|
||||
if exc_type is None and self.error is not None:
|
||||
_raise(self.error)
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import binascii
|
||||
import concurrent.futures
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Callable, Iterator, Mapping, Sequence
|
||||
from contextlib import (
|
||||
@@ -27,7 +26,6 @@ from langgraph.cache.base import BaseCache
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
@@ -97,6 +95,11 @@ from langgraph.pregel._checkpoint import (
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.pregel._checkpoint_writer import (
|
||||
AsyncCheckpointWriter,
|
||||
CheckpointRequest,
|
||||
SyncCheckpointWriter,
|
||||
)
|
||||
from langgraph.pregel._executor import (
|
||||
AsyncBackgroundExecutor,
|
||||
BackgroundExecutor,
|
||||
@@ -172,19 +175,6 @@ class PregelLoop:
|
||||
checkpointer_get_next_version: GetNextVersion
|
||||
checkpointer_put_writes: Callable[[RunnableConfig, WritesT, str], Any] | None
|
||||
checkpointer_put_writes_accepts_task_path: bool
|
||||
_checkpointer_put_after_previous: (
|
||||
Callable[
|
||||
[
|
||||
concurrent.futures.Future | None,
|
||||
RunnableConfig,
|
||||
Checkpoint,
|
||||
str,
|
||||
ChannelVersions,
|
||||
],
|
||||
Any,
|
||||
]
|
||||
| None
|
||||
)
|
||||
_migrate_checkpoint: Callable[[Checkpoint], None] | None
|
||||
submit: Submit
|
||||
channels: Mapping[str, BaseChannel]
|
||||
@@ -536,7 +526,7 @@ class PregelLoop:
|
||||
)
|
||||
|
||||
# produce debug output
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
if self.checkpointer is not None:
|
||||
self._emit(
|
||||
"checkpoints",
|
||||
map_debug_checkpoint,
|
||||
@@ -582,7 +572,7 @@ class PregelLoop:
|
||||
|
||||
return True
|
||||
|
||||
def after_tick(self) -> None:
|
||||
def _after_tick(self) -> CheckpointRequest | None:
|
||||
# finish superstep
|
||||
writes = [w for t in self.tasks.values() for w in t.writes]
|
||||
# all tasks have finished
|
||||
@@ -607,15 +597,7 @@ class PregelLoop:
|
||||
# only replay (re-execute) done tasks on the first tick
|
||||
self.is_replaying = False
|
||||
# save checkpoint
|
||||
self._put_checkpoint({"source": "loop"})
|
||||
# after execution, check if we should interrupt
|
||||
if self.interrupt_after and should_interrupt(
|
||||
self.checkpoint, self.interrupt_after, self.tasks.values()
|
||||
):
|
||||
self.status = "interrupt_after"
|
||||
raise GraphInterrupt()
|
||||
# unset resuming flag
|
||||
self.config[CONF].pop(CONFIG_KEY_RESUMING, None)
|
||||
return self._prepare_checkpoint({"source": "loop"})
|
||||
|
||||
def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
raise NotImplementedError
|
||||
@@ -664,7 +646,7 @@ class PregelLoop:
|
||||
|
||||
def _first(
|
||||
self, *, input_keys: str | Sequence[str], updated_channels: set[str] | None
|
||||
) -> set[str] | None:
|
||||
) -> tuple[set[str] | None, CheckpointRequest | None]:
|
||||
# Resuming from a previous checkpoint requires two things:
|
||||
# 1. A prior checkpoint exists (channel_versions is non-empty)
|
||||
# 2. The input signals continuation (not a fresh run with new input)
|
||||
@@ -758,6 +740,7 @@ class PregelLoop:
|
||||
)
|
||||
if updated_channels is not None:
|
||||
updated_channels.update(null_updated_channels)
|
||||
checkpoint_request = None
|
||||
# proceed past previous checkpoint
|
||||
if is_resuming:
|
||||
self.checkpoint["versions_seen"].setdefault(INTERRUPT, {})
|
||||
@@ -800,7 +783,7 @@ class PregelLoop:
|
||||
)
|
||||
# save input checkpoint
|
||||
self.updated_channels = updated_channels
|
||||
self._put_checkpoint({"source": "input"})
|
||||
checkpoint_request = self._prepare_checkpoint({"source": "input"})
|
||||
elif CONFIG_KEY_RESUMING not in configurable:
|
||||
raise EmptyInputError(f"Received no input for {input_keys}")
|
||||
# Propagate resuming and replaying flags to subgraphs.
|
||||
@@ -832,9 +815,11 @@ class PregelLoop:
|
||||
self.status = "pending"
|
||||
if is_resuming:
|
||||
self._push_graph_lifecycle_event("resume")
|
||||
return updated_channels
|
||||
return updated_channels, checkpoint_request
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
def _prepare_checkpoint(
|
||||
self, metadata: CheckpointMetadata
|
||||
) -> CheckpointRequest | None:
|
||||
# assign step and parents
|
||||
exiting = metadata is self.checkpoint_metadata
|
||||
if exiting and self.checkpoint["id"] == self.checkpoint_id_saved:
|
||||
@@ -844,8 +829,7 @@ class PregelLoop:
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
self.checkpoint_metadata = metadata
|
||||
# do checkpoint?
|
||||
do_checkpoint = self._checkpointer_put_after_previous is not None and (
|
||||
do_checkpoint = self.checkpointer is not None and (
|
||||
exiting or self.durability != "exit"
|
||||
)
|
||||
# create new checkpoint
|
||||
@@ -867,9 +851,8 @@ class PregelLoop:
|
||||
for value in self.checkpoint["channel_values"][TASKS]
|
||||
]
|
||||
self.checkpoint["channel_values"][TASKS] = sanitized_tasks
|
||||
# bail if no checkpointer
|
||||
|
||||
if do_checkpoint and self._checkpointer_put_after_previous is not None:
|
||||
request = None
|
||||
if do_checkpoint:
|
||||
self.prev_checkpoint_config = (
|
||||
self.checkpoint_config
|
||||
if CONFIG_KEY_CHECKPOINT_ID in self.checkpoint_config[CONF]
|
||||
@@ -891,17 +874,11 @@ class PregelLoop:
|
||||
self.checkpoint_previous_versions, channel_versions
|
||||
)
|
||||
self.checkpoint_previous_versions = channel_versions
|
||||
|
||||
# save it, without blocking
|
||||
# if there's a previous checkpoint save in progress, wait for it
|
||||
# ensuring checkpointers receive checkpoints in order
|
||||
self._put_checkpoint_fut = self.submit(
|
||||
self._checkpointer_put_after_previous,
|
||||
getattr(self, "_put_checkpoint_fut", None),
|
||||
self.checkpoint_config,
|
||||
copy_checkpoint(self.checkpoint),
|
||||
self.checkpoint_metadata,
|
||||
new_versions,
|
||||
request = CheckpointRequest(
|
||||
config=self.checkpoint_config,
|
||||
checkpoint=copy_checkpoint(self.checkpoint),
|
||||
metadata=self.checkpoint_metadata,
|
||||
new_versions=new_versions,
|
||||
)
|
||||
self.checkpoint_config = {
|
||||
**self.checkpoint_config,
|
||||
@@ -913,24 +890,13 @@ class PregelLoop:
|
||||
if not exiting:
|
||||
# increment step
|
||||
self.step += 1
|
||||
return request
|
||||
|
||||
def _suppress_interrupt(
|
||||
def _finalize_suppress(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
# persist current checkpoint and writes
|
||||
if self.durability == "exit" and (
|
||||
# if it's a top graph
|
||||
not self.is_nested
|
||||
# or a nested graph with error or interrupt
|
||||
or exc_value is not None
|
||||
# or a nested graph with checkpointer=True
|
||||
or all(NS_END not in part for part in self.checkpoint_ns)
|
||||
):
|
||||
self._put_checkpoint(self.checkpoint_metadata)
|
||||
self._put_pending_writes()
|
||||
# suppress interrupt
|
||||
if isinstance(exc_value, GraphInterrupt) and not self.is_nested:
|
||||
interrupt = exc_value
|
||||
@@ -975,6 +941,7 @@ class PregelLoop:
|
||||
elif exc_type is None:
|
||||
# save final output
|
||||
self.output = read_channels(self.channels, self.output_keys)
|
||||
return None
|
||||
|
||||
def _emit(
|
||||
self,
|
||||
@@ -1122,26 +1089,57 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
)
|
||||
else:
|
||||
self.checkpointer_get_next_version = increment
|
||||
self._checkpointer_put_after_previous = None # type: ignore[assignment]
|
||||
self.checkpointer_put_writes = None
|
||||
self.checkpointer_put_writes_accepts_task_path = False
|
||||
self._checkpoint_writer: SyncCheckpointWriter | None = None
|
||||
|
||||
def _checkpointer_put_after_previous(
|
||||
self,
|
||||
prev: concurrent.futures.Future | None,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
try:
|
||||
if prev is not None:
|
||||
prev.result()
|
||||
finally:
|
||||
def _dispatch_checkpoint_request(self, request: CheckpointRequest) -> None:
|
||||
if self.durability == "async" and self._checkpoint_writer is not None:
|
||||
self._checkpoint_writer.submit(request)
|
||||
else:
|
||||
cast(BaseCheckpointSaver, self.checkpointer).put(
|
||||
config, checkpoint, metadata, new_versions
|
||||
request.config,
|
||||
request.checkpoint,
|
||||
request.metadata,
|
||||
request.new_versions,
|
||||
)
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
if request := self._prepare_checkpoint(metadata):
|
||||
self._dispatch_checkpoint_request(request)
|
||||
|
||||
def _suppress_interrupt(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
# persist current checkpoint and writes
|
||||
if self.durability == "exit" and (
|
||||
# if it's a top graph
|
||||
not self.is_nested
|
||||
# or a nested graph with error or interrupt
|
||||
or exc_value is not None
|
||||
# or a nested graph with checkpointer=True
|
||||
or all(NS_END not in part for part in self.checkpoint_ns)
|
||||
):
|
||||
self._put_checkpoint(self.checkpoint_metadata)
|
||||
self._put_pending_writes()
|
||||
return self._finalize_suppress(exc_type, exc_value)
|
||||
|
||||
def after_tick(self) -> None:
|
||||
# compute tick state and save checkpoint
|
||||
if request := self._after_tick():
|
||||
self._dispatch_checkpoint_request(request)
|
||||
# after execution, check if we should interrupt
|
||||
if self.interrupt_after and should_interrupt(
|
||||
self.checkpoint, self.interrupt_after, self.tasks.values()
|
||||
):
|
||||
self.status = "interrupt_after"
|
||||
raise GraphInterrupt()
|
||||
# unset resuming flag
|
||||
self.config[CONF].pop(CONFIG_KEY_RESUMING, None)
|
||||
|
||||
def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
if self.cache is None:
|
||||
return ()
|
||||
@@ -1237,6 +1235,10 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
else []
|
||||
)
|
||||
self.submit = self.stack.enter_context(BackgroundExecutor(self.config))
|
||||
if self.checkpointer is not None and self.durability == "async":
|
||||
self._checkpoint_writer = self.stack.enter_context(
|
||||
SyncCheckpointWriter(self.checkpointer.put)
|
||||
)
|
||||
self.channels, self.managed = channels_from_checkpoint(
|
||||
self.specs, self.checkpoint
|
||||
)
|
||||
@@ -1245,12 +1247,14 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
self.step = self.checkpoint_metadata["step"] + 1
|
||||
self.stop = self.step + self.config["recursion_limit"] + 1
|
||||
self.checkpoint_previous_versions = self.checkpoint["channel_versions"].copy()
|
||||
self.updated_channels = self._first(
|
||||
self.updated_channels, checkpoint_request = self._first(
|
||||
input_keys=self.input_keys,
|
||||
updated_channels=set(self.checkpoint.get("updated_channels")) # type: ignore[arg-type]
|
||||
if self.checkpoint.get("updated_channels")
|
||||
else None,
|
||||
)
|
||||
if checkpoint_request is not None:
|
||||
self._dispatch_checkpoint_request(checkpoint_request)
|
||||
|
||||
return self
|
||||
|
||||
@@ -1321,26 +1325,34 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
)
|
||||
else:
|
||||
self.checkpointer_get_next_version = increment
|
||||
self._checkpointer_put_after_previous = None # type: ignore[assignment]
|
||||
self.checkpointer_put_writes = None
|
||||
self.checkpointer_put_writes_accepts_task_path = False
|
||||
self._checkpoint_writer: AsyncCheckpointWriter | None = None
|
||||
|
||||
async def _checkpointer_put_after_previous(
|
||||
self,
|
||||
prev: asyncio.Task | None,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
try:
|
||||
if prev is not None:
|
||||
await prev
|
||||
finally:
|
||||
async def _dispatch_checkpoint_request(self, request: CheckpointRequest) -> None:
|
||||
if self.durability == "async" and self._checkpoint_writer is not None:
|
||||
await self._checkpoint_writer.submit(request)
|
||||
else:
|
||||
await cast(BaseCheckpointSaver, self.checkpointer).aput(
|
||||
config, checkpoint, metadata, new_versions
|
||||
request.config,
|
||||
request.checkpoint,
|
||||
request.metadata,
|
||||
request.new_versions,
|
||||
)
|
||||
|
||||
async def aafter_tick(self) -> None:
|
||||
# compute tick state and save checkpoint
|
||||
if request := self._after_tick():
|
||||
await self._dispatch_checkpoint_request(request)
|
||||
# after execution, check if we should interrupt
|
||||
if self.interrupt_after and should_interrupt(
|
||||
self.checkpoint, self.interrupt_after, self.tasks.values()
|
||||
):
|
||||
self.status = "interrupt_after"
|
||||
raise GraphInterrupt()
|
||||
# unset resuming flag
|
||||
self.config[CONF].pop(CONFIG_KEY_RESUMING, None)
|
||||
|
||||
async def amatch_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
if self.cache is None:
|
||||
return []
|
||||
@@ -1385,6 +1397,32 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
},
|
||||
)
|
||||
|
||||
async def _asuppress_interrupt(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
"""Async counterpart of SyncPregelLoop._suppress_interrupt.
|
||||
|
||||
Uses async checkpoint dispatch so we don't block the event loop
|
||||
(the sync version was previously used here via stack.push, which
|
||||
would call the blocking checkpointer.put instead of aput).
|
||||
"""
|
||||
# persist current checkpoint and writes
|
||||
if self.durability == "exit" and (
|
||||
# if it's a top graph
|
||||
not self.is_nested
|
||||
# or a nested graph with error or interrupt
|
||||
or exc_value is not None
|
||||
# or a nested graph with checkpointer=True
|
||||
or all(NS_END not in part for part in self.checkpoint_ns)
|
||||
):
|
||||
if request := self._prepare_checkpoint(self.checkpoint_metadata):
|
||||
await self._dispatch_checkpoint_request(request)
|
||||
self._put_pending_writes()
|
||||
return self._finalize_suppress(exc_type, exc_value)
|
||||
|
||||
# context manager
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
@@ -1441,20 +1479,26 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
self.submit = await self.stack.enter_async_context(
|
||||
AsyncBackgroundExecutor(self.config)
|
||||
)
|
||||
if self.checkpointer is not None and self.durability == "async":
|
||||
self._checkpoint_writer = await self.stack.enter_async_context(
|
||||
AsyncCheckpointWriter(self.checkpointer.aput)
|
||||
)
|
||||
self.channels, self.managed = channels_from_checkpoint(
|
||||
self.specs, self.checkpoint
|
||||
)
|
||||
self.stack.push(self._suppress_interrupt)
|
||||
self.stack.push_async_exit(self._asuppress_interrupt)
|
||||
self.status = "input"
|
||||
self.step = self.checkpoint_metadata["step"] + 1
|
||||
self.stop = self.step + self.config["recursion_limit"] + 1
|
||||
self.checkpoint_previous_versions = self.checkpoint["channel_versions"].copy()
|
||||
self.updated_channels = self._first(
|
||||
self.updated_channels, checkpoint_request = self._first(
|
||||
input_keys=self.input_keys,
|
||||
updated_channels=set(self.checkpoint.get("updated_channels")) # type: ignore[arg-type]
|
||||
if self.checkpoint.get("updated_channels")
|
||||
else None,
|
||||
)
|
||||
if checkpoint_request is not None:
|
||||
await self._dispatch_checkpoint_request(checkpoint_request)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@@ -2775,9 +2775,6 @@ class Pregel(
|
||||
)
|
||||
loop.after_tick()
|
||||
emit_graph_lifecycle_events(loop)
|
||||
# wait for checkpoint
|
||||
if durability_ == "sync":
|
||||
loop._put_checkpoint_fut.result()
|
||||
emit_graph_lifecycle_events(loop)
|
||||
# emit output
|
||||
yield from _output(
|
||||
@@ -3196,11 +3193,8 @@ class Pregel(
|
||||
_state_mapper,
|
||||
):
|
||||
yield o
|
||||
loop.after_tick()
|
||||
await loop.aafter_tick()
|
||||
await aemit_graph_lifecycle_events(loop)
|
||||
# wait for checkpoint
|
||||
if durability_ == "sync":
|
||||
await cast(asyncio.Future, loop._put_checkpoint_fut)
|
||||
finally:
|
||||
# ensure waiter doesn't remain pending on cancel/shutdown
|
||||
if _cleanup_waiter is not None:
|
||||
@@ -3715,15 +3709,14 @@ def _coerce_checkpoint_values(payload: Any, mapper: Callable[[Any], Any]) -> Non
|
||||
def _build_server_info(
|
||||
config: RunnableConfig, parent_runtime: Runtime[Any]
|
||||
) -> ServerInfo | None:
|
||||
"""Build ServerInfo from config metadata and configurable.
|
||||
"""Build ServerInfo from config configurable.
|
||||
|
||||
The server puts assistant_id/graph_id in config metadata and the
|
||||
The server puts assistant_id/graph_id in config configurable and the
|
||||
authenticated user dict in configurable["langgraph_auth_user"].
|
||||
"""
|
||||
metadata = config.get("metadata") or {}
|
||||
configurable = config.get(CONF) or {}
|
||||
assistant_id = metadata.get("assistant_id")
|
||||
graph_id = metadata.get("graph_id")
|
||||
assistant_id = configurable.get("assistant_id")
|
||||
graph_id = configurable.get("graph_id")
|
||||
|
||||
# Read authenticated user from configurable (set by LangGraph Server).
|
||||
# We prefer isinstance(BaseUser) but fall back to hasattr("identity")
|
||||
|
||||
@@ -54,6 +54,11 @@ from langgraph.pregel import (
|
||||
NodeBuilder,
|
||||
Pregel,
|
||||
)
|
||||
from langgraph.pregel._checkpoint_writer import (
|
||||
CHECKPOINT_BACKLOG_ENV_VAR,
|
||||
DEFAULT_CHECKPOINT_BACKLOG,
|
||||
_resolve_checkpoint_backlog,
|
||||
)
|
||||
from langgraph.pregel._loop import SyncPregelLoop
|
||||
from langgraph.pregel._runner import PregelRunner
|
||||
from langgraph.types import (
|
||||
@@ -3726,6 +3731,7 @@ def test_repeat_condition(snapshot: SnapshotAssertion) -> None:
|
||||
"end": END,
|
||||
},
|
||||
)
|
||||
|
||||
workflow.add_conditional_edges(
|
||||
"Chart Generator",
|
||||
router,
|
||||
@@ -3749,6 +3755,90 @@ def test_repeat_condition(snapshot: SnapshotAssertion) -> None:
|
||||
assert app.get_graph().draw_mermaid(with_styles=False) == snapshot
|
||||
|
||||
|
||||
def test_sync_durability_applies_checkpoint_backpressure() -> None:
|
||||
first_put_started = threading.Event()
|
||||
release_first_put = threading.Event()
|
||||
put_calls = 0
|
||||
visited: list[int] = []
|
||||
result: dict[str, Any] = {}
|
||||
error: dict[str, BaseException] = {}
|
||||
|
||||
class SlowFirstPutCheckpointer(InMemorySaver):
|
||||
def put(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: Any,
|
||||
) -> RunnableConfig:
|
||||
nonlocal put_calls
|
||||
put_calls += 1
|
||||
if put_calls == 1:
|
||||
first_put_started.set()
|
||||
release_first_put.wait()
|
||||
return super().put(config, checkpoint, metadata, new_versions)
|
||||
|
||||
class State(TypedDict):
|
||||
counter: int
|
||||
|
||||
def increment(state: State) -> State:
|
||||
visited.append(state["counter"])
|
||||
return {"counter": state["counter"] + 1}
|
||||
|
||||
def should_continue(state: State) -> str:
|
||||
return "loop" if state["counter"] < 4 else "done"
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("increment", increment)
|
||||
builder.add_edge(START, "increment")
|
||||
builder.add_conditional_edges(
|
||||
"increment", should_continue, {"loop": "increment", "done": END}
|
||||
)
|
||||
|
||||
graph = builder.compile(checkpointer=SlowFirstPutCheckpointer())
|
||||
|
||||
def invoke() -> None:
|
||||
try:
|
||||
result["value"] = graph.invoke(
|
||||
{"counter": 0},
|
||||
{"configurable": {"thread_id": "1"}},
|
||||
durability="async",
|
||||
)
|
||||
except BaseException as exc:
|
||||
error["value"] = exc
|
||||
|
||||
thread = threading.Thread(target=invoke)
|
||||
thread.start()
|
||||
|
||||
assert first_put_started.wait(timeout=1)
|
||||
time.sleep(0.05)
|
||||
|
||||
assert thread.is_alive()
|
||||
assert len(visited) <= DEFAULT_CHECKPOINT_BACKLOG + 1
|
||||
|
||||
release_first_put.set()
|
||||
thread.join(timeout=1)
|
||||
|
||||
assert not thread.is_alive()
|
||||
assert "value" not in error
|
||||
assert result["value"] == {"counter": 4}
|
||||
|
||||
|
||||
def test_checkpoint_backlog_uses_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv(CHECKPOINT_BACKLOG_ENV_VAR, "7")
|
||||
assert _resolve_checkpoint_backlog() == 7
|
||||
|
||||
|
||||
def test_checkpoint_backlog_invalid_env_uses_default(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv(CHECKPOINT_BACKLOG_ENV_VAR, "not-an-int")
|
||||
assert _resolve_checkpoint_backlog() == DEFAULT_CHECKPOINT_BACKLOG
|
||||
|
||||
monkeypatch.setenv(CHECKPOINT_BACKLOG_ENV_VAR, "0")
|
||||
assert _resolve_checkpoint_backlog() == DEFAULT_CHECKPOINT_BACKLOG
|
||||
|
||||
|
||||
def test_checkpoint_metadata(sync_checkpointer: BaseCheckpointSaver) -> None:
|
||||
"""This test verifies that a run's configurable fields are merged with the
|
||||
previous checkpoint config for each step in the run.
|
||||
|
||||
@@ -53,6 +53,7 @@ from langgraph.func import entrypoint, task
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from langgraph.graph.message import MessagesState, add_messages
|
||||
from langgraph.pregel import NodeBuilder, Pregel
|
||||
from langgraph.pregel._checkpoint_writer import DEFAULT_CHECKPOINT_BACKLOG
|
||||
from langgraph.pregel._loop import AsyncPregelLoop
|
||||
from langgraph.pregel._runner import PregelRunner
|
||||
from langgraph.types import (
|
||||
@@ -484,6 +485,61 @@ async def test_checkpoint_put_after_cancellation_stream_events_anext() -> None:
|
||||
assert False, "Task should be cancelled"
|
||||
|
||||
|
||||
async def test_async_durability_applies_checkpoint_backpressure() -> None:
|
||||
first_put_started = asyncio.Event()
|
||||
release_first_put = asyncio.Event()
|
||||
put_calls = 0
|
||||
visited: list[int] = []
|
||||
|
||||
class SlowFirstPutCheckpointer(InMemorySaver):
|
||||
async def aput(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
nonlocal put_calls
|
||||
put_calls += 1
|
||||
if put_calls == 1:
|
||||
first_put_started.set()
|
||||
await release_first_put.wait()
|
||||
return await super().aput(config, checkpoint, metadata, new_versions)
|
||||
|
||||
class State(TypedDict):
|
||||
counter: int
|
||||
|
||||
def increment(state: State) -> State:
|
||||
visited.append(state["counter"])
|
||||
return {"counter": state["counter"] + 1}
|
||||
|
||||
def should_continue(state: State) -> str:
|
||||
return "loop" if state["counter"] < 4 else "done"
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("increment", increment)
|
||||
builder.add_edge(START, "increment")
|
||||
builder.add_conditional_edges(
|
||||
"increment", should_continue, {"loop": "increment", "done": END}
|
||||
)
|
||||
|
||||
graph = builder.compile(checkpointer=SlowFirstPutCheckpointer())
|
||||
task = asyncio.create_task(
|
||||
graph.ainvoke(
|
||||
{"counter": 0}, {"configurable": {"thread_id": "1"}}, durability="async"
|
||||
)
|
||||
)
|
||||
|
||||
await first_put_started.wait()
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
assert not task.done()
|
||||
assert len(visited) <= DEFAULT_CHECKPOINT_BACKLOG + 1
|
||||
|
||||
release_first_put.set()
|
||||
assert await task == {"counter": 4}
|
||||
|
||||
|
||||
async def test_node_cancellation_on_external_cancel() -> None:
|
||||
inner_task_cancelled = False
|
||||
|
||||
|
||||
@@ -501,13 +501,13 @@ async def test_execution_info_populated_in_graph_async() -> None:
|
||||
assert isinstance(info.node_first_attempt_time, float)
|
||||
|
||||
|
||||
def test_server_info_from_metadata() -> None:
|
||||
"""server_info is built from assistant_id/graph_id in config metadata."""
|
||||
def test_server_info_from_configurable() -> None:
|
||||
"""server_info is built from assistant_id/graph_id in config configurable."""
|
||||
captured: dict[str, Any] = {}
|
||||
compiled = _make_capture_graph(captured)
|
||||
compiled.invoke(
|
||||
{"message": "hi"},
|
||||
config={"metadata": {"assistant_id": "asst-abc", "graph_id": "my-graph"}},
|
||||
config={"configurable": {"assistant_id": "asst-abc", "graph_id": "my-graph"}},
|
||||
)
|
||||
si = captured["server_info"]
|
||||
assert si is not None
|
||||
@@ -516,8 +516,8 @@ def test_server_info_from_metadata() -> None:
|
||||
assert si.user is None
|
||||
|
||||
|
||||
def test_server_info_none_without_metadata() -> None:
|
||||
"""server_info is None when no assistant_id/graph_id in metadata."""
|
||||
def test_server_info_none_without_configurable() -> None:
|
||||
"""server_info is None when no assistant_id/graph_id in configurable."""
|
||||
captured: dict[str, Any] = {}
|
||||
compiled = _make_capture_graph(captured)
|
||||
compiled.invoke({"message": "hi"})
|
||||
@@ -579,8 +579,11 @@ def test_server_info_user_from_auth_user() -> None:
|
||||
compiled.invoke(
|
||||
{"message": "hi"},
|
||||
config={
|
||||
"configurable": {"langgraph_auth_user": proxy},
|
||||
"metadata": {"assistant_id": "asst-proxy", "graph_id": "graph-proxy"},
|
||||
"configurable": {
|
||||
"langgraph_auth_user": proxy,
|
||||
"assistant_id": "asst-proxy",
|
||||
"graph_id": "graph-proxy",
|
||||
},
|
||||
},
|
||||
)
|
||||
si = captured["server_info"]
|
||||
|
||||
Reference in New Issue
Block a user