deepstrike 0.2.6__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {deepstrike-0.2.6 → deepstrike-0.2.7}/Cargo.lock +5 -5
  2. {deepstrike-0.2.6 → deepstrike-0.2.7}/Cargo.toml +2 -2
  3. {deepstrike-0.2.6 → deepstrike-0.2.7}/PKG-INFO +1 -1
  4. deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/loop_until_done.rs +281 -0
  5. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/mod.rs +3 -0
  6. deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/tournament.rs +347 -0
  7. deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/workflow.rs +399 -0
  8. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/event_log.rs +4 -1
  9. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/kernel.rs +156 -0
  10. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/mod.rs +3 -3
  11. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/repair.rs +53 -63
  12. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/session.rs +9 -0
  13. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/mod.rs +1 -0
  14. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/state_machine.rs +208 -0
  15. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/state_machine_tests.rs +187 -0
  16. deepstrike-0.2.7/crates/deepstrike-core/src/scheduler/workflow_run.rs +375 -0
  17. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-py/src/lib.rs +283 -0
  18. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/__init__.py +14 -2
  19. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/__init__.py +2 -2
  20. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/anthropic.py +46 -2
  21. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/base.py +15 -0
  22. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/deepseek.py +105 -4
  23. deepstrike-0.2.7/deepstrike/providers/minimax.py +254 -0
  24. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/openai.py +26 -5
  25. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/replay.py +24 -7
  26. deepstrike-0.2.7/deepstrike/providers/replay_validator.py +75 -0
  27. deepstrike-0.2.7/deepstrike/runtime/provider_replay.py +79 -0
  28. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/runner.py +114 -0
  29. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/session_log.py +31 -0
  30. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/session_repair.py +35 -42
  31. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/types/agent.py +141 -0
  32. deepstrike-0.2.7/deepstrike/workflow.py +54 -0
  33. {deepstrike-0.2.6 → deepstrike-0.2.7}/pyproject.toml +1 -1
  34. deepstrike-0.2.6/deepstrike/providers/minimax.py +0 -33
  35. deepstrike-0.2.6/deepstrike/runtime/provider_replay.py +0 -41
  36. {deepstrike-0.2.6 → deepstrike-0.2.7}/README.md +0 -0
  37. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/Cargo.toml +0 -0
  38. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/compression.rs +0 -0
  39. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/config.rs +0 -0
  40. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  41. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/llm_summarizer.rs +0 -0
  42. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/manager.rs +0 -0
  43. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/mod.rs +0 -0
  44. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  45. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/pressure.rs +0 -0
  46. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/renderer.rs +0 -0
  47. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  48. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/sections.rs +0 -0
  49. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  50. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
  51. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  52. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/task_state.rs +0 -0
  53. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/text.rs +0 -0
  54. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  55. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  56. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  57. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  58. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  59. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  60. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  61. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  62. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  63. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  64. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  65. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/harness/eval_pipeline.rs +0 -0
  66. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  67. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/lib.rs +0 -0
  68. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  69. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  70. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  71. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  72. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  73. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  74. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  75. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/session.rs +0 -0
  76. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  77. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  78. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/working.rs +0 -0
  79. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/mm/handle.rs +0 -0
  80. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  81. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  82. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/executor.rs +0 -0
  83. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/gen_eval.rs +0 -0
  84. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/planner.rs +0 -0
  85. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  86. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/proc/mod.rs +0 -0
  87. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  88. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  89. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  90. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  91. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
  92. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  93. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  94. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  95. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/router.rs +0 -0
  96. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
  97. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/agent.rs +0 -0
  98. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/capability.rs +0 -0
  99. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/contract.rs +0 -0
  100. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/error.rs +0 -0
  101. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/message.rs +0 -0
  102. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  103. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/mod.rs +0 -0
  104. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/model.rs +0 -0
  105. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/policy.rs +0 -0
  106. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/result.rs +0 -0
  107. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/signal.rs +0 -0
  108. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/skill.rs +0 -0
  109. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/task.rs +0 -0
  110. {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-py/Cargo.toml +0 -0
  111. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/__init__.py +0 -0
  112. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/contract.py +0 -0
  113. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/handoff.py +0 -0
  114. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/harness.py +0 -0
  115. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/modes.py +0 -0
  116. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/pool.py +0 -0
  117. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/governance.py +0 -0
  118. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/harness/__init__.py +0 -0
  119. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/harness/harness.py +0 -0
  120. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/kernel/__init__.py +0 -0
  121. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/knowledge/__init__.py +0 -0
  122. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/knowledge/source.py +0 -0
  123. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/__init__.py +0 -0
  124. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/agent.py +0 -0
  125. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/protocols.py +0 -0
  126. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/working.py +0 -0
  127. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/gemini.py +0 -0
  128. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/glm.py +0 -0
  129. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/kimi.py +0 -0
  130. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/ollama.py +0 -0
  131. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/qwen.py +0 -0
  132. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/stream.py +0 -0
  133. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/__init__.py +0 -0
  134. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/archive.py +0 -0
  135. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/credential_vault.py +0 -0
  136. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/execution_plane.py +0 -0
  137. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/filtered_plane.py +0 -0
  138. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/kernel_event_log.py +0 -0
  139. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/kernel_step.py +0 -0
  140. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/large_result_spool.py +0 -0
  141. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  142. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/os_profile.py +0 -0
  143. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/os_snapshot.py +0 -0
  144. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  145. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  146. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/replay_sanitize.py +0 -0
  147. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
  148. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/safety/__init__.py +0 -0
  149. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/safety/permissions.py +0 -0
  150. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/__init__.py +0 -0
  151. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/gateway.py +0 -0
  152. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/scheduled.py +0 -0
  153. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/types.py +0 -0
  154. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/skills/__init__.py +0 -0
  155. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/skills/registry.py +0 -0
  156. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/skills/watcher.py +0 -0
  157. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/__init__.py +0 -0
  158. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/builtin/__init__.py +0 -0
  159. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/builtin/read_file.py +0 -0
  160. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/execution.py +0 -0
  161. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/registry.py +0 -0
  162. {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/types/__init__.py +0 -0
@@ -194,7 +194,7 @@ dependencies = [
194
194
 
195
195
  [[package]]
196
196
  name = "deepstrike-core"
197
- version = "0.2.6"
197
+ version = "0.2.7"
198
198
  dependencies = [
199
199
  "compact_str",
200
200
  "pretty_assertions",
@@ -206,7 +206,7 @@ dependencies = [
206
206
 
207
207
  [[package]]
208
208
  name = "deepstrike-node"
209
- version = "0.2.6"
209
+ version = "0.2.7"
210
210
  dependencies = [
211
211
  "compact_str",
212
212
  "deepstrike-core",
@@ -218,7 +218,7 @@ dependencies = [
218
218
 
219
219
  [[package]]
220
220
  name = "deepstrike-py"
221
- version = "0.2.6"
221
+ version = "0.2.7"
222
222
  dependencies = [
223
223
  "compact_str",
224
224
  "deepstrike-core",
@@ -229,7 +229,7 @@ dependencies = [
229
229
 
230
230
  [[package]]
231
231
  name = "deepstrike-sdk"
232
- version = "0.2.6"
232
+ version = "0.2.7"
233
233
  dependencies = [
234
234
  "async-stream",
235
235
  "async-trait",
@@ -263,7 +263,7 @@ dependencies = [
263
263
 
264
264
  [[package]]
265
265
  name = "deepstrike-wasm"
266
- version = "0.2.6"
266
+ version = "0.2.7"
267
267
  dependencies = [
268
268
  "compact_str",
269
269
  "deepstrike-core",
@@ -3,13 +3,13 @@ resolver = "2"
3
3
  members = ["crates/deepstrike-core", "crates/deepstrike-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.6"
6
+ version = "0.2.7"
7
7
  edition = "2024"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/kongusen/deepstrike"
10
10
 
11
11
  [workspace.dependencies]
12
- deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.6" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.7" }
13
13
  notify = "6"
14
14
  serde = { version = "1", features = ["derive"] }
15
15
  serde_json = "1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepstrike
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -0,0 +1,281 @@
1
+ //! Loop-until-done — drive an agent round after round until a stop predicate fires.
2
+ //!
3
+ //! For tasks with an unknown amount of work (keep investigating until no new findings;
4
+ //! keep fixing until no more errors). Mirrors [`super::gen_eval::GenEvalLoop`]: a pure
5
+ //! control state machine emitting **abstract actions**; the SDK spawns a worker per round
6
+ //! and feeds back what that round produced. No I/O, no clock.
7
+ //!
8
+ //! Termination is guaranteed *in-kernel*: a [`StopCondition::MaxRounds`] backstop is always
9
+ //! present (injected with [`DEFAULT_MAX_ROUNDS`] if the caller configured none), so the loop
10
+ //! cannot run forever regardless of SDK behavior. Wall-clock / token backstops are an
11
+ //! orthogonal concern owned by the SDK's [`crate::scheduler::policy::SchedulerBudget`]
12
+ //! wrapped around the loop — this state machine stays zero-clock.
13
+
14
+ /// Default hard round cap injected when the caller configures no [`StopCondition::MaxRounds`].
15
+ pub const DEFAULT_MAX_ROUNDS: u32 = 50;
16
+
17
+ /// A stop predicate. The loop stops as soon as **any** configured condition fires.
18
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
19
+ pub enum StopCondition {
20
+ /// The round produced no new findings (`new_findings == 0`).
21
+ NoNewFindings,
22
+ /// The round reported no errors (`errors == 0`).
23
+ NoErrors,
24
+ /// Hard cap: stop once this many rounds have completed.
25
+ MaxRounds(u32),
26
+ }
27
+
28
+ /// Loop configuration. Always carries a `MaxRounds` backstop after construction.
29
+ #[derive(Debug, Clone)]
30
+ pub struct LoopConfig {
31
+ conditions: Vec<StopCondition>,
32
+ }
33
+
34
+ impl LoopConfig {
35
+ /// Build a config. If no [`StopCondition::MaxRounds`] is present, a
36
+ /// [`DEFAULT_MAX_ROUNDS`] backstop is appended so termination is guaranteed.
37
+ pub fn new(conditions: Vec<StopCondition>) -> Self {
38
+ let has_max = conditions
39
+ .iter()
40
+ .any(|c| matches!(c, StopCondition::MaxRounds(_)));
41
+ let mut conditions = conditions;
42
+ if !has_max {
43
+ conditions.push(StopCondition::MaxRounds(DEFAULT_MAX_ROUNDS));
44
+ }
45
+ Self { conditions }
46
+ }
47
+
48
+ pub fn conditions(&self) -> &[StopCondition] {
49
+ &self.conditions
50
+ }
51
+ }
52
+
53
+ impl Default for LoopConfig {
54
+ fn default() -> Self {
55
+ Self::new(Vec::new())
56
+ }
57
+ }
58
+
59
+ /// What the SDK reports after running a round's worker.
60
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
61
+ pub struct RoundReport {
62
+ pub new_findings: u32,
63
+ pub errors: u32,
64
+ }
65
+
66
+ /// Why the loop stopped.
67
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
68
+ pub enum StopReason {
69
+ NoNewFindings,
70
+ NoErrors,
71
+ MaxRounds,
72
+ }
73
+
74
+ /// What the SDK should do next.
75
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
76
+ pub enum LoopAction {
77
+ /// Spawn the worker for round `round`, then call [`LoopUntilDone::feed`] with its report.
78
+ Spawn { round: u32 },
79
+ /// The loop converged.
80
+ Done {
81
+ rounds_used: u32,
82
+ reason: StopReason,
83
+ },
84
+ }
85
+
86
+ /// Loop-until-done control state machine.
87
+ #[derive(Debug)]
88
+ pub struct LoopUntilDone {
89
+ config: LoopConfig,
90
+ /// Round currently in flight (the last one emitted by `start`/`feed`).
91
+ round: u32,
92
+ done: bool,
93
+ }
94
+
95
+ impl LoopUntilDone {
96
+ pub fn new(config: LoopConfig) -> Self {
97
+ Self {
98
+ config,
99
+ round: 0,
100
+ done: false,
101
+ }
102
+ }
103
+
104
+ /// Begin the loop — always spawns round 1.
105
+ pub fn start(&mut self) -> LoopAction {
106
+ self.round = 1;
107
+ LoopAction::Spawn { round: 1 }
108
+ }
109
+
110
+ /// Feed the just-completed round's report and get the next action.
111
+ pub fn feed(&mut self, report: RoundReport) -> LoopAction {
112
+ if self.done {
113
+ return LoopAction::Done {
114
+ rounds_used: self.round,
115
+ reason: StopReason::MaxRounds,
116
+ };
117
+ }
118
+
119
+ if let Some(reason) = self.first_triggered(&report) {
120
+ self.done = true;
121
+ return LoopAction::Done {
122
+ rounds_used: self.round,
123
+ reason,
124
+ };
125
+ }
126
+
127
+ self.round += 1;
128
+ LoopAction::Spawn { round: self.round }
129
+ }
130
+
131
+ /// Return the reason for the first stop condition (in configured order) that fires for
132
+ /// the just-completed round (`self.round`).
133
+ fn first_triggered(&self, report: &RoundReport) -> Option<StopReason> {
134
+ for cond in &self.config.conditions {
135
+ let hit = match cond {
136
+ StopCondition::NoNewFindings => report.new_findings == 0,
137
+ StopCondition::NoErrors => report.errors == 0,
138
+ StopCondition::MaxRounds(max) => self.round >= *max,
139
+ };
140
+ if hit {
141
+ return Some(match cond {
142
+ StopCondition::NoNewFindings => StopReason::NoNewFindings,
143
+ StopCondition::NoErrors => StopReason::NoErrors,
144
+ StopCondition::MaxRounds(_) => StopReason::MaxRounds,
145
+ });
146
+ }
147
+ }
148
+ None
149
+ }
150
+
151
+ pub fn is_done(&self) -> bool {
152
+ self.done
153
+ }
154
+ }
155
+
156
+ #[cfg(test)]
157
+ mod tests {
158
+ use super::*;
159
+
160
+ fn report(new_findings: u32, errors: u32) -> RoundReport {
161
+ RoundReport {
162
+ new_findings,
163
+ errors,
164
+ }
165
+ }
166
+
167
+ #[test]
168
+ fn default_injects_max_rounds_backstop() {
169
+ let cfg = LoopConfig::default();
170
+ assert!(
171
+ cfg.conditions()
172
+ .iter()
173
+ .any(|c| matches!(c, StopCondition::MaxRounds(DEFAULT_MAX_ROUNDS)))
174
+ );
175
+ }
176
+
177
+ #[test]
178
+ fn explicit_max_rounds_not_duplicated() {
179
+ let cfg = LoopConfig::new(vec![StopCondition::MaxRounds(5)]);
180
+ let maxes: Vec<_> = cfg
181
+ .conditions()
182
+ .iter()
183
+ .filter(|c| matches!(c, StopCondition::MaxRounds(_)))
184
+ .collect();
185
+ assert_eq!(maxes.len(), 1);
186
+ }
187
+
188
+ #[test]
189
+ fn start_spawns_round_one() {
190
+ let mut l = LoopUntilDone::new(LoopConfig::default());
191
+ assert_eq!(l.start(), LoopAction::Spawn { round: 1 });
192
+ }
193
+
194
+ #[test]
195
+ fn stops_on_no_new_findings() {
196
+ let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::NoNewFindings]));
197
+ l.start();
198
+ // round 1 found things → continue
199
+ assert_eq!(l.feed(report(3, 0)), LoopAction::Spawn { round: 2 });
200
+ // round 2 found nothing → stop
201
+ assert_eq!(
202
+ l.feed(report(0, 9)),
203
+ LoopAction::Done {
204
+ rounds_used: 2,
205
+ reason: StopReason::NoNewFindings
206
+ }
207
+ );
208
+ assert!(l.is_done());
209
+ }
210
+
211
+ #[test]
212
+ fn stops_on_no_errors() {
213
+ let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::NoErrors]));
214
+ l.start();
215
+ assert_eq!(l.feed(report(0, 2)), LoopAction::Spawn { round: 2 });
216
+ assert_eq!(
217
+ l.feed(report(0, 0)),
218
+ LoopAction::Done {
219
+ rounds_used: 2,
220
+ reason: StopReason::NoErrors
221
+ }
222
+ );
223
+ }
224
+
225
+ #[test]
226
+ fn max_rounds_caps_the_loop() {
227
+ let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::MaxRounds(3)]));
228
+ l.start();
229
+ assert_eq!(l.feed(report(1, 1)), LoopAction::Spawn { round: 2 });
230
+ assert_eq!(l.feed(report(1, 1)), LoopAction::Spawn { round: 3 });
231
+ assert_eq!(
232
+ l.feed(report(1, 1)),
233
+ LoopAction::Done {
234
+ rounds_used: 3,
235
+ reason: StopReason::MaxRounds
236
+ }
237
+ );
238
+ }
239
+
240
+ #[test]
241
+ fn default_backstop_terminates_unbounded_predicate() {
242
+ // NoErrors configured but errors never reach 0 → default MaxRounds(50) must fire.
243
+ let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::NoErrors]));
244
+ let mut action = l.start();
245
+ let mut last = action;
246
+ for _ in 0..100 {
247
+ match action {
248
+ LoopAction::Spawn { .. } => {
249
+ last = action;
250
+ action = l.feed(report(1, 1));
251
+ }
252
+ LoopAction::Done { .. } => break,
253
+ }
254
+ }
255
+ let _ = last;
256
+ assert_eq!(
257
+ action,
258
+ LoopAction::Done {
259
+ rounds_used: DEFAULT_MAX_ROUNDS,
260
+ reason: StopReason::MaxRounds
261
+ }
262
+ );
263
+ }
264
+
265
+ #[test]
266
+ fn first_configured_condition_wins() {
267
+ // Both NoNewFindings and NoErrors would fire on (0,0); configured order picks the first.
268
+ let mut l = LoopUntilDone::new(LoopConfig::new(vec![
269
+ StopCondition::NoNewFindings,
270
+ StopCondition::NoErrors,
271
+ ]));
272
+ l.start();
273
+ assert_eq!(
274
+ l.feed(report(0, 0)),
275
+ LoopAction::Done {
276
+ rounds_used: 1,
277
+ reason: StopReason::NoNewFindings
278
+ }
279
+ );
280
+ }
281
+ }
@@ -1,4 +1,7 @@
1
1
  pub mod executor;
2
2
  pub mod gen_eval;
3
+ pub mod loop_until_done;
3
4
  pub mod planner;
4
5
  pub mod task_graph;
6
+ pub mod tournament;
7
+ pub mod workflow;
@@ -0,0 +1,347 @@
1
+ //! Single-elimination tournament — pairwise comparative judging.
2
+ //!
3
+ //! Pattern mirrors [`super::gen_eval::GenEvalLoop`]: a pure control state machine that
4
+ //! holds the bracket and emits **abstract actions**; the SDK runs a fresh-context judge
5
+ //! agent per match and feeds back winners. No prompt assembly, no I/O, no clock.
6
+ //!
7
+ //! Why a tournament instead of absolute scoring: comparative judgment ("which of these
8
+ //! two is better?") is more reliable than asking one agent to score 1000 items, and only
9
+ //! the current round's match-ups ever enter context — the deterministic loop holds the
10
+ //! whole bracket.
11
+ //!
12
+ //! ```text
13
+ //! entrants ──▶ JudgeRound{round 1, matches} ──▶ SDK runs N parallel pairwise judges
14
+ //! ▲ │
15
+ //! └────────── feed_round(winners) ◀──────┘
16
+ //! (repeat until one survivor) ──▶ Done{winner}
17
+ //! ```
18
+
19
+ use crate::types::error::{DeepStrikeError, Result};
20
+
21
+ /// A participant. The SDK maps the id back to the real item being compared.
22
+ pub type EntrantId = String;
23
+
24
+ /// One pairwise match-up in a round.
25
+ #[derive(Debug, Clone, PartialEq, Eq)]
26
+ pub struct Match {
27
+ /// Index of this match within its round (0-based).
28
+ pub id: u32,
29
+ pub left: EntrantId,
30
+ pub right: EntrantId,
31
+ }
32
+
33
+ /// What the SDK should do next.
34
+ #[derive(Debug, Clone, PartialEq, Eq)]
35
+ pub enum TournamentAction {
36
+ /// Run one fresh-context judge per match this round (matches are independent — run
37
+ /// them in parallel), then call [`Tournament::feed_round`] with the winners.
38
+ JudgeRound { round: u32, matches: Vec<Match> },
39
+ /// The bracket is resolved.
40
+ Done { winner: EntrantId, rounds_used: u32 },
41
+ }
42
+
43
+ /// Single-elimination bracket control state machine.
44
+ #[derive(Debug)]
45
+ pub struct Tournament {
46
+ /// Entrants advancing into the next round to be played.
47
+ survivors: Vec<EntrantId>,
48
+ /// Matches emitted for the current round, awaiting results.
49
+ pending: Vec<Match>,
50
+ /// Entrant that drew a bye this round (odd survivor count) — auto-advances.
51
+ bye: Option<EntrantId>,
52
+ /// Number of the most recently emitted round (0 before `start`).
53
+ round: u32,
54
+ done: bool,
55
+ }
56
+
57
+ impl Tournament {
58
+ /// Build a tournament. Requires at least one entrant.
59
+ pub fn new(entrants: Vec<EntrantId>) -> Result<Self> {
60
+ if entrants.is_empty() {
61
+ return Err(DeepStrikeError::InvalidConfig(
62
+ "tournament requires at least one entrant".into(),
63
+ ));
64
+ }
65
+ Ok(Self {
66
+ survivors: entrants,
67
+ pending: Vec::new(),
68
+ bye: None,
69
+ round: 0,
70
+ done: false,
71
+ })
72
+ }
73
+
74
+ /// Begin the tournament. A single entrant wins immediately (zero rounds);
75
+ /// otherwise the first round of match-ups is emitted.
76
+ pub fn start(&mut self) -> TournamentAction {
77
+ self.emit_round_or_done()
78
+ }
79
+
80
+ /// Report the winners of the round last emitted by [`TournamentAction::JudgeRound`].
81
+ /// `winners` must align one-to-one with that round's `matches`, and each winner must
82
+ /// be one of the two entrants in its match.
83
+ pub fn feed_round(&mut self, winners: Vec<EntrantId>) -> Result<TournamentAction> {
84
+ if self.done {
85
+ return Err(DeepStrikeError::InvalidConfig(
86
+ "tournament already complete".into(),
87
+ ));
88
+ }
89
+ if winners.len() != self.pending.len() {
90
+ return Err(DeepStrikeError::InvalidConfig(format!(
91
+ "expected {} winner(s) for round {}, got {}",
92
+ self.pending.len(),
93
+ self.round,
94
+ winners.len()
95
+ )));
96
+ }
97
+ for (w, m) in winners.iter().zip(&self.pending) {
98
+ if w != &m.left && w != &m.right {
99
+ return Err(DeepStrikeError::InvalidConfig(format!(
100
+ "winner '{w}' is not a participant in match {}",
101
+ m.id
102
+ )));
103
+ }
104
+ }
105
+
106
+ let mut next = winners;
107
+ if let Some(bye) = self.bye.take() {
108
+ next.push(bye);
109
+ }
110
+ self.survivors = next;
111
+ self.pending.clear();
112
+ Ok(self.emit_round_or_done())
113
+ }
114
+
115
+ /// Emit the next round of match-ups, or finish if only one survivor remains.
116
+ fn emit_round_or_done(&mut self) -> TournamentAction {
117
+ if self.survivors.len() == 1 {
118
+ self.done = true;
119
+ return TournamentAction::Done {
120
+ winner: self.survivors[0].clone(),
121
+ rounds_used: self.round,
122
+ };
123
+ }
124
+
125
+ self.round += 1;
126
+ let mut matches = Vec::with_capacity(self.survivors.len() / 2);
127
+ let mut i = 0;
128
+ while i + 1 < self.survivors.len() {
129
+ matches.push(Match {
130
+ id: (i / 2) as u32,
131
+ left: self.survivors[i].clone(),
132
+ right: self.survivors[i + 1].clone(),
133
+ });
134
+ i += 2;
135
+ }
136
+ // Odd count → the trailing entrant draws a bye and advances untouched.
137
+ self.bye = if self.survivors.len() % 2 == 1 {
138
+ self.survivors.last().cloned()
139
+ } else {
140
+ None
141
+ };
142
+ self.pending = matches.clone();
143
+ TournamentAction::JudgeRound {
144
+ round: self.round,
145
+ matches,
146
+ }
147
+ }
148
+
149
+ pub fn is_done(&self) -> bool {
150
+ self.done
151
+ }
152
+ }
153
+
154
+ #[cfg(test)]
155
+ mod tests {
156
+ use super::*;
157
+
158
+ fn ids(xs: &[&str]) -> Vec<EntrantId> {
159
+ xs.iter().map(|s| s.to_string()).collect()
160
+ }
161
+
162
+ #[test]
163
+ fn empty_entrants_is_error() {
164
+ assert!(Tournament::new(vec![]).is_err());
165
+ }
166
+
167
+ #[test]
168
+ fn single_entrant_wins_immediately() {
169
+ let mut t = Tournament::new(ids(&["a"])).unwrap();
170
+ match t.start() {
171
+ TournamentAction::Done {
172
+ winner,
173
+ rounds_used,
174
+ } => {
175
+ assert_eq!(winner, "a");
176
+ assert_eq!(rounds_used, 0);
177
+ }
178
+ _ => panic!("expected immediate Done"),
179
+ }
180
+ assert!(t.is_done());
181
+ }
182
+
183
+ #[test]
184
+ fn two_entrants_one_round() {
185
+ let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
186
+ match t.start() {
187
+ TournamentAction::JudgeRound { round, matches } => {
188
+ assert_eq!(round, 1);
189
+ assert_eq!(matches.len(), 1);
190
+ assert_eq!(
191
+ matches[0],
192
+ Match {
193
+ id: 0,
194
+ left: "a".into(),
195
+ right: "b".into()
196
+ }
197
+ );
198
+ }
199
+ _ => panic!("expected JudgeRound"),
200
+ }
201
+ match t.feed_round(ids(&["b"])).unwrap() {
202
+ TournamentAction::Done {
203
+ winner,
204
+ rounds_used,
205
+ } => {
206
+ assert_eq!(winner, "b");
207
+ assert_eq!(rounds_used, 1);
208
+ }
209
+ _ => panic!("expected Done"),
210
+ }
211
+ }
212
+
213
+ #[test]
214
+ fn four_entrants_two_rounds() {
215
+ let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
216
+ let r1 = t.start();
217
+ match r1 {
218
+ TournamentAction::JudgeRound { round, matches } => {
219
+ assert_eq!(round, 1);
220
+ assert_eq!(matches.len(), 2);
221
+ }
222
+ _ => panic!(),
223
+ }
224
+ // a beats b, d beats c
225
+ let r2 = t.feed_round(ids(&["a", "d"])).unwrap();
226
+ match r2 {
227
+ TournamentAction::JudgeRound { round, matches } => {
228
+ assert_eq!(round, 2);
229
+ assert_eq!(matches.len(), 1);
230
+ assert_eq!(
231
+ matches[0],
232
+ Match {
233
+ id: 0,
234
+ left: "a".into(),
235
+ right: "d".into()
236
+ }
237
+ );
238
+ }
239
+ _ => panic!(),
240
+ }
241
+ match t.feed_round(ids(&["d"])).unwrap() {
242
+ TournamentAction::Done {
243
+ winner,
244
+ rounds_used,
245
+ } => {
246
+ assert_eq!(winner, "d");
247
+ assert_eq!(rounds_used, 2);
248
+ }
249
+ _ => panic!(),
250
+ }
251
+ }
252
+
253
+ #[test]
254
+ fn three_entrants_bye_advances() {
255
+ let mut t = Tournament::new(ids(&["a", "b", "c"])).unwrap();
256
+ match t.start() {
257
+ TournamentAction::JudgeRound { round, matches } => {
258
+ assert_eq!(round, 1);
259
+ // only (a,b) plays; c gets a bye
260
+ assert_eq!(matches.len(), 1);
261
+ assert_eq!(
262
+ matches[0],
263
+ Match {
264
+ id: 0,
265
+ left: "a".into(),
266
+ right: "b".into()
267
+ }
268
+ );
269
+ }
270
+ _ => panic!(),
271
+ }
272
+ // a beats b; survivors = [a, c (bye)]
273
+ match t.feed_round(ids(&["a"])).unwrap() {
274
+ TournamentAction::JudgeRound { round, matches } => {
275
+ assert_eq!(round, 2);
276
+ assert_eq!(
277
+ matches[0],
278
+ Match {
279
+ id: 0,
280
+ left: "a".into(),
281
+ right: "c".into()
282
+ }
283
+ );
284
+ }
285
+ _ => panic!(),
286
+ }
287
+ match t.feed_round(ids(&["c"])).unwrap() {
288
+ TournamentAction::Done {
289
+ winner,
290
+ rounds_used,
291
+ } => {
292
+ assert_eq!(winner, "c");
293
+ assert_eq!(rounds_used, 2);
294
+ }
295
+ _ => panic!(),
296
+ }
297
+ }
298
+
299
+ #[test]
300
+ fn eight_entrants_three_rounds() {
301
+ let mut t = Tournament::new(ids(&["1", "2", "3", "4", "5", "6", "7", "8"])).unwrap();
302
+ let mut action = t.start();
303
+ let mut last_round = 0;
304
+ loop {
305
+ match action {
306
+ TournamentAction::JudgeRound { round, matches } => {
307
+ last_round = round;
308
+ // winners = the left entrant of each match (deterministic)
309
+ let winners: Vec<EntrantId> = matches.iter().map(|m| m.left.clone()).collect();
310
+ action = t.feed_round(winners).unwrap();
311
+ }
312
+ TournamentAction::Done {
313
+ winner,
314
+ rounds_used,
315
+ } => {
316
+ assert_eq!(winner, "1");
317
+ assert_eq!(rounds_used, 3);
318
+ assert_eq!(last_round, 3);
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ #[test]
326
+ fn wrong_winner_count_is_error() {
327
+ let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
328
+ t.start();
329
+ // round 1 has 2 matches; feeding 1 winner is invalid
330
+ assert!(t.feed_round(ids(&["a"])).is_err());
331
+ }
332
+
333
+ #[test]
334
+ fn winner_not_in_match_is_error() {
335
+ let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
336
+ t.start();
337
+ assert!(t.feed_round(ids(&["zzz"])).is_err());
338
+ }
339
+
340
+ #[test]
341
+ fn feed_after_done_is_error() {
342
+ let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
343
+ t.start();
344
+ t.feed_round(ids(&["a"])).unwrap();
345
+ assert!(t.feed_round(ids(&["a"])).is_err());
346
+ }
347
+ }