deepstrike 0.2.10__tar.gz → 0.2.12__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 (161) hide show
  1. {deepstrike-0.2.10 → deepstrike-0.2.12}/Cargo.lock +5 -5
  2. {deepstrike-0.2.10 → deepstrike-0.2.12}/Cargo.toml +2 -2
  3. {deepstrike-0.2.10 → deepstrike-0.2.12}/PKG-INFO +39 -1
  4. {deepstrike-0.2.10 → deepstrike-0.2.12}/README.md +38 -0
  5. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/manager.rs +164 -20
  6. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/pressure.rs +8 -1
  7. deepstrike-0.2.12/crates/deepstrike-core/src/context/renderer.rs +668 -0
  8. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/snapshot.rs +4 -1
  9. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/quota.rs +6 -0
  10. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/harness/eval_pipeline.rs +9 -0
  11. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/mm/handle.rs +9 -0
  12. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/orchestration/mod.rs +0 -3
  13. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/orchestration/tournament.rs +3 -3
  14. deepstrike-0.2.10/crates/deepstrike-core/src/orchestration/workflow.rs → deepstrike-0.2.12/crates/deepstrike-core/src/orchestration/workflow/mod.rs +36 -0
  15. deepstrike-0.2.10/crates/deepstrike-core/src/scheduler/workflow_run.rs → deepstrike-0.2.12/crates/deepstrike-core/src/orchestration/workflow/run.rs +442 -15
  16. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/kernel.rs +122 -4
  17. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/mod.rs +0 -1
  18. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/state_machine.rs +112 -8
  19. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/state_machine_tests.rs +178 -1
  20. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/syscall/mod.rs +5 -0
  21. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/task.rs +1 -1
  22. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-py/src/lib.rs +10 -0
  23. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/__init__.py +8 -0
  24. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/harness/harness.py +11 -0
  25. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/__init__.py +7 -5
  26. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/anthropic.py +138 -15
  27. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/base.py +86 -1
  28. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/deepseek.py +21 -0
  29. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/gemini.py +33 -5
  30. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/glm.py +21 -0
  31. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/kimi.py +21 -0
  32. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/minimax.py +8 -2
  33. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/ollama.py +2 -2
  34. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/openai.py +21 -2
  35. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/qwen.py +42 -2
  36. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/replay.py +2 -0
  37. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/stream.py +16 -0
  38. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/__init__.py +3 -0
  39. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/kernel_step.py +8 -0
  40. deepstrike-0.2.12/deepstrike/runtime/output_schema.py +123 -0
  41. deepstrike-0.2.12/deepstrike/runtime/reducers.py +72 -0
  42. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/runner.py +232 -27
  43. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/session_repair.py +17 -0
  44. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/sub_agent_orchestrator.py +18 -3
  45. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/types/agent.py +127 -22
  46. {deepstrike-0.2.10 → deepstrike-0.2.12}/pyproject.toml +1 -1
  47. deepstrike-0.2.10/crates/deepstrike-core/src/context/renderer.rs +0 -383
  48. deepstrike-0.2.10/crates/deepstrike-core/src/orchestration/executor.rs +0 -45
  49. deepstrike-0.2.10/crates/deepstrike-core/src/orchestration/gen_eval.rs +0 -153
  50. deepstrike-0.2.10/crates/deepstrike-core/src/orchestration/planner.rs +0 -47
  51. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/Cargo.toml +0 -0
  52. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/compression.rs +0 -0
  53. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/config.rs +0 -0
  54. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  55. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/mod.rs +0 -0
  56. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  57. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  58. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/sections.rs +0 -0
  59. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  60. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  61. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/task_state.rs +0 -0
  62. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/text.rs +0 -0
  63. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  64. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  65. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  66. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  67. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  68. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  69. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  70. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  71. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  72. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  73. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  74. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/lib.rs +0 -0
  75. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  76. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  77. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  78. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  79. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  80. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  81. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  82. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/session.rs +0 -0
  83. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  84. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  85. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/memory/working.rs +0 -0
  86. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  87. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  88. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  89. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/proc/mod.rs +0 -0
  90. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
  91. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
  92. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  93. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  94. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/session.rs +0 -0
  95. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/runtime/snapshot.rs +0 -0
  96. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  97. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  98. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  99. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
  100. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  101. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  102. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  103. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/signals/router.rs +0 -0
  104. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/agent.rs +0 -0
  105. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/capability.rs +0 -0
  106. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/contract.rs +0 -0
  107. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/error.rs +0 -0
  108. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/message.rs +0 -0
  109. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  110. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/mod.rs +0 -0
  111. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/model.rs +0 -0
  112. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/policy.rs +0 -0
  113. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/result.rs +0 -0
  114. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/signal.rs +0 -0
  115. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-core/src/types/skill.rs +0 -0
  116. {deepstrike-0.2.10 → deepstrike-0.2.12}/crates/deepstrike-py/Cargo.toml +0 -0
  117. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/collaboration/__init__.py +0 -0
  118. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/collaboration/contract.py +0 -0
  119. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/collaboration/handoff.py +0 -0
  120. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/collaboration/harness.py +0 -0
  121. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/collaboration/modes.py +0 -0
  122. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/collaboration/pool.py +0 -0
  123. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/governance.py +0 -0
  124. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/harness/__init__.py +0 -0
  125. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/kernel/__init__.py +0 -0
  126. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/knowledge/__init__.py +0 -0
  127. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/knowledge/source.py +0 -0
  128. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/memory/__init__.py +0 -0
  129. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/memory/agent.py +0 -0
  130. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/memory/protocols.py +0 -0
  131. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/memory/working.py +0 -0
  132. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/providers/replay_validator.py +0 -0
  133. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/archive.py +0 -0
  134. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/credential_vault.py +0 -0
  135. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/execution_plane.py +0 -0
  136. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/filtered_plane.py +0 -0
  137. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/kernel_event_log.py +0 -0
  138. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/large_result_spool.py +0 -0
  139. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  140. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/os_profile.py +0 -0
  141. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/os_snapshot.py +0 -0
  142. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  143. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/provider_replay.py +0 -0
  144. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  145. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/replay_sanitize.py +0 -0
  146. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/runtime/session_log.py +0 -0
  147. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/safety/__init__.py +0 -0
  148. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/safety/permissions.py +0 -0
  149. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/signals/__init__.py +0 -0
  150. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/signals/gateway.py +0 -0
  151. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/signals/scheduled.py +0 -0
  152. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/signals/types.py +0 -0
  153. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/skills/__init__.py +0 -0
  154. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/skills/registry.py +0 -0
  155. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/skills/watcher.py +0 -0
  156. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/tools/__init__.py +0 -0
  157. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/tools/builtin/__init__.py +0 -0
  158. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/tools/builtin/read_file.py +0 -0
  159. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/tools/execution.py +0 -0
  160. {deepstrike-0.2.10 → deepstrike-0.2.12}/deepstrike/tools/registry.py +0 -0
  161. {deepstrike-0.2.10 → deepstrike-0.2.12}/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.10"
197
+ version = "0.2.12"
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.10"
209
+ version = "0.2.12"
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.10"
221
+ version = "0.2.12"
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.10"
232
+ version = "0.2.12"
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.10"
266
+ version = "0.2.12"
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.10"
6
+ version = "0.2.12"
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.10" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.12" }
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.10
3
+ Version: 0.2.12
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -150,6 +150,44 @@ Spool, page-out, signals, processes, budgets, and memory events land in `Session
150
150
 
151
151
  ---
152
152
 
153
+ ## Dynamic workflows
154
+
155
+ Instead of planning **and** executing a hard task in one long context window, hand the kernel a declarative DAG and let it spawn a fresh-context sub-agent per node. The kernel owns the control flow (gate · budget · suspend-on-join · resume); your SDK runs the agents. See the [top-level overview](../README.md#the-six-harness-patterns-as-first-class-kernel-nodes) for the full pattern catalog.
156
+
157
+ ```py
158
+ from deepstrike import WorkflowSpec, WorkflowNodeSpec
159
+
160
+ # One fresh-context verifier per rule (no inherited author context → can't rubber-stamp),
161
+ # then a skeptic that reviews their flags. The kernel spawns the 3 verifiers as one gated
162
+ # batch, suspends on the join, and runs the skeptic once they complete.
163
+ outcome = await runner.run_workflow(WorkflowSpec(nodes=[
164
+ WorkflowNodeSpec(task="Rule: money is integer cents — violated?", role="verify"),
165
+ WorkflowNodeSpec(task="Rule: all errors propagate — violated?", role="verify"),
166
+ WorkflowNodeSpec(task="Rule: timestamps are UTC — violated?", role="verify"),
167
+ WorkflowNodeSpec(task="Skeptic: which flags are real violations?", role="verify", depends_on=[0, 1, 2]),
168
+ ]))
169
+ # => {"completed": ["wf-node0", …], "failed": []}
170
+ ```
171
+
172
+ A node's `kind` selects the control-flow shape; the same executor drives them all, every spawn passing the syscall gate:
173
+
174
+ | Node kind | Behavior |
175
+ |---|---|
176
+ | spawn (default) | Run the node's agent once |
177
+ | loop (`max_iters`) | Re-run until the agent signals it's done, capped at `max_iters` |
178
+ | classify (`branches`) | The classifier's result selects one branch; the rest are pruned |
179
+ | tournament (`entrants`) | Generate N entrants, then a pairwise-judge bracket to one winner |
180
+ | reduce (`reducer`) | **Tokenless host-compute** — a pure function (`dedupe_lines` / `merge_json_arrays` / `concat` / `count`, or your own via the `reducers` option) over the node's dependency outputs |
181
+
182
+ ### 0.2.11 capabilities
183
+
184
+ - **Runtime fan-out** — give a node the `submit_workflow_nodes_tool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resume_workflow`.
185
+ - **Quarantine, no escape** — set `trust="quarantined"` on a node that reads untrusted content; it's denied write-capable isolation in-kernel, and any nodes it submits are coerced to quarantined too (no privilege escalation).
186
+ - **Structured output** — set `output_schema` on a node; the runner instructs the agent, validates the result against the JSON-Schema subset, and re-runs once with the errors on mismatch. A node that never conforms fails (its dependents starve).
187
+ - **Budget as signal** — with a `max_workflow_nodes` / `max_concurrent_subagents` quota installed, each spawned node's goal carries its remaining headroom so a coordinator can size its fan-out to fit.
188
+
189
+ ---
190
+
153
191
  ## Providers
154
192
 
155
193
  Resource quotas are opt-in and flow through the same replayable kernel event ABI:
@@ -137,6 +137,44 @@ Spool, page-out, signals, processes, budgets, and memory events land in `Session
137
137
 
138
138
  ---
139
139
 
140
+ ## Dynamic workflows
141
+
142
+ Instead of planning **and** executing a hard task in one long context window, hand the kernel a declarative DAG and let it spawn a fresh-context sub-agent per node. The kernel owns the control flow (gate · budget · suspend-on-join · resume); your SDK runs the agents. See the [top-level overview](../README.md#the-six-harness-patterns-as-first-class-kernel-nodes) for the full pattern catalog.
143
+
144
+ ```py
145
+ from deepstrike import WorkflowSpec, WorkflowNodeSpec
146
+
147
+ # One fresh-context verifier per rule (no inherited author context → can't rubber-stamp),
148
+ # then a skeptic that reviews their flags. The kernel spawns the 3 verifiers as one gated
149
+ # batch, suspends on the join, and runs the skeptic once they complete.
150
+ outcome = await runner.run_workflow(WorkflowSpec(nodes=[
151
+ WorkflowNodeSpec(task="Rule: money is integer cents — violated?", role="verify"),
152
+ WorkflowNodeSpec(task="Rule: all errors propagate — violated?", role="verify"),
153
+ WorkflowNodeSpec(task="Rule: timestamps are UTC — violated?", role="verify"),
154
+ WorkflowNodeSpec(task="Skeptic: which flags are real violations?", role="verify", depends_on=[0, 1, 2]),
155
+ ]))
156
+ # => {"completed": ["wf-node0", …], "failed": []}
157
+ ```
158
+
159
+ A node's `kind` selects the control-flow shape; the same executor drives them all, every spawn passing the syscall gate:
160
+
161
+ | Node kind | Behavior |
162
+ |---|---|
163
+ | spawn (default) | Run the node's agent once |
164
+ | loop (`max_iters`) | Re-run until the agent signals it's done, capped at `max_iters` |
165
+ | classify (`branches`) | The classifier's result selects one branch; the rest are pruned |
166
+ | tournament (`entrants`) | Generate N entrants, then a pairwise-judge bracket to one winner |
167
+ | reduce (`reducer`) | **Tokenless host-compute** — a pure function (`dedupe_lines` / `merge_json_arrays` / `concat` / `count`, or your own via the `reducers` option) over the node's dependency outputs |
168
+
169
+ ### 0.2.11 capabilities
170
+
171
+ - **Runtime fan-out** — give a node the `submit_workflow_nodes_tool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resume_workflow`.
172
+ - **Quarantine, no escape** — set `trust="quarantined"` on a node that reads untrusted content; it's denied write-capable isolation in-kernel, and any nodes it submits are coerced to quarantined too (no privilege escalation).
173
+ - **Structured output** — set `output_schema` on a node; the runner instructs the agent, validates the result against the JSON-Schema subset, and re-runs once with the errors on mismatch. A node that never conforms fails (its dependents starve).
174
+ - **Budget as signal** — with a `max_workflow_nodes` / `max_concurrent_subagents` quota installed, each spawned node's goal carries its remaining headroom so a coordinator can size its fan-out to fit.
175
+
176
+ ---
177
+
140
178
  ## Providers
141
179
 
142
180
  Resource quotas are opt-in and flow through the same replayable kernel event ABI:
@@ -59,6 +59,13 @@ pub struct ContextManager {
59
59
  pub handles: HandleTable,
60
60
  /// Monotonic allocator for [`HandleId`]s.
61
61
  next_handle_id: HandleId,
62
+
63
+ /// P1-E: history length (message count) as of the last compaction/renewal. Messages below this
64
+ /// index are the **frozen prefix** — byte-stable until the next compaction — so the renderer can
65
+ /// hand providers a `frozen_prefix_len` for a long-lived deep cache breakpoint. 0 before any
66
+ /// compaction (no frozen region yet). Not snapshotted: on resume it resets to 0 and rebuilds at
67
+ /// the next compaction (graceful — only the deep-cache durability lapses, never correctness).
68
+ frozen_history_len: usize,
62
69
  }
63
70
 
64
71
  impl ContextManager {
@@ -84,6 +91,7 @@ impl ContextManager {
84
91
  last_compact_ms: None,
85
92
  handles: HandleTable::new(),
86
93
  next_handle_id: 0,
94
+ frozen_history_len: 0,
87
95
  }
88
96
  }
89
97
 
@@ -113,16 +121,24 @@ impl ContextManager {
113
121
 
114
122
  /// Recompute tool-result handle residency for Layer-4 read-time projection (call before
115
123
  /// `render`). When pressure (`rho`) reaches `collapse_threshold`, all but the most recent
116
- /// `preserve_recent_msgs` tool results are marked `Collapsed` (rendered as previews); when
117
- /// pressure subsides they return to `Resident`. Non-destructive: `partitions` is untouched, so
118
- /// projection fully reverses. Spooled/paged-out handles (Layer 1/page-out) are left as-is.
124
+ /// `preserve_recent_msgs` tool results are marked `Collapsed` (rendered as previews).
125
+ ///
126
+ /// **Monotonic within a cache generation (P0-C):** collapse is one-way here —
127
+ /// `Resident → Collapsed` only, never the reverse. The old two-way version un-collapsed when
128
+ /// `rho` fell back below the threshold, which (a) rewrote mid-history bytes and invalidated the
129
+ /// prompt-cache prefix on every threshold oscillation, and (b) re-billed a full tool-result body
130
+ /// for near-zero attention gain (an old result that already faded). Un-collapsing now happens
131
+ /// only at compaction/renewal boundaries via [`Self::reset_collapse_generation`] — the one moment
132
+ /// the prefix is rewritten anyway, so the cache cost is already paid. Non-destructive:
133
+ /// `partitions` is untouched. Spooled/paged-out handles are left as-is.
119
134
  pub fn recompute_handle_residency(&mut self) {
120
- let collapse = self.rho() >= self.config.collapse_threshold;
135
+ // Monotonic: below the threshold we never *un*-collapse, so there is nothing to do.
136
+ if self.rho() < self.config.collapse_threshold {
137
+ return;
138
+ }
121
139
  let keep = self.config.preserve_recent_msgs;
122
- // Single mutable pass in insertion order. The previous implementation collected ids then
123
- // re-found each via `get_mut` (a linear scan), making this O(handles²) **every turn** — the
124
- // long-session cliff. `tool_result_handles_mut().enumerate()` yields the same set in the
125
- // same order, so `i`/`cutoff` are bit-identical to the old logic, now in O(handles).
140
+ // Single mutable pass in insertion order. `tool_result_handles_mut().enumerate()` yields the
141
+ // collapse candidates oldest-first; `i < cutoff` protects the most recent `keep` results.
126
142
  let total = self
127
143
  .handles
128
144
  .all()
@@ -131,14 +147,23 @@ impl ContextManager {
131
147
  .count();
132
148
  let cutoff = total.saturating_sub(keep);
133
149
  for (i, handle) in self.handles.tool_result_handles_mut().enumerate() {
134
- // Only toggle the reversible Resident<->Collapsed axis; never clobber a handle
135
- // that has been spooled or paged out.
136
- if matches!(handle.residency, Residency::Resident | Residency::Collapsed) {
137
- handle.residency = if collapse && i < cutoff {
138
- Residency::Collapsed
139
- } else {
140
- Residency::Resident
141
- };
150
+ // Only fold the reversible ResidentCollapsed axis; never clobber a handle that has
151
+ // been spooled or paged out, and never reverse an existing collapse mid-generation.
152
+ if i < cutoff && matches!(handle.residency, Residency::Resident) {
153
+ handle.residency = Residency::Collapsed;
154
+ }
155
+ }
156
+ }
157
+
158
+ /// Start a fresh collapse generation: un-collapse every `Collapsed` handle back to `Resident`.
159
+ /// Called only at compaction/renewal boundaries — the sole points where un-collapsing is
160
+ /// cache-free, since the rendered prefix is rewritten there regardless. Between boundaries
161
+ /// [`Self::recompute_handle_residency`] keeps collapse strictly one-way (P0-C). Spooled/paged-out
162
+ /// handles are untouched (they leave the Resident↔Collapsed cycle deliberately).
163
+ pub fn reset_collapse_generation(&mut self) {
164
+ for handle in self.handles.all_mut() {
165
+ if matches!(handle.residency, Residency::Collapsed) {
166
+ handle.residency = Residency::Resident;
142
167
  }
143
168
  }
144
169
  }
@@ -257,6 +282,18 @@ impl ContextManager {
257
282
  // Archived messages have left history — drop their now-orphaned handles (bounds the table).
258
283
  if !result.2.is_empty() {
259
284
  self.prune_orphaned_handles();
285
+ // Compaction rewrote the history prefix — start a fresh collapse generation so
286
+ // surviving handles re-evaluate from Resident (P0-C: the one cache-free un-collapse point).
287
+ self.reset_collapse_generation();
288
+ }
289
+ // P2-D × P1-E: re-anchor the frozen-prefix boundary only when the compaction actually broke
290
+ // the prompt-cache prefix (`result.3` = the planner's per-step `cache_at` cost, `Some` ⇒ a
291
+ // prefix break). A prefix-safe compaction (late Snip/Excerpt that touches no early message)
292
+ // leaves `[0..frozen]` byte-stable, so the deep cache survives the compaction and the boundary
293
+ // holds — strictly more precise than the old `archived`-keyed reset, which missed an early
294
+ // in-place Snip and needlessly re-anchored after a prefix-safe pass.
295
+ if result.3.is_some() {
296
+ self.frozen_history_len = self.partitions.history.messages.len();
260
297
  }
261
298
 
262
299
  result
@@ -269,6 +306,18 @@ impl ContextManager {
269
306
  let result = self.compression.compress(&mut self.partitions, PressureAction::AutoCompact, self.max_tokens, 0, &self.engine);
270
307
  if !result.2.is_empty() {
271
308
  self.prune_orphaned_handles();
309
+ // Compaction rewrote the history prefix — start a fresh collapse generation so
310
+ // surviving handles re-evaluate from Resident (P0-C: the one cache-free un-collapse point).
311
+ self.reset_collapse_generation();
312
+ }
313
+ // P2-D × P1-E: re-anchor the frozen-prefix boundary only when the compaction actually broke
314
+ // the prompt-cache prefix (`result.3` = the planner's per-step `cache_at` cost, `Some` ⇒ a
315
+ // prefix break). A prefix-safe compaction (late Snip/Excerpt that touches no early message)
316
+ // leaves `[0..frozen]` byte-stable, so the deep cache survives the compaction and the boundary
317
+ // holds — strictly more precise than the old `archived`-keyed reset, which missed an early
318
+ // in-place Snip and needlessly re-anchored after a prefix-safe pass.
319
+ if result.3.is_some() {
320
+ self.frozen_history_len = self.partitions.history.messages.len();
272
321
  }
273
322
  result
274
323
  }
@@ -295,6 +344,18 @@ impl ContextManager {
295
344
  }
296
345
  if !result.2.is_empty() {
297
346
  self.prune_orphaned_handles();
347
+ // Compaction rewrote the history prefix — start a fresh collapse generation so
348
+ // surviving handles re-evaluate from Resident (P0-C: the one cache-free un-collapse point).
349
+ self.reset_collapse_generation();
350
+ }
351
+ // P2-D × P1-E: re-anchor the frozen-prefix boundary only when the compaction actually broke
352
+ // the prompt-cache prefix (`result.3` = the planner's per-step `cache_at` cost, `Some` ⇒ a
353
+ // prefix break). A prefix-safe compaction (late Snip/Excerpt that touches no early message)
354
+ // leaves `[0..frozen]` byte-stable, so the deep cache survives the compaction and the boundary
355
+ // holds — strictly more precise than the old `archived`-keyed reset, which missed an early
356
+ // in-place Snip and needlessly re-anchored after a prefix-safe pass.
357
+ if result.3.is_some() {
358
+ self.frozen_history_len = self.partitions.history.messages.len();
298
359
  }
299
360
  result
300
361
  }
@@ -321,8 +382,12 @@ impl ContextManager {
321
382
  self.partitions = renewed;
322
383
  self.last_handoff = Some(artifact);
323
384
  self.sprint += 1;
324
- // History was rebuilt wholesale — drop handles anchored to messages it no longer carries.
385
+ // History was rebuilt wholesale — drop handles anchored to messages it no longer carries,
386
+ // and start a fresh collapse generation (P0-C) since the whole prefix changed.
325
387
  self.prune_orphaned_handles();
388
+ self.reset_collapse_generation();
389
+ // P1-E: the renewed history is the new frozen base.
390
+ self.frozen_history_len = self.partitions.history.messages.len();
326
391
  }
327
392
 
328
393
  // ── Render ────────────────────────────────────────────────────────────────
@@ -334,6 +399,7 @@ impl ContextManager {
334
399
  &self.engine,
335
400
  self.config.preserve_recent_msgs,
336
401
  &self.handles,
402
+ self.frozen_history_len,
337
403
  )
338
404
  }
339
405
 
@@ -586,12 +652,14 @@ mod tests {
586
652
  }
587
653
 
588
654
  #[test]
589
- fn render_includes_task_state_in_turns_not_system() {
655
+ fn render_includes_task_state_in_state_turn_not_system() {
590
656
  let mut mgr = ContextManager::new(10_000);
591
657
  mgr.init_task("find anomalies".to_string(), vec![]);
592
658
  let rc = mgr.render();
593
659
  assert!(!rc.system_text.contains("[TASK STATE]"), "task_state must not be in system_text");
594
- assert!(rc.turns[0].content.as_text().unwrap().contains("[TASK STATE] goal: find anomalies"));
660
+ // State turn is separated from the cacheable history (turns).
661
+ let state = rc.state_turn.as_ref().expect("should have a state turn");
662
+ assert!(state.content.as_text().unwrap().contains("[TASK STATE] goal: find anomalies"));
595
663
  }
596
664
 
597
665
  #[test]
@@ -744,12 +812,88 @@ mod tests {
744
812
  assert_eq!(mgr.handles.residency_for_source("c0"), Some(&Residency::Collapsed));
745
813
  assert_eq!(mgr.handles.residency_for_source("c9"), Some(&Residency::Resident));
746
814
 
747
- // Reversible: once pressure drops, collapse is undone (read-time projection only).
815
+ // P0-C — monotonic within a generation: once collapsed, dropping pressure does NOT
816
+ // un-collapse (un-collapsing would re-bill the body and churn the cache prefix).
748
817
  mgr.set_observed_prompt_tokens(100); // 0.10 < 0.90
749
818
  mgr.recompute_handle_residency();
819
+ assert_eq!(
820
+ mgr.handles.residency_for_source("c0"),
821
+ Some(&Residency::Collapsed),
822
+ "collapse is sticky until a compaction boundary"
823
+ );
824
+
825
+ // Only a generation reset (compaction/renewal) un-collapses.
826
+ mgr.reset_collapse_generation();
750
827
  assert_eq!(mgr.handles.residency_for_source("c0"), Some(&Residency::Resident));
751
828
  }
752
829
 
830
+ #[test]
831
+ fn frozen_prefix_len_anchors_at_compaction_and_holds_across_appends() {
832
+ let mut mgr = ContextManager::new(1_000);
833
+ // Pre-compaction: no frozen region yet → providers use the rolling-pair fallback.
834
+ for i in 0..30 {
835
+ mgr.push_history(Message::user(format!("turn {i}: {}", "ctx ".repeat(30))), 150);
836
+ }
837
+ assert!(mgr.render().frozen_prefix_len.is_none(), "no frozen region before any compaction");
838
+
839
+ let (saved, _, archived, _) = mgr.compress(PressureAction::AutoCompact);
840
+ assert!(saved > 0 && !archived.is_empty(), "expected archival");
841
+
842
+ // Immediately after compaction the hot tail is empty → deep would coincide with the tail → None.
843
+ assert!(mgr.render().frozen_prefix_len.is_none(), "deep == tail right after compaction");
844
+
845
+ // As turns are appended, the deep boundary holds fixed while the tail grows.
846
+ mgr.push_history(Message::user("new 1"), 5);
847
+ let f1 = mgr.render().frozen_prefix_len.expect("frozen region exists once the tail grows");
848
+ mgr.push_history(Message::assistant("reply 1"), 5);
849
+ mgr.push_history(Message::user("new 2"), 5);
850
+ let rc = mgr.render();
851
+ let f2 = rc.frozen_prefix_len.expect("frozen region holds");
852
+ assert_eq!(f1, f2, "the deep boundary is fixed between compactions; only the tail grows");
853
+ assert!(f2 < rc.turns.len(), "deep boundary is distinct from the rolling tail");
854
+ }
855
+
856
+ #[test]
857
+ fn frozen_boundary_holds_through_a_prefix_safe_compaction() {
858
+ // P2-D × P1-E: the boundary re-anchors on a prefix-breaking compaction (cache_at = Some) but
859
+ // is preserved through a prefix-safe one (cache_at = None) — the deep cache survives.
860
+ let mut mgr = ContextManager::new(10_000);
861
+ for i in 0..5 {
862
+ mgr.push_history(Message::user(format!("m{i}")), 5);
863
+ }
864
+ mgr.frozen_history_len = 3; // pretend a prior compaction anchored the deep cache here
865
+
866
+ // A no-op / prefix-safe compaction (PressureAction::None ⇒ cache_at None) must NOT move the
867
+ // anchor — the cached [0..3] prefix is untouched, so the deep breakpoint stays put.
868
+ let (_, _, _, cache_at) = mgr.compress(PressureAction::None);
869
+ assert!(cache_at.is_none(), "no-op compaction is prefix-safe");
870
+ assert_eq!(mgr.frozen_history_len, 3, "prefix-safe compaction preserves the deep-cache anchor");
871
+ }
872
+
873
+ #[test]
874
+ fn collapse_generation_resets_on_autocompact() {
875
+ let mut mgr = ContextManager::new(1_000);
876
+ // Many oversized tool results: some will be archived by AutoCompact, the survivors
877
+ // should come back Resident (fresh generation), not stay stuck Collapsed.
878
+ for i in 0..20 {
879
+ mgr.push_history(tool_result_msg(&format!("c{i}"), &"x".repeat(120)), 60);
880
+ }
881
+ mgr.set_observed_prompt_tokens(980); // force collapse of the older results
882
+ mgr.recompute_handle_residency();
883
+ assert_eq!(mgr.handles.residency_for_source("c0"), Some(&Residency::Collapsed));
884
+
885
+ let (saved, _, archived, _) = mgr.compress(PressureAction::AutoCompact);
886
+ assert!(saved > 0 && !archived.is_empty(), "expected archival");
887
+
888
+ // Every surviving tool-result handle is Resident again — the compaction boundary
889
+ // rewrote the prefix, so the next pressure cycle re-decides from scratch.
890
+ for h in mgr.handles.all() {
891
+ if matches!(h.kind, HandleKind::ToolResult) {
892
+ assert_eq!(h.residency, Residency::Resident, "generation reset un-collapses survivors");
893
+ }
894
+ }
895
+ }
896
+
753
897
  #[test]
754
898
  fn mark_spooled_sets_residency_and_survives_residency_recompute() {
755
899
  let mut mgr = ContextManager::new(1_000);
@@ -2,7 +2,14 @@ use super::config::ContextConfig;
2
2
  use super::partitions::ContextPartitions;
3
3
  use super::token_engine::ContextTokenEngine;
4
4
 
5
- /// Action recommended by the pressure monitor.
5
+ /// Action recommended by the pressure monitor — the *pressure-level* vocabulary.
6
+ ///
7
+ /// This is distinct from [`crate::mm::EvictionOp`] (the *planner-op* vocabulary) and the two must
8
+ /// **not** be collapsed: `PressureAction` is what [`PressureMonitor::recommend`] /
9
+ /// `ContextManager::should_compress` return, the `Ord`-keyed cascade selector inside the compression
10
+ /// pipeline, and the canonical compaction wire label. `EvictionOp` is what `plan_eviction` emits, and
11
+ /// carries per-op data this enum has no place for. The single bridge between the two layers is
12
+ /// `LoopStateMachine::execute_eviction_op`. See the layer-boundary note on [`crate::mm::EvictionOp`].
6
13
  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7
14
  pub enum PressureAction {
8
15
  None,