diff --git a/libs/cli/js-monorepo-example/yarn.lock b/libs/cli/js-monorepo-example/yarn.lock index eeeef98e3..83cb3dd06 100644 --- a/libs/cli/js-monorepo-example/yarn.lock +++ b/libs/cli/js-monorepo-example/yarn.lock @@ -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== diff --git a/libs/cli/uv.lock b/libs/cli/uv.lock index 8c9392d32..1113fce67 100644 --- a/libs/cli/uv.lock +++ b/libs/cli/uv.lock @@ -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]] diff --git a/libs/langgraph/langgraph/callbacks.py b/libs/langgraph/langgraph/callbacks.py new file mode 100644 index 000000000..d5933b1b6 --- /dev/null +++ b/libs/langgraph/langgraph/callbacks.py @@ -0,0 +1,412 @@ +"""Graph lifecycle callback interfaces and event payloads. + +This module defines the public callback surface for observing LangGraph-specific +lifecycle transitions such as interrupt and resume. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any, Literal, TypeAlias, TypeVar +from uuid import UUID + +from langchain_core.callbacks import BaseCallbackHandler, BaseCallbackManager +from langchain_core.callbacks.manager import ahandle_event, handle_event +from langchain_core.runnables import RunnableConfig + +from langgraph.types import Interrupt + +__all__ = ( + "GraphCallbackHandler", + "GraphInterruptEvent", + "GraphLifecycleEvent", + "GraphLifecycleStatus", + "GraphResumeEvent", + "get_async_graph_callback_manager_for_config", + "get_sync_graph_callback_manager_for_config", +) + + +GraphLifecycleStatus: TypeAlias = Literal[ + "input", + "pending", + "done", + "interrupt_before", + "interrupt_after", + "out_of_steps", +] +"""Allowed lifecycle statuses reported in graph lifecycle callback events.""" + + +@dataclass(frozen=True) +class GraphInterruptEvent: + """Graph lifecycle event emitted when execution pauses for interrupts.""" + + run_id: UUID | None + """Run id for the current graph execution, if available.""" + + status: GraphLifecycleStatus + """Loop status when the interrupt was captured.""" + + checkpoint_id: str + """Checkpoint id associated with the interrupted execution.""" + + checkpoint_ns: tuple[str, ...] + """Checkpoint namespace path for the current graph or subgraph.""" + + interrupts: tuple[Interrupt, ...] + """Interrupt payloads that caused the graph to pause.""" + + +@dataclass(frozen=True) +class GraphResumeEvent: + """Graph lifecycle event emitted when execution resumes from a checkpoint.""" + + run_id: UUID | None + """Run id for the current graph execution, if available.""" + + status: GraphLifecycleStatus + """Loop status when the resume was captured.""" + + checkpoint_id: str + """Checkpoint id the graph resumed from.""" + + checkpoint_ns: tuple[str, ...] + """Checkpoint namespace path for the current graph or subgraph.""" + + +GraphLifecycleEvent: TypeAlias = GraphInterruptEvent | GraphResumeEvent +"""Union of all public graph lifecycle callback event payloads. + +Use this alias when a callback or helper can receive either interrupt or resume +lifecycle events. +""" + + +class GraphCallbackHandler(BaseCallbackHandler): + """Base class for graph-level lifecycle callbacks. + + Subclass this handler to observe graph lifecycle transitions that are + specific to LangGraph execution, rather than generic LangChain runnable + callbacks. + + Instances can be passed through `config["callbacks"]` when invoking a + graph. Only handlers that inherit from `GraphCallbackHandler` receive these + lifecycle events. + """ + + def on_interrupt(self, event: GraphInterruptEvent) -> Any: + """Run when graph execution pauses due to one or more interrupts. + + Args: + event: Interrupt lifecycle event payload. + """ + + def on_resume(self, event: GraphResumeEvent) -> Any: + """Run when graph execution resumes from a persisted checkpoint. + + Args: + event: Resume lifecycle event payload. + """ + + +_MISSING = object() + + +def _filter_graph_handlers( + handlers: list[BaseCallbackHandler], +) -> list[GraphCallbackHandler]: + return [h for h in handlers if isinstance(h, GraphCallbackHandler)] + + +def _init_base_manager( + manager: BaseCallbackManager, + handlers: Sequence[GraphCallbackHandler] | None, + inheritable_handlers: Sequence[GraphCallbackHandler] | None, + parent_run_id: UUID | None, + *, + tags: list[str] | None, + inheritable_tags: list[str] | None, + metadata: dict[str, Any] | None, + inheritable_metadata: dict[str, Any] | None, + run_id: UUID | None, +) -> None: + base_handlers: list[BaseCallbackHandler] = [] + base_inheritable_handlers: list[BaseCallbackHandler] = [] + if handlers is not None: + base_handlers.extend(handlers) + if inheritable_handlers is not None: + base_inheritable_handlers.extend(inheritable_handlers) + BaseCallbackManager.__init__( + manager, + handlers=base_handlers, + inheritable_handlers=base_inheritable_handlers, + parent_run_id=parent_run_id, + tags=tags, + inheritable_tags=inheritable_tags, + metadata=metadata, + inheritable_metadata=inheritable_metadata, + ) + manager.run_id = run_id # type: ignore[attr-defined] + + +def _configure_graph_callbacks( + cls: type[_GraphManagerT], + callbacks: object | None, + *, + run_id: UUID | None, +) -> _GraphManagerT: + if callbacks is None: + return cls(run_id=run_id) + if isinstance(callbacks, cls): + return callbacks.copy(run_id=run_id) + if isinstance(callbacks, (_GraphCallbackManager, _AsyncGraphCallbackManager)): + # Cross-type: extract handlers into the requested cls. + return cls( + handlers=_filter_graph_handlers(callbacks.handlers), + inheritable_handlers=_filter_graph_handlers(callbacks.inheritable_handlers), + parent_run_id=callbacks.parent_run_id, + tags=callbacks.tags.copy(), + inheritable_tags=callbacks.inheritable_tags.copy(), + metadata=callbacks.metadata.copy(), + inheritable_metadata=callbacks.inheritable_metadata.copy(), + run_id=run_id, + ) + if isinstance(callbacks, BaseCallbackManager): + return cls( + handlers=_filter_graph_handlers(callbacks.handlers), + inheritable_handlers=_filter_graph_handlers(callbacks.inheritable_handlers), + parent_run_id=callbacks.parent_run_id, + tags=callbacks.tags.copy(), + inheritable_tags=callbacks.inheritable_tags.copy(), + metadata=callbacks.metadata.copy(), + inheritable_metadata=callbacks.inheritable_metadata.copy(), + run_id=run_id, + ) + if isinstance(callbacks, GraphCallbackHandler): + return cls((callbacks,), run_id=run_id) + if isinstance(callbacks, (str, bytes)) or not isinstance(callbacks, Sequence): + raise TypeError("callbacks must be a handler, sequence, or manager") + return cls(_filter_graph_handlers(list(callbacks)), run_id=run_id) + + +def _copy_graph_manager( + manager: _GraphCallbackManager | _AsyncGraphCallbackManager, + cls: type[_GraphManagerT], + run_id: UUID | None | object, +) -> _GraphManagerT: + resolved_run_id: UUID | None + if run_id is _MISSING: + resolved_run_id = manager.run_id + else: + if run_id is not None and not isinstance(run_id, UUID): + raise TypeError("run_id must be a UUID or None") + resolved_run_id = run_id + + return cls( + handlers=_filter_graph_handlers(manager.handlers), + inheritable_handlers=_filter_graph_handlers(manager.inheritable_handlers), + parent_run_id=manager.parent_run_id, + tags=manager.tags.copy(), + inheritable_tags=manager.inheritable_tags.copy(), + metadata=manager.metadata.copy(), + inheritable_metadata=manager.inheritable_metadata.copy(), + run_id=resolved_run_id, + ) + + +class _GraphCallbackManager(BaseCallbackManager): + """Sync dispatcher for graph lifecycle events.""" + + run_id: UUID | None + + def __init__( + self, + handlers: Sequence[GraphCallbackHandler] | None = None, + inheritable_handlers: Sequence[GraphCallbackHandler] | None = None, + parent_run_id: UUID | None = None, + *, + tags: list[str] | None = None, + inheritable_tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + inheritable_metadata: dict[str, Any] | None = None, + run_id: UUID | None = None, + ) -> None: + _init_base_manager( + self, + handlers, + inheritable_handlers, + parent_run_id, + tags=tags, + inheritable_tags=inheritable_tags, + metadata=metadata, + inheritable_metadata=inheritable_metadata, + run_id=run_id, + ) + + def add_handler( + self, + handler: BaseCallbackHandler, + inherit: bool = True, # noqa: FBT001,FBT002 + ) -> None: + if not isinstance(handler, GraphCallbackHandler): + raise TypeError("handlers must inherit GraphCallbackHandler") + super().add_handler(handler, inherit=inherit) + + def copy( + self, + *, + run_id: UUID | None | object = _MISSING, + ) -> _GraphCallbackManager: + return _copy_graph_manager(self, _GraphCallbackManager, run_id) + + @classmethod + def configure( + cls, + callbacks: object | None = None, + *, + run_id: UUID | None = None, + ) -> _GraphCallbackManager: + return _configure_graph_callbacks(cls, callbacks, run_id=run_id) + + def on_interrupt(self, event: GraphInterruptEvent) -> None: + handle_event( + self.handlers, + "on_interrupt", + None, + event, + ) + + def on_resume(self, event: GraphResumeEvent) -> None: + handle_event( + self.handlers, + "on_resume", + None, + event, + ) + + +class _AsyncGraphCallbackManager(BaseCallbackManager): + """Async dispatcher for graph lifecycle events.""" + + run_id: UUID | None + + @property + def is_async(self) -> bool: + """Return whether the manager is async.""" + return True + + def __init__( + self, + handlers: Sequence[GraphCallbackHandler] | None = None, + inheritable_handlers: Sequence[GraphCallbackHandler] | None = None, + parent_run_id: UUID | None = None, + *, + tags: list[str] | None = None, + inheritable_tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + inheritable_metadata: dict[str, Any] | None = None, + run_id: UUID | None = None, + ) -> None: + _init_base_manager( + self, + handlers, + inheritable_handlers, + parent_run_id, + tags=tags, + inheritable_tags=inheritable_tags, + metadata=metadata, + inheritable_metadata=inheritable_metadata, + run_id=run_id, + ) + + def add_handler( + self, + handler: BaseCallbackHandler, + inherit: bool = True, # noqa: FBT001,FBT002 + ) -> None: + if not isinstance(handler, GraphCallbackHandler): + raise TypeError("handlers must inherit GraphCallbackHandler") + super().add_handler(handler, inherit=inherit) + + def copy( + self, + *, + run_id: UUID | None | object = _MISSING, + ) -> _AsyncGraphCallbackManager: + return _copy_graph_manager(self, _AsyncGraphCallbackManager, run_id) + + @classmethod + def configure( + cls, + callbacks: object | None = None, + *, + run_id: UUID | None = None, + ) -> _AsyncGraphCallbackManager: + return _configure_graph_callbacks(cls, callbacks, run_id=run_id) + + async def on_interrupt(self, event: GraphInterruptEvent) -> None: + await ahandle_event( + self.handlers, + "on_interrupt", + None, + event, + ) + + async def on_resume(self, event: GraphResumeEvent) -> None: + await ahandle_event( + self.handlers, + "on_resume", + None, + event, + ) + + +_GraphManagerT = TypeVar( + "_GraphManagerT", _GraphCallbackManager, _AsyncGraphCallbackManager +) + +GraphCallbacks: TypeAlias = ( + _GraphCallbackManager + | _AsyncGraphCallbackManager + | BaseCallbackManager + | GraphCallbackHandler + | Sequence[BaseCallbackHandler] + | Sequence[GraphCallbackHandler] + | None +) + + +def get_sync_graph_callback_manager_for_config( + config: RunnableConfig, + *, + run_id: UUID | None = None, +) -> _GraphCallbackManager: + """Build a sync graph lifecycle callback manager from a runnable config. + + This helper filters `config["callbacks"]` down to handlers that inherit + from `GraphCallbackHandler` and binds the provided `run_id` onto the + returned manager. + """ + return _GraphCallbackManager.configure( + config.get("callbacks"), + run_id=run_id, + ) + + +def get_async_graph_callback_manager_for_config( + config: RunnableConfig, + *, + run_id: UUID | None = None, +) -> _AsyncGraphCallbackManager: + """Build an async graph lifecycle callback manager from a runnable config. + + This helper filters `config["callbacks"]` down to handlers that inherit + from `GraphCallbackHandler` and binds the provided `run_id` onto the + returned manager. + """ + return _AsyncGraphCallbackManager.configure( + config.get("callbacks"), + run_id=run_id, + ) diff --git a/libs/langgraph/langgraph/pregel/_loop.py b/libs/langgraph/langgraph/pregel/_loop.py index 5ae6f361e..a4fbfa5de 100644 --- a/libs/langgraph/langgraph/pregel/_loop.py +++ b/libs/langgraph/langgraph/pregel/_loop.py @@ -60,6 +60,11 @@ from langgraph._internal._constants import ( from langgraph._internal._replay import ReplayState from langgraph._internal._scratchpad import PregelScratchpad from langgraph._internal._typing import EMPTY_SEQ, MISSING +from langgraph.callbacks import ( + GraphInterruptEvent, + GraphLifecycleEvent, + GraphResumeEvent, +) from langgraph.channels.base import BaseChannel from langgraph.channels.untracked_value import UntrackedValue from langgraph.constants import TAG_HIDDEN @@ -120,6 +125,7 @@ from langgraph.types import ( CachePolicy, Command, Durability, + Interrupt, PregelExecutableTask, RetryPolicy, Send, @@ -193,6 +199,8 @@ class PregelLoop: tasks: dict[str, PregelExecutableTask] output: None | dict[str, Any] | Any = None updated_channels: set[str] | None = None + _graph_lifecycle_events: deque[GraphLifecycleEvent] + _has_graph_lifecycle_callbacks: bool # public @@ -218,6 +226,7 @@ class PregelLoop: migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, + has_graph_lifecycle_callbacks: bool = False, ) -> None: self.stream = stream self.config = config @@ -242,6 +251,8 @@ class PregelLoop: self.retry_policy = retry_policy self.cache_policy = cache_policy self.durability = durability + 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]: self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM]) scratchpad: PregelScratchpad | None = config[CONF].get(CONFIG_KEY_SCRATCHPAD) @@ -293,6 +304,40 @@ class PregelLoop: ) self.prev_checkpoint_config = None + def _push_graph_lifecycle_event( + self, + kind: Literal["resume", "interrupt"], + *, + interrupts: tuple[Interrupt, ...] = (), + ) -> None: + if kind == "resume": + self._graph_lifecycle_events.append( + GraphResumeEvent( + run_id=None, + status=self.status, + checkpoint_id=self.checkpoint["id"], + checkpoint_ns=self.checkpoint_ns, + ) + ) + elif kind == "interrupt": + self._graph_lifecycle_events.append( + GraphInterruptEvent( + run_id=None, + status=self.status, + checkpoint_id=self.checkpoint["id"], + checkpoint_ns=self.checkpoint_ns, + interrupts=interrupts, + ) + ) + else: + msg = f"Unknown graph lifecycle event type: {kind}" + raise AssertionError(msg) + + def _pop_lifecycle_event(self) -> GraphLifecycleEvent | None: + if not self._graph_lifecycle_events: + return None + return self._graph_lifecycle_events.popleft() + def put_writes(self, task_id: str, writes: WritesT) -> None: """Put writes for a task, to be read by the next tick.""" if not writes: @@ -776,6 +821,8 @@ class PregelLoop: ) # set flag self.status = "pending" + if is_resuming: + self._push_graph_lifecycle_event("resume") return updated_channels, checkpoint_request def _prepare_checkpoint( @@ -861,8 +908,11 @@ class PregelLoop: exc_type: type[BaseException] | None, exc_value: BaseException | None, ) -> bool | None: - suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested - if suppress: + if isinstance(exc_value, GraphInterrupt) and not self.is_nested: + interrupt = exc_value + interrupts = tuple(interrupt.args[0]) if interrupt.args else () + self._push_graph_lifecycle_event("interrupt", interrupts=interrupts) + # emit one last "values" event, with pending writes applied if ( hasattr(self, "tasks") and self.checkpoint_pending_writes @@ -887,12 +937,12 @@ class PregelLoop: [w for t in self.tasks.values() for w in t.writes], self.channels, ) - if exc_value is not None and (not exc_value.args or not exc_value.args[0]): + # emit INTERRUPT if exception is empty (otherwise emitted by put_writes) + if not interrupt.args or not interrupt.args[0]: + interrupt_payload = interrupt.args[0] if interrupt.args else () self._emit( "updates", - lambda: iter( - [{INTERRUPT: cast(GraphInterrupt, exc_value).args[0]}] - ), + lambda: iter([{INTERRUPT: interrupt_payload}]), ) self.output = read_channels(self.channels, self.output_keys) return True @@ -1027,6 +1077,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, + has_graph_lifecycle_callbacks: bool = False, ) -> None: super().__init__( input, @@ -1048,6 +1099,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): retry_policy=retry_policy, cache_policy=cache_policy, durability=durability, + has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks, ) self.stack = ExitStack() if checkpointer: @@ -1127,6 +1179,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): # context manager def __enter__(self) -> Self: + self._graph_lifecycle_events = deque() if not self.checkpointer: saved = None elif self.checkpoint_config[CONF].get(CONFIG_KEY_CHECKPOINT_ID): @@ -1233,6 +1286,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, + has_graph_lifecycle_callbacks: bool = False, ) -> None: super().__init__( input, @@ -1254,6 +1308,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): retry_policy=retry_policy, cache_policy=cache_policy, durability=durability, + has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks, ) self.stack = AsyncExitStack() if checkpointer: @@ -1348,6 +1403,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): # context manager async def __aenter__(self) -> Self: + self._graph_lifecycle_events = deque() if not self.checkpointer: saved = None elif self.checkpoint_config[CONF].get(CONFIG_KEY_CHECKPOINT_ID): diff --git a/libs/langgraph/langgraph/pregel/main.py b/libs/langgraph/langgraph/pregel/main.py index 97bb1ce53..d87429be6 100644 --- a/libs/langgraph/langgraph/pregel/main.py +++ b/libs/langgraph/langgraph/pregel/main.py @@ -16,7 +16,7 @@ from collections.abc import ( Mapping, Sequence, ) -from dataclasses import is_dataclass +from dataclasses import is_dataclass, replace from functools import partial from inspect import isclass from typing import ( @@ -96,6 +96,12 @@ from langgraph._internal._runnable import ( coerce_to_runnable, ) from langgraph._internal._typing import MISSING, DeprecatedKwargs +from langgraph.callbacks import ( + GraphInterruptEvent, + GraphResumeEvent, + get_async_graph_callback_manager_for_config, + get_sync_graph_callback_manager_for_config, +) from langgraph.channels.base import BaseChannel from langgraph.channels.topic import Topic from langgraph.config import get_config @@ -2585,6 +2591,10 @@ class Pregel( name=config.get("run_name", self.get_name()), run_id=config.get("run_id"), ) + graph_callback_manager = get_sync_graph_callback_manager_for_config( + config, + run_id=run_manager.run_id, + ) try: # assign defaults ( @@ -2669,6 +2679,17 @@ class Pregel( _output_mapper = self._output_mapper if version == "v2" else None _state_mapper = self._state_mapper if version == "v2" else None + def emit_graph_lifecycle_events(loop: SyncPregelLoop) -> None: + while (event := loop._pop_lifecycle_event()) is not None: + if isinstance(event, GraphResumeEvent): + graph_callback_manager.on_resume( + replace(event, run_id=graph_callback_manager.run_id) + ) + else: + graph_callback_manager.on_interrupt( + replace(event, run_id=graph_callback_manager.run_id) + ) + with SyncPregelLoop( input, stream=StreamProtocol(stream.put, stream_modes), @@ -2689,7 +2710,9 @@ class Pregel( migrate_checkpoint=self._migrate_checkpoint, retry_policy=self.retry_policy, cache_policy=self.cache_policy, + has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers), ) as loop: + emit_graph_lifecycle_events(loop) # create runner runner = PregelRunner( submit=config[CONF].get( @@ -2751,6 +2774,8 @@ class Pregel( _state_mapper, ) loop.after_tick() + emit_graph_lifecycle_events(loop) + emit_graph_lifecycle_events(loop) # emit output yield from _output( stream_mode, @@ -2925,6 +2950,10 @@ class Pregel( name=config.get("run_name", self.get_name()), run_id=config.get("run_id"), ) + graph_callback_manager = get_async_graph_callback_manager_for_config( + config, + run_id=run_manager.run_id, + ) # if running from astream_log() run each proc with streaming do_stream = ( next( @@ -3039,6 +3068,28 @@ class Pregel( _output_mapper = self._output_mapper if version == "v2" else None _state_mapper = self._state_mapper if version == "v2" else None + async def aemit_graph_lifecycle_events(loop: AsyncPregelLoop) -> None: + while (event := loop._pop_lifecycle_event()) is not None: + if isinstance(event, GraphResumeEvent): + await graph_callback_manager.on_resume( + GraphResumeEvent( + run_id=graph_callback_manager.run_id, + status=event.status, + checkpoint_id=event.checkpoint_id, + checkpoint_ns=event.checkpoint_ns, + ) + ) + else: + await graph_callback_manager.on_interrupt( + GraphInterruptEvent( + run_id=graph_callback_manager.run_id, + status=event.status, + checkpoint_id=event.checkpoint_id, + checkpoint_ns=event.checkpoint_ns, + interrupts=event.interrupts, + ) + ) + async with AsyncPregelLoop( input, stream=StreamProtocol(stream.put_nowait, stream_modes), @@ -3059,7 +3110,9 @@ class Pregel( migrate_checkpoint=self._migrate_checkpoint, retry_policy=self.retry_policy, cache_policy=self.cache_policy, + has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers), ) as loop: + await aemit_graph_lifecycle_events(loop) # create runner runner = PregelRunner( submit=config[CONF].get( @@ -3141,11 +3194,14 @@ class Pregel( ): yield o await loop.aafter_tick() + await aemit_graph_lifecycle_events(loop) finally: # ensure waiter doesn't remain pending on cancel/shutdown if _cleanup_waiter is not None: await _cleanup_waiter() + await aemit_graph_lifecycle_events(loop) + # emit output for o in _output( stream_mode, @@ -3653,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") diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 6c1745fe2..6e4378c0c 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" description = "Building stateful, multi-actor applications with LLMs" authors = [] requires-python = ">=3.10" diff --git a/libs/langgraph/tests/test_graph_callbacks.py b/libs/langgraph/tests/test_graph_callbacks.py new file mode 100644 index 000000000..ca825f904 --- /dev/null +++ b/libs/langgraph/tests/test_graph_callbacks.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import sys +from typing import Any + +import pytest +from langchain_core.callbacks.base import BaseCallbackHandler +from langchain_core.callbacks.manager import CallbackManager +from langgraph.checkpoint.memory import InMemorySaver +from typing_extensions import TypedDict + +from langgraph.callbacks import ( + GraphCallbackHandler, + GraphInterruptEvent, + GraphResumeEvent, +) +from langgraph.graph import START, StateGraph +from langgraph.types import Command, Interrupt, interrupt + +NEEDS_CONTEXTVARS = pytest.mark.skipif( + sys.version_info < (3, 11), + reason="Python 3.11+ is required for async contextvars support", +) + + +class _GraphEventHandler(GraphCallbackHandler): + def __init__(self) -> None: + self.interrupt_events: list[GraphInterruptEvent] = [] + self.resume_events: list[GraphResumeEvent] = [] + + def on_interrupt(self, event: GraphInterruptEvent) -> Any: + self.interrupt_events.append(event) + + def on_resume(self, event: GraphResumeEvent) -> Any: + self.resume_events.append(event) + + +class _LangChainCustomEventHandler(BaseCallbackHandler): + run_inline = True + + def __init__(self) -> None: + self.events: list[str] = [] + + def on_custom_event(self, name: str, data: Any, **kwargs: Any) -> Any: + self.events.append(name) + + +class _RaisingGraphEventHandler(GraphCallbackHandler): + def __init__( + self, + *, + raise_on_interrupt: bool = False, + raise_on_resume: bool = False, + raise_error: bool = False, + ) -> None: + self.raise_on_interrupt = raise_on_interrupt + self.raise_on_resume = raise_on_resume + self.raise_error = raise_error + + def on_interrupt(self, event: GraphInterruptEvent) -> Any: + if self.raise_on_interrupt: + raise ValueError("boom-interrupt") + + def on_resume(self, event: GraphResumeEvent) -> Any: + if self.raise_on_resume: + raise ValueError("boom-resume") + + +class _AsyncRaisingGraphEventHandler(GraphCallbackHandler): + def __init__( + self, + *, + raise_on_interrupt: bool = False, + raise_on_resume: bool = False, + raise_error: bool = False, + ) -> None: + self.raise_on_interrupt = raise_on_interrupt + self.raise_on_resume = raise_on_resume + self.raise_error = raise_error + + async def on_interrupt(self, event: GraphInterruptEvent) -> Any: + if self.raise_on_interrupt: + raise ValueError("boom-interrupt") + + async def on_resume(self, event: GraphResumeEvent) -> Any: + if self.raise_on_resume: + raise ValueError("boom-resume") + + +class _State(TypedDict): + answer: str | None + + +def _build_interrupt_graph() -> Any: + def ask(state: _State) -> _State: + answer = interrupt("Provide value") + return {"answer": answer} + + builder = StateGraph(_State) + builder.add_node("ask", ask) + builder.add_edge(START, "ask") + return builder.compile(checkpointer=InMemorySaver()) + + +def test_graph_callbacks_interrupt_and_resume_sync() -> None: + graph = _build_interrupt_graph() + handler = _GraphEventHandler() + langchain_handler = _LangChainCustomEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-sync"}, + "callbacks": [langchain_handler, handler], + } + + first = graph.invoke({"answer": None}, config) + assert "__interrupt__" in first + + assert len(handler.interrupt_events) == 1 + assert handler.interrupt_events[0].interrupts + assert isinstance(handler.interrupt_events[0].interrupts[0], Interrupt) + assert handler.interrupt_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + handler.resume_events.clear() + resumed = graph.invoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + + assert len(handler.resume_events) == 1 + assert handler.resume_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + +@pytest.mark.anyio +@NEEDS_CONTEXTVARS +async def test_graph_callbacks_interrupt_and_resume_async() -> None: + graph = _build_interrupt_graph() + handler = _GraphEventHandler() + langchain_handler = _LangChainCustomEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-async"}, + "callbacks": [langchain_handler, handler], + } + + first = await graph.ainvoke({"answer": None}, config) + assert "__interrupt__" in first + + assert len(handler.interrupt_events) == 1 + assert handler.interrupt_events[0].interrupts + assert isinstance(handler.interrupt_events[0].interrupts[0], Interrupt) + assert handler.interrupt_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + handler.resume_events.clear() + resumed = await graph.ainvoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + + assert len(handler.resume_events) == 1 + assert handler.resume_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + +def test_graph_callbacks_continue_when_interrupt_handler_raises_sync() -> None: + graph = _build_interrupt_graph() + raising_handler = _RaisingGraphEventHandler(raise_on_interrupt=True) + recording_handler = _GraphEventHandler() + + first = graph.invoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-sync-raises"}, + "callbacks": [raising_handler, recording_handler], + }, + ) + + assert "__interrupt__" in first + assert len(recording_handler.interrupt_events) == 1 + + +def test_graph_callbacks_continue_when_resume_handler_raises_sync() -> None: + graph = _build_interrupt_graph() + raising_handler = _RaisingGraphEventHandler(raise_on_resume=True) + recording_handler = _GraphEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-sync-raises-resume"}, + "callbacks": [raising_handler, recording_handler], + } + + first = graph.invoke({"answer": None}, config) + assert "__interrupt__" in first + + resumed = graph.invoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + assert len(recording_handler.resume_events) == 1 + + +def test_graph_callbacks_raise_error_propagates_sync() -> None: + graph = _build_interrupt_graph() + raising_handler = _RaisingGraphEventHandler( + raise_on_interrupt=True, + raise_error=True, + ) + + with pytest.raises(ValueError, match="boom-interrupt"): + graph.invoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-sync-raise-error"}, + "callbacks": [raising_handler], + }, + ) + + +@pytest.mark.anyio +@NEEDS_CONTEXTVARS +async def test_graph_callbacks_continue_when_handler_raises_async() -> None: + graph = _build_interrupt_graph() + raising_interrupt_handler = _AsyncRaisingGraphEventHandler(raise_on_interrupt=True) + recording_handler = _GraphEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-async-raises-interrupt"}, + "callbacks": [raising_interrupt_handler, recording_handler], + } + + first = await graph.ainvoke({"answer": None}, config) + assert "__interrupt__" in first + assert len(recording_handler.interrupt_events) == 1 + + graph = _build_interrupt_graph() + raising_resume_handler = _AsyncRaisingGraphEventHandler(raise_on_resume=True) + recording_handler = _GraphEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-async-raises-resume"}, + "callbacks": [raising_resume_handler, recording_handler], + } + + first = await graph.ainvoke({"answer": None}, config) + assert "__interrupt__" in first + resumed = await graph.ainvoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + assert len(recording_handler.resume_events) == 1 + + +@pytest.mark.anyio +@NEEDS_CONTEXTVARS +async def test_graph_callbacks_raise_error_propagates_async() -> None: + graph = _build_interrupt_graph() + raising_handler = _AsyncRaisingGraphEventHandler( + raise_on_interrupt=True, + raise_error=True, + ) + + with pytest.raises(ValueError, match="boom-interrupt"): + await graph.ainvoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-async-raise-error"}, + "callbacks": [raising_handler], + }, + ) + + +def test_graph_callbacks_accept_base_callback_manager() -> None: + graph = _build_interrupt_graph() + graph_handler = _GraphEventHandler() + custom_handler = _LangChainCustomEventHandler() + manager = CallbackManager.configure(inheritable_callbacks=[custom_handler]) + manager.add_handler(graph_handler) + + first = graph.invoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-base-manager"}, + "callbacks": manager, + }, + ) + + assert "__interrupt__" in first + assert len(graph_handler.interrupt_events) == 1 diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index d056f6b0a..759a4054a 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -6361,7 +6361,7 @@ def test_sync_streaming_with_functional_api() -> None: @task() def slow() -> dict: time.sleep(time_delay) # Simulate a delay of 10 ms - return {"tic": time.time()} + return {"tic": time.monotonic()} @entrypoint() def graph(inputs: dict) -> list: @@ -6374,7 +6374,7 @@ def test_sync_streaming_with_functional_api() -> None: for chunk in graph.stream({}): if "slow" not in chunk: # We'll just look at the updates from `slow` continue - arrival_times.append(time.time()) + arrival_times.append(time.monotonic()) assert len(arrival_times) == 2 delta = arrival_times[1] - arrival_times[0] diff --git a/libs/langgraph/tests/test_runtime.py b/libs/langgraph/tests/test_runtime.py index 0796ddf9d..a85abe45f 100644 --- a/libs/langgraph/tests/test_runtime.py +++ b/libs/langgraph/tests/test_runtime.py @@ -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"] diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 7554e2765..bfb933280 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1367,7 +1367,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" source = { editable = "." } dependencies = [ { name = "langchain-core" }, diff --git a/libs/prebuilt/uv.lock b/libs/prebuilt/uv.lock index 88bdeb623..f6ce058cc 100644 --- a/libs/prebuilt/uv.lock +++ b/libs/prebuilt/uv.lock @@ -268,7 +268,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" source = { editable = "../langgraph" } dependencies = [ { name = "langchain-core" }, diff --git a/libs/sdk-py/uv.lock b/libs/sdk-py/uv.lock index 5194368d1..bb29f8686 100644 --- a/libs/sdk-py/uv.lock +++ b/libs/sdk-py/uv.lock @@ -281,7 +281,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" source = { editable = "../langgraph" } dependencies = [ { name = "langchain-core" },