deepstrike 0.2.16__tar.gz → 0.2.17__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 (168) hide show
  1. {deepstrike-0.2.16 → deepstrike-0.2.17}/Cargo.lock +5 -5
  2. {deepstrike-0.2.16 → deepstrike-0.2.17}/Cargo.toml +2 -2
  3. {deepstrike-0.2.16 → deepstrike-0.2.17}/PKG-INFO +1 -1
  4. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/orchestration/workflow/mod.rs +13 -0
  5. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/orchestration/workflow/run.rs +225 -0
  6. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/event_log.rs +2 -1
  7. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/kernel.rs +96 -0
  8. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/capability.rs +151 -0
  9. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/eviction.rs +146 -0
  10. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/gate.rs +351 -0
  11. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/milestone_exec.rs +153 -0
  12. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/mod.rs +872 -0
  13. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/process.rs +195 -0
  14. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/signal.rs +165 -0
  15. deepstrike-0.2.16/crates/deepstrike-core/src/scheduler/state_machine_tests.rs → deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/tests.rs +175 -0
  16. deepstrike-0.2.17/crates/deepstrike-core/src/scheduler/state_machine/workflow.rs +352 -0
  17. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/scheduler/tcb.rs +11 -0
  18. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/syscall/mod.rs +7 -4
  19. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/__init__.py +20 -2
  20. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/__init__.py +24 -0
  21. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/execution_plane.py +5 -1
  22. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/process_sandbox_plane.py +12 -7
  23. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/runner.py +289 -43
  24. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/sub_agent_orchestrator.py +65 -18
  25. deepstrike-0.2.17/deepstrike/runtime/workflow_control_flow.py +94 -0
  26. deepstrike-0.2.17/deepstrike/runtime/workflow_store.py +56 -0
  27. deepstrike-0.2.17/deepstrike/runtime/worktree_plane.py +101 -0
  28. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/tools/registry.py +12 -2
  29. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/types/agent.py +207 -36
  30. {deepstrike-0.2.16 → deepstrike-0.2.17}/pyproject.toml +1 -1
  31. deepstrike-0.2.16/crates/deepstrike-core/src/scheduler/state_machine.rs +0 -2190
  32. {deepstrike-0.2.16 → deepstrike-0.2.17}/README.md +0 -0
  33. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/Cargo.toml +0 -0
  34. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/compression.rs +0 -0
  35. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/config.rs +0 -0
  36. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  37. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/manager.rs +0 -0
  38. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/mod.rs +0 -0
  39. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  40. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/pressure.rs +0 -0
  41. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/renderer.rs +0 -0
  42. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  43. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/sections.rs +0 -0
  44. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  45. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
  46. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  47. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/task_state.rs +0 -0
  48. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/text.rs +0 -0
  49. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  50. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  51. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  52. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  53. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  54. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  55. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  56. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  57. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  58. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  59. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  60. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/harness/eval_pipeline.rs +0 -0
  61. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  62. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/lib.rs +0 -0
  63. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  64. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  65. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  66. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  67. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  68. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  69. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  70. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/session.rs +0 -0
  71. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  72. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  73. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/memory/working.rs +0 -0
  74. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/mm/handle.rs +0 -0
  75. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  76. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  77. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/orchestration/mod.rs +0 -0
  78. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  79. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
  80. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/proc/mod.rs +0 -0
  81. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
  82. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  83. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  84. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/session.rs +0 -0
  85. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/runtime/snapshot.rs +0 -0
  86. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  87. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/scheduler/mod.rs +0 -0
  88. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  89. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  90. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  91. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  92. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  93. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/signals/router.rs +0 -0
  94. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/agent.rs +0 -0
  95. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/capability.rs +0 -0
  96. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/contract.rs +0 -0
  97. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/error.rs +0 -0
  98. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/message.rs +0 -0
  99. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  100. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/mod.rs +0 -0
  101. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/model.rs +0 -0
  102. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/policy.rs +0 -0
  103. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/result.rs +0 -0
  104. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/signal.rs +0 -0
  105. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/skill.rs +0 -0
  106. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-core/src/types/task.rs +0 -0
  107. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-py/Cargo.toml +0 -0
  108. {deepstrike-0.2.16 → deepstrike-0.2.17}/crates/deepstrike-py/src/lib.rs +0 -0
  109. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/collaboration/__init__.py +0 -0
  110. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/collaboration/contract.py +0 -0
  111. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/collaboration/handoff.py +0 -0
  112. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/collaboration/harness.py +0 -0
  113. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/collaboration/modes.py +0 -0
  114. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/collaboration/pool.py +0 -0
  115. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/governance.py +0 -0
  116. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/harness/__init__.py +0 -0
  117. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/harness/harness.py +0 -0
  118. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/kernel/__init__.py +0 -0
  119. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/knowledge/__init__.py +0 -0
  120. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/knowledge/source.py +0 -0
  121. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/memory/__init__.py +0 -0
  122. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/memory/agent.py +0 -0
  123. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/memory/protocols.py +0 -0
  124. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/memory/working.py +0 -0
  125. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/__init__.py +0 -0
  126. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/anthropic.py +0 -0
  127. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/base.py +0 -0
  128. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/deepseek.py +0 -0
  129. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/gemini.py +0 -0
  130. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/glm.py +0 -0
  131. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/kimi.py +0 -0
  132. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/minimax.py +0 -0
  133. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/ollama.py +0 -0
  134. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/openai.py +0 -0
  135. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/qwen.py +0 -0
  136. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/replay.py +0 -0
  137. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/replay_validator.py +0 -0
  138. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/providers/stream.py +0 -0
  139. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/archive.py +0 -0
  140. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/credential_vault.py +0 -0
  141. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/filtered_plane.py +0 -0
  142. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/kernel_event_log.py +0 -0
  143. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/kernel_step.py +0 -0
  144. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/large_result_spool.py +0 -0
  145. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  146. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/os_profile.py +0 -0
  147. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/os_snapshot.py +0 -0
  148. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/output_schema.py +0 -0
  149. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/provider_replay.py +0 -0
  150. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/reducers.py +0 -0
  151. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  152. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/replay_sanitize.py +0 -0
  153. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/session_log.py +0 -0
  154. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/runtime/session_repair.py +0 -0
  155. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/safety/__init__.py +0 -0
  156. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/safety/permissions.py +0 -0
  157. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/signals/__init__.py +0 -0
  158. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/signals/gateway.py +0 -0
  159. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/signals/scheduled.py +0 -0
  160. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/signals/types.py +0 -0
  161. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/skills/__init__.py +0 -0
  162. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/skills/registry.py +0 -0
  163. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/skills/watcher.py +0 -0
  164. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/tools/__init__.py +0 -0
  165. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/tools/builtin/__init__.py +0 -0
  166. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/tools/builtin/read_file.py +0 -0
  167. {deepstrike-0.2.16 → deepstrike-0.2.17}/deepstrike/tools/execution.py +0 -0
  168. {deepstrike-0.2.16 → deepstrike-0.2.17}/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.16"
197
+ version = "0.2.17"
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.16"
209
+ version = "0.2.17"
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.16"
221
+ version = "0.2.17"
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.16"
232
+ version = "0.2.17"
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.16"
266
+ version = "0.2.17"
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.16"
6
+ version = "0.2.17"
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.16" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.17" }
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.16
3
+ Version: 0.2.17
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -102,6 +102,12 @@ pub struct WorkflowNode {
102
102
  /// Control-flow kind. Default `Spawn` (run once).
103
103
  #[serde(default, skip_serializing_if = "is_spawn")]
104
104
  pub kind: NodeKind,
105
+ /// M4/G5: optional per-node cumulative token cap. The kernel carries it to the spawn descriptor;
106
+ /// the SDK sets the node's child-run `max_total_tokens` to it, so an expensive node self-terminates
107
+ /// at the cap (the "use N tokens" budget, applied per node). Additive: omitted on the wire when
108
+ /// `None`.
109
+ #[serde(default, skip_serializing_if = "Option::is_none")]
110
+ pub token_budget: Option<u64>,
105
111
  /// Indices into [`WorkflowSpec::nodes`] this node depends on.
106
112
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
107
113
  pub depends_on: Vec<usize>,
@@ -128,10 +134,17 @@ impl WorkflowNode {
128
134
  trust: NodeTrust::Trusted,
129
135
  output_schema: None,
130
136
  kind: NodeKind::Spawn,
137
+ token_budget: None,
131
138
  depends_on: Vec::new(),
132
139
  }
133
140
  }
134
141
 
142
+ /// M4/G5: cap this node's child run at `tokens` cumulative tokens.
143
+ pub fn with_token_budget(mut self, tokens: u64) -> Self {
144
+ self.token_budget = Some(tokens);
145
+ self
146
+ }
147
+
135
148
  /// Make this a loop node: re-run the agent up to `max_iters` times before completing.
136
149
  /// Dependents wait for the whole loop to finish.
137
150
  pub fn with_loop(mut self, max_iters: usize) -> Self {
@@ -62,6 +62,24 @@ pub struct WorkflowSpawnInfo {
62
62
  /// (entrant / spawn / loop / classify) node. Additive ABI: omitted on the wire when `None`.
63
63
  #[serde(default, skip_serializing_if = "Option::is_none")]
64
64
  pub judge_match: Option<JudgeMatch>,
65
+ /// Present only for a [`NodeKind::Loop`] iteration spawn (A#2 v2): the loop's `max_iters`. It
66
+ /// both *marks* the spawn as a loop iteration — so the SDK knows to solicit and report a
67
+ /// `loop_continue` stop signal from the agent — and gives the cap for the agent's prompt. `None`
68
+ /// for every non-loop node. Mirrors how `reducer` / `judge_match` distinguish reduce / judge
69
+ /// spawns. Additive ABI: omitted on the wire when `None`.
70
+ #[serde(default, skip_serializing_if = "Option::is_none")]
71
+ pub loop_max_iters: Option<usize>,
72
+ /// Present only for a [`NodeKind::Classify`] spawn (A#2): the branch labels the classifier must
73
+ /// choose among. Non-empty *marks* the spawn as a classifier — the SDK instructs the agent to
74
+ /// pick exactly one label and reports it in the result's `classify_branch`. Empty for every
75
+ /// non-classify node. Additive ABI: omitted on the wire when empty.
76
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
77
+ pub classify_labels: Vec<String>,
78
+ /// M4/G5: the node's per-node cumulative token cap, if set. The SDK sets the child run's
79
+ /// `max_total_tokens` to this so the node self-terminates at the cap. Additive ABI: omitted when
80
+ /// `None`.
81
+ #[serde(default, skip_serializing_if = "Option::is_none")]
82
+ pub token_budget: Option<u64>,
65
83
  }
66
84
 
67
85
  fn default_trust() -> String {
@@ -102,6 +120,18 @@ pub struct WorkflowBudget {
102
120
  /// how many of a submission's nodes can spawn *immediately* rather than deferring for a slot.
103
121
  #[serde(default, skip_serializing_if = "Option::is_none")]
104
122
  pub concurrency_remaining: Option<usize>,
123
+ /// M4/G5: cumulative tokens spent across the run so far (the scheduler's `total_tokens`).
124
+ /// `#[serde(default)]` keeps older JSON (without this field) deserializing to 0 — additive ABI.
125
+ #[serde(default)]
126
+ pub tokens_used: u64,
127
+ /// M4/G5: `SchedulerBudget::max_total_tokens` — the run's cumulative token cap.
128
+ #[serde(default, skip_serializing_if = "Option::is_none")]
129
+ pub tokens_max: Option<u64>,
130
+ /// M4/G5: `tokens_max - tokens_used` (saturating) — how many tokens remain before the run-level
131
+ /// token budget terminates the workflow. Lets a coordinator scale its next submission to token
132
+ /// headroom (the analogue of "use 10k tokens").
133
+ #[serde(default, skip_serializing_if = "Option::is_none")]
134
+ pub tokens_remaining: Option<u64>,
105
135
  }
106
136
 
107
137
  fn role_label(role: AgentRole) -> &'static str {
@@ -307,6 +337,17 @@ impl WorkflowRun {
307
337
  ),
308
338
  _ => (None, Vec::new()),
309
339
  };
340
+ // A#2 v2 / classify: surface the control-flow kind so the SDK can solicit + report the
341
+ // matching result signal (`loop_continue` / `classify_branch`), mirroring how `reducer` /
342
+ // `judge_match` distinguish reduce / judge spawns.
343
+ let loop_max_iters = match &n.kind {
344
+ NodeKind::Loop { max_iters } => Some(*max_iters),
345
+ _ => None,
346
+ };
347
+ let classify_labels = match &n.kind {
348
+ NodeKind::Classify { branches } => branches.iter().map(|b| b.label.clone()).collect(),
349
+ _ => Vec::new(),
350
+ };
310
351
  WorkflowSpawnInfo {
311
352
  agent_id: self.current_agent_id(node),
312
353
  goal: n.task.goal.clone(),
@@ -319,6 +360,9 @@ impl WorkflowRun {
319
360
  reducer,
320
361
  input_agent_ids,
321
362
  judge_match: self.judge_matches.get(&node).cloned(),
363
+ loop_max_iters,
364
+ classify_labels,
365
+ token_budget: n.token_budget,
322
366
  }
323
367
  }
324
368
 
@@ -640,6 +684,7 @@ impl WorkflowRun {
640
684
 
641
685
  pub fn submit_nodes(&mut self, nodes: Vec<WorkflowNode>) -> Vec<usize> {
642
686
  let base = self.nodes.len();
687
+ let batch_len = nodes.len();
643
688
  let mut ids = Vec::with_capacity(nodes.len());
644
689
  for (offset, mut node) in nodes.into_iter().enumerate() {
645
690
  let deps: Vec<usize> = node
@@ -649,6 +694,21 @@ impl WorkflowRun {
649
694
  .map(|&d| base + d)
650
695
  .collect();
651
696
  node.depends_on = deps.clone();
697
+ // A#2/G2: a submitted `Classify` node's branch indices are *batch-relative* — they point
698
+ // at other nodes in this same submission, whose absolute graph index the submitter cannot
699
+ // know. Remap each branch node index `d` (0-based within the batch) to its absolute index
700
+ // `base + d`, dropping out-of-range references. Mirrors the `depends_on` batch-relative
701
+ // convention; without it a runtime-submitted classifier would prune the wrong nodes.
702
+ if let NodeKind::Classify { branches } = &mut node.kind {
703
+ for branch in branches.iter_mut() {
704
+ branch.nodes = branch
705
+ .nodes
706
+ .iter()
707
+ .filter(|&&d| d < batch_len)
708
+ .map(|&d| base + d)
709
+ .collect();
710
+ }
711
+ }
652
712
  let idx = self.graph.add(node.task.clone(), deps);
653
713
  debug_assert_eq!(idx, self.nodes.len(), "graph/nodes index drift");
654
714
  self.nodes.push(node);
@@ -702,6 +762,21 @@ impl WorkflowRun {
702
762
  (completed, failed)
703
763
  }
704
764
 
765
+ /// #2-B abort: outcome when the workflow is preempted — every node that has not already
766
+ /// `Completed` counts as `failed` (running / ready / pending all abort). Used to emit a terminal
767
+ /// `WorkflowCompleted` when an `InterruptNow` tears the whole `WorkflowRun` down.
768
+ pub fn abort_outcome(&self) -> (Vec<String>, Vec<String>) {
769
+ let mut completed = Vec::new();
770
+ let mut failed = Vec::new();
771
+ for i in 0..self.graph.len() {
772
+ match self.graph.get(i).map(|n| n.status) {
773
+ Some(TaskStatus::Completed) => completed.push(node_agent_id(i)),
774
+ _ => failed.push(node_agent_id(i)),
775
+ }
776
+ }
777
+ (completed, failed)
778
+ }
779
+
705
780
  /// Total node count.
706
781
  pub fn len(&self) -> usize {
707
782
  self.graph.len()
@@ -1018,6 +1093,110 @@ mod tests {
1018
1093
  assert!(run.is_complete(), "submitted loop ran its 2 iterations then finished");
1019
1094
  }
1020
1095
 
1096
+ #[test]
1097
+ fn submitted_tournament_runs_bracket_then_promotes_submitted_dependent() {
1098
+ // M2: an agent can submit a Tournament *controller* (plus a dependent) at runtime. The
1099
+ // controller expands into entrant children + a judge via the same bracket machinery, and the
1100
+ // dependent's batch-relative `depends_on` links it to the submitted controller.
1101
+ use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
1102
+ use crate::types::agent::AgentRole;
1103
+
1104
+ let spec = WorkflowSpec::new(vec![WorkflowNode::new(
1105
+ RuntimeTask::new("root"),
1106
+ AgentRole::Implement,
1107
+ )]);
1108
+ let mut run = WorkflowRun::new(&spec, "sess").unwrap();
1109
+ let id0 = run.current_agent_id(0);
1110
+ run.mark_spawned(0, &id0);
1111
+ run.record_completion(&id0, done());
1112
+
1113
+ // Submit [tournament@batch0, dependent@batch1 depends_on [0]] (batch-relative).
1114
+ let ids = run.submit_nodes(vec![
1115
+ WorkflowNode::new(RuntimeTask::new("pick best"), AgentRole::Plan)
1116
+ .with_tournament(vec![RuntimeTask::new("x"), RuntimeTask::new("y")]),
1117
+ WorkflowNode::new(RuntimeTask::new("use winner"), AgentRole::Implement)
1118
+ .with_depends_on(vec![0]),
1119
+ ]);
1120
+ assert_eq!(ids, vec![1, 2], "appended controller=1, dependent=2");
1121
+
1122
+ // Controller (node 1) expands into 2 entrant children (3,4); spawns no agent of its own.
1123
+ let entrants = spawn_round(&mut run);
1124
+ let entrant_nodes: Vec<usize> = entrants.iter().map(|(n, _)| *n).collect();
1125
+ assert_eq!(entrant_nodes, vec![3, 4], "two entrant children appended after the dependent");
1126
+ for (_, id) in &entrants {
1127
+ run.record_completion(id, done());
1128
+ }
1129
+
1130
+ // One judge over the two entrants; dependent (node 2) gated until the bracket resolves.
1131
+ let r1 = spawn_round(&mut run);
1132
+ assert_eq!(r1.len(), 1, "one judge for two entrants");
1133
+ let jm = run.spawn_info(r1[0].0).judge_match.expect("judge carries a match");
1134
+ assert_eq!(jm, JudgeMatch { left: node_agent_id(3), right: node_agent_id(4) });
1135
+
1136
+ // Entrant 3 wins → controller completes with the champion → dependent unblocks.
1137
+ run.record_completion(&r1[0].1, judge_done(&node_agent_id(3)));
1138
+ assert_eq!(run.ready_batch(), vec![2], "submitted dependent unblocks after the bracket");
1139
+ let last = spawn_round(&mut run);
1140
+ assert_eq!(last, vec![(2, node_agent_id(2))]);
1141
+ run.record_completion(&last[0].1, done());
1142
+ assert!(run.is_complete());
1143
+ }
1144
+
1145
+ #[test]
1146
+ fn submitted_classify_remaps_branch_indices_and_prunes() {
1147
+ // M2: a submitted Classify node's branch `nodes` are batch-relative; `submit_nodes` remaps
1148
+ // them to absolute indices so the chosen branch runs and the rest are pruned. Without the
1149
+ // remap a runtime-submitted classifier would prune the wrong nodes.
1150
+ use crate::orchestration::workflow::{ClassifyBranch, NodeKind, WorkflowNode, WorkflowSpec};
1151
+ use crate::types::agent::AgentRole;
1152
+
1153
+ let spec = WorkflowSpec::new(vec![WorkflowNode::new(
1154
+ RuntimeTask::new("root"),
1155
+ AgentRole::Implement,
1156
+ )]);
1157
+ let mut run = WorkflowRun::new(&spec, "sess").unwrap();
1158
+ let id0 = run.current_agent_id(0);
1159
+ run.mark_spawned(0, &id0);
1160
+ run.record_completion(&id0, done());
1161
+
1162
+ // Submit [classify@batch0 (a→[1] b→[2]), branchA@batch1 dep[0], branchB@batch2 dep[0]].
1163
+ let ids = run.submit_nodes(vec![
1164
+ WorkflowNode::new(RuntimeTask::new("route"), AgentRole::Plan).with_classify(vec![
1165
+ ClassifyBranch { label: "a".into(), nodes: vec![1] },
1166
+ ClassifyBranch { label: "b".into(), nodes: vec![2] },
1167
+ ]),
1168
+ WorkflowNode::new(RuntimeTask::new("branch-a"), AgentRole::Implement)
1169
+ .with_depends_on(vec![0]),
1170
+ WorkflowNode::new(RuntimeTask::new("branch-b"), AgentRole::Implement)
1171
+ .with_depends_on(vec![0]),
1172
+ ]);
1173
+ assert_eq!(ids, vec![1, 2, 3], "classify=1, branchA=2, branchB=3");
1174
+
1175
+ // Branch indices were remapped batch-relative → absolute: a→[2], b→[3].
1176
+ if let NodeKind::Classify { branches } = &run.nodes[1].kind {
1177
+ assert_eq!(branches[0].nodes, vec![2], "branch a remapped to absolute node 2");
1178
+ assert_eq!(branches[1].nodes, vec![3], "branch b remapped to absolute node 3");
1179
+ } else {
1180
+ panic!("node 1 should be a classify node");
1181
+ }
1182
+
1183
+ // Classifier picks "a" → branch-a (node 2) runs, branch-b (node 3) is pruned/failed.
1184
+ let r = spawn_round(&mut run);
1185
+ assert_eq!(r, vec![(1, node_agent_id(1))], "classifier runs first");
1186
+ run.record_completion(&r[0].1, LoopResult { classify_branch: Some("a".into()), ..done() });
1187
+
1188
+ assert_eq!(run.ready_batch(), vec![2], "only branch a is enabled");
1189
+ let (_c, failed) = run.outcome();
1190
+ assert!(failed.contains(&node_agent_id(3)), "branch b pruned/failed");
1191
+
1192
+ let last = spawn_round(&mut run);
1193
+ assert_eq!(last, vec![(2, node_agent_id(2))]);
1194
+ run.record_completion(&last[0].1, done());
1195
+ assert!(run.is_complete());
1196
+ let (completed, _f) = run.outcome();
1197
+ assert!(completed.contains(&node_agent_id(1)) && completed.contains(&node_agent_id(2)));
1198
+ }
1199
+
1021
1200
  #[test]
1022
1201
  fn loop_node_iterates_with_distinct_ids_then_promotes_dependent() {
1023
1202
  use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
@@ -1185,6 +1364,52 @@ mod tests {
1185
1364
  assert_eq!(t.model_hint, None);
1186
1365
  }
1187
1366
 
1367
+ #[test]
1368
+ fn spawn_info_carries_loop_and_classify_hints() {
1369
+ use crate::orchestration::workflow::{ClassifyBranch, WorkflowNode, WorkflowSpec};
1370
+ use crate::types::agent::AgentRole;
1371
+
1372
+ let spec = WorkflowSpec::new(vec![
1373
+ // 0: loop node → descriptor carries the cap so the SDK knows to solicit `loop_continue`.
1374
+ WorkflowNode::new(RuntimeTask::new("refine"), AgentRole::Implement).with_loop(3),
1375
+ // 1: classify node → descriptor carries the branch labels so the SDK can instruct + report.
1376
+ WorkflowNode::new(RuntimeTask::new("route"), AgentRole::Plan).with_classify(vec![
1377
+ ClassifyBranch { label: "bug".into(), nodes: vec![] },
1378
+ ClassifyBranch { label: "feature".into(), nodes: vec![] },
1379
+ ]),
1380
+ // 2: plain spawn → neither hint present.
1381
+ WorkflowNode::new(RuntimeTask::new("act"), AgentRole::Implement),
1382
+ ]);
1383
+ let run = WorkflowRun::new(&spec, "sess").unwrap();
1384
+
1385
+ let l = run.spawn_info(0);
1386
+ assert_eq!(l.loop_max_iters, Some(3));
1387
+ assert!(l.classify_labels.is_empty());
1388
+ assert_eq!(l.token_budget, None, "no token budget unless set");
1389
+
1390
+ let c = run.spawn_info(1);
1391
+ assert_eq!(c.classify_labels, vec!["bug".to_string(), "feature".to_string()]);
1392
+ assert_eq!(c.loop_max_iters, None);
1393
+
1394
+ let s = run.spawn_info(2);
1395
+ assert_eq!(s.loop_max_iters, None);
1396
+ assert!(s.classify_labels.is_empty());
1397
+ }
1398
+
1399
+ #[test]
1400
+ fn spawn_info_carries_token_budget() {
1401
+ use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
1402
+ use crate::types::agent::AgentRole;
1403
+
1404
+ let spec = WorkflowSpec::new(vec![
1405
+ WorkflowNode::new(RuntimeTask::new("expensive"), AgentRole::Implement).with_token_budget(10_000),
1406
+ WorkflowNode::new(RuntimeTask::new("plain"), AgentRole::Implement),
1407
+ ]);
1408
+ let run = WorkflowRun::new(&spec, "sess").unwrap();
1409
+ assert_eq!(run.spawn_info(0).token_budget, Some(10_000));
1410
+ assert_eq!(run.spawn_info(1).token_budget, None);
1411
+ }
1412
+
1188
1413
  // ── Tournament node (A#2) ───────────────────────────────────────────────────────────────────
1189
1414
 
1190
1415
  use crate::orchestration::workflow::{NodeKind, WorkflowNode, WorkflowSpec};
@@ -88,7 +88,7 @@ pub fn category_for_kind(kind: &str) -> KernelEventCategory {
88
88
  | "context_renewed"
89
89
  | "large_result_spooled" => KernelEventCategory::Mm,
90
90
  "agent_process_changed" | "agent_spawned" | "workflow_batch_spawned"
91
- | "workflow_completed" => KernelEventCategory::Proc,
91
+ | "workflow_completed" | "agent_preempted" => KernelEventCategory::Proc,
92
92
  "signal_disposed" => KernelEventCategory::Ipc,
93
93
  "memory_written" | "memory_queried" | "memory_validation_failed" => KernelEventCategory::Mm,
94
94
  _ => KernelEventCategory::Sched,
@@ -110,6 +110,7 @@ pub const KERNEL_OBSERVATION_KINDS: &[&str] = &[
110
110
  "agent_process_changed",
111
111
  "workflow_batch_spawned",
112
112
  "workflow_completed",
113
+ "agent_preempted",
113
114
  "tool_gated",
114
115
  "signal_disposed",
115
116
  "budget_exceeded",
@@ -270,6 +270,21 @@ pub enum KernelInputEvent {
270
270
  #[serde(default, skip_serializing_if = "Option::is_none")]
271
271
  submitter_agent_id: Option<String>,
272
272
  },
273
+ /// M5/G1: an agent authors a whole `WorkflowSpec` (the article's "model writes its own harness").
274
+ /// The agent-reachable analogue of the host-only `LoadWorkflow`: **bootstraps** the DAG when no
275
+ /// workflow is active, else **flattens** the spec's nodes onto the running DAG (bootstrap-or-flatten,
276
+ /// one kernel / one quota — never a workflow stack). Gated by `Syscall::LoadWorkflow`. Additive ABI:
277
+ /// a brand-new variant, byte-identical on the wire for SDKs that never send it.
278
+ SubmitWorkflow {
279
+ spec: crate::orchestration::workflow::WorkflowSpec,
280
+ /// Used only on bootstrap (no workflow active) to seed child session ids; ignored on flatten.
281
+ #[serde(default)]
282
+ parent_session_id: String,
283
+ /// G1: the authoring node's agent id (flatten case) — a quarantined author's nodes are coerced
284
+ /// quarantined. Additive: omitted (top-level bootstrap) → `None` → the run's own trust applies.
285
+ #[serde(default, skip_serializing_if = "Option::is_none")]
286
+ submitter_agent_id: Option<String>,
287
+ },
273
288
  /// Feed long-term memory entries into the knowledge partition (page-in).
274
289
  /// SDK performs retrieval I/O; kernel only applies the result.
275
290
  PageIn {
@@ -480,6 +495,17 @@ pub enum KernelObservation {
480
495
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
481
496
  failed: Vec<String>,
482
497
  },
498
+ /// #2-B: a high-urgency `InterruptNow` signal preempted in-flight work. The kernel has already
499
+ /// marked these agents `Done(UserAbort)` and reclaimed the root to reason about the interrupt; the
500
+ /// SDK must ABORT the listed in-flight child runs and discard their results (do NOT feed their
501
+ /// `SubAgentCompleted`). Additive variant (`agent_preempted`) — byte-identical for SDKs that never
502
+ /// receive it.
503
+ AgentPreempted {
504
+ turn: u32,
505
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
506
+ agent_ids: Vec<String>,
507
+ reason: String,
508
+ },
483
509
  /// A tool call needs user approval (governance `AskUser`). Not blocked by the
484
510
  /// kernel — the SDK must obtain approval before executing the named call.
485
511
  ToolGated {
@@ -866,6 +892,21 @@ impl KernelRuntime {
866
892
  }
867
893
  return KernelStep::single(action, self.sm.take_observations());
868
894
  }
895
+ KernelInputEvent::SubmitWorkflow {
896
+ spec,
897
+ parent_session_id,
898
+ submitter_agent_id,
899
+ } => {
900
+ let action = self.sm.submit_workflow(
901
+ spec,
902
+ &parent_session_id,
903
+ submitter_agent_id.as_deref(),
904
+ );
905
+ if matches!(action, LoopAction::AwaitingResume) {
906
+ return KernelStep::empty(self.sm.take_observations());
907
+ }
908
+ return KernelStep::single(action, self.sm.take_observations());
909
+ }
869
910
  KernelInputEvent::SetMemoryPolicy {
870
911
  memory_path,
871
912
  stale_warning_days,
@@ -1922,6 +1963,61 @@ mod tests {
1922
1963
  )));
1923
1964
  }
1924
1965
 
1966
+ #[test]
1967
+ fn submit_workflow_input_bootstraps_a_dag_over_the_abi() {
1968
+ // M5/G1: a top-level agent authors a whole spec over the ABI (full serde round-trip of
1969
+ // SubmitWorkflow + WorkflowSpec) with no workflow active → the kernel bootstraps and drives it.
1970
+ use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
1971
+ use crate::types::agent::AgentRole;
1972
+ use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
1973
+
1974
+ let mut runtime = KernelRuntime::new(LoopPolicy::default());
1975
+ runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1976
+ task: RuntimeTask::new("parent task"),
1977
+ run_spec: None,
1978
+ }));
1979
+ runtime.state_machine_mut().take_observations();
1980
+
1981
+ // No LoadWorkflow first — the agent itself authors the spec.
1982
+ let spec = WorkflowSpec::new(vec![WorkflowNode::new(
1983
+ RuntimeTask::new("authored root"),
1984
+ AgentRole::Implement,
1985
+ )]);
1986
+ let event = KernelInputEvent::SubmitWorkflow {
1987
+ spec,
1988
+ parent_session_id: "sess".to_string(),
1989
+ submitter_agent_id: None,
1990
+ };
1991
+ let json = serde_json::to_string(&event).expect("serialize");
1992
+ let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
1993
+ let step = runtime.step(KernelInput::new(parsed));
1994
+ // The authored node bootstraps as wf-node0 in a workflow batch.
1995
+ assert!(step.observations.iter().any(|o| matches!(
1996
+ o,
1997
+ KernelObservation::WorkflowBatchSpawned { nodes, .. }
1998
+ if nodes.len() == 1 && nodes[0].agent_id == "wf-node0" && nodes[0].goal == "authored root"
1999
+ )));
2000
+
2001
+ let step = runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2002
+ result: SubAgentResult {
2003
+ agent_id: compact_str::CompactString::new("wf-node0"),
2004
+ result: LoopResult {
2005
+ termination: TerminationReason::Completed,
2006
+ final_message: None,
2007
+ turns_used: 1,
2008
+ total_tokens_used: 1,
2009
+ loop_continue: None,
2010
+ classify_branch: None,
2011
+ tournament_winner: None,
2012
+ },
2013
+ },
2014
+ }));
2015
+ assert!(step.observations.iter().any(|o| matches!(
2016
+ o,
2017
+ KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 1
2018
+ )));
2019
+ }
2020
+
1925
2021
  #[test]
1926
2022
  fn load_workflow_resumes_from_completed_nodes() {
1927
2023
  use crate::orchestration::workflow::fanout_synthesize;
@@ -0,0 +1,151 @@
1
+ //! Capability management impl for [`super::LoopStateMachine`].
2
+
3
+ use super::{KernelObservation, LoopStateMachine};
4
+
5
+ impl LoopStateMachine {
6
+ /// Drop capability leases whose expiry turn has passed. Runs at the head of
7
+ /// every event so expired temporary capabilities are unmounted promptly.
8
+ pub(super) fn sweep_expired_leases(&mut self) {
9
+ let current_turn = self.turn;
10
+ let mut to_remove = Vec::new();
11
+ for cap in self.ctx.capabilities.capabilities() {
12
+ if let Some(ref lease) = cap.lease {
13
+ if current_turn >= lease.expires_at_turn {
14
+ to_remove.push((cap.kind, cap.id.to_string()));
15
+ }
16
+ }
17
+ }
18
+ for (kind, id) in to_remove {
19
+ self.unmount_capability(kind, &id);
20
+ }
21
+ }
22
+
23
+ /// Emit a `CapabilityChanged` observation for the current turn.
24
+ /// Single construction site for all mount/unmount/replace/pin changes.
25
+ #[allow(clippy::too_many_arguments)]
26
+ pub(super) fn push_capability_change(
27
+ &mut self,
28
+ added: Vec<String>,
29
+ removed: Vec<String>,
30
+ change_kind: &str,
31
+ capability_id: Option<String>,
32
+ version: Option<String>,
33
+ mounted_by: Option<String>,
34
+ mount_reason: Option<String>,
35
+ ) {
36
+ self.observations.push(KernelObservation::CapabilityChanged {
37
+ turn: self.turn,
38
+ added,
39
+ removed,
40
+ change_kind: Some(change_kind.to_string()),
41
+ capability_id,
42
+ version,
43
+ mounted_by,
44
+ mount_reason,
45
+ });
46
+ }
47
+
48
+ pub fn execute_capability_command(&mut self, cmd: crate::types::capability::CapabilityCommand) {
49
+ use crate::types::capability::CapabilityCommand;
50
+ match cmd {
51
+ CapabilityCommand::Mount {
52
+ capability,
53
+ mounted_by,
54
+ mount_reason,
55
+ } => {
56
+ self.mount_capability(capability, mounted_by, mount_reason);
57
+ }
58
+ CapabilityCommand::Unmount { kind, id } => {
59
+ self.unmount_capability(kind, &id);
60
+ }
61
+ CapabilityCommand::Replace {
62
+ old_kind,
63
+ old_id,
64
+ new_capability,
65
+ } => {
66
+ let new_id = new_capability.id.to_string();
67
+ let version = new_capability.version.clone();
68
+ let old_kind_str = old_kind.label();
69
+ let new_kind_str = new_capability.kind.label();
70
+
71
+ self.ctx.capabilities.remove(old_kind, &old_id);
72
+ self.ctx.capabilities.upsert(new_capability);
73
+
74
+ self.push_capability_change(
75
+ vec![format!("{}:{}", new_kind_str, new_id)],
76
+ vec![format!("{}:{}", old_kind_str, old_id)],
77
+ "replace",
78
+ Some(new_id),
79
+ version,
80
+ None,
81
+ None,
82
+ );
83
+ }
84
+ CapabilityCommand::Pin { kind, id } => {
85
+ let version = self
86
+ .ctx
87
+ .capabilities
88
+ .get_mut(kind, &id)
89
+ .and_then(|c| c.version.clone());
90
+ if let Some(cap) = self.ctx.capabilities.get_mut(kind, &id) {
91
+ cap.is_pinned = true;
92
+ self.push_capability_change(
93
+ vec![],
94
+ vec![],
95
+ "pin",
96
+ Some(id),
97
+ version,
98
+ None,
99
+ None,
100
+ );
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ pub fn mount_capability(
107
+ &mut self,
108
+ mut descriptor: crate::types::capability::CapabilityDescriptor,
109
+ mounted_by: Option<String>,
110
+ mount_reason: Option<String>,
111
+ ) {
112
+ if mounted_by.is_some() {
113
+ descriptor.mounted_by = mounted_by.clone();
114
+ }
115
+ if mount_reason.is_some() {
116
+ descriptor.mount_reason = mount_reason.clone();
117
+ }
118
+ let id = descriptor.id.to_string();
119
+ let kind_str = descriptor.kind.label();
120
+ let version = descriptor.version.clone();
121
+ self.ctx.capabilities.upsert(descriptor);
122
+ self.push_capability_change(
123
+ vec![format!("{}:{}", kind_str, id)],
124
+ vec![],
125
+ "mount",
126
+ Some(id),
127
+ version,
128
+ mounted_by,
129
+ mount_reason,
130
+ );
131
+ }
132
+
133
+ pub fn unmount_capability(&mut self, kind: crate::types::capability::CapabilityKind, id: &str) {
134
+ let version = self
135
+ .ctx
136
+ .capabilities
137
+ .get_mut(kind, id)
138
+ .and_then(|c| c.version.clone());
139
+ self.ctx.capabilities.remove(kind, id);
140
+ let kind_str = kind.label();
141
+ self.push_capability_change(
142
+ vec![],
143
+ vec![format!("{}:{}", kind_str, id)],
144
+ "unmount",
145
+ Some(id.to_string()),
146
+ version,
147
+ None,
148
+ None,
149
+ );
150
+ }
151
+ }