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.
- {deepstrike-0.2.37 → deepstrike-0.2.38}/Cargo.lock +5 -5
- {deepstrike-0.2.37 → deepstrike-0.2.38}/Cargo.toml +2 -2
- {deepstrike-0.2.37 → deepstrike-0.2.38}/PKG-INFO +1 -1
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/lib.rs +1 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/kernel.rs +118 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/session.rs +21 -0
- deepstrike-0.2.38/crates/deepstrike-core/src/scheduler/entropy.rs +282 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/mod.rs +2 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/mod.rs +82 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/tests.rs +136 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/__init__.py +2 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/stream.py +37 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/kernel_event_log.py +19 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/runner.py +60 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/session_log.py +21 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/pyproject.toml +1 -1
- {deepstrike-0.2.37 → deepstrike-0.2.38}/README.md +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/Cargo.toml +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/compression.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/config.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/fault.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/manager.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/partitions.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/pressure.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/renderer.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/renewal.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/task_state.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/text.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/permission.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/quota.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/repeat_fuse.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/governance/veto.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/harness/eval.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/harness/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/curator.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/durable.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/mm/handle.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/mm/memory.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/mm/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/workflow/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/orchestration/workflow/run.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/proc/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/capability.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/eviction.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/gate.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/milestone_exec.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/process.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/signal.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/state_machine/workflow.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/attention.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/queue.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/signals/router.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/agent.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/capability.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/contract.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/error.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/message.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/milestone.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/mod.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/policy.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/result.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/signal.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/skill.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-core/src/types/task.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-py/Cargo.toml +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/crates/deepstrike-py/src/lib.rs +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/contract.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/handoff.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/harness.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/modes.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/collaboration/pool.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/governance.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/harness/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/harness/harness.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/harness/judge.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/kernel/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/knowledge/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/knowledge/source.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/agent.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/in_memory_store.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/protocols.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/memory/working.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/anthropic.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/anthropic_compatible.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/base.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/deepseek.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/factories.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/gemini.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/glm.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/kimi.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/minimax.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/ollama.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/openai.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/openai_responses.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/qwen.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/replay.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/replay_validator.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/providers/vendor_profiles.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/archive.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/credential_vault.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/eval.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/event_stream.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/execution_plane.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/facade.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/filtered_plane.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/kernel_step.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/large_result_spool.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/loop_driver.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/os_profile.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/os_snapshot.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/output_schema.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/process_sandbox_plane.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/provider_replay.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/reactive_session.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/reducers.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/remote_vpc_plane.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/replay_fixture.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/replay_provider.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/replay_sanitize.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/run_group.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/session_repair.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/turn_policy.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/workflow_control_flow.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/workflow_store.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/runtime/worktree_plane.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/safety/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/safety/permissions.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/gateway.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/scheduled.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/signals/types.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/skills/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/skills/registry.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/skills/watcher.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/builtin/__init__.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/builtin/read_file.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/errors.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/execution.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/tools/registry.py +0 -0
- {deepstrike-0.2.37 → deepstrike-0.2.38}/deepstrike/types/__init__.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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,
|