deepstrike 0.2.37__tar.gz → 0.2.38__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 (174) hide show
  1. {deepstrike-0.2.37 → deepstrike-0.2.38}/Cargo.lock +5 -5
  2. {deepstrike-0.2.37 → deepstrike-0.2.38}/Cargo.toml +2 -2
  3. {deepstrike-0.2.37 → deepstrike-0.2.38}/PKG-INFO +1 -1
  4. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/lib.rs +1 -0
  5. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/kernel.rs +118 -0
  6. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/session.rs +21 -0
  7. deepstrike-0.2.38/crates/deepstrike-core/src/scheduler/entropy.rs +282 -0
  8. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/mod.rs +2 -0
  9. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/mod.rs +82 -0
  10. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/tests.rs +136 -0
  11. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/__init__.py +2 -0
  12. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/stream.py +37 -0
  13. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/kernel_event_log.py +19 -0
  14. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/runner.py +60 -0
  15. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/session_log.py +21 -0
  16. {deepstrike-0.2.37 → deepstrike-0.2.38}/pyproject.toml +1 -1
  17. {deepstrike-0.2.37 → deepstrike-0.2.38}/README.md +0 -0
  18. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/Cargo.toml +0 -0
  19. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/compression.rs +0 -0
  20. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/config.rs +0 -0
  21. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/fault.rs +0 -0
  22. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/manager.rs +0 -0
  23. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/mod.rs +0 -0
  24. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  25. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/pressure.rs +0 -0
  26. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/renderer.rs +0 -0
  27. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  28. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  29. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  30. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/task_state.rs +0 -0
  31. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/text.rs +0 -0
  32. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  33. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  34. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  35. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  36. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  37. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  38. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  39. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/repeat_fuse.rs +0 -0
  40. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  41. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/harness/eval.rs +0 -0
  42. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  43. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  44. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  45. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  46. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  47. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  48. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  49. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  50. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/mm/handle.rs +0 -0
  51. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  52. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  53. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/mod.rs +0 -0
  54. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  55. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
  56. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/workflow/mod.rs +0 -0
  57. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/workflow/run.rs +0 -0
  58. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/proc/mod.rs +0 -0
  59. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
  60. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
  61. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  62. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  63. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  64. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  65. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  66. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/capability.rs +0 -0
  67. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/eviction.rs +0 -0
  68. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/gate.rs +0 -0
  69. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/milestone_exec.rs +0 -0
  70. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/process.rs +0 -0
  71. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/signal.rs +0 -0
  72. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/workflow.rs +0 -0
  73. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
  74. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  75. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  76. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  77. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/router.rs +0 -0
  78. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
  79. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/agent.rs +0 -0
  80. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/capability.rs +0 -0
  81. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/contract.rs +0 -0
  82. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/error.rs +0 -0
  83. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/message.rs +0 -0
  84. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  85. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/mod.rs +0 -0
  86. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/policy.rs +0 -0
  87. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/result.rs +0 -0
  88. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/signal.rs +0 -0
  89. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/skill.rs +0 -0
  90. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/task.rs +0 -0
  91. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-py/Cargo.toml +0 -0
  92. {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-py/src/lib.rs +0 -0
  93. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/__init__.py +0 -0
  94. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/contract.py +0 -0
  95. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/handoff.py +0 -0
  96. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/harness.py +0 -0
  97. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/modes.py +0 -0
  98. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/pool.py +0 -0
  99. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/governance.py +0 -0
  100. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/harness/__init__.py +0 -0
  101. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/harness/harness.py +0 -0
  102. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/harness/judge.py +0 -0
  103. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/kernel/__init__.py +0 -0
  104. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/knowledge/__init__.py +0 -0
  105. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/knowledge/source.py +0 -0
  106. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/__init__.py +0 -0
  107. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/agent.py +0 -0
  108. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/in_memory_store.py +0 -0
  109. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/protocols.py +0 -0
  110. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/working.py +0 -0
  111. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/__init__.py +0 -0
  112. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/anthropic.py +0 -0
  113. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/anthropic_compatible.py +0 -0
  114. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/base.py +0 -0
  115. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/deepseek.py +0 -0
  116. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/factories.py +0 -0
  117. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/gemini.py +0 -0
  118. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/glm.py +0 -0
  119. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/kimi.py +0 -0
  120. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/minimax.py +0 -0
  121. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/ollama.py +0 -0
  122. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/openai.py +0 -0
  123. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/openai_responses.py +0 -0
  124. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/qwen.py +0 -0
  125. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/replay.py +0 -0
  126. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/replay_validator.py +0 -0
  127. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/vendor_profiles.py +0 -0
  128. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/__init__.py +0 -0
  129. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/archive.py +0 -0
  130. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/credential_vault.py +0 -0
  131. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/eval.py +0 -0
  132. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/event_stream.py +0 -0
  133. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/execution_plane.py +0 -0
  134. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/facade.py +0 -0
  135. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/filtered_plane.py +0 -0
  136. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/kernel_step.py +0 -0
  137. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/large_result_spool.py +0 -0
  138. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/loop_driver.py +0 -0
  139. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  140. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/os_profile.py +0 -0
  141. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/os_snapshot.py +0 -0
  142. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/output_schema.py +0 -0
  143. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  144. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/provider_replay.py +0 -0
  145. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/reactive_session.py +0 -0
  146. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/reducers.py +0 -0
  147. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  148. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/replay_fixture.py +0 -0
  149. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/replay_provider.py +0 -0
  150. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/replay_sanitize.py +0 -0
  151. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/run_group.py +0 -0
  152. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/session_repair.py +0 -0
  153. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
  154. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/turn_policy.py +0 -0
  155. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/workflow_control_flow.py +0 -0
  156. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/workflow_store.py +0 -0
  157. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/worktree_plane.py +0 -0
  158. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/safety/__init__.py +0 -0
  159. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/safety/permissions.py +0 -0
  160. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/__init__.py +0 -0
  161. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/gateway.py +0 -0
  162. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/scheduled.py +0 -0
  163. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/types.py +0 -0
  164. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/skills/__init__.py +0 -0
  165. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/skills/registry.py +0 -0
  166. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/skills/watcher.py +0 -0
  167. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/__init__.py +0 -0
  168. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/builtin/__init__.py +0 -0
  169. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/builtin/read_file.py +0 -0
  170. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/errors.py +0 -0
  171. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/execution.py +0 -0
  172. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/registry.py +0 -0
  173. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/types/__init__.py +0 -0
  174. {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/types/agent.py +0 -0
@@ -194,7 +194,7 @@ dependencies = [
194
194
 
195
195
  [[package]]
196
196
  name = "deepstrike-core"
197
- version = "0.2.37"
197
+ version = "0.2.38"
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.37"
209
+ version = "0.2.38"
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.37"
221
+ version = "0.2.38"
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.37"
232
+ version = "0.2.38"
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.37"
266
+ version = "0.2.38"
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.37"
6
+ version = "0.2.38"
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.37" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.38" }
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.37
3
+ Version: 0.2.38
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -44,6 +44,7 @@ pub use mm::{
44
44
  MemoryTierHint, PageInEntry, Residency,
45
45
  };
46
46
  pub use proc::{AgentProcess, ProcessState};
47
+ pub use scheduler::entropy::{EntropySample, EntropyTracker, EntropyWatchConfig};
47
48
  pub use scheduler::tcb::{BudgetLedger, TaskId, TaskLifecycle, TaskTable, Tcb, WaitReason};
48
49
  pub use syscall::{Disposition, Syscall};
49
50
  pub use runtime::session::SessionEvent;
@@ -175,6 +175,11 @@ pub struct RunConfig {
175
175
  /// `ContextConfig::knowledge_budget_ratio`). Absent ⇒ kernel default (0.25); `0.0` disables.
176
176
  #[serde(default, skip_serializing_if = "Option::is_none")]
177
177
  pub knowledge_budget_ratio: Option<f64>,
178
+ /// Entropy watch: opt-in threshold alerting over the per-turn session-entropy score
179
+ /// (see `SetEntropyWatch`). Absent ⇒ kernel default (disabled; sampling itself is
180
+ /// unconditional and unaffected).
181
+ #[serde(default, skip_serializing_if = "Option::is_none")]
182
+ pub entropy_watch: Option<crate::scheduler::entropy::EntropyWatchConfig>,
178
183
  }
179
184
 
180
185
  /// Build a [`GovernancePipeline`](crate::governance::pipeline::GovernancePipeline) from the ABI policy
@@ -379,6 +384,22 @@ pub enum KernelInputEvent {
379
384
  #[serde(default, skip_serializing_if = "Option::is_none")]
380
385
  terminate_after: Option<u32>,
381
386
  },
387
+ /// Entropy watch: tune the opt-in threshold alerting over the per-turn session-entropy
388
+ /// score (defaults: disabled, threshold=0.65, hysteresis=0.1, cooldown_turns=4,
389
+ /// notify_model=false). Each field is optional — an absent field keeps the current
390
+ /// value. Sampling itself is unconditional and unaffected. Additive ABI.
391
+ SetEntropyWatch {
392
+ #[serde(default, skip_serializing_if = "Option::is_none")]
393
+ enabled: Option<bool>,
394
+ #[serde(default, skip_serializing_if = "Option::is_none")]
395
+ threshold: Option<f64>,
396
+ #[serde(default, skip_serializing_if = "Option::is_none")]
397
+ hysteresis: Option<f64>,
398
+ #[serde(default, skip_serializing_if = "Option::is_none")]
399
+ cooldown_turns: Option<u32>,
400
+ #[serde(default, skip_serializing_if = "Option::is_none")]
401
+ notify_model: Option<bool>,
402
+ },
382
403
  /// Adjust the wall-clock budget at runtime (e.g. to extend or set a deadline
383
404
  /// after a run has already started). Additive: omit to keep the value from
384
405
  /// `SchedulerBudget` passed at construction.
@@ -697,6 +718,28 @@ pub enum KernelObservation {
697
718
  turn: u32,
698
719
  criteria: Vec<String>,
699
720
  },
721
+ /// Session-entropy sample at a completed turn boundary (the heartbeat watch source).
722
+ /// One per completed turn, unconditional — like `CheckpointTaken`. The component
723
+ /// vector is the contract; `score` is a versioned default fold (`score_version`).
724
+ /// See `scheduler::entropy`. Additive ABI.
725
+ EntropySample {
726
+ turn: u32,
727
+ score: f64,
728
+ score_version: u32,
729
+ rho: f64,
730
+ repeat_pressure: f64,
731
+ failure_rate: f64,
732
+ rollbacks_in_window: u32,
733
+ window_turns: u32,
734
+ },
735
+ /// The opt-in entropy watch tripped: `score` crossed `threshold` while armed and
736
+ /// cooled down (`EntropyWatchConfig`). Correlate components via the same-turn
737
+ /// `EntropySample`. Additive ABI.
738
+ EntropyAlert {
739
+ turn: u32,
740
+ score: f64,
741
+ threshold: f64,
742
+ },
700
743
  /// Kernel process table changed for a spawned sub-agent.
701
744
  AgentProcessChanged {
702
745
  turn: u32,
@@ -1034,6 +1077,7 @@ impl KernelRuntime {
1034
1077
  repeat_fuse,
1035
1078
  criteria_gate,
1036
1079
  knowledge_budget_ratio,
1080
+ entropy_watch,
1037
1081
  } = config;
1038
1082
  if let Some(tools) = tools {
1039
1083
  self.sm.tools = tools;
@@ -1092,6 +1136,9 @@ impl KernelRuntime {
1092
1136
  if let Some(ratio) = knowledge_budget_ratio {
1093
1137
  self.sm.ctx.config.knowledge_budget_ratio = ratio;
1094
1138
  }
1139
+ if let Some(watch) = entropy_watch {
1140
+ self.sm.set_entropy_watch(watch);
1141
+ }
1095
1142
  return KernelStep::empty(self.sm.take_observations());
1096
1143
  }
1097
1144
  KernelInputEvent::SetAttentionPolicy { max_queue_size } => {
@@ -1145,6 +1192,16 @@ impl KernelRuntime {
1145
1192
  self.sm.set_repeat_fuse(cfg);
1146
1193
  return KernelStep::empty(self.sm.take_observations());
1147
1194
  }
1195
+ KernelInputEvent::SetEntropyWatch { enabled, threshold, hysteresis, cooldown_turns, notify_model } => {
1196
+ let mut cfg = self.sm.entropy_watch_config();
1197
+ if let Some(e) = enabled { cfg.enabled = e; }
1198
+ if let Some(t) = threshold { cfg.threshold = t; }
1199
+ if let Some(h) = hysteresis { cfg.hysteresis = h; }
1200
+ if let Some(c) = cooldown_turns { cfg.cooldown_turns = c; }
1201
+ if let Some(n) = notify_model { cfg.notify_model = n; }
1202
+ self.sm.set_entropy_watch(cfg);
1203
+ return KernelStep::empty(self.sm.take_observations());
1204
+ }
1148
1205
  KernelInputEvent::ProviderResult {
1149
1206
  message,
1150
1207
  observed_input_tokens,
@@ -2186,6 +2243,67 @@ mod tests {
2186
2243
  );
2187
2244
  }
2188
2245
 
2246
+ #[test]
2247
+ fn configure_run_entropy_watch_flows_through_the_abi() {
2248
+ // The `entropy_watch` bundle field arms the watch; a completed tool round then
2249
+ // surfaces BOTH the unconditional sample and (threshold 0 + all-error results) the alert.
2250
+ let mut runtime = KernelRuntime::new(SchedulerBudget::default());
2251
+ runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
2252
+ config: RunConfig {
2253
+ entropy_watch: Some(crate::scheduler::entropy::EntropyWatchConfig {
2254
+ enabled: true,
2255
+ threshold: 0.1,
2256
+ hysteresis: 0.05,
2257
+ cooldown_turns: 0,
2258
+ notify_model: false,
2259
+ }),
2260
+ ..RunConfig::default()
2261
+ },
2262
+ }));
2263
+ run_with_tool_call(&mut runtime, "step");
2264
+ let step = runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
2265
+ results: vec![ToolResult {
2266
+ call_id: "call-1".into(),
2267
+ output: crate::types::message::Content::Text("boom".into()),
2268
+ is_error: true,
2269
+ is_fatal: false,
2270
+ error_kind: None,
2271
+ token_count: None,
2272
+ }],
2273
+ }));
2274
+ assert!(
2275
+ step.observations
2276
+ .iter()
2277
+ .any(|o| matches!(o, KernelObservation::EntropySample { failure_rate, .. } if *failure_rate > 0.9)),
2278
+ "completed boundary must carry the entropy sample: {:?}",
2279
+ step.observations
2280
+ );
2281
+ assert!(
2282
+ step.observations
2283
+ .iter()
2284
+ .any(|o| matches!(o, KernelObservation::EntropyAlert { threshold, .. } if (*threshold - 0.1).abs() < 1e-9)),
2285
+ "armed watch + errored turn must alert: {:?}",
2286
+ step.observations
2287
+ );
2288
+ }
2289
+
2290
+ #[test]
2291
+ fn set_entropy_watch_event_parses_from_json_and_applies_partially() {
2292
+ // Additive ABI: the granular event round-trips from JSON with absent fields
2293
+ // keeping current values (mirrors SetRepeatFuse).
2294
+ let mut runtime = KernelRuntime::new(SchedulerBudget::default());
2295
+ let input: KernelInput = serde_json::from_str(
2296
+ r#"{"version":1,"event":{"kind":"set_entropy_watch","enabled":true,"threshold":0.4}}"#,
2297
+ )
2298
+ .expect("granular event must deserialize");
2299
+ runtime.step(input);
2300
+ let cfg = runtime.state_machine().entropy_watch_config();
2301
+ assert!(cfg.enabled);
2302
+ assert!((cfg.threshold - 0.4).abs() < 1e-9);
2303
+ assert!((cfg.hysteresis - 0.1).abs() < 1e-9, "absent field keeps the default");
2304
+ assert_eq!(cfg.cooldown_turns, 4, "absent field keeps the default");
2305
+ }
2306
+
2189
2307
  #[test]
2190
2308
  fn configure_run_round_trips_over_the_abi() {
2191
2309
  // The bundle must survive the versioned JSON ABI (replayable / session-loggable) like every
@@ -203,6 +203,23 @@ pub enum SessionEvent {
203
203
  turn: u32,
204
204
  history_len: u32,
205
205
  },
206
+ /// Session-entropy sample at a completed turn boundary (see `scheduler::entropy`).
207
+ EntropySample {
208
+ turn: u32,
209
+ score: f64,
210
+ score_version: u32,
211
+ rho: f64,
212
+ repeat_pressure: f64,
213
+ failure_rate: f64,
214
+ rollbacks_in_window: u32,
215
+ window_turns: u32,
216
+ },
217
+ /// The opt-in entropy watch tripped (score crossed the configured threshold).
218
+ EntropyAlert {
219
+ turn: u32,
220
+ score: f64,
221
+ threshold: f64,
222
+ },
206
223
  /// Transaction rollback indicating state was restored to a checkpoint.
207
224
  Rollbacked {
208
225
  turn: u32,
@@ -294,6 +311,8 @@ impl SessionEvent {
294
311
  Self::SignalDisposed { .. } => "signal_disposed",
295
312
  Self::BudgetExceeded { .. } => "budget_exceeded",
296
313
  Self::CheckpointTaken { .. } => "checkpoint_taken",
314
+ Self::EntropySample { .. } => "entropy_sample",
315
+ Self::EntropyAlert { .. } => "entropy_alert",
297
316
  Self::Rollbacked { .. } => "rollbacked",
298
317
  Self::AgentProcessChanged { .. } => "agent_process_changed",
299
318
  Self::MilestoneAdvanced { .. } => "milestone_advanced",
@@ -321,6 +340,8 @@ impl SessionEvent {
321
340
  | Self::SignalDisposed { .. }
322
341
  | Self::BudgetExceeded { .. }
323
342
  | Self::CheckpointTaken { .. }
343
+ | Self::EntropySample { .. }
344
+ | Self::EntropyAlert { .. }
324
345
  | Self::Rollbacked { .. }
325
346
  | Self::AgentProcessChanged { .. }
326
347
  | Self::MilestoneAdvanced { .. }
@@ -0,0 +1,282 @@
1
+ //! Session-entropy sampling — the kernel-side measurement behind a host "heartbeat
2
+ //! entropy watch" source.
3
+ //!
4
+ //! "Entropy" here is session *disorder*: the degree to which a run is churning without
5
+ //! converging — repeating itself, failing tool calls, rolling turns back, and running out
6
+ //! of context headroom. The kernel already detects each symptom in isolation (2c STOP /
7
+ //! RepeatFuse / rollback / eviction); this module folds them into one per-turn sample the
8
+ //! host can subscribe to, so an external supervisor (heartbeat) can decide *its* policy —
9
+ //! e.g. inject a corrective note — without re-deriving kernel state from the audit log.
10
+ //!
11
+ //! Two honesty rules govern the shape:
12
+ //! - The component vector is the contract; `score` is only a *versioned* default fold
13
+ //! ([`ENTROPY_SCORE_VERSION`]). Hosts that care should threshold on components.
14
+ //! - Measurement is unconditional (one sample per completed turn boundary, like
15
+ //! `CheckpointTaken`); only the alert — a kernel-side threshold decision — is opt-in
16
+ //! via [`EntropyWatchConfig`].
17
+
18
+ use serde::{Deserialize, Serialize};
19
+ use std::collections::VecDeque;
20
+
21
+ /// Version of the default `score` fold. Bump when the formula or weights change so hosts
22
+ /// thresholding on `score` can detect the semantics shift.
23
+ pub const ENTROPY_SCORE_VERSION: u32 = 1;
24
+
25
+ /// Sliding window (in completed turns) for the failure/rollback components.
26
+ pub const ENTROPY_WINDOW_TURNS: usize = 8;
27
+
28
+ /// Saturation point for the rollback component: this many rollbacks inside the window
29
+ /// reads as fully disordered (1.0) on that axis.
30
+ const ROLLBACK_SATURATION: f64 = 3.0;
31
+
32
+ /// Opt-in threshold watch over the per-turn entropy score (③). When the score crosses
33
+ /// `threshold` the kernel emits an `EntropyAlert` observation — at most once per crossing
34
+ /// (hysteresis re-arm) and never more often than `cooldown_turns`. With `notify_model`
35
+ /// the alert is *also* routed through the kernel's own signal dispatch as a
36
+ /// `Heartbeat/Alert` [`RuntimeSignal`](crate::types::signal::RuntimeSignal), so the model
37
+ /// sees a durable `[SIGNAL]` directive at the next boundary. Default OFF: the primary
38
+ /// consumer is the host supervisor, which can inject a task-aware note itself — an
39
+ /// unconditional self-nudge risks a feedback loop (the note churns context → more entropy).
40
+ #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
41
+ pub struct EntropyWatchConfig {
42
+ #[serde(default)]
43
+ pub enabled: bool,
44
+ /// Alert when `score >= threshold`.
45
+ #[serde(default = "default_threshold")]
46
+ pub threshold: f64,
47
+ /// Re-arm only after the score falls below `threshold - hysteresis` (anti-flap).
48
+ #[serde(default = "default_hysteresis")]
49
+ pub hysteresis: f64,
50
+ /// Minimum completed turns between two alerts.
51
+ #[serde(default = "default_cooldown_turns")]
52
+ pub cooldown_turns: u32,
53
+ /// Also self-signal the model (Heartbeat/Alert, High urgency) when the alert fires.
54
+ #[serde(default)]
55
+ pub notify_model: bool,
56
+ }
57
+
58
+ fn default_threshold() -> f64 { 0.65 }
59
+ fn default_hysteresis() -> f64 { 0.1 }
60
+ fn default_cooldown_turns() -> u32 { 4 }
61
+
62
+ impl Default for EntropyWatchConfig {
63
+ fn default() -> Self {
64
+ Self {
65
+ enabled: false,
66
+ threshold: default_threshold(),
67
+ hysteresis: default_hysteresis(),
68
+ cooldown_turns: default_cooldown_turns(),
69
+ notify_model: false,
70
+ }
71
+ }
72
+ }
73
+
74
+ /// One per-turn entropy measurement. All normalized components are in `[0, 1]`.
75
+ #[derive(Debug, Clone, Copy, PartialEq)]
76
+ pub struct EntropySample {
77
+ pub turn: u32,
78
+ /// Versioned default fold of the components (see [`ENTROPY_SCORE_VERSION`]).
79
+ pub score: f64,
80
+ /// Context pressure after this boundary's eviction pass (`ContextManager::rho`).
81
+ pub rho: f64,
82
+ /// Consecutive-identical-turn streak, normalized against the RepeatFuse deny rung
83
+ /// (0 when the streak is 1 — a first occurrence is not repetition — or the fuse is off).
84
+ pub repeat_pressure: f64,
85
+ /// Errored tool results / total tool results over the window.
86
+ pub failure_rate: f64,
87
+ /// Raw rollback count inside the window (normalize with `window_turns`).
88
+ pub rollbacks_in_window: u32,
89
+ /// Effective window size (completed turns currently held, ≤ [`ENTROPY_WINDOW_TURNS`]).
90
+ pub window_turns: u32,
91
+ }
92
+
93
+ /// Sliding-window state feeding [`EntropySample`]. Owned by the state machine; fed at the
94
+ /// completed-turn boundary (`ToolResults`) and by `rollback()`. Deliberately NOT part of
95
+ /// the turn checkpoint: a rollback must not launder the disorder it just evidenced —
96
+ /// the same reasoning as the RepeatFuse streak.
97
+ #[derive(Debug, Default)]
98
+ pub struct EntropyTracker {
99
+ /// Per completed turn: (errored results, total results).
100
+ turn_stats: VecDeque<(u32, u32)>,
101
+ /// Per completed turn: rollbacks observed since the previous completed boundary.
102
+ rollback_stats: VecDeque<u32>,
103
+ /// Rollbacks seen since the last completed boundary (turns that roll back return
104
+ /// early and never reach the sample point — they accrue here until one completes).
105
+ rollbacks_pending: u32,
106
+ /// ③ watch state: armed ⇒ the next threshold crossing may alert.
107
+ disarmed: bool,
108
+ last_alert_turn: Option<u32>,
109
+ }
110
+
111
+ impl EntropyTracker {
112
+ /// Record a rollback (any reason). Called from the state machine's `rollback()`.
113
+ pub fn note_rollback(&mut self) {
114
+ self.rollbacks_pending += 1;
115
+ }
116
+
117
+ /// Fold this boundary's outcomes into the window and produce the turn's sample.
118
+ /// `repeat_streak` is the RepeatFuse consecutive-signature count (0/1 ⇒ no repetition).
119
+ pub fn sample(
120
+ &mut self,
121
+ turn: u32,
122
+ rho: f64,
123
+ repeat_streak: u32,
124
+ repeat_deny_after: u32,
125
+ errored_results: u32,
126
+ total_results: u32,
127
+ ) -> EntropySample {
128
+ self.turn_stats.push_back((errored_results, total_results));
129
+ self.rollback_stats.push_back(std::mem::take(&mut self.rollbacks_pending));
130
+ while self.turn_stats.len() > ENTROPY_WINDOW_TURNS {
131
+ self.turn_stats.pop_front();
132
+ }
133
+ while self.rollback_stats.len() > ENTROPY_WINDOW_TURNS {
134
+ self.rollback_stats.pop_front();
135
+ }
136
+
137
+ let (errors, totals) = self
138
+ .turn_stats
139
+ .iter()
140
+ .fold((0u32, 0u32), |(e, t), (te, tt)| (e + te, t + tt));
141
+ let rollbacks_in_window: u32 = self.rollback_stats.iter().sum();
142
+
143
+ let rho = rho.clamp(0.0, 1.0);
144
+ // Streak 1 = first occurrence = zero repetition; pressure saturates at the deny rung.
145
+ let repeat_pressure = (f64::from(repeat_streak.saturating_sub(1))
146
+ / f64::from(repeat_deny_after.max(1)))
147
+ .clamp(0.0, 1.0);
148
+ let failure_rate = if totals == 0 { 0.0 } else { f64::from(errors) / f64::from(totals) };
149
+ let rollback_term = (f64::from(rollbacks_in_window) / ROLLBACK_SATURATION).clamp(0.0, 1.0);
150
+
151
+ // v1 fold: repetition and failures dominate (they are the direct "no forward
152
+ // progress" evidence), pressure and rollbacks corroborate.
153
+ let score = 0.35 * repeat_pressure + 0.30 * failure_rate + 0.20 * rho + 0.15 * rollback_term;
154
+
155
+ EntropySample {
156
+ turn,
157
+ score,
158
+ rho,
159
+ repeat_pressure,
160
+ failure_rate,
161
+ rollbacks_in_window,
162
+ window_turns: self.turn_stats.len() as u32,
163
+ }
164
+ }
165
+
166
+ /// ③ threshold decision for this sample: hysteresis re-arm + cooldown. Mutates the
167
+ /// watch state; returns `true` when an alert should fire.
168
+ pub fn should_alert(&mut self, config: &EntropyWatchConfig, sample: &EntropySample) -> bool {
169
+ if !config.enabled {
170
+ return false;
171
+ }
172
+ if sample.score < config.threshold {
173
+ if sample.score < config.threshold - config.hysteresis {
174
+ self.disarmed = false;
175
+ }
176
+ return false;
177
+ }
178
+ if self.disarmed {
179
+ return false;
180
+ }
181
+ let cooled_down = self
182
+ .last_alert_turn
183
+ .is_none_or(|t| sample.turn.saturating_sub(t) >= config.cooldown_turns);
184
+ if !cooled_down {
185
+ return false;
186
+ }
187
+ self.disarmed = true;
188
+ self.last_alert_turn = Some(sample.turn);
189
+ true
190
+ }
191
+ }
192
+
193
+ #[cfg(test)]
194
+ mod tests {
195
+ use super::*;
196
+
197
+ fn quiet_sample(tracker: &mut EntropyTracker, turn: u32) -> EntropySample {
198
+ tracker.sample(turn, 0.1, 1, 5, 0, 2)
199
+ }
200
+
201
+ #[test]
202
+ fn healthy_turn_scores_near_zero() {
203
+ let mut t = EntropyTracker::default();
204
+ let s = quiet_sample(&mut t, 1);
205
+ assert!(s.score < 0.05, "healthy turn score {} should be ~0", s.score);
206
+ assert_eq!(s.repeat_pressure, 0.0);
207
+ assert_eq!(s.failure_rate, 0.0);
208
+ assert_eq!(s.rollbacks_in_window, 0);
209
+ }
210
+
211
+ #[test]
212
+ fn repetition_and_failures_raise_the_score() {
213
+ let mut t = EntropyTracker::default();
214
+ // 4-streak against deny_after=5, every result errored, high pressure.
215
+ let s = t.sample(3, 0.9, 4, 5, 2, 2);
216
+ assert!(s.score > 0.6, "disordered turn score {} should be high", s.score);
217
+ assert!((s.repeat_pressure - 0.6).abs() < 1e-9);
218
+ assert!((s.failure_rate - 1.0).abs() < 1e-9);
219
+ }
220
+
221
+ #[test]
222
+ fn failure_rate_windows_out_old_turns() {
223
+ let mut t = EntropyTracker::default();
224
+ t.sample(1, 0.1, 1, 5, 3, 3); // all-error turn
225
+ for turn in 2..=(ENTROPY_WINDOW_TURNS as u32 + 1) {
226
+ let s = quiet_sample(&mut t, turn);
227
+ if turn <= ENTROPY_WINDOW_TURNS as u32 {
228
+ assert!(s.failure_rate > 0.0, "turn {turn} still inside the window");
229
+ } else {
230
+ assert_eq!(s.failure_rate, 0.0, "turn {turn} should have evicted the errors");
231
+ }
232
+ }
233
+ }
234
+
235
+ #[test]
236
+ fn rollbacks_accrue_until_a_boundary_completes() {
237
+ let mut t = EntropyTracker::default();
238
+ t.note_rollback();
239
+ t.note_rollback();
240
+ let s = quiet_sample(&mut t, 2);
241
+ assert_eq!(s.rollbacks_in_window, 2);
242
+ // Consumed into the window — not double-counted next turn (still windowed though).
243
+ let s = quiet_sample(&mut t, 3);
244
+ assert_eq!(s.rollbacks_in_window, 2);
245
+ }
246
+
247
+ #[test]
248
+ fn watch_fires_once_then_rearms_below_hysteresis() {
249
+ let cfg = EntropyWatchConfig { enabled: true, threshold: 0.5, hysteresis: 0.1, cooldown_turns: 0, notify_model: false };
250
+ let mut t = EntropyTracker::default();
251
+ let hot = EntropySample { turn: 1, score: 0.7, rho: 0.0, repeat_pressure: 0.0, failure_rate: 0.0, rollbacks_in_window: 0, window_turns: 1 };
252
+ assert!(t.should_alert(&cfg, &hot));
253
+ // Still hot: no re-fire until re-armed.
254
+ assert!(!t.should_alert(&cfg, &EntropySample { turn: 2, ..hot }));
255
+ // Inside the hysteresis band (0.45 ≥ threshold − hysteresis): stays disarmed.
256
+ assert!(!t.should_alert(&cfg, &EntropySample { turn: 3, score: 0.45, ..hot }));
257
+ assert!(!t.should_alert(&cfg, &EntropySample { turn: 4, ..hot }));
258
+ // Below the band: re-arms; the next crossing fires again.
259
+ assert!(!t.should_alert(&cfg, &EntropySample { turn: 5, score: 0.3, ..hot }));
260
+ assert!(t.should_alert(&cfg, &EntropySample { turn: 6, ..hot }));
261
+ }
262
+
263
+ #[test]
264
+ fn watch_cooldown_gates_refire_even_after_rearm() {
265
+ let cfg = EntropyWatchConfig { enabled: true, threshold: 0.5, hysteresis: 0.1, cooldown_turns: 5, notify_model: false };
266
+ let mut t = EntropyTracker::default();
267
+ let hot = EntropySample { turn: 1, score: 0.9, rho: 0.0, repeat_pressure: 0.0, failure_rate: 0.0, rollbacks_in_window: 0, window_turns: 1 };
268
+ assert!(t.should_alert(&cfg, &hot));
269
+ assert!(!t.should_alert(&cfg, &EntropySample { turn: 2, score: 0.2, ..hot })); // re-arm
270
+ assert!(!t.should_alert(&cfg, &EntropySample { turn: 3, ..hot })); // armed but cooling
271
+ assert!(t.should_alert(&cfg, &EntropySample { turn: 6, ..hot })); // 6−1 ≥ 5
272
+ }
273
+
274
+ #[test]
275
+ fn watch_disabled_never_alerts() {
276
+ let cfg = EntropyWatchConfig::default();
277
+ assert!(!cfg.enabled);
278
+ let mut t = EntropyTracker::default();
279
+ let hot = EntropySample { turn: 1, score: 1.0, rho: 1.0, repeat_pressure: 1.0, failure_rate: 1.0, rollbacks_in_window: 9, window_turns: 8 };
280
+ assert!(!t.should_alert(&cfg, &hot));
281
+ }
282
+ }
@@ -1,9 +1,11 @@
1
+ pub mod entropy;
1
2
  pub mod milestone;
2
3
  pub mod policy;
3
4
  pub mod rollback;
4
5
  pub mod state_machine;
5
6
  pub mod tcb;
6
7
 
8
+ pub use entropy::{EntropySample, EntropyTracker, EntropyWatchConfig};
7
9
  pub use milestone::MilestoneTracker;
8
10
  pub use tcb::{
9
11
  BudgetLedger, TaskId, TaskLifecycle, TaskTable, Tcb, WaitReason,