mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-29 22:50:37 +00:00
Compare commits
14 Commits
v2.2.0-rc0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d52d2541d | ||
|
|
81e1ab8dcc | ||
|
|
4fd2b211cc | ||
|
|
1e24a2115c | ||
|
|
477e53386d | ||
|
|
771cf619c1 | ||
|
|
8d11aeb97b | ||
|
|
30aa601f6a | ||
|
|
44b88151be | ||
|
|
b3f23326a5 | ||
|
|
dedb154e4c | ||
|
|
6ca83a32cd | ||
|
|
5c4ba4d2fe | ||
|
|
76835c1ff2 |
15
AGENTS.md
15
AGENTS.md
@ -238,13 +238,12 @@ Repo-wide conventions:
|
||||
- **Skill text encoding** — use explicit UTF-8 for textual skill resources.
|
||||
Read `SKILL.md` with `encoding="utf-8-sig"` to strip a leading BOM; write with
|
||||
`encoding="utf-8"`. Never rely on the platform locale.
|
||||
- **Version sources must stay in lockstep** — a release version must match identically in
|
||||
`backend/pyproject.toml`, `frontend/package.json`, and `deploy/helm/deer-flow/Chart.yaml`
|
||||
(`version` + `appVersion`), and `backend/uv.lock` must record the same version for the root
|
||||
package (uv stores its PEP 440 form, e.g. `2.1.0rc0`). Pushing a `v*` git tag triggers CI
|
||||
that runs `scripts/verify_versions.sh` and **blocks all publishing** if any source drifts.
|
||||
Before bumping a version, run `scripts/bump_version.sh <ver>` (aligns the four fields and
|
||||
refreshes `backend/uv.lock` at once — it needs `uv` on `PATH`) and
|
||||
`scripts/verify_versions.sh <ver>` to catch drift early. See [RELEASING.md](RELEASING.md).
|
||||
- **Version lockstep** — keep `backend/pyproject.toml`,
|
||||
`backend/packages/harness/pyproject.toml`, `frontend/package.json`, and
|
||||
`deploy/helm/deer-flow/Chart.yaml` (`version`/`appVersion`) identical.
|
||||
`backend/uv.lock` must match both `deer-flow` and `deerflow-harness` in PEP 440 form.
|
||||
Run `scripts/bump_version.sh <ver>` (requires `uv` on `PATH`) to update all five
|
||||
fields and the lock, then `scripts/verify_versions.sh <ver>`. `v*` tag CI blocks
|
||||
all publishing on drift. See [RELEASING.md](RELEASING.md).
|
||||
- **Don't edit `CLAUDE.md`** — it only contains `@AGENTS.md`. All agent guidance changes
|
||||
belong here in `AGENTS.md`; `CLAUDE.md` is a thin import shim.
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
This section accumulates work toward the **2.3.0** milestone
|
||||
([2.3.0](https://github.com/bytedance/deer-flow/milestone/4)).
|
||||
|
||||
## [2.2.0] — 2026-10-29
|
||||
|
||||
This section accumulates work toward the **2.2.0** milestone
|
||||
([2.2.0](https://github.com/bytedance/deer-flow/milestone/3)).
|
||||
As of 2026-10-28, the milestone includes **1068 merged pull requests**.
|
||||
This release closes that milestone with **1068 merged pull requests**.
|
||||
|
||||
### Added
|
||||
|
||||
@ -14678,7 +14683,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5217]: https://github.com/bytedance/deer-flow/pull/5217
|
||||
[#5219]: https://github.com/bytedance/deer-flow/pull/5219
|
||||
[#5221]: https://github.com/bytedance/deer-flow/pull/5221
|
||||
[#5224]: https://github.com/bytedance/deer-flow/pull/5224
|
||||
[#5224]: https://github.com/bytedance/deer-flow/commit/572744975d369fda88168aaa7b481282f939affe
|
||||
[#5225]: https://github.com/bytedance/deer-flow/pull/5225
|
||||
[#5227]: https://github.com/bytedance/deer-flow/pull/5227
|
||||
[#5228]: https://github.com/bytedance/deer-flow/pull/5228
|
||||
@ -14687,9 +14692,9 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5236]: https://github.com/bytedance/deer-flow/pull/5236
|
||||
[#5238]: https://github.com/bytedance/deer-flow/pull/5238
|
||||
[#5239]: https://github.com/bytedance/deer-flow/pull/5239
|
||||
[#5244]: https://github.com/bytedance/deer-flow/pull/5244
|
||||
[#5244]: https://github.com/bytedance/deer-flow/commit/9ce6fdcb2252ea005b4fa98ca50bd3857e1f681a
|
||||
[#5245]: https://github.com/bytedance/deer-flow/pull/5245
|
||||
[#5247]: https://github.com/bytedance/deer-flow/pull/5247
|
||||
[#5247]: https://github.com/bytedance/deer-flow/commit/fa89a125267be4213009376ec0e232a0c2411ca8
|
||||
[#5249]: https://github.com/bytedance/deer-flow/pull/5249
|
||||
[#5251]: https://github.com/bytedance/deer-flow/pull/5251
|
||||
[#5254]: https://github.com/bytedance/deer-flow/pull/5254
|
||||
@ -14746,7 +14751,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5367]: https://github.com/bytedance/deer-flow/pull/5367
|
||||
[#5369]: https://github.com/bytedance/deer-flow/pull/5369
|
||||
[#5371]: https://github.com/bytedance/deer-flow/pull/5371
|
||||
[#5373]: https://github.com/bytedance/deer-flow/pull/5373
|
||||
[#5373]: https://github.com/bytedance/deer-flow/commit/d5ae3882b6708fa9af7289a0c3c0be3eee157203
|
||||
[#5374]: https://github.com/bytedance/deer-flow/pull/5374
|
||||
[#5375]: https://github.com/bytedance/deer-flow/pull/5375
|
||||
[#5377]: https://github.com/bytedance/deer-flow/pull/5377
|
||||
@ -14773,7 +14778,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5408]: https://github.com/bytedance/deer-flow/pull/5408
|
||||
[#5410]: https://github.com/bytedance/deer-flow/pull/5410
|
||||
[#5411]: https://github.com/bytedance/deer-flow/pull/5411
|
||||
[#5413]: https://github.com/bytedance/deer-flow/pull/5413
|
||||
[#5413]: https://github.com/bytedance/deer-flow/commit/daf32cd24632f2cc7465aeb62f2a1e1e768eceb2
|
||||
[#5415]: https://github.com/bytedance/deer-flow/pull/5415
|
||||
[#5416]: https://github.com/bytedance/deer-flow/pull/5416
|
||||
[#5418]: https://github.com/bytedance/deer-flow/pull/5418
|
||||
@ -14956,8 +14961,8 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5736]: https://github.com/bytedance/deer-flow/pull/5736
|
||||
[#5738]: https://github.com/bytedance/deer-flow/pull/5738
|
||||
[#5739]: https://github.com/bytedance/deer-flow/pull/5739
|
||||
[#5740]: https://github.com/bytedance/deer-flow/pull/5740
|
||||
[#5741]: https://github.com/bytedance/deer-flow/pull/5741
|
||||
[#5740]: https://github.com/bytedance/deer-flow/commit/e7128dc7d6b4d49153b10f7276f010384ec4e6af
|
||||
[#5741]: https://github.com/bytedance/deer-flow/commit/363b0fc1b4a5bd6c142d1f1df9fb9743cca550a3
|
||||
[#5745]: https://github.com/bytedance/deer-flow/pull/5745
|
||||
[#5748]: https://github.com/bytedance/deer-flow/pull/5748
|
||||
[#5750]: https://github.com/bytedance/deer-flow/pull/5750
|
||||
@ -14970,7 +14975,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5760]: https://github.com/bytedance/deer-flow/pull/5760
|
||||
[#5761]: https://github.com/bytedance/deer-flow/pull/5761
|
||||
[#5762]: https://github.com/bytedance/deer-flow/pull/5762
|
||||
[#5763]: https://github.com/bytedance/deer-flow/pull/5763
|
||||
[#5763]: https://github.com/bytedance/deer-flow/commit/39351315bf5d096fc84ad8307d3702b6aa5b3e24
|
||||
[#5767]: https://github.com/bytedance/deer-flow/pull/5767
|
||||
[#5769]: https://github.com/bytedance/deer-flow/pull/5769
|
||||
[#5775]: https://github.com/bytedance/deer-flow/pull/5775
|
||||
@ -15038,7 +15043,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5880]: https://github.com/bytedance/deer-flow/pull/5880
|
||||
[#5881]: https://github.com/bytedance/deer-flow/pull/5881
|
||||
[#5883]: https://github.com/bytedance/deer-flow/pull/5883
|
||||
[#5884]: https://github.com/bytedance/deer-flow/pull/5884
|
||||
[#5884]: https://github.com/bytedance/deer-flow/commit/e8cb5e9c4f5f274401eeb555aff62e713035cbf4
|
||||
[#5888]: https://github.com/bytedance/deer-flow/pull/5888
|
||||
[#5890]: https://github.com/bytedance/deer-flow/pull/5890
|
||||
[#5892]: https://github.com/bytedance/deer-flow/pull/5892
|
||||
@ -15100,9 +15105,9 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5999]: https://github.com/bytedance/deer-flow/pull/5999
|
||||
[#6000]: https://github.com/bytedance/deer-flow/pull/6000
|
||||
[#6008]: https://github.com/bytedance/deer-flow/pull/6008
|
||||
[#6009]: https://github.com/bytedance/deer-flow/pull/6009
|
||||
[#6009]: https://github.com/bytedance/deer-flow/commit/b87ebd940227465d611084e5bf49582aaeb4e9bd
|
||||
[#6013]: https://github.com/bytedance/deer-flow/pull/6013
|
||||
[#6015]: https://github.com/bytedance/deer-flow/pull/6015
|
||||
[#6015]: https://github.com/bytedance/deer-flow/commit/f87a04e581829ff35afd4fa228240071b486192f
|
||||
[#6017]: https://github.com/bytedance/deer-flow/pull/6017
|
||||
[#6018]: https://github.com/bytedance/deer-flow/pull/6018
|
||||
[#6020]: https://github.com/bytedance/deer-flow/pull/6020
|
||||
@ -15126,7 +15131,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#6060]: https://github.com/bytedance/deer-flow/pull/6060
|
||||
[#6062]: https://github.com/bytedance/deer-flow/pull/6062
|
||||
[#6063]: https://github.com/bytedance/deer-flow/pull/6063
|
||||
[#6066]: https://github.com/bytedance/deer-flow/pull/6066
|
||||
[#6066]: https://github.com/bytedance/deer-flow/commit/73fdbcfdf4b5f4037b62bc2ce9d8b60ec04e69f8
|
||||
[#6067]: https://github.com/bytedance/deer-flow/pull/6067
|
||||
[#6068]: https://github.com/bytedance/deer-flow/pull/6068
|
||||
[#6069]: https://github.com/bytedance/deer-flow/pull/6069
|
||||
@ -15152,7 +15157,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#6105]: https://github.com/bytedance/deer-flow/pull/6105
|
||||
[#6107]: https://github.com/bytedance/deer-flow/pull/6107
|
||||
[#6109]: https://github.com/bytedance/deer-flow/pull/6109
|
||||
[#6112]: https://github.com/bytedance/deer-flow/pull/6112
|
||||
[#6112]: https://github.com/bytedance/deer-flow/commit/52b21d13ac6e674a19cb76013d097e73f108c994
|
||||
[#6113]: https://github.com/bytedance/deer-flow/pull/6113
|
||||
[#6115]: https://github.com/bytedance/deer-flow/pull/6115
|
||||
[#6117]: https://github.com/bytedance/deer-flow/pull/6117
|
||||
@ -15253,7 +15258,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#6298]: https://github.com/bytedance/deer-flow/pull/6298
|
||||
[#6299]: https://github.com/bytedance/deer-flow/pull/6299
|
||||
[#6302]: https://github.com/bytedance/deer-flow/pull/6302
|
||||
[#6304]: https://github.com/bytedance/deer-flow/pull/6304
|
||||
[#6304]: https://github.com/bytedance/deer-flow/commit/b85e4ae74bb407e1e2396243a8a6e8f5b2420e3f
|
||||
[#6305]: https://github.com/bytedance/deer-flow/pull/6305
|
||||
[#6306]: https://github.com/bytedance/deer-flow/pull/6306
|
||||
[#6307]: https://github.com/bytedance/deer-flow/pull/6307
|
||||
|
||||
@ -9,9 +9,14 @@
|
||||
|
||||
## [未发布]
|
||||
|
||||
本节累积面向 **2.2.0** 里程碑([2.2.0](https://github.com/bytedance/deer-flow/milestone/3))的工作。
|
||||
本节累积面向 **2.3.0** 里程碑([2.3.0](https://github.com/bytedance/deer-flow/milestone/4))的工作。
|
||||
截至 2026-10-28,该里程碑已合并 **1068 个拉取请求**。
|
||||
|
||||
## [2.2.0] — 2026-10-29
|
||||
|
||||
本节累积面向 **2.2.0** 里程碑([2.2.0](https://github.com/bytedance/deer-flow/milestone/3))的工作。
|
||||
该里程碑随本次发布收尾,共合并 **1068 个拉取请求**。
|
||||
|
||||
### 新增
|
||||
|
||||
- **插件:** 新增仅管理员可访问的 `GET /api/plugins/diagnostics` 端点,
|
||||
@ -11449,7 +11454,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5217]: https://github.com/bytedance/deer-flow/pull/5217
|
||||
[#5219]: https://github.com/bytedance/deer-flow/pull/5219
|
||||
[#5221]: https://github.com/bytedance/deer-flow/pull/5221
|
||||
[#5224]: https://github.com/bytedance/deer-flow/pull/5224
|
||||
[#5224]: https://github.com/bytedance/deer-flow/commit/572744975d369fda88168aaa7b481282f939affe
|
||||
[#5225]: https://github.com/bytedance/deer-flow/pull/5225
|
||||
[#5227]: https://github.com/bytedance/deer-flow/pull/5227
|
||||
[#5228]: https://github.com/bytedance/deer-flow/pull/5228
|
||||
@ -11458,9 +11463,9 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5236]: https://github.com/bytedance/deer-flow/pull/5236
|
||||
[#5238]: https://github.com/bytedance/deer-flow/pull/5238
|
||||
[#5239]: https://github.com/bytedance/deer-flow/pull/5239
|
||||
[#5244]: https://github.com/bytedance/deer-flow/pull/5244
|
||||
[#5244]: https://github.com/bytedance/deer-flow/commit/9ce6fdcb2252ea005b4fa98ca50bd3857e1f681a
|
||||
[#5245]: https://github.com/bytedance/deer-flow/pull/5245
|
||||
[#5247]: https://github.com/bytedance/deer-flow/pull/5247
|
||||
[#5247]: https://github.com/bytedance/deer-flow/commit/fa89a125267be4213009376ec0e232a0c2411ca8
|
||||
[#5249]: https://github.com/bytedance/deer-flow/pull/5249
|
||||
[#5251]: https://github.com/bytedance/deer-flow/pull/5251
|
||||
[#5254]: https://github.com/bytedance/deer-flow/pull/5254
|
||||
@ -11517,7 +11522,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5367]: https://github.com/bytedance/deer-flow/pull/5367
|
||||
[#5369]: https://github.com/bytedance/deer-flow/pull/5369
|
||||
[#5371]: https://github.com/bytedance/deer-flow/pull/5371
|
||||
[#5373]: https://github.com/bytedance/deer-flow/pull/5373
|
||||
[#5373]: https://github.com/bytedance/deer-flow/commit/d5ae3882b6708fa9af7289a0c3c0be3eee157203
|
||||
[#5374]: https://github.com/bytedance/deer-flow/pull/5374
|
||||
[#5375]: https://github.com/bytedance/deer-flow/pull/5375
|
||||
[#5377]: https://github.com/bytedance/deer-flow/pull/5377
|
||||
@ -11544,7 +11549,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5408]: https://github.com/bytedance/deer-flow/pull/5408
|
||||
[#5410]: https://github.com/bytedance/deer-flow/pull/5410
|
||||
[#5411]: https://github.com/bytedance/deer-flow/pull/5411
|
||||
[#5413]: https://github.com/bytedance/deer-flow/pull/5413
|
||||
[#5413]: https://github.com/bytedance/deer-flow/commit/daf32cd24632f2cc7465aeb62f2a1e1e768eceb2
|
||||
[#5415]: https://github.com/bytedance/deer-flow/pull/5415
|
||||
[#5416]: https://github.com/bytedance/deer-flow/pull/5416
|
||||
[#5418]: https://github.com/bytedance/deer-flow/pull/5418
|
||||
@ -11727,8 +11732,8 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5736]: https://github.com/bytedance/deer-flow/pull/5736
|
||||
[#5738]: https://github.com/bytedance/deer-flow/pull/5738
|
||||
[#5739]: https://github.com/bytedance/deer-flow/pull/5739
|
||||
[#5740]: https://github.com/bytedance/deer-flow/pull/5740
|
||||
[#5741]: https://github.com/bytedance/deer-flow/pull/5741
|
||||
[#5740]: https://github.com/bytedance/deer-flow/commit/e7128dc7d6b4d49153b10f7276f010384ec4e6af
|
||||
[#5741]: https://github.com/bytedance/deer-flow/commit/363b0fc1b4a5bd6c142d1f1df9fb9743cca550a3
|
||||
[#5745]: https://github.com/bytedance/deer-flow/pull/5745
|
||||
[#5748]: https://github.com/bytedance/deer-flow/pull/5748
|
||||
[#5750]: https://github.com/bytedance/deer-flow/pull/5750
|
||||
@ -11741,7 +11746,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5760]: https://github.com/bytedance/deer-flow/pull/5760
|
||||
[#5761]: https://github.com/bytedance/deer-flow/pull/5761
|
||||
[#5762]: https://github.com/bytedance/deer-flow/pull/5762
|
||||
[#5763]: https://github.com/bytedance/deer-flow/pull/5763
|
||||
[#5763]: https://github.com/bytedance/deer-flow/commit/39351315bf5d096fc84ad8307d3702b6aa5b3e24
|
||||
[#5767]: https://github.com/bytedance/deer-flow/pull/5767
|
||||
[#5769]: https://github.com/bytedance/deer-flow/pull/5769
|
||||
[#5775]: https://github.com/bytedance/deer-flow/pull/5775
|
||||
@ -11809,7 +11814,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5880]: https://github.com/bytedance/deer-flow/pull/5880
|
||||
[#5881]: https://github.com/bytedance/deer-flow/pull/5881
|
||||
[#5883]: https://github.com/bytedance/deer-flow/pull/5883
|
||||
[#5884]: https://github.com/bytedance/deer-flow/pull/5884
|
||||
[#5884]: https://github.com/bytedance/deer-flow/commit/e8cb5e9c4f5f274401eeb555aff62e713035cbf4
|
||||
[#5888]: https://github.com/bytedance/deer-flow/pull/5888
|
||||
[#5890]: https://github.com/bytedance/deer-flow/pull/5890
|
||||
[#5892]: https://github.com/bytedance/deer-flow/pull/5892
|
||||
@ -11871,9 +11876,9 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5999]: https://github.com/bytedance/deer-flow/pull/5999
|
||||
[#6000]: https://github.com/bytedance/deer-flow/pull/6000
|
||||
[#6008]: https://github.com/bytedance/deer-flow/pull/6008
|
||||
[#6009]: https://github.com/bytedance/deer-flow/pull/6009
|
||||
[#6009]: https://github.com/bytedance/deer-flow/commit/b87ebd940227465d611084e5bf49582aaeb4e9bd
|
||||
[#6013]: https://github.com/bytedance/deer-flow/pull/6013
|
||||
[#6015]: https://github.com/bytedance/deer-flow/pull/6015
|
||||
[#6015]: https://github.com/bytedance/deer-flow/commit/f87a04e581829ff35afd4fa228240071b486192f
|
||||
[#6017]: https://github.com/bytedance/deer-flow/pull/6017
|
||||
[#6018]: https://github.com/bytedance/deer-flow/pull/6018
|
||||
[#6020]: https://github.com/bytedance/deer-flow/pull/6020
|
||||
@ -11897,7 +11902,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#6060]: https://github.com/bytedance/deer-flow/pull/6060
|
||||
[#6062]: https://github.com/bytedance/deer-flow/pull/6062
|
||||
[#6063]: https://github.com/bytedance/deer-flow/pull/6063
|
||||
[#6066]: https://github.com/bytedance/deer-flow/pull/6066
|
||||
[#6066]: https://github.com/bytedance/deer-flow/commit/73fdbcfdf4b5f4037b62bc2ce9d8b60ec04e69f8
|
||||
[#6067]: https://github.com/bytedance/deer-flow/pull/6067
|
||||
[#6068]: https://github.com/bytedance/deer-flow/pull/6068
|
||||
[#6069]: https://github.com/bytedance/deer-flow/pull/6069
|
||||
@ -11923,7 +11928,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#6105]: https://github.com/bytedance/deer-flow/pull/6105
|
||||
[#6107]: https://github.com/bytedance/deer-flow/pull/6107
|
||||
[#6109]: https://github.com/bytedance/deer-flow/pull/6109
|
||||
[#6112]: https://github.com/bytedance/deer-flow/pull/6112
|
||||
[#6112]: https://github.com/bytedance/deer-flow/commit/52b21d13ac6e674a19cb76013d097e73f108c994
|
||||
[#6113]: https://github.com/bytedance/deer-flow/pull/6113
|
||||
[#6115]: https://github.com/bytedance/deer-flow/pull/6115
|
||||
[#6117]: https://github.com/bytedance/deer-flow/pull/6117
|
||||
@ -12024,7 +12029,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#6298]: https://github.com/bytedance/deer-flow/pull/6298
|
||||
[#6299]: https://github.com/bytedance/deer-flow/pull/6299
|
||||
[#6302]: https://github.com/bytedance/deer-flow/pull/6302
|
||||
[#6304]: https://github.com/bytedance/deer-flow/pull/6304
|
||||
[#6304]: https://github.com/bytedance/deer-flow/commit/b85e4ae74bb407e1e2396243a8a6e8f5b2420e3f
|
||||
[#6305]: https://github.com/bytedance/deer-flow/pull/6305
|
||||
[#6306]: https://github.com/bytedance/deer-flow/pull/6306
|
||||
[#6307]: https://github.com/bytedance/deer-flow/pull/6307
|
||||
|
||||
11
README.md
11
README.md
@ -694,7 +694,11 @@ DeerFlow still uses `Forwarded` / `X-Forwarded-*` headers to recover the browser
|
||||
> The producer-lease recovery behavior above is gated by `stream_bridge.producer_lease_recovery_enabled`. Keep it disabled while old Gateway binaries or their runs remain; enable it on every instance only after the writer rollout and drain complete.
|
||||
>
|
||||
> With run ownership heartbeats enabled, terminal success requires acknowledgment
|
||||
> from that worker's own durable write. A matching database row alone cannot
|
||||
> from that worker's own durable write. Run creation and replacement serialize
|
||||
> per conversation: a slow admission does not hold the manager-wide lock needed
|
||||
> by another conversation's cached run reads or heartbeat snapshots. Their own
|
||||
> Store I/O can still encounter database or pool contention. New runs remain
|
||||
> hidden until their initial Store write succeeds. A matching database row alone cannot
|
||||
> confirm success, and completion metadata cannot retry a rejected terminal
|
||||
> transition. Each run renews independently, so a stalled store operation does
|
||||
> not delay other runs. If acknowledgment remains unavailable when the last
|
||||
@ -1341,6 +1345,7 @@ Notes:
|
||||
- Telegram shutdown retains ongoing cleanup across a timeout or cancellation so a retry can finish it. Stopping during startup also cancels initialization and closes the partially initialized bot's HTTP clients before retiring its worker loop. If the Telegram loop has already stopped, shutdown warns that download clients may remain open and releases the abandoned cleanup state; later stops do not repeat that warning.
|
||||
- Slack stop waits up to five seconds for an in-flight Socket Mode connection and its cleanup. If cleanup is still pending, the previous instance stays tracked and a restart can be retried after it finishes. SDK cleanup failures report their original error and remain retryable.
|
||||
- In Telegram private chats, text, attachment and streaming replies (including "Working on it...") can still be delivered if their referenced user or bot message was deleted, such as after clearing chat history during a run. Group reply-target requirements remain unchanged to preserve forum routing.
|
||||
- In Telegram groups, replying to a delivered bot response or attachment continues its DeerFlow conversation. Reply mappings survive restarts and stay scoped to the user's connection when one is bound. New, unrelated group messages still start separate conversations; private-chat routing is unchanged.
|
||||
- Feishu/Lark now queues rapid follow-up messages per mapped DeerFlow `thread_id` instead of immediately surfacing the generic busy reply, and topic replies keep a per-message card with a compact source-message preview across queued/running/final patches.
|
||||
- Streaming IM channels treat backend `error` events like transport failures and follow the same reply and retry path. The channel logs the error type and message for diagnosis.
|
||||
|
||||
@ -2812,6 +2817,8 @@ The factory still does not load YAML or create SQL infrastructure: the caller su
|
||||
|
||||
Administrators can add, edit, disable, and delete reusable worker definitions from **Settings → Subagents**. Built-in and `config.yaml` definitions remain visible there as read-only entries. The default Lead Agent can use every enabled runtime sub-agent; each page-created Custom Agent can instead allow all, none, or a selected set. That selection is enforced both in the model-visible directory and by the server-side `task` tool. Managed definitions are deployment-wide in this version and follow `agent_storage.backend`: atomic files for a local deployment or the shared application database for multiple instances.
|
||||
|
||||
Native workers support per-subagent `thinking_enabled` and `reasoning_effort` through YAML configuration and the managed-subagent API. Unconfigured workers keep the existing thinking-off request; the selected model's required-thinking policy, supported effort values and aliases still apply. These settings do not inherit the lead agent's thinking preferences.
|
||||
|
||||
For example, independent read-only research can run concurrently when the wall-clock savings outweigh duplicated discovery and synthesis cost, while a repository refactor with shared files and sequential test feedback remains with the lead agent. When `max_concurrent_subagents` is `1`, parallel and multi-batch routing guidance is disabled; delegation remains available only for material specialist or context-isolation benefit.
|
||||
|
||||
### Sandbox & File System
|
||||
@ -3047,7 +3054,7 @@ recording the others. A concurrent scratch-file removal therefore does not
|
||||
discard the whole snapshot or suppress detection of other produced outputs.
|
||||
This remains a best-effort scan, not an atomic filesystem snapshot.
|
||||
|
||||
Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh. Long artifact lists scroll within the panel on desktop and in the mobile drawer, keeping every file reachable. When a completed response successfully presents between 2 and 50 files, its final file card also offers one ZIP download. Archive membership comes from the terminal delivery receipt rather than browser-supplied paths, and the ZIP contains the current file versions, which may have changed since the response. The currently selected formal artifact is refreshed once when the run finishes so edits become visible without a manual reload. Existing UTF-8 text artifacts under `/mnt/user-data/outputs` can also be edited and explicitly saved from the panel on Unix and Windows while the thread is idle; saves use content revisions to prevent overwriting agent changes. Source previews also recognize extensionless `Dockerfile` and `Makefile` artifacts by their file names. Unknown file types, including names such as `constructor` and `__proto__`, retain the download fallback.
|
||||
Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh. Long artifact lists scroll within the panel on desktop and in the mobile drawer, keeping every file reachable. When a completed response successfully presents between 2 and 50 files, its final file card also offers one ZIP download. Archive membership comes from the terminal delivery receipt rather than browser-supplied paths, and the ZIP contains the current file versions, which may have changed since the response. The currently selected formal artifact is refreshed once when the run finishes so edits become visible without a manual reload. Existing UTF-8 text artifacts under `/mnt/user-data/outputs` can also be edited and explicitly saved from the panel on Unix and Windows while the thread is idle; saves use content revisions to prevent overwriting agent changes. A delayed background read cannot replace a successfully saved revision in the editor; cancelled preview and full-file reads also abort their HTTP requests. Source previews also recognize extensionless `Dockerfile` and `Makefile` artifacts by their file names. Unknown file types, including names such as `constructor` and `__proto__`, retain the download fallback.
|
||||
|
||||
Unsaved artifact edits survive switching chats, workspace pages, or visiting the
|
||||
home page and returning in the same browser tab. Drafts stay in memory for the
|
||||
|
||||
@ -8,11 +8,12 @@ gates the release on them agreeing with the tag.
|
||||
|
||||
## Version sources
|
||||
|
||||
A release version must appear, identically, in four places:
|
||||
A release version must appear, identically, in five places:
|
||||
|
||||
| File | Field |
|
||||
| -------------------------------------- | -------------------- |
|
||||
| `backend/pyproject.toml` | `version = "X.Y.Z"` |
|
||||
| `backend/packages/harness/pyproject.toml` | `version = "X.Y.Z"` |
|
||||
| `frontend/package.json` | `"version": "X.Y.Z"` |
|
||||
| `deploy/helm/deer-flow/Chart.yaml` | `version: X.Y.Z` |
|
||||
| `deploy/helm/deer-flow/Chart.yaml` | `appVersion: "X.Y.Z"`|
|
||||
@ -32,7 +33,7 @@ distinguishes it from a release.
|
||||
|
||||
## Helper scripts
|
||||
|
||||
- `scripts/bump_version.sh <version>` — set all four fields at once, refresh
|
||||
- `scripts/bump_version.sh <version>` — set all five fields at once, refresh
|
||||
`backend/uv.lock`, then self-verify. Tolerates a leading `v` (e.g. `v2.1.0`).
|
||||
Needs `uv` on `PATH` (`backend/uv.lock` pins the root package version too, and
|
||||
lint CI runs `uv lock --check`); the script fails before editing anything when
|
||||
@ -77,7 +78,7 @@ distinguishes it from a release.
|
||||
|
||||
### Release candidates
|
||||
|
||||
Release-candidate tags must include the same prerelease suffix in all four
|
||||
Release-candidate tags must include the same prerelease suffix in all five
|
||||
version fields. For example, before tagging `v2.1.0-rc0`, run
|
||||
`bash scripts/bump_version.sh 2.1.0-rc0` and run
|
||||
`bash scripts/verify_versions.sh 2.1.0-rc0` from the repository root. Commit
|
||||
@ -150,7 +151,7 @@ Both features stay opt-in: the provisioner ignores them until
|
||||
|
||||
Both publishing workflows call `.github/workflows/verify-versions.yml` as their
|
||||
first job. It runs `scripts/verify_versions.sh` against the tag (minus the
|
||||
`v`). If any of the four version sources doesn't match the tag, or
|
||||
`v`). If any of the five version sources doesn't match the tag, or
|
||||
`backend/uv.lock` is stale, the verify job fails and **all** publish jobs are
|
||||
skipped — no images, no chart.
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ Shared attachment feedback uses public `attachment_delivery_text`, preserves rou
|
||||
6. Slack/Discord chat: `runs.wait()` → extract final response → publish outbound
|
||||
6b. GitHub chat (`ChannelRunPolicy.fire_and_forget=True`): `runs.create()` returns once the run is `pending`; the manager does not wait for the final state and does not publish an outbound; the agent posts its own reply via sandbox `gh`. `ConflictError` on a busy thread still trips the standard `THREAD_BUSY_MESSAGE` path (log-only on GitHub); with `buffer_followups_on_busy=True` (GitHub's default) the triggering message is captured into a per-thread buffer so a concurrent comment is not silently dropped (see "Follow-up buffering while busy").
|
||||
7. Feishu patches one card per source (`config.update_multi=true`), with topic previews and queued → running → final states. Opt-in cards show context/question; bounded tokens preserve request ID/source. Originator/option checks consume once; consumption fails closed across workers (`feishu_card_interaction.py`). Card replies keep bound aliases in fields/metadata; ordinary replies preserve `/new`. Accepted intake retires only that card's buttons/text hint before terminal PATCH; rejected intake keeps text fallback. Non-final chunks store each thread alias once per reply; the final rewrites all.
|
||||
8. Telegram edits the "Working on it..." stream target: 1s private/3s group throttle (20 msg/min), 4096-char truncation, rate-limited updates dropped; final edits split overflow. Text/preview/photo/document reply sends use `allow_sending_without_reply` only for `chat_id > 0`: missing private targets degrade atomically; group/forum routing and transport retries stay unchanged.
|
||||
8. Telegram previews: 1s private/3s group, 20/min, 4096 chars; drop rate-limited updates, split final overflow. Only private sends (`chat_id > 0`) use `allow_sending_without_reply`; degrade missing targets atomically. Preserve group/forum routing/retries and durable owner aliases. Stream mapping errors log one traceback/answer, then debug; delivery/retries continue.
|
||||
9. DingTalk AI Cards (`card_template_id`): `runs.stream()` → create card with initial text → `PUT /v1.0/card/streaming` → finalize (`is_final=True`). Creation/streaming failure uses `sampleMarkdown`; tables preserve escaped pipes and empty edge cells. Card calls get the channel token, not the SDK's.
|
||||
10. For commands (`/new`, `/status`, `/models`, `/model`, `/memory`, `/goal`, `/agent`, `/help`): handle locally or query Gateway API. `/agent list` reads the effective owner's Custom Agents; `/agent use <name>` validates in the same owner bucket, creates a new thread, and persists `channel_agent_name` in its metadata. A Custom Agent writes canonical `metadata.agent_name` so thread-search routes Web continuation to `/workspace/agents/<name>/chats/<thread_id>`; `lead_agent` omits it and stays on the ordinary chat route.
|
||||
`/model <name>` persists `channel_model_name` in the *current* thread's metadata (in place, no fresh thread); `/model` reports the effective model, `/model default` clears it (stale pins self-clear).
|
||||
|
||||
@ -323,14 +323,20 @@ class TelegramChannel(Channel):
|
||||
|
||||
if not msg.is_final:
|
||||
await self._send_stream_update(chat_id, key, msg.text, reply_to=self._parse_message_id(msg.thread_ts))
|
||||
state = self._stream_messages.get(key)
|
||||
if state is not None:
|
||||
binding = (msg.thread_id, msg.connection_id, msg.owner_user_id, msg.thread_ts, state["message_id"])
|
||||
if state.get("thread_binding") != binding and await self._remember_reply_thread(msg, state["message_id"], stream_state=state):
|
||||
state["thread_binding"] = binding
|
||||
return
|
||||
|
||||
state = self._stream_messages.pop(key, None)
|
||||
if state is not None:
|
||||
await self._remember_reply_thread(msg, state["message_id"], stream_state=state)
|
||||
if self._can_send_rich(msg.text) and await self._edit_rich_message(chat_id, state["message_id"], msg.text):
|
||||
self._last_bot_message[msg.chat_id] = state["message_id"]
|
||||
return
|
||||
await self._finalize_stream_message(chat_id, msg.chat_id, state, msg.text)
|
||||
await self._finalize_stream_message(chat_id, msg.chat_id, state, msg.text, msg)
|
||||
return
|
||||
|
||||
if self._can_send_rich(msg.text):
|
||||
@ -340,10 +346,51 @@ class TelegramChannel(Channel):
|
||||
logger.warning("[Telegram] Rich Message send failed in chat=%s; falling back to plain text: %s", chat_id, exc)
|
||||
message_id = None
|
||||
if message_id is not None:
|
||||
await self._remember_reply_thread(msg, message_id)
|
||||
return
|
||||
|
||||
for chunk in self._split_message(msg.text):
|
||||
await self._send_new_message(chat_id, msg.chat_id, chunk, _max_retries=_max_retries)
|
||||
message_id = await self._send_new_message(chat_id, msg.chat_id, chunk, _max_retries=_max_retries)
|
||||
await self._remember_reply_thread(msg, message_id)
|
||||
|
||||
async def _remember_reply_thread(self, msg: OutboundMessage, message_id: int | None, *, stream_state: dict[str, Any] | None = None) -> bool:
|
||||
"""Keep group replies on their source thread, without undoing a later /new."""
|
||||
if not msg.thread_id or message_id is None or int(msg.chat_id) >= 0:
|
||||
return True
|
||||
store = self.config.get("channel_store")
|
||||
if msg.connection_id:
|
||||
# Bound conversations must never fall back to the shared legacy store.
|
||||
if self._connection_repo is None or not msg.owner_user_id:
|
||||
return False
|
||||
elif store is None:
|
||||
return False
|
||||
topic_ids = {str(message_id)}
|
||||
if msg.thread_ts:
|
||||
topic_ids.add(msg.thread_ts)
|
||||
try:
|
||||
for topic_id in topic_ids:
|
||||
if msg.connection_id:
|
||||
await self._connection_repo.claim_thread_id(
|
||||
connection_id=msg.connection_id,
|
||||
owner_user_id=msg.owner_user_id,
|
||||
provider=self.name,
|
||||
external_conversation_id=msg.chat_id,
|
||||
external_topic_id=topic_id,
|
||||
thread_id=msg.thread_id,
|
||||
)
|
||||
else:
|
||||
await store.claim_thread_id(self.name, msg.chat_id, msg.thread_id, topic_id=topic_id)
|
||||
except Exception:
|
||||
# Delivery has already succeeded. Retry the binding on the next update,
|
||||
# not the Telegram send, which could duplicate a visible reply.
|
||||
if stream_state is not None and stream_state.get("thread_binding_error_logged"):
|
||||
logger.debug("[Telegram] reply thread mapping retry failed in chat=%s", msg.chat_id)
|
||||
else:
|
||||
logger.exception("[Telegram] failed to remember reply thread mapping in chat=%s", msg.chat_id)
|
||||
if stream_state is not None:
|
||||
stream_state["thread_binding_error_logged"] = True
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _send_stream_update(self, chat_id: int, key: str, text: str, reply_to: int | None = None) -> None:
|
||||
"""Edit the in-flight streamed message with accumulated text.
|
||||
@ -409,7 +456,7 @@ class TelegramChannel(Channel):
|
||||
state["last_edit_at"] = _monotonic()
|
||||
state["last_text"] = display
|
||||
|
||||
async def _finalize_stream_message(self, chat_id: int, chat_key: str, state: dict[str, Any], text: str) -> None:
|
||||
async def _finalize_stream_message(self, chat_id: int, chat_key: str, state: dict[str, Any], text: str, msg: OutboundMessage) -> None:
|
||||
"""Apply the final text: edit the streamed message, splitting overflow into follow-ups."""
|
||||
bot = self._application.bot
|
||||
chunks = self._split_message(text or "")
|
||||
@ -423,10 +470,12 @@ class TelegramChannel(Channel):
|
||||
else:
|
||||
# Edit could not be applied (e.g. message deleted) — deliver the
|
||||
# first chunk as a fresh message with the standard retry policy.
|
||||
await self._send_new_message(chat_id, chat_key, chunks[0])
|
||||
message_id = await self._send_new_message(chat_id, chat_key, chunks[0])
|
||||
await self._remember_reply_thread(msg, message_id, stream_state=state)
|
||||
|
||||
for chunk in chunks[1:]:
|
||||
await self._send_new_message(chat_id, chat_key, chunk)
|
||||
message_id = await self._send_new_message(chat_id, chat_key, chunk)
|
||||
await self._remember_reply_thread(msg, message_id, stream_state=state)
|
||||
|
||||
async def _edit_final_chunk(self, bot, chat_id: int, message_id: int, text: str) -> bool:
|
||||
"""Edit with one rate-limit retry. Returns False if the edit could not be applied."""
|
||||
@ -561,6 +610,7 @@ class TelegramChannel(Channel):
|
||||
sent = await bot.send_document(**kwargs)
|
||||
|
||||
self._last_bot_message[msg.chat_id] = sent.message_id
|
||||
await self._remember_reply_thread(msg, sent.message_id)
|
||||
logger.info("[Telegram] file sent: %s to chat=%s", attachment.filename, msg.chat_id)
|
||||
return True
|
||||
except Exception:
|
||||
|
||||
@ -76,28 +76,32 @@ OIDC redirects retain existing endpoint query bytes (RFC 6749 section 3.1).
|
||||
Malformed IdP bodies raise `OIDCError` (502/`sso_failed`).
|
||||
`tests/test_oidc_auth.py` pins query, state-cookie, nonce and PKCE agreement.
|
||||
|
||||
Personal Access Tokens (`app.gateway.auth.pat`, `Authorization: Bearer dfp_...`) run as their owning user: an invalid Bearer is a hard 401 with no cookie fallback, which keeps `CSRFMiddleware`'s Bearer skip safe (origin checks still run). Scopes narrow within the allowlisted threads/runs/projects routes (including `POST /api/threads/{id}/move`); every other authenticated route 403s PAT callers (admin included). PAT management and `/change-password` require session auth; only SHA-256 digests are stored (`0017`).
|
||||
PATs (`app.gateway.auth.pat`, `Authorization: Bearer dfp_...`) act as their owner.
|
||||
Invalid Bearers return 401 without cookie fallback, keeping `CSRFMiddleware`'s
|
||||
Bearer skip safe; origin checks still run. Scopes narrow allowlisted thread/run/
|
||||
project routes, including `POST /api/threads/{id}/move`; other authenticated
|
||||
routes reject PATs with 403, even for admins. PAT management and `/change-password`
|
||||
require session auth. Store only SHA-256 digests (`0017`).
|
||||
|
||||
Thread→project membership is written by thread creation (`POST /api/threads` with
|
||||
a validated `project_id`), branch creation (the new row inherits the source
|
||||
thread's project; an archived/deleted project degrades the branch to unassigned
|
||||
instead of failing), and explicit moves (`POST /api/threads/{id}/move`); run
|
||||
admission never modifies membership; it only pins the resolved project context
|
||||
read-only into the run context. The server-reserved `deerflow_project_id`
|
||||
metadata key is a read-only exposure of the `threads_meta.project_id` column and
|
||||
is stripped from client writes.
|
||||
Thread→project membership is written only by thread creation (`POST /api/threads`
|
||||
with validated `project_id`), branch creation (inherit the source project;
|
||||
archived/deleted projects become unassigned rather than failing), and explicit
|
||||
`POST /api/threads/{id}/move`. Run admission pins resolved project context
|
||||
read-only and never changes membership. Server-reserved `deerflow_project_id`
|
||||
metadata exposes `threads_meta.project_id` read-only and is stripped from client writes.
|
||||
|
||||
Localhost persistence deliberately reads the direct request `Host` and ignores `Forwarded` / `X-Forwarded-Host`. Scheme and auth-origin reconstruction still consume forwarding headers. The bundled nginx sets `X-Forwarded-Proto`, but preserves an upstream HTTPS value and does not overwrite every forwarded header, so the outer trusted proxy must replace or strip client-supplied forwarding headers before traffic reaches DeerFlow.
|
||||
Localhost persistence trusts direct `Host`, not `Forwarded`/`X-Forwarded-Host`.
|
||||
Scheme/auth-origin reconstruction still uses forwarding headers. Nginx sets
|
||||
`X-Forwarded-Proto` but preserves upstream HTTPS and some other forwarded headers;
|
||||
the outer trusted proxy must replace/strip client copies before DeerFlow.
|
||||
|
||||
Standalone local LangGraph Studio is recognized only through the upstream
|
||||
`Auth.types.StudioUser` principal type, never by its reusable identity string.
|
||||
The type is resolved once at import; an older SDK without it degrades to normal
|
||||
owner scoping.
|
||||
For that principal's assistant reads/searches, `langgraph_auth.add_owner_filter`
|
||||
selects genuine server-registered assistants plus assistants owned by Studio;
|
||||
all other resources remain owner-scoped. Assistant create/update handlers make
|
||||
both `user_id` and `created_by=user` server-owned, because LangGraph gives
|
||||
`created_by=system` privileged ownership semantics during run creation. The
|
||||
Recognize standalone Studio only by upstream `Auth.types.StudioUser`, never its
|
||||
reusable identity string. Resolve the type once at import; older SDKs without
|
||||
it retain normal owner scoping. For its assistant reads/searches,
|
||||
`langgraph_auth.add_owner_filter` admits genuine server-registered assistants
|
||||
and Studio-owned rows; other resources stay owner-scoped. Assistant create/update
|
||||
forces server-owned `user_id` and `created_by=user`: LangGraph treats
|
||||
`created_by=system` as privileged run ownership. The
|
||||
custom application module in `langgraph_studio.py` is imported before the
|
||||
locked in-memory runtime lifespan. At that pre-runtime boundary it derives
|
||||
genuine system assistant IDs from the CLI-provided graph registry, removes
|
||||
|
||||
@ -35,6 +35,8 @@ class SubagentResponse(BaseModel):
|
||||
disallowed_tools: list[str] | None = None
|
||||
skills: list[str] | None = None
|
||||
model: str = "inherit"
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
max_turns: int = 50
|
||||
timeout_seconds: int = 900
|
||||
enabled: bool = True
|
||||
@ -57,6 +59,8 @@ class ManagedSubagentCreateRequest(BaseModel):
|
||||
disallowed_tools: list[str] | None = None
|
||||
skills: list[str] | None = None
|
||||
model: str = "inherit"
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
max_turns: int = Field(default=50, ge=1)
|
||||
timeout_seconds: int = Field(default=900, ge=1)
|
||||
enabled: bool = True
|
||||
@ -70,6 +74,8 @@ class ManagedSubagentUpdateRequest(BaseModel):
|
||||
disallowed_tools: list[str] | None = None
|
||||
skills: list[str] | None = None
|
||||
model: str | None = None
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
max_turns: int | None = Field(default=None, ge=1)
|
||||
timeout_seconds: int | None = Field(default=None, ge=1)
|
||||
enabled: bool | None = None
|
||||
@ -94,7 +100,7 @@ def _explicit_overrides(name: str, app_config) -> dict[str, Any]:
|
||||
if override is None:
|
||||
return {}
|
||||
result: dict[str, Any] = {}
|
||||
for field in ("timeout_seconds", "max_turns", "model", "skills"):
|
||||
for field in ("timeout_seconds", "max_turns", "model", "thinking_enabled", "reasoning_effort", "skills"):
|
||||
if field not in override.model_fields_set:
|
||||
continue
|
||||
value = getattr(override, field)
|
||||
@ -119,6 +125,8 @@ def _catalog(include_system_prompt: bool) -> SubagentsListResponse:
|
||||
disallowed_tools=definition.disallowed_tools,
|
||||
skills=definition.skills,
|
||||
model=definition.model,
|
||||
thinking_enabled=definition.thinking_enabled,
|
||||
reasoning_effort=definition.reasoning_effort,
|
||||
max_turns=definition.max_turns,
|
||||
timeout_seconds=definition.timeout_seconds,
|
||||
source="builtin",
|
||||
@ -136,6 +144,8 @@ def _catalog(include_system_prompt: bool) -> SubagentsListResponse:
|
||||
disallowed_tools=definition.disallowed_tools,
|
||||
skills=definition.skills,
|
||||
model=definition.model,
|
||||
thinking_enabled=definition.thinking_enabled,
|
||||
reasoning_effort=definition.reasoning_effort,
|
||||
max_turns=definition.max_turns,
|
||||
timeout_seconds=definition.timeout_seconds,
|
||||
source="config",
|
||||
|
||||
@ -1689,11 +1689,60 @@ class McpTaskService:
|
||||
_CANCELLATION_DRAIN_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
def _log_pending_compensation_tasks(self, *, reason: str) -> None:
|
||||
pending_count = sum(not task.done() for task in self._compensation_tasks)
|
||||
if pending_count:
|
||||
logger.warning(
|
||||
"%s; %d MCP compensation task(s) remain in the background",
|
||||
reason,
|
||||
pending_count,
|
||||
)
|
||||
|
||||
async def _drain_compensation_tasks(self, *, deadline: float) -> None:
|
||||
"""Drain service-owned compensation work before Gateway dependencies close."""
|
||||
loop = asyncio.get_running_loop()
|
||||
while self._compensation_tasks:
|
||||
tasks = tuple(self._compensation_tasks)
|
||||
remaining = deadline - loop.time()
|
||||
if remaining <= 0:
|
||||
break
|
||||
done, _ = await asyncio.wait(
|
||||
tasks,
|
||||
timeout=remaining,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
if not done:
|
||||
break
|
||||
# Run completion callbacks before taking the next snapshot. A
|
||||
# handoff may finish one task while registering another.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
self._log_pending_compensation_tasks(
|
||||
reason="MCP compensation drain reached its deadline",
|
||||
)
|
||||
|
||||
async def _drain_compensation_tasks_preserving_cancellation(self, *, deadline: float) -> None:
|
||||
"""Finish the bounded drain before propagating caller cancellation."""
|
||||
drain_task = asyncio.create_task(
|
||||
self._drain_compensation_tasks(deadline=deadline),
|
||||
name="deerflow-mcp-compensation-drain",
|
||||
)
|
||||
try:
|
||||
await asyncio.shield(drain_task)
|
||||
except asyncio.CancelledError:
|
||||
await wait_for_task_until(drain_task, deadline=deadline)
|
||||
if drain_task.done():
|
||||
drain_task.result()
|
||||
raise
|
||||
|
||||
async def stop(self) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
task = self._task
|
||||
if task is None:
|
||||
await self._drain_compensation_tasks_preserving_cancellation(
|
||||
deadline=loop.time() + _CANCELLATION_DRAIN_TIMEOUT_SECONDS,
|
||||
)
|
||||
return
|
||||
loop = asyncio.get_running_loop()
|
||||
if self._stopping_task is not task:
|
||||
self._stopping_task = task
|
||||
self._stop_deadline = loop.time() + _CANCELLATION_DRAIN_TIMEOUT_SECONDS
|
||||
@ -1710,9 +1759,19 @@ class McpTaskService:
|
||||
except asyncio.CancelledError:
|
||||
if not await wait_for_task_until(task, deadline=deadline):
|
||||
self._log_stop_timeout(task)
|
||||
self._log_pending_compensation_tasks(
|
||||
reason="MCP task poller cleanup reached its deadline before compensation drain could start",
|
||||
)
|
||||
else:
|
||||
await self._drain_compensation_tasks_preserving_cancellation(deadline=deadline)
|
||||
raise
|
||||
if task not in done:
|
||||
self._log_stop_timeout(task)
|
||||
self._log_pending_compensation_tasks(
|
||||
reason="MCP task poller cleanup reached its deadline before compensation drain could start",
|
||||
)
|
||||
return
|
||||
await self._drain_compensation_tasks_preserving_cancellation(deadline=deadline)
|
||||
|
||||
async def _run_loop(self) -> None:
|
||||
while not self._stop.is_set():
|
||||
|
||||
@ -333,6 +333,25 @@ Checkpointer storage runs in one of two channel modes, selected by `checkpoint_c
|
||||
|
||||
## Deferred terminal commits and lease ownership
|
||||
|
||||
RunManager serializes creation, replacement and assembly publication per thread.
|
||||
The global registry lock never spans Store I/O; unrelated cached reads and
|
||||
heartbeat snapshots do not queue behind an admission gate. Their own Store I/O
|
||||
can still encounter database or connection-pool contention. A pending-ID map
|
||||
lets readers of preallocated run IDs join the same-thread gate before the record
|
||||
is published. Failed or cancelled admissions remove that temporary membership;
|
||||
normal creation registers only after its initial write succeeds. Heartbeats use
|
||||
the short registry snapshot, not the thread gate, so an admitted run can renew
|
||||
while its replacement awaits a Store result. Preserve terminal-attribution and
|
||||
cancellation compensation below; changing lock granularity does not grant a new
|
||||
worker authority over a durable row.
|
||||
|
||||
Assembly evidence persists a full row snapshot. Serialize that write with the
|
||||
same run's lease renewal and local acknowledgment, so it cannot restore an old
|
||||
lease after renewal. Lock order is thread gate, then snapshot gate, then the
|
||||
short registry lock. Renewal uses only the snapshot gate and must release it
|
||||
before signaling local cancellation, which acquires the thread gate. Keep the
|
||||
lease-expiry timer armed while waiting for the snapshot gate.
|
||||
|
||||
**Deferred terminal commit:** With an event store, the worker stages its terminal
|
||||
status locally and commits it only after finalization's receipt and duration
|
||||
writes. `RunRecord.terminal_commit_pending` keeps `_renew_leases()` renewing that
|
||||
|
||||
@ -106,6 +106,14 @@ class SubagentOverrideConfig(BaseModel):
|
||||
min_length=1,
|
||||
description="Model name for this subagent (None = inherit from parent agent)",
|
||||
)
|
||||
thinking_enabled: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable thinking/reasoning for this subagent (None = no override)",
|
||||
)
|
||||
reasoning_effort: str | None = Field(
|
||||
default=None,
|
||||
description="Reasoning effort accepted or mapped by the selected model (None = use its default)",
|
||||
)
|
||||
skills: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Skill names whitelist for this subagent (None = inherit all enabled skills, [] = no skills)",
|
||||
@ -146,6 +154,14 @@ class CustomSubagentConfig(BaseModel):
|
||||
default="inherit",
|
||||
description="Model to use - 'inherit' uses parent's model",
|
||||
)
|
||||
thinking_enabled: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable thinking/reasoning for this subagent (None = no override)",
|
||||
)
|
||||
reasoning_effort: str | None = Field(
|
||||
default=None,
|
||||
description="Reasoning effort accepted or mapped by the selected model (None = use its default)",
|
||||
)
|
||||
max_turns: int = Field(
|
||||
default=50,
|
||||
ge=1,
|
||||
@ -266,6 +282,20 @@ class SubagentsAppConfig(BaseModel):
|
||||
return override.skills
|
||||
return None
|
||||
|
||||
def get_thinking_enabled_for(self, agent_name: str) -> bool | None:
|
||||
"""Return an explicit per-agent thinking override, including False."""
|
||||
override = self.agents.get(agent_name)
|
||||
if override is not None and override.thinking_enabled is not None:
|
||||
return override.thinking_enabled
|
||||
return None
|
||||
|
||||
def get_reasoning_effort_for(self, agent_name: str) -> str | None:
|
||||
"""Return the per-agent effort request; the selected model resolves it."""
|
||||
override = self.agents.get(agent_name)
|
||||
if override is not None and override.reasoning_effort is not None:
|
||||
return override.reasoning_effort
|
||||
return None
|
||||
|
||||
def get_token_budget_for(
|
||||
self,
|
||||
agent_name: str,
|
||||
@ -331,6 +361,10 @@ def load_subagents_config_from_dict(config_dict: dict) -> None:
|
||||
parts.append(f"max_turns={override.max_turns}")
|
||||
if override.model is not None:
|
||||
parts.append(f"model={override.model}")
|
||||
if override.thinking_enabled is not None:
|
||||
parts.append(f"thinking_enabled={override.thinking_enabled}")
|
||||
if override.reasoning_effort is not None:
|
||||
parts.append(f"reasoning_effort={override.reasoning_effort}")
|
||||
if override.skills is not None:
|
||||
parts.append(f"skills={override.skills}")
|
||||
if parts:
|
||||
|
||||
@ -11,7 +11,7 @@ The optional background-task `status` filter flows through `McpTaskSubmitter.lis
|
||||
|
||||
- Uses `langchain-mcp-adapters` `MultiServerMCPClient` for multi-server management
|
||||
- **OAuth expiry**: Missing, non-convertible or overflowing `expires_in` values use the one-hour default. Guard both `timedelta` construction and addition to one captured UTC `now`; avoid float-derived range ceilings. `tests/test_mcp_oauth.py` pins both overflow stages and the exact `datetime` boundary with a fixed clock.
|
||||
- **Long-running task foundation**: `mcp/tasks/` defines the protocol-neutral `McpTaskDriver` and `TaskSnapshot` states (`submitted`, `working`, `input_required`, `completed`, `failed`, `cancelled`); validate finite positive `poll_after_seconds` at that shared boundary. `persistence/mcp_tasks/` owns remote handles, schedules, notification state, leases and separate consecutive poll/delivery error counters. `app/mcp_tasks/McpTaskService` runs outside the Agent/LLM loop. Every claim gets a fresh token; releases and snapshot writes match owner/token, and results require an unexpired lease even before a peer reclaims. Each claim phase has a single-flight owner while its database outcome is ambiguous: later scans skip with a warning. Once it resolves, release that phase owner immediately and hand returned rows to bounded, service-owned release work. Token fencing protects newer generations; a stuck release must not block later claims. Expiry is the cross-process crash fallback, not normal recovery for a live service's late claim. Poll timestamps and retry schedules use remote completion time. Submission persistence failure/cancellation best-effort cancels the remote handle while preserving the original error, except `uq_mcp_tasks_user_server_remote`: an existing row owns that handle and must not be cancelled. Isolate per-task failures; unexpected poll failures recover by expiry. Shutdown cancels the poller with bounded handoff. Cancellation release clears only owner/token and preserves diagnostics, preclaim schedules and poll-failure state; cancellation is not a task failure. Real failures retain backoff and tracking degradation. `input_required` and terminal events become pending notifications; delivery budgets, idempotency and strict target rejection are detailed below. Durable recovery needs SQLite/PostgreSQL; memory leaves the service unavailable. The startup-configured `mcp_tasks` runtime is disabled by default until a concrete driver is registered and does not change ordinary tool behavior.
|
||||
- **Long-running task foundation**: `mcp/tasks/` defines the protocol-neutral `McpTaskDriver` and `TaskSnapshot` states (`submitted`, `working`, `input_required`, `completed`, `failed`, `cancelled`); validate finite positive `poll_after_seconds` at that shared boundary. `persistence/mcp_tasks/` owns remote handles, schedules, notification state, leases and separate consecutive poll/delivery error counters. `app/mcp_tasks/McpTaskService` runs outside the Agent/LLM loop. Every claim gets a fresh token; releases and snapshot writes match owner/token, and results require an unexpired lease even before a peer reclaims. Each claim phase has a single-flight owner while its database outcome is ambiguous: later scans skip with a warning. Once it resolves, release that phase owner immediately and hand returned rows to bounded, service-owned release work. Token fencing protects newer generations; a stuck release must not block later claims. Expiry is the cross-process crash fallback, not normal recovery for a live service's late claim. Poll timestamps and retry schedules use remote completion time. Submission persistence failure/cancellation best-effort cancels the remote handle while preserving the original error, except `uq_mcp_tasks_user_server_remote`: an existing row owns that handle and must not be cancelled. Isolate per-task failures; unexpected poll failures recover by expiry. Shutdown cancels the poller with bounded handoff, then drains service-owned compensation work within the same deadline; timed-out work remains tracked in the background. Cancellation release clears only owner/token and preserves diagnostics, preclaim schedules and poll-failure state; cancellation is not a task failure. Real failures retain backoff and tracking degradation. `input_required` and terminal events become pending notifications; delivery budgets, idempotency and strict target rejection are detailed below. Durable recovery needs SQLite/PostgreSQL; memory leaves the service unavailable. The startup-configured `mcp_tasks` runtime is disabled by default until a concrete driver is registered and does not change ordinary tool behavior.
|
||||
- **Control-call ownership**: Poll claims recheck eligibility with a conditional UPDATE after SELECT; cancel claims reserve SQLite writers with `BEGIN IMMEDIATE` before SELECT. PostgreSQL retains `SKIP LOCKED`. `renew_control_claim` atomically matches phase, owner, token, nonterminal state and unexpired lease, preserves `updated_at`, and never shortens expiry on a backward clock correction. `McpTaskService._with_control_lease` validates admission, renews every lease/3 with a lease/3 renewal deadline, and supervises the entire operation through persistence. Ownership loss or uncertain renewal cancels the local operation; the task-local loss fence rejects late snapshot/retry writes if the driver swallows cancellation. Bounded handoff retains operation/renewal cleanup through caller cancellation or autonomous completion/loss. Local active IDs remain excluded from poll/cancel claims and consume control capacity until cleanup settles. Notification claims use their existing independent lifecycle. Contract/tests: [control leases](../../../../docs/mcp-control-leases.md), `tests/test_mcp_task_lease_lifecycle.py`.
|
||||
- **Runtime availability boundary**: the installed process-local submitter is the source of truth for durable task-management tool exposure. `mcp_tasks` is startup-only; changing it on disk does not alter the live toolset until the Gateway restarts.
|
||||
- **Task OAuth lifetime**: `tasks/runtime.py` owns shared OAuth state only for enabled deployment servers with `task_toolsets`, from `set_mcp_task_config_snapshot(config)` until its `None` teardown. Use `_task_enabled_servers` for both snapshot and OAuth selection. Validate the frozen snapshot before reusing that state for discovery, ordinary tools, or durable calls. Cache resets must not discard it. Keep tokens and the cross-loop refresh lock together; rotate a private config copy, never the parsed snapshot or config file. Non-task servers remain reloadable and personal callers use independent managers, even on a name collision.
|
||||
|
||||
@ -32,6 +32,8 @@ class ManagedSubagentDefinition(BaseModel):
|
||||
disallowed_tools: list[str] = Field(default_factory=lambda: sorted(REQUIRED_DISALLOWED_TOOLS))
|
||||
skills: list[str] | None = None
|
||||
model: str = "inherit"
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
max_turns: int = Field(default=50, ge=1)
|
||||
timeout_seconds: int = Field(default=900, ge=1)
|
||||
enabled: bool = True
|
||||
|
||||
@ -13,6 +13,13 @@ errors preserve cache fallbacks and valid neighbors, including Redis outages/pur
|
||||
Escape literal stems for SCAN; recheck byte/string keys before UNLINK. Preserve
|
||||
namespace/thread bounds, cursor iteration and outage/TTL fallback.
|
||||
|
||||
### Run Admission
|
||||
|
||||
Run admission and assembly publication serialize per thread across Store I/O.
|
||||
Keep registry locks short; heartbeat snapshots must not queue on an
|
||||
admission gate. Preserve same-thread publication visibility, including reads of
|
||||
preallocated run IDs. Details: [admission isolation](../../../../docs/runtime-guidance-details.md#deferred-terminal-commits-and-lease-ownership).
|
||||
|
||||
### Stream Bridge Heartbeats
|
||||
|
||||
Observed Redis streams removed during replay yield `StreamGap` with empty bounds; initially empty streams still wait. After this gap, `/wait` refreshes store-only status and completes only for a terminal run with no retained stream.
|
||||
|
||||
@ -10,7 +10,7 @@ import socket
|
||||
import sqlite3
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from enum import StrEnum
|
||||
@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any
|
||||
from sqlalchemy.exc import IntegrityError as SAIntegrityError
|
||||
|
||||
from deerflow.runtime.events.terminal import RecoveryTerminalPublisher
|
||||
from deerflow.runtime.keyed_lock import AsyncKeyedLockTable
|
||||
from deerflow.runtime.user_context import AUTO, _AutoSentinel, get_current_user, resolve_user_id
|
||||
from deerflow.utils.time import is_lease_expired
|
||||
from deerflow.utils.time import now_iso as _now_iso
|
||||
@ -353,6 +354,9 @@ class RunManager:
|
||||
# preserving ``_runs`` iteration order (see ``_thread_records_locked``).
|
||||
self._runs_by_thread: dict[str, dict[str, None]] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._admission_locks = AsyncKeyedLockTable[str]()
|
||||
self._snapshot_locks = AsyncKeyedLockTable[str]()
|
||||
self._pending_admissions: dict[str, dict[str, int]] = {}
|
||||
self._store = store
|
||||
self._persistence_retry_policy = persistence_retry_policy or PersistenceRetryPolicy()
|
||||
self._worker_id = worker_id or _generate_worker_id()
|
||||
@ -370,6 +374,43 @@ class RunManager:
|
||||
self._lease_operations: dict[str, asyncio.Task] = {}
|
||||
self._lease_deadlines: dict[str, asyncio.TimerHandle] = {}
|
||||
|
||||
@asynccontextmanager
|
||||
async def _admission_scope(self, thread_id: str, run_id: str) -> AsyncIterator[None]:
|
||||
# Manager state is owned by one event loop. Register before the first
|
||||
# await so readers of a preallocated ID also wait for its publication.
|
||||
threads = self._pending_admissions.setdefault(run_id, {})
|
||||
threads[thread_id] = threads.get(thread_id, 0) + 1
|
||||
try:
|
||||
async with self._admission_locks.hold(thread_id):
|
||||
yield
|
||||
finally:
|
||||
threads[thread_id] -= 1
|
||||
if not threads[thread_id]:
|
||||
del threads[thread_id]
|
||||
if not threads:
|
||||
self._pending_admissions.pop(run_id, None)
|
||||
|
||||
@asynccontextmanager
|
||||
async def _run_scope(self, run_id: str) -> AsyncIterator[None]:
|
||||
threads = set(self._pending_admissions.get(run_id, ()))
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
threads.add(record.thread_id)
|
||||
async with AsyncExitStack() as stack:
|
||||
for thread_id in sorted(threads):
|
||||
await stack.enter_async_context(self._admission_locks.hold(thread_id))
|
||||
yield
|
||||
|
||||
@asynccontextmanager
|
||||
async def _run_registry_lock(self, run_id: str) -> AsyncIterator[None]:
|
||||
async with self._run_scope(run_id), self._lock:
|
||||
yield
|
||||
|
||||
@asynccontextmanager
|
||||
async def _thread_registry_lock(self, thread_id: str) -> AsyncIterator[None]:
|
||||
async with self._admission_locks.hold(thread_id), self._lock:
|
||||
yield
|
||||
|
||||
def _index_run_locked(self, record: RunRecord) -> None:
|
||||
"""Register *record* in the thread index. Caller must hold ``self._lock``."""
|
||||
self._runs_by_thread.setdefault(record.thread_id, {})[record.run_id] = None
|
||||
@ -500,8 +541,8 @@ class RunManager:
|
||||
Initial run creation is part of the run visibility boundary: callers
|
||||
should not observe a run in memory unless its backing store row exists.
|
||||
Unlike follow-up status/model updates, failures are propagated so the
|
||||
caller can treat creation as failed. Rollback is the caller's
|
||||
responsibility after inserting the record into ``_runs``.
|
||||
caller can treat creation as failed. The caller keeps the record private
|
||||
until this method returns successfully.
|
||||
"""
|
||||
if self._store is None:
|
||||
return
|
||||
@ -793,7 +834,7 @@ class RunManager:
|
||||
"""Persist token usage and completion data to the backing store."""
|
||||
row_recovery_payload: dict[str, Any] | None = None
|
||||
record: RunRecord | None = None
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None and record.ownership_lost:
|
||||
logger.warning("Skipped completion persistence for run %s after lease ownership was lost", run_id)
|
||||
@ -889,7 +930,7 @@ class RunManager:
|
||||
async def update_run_progress(self, run_id: str, **kwargs) -> None:
|
||||
"""Persist a running token/message snapshot without changing status."""
|
||||
should_persist = True
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
should_persist = record.status == RunStatus.running and not record.ownership_lost
|
||||
@ -907,7 +948,7 @@ class RunManager:
|
||||
async def update_finalizing_progress(self, run_id: str, **kwargs) -> None:
|
||||
"""Persist final fields while the durable row is deliberately active."""
|
||||
should_persist = False
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None and not record.ownership_lost:
|
||||
should_persist = record.status not in (RunStatus.pending, RunStatus.running)
|
||||
@ -931,7 +972,7 @@ class RunManager:
|
||||
|
||||
async def seal_completed_evidence(self, run_id: str, receipt) -> bool:
|
||||
"""Best-effort evidence metadata is independent of business completion."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or record.ownership_lost:
|
||||
return False
|
||||
@ -942,7 +983,7 @@ class RunManager:
|
||||
return await seal(run_id, owner_worker_id=owner_worker_id, receipt=receipt)
|
||||
|
||||
async def mark_evidence_partial(self, run_id: str, *, error: str) -> bool:
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or record.ownership_lost:
|
||||
return False
|
||||
@ -998,21 +1039,15 @@ class RunManager:
|
||||
evidence_origin=evidence_origin,
|
||||
evidence_agent_id=evidence_agent_id,
|
||||
)
|
||||
async with self._lock:
|
||||
self._runs[run_id] = record
|
||||
self._index_run_locked(record)
|
||||
persisted = False
|
||||
async with self._admission_scope(thread_id, run_id):
|
||||
try:
|
||||
await self._persist_new_run_to_store(record)
|
||||
persisted = True
|
||||
except Exception:
|
||||
logger.warning("Failed to persist run %s; rolled back in-memory record", run_id, exc_info=True)
|
||||
logger.warning("Failed to persist run %s; record was not registered", run_id, exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
# Also covers cancellation, which bypasses ``except Exception``.
|
||||
if not persisted:
|
||||
self._runs.pop(run_id, None)
|
||||
self._unindex_run_locked(run_id, record.thread_id)
|
||||
async with self._lock:
|
||||
self._runs[run_id] = record
|
||||
self._index_run_locked(record)
|
||||
logger.info("Run created: run_id=%s thread_id=%s", run_id, thread_id)
|
||||
return record
|
||||
|
||||
@ -1031,7 +1066,7 @@ class RunManager:
|
||||
raise_on_store_error: Propagate store hydration/mapping failures so
|
||||
lifecycle callers can distinguish them from a missing run.
|
||||
"""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
return record
|
||||
@ -1046,7 +1081,7 @@ class RunManager:
|
||||
return None
|
||||
# Re-check after store await: a concurrent create() may have inserted the
|
||||
# in-memory record while the store call was in flight.
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
return record
|
||||
@ -1115,7 +1150,7 @@ class RunManager:
|
||||
def _page(records: list[RunRecord]) -> list[RunRecord]:
|
||||
return sorted(records, key=lambda record: run_sort_key(record.created_at, record.run_id), reverse=True)[:limit]
|
||||
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
memory_records = [
|
||||
record
|
||||
for record in self._thread_records_locked(thread_id)
|
||||
@ -1169,7 +1204,7 @@ class RunManager:
|
||||
correct pagination.
|
||||
"""
|
||||
resolved_user_id = resolve_user_id(user_id, method_name="RunManager.list_successful_regenerate_sources")
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
memory_records = [record for record in self._thread_records_locked(thread_id) if record.operation_kind == ThreadOperationKind.run and (resolved_user_id is None or record.user_id == resolved_user_id)]
|
||||
|
||||
sources = set(await self._store.list_successful_regenerate_sources(thread_id, user_id=resolved_user_id)) if self._store is not None else set()
|
||||
@ -1240,7 +1275,7 @@ class RunManager:
|
||||
continue
|
||||
records_by_id[record.run_id] = record
|
||||
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
memory_records = [record for record in self._thread_records_locked(thread_id) if resolved_user_id is None or record.user_id == resolved_user_id]
|
||||
for record in memory_records:
|
||||
records_by_id[record.run_id] = record
|
||||
@ -1249,13 +1284,13 @@ class RunManager:
|
||||
|
||||
async def try_start(self, run_id: str) -> RunStartOutcome:
|
||||
"""Transition an uncancelled pending run to running before building the agent."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None:
|
||||
raise RunStartupError(f"Cannot start unknown run {run_id}")
|
||||
|
||||
async with record.start_lock:
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
if record.abort_event.is_set() or record.status != RunStatus.pending:
|
||||
return RunStartOutcome.cancelled
|
||||
|
||||
@ -1269,7 +1304,7 @@ class RunManager:
|
||||
except Exception as exc:
|
||||
raise RunStartupError(f"Failed to start run {run_id}: {exc}") from exc
|
||||
if updated is False:
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
if record.status == RunStatus.pending:
|
||||
record.status = RunStatus.interrupted
|
||||
record.terminal_status_persisted = False
|
||||
@ -1277,7 +1312,7 @@ class RunManager:
|
||||
record.updated_at = _now_iso()
|
||||
return RunStartOutcome.cancelled
|
||||
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
if record.abort_event.is_set() or record.status != RunStatus.pending:
|
||||
restore_status = record.status
|
||||
restore_error = record.error
|
||||
@ -1299,7 +1334,7 @@ class RunManager:
|
||||
|
||||
async def fail_start_if_pending(self, run_id: str, *, error: str) -> bool:
|
||||
"""Mark an admitted run as failed if its worker task could not be attached."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or record.status != RunStatus.pending:
|
||||
return False
|
||||
@ -1314,7 +1349,7 @@ class RunManager:
|
||||
RunStatus.error,
|
||||
error=error,
|
||||
)
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
if self._runs.get(run_id) is record:
|
||||
record.terminal_status_persisted = persisted
|
||||
return True
|
||||
@ -1330,7 +1365,7 @@ class RunManager:
|
||||
if not run_ids:
|
||||
return {}
|
||||
resolved_user_id = resolve_user_id(user_id, method_name="RunManager.get_many_by_thread")
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
records_by_id = {
|
||||
record.run_id: record for record in self._thread_records_locked(thread_id) if record.operation_kind == ThreadOperationKind.run and record.run_id in run_ids and (resolved_user_id is None or record.user_id == resolved_user_id)
|
||||
}
|
||||
@ -1365,7 +1400,7 @@ class RunManager:
|
||||
persist: bool = True,
|
||||
) -> None:
|
||||
"""Transition a run to a new status."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None:
|
||||
logger.warning("set_status called for unknown run %s", run_id)
|
||||
@ -1401,7 +1436,7 @@ class RunManager:
|
||||
if persist:
|
||||
persisted = await self._persist_status(record, status, error=error, stop_reason=stop_reason)
|
||||
if status not in (RunStatus.pending, RunStatus.running):
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record.terminal_status_persisted = self._has_final_terminal_ack(record) if self.heartbeat_enabled and self._store is not None else persisted
|
||||
if not persisted and self.heartbeat_enabled and status == RunStatus.success and not record.ownership_lost:
|
||||
await self._mark_ownership_lost(
|
||||
@ -1415,7 +1450,7 @@ class RunManager:
|
||||
|
||||
async def persist_current_status(self, run_id: str) -> bool:
|
||||
"""Persist the status already staged on the in-memory run record."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None:
|
||||
logger.warning("persist_current_status called for unknown run %s", run_id)
|
||||
@ -1425,7 +1460,7 @@ class RunManager:
|
||||
stop_reason = record.stop_reason
|
||||
persisted = await self._persist_status(record, status, error=error, stop_reason=stop_reason)
|
||||
if status not in (RunStatus.pending, RunStatus.running):
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record.terminal_status_persisted = self._has_final_terminal_ack(record) if self.heartbeat_enabled and self._store is not None else persisted
|
||||
if not persisted and self.heartbeat_enabled and status == RunStatus.success and not record.ownership_lost:
|
||||
await self._mark_ownership_lost(
|
||||
@ -1457,7 +1492,7 @@ class RunManager:
|
||||
)
|
||||
return None
|
||||
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or record.store_only or record.ownership_lost:
|
||||
return None
|
||||
@ -1507,7 +1542,7 @@ class RunManager:
|
||||
|
||||
async def set_finalizing(self, run_id: str, finalizing: bool) -> None:
|
||||
"""Mark whether a run is performing post-cancel cleanup."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None:
|
||||
logger.warning("set_finalizing called for unknown run %s", run_id)
|
||||
@ -1525,7 +1560,7 @@ class RunManager:
|
||||
) -> None:
|
||||
"""Wait until older same-thread runs have finished post-cancel cleanup."""
|
||||
while True:
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
found_current = False
|
||||
prior_finalizing = False
|
||||
for record in self._thread_records_locked(thread_id):
|
||||
@ -1549,7 +1584,7 @@ class RunManager:
|
||||
|
||||
async def has_later_run(self, thread_id: str, run_id: str) -> bool:
|
||||
"""Return whether a newer in-memory run has been admitted for the thread."""
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
seen_current = False
|
||||
for record in self._thread_records_locked(thread_id):
|
||||
if record.run_id == run_id:
|
||||
@ -1561,7 +1596,7 @@ class RunManager:
|
||||
|
||||
async def has_later_started_run(self, thread_id: str, run_id: str) -> bool:
|
||||
"""Return whether a newer same-thread run may have already advanced state."""
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
seen_current = False
|
||||
for record in self._thread_records_locked(thread_id):
|
||||
if record.run_id == run_id:
|
||||
@ -1588,22 +1623,23 @@ class RunManager:
|
||||
"""Publish only host assembly evidence, once, before execution."""
|
||||
from deerflow.runtime.assembly_evidence import EVIDENCE_KEY
|
||||
|
||||
async with self._lock:
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or evidence.get("run_id") != run_id or evidence.get("thread_id") != record.thread_id:
|
||||
raise ValueError("Assembly/run binding mismatch")
|
||||
if EVIDENCE_KEY in record.metadata:
|
||||
if record.metadata[EVIDENCE_KEY] != evidence:
|
||||
raise ValueError("Run assembly evidence is immutable")
|
||||
return
|
||||
record.metadata = {**record.metadata, EVIDENCE_KEY: copy.deepcopy(evidence)}
|
||||
async with self._run_scope(run_id), self._snapshot_locks.hold(run_id):
|
||||
async with self._lock:
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or evidence.get("run_id") != run_id or evidence.get("thread_id") != record.thread_id:
|
||||
raise ValueError("Assembly/run binding mismatch")
|
||||
if EVIDENCE_KEY in record.metadata:
|
||||
if record.metadata[EVIDENCE_KEY] != evidence:
|
||||
raise ValueError("Run assembly evidence is immutable")
|
||||
return
|
||||
record.metadata = {**record.metadata, EVIDENCE_KEY: copy.deepcopy(evidence)}
|
||||
if not await self._persist_to_store(record):
|
||||
record.metadata.pop(EVIDENCE_KEY, None)
|
||||
raise RuntimeError("Unable to persist assembly evidence")
|
||||
|
||||
async def update_model_name(self, run_id: str, model_name: str | None) -> None:
|
||||
"""Update the model name for a run."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None:
|
||||
logger.warning("update_model_name called for unknown run %s", run_id)
|
||||
@ -1688,7 +1724,7 @@ class RunManager:
|
||||
action: str,
|
||||
) -> None:
|
||||
"""Set process-local abort state without status persistence or cleanup."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is None or (record.status not in (RunStatus.pending, RunStatus.running) and not self._awaits_terminal_commit(record)) or record.abort_event.is_set():
|
||||
return
|
||||
@ -1746,7 +1782,7 @@ class RunManager:
|
||||
# ------------------------------------------------------------------
|
||||
# Local path — this worker owns the run in-memory.
|
||||
# ------------------------------------------------------------------
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
if record.status == RunStatus.interrupted:
|
||||
@ -1771,7 +1807,7 @@ class RunManager:
|
||||
elif outcome != CancelOutcome.lease_valid_elsewhere:
|
||||
return outcome
|
||||
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
if record.status == RunStatus.interrupted or record.abort_event.is_set():
|
||||
@ -1797,7 +1833,7 @@ class RunManager:
|
||||
# cancellation; ordinary runs retain immediate persistence.
|
||||
return CancelOutcome.cancelled
|
||||
persisted = await self._persist_status(record, RunStatus.interrupted)
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
if self._runs.get(run_id) is record:
|
||||
record.terminal_status_persisted = self._has_final_terminal_ack(record) if self.heartbeat_enabled and self._store is not None else persisted
|
||||
if not persisted and self._store is not None:
|
||||
@ -2001,14 +2037,14 @@ class RunManager:
|
||||
raise RuntimeError(f"Cancelled admission {record.run_id} remains active in the run store")
|
||||
|
||||
if stored is None:
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(record.run_id):
|
||||
if self._runs.get(record.run_id) is record:
|
||||
self._runs.pop(record.run_id, None)
|
||||
self._unindex_run_locked(record.run_id, record.thread_id)
|
||||
return
|
||||
|
||||
stored_status = RunStatus(stored.get("status") or RunStatus.pending.value)
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(record.run_id):
|
||||
if self._runs.get(record.run_id) is record:
|
||||
record.status = stored_status
|
||||
record.terminal_status_persisted = stored_status not in (
|
||||
@ -2044,7 +2080,7 @@ class RunManager:
|
||||
already has a pending/running run. For ``interrupt``/``rollback``,
|
||||
cancels inflight runs before creating.
|
||||
|
||||
Lock ordering invariant: the local ``self._lock`` is held across
|
||||
Lock ordering invariant: the per-thread admission gate is held across
|
||||
the local check, the store insert, and the local register, so the
|
||||
store insert can never succeed while a same-worker ConflictError
|
||||
is about to fire (which would leak a pending row in the store).
|
||||
@ -2093,52 +2129,53 @@ class RunManager:
|
||||
idempotency_request=idempotency_request,
|
||||
)
|
||||
|
||||
async with self._lock:
|
||||
if idempotency_key is not None and execution_fence is None:
|
||||
for existing in self._runs.values():
|
||||
if existing.idempotency_key != idempotency_key:
|
||||
continue
|
||||
async with self._admission_scope(thread_id, run_id):
|
||||
async with self._lock:
|
||||
if idempotency_key is not None and execution_fence is None:
|
||||
for existing in self._runs.values():
|
||||
if existing.idempotency_key != idempotency_key:
|
||||
continue
|
||||
if existing.thread_id != thread_id or existing.user_id != user_id:
|
||||
raise RunIdempotencyKeyMismatch("Run idempotency key resolved to a different thread or user")
|
||||
existing.idempotency_reused = True
|
||||
return existing
|
||||
|
||||
def reuse_idempotent_run(conflict: RunIdempotencyConflict) -> RunRecord:
|
||||
# A locally held record for this key already returned above, so
|
||||
# the conflicting row belongs to a peer or to a run this worker
|
||||
# has cleaned up. Return a store-only handle without registering
|
||||
# it: nothing here finalizes or cleans up that record, so a
|
||||
# registered copy would keep its admission-time status, reject
|
||||
# later admissions for the thread, and shadow the durable row
|
||||
# for get(), cancel(), and orphan reconciliation.
|
||||
existing = self._record_from_store(conflict.existing)
|
||||
if existing.thread_id != thread_id or existing.user_id != user_id:
|
||||
raise RunIdempotencyKeyMismatch("Run idempotency key resolved to a different thread or user")
|
||||
raise RunIdempotencyKeyMismatch("Run idempotency key resolved to a different thread or user") from conflict
|
||||
existing.idempotency_reused = True
|
||||
return existing
|
||||
|
||||
def reuse_idempotent_run(conflict: RunIdempotencyConflict) -> RunRecord:
|
||||
# A locally held record for this key already returned above, so
|
||||
# the conflicting row belongs to a peer or to a run this worker
|
||||
# has cleaned up. Return a store-only handle without registering
|
||||
# it: nothing here finalizes or cleans up that record, so a
|
||||
# registered copy would keep its admission-time status, reject
|
||||
# later admissions for the thread, and shadow the durable row
|
||||
# for get(), cancel(), and orphan reconciliation.
|
||||
existing = self._record_from_store(conflict.existing)
|
||||
if existing.thread_id != thread_id or existing.user_id != user_id:
|
||||
raise RunIdempotencyKeyMismatch("Run idempotency key resolved to a different thread or user") from conflict
|
||||
existing.idempotency_reused = True
|
||||
return existing
|
||||
# 1) Local inflight check (same-worker guard; cross-worker is the
|
||||
# store's partial unique index below).
|
||||
local_inflight = [r for r in self._thread_records_locked(thread_id) if r.status in (RunStatus.pending, RunStatus.running) or r.finalizing or r.scheduled_goal_cleanup_pending]
|
||||
|
||||
# 1) Local inflight check (same-worker guard; cross-worker is the
|
||||
# store's partial unique index below).
|
||||
local_inflight = [r for r in self._thread_records_locked(thread_id) if r.status in (RunStatus.pending, RunStatus.running) or r.finalizing or r.scheduled_goal_cleanup_pending]
|
||||
if any(r.scheduled_goal_cleanup_pending for r in local_inflight):
|
||||
raise ConflictError(f"Thread {thread_id} has a scheduled goal awaiting finalization")
|
||||
|
||||
if any(r.scheduled_goal_cleanup_pending for r in local_inflight):
|
||||
raise ConflictError(f"Thread {thread_id} has a scheduled goal awaiting finalization")
|
||||
if multitask_strategy in ("interrupt", "rollback") and any(record.operation_kind != ThreadOperationKind.run for record in local_inflight):
|
||||
raise ConflictError(f"Thread {thread_id} has an active checkpoint write")
|
||||
|
||||
if multitask_strategy in ("interrupt", "rollback") and any(record.operation_kind != ThreadOperationKind.run for record in local_inflight):
|
||||
raise ConflictError(f"Thread {thread_id} has an active checkpoint write")
|
||||
if multitask_strategy == "reject" and local_inflight:
|
||||
raise ConflictError(f"Thread {thread_id} already has an active run")
|
||||
|
||||
if multitask_strategy == "reject" and local_inflight:
|
||||
raise ConflictError(f"Thread {thread_id} already has an active run")
|
||||
if multitask_strategy in ("interrupt", "rollback") and local_inflight:
|
||||
logger.info(
|
||||
"Preparing to cancel %d inflight run(s) on thread %s (strategy=%s)",
|
||||
len(local_inflight),
|
||||
thread_id,
|
||||
multitask_strategy,
|
||||
)
|
||||
|
||||
if multitask_strategy in ("interrupt", "rollback") and local_inflight:
|
||||
logger.info(
|
||||
"Preparing to cancel %d inflight run(s) on thread %s (strategy=%s)",
|
||||
len(local_inflight),
|
||||
thread_id,
|
||||
multitask_strategy,
|
||||
)
|
||||
|
||||
# 2) Persist to store while still holding the local lock. The
|
||||
# 2) Persist with only the same-thread admission gate held. The
|
||||
# store is the source of truth for cross-process atomicity.
|
||||
if self._store is not None:
|
||||
if idempotency_request is not None and not _store_accepts_idempotency_parameter(self._store, "idempotency_request"):
|
||||
@ -2242,27 +2279,28 @@ class RunManager:
|
||||
# rows as interrupted in the same transaction; no extra
|
||||
# store write is needed for them.
|
||||
|
||||
# 3) Only now safe to register locally — store insert succeeded.
|
||||
self._runs[run_id] = record
|
||||
self._index_run_locked(record)
|
||||
self._arm_lease_deadline(record)
|
||||
async with self._lock:
|
||||
# 3) Only now safe to register locally — store insert succeeded.
|
||||
self._runs[run_id] = record
|
||||
self._index_run_locked(record)
|
||||
self._arm_lease_deadline(record)
|
||||
|
||||
# 4) Cancel local in-memory inflight (interrupt/rollback). The
|
||||
# store-side counterparts were already cancelled in step 2.
|
||||
if self._store is None and multitask_strategy in ("interrupt", "rollback"):
|
||||
for r in local_inflight:
|
||||
if r.finalizing:
|
||||
continue
|
||||
r.abort_action = multitask_strategy
|
||||
r.abort_event.set()
|
||||
task_active = r.task is not None and not r.task.done()
|
||||
r.finalizing = task_active
|
||||
if task_active:
|
||||
r.task.cancel()
|
||||
r.status = RunStatus.interrupted
|
||||
r.terminal_status_persisted = self._store is None
|
||||
r.updated_at = now
|
||||
interrupted_records.append(r)
|
||||
# 4) Cancel local in-memory inflight (interrupt/rollback). The
|
||||
# store-side counterparts were already cancelled in step 2.
|
||||
if self._store is None and multitask_strategy in ("interrupt", "rollback"):
|
||||
for r in local_inflight:
|
||||
if r.finalizing:
|
||||
continue
|
||||
r.abort_action = multitask_strategy
|
||||
r.abort_event.set()
|
||||
task_active = r.task is not None and not r.task.done()
|
||||
r.finalizing = task_active
|
||||
if task_active:
|
||||
r.task.cancel()
|
||||
r.status = RunStatus.interrupted
|
||||
r.terminal_status_persisted = self._store is None
|
||||
r.updated_at = now
|
||||
interrupted_records.append(r)
|
||||
|
||||
# Outside the lock: persist interrupted status for locally-cancelled
|
||||
# runs. Store-side claimed rows are already finalised. Cancellation at
|
||||
@ -2328,7 +2366,7 @@ class RunManager:
|
||||
if reservation_task is None:
|
||||
raise RuntimeError("Thread operation reservation requires an active asyncio task")
|
||||
lease_lost = True
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(record.run_id):
|
||||
if self._runs.get(record.run_id) is record:
|
||||
record.task = reservation_task
|
||||
lease_lost = record.abort_event.is_set()
|
||||
@ -2355,7 +2393,7 @@ class RunManager:
|
||||
exc_info=True,
|
||||
)
|
||||
finally:
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(record.run_id):
|
||||
removed = self._runs.pop(record.run_id, None)
|
||||
if removed is not None:
|
||||
self._unindex_run_locked(record.run_id, removed.thread_id)
|
||||
@ -2472,7 +2510,7 @@ class RunManager:
|
||||
|
||||
async def has_inflight(self, thread_id: str) -> bool:
|
||||
"""Return ``True`` if *thread_id* has a pending or running run."""
|
||||
async with self._lock:
|
||||
async with self._thread_registry_lock(thread_id):
|
||||
return any(r.operation_kind == ThreadOperationKind.run and (r.status in (RunStatus.pending, RunStatus.running) or r.finalizing) for r in self._thread_records_locked(thread_id))
|
||||
|
||||
async def cleanup(self, run_id: str, *, delay: float = 300) -> None:
|
||||
@ -2488,7 +2526,7 @@ class RunManager:
|
||||
return
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(run_id):
|
||||
record = self._runs.pop(run_id, None)
|
||||
if record is not None:
|
||||
self._unindex_run_locked(run_id, record.thread_id)
|
||||
@ -2505,7 +2543,7 @@ class RunManager:
|
||||
|
||||
async def owns_active_admission(self, record: RunRecord) -> bool:
|
||||
"""Confirm the existing run slot without holding a SQL writer transaction."""
|
||||
async with self._lock:
|
||||
async with self._run_registry_lock(record.run_id):
|
||||
if self._runs.get(record.run_id) is not record or record.ownership_lost:
|
||||
return False
|
||||
if self._store is None:
|
||||
@ -2841,6 +2879,18 @@ class RunManager:
|
||||
return loop.time() < deadline and self._rollback_retry_authorized(record, initial_allow_interrupted=initial_allow_interrupted)
|
||||
|
||||
async def _renew_one_lease(self, record: RunRecord) -> None:
|
||||
# Assembly publication writes a complete snapshot. Keep the Store
|
||||
# renewal and its local ACK together so that snapshot cannot restore
|
||||
# the previous lease. Waiting for this gate retains the expiry timer.
|
||||
self._arm_lease_deadline(record)
|
||||
async with self._snapshot_locks.hold(record.run_id):
|
||||
action = await self._renew_one_lease_locked(record)
|
||||
if action is not None:
|
||||
# Do not acquire the thread gate while holding the snapshot gate:
|
||||
# assembly publication takes them in the opposite order.
|
||||
await self._signal_local_cancel(record.run_id, action=action)
|
||||
|
||||
async def _renew_one_lease_locked(self, record: RunRecord) -> str | None:
|
||||
assert self._store is not None and self._run_ownership_config is not None
|
||||
if self._has_final_terminal_ack(record):
|
||||
await self._release_confirmed_terminal_barrier(record, timeout=0)
|
||||
@ -2915,7 +2965,7 @@ class RunManager:
|
||||
if renewal.cancel_action is not None:
|
||||
action = renewal.cancel_action if renewal.cancel_action in ("interrupt", "rollback") else "interrupt"
|
||||
# Signal here, before joining any other run's decision.
|
||||
await self._signal_local_cancel(record.run_id, action=action)
|
||||
return action
|
||||
return
|
||||
if (
|
||||
retry_index == 0
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
"""Bounded byte snapshots for managed custom packages (POSIX, no links)."""
|
||||
"""Bounded byte snapshots for managed custom packages (no links).
|
||||
|
||||
POSIX walks the tree with ``dir_fd`` descriptors so every path component is
|
||||
anchored. Hosts without ``dir_fd``/``O_NOFOLLOW`` (Windows) use
|
||||
:func:`_capture_package_portable`, which re-checks each component with ``lstat``
|
||||
before use and compares the pre-open and post-open ``stat`` of every file, so a
|
||||
swap during the walk is still rejected instead of followed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -47,7 +54,7 @@ class PackageSnapshot:
|
||||
def capture_package(root: Path) -> PackageSnapshot:
|
||||
"""Caller owns mutation guard. Never read outside the opened package tree."""
|
||||
if os.name != "posix":
|
||||
raise ValueError("UNSUPPORTED_ASSET")
|
||||
return _capture_package_portable(root)
|
||||
files: list[PackageFile] = []
|
||||
total = 0
|
||||
entries = 0
|
||||
@ -146,3 +153,101 @@ def open_directory_chain(path: Path) -> int:
|
||||
except BaseException:
|
||||
os.close(fd)
|
||||
raise
|
||||
|
||||
|
||||
def _resolve_unlinked_ancestors(path: Path) -> Path:
|
||||
"""Return *path* after rejecting a link in any component above it.
|
||||
|
||||
The non-POSIX twin of :func:`open_directory_chain`: there is no ``dir_fd``
|
||||
to anchor the walk, so each component is ``lstat``-ed in order and a link
|
||||
stops the walk instead of being traversed.
|
||||
"""
|
||||
path = path.absolute()
|
||||
if ".." in path.parts or len(path.parts) > 128 or len(os.fsencode(path)) > 4096:
|
||||
raise ValueError("UNSUPPORTED_ASSET")
|
||||
current = Path(path.anchor)
|
||||
for part in path.parts[1:]:
|
||||
current = current / part
|
||||
info = os.lstat(current)
|
||||
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
|
||||
raise ValueError("UNSUPPORTED_ASSET")
|
||||
return path
|
||||
|
||||
|
||||
def _capture_package_portable(root: Path) -> PackageSnapshot:
|
||||
"""Capture a package where ``dir_fd``/``O_NOFOLLOW`` are unavailable.
|
||||
|
||||
``os.lstat`` runs before every open and the opened descriptor is compared
|
||||
against that ``lstat`` afterwards, so a component replaced between the two
|
||||
calls raises ``REVISION_CONFLICT`` instead of publishing foreign bytes.
|
||||
Windows keeps ``st_dev``/``st_ino`` for NTFS volumes, which makes that
|
||||
comparison meaningful there; the executable bit stays unset because the
|
||||
platform has no mode bits, and that is consistent within a host.
|
||||
"""
|
||||
files: list[PackageFile] = []
|
||||
total = 0
|
||||
entries = 0
|
||||
open_flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOINHERIT", 0)
|
||||
|
||||
def walk(directory: Path, prefix: str = "", depth: int = 0) -> None:
|
||||
nonlocal total, entries
|
||||
if depth > MAX_DEPTH:
|
||||
raise ValueError("QUOTA_EXCEEDED")
|
||||
names = []
|
||||
with os.scandir(directory) as iterator:
|
||||
for entry in iterator:
|
||||
entries += 1
|
||||
if entries > MAX_FILES * 2:
|
||||
raise ValueError("QUOTA_EXCEEDED")
|
||||
names.append(entry.name)
|
||||
for name in sorted(names):
|
||||
if "\\" in name or any(ord(c) < 32 for c in name):
|
||||
raise ValueError("UNSUPPORTED_ASSET")
|
||||
path = f"{prefix}/{name}" if prefix else name
|
||||
if len(path.encode("utf-8")) > 1024:
|
||||
raise ValueError("QUOTA_EXCEEDED")
|
||||
target = directory / name
|
||||
info = os.lstat(target)
|
||||
if stat.S_ISLNK(info.st_mode):
|
||||
raise ValueError("UNSUPPORTED_ASSET")
|
||||
if stat.S_ISDIR(info.st_mode):
|
||||
walk(target, path, depth + 1)
|
||||
continue
|
||||
if not stat.S_ISREG(info.st_mode):
|
||||
raise ValueError("UNSUPPORTED_ASSET")
|
||||
if len(files) >= MAX_FILES:
|
||||
raise ValueError("QUOTA_EXCEEDED")
|
||||
limit = min(MAX_PACKAGE_BYTES - total, MAX_MAIN_BYTES if path == "SKILL.md" else MAX_PACKAGE_BYTES)
|
||||
if info.st_size > limit:
|
||||
raise ValueError("QUOTA_EXCEEDED")
|
||||
fd = os.open(target, open_flags)
|
||||
try:
|
||||
opened = os.fstat(fd)
|
||||
if (info.st_dev, info.st_ino, info.st_mode) != (opened.st_dev, opened.st_ino, opened.st_mode):
|
||||
raise ValueError("REVISION_CONFLICT")
|
||||
data = bytearray()
|
||||
while True:
|
||||
chunk = os.read(fd, min(65536, limit + 1 - len(data)))
|
||||
if not chunk:
|
||||
break
|
||||
data.extend(chunk)
|
||||
if len(data) > limit:
|
||||
raise ValueError("QUOTA_EXCEEDED")
|
||||
after = os.fstat(fd)
|
||||
if (opened.st_size, opened.st_mtime_ns, opened.st_ctime_ns) != (after.st_size, after.st_mtime_ns, after.st_ctime_ns):
|
||||
raise ValueError("REVISION_CONFLICT")
|
||||
total += len(data)
|
||||
files.append(PackageFile(path, bytes(data), bool(opened.st_mode & 0o111)))
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
try:
|
||||
walk(_resolve_unlinked_ancestors(root))
|
||||
except OSError as exc:
|
||||
raise ValueError("UNSUPPORTED_ASSET") from exc
|
||||
result = PackageSnapshot(tuple(sorted(files, key=lambda item: item.path)))
|
||||
try:
|
||||
result.main_content
|
||||
except (StopIteration, UnicodeError) as exc:
|
||||
raise ValueError("UNSUPPORTED_ASSET") from exc
|
||||
return result
|
||||
|
||||
@ -76,6 +76,8 @@ by lease/3. `tests/test_batch_lease_lifecycle.py` covers real SQLite interleavin
|
||||
threaded initialization, peer cancellation and renewal drain.
|
||||
|
||||
**Built-in Agents**: `general-purpose` (all tools except `task`) and `bash` (command specialist)
|
||||
|
||||
Native worker reasoning settings come from YAML or managed definitions, with explicit per-agent overrides. Unset values request False/None, never parent thinking. Send original settings to the model factory to preserve validation and adjustment logs; record normalized effective settings in the assembly observer. Legacy stored definitions may omit both fields.
|
||||
**Registry and managed definitions**: Resolve built-ins → `config.yaml custom_agents` → enabled admin-managed definitions → explicit `subagents.agents.<name>` overrides. Managed definitions are deployment-wide, use Custom Agents' `agent_storage.backend`, and remain stored but inactive on built-in/config name collisions. Default Lead Agent sees the full enabled catalog. Snapshot a Custom Agent's `allowed_subagents` into run metadata (`None` = all, `[]` = deny, list = allowlist); enforce it in prompt discovery and `task`, never reload mutable caller policy inside the tool.
|
||||
**Benefit-based routing policy**: Default to direct execution. Permit `task` only when parallel latency, specialist skill or context isolation clearly outweigh startup, duplicate discovery, synthesis, state conflicts and side effects. Parallel scopes need independent outputs and disjoint mutable state; use the fewest useful agents, reassessing each batch. Bounded sequential delegation requires strong specialist/context benefit. A per-response limit of 1 permits only material specialist/context benefit: omit parallel/multi-batch benefits. Align `lead_agent/prompt.py`, the `task` description and built-in roles. Tests: `test_subagent_routing_prompt.py`, `test_subagent_prompt_security.py`, `test_lead_agent_prompt.py`.
|
||||
**User-scoped Skills**: Use `get_or_new_user_skill_storage(user_id)` with parent runtime identity (`DEFAULT_USER_ID` only if absent), preserving lead-agent custom-skill shadowing/visibility; never use the global-only catalog.
|
||||
|
||||
@ -25,6 +25,9 @@ class SubagentConfig:
|
||||
allowed-tools is compatibility metadata only and never restricts
|
||||
this subagent's tools.
|
||||
model: Model to use - 'inherit' uses parent's model.
|
||||
thinking_enabled: Whether to enable thinking/reasoning for this subagent.
|
||||
None means use default (False).
|
||||
reasoning_effort: Selected model's reasoning effort or None for its default.
|
||||
max_turns: Maximum agent turns — model call plus the tools it runs —
|
||||
before stopping. Built-in agents use the value set here
|
||||
(general-purpose=150, bash=60) unless the global
|
||||
@ -46,6 +49,8 @@ class SubagentConfig:
|
||||
disallowed_tools: list[str] | None = field(default_factory=lambda: ["task"])
|
||||
skills: list[str] | None = None
|
||||
model: str = "inherit"
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
max_turns: int = 50
|
||||
timeout_seconds: int = 900
|
||||
prompt_overlay: PromptOverlay = field(default_factory=PromptOverlay)
|
||||
|
||||
@ -66,6 +66,8 @@ from deerflow.utils.file_io import await_drained
|
||||
from deerflow.utils.messages import message_content_to_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deerflow.models.reasoning import ResolvedReasoning
|
||||
|
||||
# Imported lazily at runtime inside _build_initial_state: importing
|
||||
# tool_search eagerly would run tools/builtins/__init__ -> task_tool ->
|
||||
# `from deerflow.subagents import SubagentExecutor`, which re-enters this
|
||||
@ -989,6 +991,11 @@ class SubagentExecutor:
|
||||
self.model_name: str | None = resolve_subagent_model_name(config, parent_model, app_config=app_config)
|
||||
else:
|
||||
self.model_name = None
|
||||
# Resolve thinking_enabled: config explicit > False
|
||||
self.thinking_enabled = config.thinking_enabled if config.thinking_enabled is not None else False
|
||||
# Resolve reasoning_effort: config explicit > None
|
||||
self.reasoning_effort = config.reasoning_effort
|
||||
|
||||
self.sandbox_state = sandbox_state
|
||||
self.thread_data = thread_data
|
||||
self.uploaded_files = deepcopy(uploaded_files) if uploaded_files is not None else None
|
||||
@ -1100,7 +1107,24 @@ class SubagentExecutor:
|
||||
app_config = self._get_resolved_app_config()
|
||||
if self.model_name is None:
|
||||
self.model_name = resolve_subagent_model_name(self.config, self.parent_model, app_config=app_config)
|
||||
model = create_chat_model(name=self.model_name, thinking_enabled=False, app_config=app_config, attach_tracing=False)
|
||||
from deerflow.models.reasoning import ReasoningPolicyError, resolve_reasoning_contract, resolve_reasoning_request
|
||||
|
||||
get_model_config = getattr(app_config, "get_model_config", None)
|
||||
model_config = get_model_config(self.model_name) if callable(get_model_config) else None
|
||||
if model_config is None:
|
||||
model_config = next((profile for profile in getattr(app_config, "models", []) if profile.name == self.model_name), None)
|
||||
try:
|
||||
effective_reasoning = resolve_reasoning_request(resolve_reasoning_contract(model_config), thinking_enabled=self.thinking_enabled, reasoning_effort=self.reasoning_effort)
|
||||
except ReasoningPolicyError as exc:
|
||||
raise ValueError(f"Model {self.model_name}: {exc}") from exc
|
||||
# Preserve the original request for the factory's validation and logs.
|
||||
model = create_chat_model(
|
||||
name=self.model_name,
|
||||
thinking_enabled=self.thinking_enabled,
|
||||
reasoning_effort=self.reasoning_effort,
|
||||
app_config=app_config,
|
||||
attach_tracing=False,
|
||||
)
|
||||
|
||||
from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares
|
||||
|
||||
@ -1218,7 +1242,8 @@ class SubagentExecutor:
|
||||
tool_node = tool_graph_node.data if tool_graph_node is not None else None
|
||||
self._return_direct_tools = {tool.name for tool in tool_node.tools_by_name.values() if tool.return_direct} if isinstance(tool_node, ToolNode) else set()
|
||||
self._describe_assembly(
|
||||
app_config=app_config,
|
||||
model_config=model_config,
|
||||
effective_reasoning=effective_reasoning,
|
||||
tools=bound_tools,
|
||||
middlewares=middlewares,
|
||||
deferred_setup=deferred_setup,
|
||||
@ -1267,7 +1292,8 @@ class SubagentExecutor:
|
||||
def _describe_assembly(
|
||||
self,
|
||||
*,
|
||||
app_config: Any,
|
||||
model_config: Any,
|
||||
effective_reasoning: "ResolvedReasoning",
|
||||
tools: list[Any],
|
||||
middlewares: list[Any],
|
||||
deferred_setup: "DeferredToolSetup | None",
|
||||
@ -1289,8 +1315,6 @@ class SubagentExecutor:
|
||||
from deerflow.extensions.notify import notify_agent_assembled
|
||||
|
||||
try:
|
||||
get_model_config = getattr(app_config, "get_model_config", None)
|
||||
model_config = get_model_config(self.model_name) if callable(get_model_config) else None
|
||||
if model_config is None:
|
||||
# A name the profile table does not know still has an identity;
|
||||
# a missing profile must not blank out the whole descriptor.
|
||||
@ -1302,12 +1326,6 @@ class SubagentExecutor:
|
||||
supports_vision=False,
|
||||
)
|
||||
deferred_names = deferred_setup.deferred_names if deferred_setup is not None else frozenset()
|
||||
# Subagents request thinking off; the model's reasoning contract
|
||||
# decides what that means (a required-thinking model stays on), and
|
||||
# the descriptor reports the effective policy the factory applied.
|
||||
from deerflow.models.reasoning import resolve_reasoning_contract, resolve_reasoning_request
|
||||
|
||||
effective_reasoning = resolve_reasoning_request(resolve_reasoning_contract(model_config), thinking_enabled=False, reasoning_effort=None)
|
||||
descriptor = build_assembly_descriptor(
|
||||
namespace="deerflow",
|
||||
agent_name=self.config.name,
|
||||
|
||||
@ -49,6 +49,8 @@ def _build_custom_subagent_config(name: str, *, app_config: Any | None = None) -
|
||||
disallowed_tools=custom.disallowed_tools,
|
||||
skills=custom.skills,
|
||||
model=custom.model,
|
||||
thinking_enabled=custom.thinking_enabled,
|
||||
reasoning_effort=custom.reasoning_effort,
|
||||
max_turns=custom.max_turns,
|
||||
timeout_seconds=custom.timeout_seconds,
|
||||
)
|
||||
@ -97,6 +99,8 @@ def _build_managed_subagent_config(name: str, *, app_config: Any | None = None)
|
||||
disallowed_tools=definition.disallowed_tools,
|
||||
skills=definition.skills,
|
||||
model=definition.model,
|
||||
thinking_enabled=definition.thinking_enabled,
|
||||
reasoning_effort=definition.reasoning_effort,
|
||||
max_turns=definition.max_turns,
|
||||
timeout_seconds=definition.timeout_seconds,
|
||||
)
|
||||
@ -110,7 +114,7 @@ def get_subagent_config(name: str, *, app_config: Any | None = None) -> Subagent
|
||||
1. Built-in subagents (general-purpose, bash)
|
||||
2. Custom subagents from config.yaml custom_agents section
|
||||
3. Enabled administrator-managed subagents
|
||||
4. Per-agent overrides from config.yaml agents section (timeout, max_turns, model, skills)
|
||||
4. Per-agent overrides from config.yaml agents section (timeout, max_turns, model, thinking_enabled, reasoning_effort, skills)
|
||||
|
||||
Args:
|
||||
name: The name of the subagent.
|
||||
@ -173,6 +177,18 @@ def get_subagent_config(name: str, *, app_config: Any | None = None) -> Subagent
|
||||
logger.debug("Subagent '%s': skills overridden (%s -> %s)", name, config.skills, effective_skills)
|
||||
overrides["skills"] = effective_skills
|
||||
|
||||
# Thinking: per-agent override only (no global default)
|
||||
effective_thinking = subagents_config.get_thinking_enabled_for(name)
|
||||
if effective_thinking is not None and effective_thinking != config.thinking_enabled:
|
||||
logger.debug("Subagent '%s': thinking_enabled overridden (%s -> %s)", name, config.thinking_enabled, effective_thinking)
|
||||
overrides["thinking_enabled"] = effective_thinking
|
||||
|
||||
# Reasoning effort: per-agent override only (no global default)
|
||||
effective_reasoning = subagents_config.get_reasoning_effort_for(name)
|
||||
if effective_reasoning is not None and effective_reasoning != config.reasoning_effort:
|
||||
logger.debug("Subagent '%s': reasoning_effort overridden (%s -> %s)", name, config.reasoning_effort, effective_reasoning)
|
||||
overrides["reasoning_effort"] = effective_reasoning
|
||||
|
||||
if overrides:
|
||||
config = replace(config, **overrides)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "deerflow-harness"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0-dev"
|
||||
description = "DeerFlow agent harness framework"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "deer-flow"
|
||||
version = "2.2.0-dev"
|
||||
version = "2.3.0-dev"
|
||||
description = "LangGraph-based AI agent system with sandbox execution capabilities"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@ -45,19 +45,20 @@ pytestmark = pytest.mark.skipif(
|
||||
reason="the fake `uv` on PATH is an extensionless shebang script; Windows does not execute those",
|
||||
)
|
||||
|
||||
# Minimal versions of the three files bump_version.sh rewrites, plus the lock
|
||||
# entry uv writes for the root ("virtual") package. Only the fields the script
|
||||
# touches are present, so a stray rewrite shows up as a diff.
|
||||
# Minimal versions of the files bump_version.sh rewrites, plus the lock entries
|
||||
# uv writes for the root package and the deerflow-harness workspace member. Only
|
||||
# the fields the script touches are present, so a stray rewrite shows up as a diff.
|
||||
PYPROJECT = '[project]\nname = "deer-flow"\nversion = "2.1.0-rc0"\n'
|
||||
HARNESS_PYPROJECT = '[project]\nname = "deerflow-harness"\nversion = "2.1.0-rc0"\n'
|
||||
PACKAGE_JSON = '{\n "name": "deer-flow-frontend",\n "version": "2.1.0-rc0",\n "private": true\n}\n'
|
||||
CHART = 'apiVersion: v2\nname: deer-flow\nversion: 2.1.0-rc0\nappVersion: "2.1.0-rc0"\n'
|
||||
UV_LOCK = 'version = 1\n\n[[package]]\nname = "deer-flow"\nversion = "2.1.0rc0"\nsource = { virtual = "." }\n'
|
||||
UV_LOCK = 'version = 1\n\n[[package]]\nname = "deer-flow"\nversion = "2.1.0rc0"\nsource = { virtual = "." }\n\n[[package]]\nname = "deerflow-harness"\nversion = "2.1.0rc0"\nsource = { editable = "packages/harness" }\n'
|
||||
|
||||
# Emulates the uv behavior the release flow depends on: `uv lock` records the
|
||||
# pyproject version in the root lock entry, in uv's PEP 440 form (for these
|
||||
# inputs that only means dropping the `-`), and `uv lock --check` (run by the
|
||||
# verify_versions.sh self-check) fails while the entry lags. The invocation is
|
||||
# logged so the test can tell whether the script called uv at all.
|
||||
# Emulates the uv behavior the release flow depends on: `uv lock` records each
|
||||
# workspace member's pyproject version in the lock, in uv's PEP 440 form (for
|
||||
# these inputs that only means dropping the `-`), and `uv lock --check` (run by
|
||||
# the verify_versions.sh self-check) fails while an entry lags. The invocation
|
||||
# is logged so the test can tell whether the script called uv at all.
|
||||
FAKE_UV = """#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
@ -74,21 +75,36 @@ if args not in (["lock"], ["lock", "--check"]):
|
||||
root = Path.cwd()
|
||||
lock_path = root / "uv.lock"
|
||||
lock_text = lock_path.read_text(encoding="utf-8")
|
||||
pyproject = (root / "pyproject.toml").read_text(encoding="utf-8")
|
||||
declared = re.search(r'(?m)^version\\s*=\\s*"([^"]+)"', pyproject).group(1)
|
||||
normalized = declared.replace("-", "")
|
||||
recorded = re.search(r'(?m)^name = "deer-flow"\\nversion = "([^"]+)"', lock_text).group(1)
|
||||
|
||||
members = [("deer-flow", root / "pyproject.toml")]
|
||||
harness = root / "packages" / "harness" / "pyproject.toml"
|
||||
if harness.exists():
|
||||
members.append(("deerflow-harness", harness))
|
||||
|
||||
for name, pyproject in members:
|
||||
declared = re.search(r'(?m)^version\\s*=\\s*"([^"]+)"', pyproject.read_text(encoding="utf-8")).group(1)
|
||||
normalized = declared.replace("-", "")
|
||||
recorded = re.search(rf'(?m)^name = "{name}"\\nversion = "([^"]+)"', lock_text).group(1)
|
||||
if args == ["lock", "--check"]:
|
||||
if recorded != normalized:
|
||||
sys.exit("The lockfile at `uv.lock` needs to be updated, but `--check` was provided.")
|
||||
else:
|
||||
lock_text = lock_text.replace(
|
||||
f'name = "{name}"\\nversion = "{recorded}"',
|
||||
f'name = "{name}"\\nversion = "{normalized}"',
|
||||
1,
|
||||
)
|
||||
|
||||
if args == ["lock", "--check"]:
|
||||
sys.exit(0 if recorded == normalized else "The lockfile at `uv.lock` needs to be updated, but `--check` was provided.")
|
||||
lock_path.write_text(lock_text.replace(f'version = "{recorded}"', f'version = "{normalized}"', 1), encoding="utf-8")
|
||||
sys.exit(0)
|
||||
lock_path.write_text(lock_text, encoding="utf-8")
|
||||
"""
|
||||
|
||||
|
||||
def _write_sandbox(root: Path) -> Path:
|
||||
"""Materialize a repo-shaped tree the scripts can run against."""
|
||||
(root / "scripts").mkdir(parents=True)
|
||||
(root / "backend").mkdir()
|
||||
(root / "backend" / "packages" / "harness").mkdir(parents=True)
|
||||
(root / "frontend").mkdir()
|
||||
(root / "deploy" / "helm" / "deer-flow").mkdir(parents=True)
|
||||
|
||||
@ -96,6 +112,7 @@ def _write_sandbox(root: Path) -> Path:
|
||||
shutil.copy2(REPO_ROOT / "scripts" / name, root / "scripts" / name)
|
||||
|
||||
(root / "backend" / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8")
|
||||
(root / "backend" / "packages" / "harness" / "pyproject.toml").write_text(HARNESS_PYPROJECT, encoding="utf-8")
|
||||
(root / "backend" / "uv.lock").write_text(UV_LOCK, encoding="utf-8")
|
||||
(root / "frontend" / "package.json").write_text(PACKAGE_JSON, encoding="utf-8")
|
||||
(root / "deploy" / "helm" / "deer-flow" / "Chart.yaml").write_text(CHART, encoding="utf-8")
|
||||
@ -144,6 +161,8 @@ def test_bump_version_refreshes_the_lockfile(tmp_path: Path) -> None:
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert _lock_version(root) == "2.2.0", "backend/uv.lock still pins the previous version"
|
||||
assert (root / "backend" / "pyproject.toml").read_text(encoding="utf-8") == PYPROJECT.replace("2.1.0-rc0", "2.2.0")
|
||||
assert (root / "backend" / "packages" / "harness" / "pyproject.toml").read_text(encoding="utf-8") == HARNESS_PYPROJECT.replace("2.1.0-rc0", "2.2.0"), "the deerflow-harness workspace member must move with the release"
|
||||
assert 'name = "deerflow-harness"\nversion = "2.2.0"' in (root / "backend" / "uv.lock").read_text(encoding="utf-8"), "uv rewrites the harness lock entry, and the script must let it"
|
||||
invocations = (bin_dir / "uv-calls.log").read_text(encoding="utf-8").splitlines()
|
||||
assert invocations and all(line.startswith("uv lock") for line in invocations), f"bump_version.sh never invoked uv lock: {invocations}"
|
||||
|
||||
@ -157,6 +176,7 @@ def test_bump_version_normalizes_a_prerelease_into_the_lock(tmp_path: Path) -> N
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert (root / "backend" / "pyproject.toml").read_text(encoding="utf-8") == PYPROJECT.replace("2.1.0-rc0", "2.1.0-rc1")
|
||||
assert (root / "backend" / "packages" / "harness" / "pyproject.toml").read_text(encoding="utf-8") == HARNESS_PYPROJECT.replace("2.1.0-rc0", "2.1.0-rc1")
|
||||
assert _lock_version(root) == "2.1.0rc1", "uv normalizes the prerelease in the lock, and the script must let it"
|
||||
|
||||
|
||||
@ -175,4 +195,5 @@ def test_bump_version_refuses_to_run_without_uv(tmp_path: Path) -> None:
|
||||
assert result.returncode != 0, "a missing uv must be an error: the lock would stay stale"
|
||||
assert "uv" in (result.stderr + result.stdout)
|
||||
assert (root / "backend" / "pyproject.toml").read_text(encoding="utf-8") == PYPROJECT, "the script edited files before checking for uv"
|
||||
assert (root / "backend" / "packages" / "harness" / "pyproject.toml").read_text(encoding="utf-8") == HARNESS_PYPROJECT, "the script edited files before checking for uv"
|
||||
assert _lock_version(root) == "2.1.0rc0"
|
||||
|
||||
@ -4,7 +4,7 @@ import copy
|
||||
import pytest
|
||||
from deerflow_extension_api import ExtensionData
|
||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
from langgraph.checkpoint.base import empty_checkpoint, uuid6
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, empty_checkpoint, uuid6
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from deerflow.extensions.registry import ExtensionRegistry
|
||||
@ -92,7 +92,7 @@ class _ClearBeforeSecondGoalReadCheckpointer:
|
||||
return await self.inner.aput(*args, **kwargs)
|
||||
|
||||
|
||||
class _RaceAfterFirstContinuationCommitCheckpointer:
|
||||
class _RaceAfterFirstContinuationCommitCheckpointer(BaseCheckpointSaver):
|
||||
"""Wrap a saver and inject a racing user message right after the first
|
||||
goal-continuation commit lands.
|
||||
|
||||
@ -107,6 +107,7 @@ class _RaceAfterFirstContinuationCommitCheckpointer:
|
||||
"""
|
||||
|
||||
def __init__(self, inner: InMemorySaver, thread_id: str) -> None:
|
||||
super().__init__(serde=inner.serde)
|
||||
self.inner = inner
|
||||
self.thread_id = thread_id
|
||||
self.put_count = 0
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
@ -48,7 +49,7 @@ def sql_store(tmp_path):
|
||||
def test_crud_and_signature(request, store_fixture):
|
||||
store = request.getfixturevalue(store_fixture)
|
||||
empty_signature = store.signature()
|
||||
definition = _definition()
|
||||
definition = _definition(thinking_enabled=True, reasoning_effort="xhigh")
|
||||
|
||||
store.create(definition)
|
||||
assert store.get("RESEARCHER") == definition
|
||||
@ -60,6 +61,8 @@ def test_crud_and_signature(request, store_fixture):
|
||||
store.update(updated)
|
||||
assert store.get("researcher").enabled is False
|
||||
assert store.get("researcher").max_turns == 75
|
||||
assert store.get("researcher").thinking_enabled is True
|
||||
assert store.get("researcher").reasoning_effort == "xhigh"
|
||||
updated_signature = store.signature()
|
||||
assert updated_signature != created_signature
|
||||
|
||||
@ -70,6 +73,24 @@ def test_crud_and_signature(request, store_fixture):
|
||||
store.get("researcher")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("store_fixture", ["file_store", "sql_store"])
|
||||
def test_legacy_definition_without_reasoning_fields_keeps_defaults(request, store_fixture, tmp_path):
|
||||
store = request.getfixturevalue(store_fixture)
|
||||
legacy = {"name": "researcher", "description": "Research", "system_prompt": "Research the task."}
|
||||
if store_fixture == "file_store":
|
||||
root = tmp_path / "managed-subagents"
|
||||
root.mkdir()
|
||||
(root / "researcher.json").write_text(json.dumps(legacy), encoding="utf-8")
|
||||
else:
|
||||
with store._Session() as session:
|
||||
session.add(ManagedSubagentRow(id="legacy", name="researcher", definition=legacy))
|
||||
session.commit()
|
||||
definition = store.get("researcher")
|
||||
assert definition.thinking_enabled is None
|
||||
assert definition.reasoning_effort is None
|
||||
assert store.list() == [definition]
|
||||
|
||||
|
||||
def test_sql_signature_detects_update_behind_existing_max_timestamp(sql_store):
|
||||
sql_store.create(_definition("ahead"))
|
||||
sql_store.create(_definition("writer"))
|
||||
|
||||
@ -4845,3 +4845,170 @@ async def test_background_compensation_failure_is_consumed(monkeypatch, caplog):
|
||||
assert len(failures) == 1
|
||||
assert any("release remained unavailable" in failure.getMessage() for failure in failures)
|
||||
assert not service._compensation_tasks
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_drains_service_owned_compensation_tasks(monkeypatch):
|
||||
monkeypatch.setattr(service_module, "_CANCELLATION_DRAIN_TIMEOUT_SECONDS", 0.5)
|
||||
service = McpTaskService(
|
||||
repository=FakeRepository(),
|
||||
drivers=McpTaskDriverRegistry(),
|
||||
poll_interval_seconds=60,
|
||||
lease_seconds=120,
|
||||
max_concurrent_polls=3,
|
||||
)
|
||||
release_started = asyncio.Event()
|
||||
release_gate = asyncio.Event()
|
||||
|
||||
async def release_claim():
|
||||
release_started.set()
|
||||
await release_gate.wait()
|
||||
|
||||
compensation = asyncio.create_task(release_claim(), name="test-mcp-compensation")
|
||||
service._track_compensation_task(compensation, action="release poll claim", task_id="task-1")
|
||||
await release_started.wait()
|
||||
await service.start()
|
||||
|
||||
original_drain = service._drain_compensation_tasks
|
||||
drain_started = asyncio.Event()
|
||||
|
||||
async def observed_drain(*, deadline: float):
|
||||
drain_started.set()
|
||||
await original_drain(deadline=deadline)
|
||||
|
||||
monkeypatch.setattr(service, "_drain_compensation_tasks", observed_drain)
|
||||
stopping = asyncio.create_task(service.stop())
|
||||
try:
|
||||
await asyncio.wait_for(drain_started.wait(), timeout=1)
|
||||
assert not stopping.done(), "stop must drain service-owned compensation work"
|
||||
finally:
|
||||
release_gate.set()
|
||||
await asyncio.wait_for(stopping, timeout=1)
|
||||
|
||||
await _wait_for_compensation_tasks_to_clear(service)
|
||||
assert not service._compensation_tasks
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_bounds_drain_of_service_owned_compensation_tasks(monkeypatch, caplog):
|
||||
monkeypatch.setattr(service_module, "_CANCELLATION_DRAIN_TIMEOUT_SECONDS", 0.05)
|
||||
service = McpTaskService(
|
||||
repository=FakeRepository(),
|
||||
drivers=McpTaskDriverRegistry(),
|
||||
poll_interval_seconds=60,
|
||||
lease_seconds=120,
|
||||
max_concurrent_polls=3,
|
||||
)
|
||||
release_started = asyncio.Event()
|
||||
release_gate = asyncio.Event()
|
||||
|
||||
async def release_claim():
|
||||
release_started.set()
|
||||
await release_gate.wait()
|
||||
|
||||
compensation = asyncio.create_task(release_claim(), name="test-mcp-stuck-compensation")
|
||||
service._track_compensation_task(compensation, action="release poll claim", task_id="task-1")
|
||||
await release_started.wait()
|
||||
await service.start()
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await asyncio.wait_for(service.stop(), timeout=0.5)
|
||||
assert "MCP compensation task(s)" in caplog.text
|
||||
assert compensation in service._compensation_tasks
|
||||
finally:
|
||||
release_gate.set()
|
||||
await asyncio.wait_for(compensation, timeout=1)
|
||||
await _wait_for_compensation_tasks_to_clear(service)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_logs_pending_compensation_on_poller_timeout(monkeypatch, caplog):
|
||||
monkeypatch.setattr(service_module, "_CANCELLATION_DRAIN_TIMEOUT_SECONDS", 0.05)
|
||||
service = McpTaskService(
|
||||
repository=FakeRepository(),
|
||||
drivers=McpTaskDriverRegistry(),
|
||||
poll_interval_seconds=60,
|
||||
lease_seconds=120,
|
||||
max_concurrent_polls=3,
|
||||
)
|
||||
poller_started = asyncio.Event()
|
||||
poller_release = asyncio.Event()
|
||||
compensation_started = asyncio.Event()
|
||||
compensation_release = asyncio.Event()
|
||||
|
||||
async def blocked_run_once(*, now):
|
||||
poller_started.set()
|
||||
try:
|
||||
await asyncio.Future()
|
||||
except asyncio.CancelledError:
|
||||
await poller_release.wait()
|
||||
|
||||
async def release_claim():
|
||||
compensation_started.set()
|
||||
await compensation_release.wait()
|
||||
|
||||
monkeypatch.setattr(service, "run_once", blocked_run_once)
|
||||
compensation = asyncio.create_task(release_claim(), name="test-mcp-timeout-compensation")
|
||||
service._track_compensation_task(compensation, action="release poll claim", task_id="task-1")
|
||||
await compensation_started.wait()
|
||||
await service.start()
|
||||
await poller_started.wait()
|
||||
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await asyncio.wait_for(service.stop(), timeout=0.5)
|
||||
assert "before compensation drain could start" in caplog.text
|
||||
assert "1 MCP compensation task(s)" in caplog.text
|
||||
assert compensation in service._compensation_tasks
|
||||
finally:
|
||||
poller_release.set()
|
||||
compensation_release.set()
|
||||
if service._task is not None:
|
||||
await asyncio.wait_for(service._task, timeout=1)
|
||||
await _wait_for_compensation_tasks_to_clear(service)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_preserves_cancellation_while_draining_compensation_tasks(monkeypatch):
|
||||
monkeypatch.setattr(service_module, "_CANCELLATION_DRAIN_TIMEOUT_SECONDS", 0.5)
|
||||
service = McpTaskService(
|
||||
repository=FakeRepository(),
|
||||
drivers=McpTaskDriverRegistry(),
|
||||
poll_interval_seconds=60,
|
||||
lease_seconds=120,
|
||||
max_concurrent_polls=3,
|
||||
)
|
||||
release_started = asyncio.Event()
|
||||
release_gate = asyncio.Event()
|
||||
|
||||
async def release_claim():
|
||||
release_started.set()
|
||||
await release_gate.wait()
|
||||
|
||||
compensation = asyncio.create_task(release_claim(), name="test-mcp-cancelled-drain")
|
||||
service._track_compensation_task(compensation, action="release poll claim", task_id="task-1")
|
||||
await release_started.wait()
|
||||
await service.start()
|
||||
|
||||
original_drain = service._drain_compensation_tasks
|
||||
drain_started = asyncio.Event()
|
||||
|
||||
async def observed_drain(*, deadline: float):
|
||||
drain_started.set()
|
||||
await original_drain(deadline=deadline)
|
||||
|
||||
monkeypatch.setattr(service, "_drain_compensation_tasks", observed_drain)
|
||||
stopping = asyncio.create_task(service.stop())
|
||||
try:
|
||||
await asyncio.wait_for(drain_started.wait(), timeout=1)
|
||||
stopping.cancel()
|
||||
await asyncio.sleep(0)
|
||||
assert not stopping.done(), "stop must keep draining after caller cancellation"
|
||||
release_gate.set()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await asyncio.wait_for(stopping, timeout=1)
|
||||
finally:
|
||||
release_gate.set()
|
||||
if not stopping.done():
|
||||
stopping.cancel()
|
||||
await _wait_for_compensation_tasks_to_clear(service)
|
||||
|
||||
280
backend/tests/test_run_admission_isolation.py
Normal file
280
backend/tests/test_run_admission_isolation.py
Normal file
@ -0,0 +1,280 @@
|
||||
"""An admission waiting on its store must not block unrelated run control."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.config.run_ownership_config import RunOwnershipConfig
|
||||
from deerflow.runtime.runs.manager import ConflictError, RunManager
|
||||
from deerflow.runtime.runs.store.memory import MemoryRunStore
|
||||
|
||||
|
||||
class HeldAdmissionStore(MemoryRunStore):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.entered = asyncio.Event()
|
||||
self.release = asyncio.Event()
|
||||
self.renewed = asyncio.Event()
|
||||
|
||||
async def put(self, run_id, *, thread_id, **kwargs):
|
||||
if thread_id == "slow-thread":
|
||||
self.entered.set()
|
||||
await self.release.wait()
|
||||
return await super().put(run_id, thread_id=thread_id, **kwargs)
|
||||
|
||||
async def create_thread_operation_atomic(self, *, thread_id, idempotency_key=None, **kwargs):
|
||||
if thread_id == "slow-thread":
|
||||
self.entered.set()
|
||||
await self.release.wait()
|
||||
return await super().create_thread_operation_atomic(thread_id=thread_id, idempotency_key=idempotency_key, **kwargs)
|
||||
|
||||
async def renew_lease(self, run_id, **kwargs):
|
||||
result = await super().renew_lease(run_id, **kwargs)
|
||||
if result.renewed:
|
||||
self.renewed.set()
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("entrypoint", ["create", "create_or_reject", "record_assembly_evidence"])
|
||||
async def test_admission_wait_does_not_block_another_threads_read_and_cancel(entrypoint):
|
||||
store = HeldAdmissionStore()
|
||||
manager = RunManager(store=store)
|
||||
record = await manager.create_or_reject("other-thread", user_id="owner")
|
||||
await manager.try_start(record.run_id)
|
||||
started = asyncio.Event()
|
||||
|
||||
async def work():
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
record.task = asyncio.create_task(work())
|
||||
await started.wait()
|
||||
if entrypoint == "record_assembly_evidence":
|
||||
store.release.set()
|
||||
other = await manager.create_or_reject("slow-thread", user_id="owner")
|
||||
store.entered.clear()
|
||||
store.release.clear()
|
||||
admission = asyncio.create_task(manager.record_assembly_evidence(other.run_id, {"run_id": other.run_id, "thread_id": other.thread_id}))
|
||||
else:
|
||||
admission = asyncio.create_task(getattr(manager, entrypoint)("slow-thread", user_id="owner"))
|
||||
readers = []
|
||||
try:
|
||||
await asyncio.wait_for(store.entered.wait(), timeout=5)
|
||||
readers = [asyncio.create_task(manager.get(record.run_id, user_id="owner")), asyncio.create_task(manager.cancel(record.run_id))]
|
||||
result = await asyncio.wait_for(asyncio.gather(*readers), timeout=5)
|
||||
assert result[0] is record
|
||||
await asyncio.wait_for(asyncio.gather(record.task, return_exceptions=True), timeout=5)
|
||||
assert record.task.cancelled()
|
||||
assert not admission.done()
|
||||
finally:
|
||||
store.release.set()
|
||||
await asyncio.wait_for(asyncio.gather(admission, return_exceptions=True), timeout=5)
|
||||
record.task.cancel()
|
||||
await asyncio.gather(record.task, *readers, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_admission_wait_does_not_stop_healthy_run_heartbeats():
|
||||
store = HeldAdmissionStore()
|
||||
manager = RunManager(store=store, run_ownership_config=RunOwnershipConfig(heartbeat_enabled=True, lease_seconds=5, grace_seconds=0))
|
||||
record = await manager.create_or_reject("healthy-thread", user_id="owner")
|
||||
await manager.try_start(record.run_id)
|
||||
record.task = asyncio.create_task(asyncio.Event().wait())
|
||||
initial_lease = record.lease_expires_at
|
||||
store.renewed.clear()
|
||||
admission = asyncio.create_task(manager.create_or_reject("slow-thread", user_id="owner"))
|
||||
try:
|
||||
await asyncio.wait_for(store.entered.wait(), timeout=5)
|
||||
await manager.start_heartbeat()
|
||||
await asyncio.wait_for(store.renewed.wait(), timeout=5)
|
||||
# The Store operation and its Manager decision run in separate tasks.
|
||||
# Observe the Manager's acknowledgment, not merely the Store return.
|
||||
async with asyncio.timeout(5):
|
||||
while record.lease_expires_at == initial_lease:
|
||||
await asyncio.sleep(0)
|
||||
assert record.lease_expires_at > initial_lease
|
||||
assert not record.ownership_lost
|
||||
assert not record.task.done()
|
||||
assert not admission.done()
|
||||
finally:
|
||||
store.release.set()
|
||||
admitted = await asyncio.wait_for(asyncio.gather(admission, return_exceptions=True), timeout=5)
|
||||
if not isinstance(admitted[0], BaseException):
|
||||
await manager.cancel(admitted[0].run_id)
|
||||
await manager.cancel(record.run_id)
|
||||
await manager.stop_heartbeat(timeout=3)
|
||||
record.task.cancel()
|
||||
await asyncio.gather(record.task, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_same_thread_admissions_and_preallocated_id_reads_wait_for_publication():
|
||||
store = HeldAdmissionStore()
|
||||
manager = RunManager(store=store)
|
||||
first = asyncio.create_task(manager.create_or_reject("slow-thread", run_id="first-run"))
|
||||
tasks = [first]
|
||||
started = asyncio.Event()
|
||||
|
||||
async def second_admission():
|
||||
started.set()
|
||||
return await manager.create_or_reject("slow-thread", run_id="second-run")
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(store.entered.wait(), timeout=5)
|
||||
second = asyncio.create_task(second_admission())
|
||||
read = asyncio.create_task(manager.get("first-run"))
|
||||
history = asyncio.create_task(manager.list_by_thread("slow-thread"))
|
||||
tasks.extend([second, read, history])
|
||||
await asyncio.wait_for(started.wait(), timeout=5)
|
||||
assert not second.done() and not read.done() and not history.done()
|
||||
assert await store.get("first-run") is None
|
||||
store.release.set()
|
||||
record = await asyncio.wait_for(first, timeout=5)
|
||||
with pytest.raises(ConflictError):
|
||||
await asyncio.wait_for(second, timeout=5)
|
||||
assert await asyncio.wait_for(read, timeout=5) is record
|
||||
assert await asyncio.wait_for(history, timeout=5) == [record]
|
||||
assert await store.get("second-run") is None
|
||||
finally:
|
||||
store.release.set()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_cancelled_waiter_does_not_release_an_active_admission():
|
||||
store = HeldAdmissionStore()
|
||||
manager = RunManager(store=store)
|
||||
owner = asyncio.create_task(manager.create_or_reject("slow-thread", run_id="owner-run"))
|
||||
tasks = [owner]
|
||||
waiter_entered = asyncio.Event()
|
||||
|
||||
async def waiter():
|
||||
waiter_entered.set()
|
||||
return await manager.create_or_reject("slow-thread", run_id="cancelled-run")
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(store.entered.wait(), timeout=5)
|
||||
queued = asyncio.create_task(waiter())
|
||||
tasks.append(queued)
|
||||
await asyncio.wait_for(waiter_entered.wait(), timeout=5)
|
||||
queued.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await queued
|
||||
assert not owner.done()
|
||||
store.release.set()
|
||||
first = await asyncio.wait_for(owner, timeout=5)
|
||||
assert await manager.get("cancelled-run") is None
|
||||
assert await manager.list_by_thread("slow-thread") == [first]
|
||||
with pytest.raises(ConflictError):
|
||||
await manager.create_or_reject("slow-thread")
|
||||
finally:
|
||||
store.release.set()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_same_thread_keyed_retry_reuses_the_committed_admission():
|
||||
store = HeldAdmissionStore()
|
||||
manager = RunManager(store=store)
|
||||
first = asyncio.create_task(manager.create_or_reject("slow-thread", idempotency_key="request-key"))
|
||||
tasks = [first]
|
||||
try:
|
||||
await asyncio.wait_for(store.entered.wait(), timeout=5)
|
||||
retry = asyncio.create_task(manager.create_or_reject("slow-thread", idempotency_key="request-key"))
|
||||
tasks.append(retry)
|
||||
store.release.set()
|
||||
original, reused = await asyncio.wait_for(asyncio.gather(first, retry), timeout=5)
|
||||
assert reused.run_id == original.run_id
|
||||
assert reused.idempotency_reused
|
||||
assert len(await manager.list_by_thread("slow-thread")) == 1
|
||||
finally:
|
||||
store.release.set()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_assembly_snapshot_does_not_overwrite_an_acknowledged_renewal():
|
||||
store = MemoryRunStore()
|
||||
manager = RunManager(store=store, run_ownership_config=RunOwnershipConfig(heartbeat_enabled=True, lease_seconds=5, grace_seconds=0))
|
||||
record = await manager.create_or_reject("thread", user_id="owner")
|
||||
# Assembly is published before execution; pending runs also renew leases.
|
||||
renewed = asyncio.Event()
|
||||
release_ack = asyncio.Event()
|
||||
assembly_started = asyncio.Event()
|
||||
original_renew = store.renew_lease
|
||||
renewal_targets = []
|
||||
|
||||
async def delayed_ack(run_id, **kwargs):
|
||||
result = await original_renew(run_id, **kwargs)
|
||||
renewal_targets.append(kwargs["lease_expires_at"])
|
||||
renewed.set()
|
||||
await release_ack.wait()
|
||||
return result
|
||||
|
||||
async def publish_assembly():
|
||||
assembly_started.set()
|
||||
await manager.record_assembly_evidence(record.run_id, {"run_id": record.run_id, "thread_id": record.thread_id})
|
||||
|
||||
store.renew_lease = delayed_ack
|
||||
publication = None
|
||||
try:
|
||||
await manager.start_heartbeat()
|
||||
await asyncio.wait_for(renewed.wait(), timeout=5)
|
||||
publication = asyncio.create_task(publish_assembly())
|
||||
await asyncio.wait_for(assembly_started.wait(), timeout=5)
|
||||
release_ack.set()
|
||||
await manager.stop_heartbeat(timeout=5)
|
||||
await asyncio.wait_for(publication, timeout=5)
|
||||
stored = await store.get(record.run_id)
|
||||
assert stored["lease_expires_at"] >= renewal_targets[0]
|
||||
assert stored["lease_expires_at"] == record.lease_expires_at
|
||||
finally:
|
||||
release_ack.set()
|
||||
if publication is not None:
|
||||
await asyncio.gather(publication, return_exceptions=True)
|
||||
await manager.cancel(record.run_id)
|
||||
await manager.stop_heartbeat(timeout=3)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_renewal_cancel_does_not_hold_snapshot_gate_while_waiting_for_assembly():
|
||||
store = MemoryRunStore()
|
||||
manager = RunManager(store=store, run_ownership_config=RunOwnershipConfig(heartbeat_enabled=True, lease_seconds=5, grace_seconds=0))
|
||||
record = await manager.create_or_reject("thread", user_id="owner")
|
||||
await store.request_cancel(record.run_id, action="interrupt")
|
||||
renewed, release_ack, assembly_started = asyncio.Event(), asyncio.Event(), asyncio.Event()
|
||||
original_renew = store.renew_lease
|
||||
|
||||
async def delayed_ack(run_id, **kwargs):
|
||||
result = await original_renew(run_id, **kwargs)
|
||||
assert result.cancel_action == "interrupt"
|
||||
renewed.set()
|
||||
await release_ack.wait()
|
||||
return result
|
||||
|
||||
async def publish_assembly():
|
||||
assembly_started.set()
|
||||
await manager.record_assembly_evidence(record.run_id, {"run_id": record.run_id, "thread_id": record.thread_id})
|
||||
|
||||
store.renew_lease = delayed_ack
|
||||
renewal = asyncio.create_task(manager._renew_leases())
|
||||
publication = None
|
||||
try:
|
||||
await asyncio.wait_for(renewed.wait(), timeout=5)
|
||||
publication = asyncio.create_task(publish_assembly())
|
||||
await asyncio.wait_for(assembly_started.wait(), timeout=5)
|
||||
release_ack.set()
|
||||
await asyncio.wait_for(asyncio.shield(renewal), timeout=2)
|
||||
await asyncio.wait_for(publication, timeout=2)
|
||||
assert record.abort_event.is_set()
|
||||
assert record.abort_action == "interrupt"
|
||||
assert not record.ownership_lost
|
||||
finally:
|
||||
release_ack.set()
|
||||
if publication is not None:
|
||||
publication.cancel()
|
||||
await asyncio.gather(publication, return_exceptions=True)
|
||||
await manager.stop_heartbeat(timeout=3)
|
||||
await asyncio.gather(renewal, return_exceptions=True)
|
||||
await manager.cancel(record.run_id)
|
||||
@ -24,7 +24,7 @@ def assets(tmp_path, monkeypatch):
|
||||
storage = UserScopedSkillStorage("owner", host_path=str(tmp_path / "skills"))
|
||||
root = storage.get_custom_skill_dir("example")
|
||||
root.mkdir(parents=True)
|
||||
(root / "SKILL.md").write_text(CONTENT, encoding="utf-8")
|
||||
(root / "SKILL.md").write_text(CONTENT, encoding="utf-8", newline="")
|
||||
engine = create_engine(f"sqlite:///{tmp_path / 'mutations.db'}")
|
||||
Base.metadata.create_all(engine)
|
||||
sessions = sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
@ -1080,6 +1080,8 @@ def test_subagent_executor_resolves_skill_authorization_for_chain(monkeypatch):
|
||||
executor = SubagentExecutor.__new__(SubagentExecutor)
|
||||
executor.config = SimpleNamespace(name="sub", skills=None, max_turns=25)
|
||||
executor.model_name = "m"
|
||||
executor.thinking_enabled = False
|
||||
executor.reasoning_effort = None
|
||||
executor.app_config = app_config
|
||||
executor._resolved_app_config = app_config
|
||||
executor._available_skill_names = None
|
||||
@ -1444,6 +1446,8 @@ def test_subagent_executor_shares_one_skill_authorization_instance(monkeypatch):
|
||||
executor = SubagentExecutor.__new__(SubagentExecutor)
|
||||
executor.config = SimpleNamespace(name="sub", skills=None, max_turns=25)
|
||||
executor.model_name = "m"
|
||||
executor.thinking_enabled = False
|
||||
executor.reasoning_effort = None
|
||||
executor.app_config = app_config
|
||||
executor._resolved_app_config = app_config
|
||||
executor._available_skill_names = None
|
||||
|
||||
@ -9,6 +9,7 @@ from test_terminal_store_contract import store as store
|
||||
|
||||
from deerflow.config.run_ownership_config import RunOwnershipConfig
|
||||
from deerflow.runtime import RunManager, RunStatus
|
||||
from deerflow.runtime.runs import manager as manager_module
|
||||
|
||||
|
||||
def manager_for(run_store):
|
||||
@ -41,7 +42,15 @@ async def finish(manager, record, *tasks):
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pre_rollback_renewal_retries_after_interrupted_ack(store):
|
||||
async def test_pre_rollback_renewal_retries_after_interrupted_ack(store, monkeypatch):
|
||||
now = datetime.now(UTC)
|
||||
|
||||
class Clock(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return now if tz is not None else now.replace(tzinfo=None)
|
||||
|
||||
monkeypatch.setattr(manager_module, "datetime", Clock)
|
||||
manager = manager_for(store)
|
||||
record = await running_record(manager)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
@ -66,6 +75,9 @@ async def test_pre_rollback_renewal_retries_after_interrupted_ack(store):
|
||||
assert record.rollback_finalization_pending
|
||||
old_expiry = record.lease_expires_at
|
||||
|
||||
# Memory calls may finish within one Windows wall-clock tick. Pin a
|
||||
# later renewal instant instead of relying on incidental test latency.
|
||||
now += timedelta(seconds=1)
|
||||
release.set()
|
||||
await asyncio.wait_for(renewal, 2)
|
||||
|
||||
|
||||
@ -169,6 +169,8 @@ def _setup_executor_classes():
|
||||
sandbox_provider_module = importlib.import_module("deerflow.sandbox.sandbox_provider")
|
||||
sandbox_overwrite_module = importlib.import_module("deerflow.sandbox.overwrite")
|
||||
sandbox_prewarm_module = importlib.import_module("deerflow.sandbox.prewarm")
|
||||
for module_name in ("deerflow.models.factory", "deerflow.models.openai_codex_provider", "deerflow.agents.assembly_descriptor"):
|
||||
importlib.import_module(module_name)
|
||||
|
||||
# Remove mocked executor if exists (from conftest.py)
|
||||
if "deerflow.subagents.executor" in sys.modules:
|
||||
@ -395,6 +397,105 @@ def msg(classes):
|
||||
class TestAgentConstruction:
|
||||
"""Test _create_agent() wiring before execution starts."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thinking,effort,mode,expected_thinking,expected_effort,expected_adjustment",
|
||||
[
|
||||
(None, None, "optional", False, "lite", None),
|
||||
(True, "high", "optional", True, "deep", "effort_aliased"),
|
||||
(False, None, "required", True, "lite", "thinking_forced_on"),
|
||||
(True, None, "unsupported", False, None, "thinking_unsupported"),
|
||||
(True, "unknown", "optional", True, "lite", "effort_unsupported_value"),
|
||||
(True, "high", "legacy", True, "high", None),
|
||||
(True, None, "legacy-unsupported", False, None, None),
|
||||
],
|
||||
)
|
||||
def test_reasoning_factory_and_observer_agree(self, classes, base_config, monkeypatch, caplog, thinking, effort, mode, expected_thinking, expected_effort, expected_adjustment):
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
from deerflow.config.sandbox_config import SandboxConfig
|
||||
from deerflow.extensions.registry import ExtensionRegistry
|
||||
from deerflow.subagents import executor as executor_module
|
||||
|
||||
factory = importlib.import_module("deerflow.models.factory")
|
||||
profile_settings = {}
|
||||
if mode in {"legacy", "legacy-unsupported"}:
|
||||
profile_settings = {
|
||||
"supports_thinking": mode == "legacy",
|
||||
"supports_reasoning_effort": True,
|
||||
"when_thinking_enabled": {"extra_body": {"thinking": {"type": "enabled"}}},
|
||||
}
|
||||
else:
|
||||
reasoning = {"thinking": mode, "dialect": "openai_extra_body"}
|
||||
if mode != "unsupported":
|
||||
reasoning["effort"] = {"values": ["lite", "deep"], "default": "lite", "aliases": {"high": "deep"}}
|
||||
profile_settings["reasoning"] = reasoning
|
||||
profile = ModelConfig(name="child-model", use="langchain_openai:ChatOpenAI", model="child-model", **profile_settings)
|
||||
app_config = AppConfig(models=[profile], sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"))
|
||||
unchanged = app_config.model_dump()
|
||||
captured = {}
|
||||
|
||||
class RecordingModel:
|
||||
def __init__(self, **kwargs):
|
||||
captured.update(kwargs)
|
||||
|
||||
class Observer:
|
||||
def on_agent_assembled(self, app_store, descriptor):
|
||||
captured["descriptor"] = descriptor
|
||||
|
||||
registry = ExtensionRegistry()
|
||||
with registry.attributed_to("reasoning-test"):
|
||||
registry.agent_assembly_observer(Observer())
|
||||
monkeypatch.setattr(factory, "resolve_class", lambda *args: RecordingModel)
|
||||
monkeypatch.setattr(executor_module, "create_chat_model", factory.create_chat_model)
|
||||
monkeypatch.setattr(executor_module, "create_agent", lambda **kwargs: SimpleNamespace(get_graph=lambda: SimpleNamespace(nodes={})))
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"deerflow.agents.middlewares.tool_error_handling_middleware",
|
||||
_module("deerflow.agents.middlewares.tool_error_handling_middleware", build_subagent_runtime_middlewares=lambda **kwargs: []),
|
||||
)
|
||||
base_config.thinking_enabled = thinking
|
||||
base_config.reasoning_effort = effort
|
||||
executor = classes["SubagentExecutor"](config=base_config, tools=[], app_config=app_config, parent_model="child-model", extensions=registry.build())
|
||||
if mode == "legacy-unsupported":
|
||||
with pytest.raises(ValueError, match="Model child-model does not support thinking.*supports_thinking"):
|
||||
asyncio.run(executor._create_agent())
|
||||
assert not captured
|
||||
assert app_config.model_dump() == unchanged
|
||||
return
|
||||
|
||||
with caplog.at_level(logging.INFO, logger=factory.__name__):
|
||||
asyncio.run(executor._create_agent())
|
||||
if expected_adjustment is not None:
|
||||
assert any(record.name == factory.__name__ and expected_adjustment in record.getMessage() for record in caplog.records)
|
||||
descriptor = captured["descriptor"]
|
||||
assert descriptor.thinking_enabled is expected_thinking
|
||||
assert descriptor.reasoning_effort == expected_effort
|
||||
assert captured.get("reasoning_effort") == expected_effort
|
||||
assert captured["extra_body"]["thinking"]["type"] == ("enabled" if expected_thinking else "disabled")
|
||||
assert app_config.model_dump() == unchanged
|
||||
|
||||
def test_required_thinking_rejection_names_model_before_creation(self, classes, base_config, monkeypatch):
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
from deerflow.config.sandbox_config import SandboxConfig
|
||||
from deerflow.models.reasoning import ReasoningPolicyError
|
||||
from deerflow.subagents import executor as executor_module
|
||||
|
||||
profile = ModelConfig(name="required-worker-model", use="langchain_openai:ChatOpenAI", model="test-model", reasoning={"thinking": "required", "on_disable_request": "reject"})
|
||||
app_config = AppConfig(models=[profile], sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"))
|
||||
model_factory = MagicMock(side_effect=AssertionError("model creation must not start"))
|
||||
graph_factory = MagicMock(side_effect=AssertionError("graph creation must not start"))
|
||||
monkeypatch.setattr(executor_module, "create_chat_model", model_factory)
|
||||
monkeypatch.setattr(executor_module, "create_agent", graph_factory)
|
||||
executor = classes["SubagentExecutor"](config=base_config, tools=[], app_config=app_config, parent_model=profile.name)
|
||||
|
||||
with pytest.raises(ValueError, match=f"Model {profile.name}: model requires thinking") as error:
|
||||
asyncio.run(executor._create_agent())
|
||||
|
||||
assert isinstance(error.value.__cause__, ReasoningPolicyError)
|
||||
model_factory.assert_not_called()
|
||||
graph_factory.assert_not_called()
|
||||
|
||||
def test_create_agent_threads_explicit_app_config_to_model_and_middlewares(
|
||||
self,
|
||||
classes,
|
||||
@ -460,6 +561,7 @@ class TestAgentConstruction:
|
||||
assert captured["model"] == {
|
||||
"name": "parent-model",
|
||||
"thinking_enabled": False,
|
||||
"reasoning_effort": None,
|
||||
"app_config": app_config,
|
||||
# attach_tracing=False pairs with graph-root tracing callbacks
|
||||
# injected in _aexecute (see TestSubagentTracingWiring). Without
|
||||
|
||||
41
backend/tests/test_subagent_thinking_config.py
Normal file
41
backend/tests/test_subagent_thinking_config.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Native registry coverage for per-worker reasoning defaults and overrides."""
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.config.subagents_config import CustomSubagentConfig, SubagentOverrideConfig, SubagentsAppConfig
|
||||
from deerflow.persistence.managed_subagents import ManagedSubagentDefinition
|
||||
from deerflow.subagents import registry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["builtin", "config", "managed"])
|
||||
def test_reasoning_settings_flow_through_registry_without_mutating_definitions(monkeypatch, source):
|
||||
name = "general-purpose" if source == "builtin" else "researcher"
|
||||
settings = {"thinking_enabled": True, "reasoning_effort": "deep"}
|
||||
app_config = SubagentsAppConfig()
|
||||
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [])
|
||||
if source == "builtin":
|
||||
app_config.agents[name] = SubagentOverrideConfig(**settings)
|
||||
original = registry.BUILTIN_SUBAGENTS[name]
|
||||
elif source == "config":
|
||||
original = CustomSubagentConfig(description="Research", system_prompt="Research the task.", **settings)
|
||||
app_config.custom_agents[name] = original
|
||||
else:
|
||||
original = ManagedSubagentDefinition(name=name, description="Research", system_prompt="Research the task.", **settings)
|
||||
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [original])
|
||||
|
||||
configured = registry.get_subagent_config(name, app_config=app_config)
|
||||
assert (configured.thinking_enabled, configured.reasoning_effort) == (True, "deep")
|
||||
|
||||
app_config.agents[name] = SubagentOverrideConfig(thinking_enabled=False, reasoning_effort="lite")
|
||||
overridden = registry.get_subagent_config(name, app_config=app_config)
|
||||
assert (overridden.thinking_enabled, overridden.reasoning_effort) == (False, "lite")
|
||||
assert (configured.thinking_enabled, configured.reasoning_effort) == (True, "deep")
|
||||
assert original.thinking_enabled is (None if source == "builtin" else True)
|
||||
assert original.reasoning_effort == (None if source == "builtin" else "deep")
|
||||
|
||||
|
||||
def test_unconfigured_workers_keep_reasoning_defaults(monkeypatch):
|
||||
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [])
|
||||
configured = registry.get_subagent_config("general-purpose", app_config=SubagentsAppConfig())
|
||||
assert configured.thinking_enabled is None
|
||||
assert configured.reasoning_effort is None
|
||||
@ -137,17 +137,23 @@ async def test_admin_can_create_update_and_delete_managed_subagent():
|
||||
name="planner",
|
||||
description="Plans creative work",
|
||||
system_prompt="You are a creative planner.",
|
||||
thinking_enabled=True,
|
||||
reasoning_effort="xhigh",
|
||||
),
|
||||
)
|
||||
assert created.source == "managed"
|
||||
assert created.system_prompt == "You are a creative planner."
|
||||
assert created.thinking_enabled is True
|
||||
assert created.reasoning_effort == "xhigh"
|
||||
|
||||
updated = await router.update_managed_subagent(
|
||||
"planner",
|
||||
_request("admin"),
|
||||
router.ManagedSubagentUpdateRequest(enabled=False),
|
||||
router.ManagedSubagentUpdateRequest(enabled=False, thinking_enabled=False, reasoning_effort=None),
|
||||
)
|
||||
assert updated.enabled is False
|
||||
assert updated.thinking_enabled is False
|
||||
assert updated.reasoning_effort is None
|
||||
|
||||
await router.delete_managed_subagent("planner", _request("admin"))
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
|
||||
256
backend/tests/test_telegram_reply_threads.py
Normal file
256
backend/tests/test_telegram_reply_threads.py
Normal file
@ -0,0 +1,256 @@
|
||||
"""Telegram reply routing through real stores, with an offline Bot API double."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from itertools import count
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.channels.manager import ChannelManager
|
||||
from app.channels.message_bus import MessageBus, OutboundMessage, ResolvedAttachment
|
||||
from app.channels.store import JsonChannelStore, SqlChannelStore
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.fixture(params=["json", "sqlite", "bound"])
|
||||
async def routing(request, tmp_path):
|
||||
repo = None
|
||||
connection = None
|
||||
session_factory = None
|
||||
if request.param != "json":
|
||||
from deerflow.persistence.channel_connections import ChannelConnectionRepository, ChannelCredentialCipher
|
||||
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine
|
||||
|
||||
await init_engine("sqlite", url=f"sqlite+aiosqlite:///{tmp_path / 'channel.db'}", sqlite_dir=str(tmp_path))
|
||||
session_factory = get_session_factory()
|
||||
if request.param == "bound":
|
||||
repo = ChannelConnectionRepository(session_factory, cipher=ChannelCredentialCipher.from_key("test-key"))
|
||||
connection = await repo.upsert_connection(
|
||||
owner_user_id="owner-a",
|
||||
provider="telegram",
|
||||
external_account_id="42",
|
||||
workspace_id="-10042",
|
||||
)
|
||||
store = SqlChannelStore(session_factory) if session_factory else JsonChannelStore(tmp_path / "store.json")
|
||||
bus = MessageBus()
|
||||
channel = TelegramChannel(bus=bus, config={"bot_token": "test-token", "channel_store": store, "connection_repo": repo})
|
||||
channel._main_loop = asyncio.get_running_loop()
|
||||
message_ids = count(100)
|
||||
sent_ids = []
|
||||
|
||||
async def send(**kwargs):
|
||||
message_id = next(message_ids)
|
||||
sent_ids.append(message_id)
|
||||
return SimpleNamespace(message_id=message_id)
|
||||
|
||||
async def rich(endpoint, api_kwargs):
|
||||
if endpoint == "editMessageText":
|
||||
return {"message_id": api_kwargs["message_id"]}
|
||||
return {"message_id": (await send()).message_id}
|
||||
|
||||
bot = SimpleNamespace(send_message=AsyncMock(side_effect=send), edit_message_text=AsyncMock(), do_api_request=AsyncMock(side_effect=rich), send_photo=AsyncMock(side_effect=send), send_document=AsyncMock(side_effect=send))
|
||||
channel._application = SimpleNamespace(bot=bot)
|
||||
thread_ids = count(1)
|
||||
client = SimpleNamespace(threads=SimpleNamespace(create=AsyncMock(side_effect=lambda **kw: {"thread_id": f"thread-{next(thread_ids)}"})))
|
||||
manager = ChannelManager(bus=bus, store=store, connection_repo=repo)
|
||||
|
||||
async def incoming(message_id, reply_id=None, *, chat_type="supergroup", user_id=42):
|
||||
chat_id = 42 if chat_type == "private" else -10042
|
||||
update = SimpleNamespace(
|
||||
effective_chat=SimpleNamespace(id=chat_id, type=chat_type),
|
||||
effective_user=SimpleNamespace(id=user_id),
|
||||
message=SimpleNamespace(text="question", caption=None, photo=[], document=None, message_id=message_id, reply_to_message=SimpleNamespace(message_id=reply_id) if reply_id else None),
|
||||
)
|
||||
await channel._on_text(update, None)
|
||||
return await asyncio.wait_for(bus.get_inbound(), timeout=5)
|
||||
|
||||
def outbound(inbound, thread_id, **kwargs):
|
||||
return OutboundMessage(
|
||||
channel_name="telegram",
|
||||
chat_id=inbound.chat_id,
|
||||
thread_id=thread_id,
|
||||
thread_ts=inbound.thread_ts,
|
||||
connection_id=inbound.connection_id,
|
||||
owner_user_id=inbound.owner_user_id,
|
||||
metadata=inbound.metadata,
|
||||
text=kwargs.pop("text", "Answer"),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
harness = SimpleNamespace(channel=channel, bot=bot, bus=bus, store=store, repo=repo, connection=connection, manager=manager, client=client, incoming=incoming, outbound=outbound, sent_ids=sent_ids)
|
||||
try:
|
||||
yield harness
|
||||
finally:
|
||||
await channel._close_and_drain_threadsafe_futures()
|
||||
if session_factory:
|
||||
await close_engine()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chat_type", ["private", "group", "supergroup"])
|
||||
@pytest.mark.parametrize("reply_to_bot", [False, True])
|
||||
async def test_reply_reuses_conversation(routing, chat_type, reply_to_bot):
|
||||
h = routing
|
||||
first = await h.incoming(10, chat_type=chat_type)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
await h.channel.send(h.outbound(first, thread_id))
|
||||
response_id = h.bot.edit_message_text.await_args.kwargs["message_id"]
|
||||
followup = await h.incoming(11, response_id if reply_to_bot else 10, chat_type=chat_type)
|
||||
assert await h.manager._get_or_create_thread(h.client, followup) == (thread_id, False)
|
||||
h.client.threads.create.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_stream_aliases_are_not_rewritten_and_new_keeps_its_binding(routing):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
repository = h.repo or h.store
|
||||
claim = repository.claim_thread_id
|
||||
repository.claim_thread_id = AsyncMock(wraps=claim)
|
||||
await h.channel.send(h.outbound(first, thread_id, text="Partial", is_final=False))
|
||||
calls = repository.claim_thread_id.await_count
|
||||
assert calls == 2 # Source message and bot preview.
|
||||
await h.channel.send(h.outbound(first, thread_id, text="Partial", is_final=False))
|
||||
assert repository.claim_thread_id.await_count == calls
|
||||
|
||||
command = await h.incoming(11, h.sent_ids[0])
|
||||
new_thread = await h.manager._create_thread(h.client, command)
|
||||
assert new_thread != thread_id
|
||||
await h.channel.send(h.outbound(first, thread_id, text="Old run finished"))
|
||||
assert await h.manager._lookup_thread_id(command) == new_thread
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delivery", ["plain", "rich", "split", "fallback"])
|
||||
async def test_stream_binding_outage_logs_once_per_answer(routing, caplog, delivery):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
repository = h.repo or h.store
|
||||
repository.claim_thread_id = AsyncMock(side_effect=RuntimeError("store unavailable"))
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger="app.channels.telegram"):
|
||||
for index in range(20):
|
||||
await h.channel.send(h.outbound(first, thread_id, text=f"Partial {index}", is_final=False))
|
||||
if delivery == "rich":
|
||||
h.channel.config["rich_messages"] = True
|
||||
if delivery == "fallback":
|
||||
h.bot.edit_message_text.side_effect = RuntimeError("edit unavailable")
|
||||
text = "x" * 9000 if delivery == "split" else "Final answer"
|
||||
await h.channel.send(h.outbound(first, thread_id, text=text))
|
||||
|
||||
failures = [record for record in caplog.records if record.name == "app.channels.telegram" and "thread mapping" in record.getMessage()]
|
||||
assert len(failures) == repository.claim_thread_id.await_count
|
||||
assert len(failures) >= 21, "every failed update must keep retrying the mapping"
|
||||
assert sum(record.levelno == logging.ERROR for record in failures) == 1
|
||||
assert failures[0].exc_info is not None
|
||||
assert all(record.levelno == logging.DEBUG and record.exc_info is None for record in failures[1:])
|
||||
assert len(h.sent_ids) == {"plain": 1, "rich": 1, "split": 3, "fallback": 2}[delivery]
|
||||
|
||||
# Finalization retires the per-answer log state. A subsequent answer
|
||||
# must report its first outage even on the same conversation key.
|
||||
await h.channel.send(h.outbound(first, thread_id, text="Next answer", is_final=False))
|
||||
errors = [record for record in caplog.records if record.name == "app.channels.telegram" and "thread mapping" in record.getMessage() and record.levelno == logging.ERROR]
|
||||
assert len(errors) == 2
|
||||
await h.channel.send(h.outbound(first, thread_id, text="Next answer"))
|
||||
|
||||
|
||||
async def test_binding_failure_does_not_fail_delivery_and_stream_retries(routing, caplog):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
repository = h.repo or h.store
|
||||
claim = repository.claim_thread_id
|
||||
repository.claim_thread_id = AsyncMock(side_effect=RuntimeError("store unavailable"))
|
||||
await h.channel.send(h.outbound(first, thread_id, is_final=False))
|
||||
assert h.bot.edit_message_text.await_count == 1
|
||||
assert "thread mapping" in caplog.text
|
||||
if h.repo:
|
||||
assert await h.store.list_entries() == []
|
||||
repository.claim_thread_id = claim
|
||||
await h.channel.send(h.outbound(first, thread_id, is_final=False))
|
||||
followup = await h.incoming(11, h.sent_ids[0])
|
||||
assert await h.manager._get_or_create_thread(h.client, followup) == (thread_id, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routing", ["bound"], indirect=True)
|
||||
async def test_bound_reply_does_not_expose_thread_to_another_owner(routing):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
await h.channel.send(h.outbound(first, thread_id))
|
||||
response_id = h.sent_ids[0]
|
||||
assert await h.store.list_entries() == []
|
||||
await h.repo.upsert_connection(owner_user_id="owner-b", provider="telegram", external_account_id="43", workspace_id="-10042")
|
||||
other = await h.incoming(11, response_id, user_id=43)
|
||||
assert other.owner_user_id == "owner-b"
|
||||
assert await h.manager._lookup_thread_id(other) is None
|
||||
unbound = await h.incoming(12, response_id, user_id=44)
|
||||
assert unbound.connection_id is None
|
||||
assert await h.manager._lookup_thread_id(unbound) is None
|
||||
h.client.threads.create.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delivery", ["split", "fallback", "rich", "rich-new", "rich-fallback", "plain-new", "preview", "preview-fallback"])
|
||||
async def test_every_delivered_reply_keeps_thread(routing, delivery):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
placeholder_id = h.sent_ids[0]
|
||||
if delivery.endswith("new"):
|
||||
h.channel._stream_messages.clear()
|
||||
if delivery == "fallback":
|
||||
h.bot.edit_message_text.side_effect = RuntimeError("edit unavailable")
|
||||
if delivery.startswith("rich"):
|
||||
h.channel.config["rich_messages"] = True
|
||||
if delivery == "rich-fallback":
|
||||
from telegram.error import BadRequest
|
||||
|
||||
h.bot.do_api_request.side_effect = BadRequest("unsupported")
|
||||
text = "x" * 9000 if delivery == "split" else "**Answer**"
|
||||
await h.channel.send(h.outbound(first, thread_id, text=text, is_final=not delivery.startswith("preview")))
|
||||
if delivery == "preview-fallback":
|
||||
h.channel._stream_messages[f"{first.chat_id}:{first.thread_ts}"]["last_edit_at"] = 0.0
|
||||
h.bot.edit_message_text.side_effect = RuntimeError("edit unavailable")
|
||||
await h.channel.send(h.outbound(first, thread_id, text="Next chunk", is_final=False))
|
||||
reply_ids = h.sent_ids[1:] if delivery.endswith("new") or delivery == "fallback" else list(h.sent_ids)
|
||||
assert reply_ids
|
||||
for reply_id in reply_ids:
|
||||
followup = await h.incoming(200 + reply_id, reply_id)
|
||||
assert await h.manager._get_or_create_thread(h.client, followup) == (thread_id, False)
|
||||
if delivery == "split":
|
||||
assert placeholder_id in reply_ids and len(reply_ids) == 3
|
||||
|
||||
|
||||
async def test_followup_source_alias_and_restart(routing):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
await h.channel.send(h.outbound(first, thread_id))
|
||||
followup = await h.incoming(11, h.sent_ids[0])
|
||||
assert await h.manager._get_or_create_thread(h.client, followup) == (thread_id, False)
|
||||
await h.channel.send(h.outbound(followup, thread_id))
|
||||
store = JsonChannelStore(h.store.path) if isinstance(h.store, JsonChannelStore) else h.store
|
||||
restarted = ChannelManager(bus=h.bus, store=store, connection_repo=h.repo)
|
||||
for reply_id in (11, h.sent_ids[-1]):
|
||||
incoming = await h.incoming(300 + reply_id, reply_id)
|
||||
assert await restarted._get_or_create_thread(h.client, incoming) == (thread_id, False)
|
||||
unrelated = await h.incoming(900)
|
||||
assert (await restarted._get_or_create_thread(h.client, unrelated))[0] != thread_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("image", [False, True])
|
||||
async def test_reply_to_attachment_keeps_thread(routing, tmp_path, image):
|
||||
h = routing
|
||||
first = await h.incoming(10)
|
||||
thread_id, _ = await h.manager._get_or_create_thread(h.client, first)
|
||||
path = tmp_path / ("chart.png" if image else "report.txt")
|
||||
path.write_bytes(b"fixture")
|
||||
attachment = ResolvedAttachment(virtual_path=f"/mnt/user-data/outputs/{path.name}", actual_path=path, filename=path.name, size=7, mime_type="image/png" if image else "text/plain", is_image=image)
|
||||
assert await h.channel.send_file(h.outbound(first, thread_id), attachment)
|
||||
followup = await h.incoming(11, h.sent_ids[-1])
|
||||
assert await h.manager._get_or_create_thread(h.client, followup) == (thread_id, False)
|
||||
@ -68,6 +68,12 @@ declared = re.search(r'(?m)^version\\s*=\\s*"([^"]+)"', (root / "pyproject.toml"
|
||||
recorded = re.search(r'(?m)^name = "deer-flow"\\nversion = "([^"]+)"', (root / "uv.lock").read_text(encoding="utf-8"))
|
||||
if recorded is None or recorded.group(1) != declared.replace("-", ""):
|
||||
sys.exit("The lockfile at `uv.lock` needs to be updated, but `--check` was provided. To update the lockfile, run `uv lock`.")
|
||||
harness = root / "packages" / "harness" / "pyproject.toml"
|
||||
if harness.exists():
|
||||
harness_declared = re.search(r'(?m)^version\\s*=\\s*"([^"]+)"', harness.read_text(encoding="utf-8")).group(1)
|
||||
harness_recorded = re.search(r'(?m)^name = "deerflow-harness"\\nversion = "([^"]+)"', (root / "uv.lock").read_text(encoding="utf-8"))
|
||||
if harness_recorded is None or harness_recorded.group(1) != harness_declared.replace("-", ""):
|
||||
sys.exit("The lockfile at `uv.lock` needs to be updated, but `--check` was provided. To update the lockfile, run `uv lock`.")
|
||||
print("Resolved 1 package")
|
||||
"""
|
||||
|
||||
@ -75,14 +81,15 @@ print("Resolved 1 package")
|
||||
def _write_sandbox(root: Path, *, sources: str, lock: str, lock_name: str = "deer-flow") -> Path:
|
||||
"""A repo-shaped tree with every version source at ``sources`` and the lock at ``lock``."""
|
||||
(root / "scripts").mkdir(parents=True)
|
||||
(root / "backend").mkdir()
|
||||
(root / "backend" / "packages" / "harness").mkdir(parents=True)
|
||||
(root / "frontend").mkdir()
|
||||
(root / "deploy" / "helm" / "deer-flow").mkdir(parents=True)
|
||||
shutil.copy2(REPO_ROOT / "scripts" / "verify_versions.sh", root / "scripts" / "verify_versions.sh")
|
||||
|
||||
(root / "backend" / "pyproject.toml").write_text(f'[project]\nname = "deer-flow"\nversion = "{sources}"\n', encoding="utf-8")
|
||||
(root / "backend" / "packages" / "harness" / "pyproject.toml").write_text(f'[project]\nname = "deerflow-harness"\nversion = "{sources}"\n', encoding="utf-8")
|
||||
(root / "backend" / "uv.lock").write_text(
|
||||
f'version = 1\n\n[[package]]\nname = "{lock_name}"\nversion = "{lock}"\nsource = {{ virtual = "." }}\n',
|
||||
f'version = 1\n\n[[package]]\nname = "{lock_name}"\nversion = "{lock}"\nsource = {{ virtual = "." }}\n\n[[package]]\nname = "deerflow-harness"\nversion = "{lock}"\nsource = {{ editable = "packages/harness" }}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "frontend" / "package.json").write_text(f'{{\n "name": "deer-flow-frontend",\n "version": "{sources}"\n}}\n', encoding="utf-8")
|
||||
@ -131,6 +138,7 @@ def test_gate_passes_when_the_lock_matches(tmp_path: Path, sources: str, lock: s
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert f"backend/uv.lock: {lock}" in result.stdout
|
||||
assert f"harness/pyproject.toml: {sources}" in result.stdout
|
||||
assert _uv_calls(bin_dir) == [f"{(root / 'backend').resolve()} uv lock --check"]
|
||||
|
||||
|
||||
@ -149,6 +157,22 @@ def test_gate_rejects_a_lock_left_on_the_previous_version(tmp_path: Path, args:
|
||||
assert "OK —" not in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [("2.2.0",), ()], ids=["against-tag", "mutual"])
|
||||
def test_gate_rejects_a_harness_pyproject_left_on_the_previous_version(tmp_path: Path, args: tuple[str, ...]):
|
||||
"""The other sources were bumped; backend/packages/harness/pyproject.toml was not."""
|
||||
root = _write_sandbox(tmp_path / "repo", sources="2.2.0", lock="2.2.0")
|
||||
harness = root / "backend" / "packages" / "harness" / "pyproject.toml"
|
||||
harness.write_text('[project]\nname = "deerflow-harness"\nversion = "2.1.0"\n', encoding="utf-8")
|
||||
bin_dir = _install_fake_uv(root)
|
||||
|
||||
result = _run_verify(root, *args, path=_with_fake_uv(bin_dir))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "::error::backend/packages/harness/pyproject.toml is '2.1.0' but expected '2.2.0'." in result.stderr
|
||||
assert "Tip: run scripts/bump_version.sh" in result.stderr
|
||||
assert "OK —" not in result.stdout
|
||||
|
||||
|
||||
def test_gate_does_not_blame_the_lock_for_an_unrelated_uv_failure(tmp_path: Path):
|
||||
"""A uv failure that is not a stale lock is still fatal, but the annotation must not claim staleness."""
|
||||
root = _write_sandbox(tmp_path / "repo", sources="2.2.0", lock="2.2.0")
|
||||
|
||||
560
backend/uv.lock
generated
560
backend/uv.lock
generated
@ -845,7 +845,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "deer-flow"
|
||||
version = "2.2.0.dev0"
|
||||
version = "2.3.0.dev0"
|
||||
source = { virtual = "." }
|
||||
default-groups = ["dev", "extensions"]
|
||||
dependencies = [
|
||||
@ -972,7 +972,7 @@ source = { editable = "packages/extension-api" }
|
||||
|
||||
[[package]]
|
||||
name = "deerflow-harness"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0.dev0"
|
||||
source = { editable = "packages/harness" }
|
||||
dependencies = [
|
||||
{ name = "agent-client-protocol" },
|
||||
@ -1168,11 +1168,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1564,99 +1564,99 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.80.0"
|
||||
version = "1.81.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264, upload-time = "2026-06-11T12:45:21.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/95/539706ca0d3bd40dbad583dc56fd883da941f37556b629132da5762781b9/grpcio-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:12b7524c88d4026d3dcb7b0ebe16b6714f3b4af402ddd0f0639ab064a00c87c3", size = 12052560, upload-time = "2026-06-11T12:45:23.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983, upload-time = "2026-06-11T12:45:26.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455, upload-time = "2026-06-11T12:45:29.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167, upload-time = "2026-06-11T12:45:32.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536, upload-time = "2026-06-11T12:45:35.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276, upload-time = "2026-06-11T12:45:37.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660, upload-time = "2026-06-11T12:45:40.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/40/7debc0da45d2efebafb82da75644be347497fe4ee250514b8cd3b86ae8bf/grpcio-1.81.1-cp312-cp312-win32.whl", hash = "sha256:a185a04039df6cae8648bc8ab6d6fde7bf94f7188ecf7828e76ac52eef1e41d6", size = 4185819, upload-time = "2026-06-11T12:45:43.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b9/8fe3ba5ed462067774ebc1f9c7f26aa7ebcc280ddd476be107153de1339e/grpcio-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ad74f8bb1a18963914c5452d289422830b39459e8776ebbcd207be1fbfb1d94", size = 4930461, upload-time = "2026-06-11T12:45:45.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio-health-checking"
|
||||
version = "1.80.0"
|
||||
version = "1.81.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/a2/aa3cc47f19c03f8e5287b987317059753141a3af8f66b96d5a64b3be10b8/grpcio_health_checking-1.80.0.tar.gz", hash = "sha256:2cc5f08bc8b816b8655ab6f59c71450063ba20766d31e21a493e912e3560c8b1", size = 17117, upload-time = "2026-03-30T08:54:41.899Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/46/6b6678b5a922765ae7637205bb6d0618a4da8b35f2ce6116f8bcff262370/grpcio_health_checking-1.81.1.tar.gz", hash = "sha256:ecc61480e25058a4a04e11e4ab6900ad7439b32e60a8ce4ece7d9f219221c85d", size = 17107, upload-time = "2026-06-11T12:58:49.632Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/d1/d97eb30386feff6ac2a662620e2ed68be352e9a182d62e06213db694906a/grpcio_health_checking-1.80.0-py3-none-any.whl", hash = "sha256:d804d4549cbb71e90ca2c7bf0c501060135dfd220aca8e2c54f96d3e79e210e5", size = 19125, upload-time = "2026-03-30T08:53:44.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl", hash = "sha256:cbc6a4171825ec64389de2f062d296ba129a5c27eefd0dd55fa837909184bdf9", size = 19120, upload-time = "2026-06-11T12:58:38.008Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio-tools"
|
||||
version = "1.80.0"
|
||||
version = "1.81.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/c8/1223f29c84a143ae9a56c084fc96894de0ba84b6e8d60a26241abd81d278/grpcio_tools-1.80.0.tar.gz", hash = "sha256:26052b19c6ce0dcf52d1024496aea3e2bdfa864159f06dc7b97b22d041a94b26", size = 6133212, upload-time = "2026-03-30T08:52:39.077Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/b9/65929df8c9614792db900a8e45d4997fadbd1734c827da3f0eb1f2fe4866/grpcio_tools-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:d19d5a8244311947b96f749c417b32d144641c6953f1164824579e1f0a51d040", size = 2550856, upload-time = "2026-03-30T08:50:57.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/af1557544d68d1aeca9d9ea53ed16524022d521fec6ba334ab3530e9c1a6/grpcio_tools-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fb599a3dc89ed1bb24489a2724b2f6dd4cddbbf0f7bdd69c073477bab0dc7554", size = 5710883, upload-time = "2026-03-30T08:51:00.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/48/aa9b4f7519ca972bc40d315d5c28f05ca28fa08de13d4e8b69f551b798ab/grpcio_tools-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:623ee31fc2ff7df9a987b4f3d139c30af17ce46a861ae0e25fb8c112daa32dd8", size = 2598004, upload-time = "2026-03-30T08:51:02.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b8/b01371c119924b3beca1fe3f047b1bc2cdc66b3d37f0f3acc9d10c567a43/grpcio_tools-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b46570a68378539ee2b75a5a43202561f8d753c832798b1047099e3c551cf5d6", size = 2909568, upload-time = "2026-03-30T08:51:04.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/7c/1108f7bdb58475a7e701ec89b55eb494538b6e76acd211ba0d4cc5fd28e8/grpcio_tools-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51caf99c28999e7e0f97e9cea190c1405b7681a57bb2e0631205accd92b43fa4", size = 2660938, upload-time = "2026-03-30T08:51:06.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/59/d1c0063d4cd3b85363c7044ff3e5159d6d5df96e2692a9a5312d9c8cb290/grpcio_tools-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cdaa1c9aa8d3a87891a96700cadd29beec214711d6522818d207277f6452567c", size = 3113814, upload-time = "2026-03-30T08:51:08.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/21/18d34a4efe524c903cf66b0cfa5260d81f277b6ae668b647edf795df9ce5/grpcio_tools-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3399b5fd7b59bcffd59c6b9975a969d9f37a3c87f3e3d63c3a09c147907acb0d", size = 3662793, upload-time = "2026-03-30T08:51:11.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/40/cf2d9295a6bd593244ea703858f8fc2efd315046ca3ef7c6f9ebc5b810fa/grpcio_tools-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c6abc08d3485b2aac99bb58afcd31dc6cd4316ce36cf263ff09cb6df15f287f", size = 3329149, upload-time = "2026-03-30T08:51:13.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/1d/fc34b32167966df20d69429b71dfca83c48434b047a5ac4fd6cd91ca4eed/grpcio_tools-1.80.0-cp312-cp312-win32.whl", hash = "sha256:18c51e07652ac7386fcdbd11866f8d55a795de073337c12447b5805575339f74", size = 997519, upload-time = "2026-03-30T08:51:14.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/98/6d6563cdf51085b75f8ec24605c6f2ce84197571878ca8ab4af949c6be2d/grpcio_tools-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6fdd42d5bb18f0d903a067e2825be172deff70cf197164b6f65676cb506c9b", size = 1162407, upload-time = "2026-03-30T08:51:16.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d9/f7887a4805939e9a85d03744b66fc02575dc1df3c3e8b4d9ec000ee7a33d/grpcio_tools-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e7046837859bbfd10b01786056145480155c16b222c9e209215b68d3be13060e", size = 2550319, upload-time = "2026-03-30T08:51:19.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5a/c8a05b32bd7203f1b9f4c0151090a2d6179d6c97692d32f2066dc29c67a6/grpcio_tools-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a447f28958a8fe84ff0d9d3d9473868feb27ee4a9c9c805e66f5b670121cec59", size = 5709681, upload-time = "2026-03-30T08:51:21.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/6b/794350ed645c12c310008f97068f6a6fd927150b0d0d08aad1d909e880b1/grpcio_tools-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:75f00450e08fe648ad8a1eeb25bc52219679d54cdd02f04dfdddc747309d83f6", size = 2596820, upload-time = "2026-03-30T08:51:24.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b2/b39e7b79f7c878135e0784a53cd7260ee77260c8c7f2c9e46bca8e05d017/grpcio_tools-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3db830eaff1f2c2797328f2fa86c9dcdbd7d81af573a68db81e27afa2182a611", size = 2909193, upload-time = "2026-03-30T08:51:27.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f3/abe089b058f87f9910c9a458409505cbeb0b3e1c2d993a79721d02ee6a32/grpcio_tools-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7982b5fe42f012686b667dda12916884de95c4b1c65ff64371fb7232a1474b23", size = 2660197, upload-time = "2026-03-30T08:51:29.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/c3/3f7806ad8b731d8a89fe3c6ed496473abd1ef4c9c42c9e9a8836ce96e377/grpcio_tools-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6451b3f4eb52d12c7f32d04bf8e0185f80521f3f088ad04b8d222b3a4819c71e", size = 3113144, upload-time = "2026-03-30T08:51:31.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f5/415ef205e0b7e75d2a2005df6120145c4f02fda28d7b3715b55d924fe1a4/grpcio_tools-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:258bc30654a9a2236be4ca8e2ad443e2ac6db7c8cc20454d34cce60265922726", size = 3661897, upload-time = "2026-03-30T08:51:34.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d3/2ad54764c2a9547080dd8518f4a4dc7899c7e6e747a1b1de542ce6a12066/grpcio_tools-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:865a2b8e6334c838976ab02a322cbd55c863d2eaf3c1e1a0255883c63996772a", size = 3328786, upload-time = "2026-03-30T08:51:37.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/63/23ab7db01f9630ab4f3742a2fc9fbff38b0cfc30c976114f913950664a75/grpcio_tools-1.80.0-cp313-cp313-win32.whl", hash = "sha256:f760ac1722f33e774814c37b6aa0444143f612e85088ead7447a0e9cd306a1f1", size = 997087, upload-time = "2026-03-30T08:51:39.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/af/b1c1c4423fb49cb7c8e9d2c02196b038c44160b7028b425466743c6c81fa/grpcio_tools-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:7843b9ac6ff8ca508424d0dd968bd9a1a4559967e4a290f26be5bd6f04af2234", size = 1162167, upload-time = "2026-03-30T08:51:41.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/44/7beeee2348f9f412804f5bf80b7d13b81d522bf926a338ae3da46b2213b7/grpcio_tools-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:12f950470449dbeec78317dbc090add7a00eb6ca812af7b0538ab7441e0a42c3", size = 2550303, upload-time = "2026-03-30T08:51:44.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/aa/f77dd85409a1855f8c6319ffc69d81e8c3ffe122ee3a7136653e1991d8b6/grpcio_tools-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d3f9a376a29c9adf62bb56f7ff5bc81eb4abeaf53d1e7dde5015564832901a51", size = 5709778, upload-time = "2026-03-30T08:51:47.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/7c/ab7af4883ebdfdc228b853de89fed409703955e8d47285b321a5794856bd/grpcio_tools-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ba1ffbf2cff71533615e2c5a138ed5569611eec9ae7f9c67b8898e127b54ac0", size = 2597928, upload-time = "2026-03-30T08:51:49.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e8/4381a963d472e3ab6690ba067ed2b1f1abf8518b10f402678bd2dcb79a54/grpcio_tools-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:13f60f8d9397c514c6745a967d22b5c8c698347e88deebca1ff2e1b94555e450", size = 2909333, upload-time = "2026-03-30T08:51:52.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cb/356b5fdf79dd99455b425fb16302fe60995554ceb721afbf3cf770a19208/grpcio_tools-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:88d77bad5dd3cd5e6f952c4ecdd0ee33e0c02ecfc2e4b0cbee3391ac19e0a431", size = 2660217, upload-time = "2026-03-30T08:51:55.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/d7/1752018cc2c36b2c5612051379e2e5f59f2dbe612de23e817d2f066a9487/grpcio_tools-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:017945c3e98a4ed1c4e21399781b4137fc08dfc1f802c8ace2e64ef52d32b142", size = 3113896, upload-time = "2026-03-30T08:51:57.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/17/695bbe454f70df35c03e22b48c5314683b913d3e6ed35ec90d065418c1ab/grpcio_tools-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a33e265d4db803495007a6c623eafb0f6b9bb123ff4a0af89e44567dad809b88", size = 3661950, upload-time = "2026-03-30T08:51:59.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d0/533d87629ec823c02c9169ee20228f734c264b209dcdf55268b5a14cde0a/grpcio_tools-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c129da370c5f85f569be2e545317dda786a60dd51d7deea29b03b0c05f6aac3", size = 3328755, upload-time = "2026-03-30T08:52:02.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/a1/504d7838770c73a9761e8a8ff4869dba1146b44f297ff0ac6641481942d3/grpcio_tools-1.80.0-cp314-cp314-win32.whl", hash = "sha256:25742de5958ae4325249a37e724e7c0e5120f8e302a24a977ebd1737b48a5e97", size = 1019620, upload-time = "2026-03-30T08:52:05.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/75/8b7cd281c5cdfb4ca2c308f7e9b2799bab2be6e7a9e9212ea5a82e2aecd4/grpcio_tools-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:bbf8eeef78fda1966f732f79c1c802fadd5cfd203d845d2af4d314d18569069c", size = 1194210, upload-time = "2026-03-30T08:52:08.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/90/5faa8b26e03495e5117f93bef8293cbada4af136362745dad7d1813ef0b0/grpcio_tools-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7", size = 2586071, upload-time = "2026-06-11T12:50:28.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/9a/85dc589fa6ae2439451eaa81a1578de31e29c676980d38bef7549b8a1f45/grpcio_tools-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126", size = 5813299, upload-time = "2026-06-11T12:50:31.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/fd/c53994e58a837e6eefe48f53eb3492afc04f2b8af255df4adb37d14378f8/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b", size = 2634668, upload-time = "2026-06-11T12:50:33.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/32/de988e86688686a2117e7ce6ce9eff4f638c929bb55b0afe60d6fbd2e45c/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290", size = 2957930, upload-time = "2026-06-11T12:50:36.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/97/3f18a0ea32b5f809d21961dbd0bc382b589a4c3d501e3d67c345d5456ed3/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90", size = 2697760, upload-time = "2026-06-11T12:50:39.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/c0/dbf5cbc877290ff7504a59959a8af4fdcfdaa1e84237948405ccf1aa82a6/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9", size = 3151456, upload-time = "2026-06-11T12:50:41.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/ea/16fe2dc83140a59e5c0a0b9dc2693dd36bfaa6bd835724b4ec66a68eab7b/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd", size = 3710469, upload-time = "2026-06-11T12:50:44.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/7d/df987d7d81e7ad2f7516d9e9d56ff29c54dbc6d8587e425688dca9a28e49/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c", size = 3370488, upload-time = "2026-06-11T12:50:47.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c5/5a63444d694ea47bf670138208f71830cc1759c402c8818092b28ab2dc5f/grpcio_tools-1.81.1-cp313-cp313-win32.whl", hash = "sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe", size = 1008229, upload-time = "2026-06-11T12:50:49.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/75/3945e26d5c94ae6ed9be5caef73d4d66c47dc8cfdd7b4995efaf942754e0/grpcio_tools-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10", size = 1174523, upload-time = "2026-06-11T12:50:51.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/08/e581ad42ae517a61172285047e4d710e2ac75f2f1915f7c91f284254e6d5/grpcio_tools-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c", size = 2585944, upload-time = "2026-06-11T12:50:54.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/c8/200d90ebad685af7eea5ff7e0360c504dd01ec053fe0f1f9c4abe3ea2d5a/grpcio_tools-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828", size = 5813492, upload-time = "2026-06-11T12:50:57.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c0/60da2a1af37aa8eb47308cec24d9f7709a8976fdec3a53fd35b56b358326/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555", size = 2634991, upload-time = "2026-06-11T12:50:59.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/7f/dede28b579ae9bf9079ba1aa913e8088d1dc0cdbe21c85caa22f0790cad2/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876", size = 2957913, upload-time = "2026-06-11T12:51:02.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/38/4de2118adb58ec7ffba65ec623b5836db769665c192517cbf187db3f6145/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c", size = 2697709, upload-time = "2026-06-11T12:51:05.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e1/762ced51059e4f694fd337ecae491581d42a4e61dcb0415d8c5c60e6ddcb/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97", size = 3151884, upload-time = "2026-06-11T12:51:07.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d8/9823090dc801e7229944874e7429c3b98e741ac778d8dc373f60240e1c43/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25", size = 3710404, upload-time = "2026-06-11T12:51:10.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/4e/4eae98d02148cb6f9f452f09942afba407afa6851e6c1fddc5ae9ec0b4ed/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a", size = 3370525, upload-time = "2026-06-11T12:51:12.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/3e/2206e597a128da6a03a6106d2eaf2c3e72c7d80843d4be933e3a3d10d02a/grpcio_tools-1.81.1-cp314-cp314-win32.whl", hash = "sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539", size = 1030582, upload-time = "2026-06-11T12:51:14.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/f2/bbeef86c687225b7bbc7c0acdfbd25c8bcaa3f5b1c941db053e5c3d9e859/grpcio_tools-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af", size = 1207490, upload-time = "2026-06-11T12:51:17.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2064,16 +2064,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-anthropic"
|
||||
version = "1.4.1"
|
||||
version = "1.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/19/55e0d3548a4d85ccee630fcfc0979af54ff4ea4f39ab0ed1ed3c6bf25f4e/langchain_anthropic-1.4.1.tar.gz", hash = "sha256:e17d027091438620e35ff2f06aefdfd63c8dcdf6abc606009ddfe3c764f2bc2e", size = 676043, upload-time = "2026-04-17T14:26:17.31Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/f5/cd397b94aeed5fa0e8ab9595b9fb578ac99f424d42220defe6626e6a1a7b/langchain_anthropic-1.4.6.tar.gz", hash = "sha256:78942d4458d883b7d362438a095ed501ed84f44d402622404482481fc973b9da", size = 706540, upload-time = "2026-06-12T16:54:15.352Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/0a/20625dfea38a26e8b43654e18cafbc3595e7dc223da80f23d4fa8451dc1d/langchain_anthropic-1.4.1-py3-none-any.whl", hash = "sha256:5a48afbb2b1bad9c46badaccc8e23b0dd7ae07b7583f76bac21ddb3dac831efd", size = 49020, upload-time = "2026-04-17T14:26:15.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/af/927dbbc5a1f5fea1a69adc2883f034cbd1430004e36f4eacd302d500393a/langchain_anthropic-1.4.6-py3-none-any.whl", hash = "sha256:dbd412a956b6b8b0716d9d8460ef71f834a6731cdbfc59e6160482a4a9fb5200", size = 51797, upload-time = "2026-06-12T16:54:14.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2212,7 +2212,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.2.9"
|
||||
version = "1.2.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@ -2222,16 +2222,17 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "xxhash" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/4b/0d1130e26b41a99dcc88353bbe7162a1f255c4db746bd94024268e6af27b/langgraph-1.2.9.tar.gz", hash = "sha256:385f87bc1802c35af7e0aa479278ecba8582d103515eb48256cb2ddcd42d0bd4", size = 722869, upload-time = "2026-07-10T01:30:14.985Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/3f/6e60789eab2c5ceaf34ba2c626ac345df93b026bbe7385018aa4b9c30397/langgraph-1.2.14.tar.gz", hash = "sha256:5b1a72b7a7f994ac6947b7751fbbe00e949f8bb44d9ab78881604b703247ae55", size = 752793, upload-time = "2026-10-06T14:41:19.789Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/16/0b8dc48823f1326f3e0c8012a3c07a40da6f194299e2ec080df236287baf/langgraph-1.2.9-py3-none-any.whl", hash = "sha256:c2d98ad94333937922ba04148641c1da2bfe45b5b8e55d7b6dcb0bb2df809e76", size = 247473, upload-time = "2026-07-10T01:30:13.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4a/f976df0d3cf7fb7986dc39f656c6b7b5655834430e9bdfaef1e1e4fffaba/langgraph-1.2.14-py3-none-any.whl", hash = "sha256:e3f5f8ff4259f9281b73ff50fc9ef5cc3b025b8936b8283ecdca7cc0c4128690", size = 255980, upload-time = "2026-10-06T14:41:18.129Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.10.0"
|
||||
version = "0.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "cloudpickle" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "grpcio" },
|
||||
@ -2249,8 +2250,10 @@ dependencies = [
|
||||
{ name = "langsmith", extra = ["otel"] },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
||||
{ name = "opentelemetry-exporter-prometheus" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "orjson" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "sse-starlette" },
|
||||
@ -2265,9 +2268,9 @@ dependencies = [
|
||||
{ name = "websockets" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/17/a23ecdad3de7e48209bc8d534312ba83e332126ec60da9e6487461b02597/langgraph_api-0.10.0.tar.gz", hash = "sha256:f594a160857e2cd7fb2352bba585111177a4a394625ad0cf5898814754fb7bc1", size = 713903, upload-time = "2026-06-11T14:36:50.469Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/1a/d9a8047905677d9740d83f4ee3dbf5880e054740b229b5d2962f1c11c034/langgraph_api-0.15.0.tar.gz", hash = "sha256:2cea18a0fdb26636382bdb6968f0a9d269f21e13fc4f741195d9cd4c17bbb759", size = 825569, upload-time = "2026-09-25T14:49:35.557Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/38/6b0cdcaaed9b3105102b452a5a70fb08894a9511458cf00c061c4725347c/langgraph_api-0.10.0-py3-none-any.whl", hash = "sha256:f4b545bf1936c4e90bed1cc07a6852b1be37e1667007ab4736d800cc18e9f9b3", size = 577475, upload-time = "2026-06-11T14:36:48.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/04/5829323d4fe69ec79ae196788ae533f830424c93018a092c67ad3eb009e1/langgraph_api-0.15.0-py3-none-any.whl", hash = "sha256:5d923b0417a1e8df817e6a308492a9a0941bb99543f0ef131965cf6c6e872172", size = 637444, upload-time = "2026-09-25T14:49:34.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2343,7 +2346,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-runtime-inmem"
|
||||
version = "0.30.0"
|
||||
version = "0.35.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blockbuster" },
|
||||
@ -2354,14 +2357,14 @@ dependencies = [
|
||||
{ name = "starlette" },
|
||||
{ name = "structlog" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/19/5d330e22e3ee2569742d3577b4ddfbb6b8e383f2edd9724b4a4b73735717/langgraph_runtime_inmem-0.30.0.tar.gz", hash = "sha256:31954e1de6cd43c284b425243d4faeae818f68b3daca4dbe4313e651b6f5e11d", size = 129940, upload-time = "2026-06-11T14:22:46.985Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/b9/717000dc56ae3419807fc26fe0ec5af105bd779ffecf24091621517bfca8/langgraph_runtime_inmem-0.35.10.tar.gz", hash = "sha256:09032079d65fce5ba5ec686471e97bfaa459e5a5834b2abe627b09c2db6af286", size = 133751, upload-time = "2026-10-27T19:35:04.989Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/df/cfb15c70fa4dcab9ea48f8fda9f4ed87d1767eea6e4d2901dc704da297ba/langgraph_runtime_inmem-0.30.0-py3-none-any.whl", hash = "sha256:08a36b83b5e32fe6e30cb25062a1e671a42e2a0206e4043cc95f41198dcbbb12", size = 51762, upload-time = "2026-06-11T14:22:46.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/c5/004c79b1181589cab3f9a44178f02f14b10ad62420caf5aea02816bbe9db/langgraph_runtime_inmem-0.35.10-py3-none-any.whl", hash = "sha256:568526bbbc149438b8bb37d6472b5f81d7b84543fdf52390617c0514b17db2b2", size = 52997, upload-time = "2026-10-27T19:35:03.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.4.4"
|
||||
version = "0.4.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@ -2370,9 +2373,9 @@ dependencies = [
|
||||
{ name = "orjson" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/33/dd9cd2218197debafee7b305a975355afa1c14278200019122d11717ce96/langgraph_sdk-0.4.10.tar.gz", hash = "sha256:d8d7afd326b4df42ddc0878b7fbf53f03c3237c664c236bf5bac4ab16fc38ed9", size = 360685, upload-time = "2026-10-26T19:59:45.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/0c/c14a178c018fffdb818a7e098cb28d6ecead53a07e161a03c083003ff137/langgraph_sdk-0.4.10-py3-none-any.whl", hash = "sha256:306ede340814e79332812e3b5a8bda2587a09657c95e14e5c7af3370e06c22fe", size = 167854, upload-time = "2026-10-26T19:59:44.075Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2796,101 +2799,140 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.1"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/0d/4b5afb6d3e545c9af0cdfe2db8f6f4c6664568c23d863d888674e447e6a4/multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe", size = 98360, upload-time = "2026-09-21T17:55:11.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/ab/1b9ca66251899981b21138b87da9d5a9c2c81af12b1ea7d19466972f7fe2/multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006", size = 59979, upload-time = "2026-09-21T17:55:12.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/eb/6ae44062466c26c8469ef43f2481a6a48d8cea0587b2d54514ec92e2adfd/multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42", size = 57736, upload-time = "2026-09-21T17:55:14.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/5c/a67817593019257a4ac8b0d1b4c426030e637c047b0692ef405439ecea7a/multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5", size = 333097, upload-time = "2026-09-21T17:55:15.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/bf/599ae2e6222822d88a247a8a7ae82fe6fd25d5700757b79603d5edafe6a0/multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea", size = 334196, upload-time = "2026-09-21T17:55:17.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/10/d8aac5acacbe7f5c117866c776ec26d5f15a868759b6f37ad8e7ed3b5b02/multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24", size = 317465, upload-time = "2026-09-21T17:55:18.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0a/714f796f7293a8b1c5c3f465a26996231d5450c54e85d64ef1258c091134/multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24", size = 345914, upload-time = "2026-09-21T17:55:20.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b1/e37fbf769c567be277bcf32df6234035a4384677fcc1bd852752be3d6b93/multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a", size = 351163, upload-time = "2026-09-21T17:55:22.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/5b/db08419c1e1f7c9d60cfd2787b2b517d7ae4ebbda8281b48a33eb5141467/multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d", size = 336881, upload-time = "2026-09-21T17:55:23.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/07/cc9bc8a62651d2d53ab93ee4993b3a71b7cb78eb8ebfc9c757a5b6698617/multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805", size = 303405, upload-time = "2026-09-21T17:55:25.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/0c/8e912afafa70e944dbb8bec4b66ca6e008511395278c0d3dd0e89567536a/multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02", size = 324265, upload-time = "2026-09-21T17:55:26.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/6a/62c2af80fb085e6234805017af857b8e913dfac7a55e9c1349c27768c58a/multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8", size = 322085, upload-time = "2026-09-21T17:55:28.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/6b/35bf801b336fd960811207203ffdcdc24acc3558b3e7ff2e1c914b141e70/multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580", size = 338107, upload-time = "2026-09-21T17:55:30.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/41/495ef65bf5bba29d142d81b3fe1b8154b919bed70e96491b1e93c3c26f0f/multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004", size = 339631, upload-time = "2026-09-21T17:55:32.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bd/057fdff5f4e04dcd40a960e38f77d19d3c4b67dd243ffa5f43718edb29fc/multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60", size = 302080, upload-time = "2026-09-21T17:55:33.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/de/9ace933ee8dad808632523726f42255b09600087219e3d4ead7369820910/multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a", size = 341107, upload-time = "2026-09-21T17:55:35.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/ac/7c1204406097bfc5c283d4a3287d61166807d189917567df4c1318484fc4/multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6", size = 333482, upload-time = "2026-09-21T17:55:37.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c0/a70c32ea3299ebe00f44533740cb46905717c51faf29bbd3ce8bb5886d9d/multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb", size = 52548, upload-time = "2026-09-21T17:55:38.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/2e/8c9c2591df01e5692ad1bf92febb082a17b1c6c3a19aa8b7ff49988fd989/multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203", size = 59622, upload-time = "2026-09-21T17:55:39.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/95/59f4472ec512bc180fd207899594c7803ece96262ff9aaa3a4b6d7da6940/multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc", size = 54930, upload-time = "2026-09-21T17:55:41.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/45/ddf7c76860f5f553a23ad3ca38463ccf5247081618977742c8dc4625355e/multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3", size = 63266, upload-time = "2026-09-21T17:55:42.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/d3/f4ae5945ea2de597eaeb4eca3a74c59783ace54482c9b49435442b63efa8/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382", size = 55771, upload-time = "2026-09-21T17:55:44.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/7d/15468239920040d01c686e5ae669e6382f7bf31fc3e5e0a6b8f1c3b32d3b/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a", size = 57116, upload-time = "2026-09-21T17:55:45.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/1b/b958f06aac2d8b1e485eb1c105b1b159cbf3868249e5142142451577dad2/multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40", size = 97692, upload-time = "2026-09-21T17:55:46.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/6c/d6cfe18e61010166d7237d7527c775eb9843e7078feadea45b7628751b60/multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8", size = 59556, upload-time = "2026-09-21T17:55:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/46/9b4c1127cece0289fb207d04aae5116382ddfcb2a4dc8e0cb49a33f3c7b3/multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c", size = 57357, upload-time = "2026-09-21T17:55:49.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/fc/c18b07100a6064573e49e538d13d47eb2b5221f48080512df78aef54ab04/multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a", size = 330433, upload-time = "2026-09-21T17:55:51.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ff/52a0082adeb69656b634609d4fb0ae65456deaae4cca3d5a12656626dc50/multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8", size = 332487, upload-time = "2026-09-21T17:55:52.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/3e/80bd4729635a7af371ff4dcc312594cf96e73f98854491be8740792de430/multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94", size = 316911, upload-time = "2026-09-21T17:55:54.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/f4/7ea4a907e053e924e2e1694d90560f79f72056504bfd5e8483695f9527d2/multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa", size = 344950, upload-time = "2026-09-21T17:55:56.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/16/9642ae41fbfbdc546aa481c2b07ae1bc087a94ab5f5020b4d9ab77da401f/multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5", size = 350264, upload-time = "2026-09-21T17:55:58.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/f2/e06c8e42074d0a4b8419bfe92afbd1d262190b69549ecbcd2dffa4f103c2/multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c", size = 336213, upload-time = "2026-09-21T17:55:59.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/43/d7cf9ef4700e7c25244590d1b4f20cf12cbed86beba6c17be4d9e48c1099/multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b", size = 302555, upload-time = "2026-09-21T17:56:01.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/3a/54209920324bc3928f5f2ba28b01a6dd957a9d48c3aaff77e38faaf5668d/multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05", size = 322247, upload-time = "2026-09-21T17:56:03.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/50/df96f961b178b621ab0adbf9221d4fd21534e1064fd9f5e85c1fa27fea55/multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a", size = 321787, upload-time = "2026-09-21T17:56:04.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/9b/37f354562a8f82f9c1f63d3a94300fc82126d50320175f0702bbd544f87c/multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418", size = 335498, upload-time = "2026-09-21T17:56:06.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/44/78e366efc6c004185295cc9eb9711c10f13eadf3b547603b9c5526329976/multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1", size = 338244, upload-time = "2026-09-21T17:56:09.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/2b/ab5bd3964691abe4d14b43bdbdb8a621b77cea2ca352dc93159ec0a4575b/multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721", size = 301641, upload-time = "2026-09-21T17:56:11.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/a8/f6bc899f5aafaba755edc8e4bb934bc00b1e405f8d3fbc1dd10283738074/multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771", size = 340162, upload-time = "2026-09-21T17:56:13.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/03/a8fc809ef364b8c231c065ea2ea2f97ae86d4f9a0e36b5721759fd606778/multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14", size = 332826, upload-time = "2026-09-21T17:56:15.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/27/32dde80245e2024e9bb7a6ca3c2fb7c695dc74014cba557cc35be6f91e24/multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40", size = 52378, upload-time = "2026-09-21T17:56:16.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/78/1bac2987edac273a6b27fa50e9204bc3466c94a7b4f6a44828d730f8810f/multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d", size = 59449, upload-time = "2026-09-21T17:56:18.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/32/2a77ce19eea48cb9b3521a202b513ba3349aafa702c8317be0b645ffb74b/multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4", size = 54713, upload-time = "2026-09-21T17:56:19.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/0f/c6041015aa2cdc11e1dff0cae58898ae5525d19e35efd65a4ecf3c6ea235/multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5", size = 62336, upload-time = "2026-09-21T17:56:21.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/f1/ba67f668478f152bdfa18abe72e7588bd078bf8b42c172fcf542fcf45826/multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841", size = 51715, upload-time = "2026-09-21T17:56:57.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/81/014bb2128aedc8157d2848f0ec8a0209c6b822fb88deea012834e1c4a4e8/multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3", size = 58621, upload-time = "2026-09-21T17:56:58.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/34/8f43c03c2d8821a2320b8c560d8a49d0be2920627edfac3f546922f91969/multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9", size = 53959, upload-time = "2026-09-21T17:57:00.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/f2/f0ff3384a756227741c72f4ee94cadf1f9900df0ae961e48639f96115b1c/multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd", size = 57776, upload-time = "2026-09-21T17:57:32.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/87/013e1eed30ad46bec1eb68947bc9caf55ba2fd0d6aed04fe964e3b44c23b/multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9", size = 64881, upload-time = "2026-09-21T17:57:34.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/95/64dd049bb4c53426e2fee7afb874daaa8b4426c41878785883a94511ecf8/multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3", size = 58582, upload-time = "2026-09-21T17:57:36.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2c/26bc1723592cfe965b575652a7749026474b0625b04b04564e1fba265dc5/multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1", size = 62339, upload-time = "2026-09-21T17:57:37.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/4f/5cbbe852b45d8a7f0ab3ed042386e4894e463010eb230603596cb15d0fee/multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d", size = 51714, upload-time = "2026-09-21T17:58:16.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/77/1fa118386deeae4e7a48870fa5cb073e748b0ac5265a6990651cefc48068/multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f", size = 58619, upload-time = "2026-09-21T17:58:18.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ee/51c45748acfce9cb69d89dcfcd09c874b385407fd6993e885d6905659865/multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f", size = 53969, upload-time = "2026-09-21T17:58:20.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/0c/ca62ae2882b89a4cfeb0bbca873a4afdb5fc32994c4f8a37a7cb52ede7a0/multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1", size = 57810, upload-time = "2026-09-21T17:58:57.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/8b/b2c2805c3eb6fa4cdf5512fb95ac6174dde71e34dd601c5be52f4df5dc82/multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec", size = 64984, upload-time = "2026-09-21T17:58:59.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/34/03f1d204d698c408f4754c969882609b6b7a902c0e3d16a7033f219cdeda/multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3", size = 58742, upload-time = "2026-09-21T17:59:01.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3077,80 +3119,63 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.45.1"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-http-transport"
|
||||
version = "0.66b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
requests = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-common"
|
||||
version = "0.66b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-sdk" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.45.1"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.45.1"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-http-transport", extra = ["requests"] },
|
||||
{ name = "opentelemetry-exporter-otlp-common" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common" },
|
||||
{ name = "opentelemetry-proto" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-prometheus"
|
||||
version = "0.63b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "prometheus-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/2a/dfeddff262b12eff0c72f4ad9e258aab8889f48c4dc1417a0377a13bc427/opentelemetry_exporter_prometheus-0.63b1.tar.gz", hash = "sha256:31902e22c89431058a95b6dcdb644f9309f226aa4872cc755f0a780d2895e97f", size = 15234, upload-time = "2026-05-21T16:32:57.797Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/ec/d7c7435e9000fb69837cf7753b7cbbbdeb5d0585203daf1b6ebf8fa93e02/opentelemetry_exporter_prometheus-0.63b1-py3-none-any.whl", hash = "sha256:0efd00aa6b1939345ddcc6de141b83ebffa2b4401a37a68f880e54217602701d", size = 12466, upload-time = "2026-05-21T16:32:36.622Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation"
|
||||
version = "0.66b1"
|
||||
version = "0.63b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
@ -3158,48 +3183,48 @@ dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/03/89e47ff8d52a4f83b343e6eb9ef1698ff45357216e5b6b2b21e0da5c5c7d/opentelemetry_instrumentation-0.66b1.tar.gz", hash = "sha256:e79a510f7d87c72d95e964ddb42193a0d9a75668c027d980eab032ea1322a5ce", size = 43308, upload-time = "2026-10-06T17:36:10.703Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b2/d1413681ff43e13ac9860df27e1226d3199ab0b97b352ceea41abcc660a5/opentelemetry_instrumentation-0.66b1-py3-none-any.whl", hash = "sha256:4c4aa14dc9a24a02325a9d4c42c4d0208dbb1374c2b1b8fe6c9392d59f3e1008", size = 36904, upload-time = "2026-10-06T17:35:11.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.45.1"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.45.1"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.66b1"
|
||||
version = "0.63b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3554,6 +3579,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/78/882563dc554c5f710b4e47c58978586c2528703435800a7a84e81b339d90/primp-1.2.3-cp314-cp314t-win_arm64.whl", hash = "sha256:6513dcb55e6b511c1ae383a3a0e887929b09d8d6b2c70e2b188b425c51471a02", size = 3874087, upload-time = "2026-04-20T08:33:58.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-client"
|
||||
version = "0.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
@ -4886,24 +4920,54 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid-utils"
|
||||
version = "0.14.1"
|
||||
version = "0.19.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/0f/cd9b3e495e514441e9af3a42c5c63436aee620f61412cafcce67e259228b/uuid_utils-0.19.0.tar.gz", hash = "sha256:4b2996bb9d12da9b62f6f4c64a066b26926fa75dbe47b4f8e309e24e187abf1d", size = 43343, upload-time = "2026-10-26T10:55:23.821Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ce/78f9d79c820b8afc9b0498b0a5f5ef87ea970a19cc382127087f36cd4839/uuid_utils-0.19.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:84929d8c594381c6f1444afa2a25cce9ff7edac99d35e071ebc48275bae83730", size = 574701, upload-time = "2026-10-26T10:54:06.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/63/fe81dda73d651fb6bf8b0e21c4ea34209fc535ca109634c0d93f14ac21dc/uuid_utils-0.19.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a416c78167b750186095a0ea950d95b8b4e157e3172fc6ccdc317b6ca2896580", size = 294226, upload-time = "2026-10-26T10:54:08.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/6c/118c7e11fb579b5631212bfbbb31542a1778936dfb7c55591aa9b52c7c99/uuid_utils-0.19.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82573c0e5090732c0ae2e18626e06cb35fe7cfcf6fe9534d4c1638af41d25d36", size = 327869, upload-time = "2026-10-26T10:54:10.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/76/80e9bad735cbf92f127183ee5c44ee0d83604e56197bddcaaaed2c66f7e6/uuid_utils-0.19.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c78d527fddf5495e80032caeca2f140731ef394298ef4cbecaaf51aa2c257e1e", size = 334737, upload-time = "2026-10-26T10:54:11.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/7b/c8eb8e45569e8681f20e381859248d18635d823b8aedc70de8a0bd5faca6/uuid_utils-0.19.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c33b5c62cf82d509945c770201b1656448c2560e12931d1a8d698a683e9125d", size = 440438, upload-time = "2026-10-26T10:54:12.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/45/8238eac588b668e5c500cb187fd74a9f6d984c09f3f3f2fa4161d3c07eb7/uuid_utils-0.19.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8060530a8fa27f11947489c79bc36e6ebb465b2d714945364dc397283309417", size = 330342, upload-time = "2026-10-26T10:54:14.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/45/8215129786e67842d479480a44066cac8a7086f345ac9707f629ed777cfe/uuid_utils-0.19.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96429c78eab28ac71b2121fb47c66783bfe5d06632b5bd5200004707fdc0faad", size = 354275, upload-time = "2026-10-26T10:54:15.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/bc/a673bfd116eb26419dd82bffa9bc2bc1e915af02bf68ac8c09598dc678ec/uuid_utils-0.19.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80e0b37b3ddf60c50c51b869e1b02df61415941aa68bc903f61562fd8df04b18", size = 504446, upload-time = "2026-10-26T10:54:17.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/ff/61867210f202c603d4a5ca1c5b3c4c76e8d8724eeebd7cf1307d18b63a9a/uuid_utils-0.19.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f629f2a4f95e46d99ee673536ab9706899696cdc6686e93314ea756d3b394ce4", size = 609016, upload-time = "2026-10-26T10:54:18.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/81/6fb8a2ee8010a6a0c5cb29f5efed2604717613cc9888f9e5c999dc36b84b/uuid_utils-0.19.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:ec330297c09e40b82d613326b57bb381d68535e8a341fa2e501b46791cbdfeec", size = 570293, upload-time = "2026-10-26T10:54:20.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/2f/e1c749b65c584ebd8bc5379cf18e26c67cdff8f2bd9f8f77111a47083408/uuid_utils-0.19.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:313de97d59dbbb09f4598cece2c799cc233d8673bb075ad7eff442cd485623c2", size = 535321, upload-time = "2026-10-26T10:54:21.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ba/f423f68fd784124e68ab1b1afe27ab4591ffd5363b39eccfcbaf8f3b0ff3/uuid_utils-0.19.0-cp310-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:5c47c7fc35e91a801996477f0d746d506a2e5b47a13accbf41dc29844786c40c", size = 103141, upload-time = "2026-10-26T10:54:23.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/9a/7a0a0dad166db9f4437f3d259e47ce74d552ccc50d8aa63d05b2510e3096/uuid_utils-0.19.0-cp310-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:23868cdc4dfa71021fec6cf43fbb83797345362ac1c866b50dbf17f479587413", size = 103069, upload-time = "2026-10-26T10:54:24.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/3c/c5fef96cea027de66a035b4173951fc29c2e83ed49a5d5b3fa7b632f95b5/uuid_utils-0.19.0-cp310-abi3-win32.whl", hash = "sha256:0cff166cf6348e9a705740e6575ce7bc727554a71a6add60b9ceceaaf169002a", size = 175479, upload-time = "2026-10-26T10:54:25.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/16/edfb8edc8a4624cfaf1387a57bf37cd53e5d7f213ebeb8dd6fbd2f6b2c64/uuid_utils-0.19.0-cp310-abi3-win_amd64.whl", hash = "sha256:b763ae8f23334ccd95d0b81fb19ee418f38f9e80ebfb521f313a9745280bc07c", size = 182891, upload-time = "2026-10-26T10:54:27.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/17/258e5dd380bc594731b2b705082360fb4cef1e5cbaab46ef307439a6f415/uuid_utils-0.19.0-cp310-abi3-win_arm64.whl", hash = "sha256:b244ea25bfe1b34e40fabf8f079d59a9f153b22bd4095842065bb0a9dfaa477b", size = 180849, upload-time = "2026-10-26T10:54:28.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/64/b5554f686d5395db970bc07944b9c59c26ab0e192a27d7c8b555e5fc6332/uuid_utils-0.19.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9adf8d69574f0018a25aed0cc194bffacc05fb91b922fdd8e794109c45ad80ed", size = 565592, upload-time = "2026-10-26T10:54:30.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/a3/d8d0b7bf4892c41dbbb0e81a356d20aad781cc28d3029669e167eb72d9a1/uuid_utils-0.19.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ac37f30acc7fe01266c73f19a686d0ea1fbd22495d81205cc3b572e2fb2293a6", size = 292083, upload-time = "2026-10-26T10:54:32.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/c1/770ccb569b4c15ee81c36bc40e74f03c5e0a2ee71ecc534d3fe7cfa5696d/uuid_utils-0.19.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afc1dc05c83163ec58304350ee61d20f1f3285d0e8fb873e4d85a97988c284ad", size = 323275, upload-time = "2026-10-26T10:54:33.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/12/798e09469358bf59848534291aa62d16cc6fbd66767f469c5d8278d0bc4d/uuid_utils-0.19.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16e68b1a4b81301fa7ba52d500947094226d8b0c1521695746ec57ff0779ff6f", size = 328681, upload-time = "2026-10-26T10:54:35.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/16/71df642a7969381747de36d4e365e59683cdaa2c425da0530e9383e4de86/uuid_utils-0.19.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833edd2b38ef08a324e9f3aed4269d2ab955d4cb66417422eeb3af28a6237427", size = 434504, upload-time = "2026-10-26T10:54:36.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/18/35c86d901bbd6d3f088217f37a6ff01aae4ce8f8cdb3f9ddb5220887a122/uuid_utils-0.19.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21356ba99b82c813c1945054a09ff7ebc1bf55b858b41a6ae084e6ed2c8bb9a9", size = 325810, upload-time = "2026-10-26T10:54:38.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/51/25427eee1031946d5abe0d8985123f6fd4a1931c8a5a8660b1c76368ca12/uuid_utils-0.19.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2477161656105e787a5f986adb63191b61f129c9d7399b2bce406f87a1407b4d", size = 346300, upload-time = "2026-10-26T10:54:39.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/4b/edbb0ebdc948549a0250363f441883c31fef42fb4e4bb607b9a083efaeba/uuid_utils-0.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ddf61d8443993d683c8d6fcd05cbb711c2655042952850fc0cf25af969e3a213", size = 501365, upload-time = "2026-10-26T10:54:41.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/e4/f5a788fae45e315d8d04d5889a0b0809543e27c8d08b9fe10b09d0766b9b/uuid_utils-0.19.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cb9ead0aeae67ad2de3f660f2e978ece77e14ad3f2a7082b768e2c75866664a2", size = 604290, upload-time = "2026-10-26T10:54:42.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/90/3800a0b2083d6c156a4d686e75f26c055a48fcf315b9310ba211b7e3659c/uuid_utils-0.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e5155902b7779ae7b692ac69e6031f41481d62fa85a530d3725c270799940a0", size = 563566, upload-time = "2026-10-26T10:54:44.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/37/1b58a17a041bd560a1d652afccd57ecd5d0e0c4d6280deded663b783de43/uuid_utils-0.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd3bcc556e249bb70b1c7fd78e659a1ebfb5572e7ff2091d096aecbac62edb1c", size = 531143, upload-time = "2026-10-26T10:54:45.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ac/f6286bcfad2f321be2acd04961934a58e4a6c15ba78070a13d193cbbb9f0/uuid_utils-0.19.0-cp314-cp314t-win32.whl", hash = "sha256:9b9064d96c6fe5e43ff4486722c5c891083a7fa91ee36311286cfd833b4b5562", size = 172229, upload-time = "2026-10-26T10:54:46.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/45/0df876abf12875ef82bcd8f034c0c7adc1507dd721a5942232120b6267e8/uuid_utils-0.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:eeaf6b77ed2ec9815988e10d043d1533a1ae8c6535e7fb5479fd7e2e6ad25dff", size = 178436, upload-time = "2026-10-26T10:54:48.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ae/c9b4aaa8ee95581b444a7be681dc0ceb57a465b50b28b2f2517f5fec1c51/uuid_utils-0.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ebb011b9010ddbd8acd4650896e87db2715f2cfc70fb194c03ebcbf4150d3844", size = 175725, upload-time = "2026-10-26T10:54:49.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/6e/3a3fdaa9689b687f105ba885e61f6b2e8336ec4d209fd51d41923ad83479/uuid_utils-0.19.0-cp315-cp315t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8c76f01a815a0ca8b698202f2f5e40098a4b3cd56305e1fc9ed3d959ac718ff0", size = 565325, upload-time = "2026-10-26T10:54:50.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/1a/23dacf86dfe52f4606694501a6072379156025a3c6988e16f477f627249c/uuid_utils-0.19.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:f3828e1fc0831a9d539e702992e9fcfeb400e80f737467b8e557921cdc8ae067", size = 291930, upload-time = "2026-10-26T10:54:52.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f2/adf223ded569b0d75e8ac10db14be8fc253da900a63e678436bc3ff3b72a/uuid_utils-0.19.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25ddec2775c2f500a36029ab93af09f39d5c417786df02a464464e030152014", size = 322729, upload-time = "2026-10-26T10:54:54.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/df/0f78e7f22c778d2e48ef092a3a1c44d5b8b487b7e178e1044e7a39195b17/uuid_utils-0.19.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1fc412a5ef2f1e6f96d132a6656af59bae2dba9fd04f8df2a2f6eb913fe6180a", size = 328838, upload-time = "2026-10-26T10:54:55.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/96/9a9d6f7bef37b3545203eaf643e7f4d574f4ea3a6364af8865c071847df5/uuid_utils-0.19.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94cbef978d789b18014c477f51c8311800803cd2a0c7e1de6d2af85377210053", size = 433992, upload-time = "2026-10-26T10:54:56.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/92/62f67dc40a036ab890ac3f55eaab610999c713654bb3d0b56d5676ce3eaa/uuid_utils-0.19.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b012d2a2c5516e11ded4fb70cdd730f36e2d40b45018b9a40c6605dc5c0666", size = 326138, upload-time = "2026-10-26T10:54:58.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b7/740525de1aa1a150eb7a80a667c997d54284f407b7f3b38ce8f90f948813/uuid_utils-0.19.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4062c0dfd6697ce7c91a504d8ff6eb8dcaf4e790fb6adec69c4c83fd89870d4a", size = 346714, upload-time = "2026-10-26T10:55:00.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/1a/c9d2be770a2061da608164d022becda3fc7ddabb34ec7a1950d29ae51ca0/uuid_utils-0.19.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:361cfd0fba0a49f1c8b6df741a35f7d948f0fae3d3914f8622ba261dc4656a5a", size = 500974, upload-time = "2026-10-26T10:55:01.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/2e/ee7522207a7f9477c58757f040f05c4eedb68f362623d4e40da4983bdf5b/uuid_utils-0.19.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:a15c8943e02ac59992eaad1ce69793720dc711ce66188d386be1cbf8f5b5a2e4", size = 604607, upload-time = "2026-10-26T10:55:03.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b8/c9a1f21627238171a2021dd777177a211df20b12c0c5e45b828ee9bc3c84/uuid_utils-0.19.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:4bd00a485e973cb3bcefe06f13a663cf4ee0c54caeacb2a656a5f901d1505a03", size = 563749, upload-time = "2026-10-26T10:55:04.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/9e/ed7d14d4d7bdb94159909e722337a557c855e8990a7fdff55ef6b016fb38/uuid_utils-0.19.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:9fe68c4b2e017fb13c5e460b76b76c5cfa717ddd653cea184c2664bc2a852011", size = 530994, upload-time = "2026-10-26T10:55:06.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/92/5b84a87dd4127a14337d42eb4a24a764583e4decc2675a9949d4568b16e1/uuid_utils-0.19.0-cp315-cp315t-win32.whl", hash = "sha256:5397581f40e268535cd48230e4f2f372dc5f3e70ce79e0aff9cb5ceb838163c8", size = 172512, upload-time = "2026-10-26T10:55:08.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/a11a33e39f64c50f17483842652933b8ef769ce7cac3399ed7d2a2b928c2/uuid_utils-0.19.0-cp315-cp315t-win_amd64.whl", hash = "sha256:225752e3c36b7c861eb1bbceb5c9197c134c806b210ed53dcacf6339a3d599e6", size = 178626, upload-time = "2026-10-26T10:55:09.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/78/4f9dc891db194ce8107c9704d7e2e51da558af457e64134da1b1a7c5db1f/uuid_utils-0.19.0-cp315-cp315t-win_arm64.whl", hash = "sha256:bfcda2352c1500230227da5475139ab177bb2516309254cd74ade19a28cb842d", size = 175582, upload-time = "2026-10-26T10:55:11.001Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
# ============================================================================
|
||||
# Bump this number when the config schema changes.
|
||||
# Run `make config-upgrade` to merge new fields into your local config.yaml.
|
||||
config_version: 63
|
||||
config_version: 64
|
||||
|
||||
# Optional operator-owned literal prompt extensions (see CONFIGURATION.md):
|
||||
# lead_prompt_overlay:
|
||||
@ -2036,6 +2036,8 @@ subagent_runtime:
|
||||
# # token_budget: # per-agent override of the global token_budget above
|
||||
# # max_tokens: 3000000 # raise the ceiling for deep-research tasks
|
||||
# # model: qwen3:32b # Use a specific model (default: inherit from lead agent)
|
||||
# # thinking_enabled: true # Enable thinking/reasoning for this subagent (default: false)
|
||||
# # reasoning_effort: medium # Use an effort value or alias supported by this model
|
||||
# # skills: # Skill discovery/activation allowlist (default: all enabled)
|
||||
# # - web-search
|
||||
# # - data-analysis
|
||||
@ -2063,11 +2065,15 @@ subagent_runtime:
|
||||
# # - data-analysis
|
||||
# # - visualization
|
||||
# # model: inherit # 'inherit' uses parent's model
|
||||
# # thinking_enabled: true # Enable thinking/reasoning (default: false)
|
||||
# # reasoning_effort: medium # Use an effort value or alias supported by this model
|
||||
# # max_turns: 80
|
||||
# # timeout_seconds: 600
|
||||
#
|
||||
# # Model override: by default, subagents inherit the lead agent's model.
|
||||
# # Set `model` to use a different model (e.g., a local Ollama model for cost savings).
|
||||
# # Unset thinking requests false; the selected model's required-thinking policy
|
||||
# # and effort defaults/aliases still apply. Parent thinking settings are not inherited.
|
||||
# # The model name must match a name defined in the `models:` section above.
|
||||
|
||||
# Durable native-subagent batches. Disabled by default because enabling it can
|
||||
|
||||
@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: deer-flow
|
||||
description: DeerFlow — LangGraph-based AI super-agent (gateway + frontend + nginx + sandbox provisioner) deployed to Kubernetes.
|
||||
type: application
|
||||
version: 2.2.0-dev
|
||||
appVersion: "2.2.0-dev"
|
||||
version: 2.3.0-dev
|
||||
appVersion: "2.3.0-dev"
|
||||
keywords:
|
||||
- deerflow
|
||||
- langgraph
|
||||
|
||||
@ -135,7 +135,7 @@ they resolve from the `secrets` map):
|
||||
|
||||
```yaml
|
||||
config: |
|
||||
config_version: 63
|
||||
config_version: 64
|
||||
models:
|
||||
- name: gpt-4
|
||||
use: langchain_openai:ChatOpenAI
|
||||
|
||||
@ -367,7 +367,7 @@ ingress:
|
||||
# -- DeerFlow config.yaml content. Secrets MUST stay as $VAR references — never
|
||||
# inline literal secret values here. The default enables provisioner sandbox.
|
||||
config: |
|
||||
config_version: 63
|
||||
config_version: 64
|
||||
log_level: info
|
||||
recursion_limit: 100
|
||||
max_recursion_limit: 1000
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "deer-flow-frontend",
|
||||
"version": "2.2.0-dev",
|
||||
"version": "2.3.0-dev",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -29,7 +29,8 @@ Tests: `html-assets.dom.test.ts`, `artifact-file-preview.dom.test.tsx`,
|
||||
2. Stream events update thread state (messages, artifacts, todos, goal). The main thread stream uses the LangGraph SDK's `throttle: true` mode so updates received in the same macrotask coalesce before React is notified; do not replace it with a numeric delay without validating the SDK's trailing-debounce behavior on a continuous stream.
|
||||
File-tool artifact auto-open work must run in an effect with timer cleanup; never schedule timers while rendering streamed `write_file` or `str_replace` updates.
|
||||
`ThreadState.artifacts` remains the authoritative artifact list. The artifacts provider persists only thread-scoped panel UI state (`open`, selected path, and a refresh bootstrap cache) in session storage; an initial empty stream value must not overwrite that restored state before history finishes loading.
|
||||
Formal artifact content is refreshed once when the run finishes; transient `write-file:` previews remain message-driven.
|
||||
Refetch formal artifacts when runs finish; `write-file:` previews stay message-driven.
|
||||
On save, cancel reads before cache/draft writes; pass both hooks' signals via loader to fetch.
|
||||
UTF-8 outputs under `/mnt/user-data/outputs` are editable while idle. Root drafts survive SPA/home/showcase routes and unknown auth; confirmed account change/logout clears them. Owner/generation fences reject stale updates. Auth binds through a stable setter context, outside the typing state subscription. Drafts preview in Markdown/HTML and warn on unload off-route. SHA-256 conflicts preserve edits. Layout, draft-context and editor DOM tests cover this.
|
||||
Regular artifact text loads request at most the first 1 MiB through an HTTP
|
||||
byte range. A truncated preview must stay lightweight and expose an explicit
|
||||
|
||||
@ -330,6 +330,11 @@ export function ArtifactFileDetail({
|
||||
content: submittedDraftContent,
|
||||
expectedSha256: submittedBaselineSha256,
|
||||
});
|
||||
// A refetch started before or during the save can still return old bytes.
|
||||
// Cancel both preview and full reads before publishing the saved revision.
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["artifact", filepathFromProps, threadId, isMock],
|
||||
});
|
||||
setDrafts((current) => ({
|
||||
...current,
|
||||
[filepath]: (() => {
|
||||
|
||||
@ -34,12 +34,13 @@ export function useArtifactContent({
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ["artifact", filepath, threadId, isMock, fullContentRequested],
|
||||
queryFn: () => {
|
||||
queryFn: ({ signal }) => {
|
||||
return loadArtifactContent({
|
||||
filepath,
|
||||
threadId,
|
||||
isMock,
|
||||
full: fullContentRequested,
|
||||
signal,
|
||||
});
|
||||
},
|
||||
enabled,
|
||||
@ -103,12 +104,13 @@ export function useStandaloneArtifactContent({
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["artifact", filepath, threadId, isMock, fullContentRequested],
|
||||
queryFn: () =>
|
||||
queryFn: ({ signal }) =>
|
||||
loadArtifactContent({
|
||||
filepath,
|
||||
threadId,
|
||||
isMock,
|
||||
full: fullContentRequested,
|
||||
signal,
|
||||
}),
|
||||
staleTime: 0,
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
@ -52,11 +52,13 @@ export async function loadArtifactContent({
|
||||
threadId,
|
||||
isMock,
|
||||
full = false,
|
||||
signal,
|
||||
}: {
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
isMock?: boolean;
|
||||
full?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
let enhancedFilepath = filepath;
|
||||
if (filepath.endsWith(".skill")) {
|
||||
@ -65,6 +67,7 @@ export async function loadArtifactContent({
|
||||
const url = urlOfArtifact({ filepath: enhancedFilepath, threadId, isMock });
|
||||
const response = await fetch(url, {
|
||||
cache: "no-store",
|
||||
signal,
|
||||
headers: full
|
||||
? undefined
|
||||
: { Range: `bytes=0-${ARTIFACT_PREVIEW_MAX_BYTES - 1}` },
|
||||
|
||||
@ -9,6 +9,8 @@ export interface Subagent {
|
||||
disallowed_tools: string[] | null;
|
||||
skills: string[] | null;
|
||||
model: string;
|
||||
thinking_enabled?: boolean | null;
|
||||
reasoning_effort?: string | null;
|
||||
max_turns: number;
|
||||
timeout_seconds: number;
|
||||
enabled: boolean;
|
||||
@ -27,6 +29,8 @@ export interface CreateManagedSubagentRequest {
|
||||
disallowed_tools?: string[] | null;
|
||||
skills?: string[] | null;
|
||||
model?: string;
|
||||
thinking_enabled?: boolean | null;
|
||||
reasoning_effort?: string | null;
|
||||
max_turns?: number;
|
||||
timeout_seconds?: number;
|
||||
enabled?: boolean;
|
||||
|
||||
@ -0,0 +1,275 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
rs.mock("@/components/workspace/messages/context", () => ({
|
||||
useThread: () => ({
|
||||
thread: { isLoading: false, messages: [] },
|
||||
isMock: false,
|
||||
}),
|
||||
}));
|
||||
rs.mock("@/components/ui/sidebar", () => ({
|
||||
useSidebar: () => ({ setOpen: rs.fn() }),
|
||||
}));
|
||||
rs.mock("next/navigation", () => ({
|
||||
usePathname: () => "/workspace/chats/artifact-save-race",
|
||||
}));
|
||||
rs.mock("@/core/auth/AuthProvider", () => ({
|
||||
useAuth: () => ({ user: null }),
|
||||
}));
|
||||
rs.mock("@/core/config", () => ({
|
||||
getBackendBaseURL: () => "/backend",
|
||||
}));
|
||||
rs.mock("@/env", () => ({
|
||||
env: { NEXT_PUBLIC_STATIC_WEBSITE_ONLY: "false" },
|
||||
}));
|
||||
rs.mock("@/core/api/fetcher", () => ({ fetch: rs.fn() }));
|
||||
rs.mock("sonner", () => ({ toast: { success: rs.fn(), error: rs.fn() } }));
|
||||
|
||||
// Keep the production content hook, query client and draft providers. Only the
|
||||
// editor surface is simplified so the test can exercise actual edit/save events.
|
||||
rs.mock("@/components/workspace/code-editor", () => ({
|
||||
CodeEditor: ({
|
||||
value,
|
||||
readonly,
|
||||
disabled,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
}) => (
|
||||
<textarea
|
||||
aria-label="Artifact source"
|
||||
value={value}
|
||||
readOnly={readonly}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
import { ArtifactFileDetail } from "@/components/workspace/artifacts/artifact-file-detail";
|
||||
import { ArtifactsProvider } from "@/components/workspace/artifacts/context";
|
||||
import { fetch as apiFetch } from "@/core/api/fetcher";
|
||||
import { ArtifactDraftsProvider } from "@/core/artifacts/draft-context";
|
||||
import { I18nProvider } from "@/core/i18n/context";
|
||||
|
||||
const filepath = "/mnt/user-data/outputs/report.txt";
|
||||
const threadId = "artifact-save-race";
|
||||
const oldRevision = "a".repeat(64);
|
||||
const savedRevision = "b".repeat(64);
|
||||
const nextRevision = "c".repeat(64);
|
||||
const mockedFetch = rs.mocked(apiFetch);
|
||||
let queryClient: QueryClient;
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function textResponse(content: string, revision: string) {
|
||||
return new Response(content, {
|
||||
headers: { ETag: `"${revision}"` },
|
||||
});
|
||||
}
|
||||
|
||||
function saveResponse(content: string, revision: string) {
|
||||
return new Response(
|
||||
JSON.stringify({ path: filepath, sha256: revision, size: content.length }),
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
function Wrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale="en-US">
|
||||
<ArtifactDraftsProvider>
|
||||
<ArtifactsProvider>{children}</ArtifactsProvider>
|
||||
</ArtifactDraftsProvider>
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function source() {
|
||||
return screen.getByRole<HTMLTextAreaElement>("textbox", {
|
||||
name: "Artifact source",
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.sessionStorage.clear();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
queryClient.clear();
|
||||
rs.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("artifact saves racing content refetches", () => {
|
||||
it.each([
|
||||
["before the save", false, false, false],
|
||||
["during the save", true, false, false],
|
||||
["during a full-file save", true, true, false],
|
||||
["during a save followed by more editing", true, false, true],
|
||||
])(
|
||||
"keeps the saved content and revision when a read begun %s finishes late",
|
||||
async (
|
||||
_scenario,
|
||||
refetchDuringSave,
|
||||
fullContentRequested,
|
||||
editWhileSaving,
|
||||
) => {
|
||||
const pendingRead = deferred<Response>();
|
||||
const pendingSave = deferred<Response>();
|
||||
const savedBodies: unknown[] = [];
|
||||
let delayReads = false;
|
||||
|
||||
mockedFetch.mockImplementation(async (_url, options) => {
|
||||
if (options?.method === "PUT") {
|
||||
savedBodies.push(JSON.parse(options.body as string));
|
||||
return savedBodies.length === 1
|
||||
? pendingSave.promise
|
||||
: saveResponse("next edit", nextRevision);
|
||||
}
|
||||
if (delayReads) return pendingRead.promise;
|
||||
if (
|
||||
fullContentRequested &&
|
||||
new Headers(options?.headers).has("Range")
|
||||
) {
|
||||
return new Response("old", {
|
||||
status: 206,
|
||||
headers: {
|
||||
ETag: `"${oldRevision}"`,
|
||||
"Content-Range": "bytes 0-2/8",
|
||||
},
|
||||
});
|
||||
}
|
||||
return textResponse("old file", oldRevision);
|
||||
});
|
||||
|
||||
// Restore the provider before mounting the detail, as the real async
|
||||
// thread/artifact load does.
|
||||
const view = render(<div />, { wrapper: Wrapper });
|
||||
view.rerender(
|
||||
<ArtifactFileDetail filepath={filepath} threadId={threadId} />,
|
||||
);
|
||||
if (fullContentRequested) {
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: "Load full file" }),
|
||||
);
|
||||
}
|
||||
await waitFor(() => expect(source().value).toBe("old file"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Edit" }));
|
||||
fireEvent.change(source(), { target: { value: "saved edit" } });
|
||||
|
||||
const queryKey = [
|
||||
"artifact",
|
||||
filepath,
|
||||
threadId,
|
||||
false,
|
||||
fullContentRequested,
|
||||
];
|
||||
let refetch: Promise<void> | undefined;
|
||||
const startRead = async () => {
|
||||
delayReads = true;
|
||||
await act(async () => {
|
||||
refetch = queryClient.refetchQueries({ queryKey, exact: true });
|
||||
});
|
||||
expect(queryClient.isFetching({ queryKey, exact: true })).toBe(1);
|
||||
};
|
||||
|
||||
if (!refetchDuringSave) await startRead();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
await waitFor(() => expect(savedBodies).toHaveLength(1));
|
||||
if (refetchDuringSave) await startRead();
|
||||
|
||||
if (editWhileSaving) {
|
||||
// Reopening the panel restores its draft while the original save is
|
||||
// still pending, so subsequent typing must survive that completion.
|
||||
view.rerender(<div />);
|
||||
view.rerender(
|
||||
<ArtifactFileDetail filepath={filepath} threadId={threadId} />,
|
||||
);
|
||||
expect(source().disabled).toBe(false);
|
||||
fireEvent.change(source(), { target: { value: "newer draft" } });
|
||||
}
|
||||
const expectedContent = editWhileSaving ? "newer draft" : "saved edit";
|
||||
const unrelatedRead = deferred<string>();
|
||||
const unrelatedKeys = [
|
||||
[
|
||||
"artifact",
|
||||
"/mnt/user-data/outputs/other.txt",
|
||||
threadId,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
["artifact", filepath, "other-thread", false, false],
|
||||
];
|
||||
const unrelatedQueries = unrelatedKeys.map((key) =>
|
||||
queryClient
|
||||
.fetchQuery({ queryKey: key, queryFn: () => unrelatedRead.promise })
|
||||
.catch(() => null),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
pendingSave.resolve(saveResponse("saved edit", savedRevision));
|
||||
});
|
||||
await waitFor(() => expect(toast.success).toHaveBeenCalledOnce());
|
||||
expect(source().value).toBe(expectedContent);
|
||||
for (const key of unrelatedKeys) {
|
||||
expect(queryClient.isFetching({ queryKey: key, exact: true })).toBe(1);
|
||||
}
|
||||
unrelatedRead.resolve("unrelated content");
|
||||
expect(await Promise.all(unrelatedQueries)).toEqual([
|
||||
"unrelated content",
|
||||
"unrelated content",
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
pendingRead.resolve(textResponse("old file", oldRevision));
|
||||
await refetch;
|
||||
// Query observer notifications are batched onto the next task.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(source().value).toBe(expectedContent);
|
||||
expect(screen.queryByText("Changed remotely")).toBeNull();
|
||||
expect(queryClient.getQueryData(queryKey)).toMatchObject({
|
||||
content: "saved edit",
|
||||
sha256: savedRevision,
|
||||
});
|
||||
|
||||
// A later edit must compare against the committed revision, otherwise
|
||||
// the Gateway rejects it as a false optimistic-concurrency conflict.
|
||||
fireEvent.change(source(), { target: { value: "next edit" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
await waitFor(() => expect(savedBodies).toHaveLength(2));
|
||||
expect(savedBodies[1]).toEqual({
|
||||
content: "next edit",
|
||||
expected_sha256: savedRevision,
|
||||
});
|
||||
await waitFor(() => expect(toast.success).toHaveBeenCalledTimes(2));
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -67,6 +67,7 @@ describe("ArtifactViewer", () => {
|
||||
threadId,
|
||||
isMock: false,
|
||||
full: false,
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
@ -79,6 +80,7 @@ describe("ArtifactViewer", () => {
|
||||
threadId,
|
||||
isMock: true,
|
||||
full: false,
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -139,6 +141,7 @@ describe("ArtifactViewer", () => {
|
||||
threadId,
|
||||
isMock: false,
|
||||
full: true,
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
expect(screen.queryByRole("button", { name: "Load full file" })).toBe(null);
|
||||
});
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
import { afterEach, describe, expect, it, rs } from "@rstest/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
rs.mock("@/components/workspace/messages/context", () => ({
|
||||
useThread: () => ({
|
||||
thread: { isLoading: false, messages: [] },
|
||||
isMock: false,
|
||||
}),
|
||||
}));
|
||||
rs.mock("@/core/config", () => ({
|
||||
getBackendBaseURL: () => "/backend",
|
||||
}));
|
||||
rs.mock("@/env", () => ({
|
||||
env: { NEXT_PUBLIC_STATIC_WEBSITE_ONLY: "false" },
|
||||
}));
|
||||
|
||||
import {
|
||||
useArtifactContent,
|
||||
useStandaloneArtifactContent,
|
||||
} from "@/core/artifacts/hooks";
|
||||
|
||||
const filepath = "/mnt/user-data/outputs/report.txt";
|
||||
const threadId = "content-cancellation";
|
||||
|
||||
function usePanelContent() {
|
||||
return useArtifactContent({ filepath, threadId, enabled: true });
|
||||
}
|
||||
|
||||
function useStandaloneContent() {
|
||||
return useStandaloneArtifactContent({ filepath, threadId });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
rs.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["panel", usePanelContent],
|
||||
["standalone", useStandaloneContent],
|
||||
] as const)("%s artifact content cancellation", (_viewer, useContent) => {
|
||||
it.each([false, true])(
|
||||
"aborts the HTTP request before reading its body (full=%s)",
|
||||
async (full) => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
|
||||
});
|
||||
const response = new Response("obsolete file");
|
||||
const readBody = rs.spyOn(response, "arrayBuffer");
|
||||
const aborted = rs.fn();
|
||||
let finishRead!: (response: Response) => void;
|
||||
let failRead!: (reason: unknown) => void;
|
||||
const pendingRead = new Promise<Response>((resolve, reject) => {
|
||||
finishRead = resolve;
|
||||
failRead = reject;
|
||||
});
|
||||
// Model fetch's native abort behavior at the HTTP boundary; the hook,
|
||||
// loader and authenticated fetch wrapper all remain production code.
|
||||
rs.spyOn(globalThis, "fetch").mockImplementation(async (_url, init) => {
|
||||
if (full && new Headers(init?.headers).has("Range")) {
|
||||
return new Response("preview", {
|
||||
status: 206,
|
||||
headers: { "Content-Range": "bytes 0-6/100" },
|
||||
});
|
||||
}
|
||||
init?.signal?.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
aborted();
|
||||
failRead(new DOMException("Aborted", "AbortError"));
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
return pendingRead;
|
||||
});
|
||||
|
||||
function Wrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const view = renderHook(() => useContent(), { wrapper: Wrapper });
|
||||
try {
|
||||
if (full) {
|
||||
await waitFor(() => expect(view.result.current.truncated).toBe(true));
|
||||
act(() => view.result.current.loadFullContent());
|
||||
}
|
||||
await waitFor(() =>
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(full ? 2 : 1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await client.cancelQueries({
|
||||
queryKey: ["artifact", filepath, threadId, false],
|
||||
});
|
||||
});
|
||||
|
||||
expect(aborted).toHaveBeenCalledOnce();
|
||||
expect(readBody).not.toHaveBeenCalled();
|
||||
expect(view.result.current.error).toBeNull();
|
||||
expect(client.isFetching()).toBe(0);
|
||||
} finally {
|
||||
finishRead(response);
|
||||
view.unmount();
|
||||
client.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -72,6 +72,7 @@ describe("useArtifactContent", () => {
|
||||
threadId: "thread-a",
|
||||
isMock: false,
|
||||
full: false,
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
@ -85,6 +86,7 @@ describe("useArtifactContent", () => {
|
||||
threadId: "thread-a",
|
||||
isMock: false,
|
||||
full: true,
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
@ -96,6 +98,7 @@ describe("useArtifactContent", () => {
|
||||
threadId: "thread-b",
|
||||
isMock: false,
|
||||
full: false,
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,10 +5,11 @@
|
||||
# scripts/bump_version.sh <version> # e.g. scripts/bump_version.sh 2.2.0
|
||||
#
|
||||
# Updates:
|
||||
# backend/pyproject.toml (version = "...")
|
||||
# backend/uv.lock (root package version, via `uv lock`)
|
||||
# frontend/package.json ("version": "...")
|
||||
# deploy/helm/deer-flow/Chart.yaml (version: + appVersion:)
|
||||
# backend/pyproject.toml (version = "...")
|
||||
# backend/packages/harness/pyproject.toml (deerflow-harness version = "...")
|
||||
# backend/uv.lock (root + harness package versions, via `uv lock`)
|
||||
# frontend/package.json ("version": "...")
|
||||
# deploy/helm/deer-flow/Chart.yaml (version: + appVersion:)
|
||||
#
|
||||
# Requires `uv` on PATH: backend/uv.lock pins the root package version too, and
|
||||
# lint CI runs `uv lock --check`, so the lock has to move with the sources.
|
||||
@ -33,10 +34,11 @@ fi
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYPROJECT="$ROOT/backend/pyproject.toml"
|
||||
HARNESS_PYPROJECT="$ROOT/backend/packages/harness/pyproject.toml"
|
||||
PACKAGE="$ROOT/frontend/package.json"
|
||||
CHART="$ROOT/deploy/helm/deer-flow/Chart.yaml"
|
||||
|
||||
for f in "$PYPROJECT" "$PACKAGE" "$CHART"; do
|
||||
for f in "$PYPROJECT" "$HARNESS_PYPROJECT" "$PACKAGE" "$CHART"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "error: expected version file not found: $f" >&2
|
||||
exit 1
|
||||
@ -54,20 +56,23 @@ if ! command -v uv >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$PYPROJECT" "$PACKAGE" "$CHART" "$VERSION" <<'PY'
|
||||
python3 - "$PYPROJECT" "$HARNESS_PYPROJECT" "$PACKAGE" "$CHART" "$VERSION" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
pyproject, package, chart, version = sys.argv[1:5]
|
||||
pyproject, harness, package, chart, version = sys.argv[1:6]
|
||||
|
||||
# backend/pyproject.toml — version = "..."
|
||||
with open(pyproject) as f:
|
||||
src = f.read()
|
||||
new = re.sub(r'(?m)^version\s*=\s*".*?"', f'version = "{version}"', src, count=1)
|
||||
if new == src:
|
||||
sys.exit(f"error: no top-level 'version' field in {pyproject}")
|
||||
with open(pyproject, "w") as f:
|
||||
f.write(new)
|
||||
# backend/pyproject.toml and backend/packages/harness/pyproject.toml —
|
||||
# version = "..." (deerflow-harness is a workspace member; `uv lock` below
|
||||
# moves its backend/uv.lock entry in lockstep)
|
||||
for path in (pyproject, harness):
|
||||
with open(path) as f:
|
||||
src = f.read()
|
||||
new = re.sub(r'(?m)^version\s*=\s*".*?"', f'version = "{version}"', src, count=1)
|
||||
if new == src:
|
||||
sys.exit(f"error: no top-level 'version' field in {path}")
|
||||
with open(path, "w") as f:
|
||||
f.write(new)
|
||||
|
||||
# frontend/package.json — "version": "..." (preserve indentation; minimal diff)
|
||||
with open(package) as f:
|
||||
@ -101,6 +106,7 @@ fi
|
||||
|
||||
echo "Bumped version to $VERSION in:"
|
||||
echo " backend/pyproject.toml"
|
||||
echo " backend/packages/harness/pyproject.toml"
|
||||
echo " backend/uv.lock"
|
||||
echo " frontend/package.json"
|
||||
echo " deploy/helm/deer-flow/Chart.yaml (version + appVersion)"
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
# Verify that every project version source agrees.
|
||||
#
|
||||
# Sources checked:
|
||||
# deploy/helm/deer-flow/Chart.yaml — version + appVersion
|
||||
# backend/pyproject.toml — version
|
||||
# frontend/package.json — version
|
||||
# backend/uv.lock — root package version, via `uv lock --check`
|
||||
# deploy/helm/deer-flow/Chart.yaml — version + appVersion
|
||||
# backend/pyproject.toml — version
|
||||
# backend/packages/harness/pyproject.toml — deerflow-harness version
|
||||
# frontend/package.json — version
|
||||
# backend/uv.lock — root + harness entries, via `uv lock --check`
|
||||
#
|
||||
# Usage:
|
||||
# scripts/verify_versions.sh # all sources must be mutually equal
|
||||
@ -22,10 +23,11 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
CHART="$ROOT/deploy/helm/deer-flow/Chart.yaml"
|
||||
PYPROJECT="$ROOT/backend/pyproject.toml"
|
||||
HARNESS_PYPROJECT="$ROOT/backend/packages/harness/pyproject.toml"
|
||||
PACKAGE="$ROOT/frontend/package.json"
|
||||
UV_LOCK="$ROOT/backend/uv.lock"
|
||||
|
||||
for f in "$CHART" "$PYPROJECT" "$PACKAGE" "$UV_LOCK"; do
|
||||
for f in "$CHART" "$PYPROJECT" "$HARNESS_PYPROJECT" "$PACKAGE" "$UV_LOCK"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "::error::missing version file: $f" >&2
|
||||
exit 1
|
||||
@ -35,6 +37,7 @@ done
|
||||
CHART_VERSION=$(awk '/^version:/ {print $2; exit}' "$CHART")
|
||||
APP_VERSION=$(awk '/^appVersion:/ {gsub(/"/, ""); print $2; exit}' "$CHART")
|
||||
PY_VERSION=$(awk -F'"' '/^version[[:space:]]*=/ {print $2; exit}' "$PYPROJECT")
|
||||
HARNESS_VERSION=$(awk -F'"' '/^version[[:space:]]*=/ {print $2; exit}' "$HARNESS_PYPROJECT")
|
||||
JS_VERSION=$(grep -m1 '"version"' "$PACKAGE" | awk -F'"' '{print $4}')
|
||||
PY_NAME=$(awk -F'"' '/^name[[:space:]]*=/ {print $2; exit}' "$PYPROJECT")
|
||||
LOCK_VERSION=$(awk -v name="$PY_NAME" '
|
||||
@ -48,6 +51,7 @@ LOCK_VERSION=$(awk -v name="$PY_NAME" '
|
||||
printf 'Chart.yaml version: %s\n' "$CHART_VERSION"
|
||||
printf 'Chart.yaml appVersion: %s\n' "$APP_VERSION"
|
||||
printf 'backend/pyproject.toml: %s\n' "$PY_VERSION"
|
||||
printf 'harness/pyproject.toml: %s\n' "$HARNESS_VERSION"
|
||||
printf 'frontend/package.json: %s\n' "$JS_VERSION"
|
||||
printf 'backend/uv.lock: %s\n' "$LOCK_VERSION"
|
||||
|
||||
@ -69,11 +73,13 @@ if [ -n "$EXPECTED" ]; then
|
||||
mismatch "Chart.yaml version" "$CHART_VERSION" "$EXPECTED" || status=1
|
||||
mismatch "Chart.yaml appVersion" "$APP_VERSION" "$EXPECTED" || status=1
|
||||
mismatch "backend/pyproject.toml" "$PY_VERSION" "$EXPECTED" || status=1
|
||||
mismatch "backend/packages/harness/pyproject.toml" "$HARNESS_VERSION" "$EXPECTED" || status=1
|
||||
mismatch "frontend/package.json" "$JS_VERSION" "$EXPECTED" || status=1
|
||||
else
|
||||
echo
|
||||
mismatch "Chart.yaml appVersion" "$APP_VERSION" "$CHART_VERSION" || status=1
|
||||
mismatch "backend/pyproject.toml" "$PY_VERSION" "$CHART_VERSION" || status=1
|
||||
mismatch "backend/packages/harness/pyproject.toml" "$HARNESS_VERSION" "$CHART_VERSION" || status=1
|
||||
mismatch "frontend/package.json" "$JS_VERSION" "$CHART_VERSION" || status=1
|
||||
fi
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user