deepstrike 0.2.35__tar.gz → 0.2.36__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 (184) hide show
  1. {deepstrike-0.2.35 → deepstrike-0.2.36}/Cargo.lock +5 -5
  2. {deepstrike-0.2.35 → deepstrike-0.2.36}/Cargo.toml +2 -2
  3. {deepstrike-0.2.35 → deepstrike-0.2.36}/PKG-INFO +4 -4
  4. {deepstrike-0.2.35 → deepstrike-0.2.36}/README.md +3 -3
  5. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/config.rs +11 -0
  6. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/manager.rs +237 -11
  7. deepstrike-0.2.36/crates/deepstrike-core/src/context/partitions.rs +339 -0
  8. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/renderer.rs +1 -2
  9. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/renewal.rs +5 -4
  10. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/mod.rs +1 -0
  11. deepstrike-0.2.36/crates/deepstrike-core/src/governance/repeat_fuse.rs +48 -0
  12. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/mm/mod.rs +11 -49
  13. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/proc/mod.rs +1 -0
  14. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/kernel.rs +331 -25
  15. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/snapshot.rs +87 -1
  16. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/eviction.rs +31 -16
  17. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/gate.rs +91 -0
  18. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/mod.rs +100 -4
  19. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/signal.rs +2 -0
  20. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/tests.rs +269 -14
  21. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/result.rs +5 -0
  22. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/large_result_spool.py +20 -0
  23. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/runner.py +385 -91
  24. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/sub_agent_orchestrator.py +4 -3
  25. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/types/agent.py +10 -0
  26. {deepstrike-0.2.35 → deepstrike-0.2.36}/pyproject.toml +1 -1
  27. deepstrike-0.2.35/crates/deepstrike-core/src/context/partitions.rs +0 -127
  28. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/Cargo.toml +0 -0
  29. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/compression.rs +0 -0
  30. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  31. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/mod.rs +0 -0
  32. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/pressure.rs +0 -0
  33. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/sections.rs +0 -0
  34. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  35. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
  36. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  37. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/task_state.rs +0 -0
  38. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/text.rs +0 -0
  39. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  40. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  41. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  42. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  43. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  44. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  45. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  46. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  47. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  48. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  49. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/harness/eval.rs +0 -0
  50. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  51. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/lib.rs +0 -0
  52. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  53. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  54. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  55. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  56. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  57. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  58. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  59. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/session.rs +0 -0
  60. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  61. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  62. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/memory/working.rs +0 -0
  63. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/mm/handle.rs +0 -0
  64. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  65. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/orchestration/mod.rs +0 -0
  66. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  67. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
  68. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/orchestration/workflow/mod.rs +0 -0
  69. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/orchestration/workflow/run.rs +0 -0
  70. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
  71. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
  72. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  73. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  74. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/runtime/session.rs +0 -0
  75. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  76. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/mod.rs +0 -0
  77. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  78. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  79. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/capability.rs +0 -0
  80. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/milestone_exec.rs +0 -0
  81. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/process.rs +0 -0
  82. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/state_machine/workflow.rs +0 -0
  83. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
  84. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  85. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  86. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  87. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/signals/router.rs +0 -0
  88. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
  89. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/agent.rs +0 -0
  90. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/capability.rs +0 -0
  91. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/contract.rs +0 -0
  92. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/error.rs +0 -0
  93. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/message.rs +0 -0
  94. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  95. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/mod.rs +0 -0
  96. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/model.rs +0 -0
  97. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/policy.rs +0 -0
  98. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/signal.rs +0 -0
  99. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/skill.rs +0 -0
  100. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-core/src/types/task.rs +0 -0
  101. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-py/Cargo.toml +0 -0
  102. {deepstrike-0.2.35 → deepstrike-0.2.36}/crates/deepstrike-py/src/lib.rs +0 -0
  103. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/__init__.py +0 -0
  104. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/collaboration/__init__.py +0 -0
  105. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/collaboration/contract.py +0 -0
  106. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/collaboration/handoff.py +0 -0
  107. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/collaboration/harness.py +0 -0
  108. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/collaboration/modes.py +0 -0
  109. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/collaboration/pool.py +0 -0
  110. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/governance.py +0 -0
  111. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/harness/__init__.py +0 -0
  112. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/harness/harness.py +0 -0
  113. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/harness/judge.py +0 -0
  114. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/kernel/__init__.py +0 -0
  115. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/knowledge/__init__.py +0 -0
  116. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/knowledge/source.py +0 -0
  117. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/memory/__init__.py +0 -0
  118. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/memory/agent.py +0 -0
  119. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/memory/in_memory_store.py +0 -0
  120. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/memory/protocols.py +0 -0
  121. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/memory/working.py +0 -0
  122. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/__init__.py +0 -0
  123. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/anthropic.py +0 -0
  124. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/anthropic_compatible.py +0 -0
  125. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/base.py +0 -0
  126. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/deepseek.py +0 -0
  127. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/factories.py +0 -0
  128. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/gemini.py +0 -0
  129. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/glm.py +0 -0
  130. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/kimi.py +0 -0
  131. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/minimax.py +0 -0
  132. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/ollama.py +0 -0
  133. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/openai.py +0 -0
  134. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/openai_responses.py +0 -0
  135. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/qwen.py +0 -0
  136. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/replay.py +0 -0
  137. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/replay_validator.py +0 -0
  138. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/stream.py +0 -0
  139. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/providers/vendor_profiles.py +0 -0
  140. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/__init__.py +0 -0
  141. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/archive.py +0 -0
  142. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/credential_vault.py +0 -0
  143. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/eval.py +0 -0
  144. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/event_stream.py +0 -0
  145. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/execution_plane.py +0 -0
  146. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/facade.py +0 -0
  147. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/filtered_plane.py +0 -0
  148. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/kernel_event_log.py +0 -0
  149. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/kernel_step.py +0 -0
  150. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  151. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/os_profile.py +0 -0
  152. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/os_snapshot.py +0 -0
  153. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/output_schema.py +0 -0
  154. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  155. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/provider_replay.py +0 -0
  156. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/reactive_session.py +0 -0
  157. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/reducers.py +0 -0
  158. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  159. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/replay_fixture.py +0 -0
  160. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/replay_provider.py +0 -0
  161. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/replay_sanitize.py +0 -0
  162. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/run_group.py +0 -0
  163. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/session_log.py +0 -0
  164. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/session_repair.py +0 -0
  165. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/turn_policy.py +0 -0
  166. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/workflow_control_flow.py +0 -0
  167. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/workflow_store.py +0 -0
  168. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/runtime/worktree_plane.py +0 -0
  169. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/safety/__init__.py +0 -0
  170. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/safety/permissions.py +0 -0
  171. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/signals/__init__.py +0 -0
  172. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/signals/gateway.py +0 -0
  173. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/signals/scheduled.py +0 -0
  174. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/signals/types.py +0 -0
  175. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/skills/__init__.py +0 -0
  176. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/skills/registry.py +0 -0
  177. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/skills/watcher.py +0 -0
  178. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/tools/__init__.py +0 -0
  179. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/tools/builtin/__init__.py +0 -0
  180. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/tools/builtin/read_file.py +0 -0
  181. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/tools/errors.py +0 -0
  182. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/tools/execution.py +0 -0
  183. {deepstrike-0.2.35 → deepstrike-0.2.36}/deepstrike/tools/registry.py +0 -0
  184. {deepstrike-0.2.35 → deepstrike-0.2.36}/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.35"
197
+ version = "0.2.36"
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.35"
209
+ version = "0.2.36"
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.35"
221
+ version = "0.2.36"
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.35"
232
+ version = "0.2.36"
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.35"
266
+ version = "0.2.36"
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.35"
6
+ version = "0.2.36"
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.35" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.36" }
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.35
3
+ Version: 0.2.36
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -149,7 +149,7 @@ The mechanisms above are not internal refactors — they change what you can bui
149
149
  Tool calls, spawns, compression, and signals pass through one kernel gate with an explicit lifecycle (Ready / Running / Blocked / Suspended). You implement I/O; the kernel decides *when* and *whether*. Node, Python, and Rust share the same decision path, so `wake(session_id)` and cross-language tooling see consistent behavior.
150
150
 
151
151
  **Longer, sturdier sessions (Layer-1 spool + semantic page-out)**
152
- Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore` and satisfies `page_in_requested` on the way back in. Long tasks survive token pressure instead of failing mid-run.
152
+ Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore`. Long tasks survive token pressure instead of failing mid-run.
153
153
 
154
154
  **Safety and governance by default (OS native profile)**
155
155
  Every run loads declarative `governance_policy` (deny / ask_user / rate-limit / param rules) and in-kernel signal routing (`attention_policy`, default queue 64). Dangerous tools, external interrupts, and approval flows are policy — not ad-hoc checks in your handlers.
@@ -443,9 +443,9 @@ effort: 1
443
443
 
444
444
  ## Knowledge
445
445
 
446
- Implement `KnowledgeSource` — the kernel injects a `knowledge` meta-tool. Runtime retrieval results land in **history** as tool results. Use `initial_memory` for durable preload into Slot 2.
446
+ Implement `KnowledgeSource` — the kernel injects a `knowledge` meta-tool. Runtime retrieval results land in **history** as tool results (single-use fact content that decays with compaction), not in the durable `knowledge` partition. Use `initial_memory` for durable preload into Slot 2.
447
447
 
448
- Before tool execution the kernel may emit `page_in_requested`; the SDK satisfies it from `DreamStore`, `KnowledgeSource`, and a local semantic page-out cache, then feeds `page_in` back to the kernel.
448
+ For durable content at runtime use `runner.push_knowledge(content, key=..., pinned=...)` a **keyed** entry upserts on a repeated key and `runner.remove_knowledge(key)` removes it, both applied at the next compaction/renewal boundary. `knowledge_budget_ratio` (default 0.25, 0 disables) caps the partition: over budget, the oldest unpinned, non-skill entries are evicted at boundaries while `pinned=True` entries survive. A loaded skill's body is pinned here as `skill:<name>` and unpinned by `runner.deactivate_skill(name)` or a `skill_lease_turns` expiry.
449
449
 
450
450
  ```python
451
451
  from deepstrike import KnowledgeSource
@@ -136,7 +136,7 @@ The mechanisms above are not internal refactors — they change what you can bui
136
136
  Tool calls, spawns, compression, and signals pass through one kernel gate with an explicit lifecycle (Ready / Running / Blocked / Suspended). You implement I/O; the kernel decides *when* and *whether*. Node, Python, and Rust share the same decision path, so `wake(session_id)` and cross-language tooling see consistent behavior.
137
137
 
138
138
  **Longer, sturdier sessions (Layer-1 spool + semantic page-out)**
139
- Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore` and satisfies `page_in_requested` on the way back in. Long tasks survive token pressure instead of failing mid-run.
139
+ Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore`. Long tasks survive token pressure instead of failing mid-run.
140
140
 
141
141
  **Safety and governance by default (OS native profile)**
142
142
  Every run loads declarative `governance_policy` (deny / ask_user / rate-limit / param rules) and in-kernel signal routing (`attention_policy`, default queue 64). Dangerous tools, external interrupts, and approval flows are policy — not ad-hoc checks in your handlers.
@@ -430,9 +430,9 @@ effort: 1
430
430
 
431
431
  ## Knowledge
432
432
 
433
- Implement `KnowledgeSource` — the kernel injects a `knowledge` meta-tool. Runtime retrieval results land in **history** as tool results. Use `initial_memory` for durable preload into Slot 2.
433
+ Implement `KnowledgeSource` — the kernel injects a `knowledge` meta-tool. Runtime retrieval results land in **history** as tool results (single-use fact content that decays with compaction), not in the durable `knowledge` partition. Use `initial_memory` for durable preload into Slot 2.
434
434
 
435
- Before tool execution the kernel may emit `page_in_requested`; the SDK satisfies it from `DreamStore`, `KnowledgeSource`, and a local semantic page-out cache, then feeds `page_in` back to the kernel.
435
+ For durable content at runtime use `runner.push_knowledge(content, key=..., pinned=...)` a **keyed** entry upserts on a repeated key and `runner.remove_knowledge(key)` removes it, both applied at the next compaction/renewal boundary. `knowledge_budget_ratio` (default 0.25, 0 disables) caps the partition: over budget, the oldest unpinned, non-skill entries are evicted at boundaries while `pinned=True` entries survive. A loaded skill's body is pinned here as `skill:<name>` and unpinned by `runner.deactivate_skill(name)` or a `skill_lease_turns` expiry.
436
436
 
437
437
  ```python
438
438
  from deepstrike import KnowledgeSource
@@ -83,6 +83,16 @@ pub struct ContextConfig {
83
83
 
84
84
  /// Preview byte budget kept in context when a tool result is spooled. Default: 2 KiB.
85
85
  pub spool_preview_bytes: u32,
86
+
87
+ // ── K2: knowledge budget ─────────────────────────────────────────────
88
+
89
+ /// Max share of `max_tokens` the knowledge partition may occupy. Exceeding it emits a
90
+ /// `KnowledgeBudgetExceeded` observation (once per cache generation) and marks the OLDEST
91
+ /// unpinned, non-skill entries for eviction at the next compaction/renewal boundary until the
92
+ /// projected usage fits. Pinned entries and `skill:`-keyed pins are never budget-evicted
93
+ /// (skills are governed by deactivation/lease, not the budget). `0.0` disables (no cap).
94
+ /// Default: 0.25.
95
+ pub knowledge_budget_ratio: f64,
86
96
  }
87
97
 
88
98
  fn default_micro_compact_idle_minutes() -> u32 {
@@ -119,6 +129,7 @@ impl Default for ContextConfig {
119
129
  autocompact_buffer: 13_000,
120
130
  spool_threshold_bytes: 50 * 1024,
121
131
  spool_preview_bytes: 2 * 1024,
132
+ knowledge_budget_ratio: 0.25,
122
133
  }
123
134
  }
124
135
  }
@@ -17,6 +17,8 @@ use compact_str::CompactString;
17
17
 
18
18
  pub const MEMORY_TOOL_NAME: &str = "memory";
19
19
  pub const KNOWLEDGE_TOOL_NAME: &str = "knowledge";
20
+ /// O7: the evicted-result re-fetch meta-tool (see `read_result_tool_schema`).
21
+ pub const READ_RESULT_TOOL_NAME: &str = "read_result";
20
22
 
21
23
  /// Control-plane meta-tools: kernel-handled tools that drive state/capabilities rather than do task
22
24
  /// work. Excluded from the `recent_actions` progress log (2b) so the footer reflects real progress.
@@ -25,11 +27,14 @@ const META_TOOL_NAMES: &[&str] = &[
25
27
  "skill",
26
28
  MEMORY_TOOL_NAME,
27
29
  KNOWLEDGE_TOOL_NAME,
30
+ READ_RESULT_TOOL_NAME,
28
31
  "submit_workflow_nodes",
29
32
  "start_workflow",
30
33
  ];
31
34
 
32
- fn is_meta_tool(name: &str) -> bool {
35
+ /// Control-plane meta-tools are noise, not task progress — filtered out of the recency log (2b)
36
+ /// and out of the O6 repeat-fuse signature (the two must agree on what "an action" is).
37
+ pub(crate) fn is_meta_tool(name: &str) -> bool {
33
38
  META_TOOL_NAMES.contains(&name)
34
39
  }
35
40
 
@@ -46,11 +51,14 @@ pub struct ContextManager {
46
51
  pub sprint: u32,
47
52
  pub last_handoff: Option<HandoffArtifact>,
48
53
  pub skills: SkillCatalog,
49
- /// P1-B tool gating: the set of skills the model has loaded this session (by name). Their
50
- /// declared `allowed_tools` are unioned to narrow the exposed toolset in `emit_call_llm`.
51
- /// A set (not a single value) because the model may load several skills and still needs each
52
- /// one's tools (D1). v1 accumulates (no eviction). Snapshotted for wake/resume.
53
- pub active_skills: std::collections::BTreeSet<CompactString>,
54
+ /// P1-B tool gating: the skills the model has loaded this session (by name), each with an
55
+ /// optional lease expiry turn (K3: `None` = permanent, today's default). Their declared
56
+ /// `allowed_tools` are unioned to narrow the exposed toolset in `emit_call_llm`. A map (not a
57
+ /// single value) because the model may load several skills and still needs each one's tools
58
+ /// (D1). K3 adds eviction: explicit `deactivate_skill` or lease expiry — both also unpin the
59
+ /// skill's `skill:<name>` knowledge entry (boundary-swept). NOT snapshotted — rebuilt on wake
60
+ /// by replaying `skill` tool calls (graceful).
61
+ pub active_skills: std::collections::BTreeMap<CompactString, Option<u32>>,
54
62
  /// P1-B/D stable-core: tool ids that stay exposed even when a skill narrows the toolset (the
55
63
  /// "everyone uses these" set — read/search/bash etc.). Configured once by the SDK; empty by
56
64
  /// default (铁律: no config ⇒ skills narrow to exactly their declared tools + meta-tools).
@@ -90,6 +98,14 @@ pub struct ContextManager {
90
98
  /// compaction (no frozen region yet). Not snapshotted: on resume it resets to 0 and rebuilds at
91
99
  /// the next compaction (graceful — only the deep-cache durability lapses, never correctness).
92
100
  frozen_history_len: usize,
101
+
102
+ /// K1: boundary-sweep results awaiting drain into `KnowledgeSwept` observations. Not
103
+ /// snapshotted (observation-only bookkeeping, same class as `frozen_history_len`).
104
+ pending_knowledge_sweeps: Vec<crate::context::partitions::KnowledgeSweep>,
105
+
106
+ /// K2: whether the budget warning already fired this cache generation (warn-once; reset by
107
+ /// the boundary sweep). Not snapshotted — a resume re-warns at most once, harmless.
108
+ knowledge_budget_warned: bool,
93
109
  }
94
110
 
95
111
  impl ContextManager {
@@ -106,7 +122,7 @@ impl ContextManager {
106
122
  partitions, max_tokens, config, engine,
107
123
  sprint: 0, last_handoff: None,
108
124
  skills: SkillCatalog::new(),
109
- active_skills: std::collections::BTreeSet::new(),
125
+ active_skills: std::collections::BTreeMap::new(),
110
126
  stable_core_tools: std::collections::HashSet::new(),
111
127
  capabilities: CapabilityManifest::new(),
112
128
  sections: ContextSectionRegistry::default_agent_sections(),
@@ -118,6 +134,8 @@ impl ContextManager {
118
134
  handles: HandleTable::new(),
119
135
  next_handle_id: 0,
120
136
  frozen_history_len: 0,
137
+ pending_knowledge_sweeps: Vec::new(),
138
+ knowledge_budget_warned: false,
121
139
  }
122
140
  }
123
141
 
@@ -311,6 +329,9 @@ impl ContextManager {
311
329
  // Compaction rewrote the history prefix — start a fresh collapse generation so
312
330
  // surviving handles re-evaluate from Resident (P0-C: the one cache-free un-collapse point).
313
331
  self.reset_collapse_generation();
332
+ // K1: the prompt-cache prefix is being rebuilt anyway — the one cache-free moment to
333
+ // apply deferred knowledge upserts/removals (rewriting system[1] bytes).
334
+ self.sweep_knowledge_at_boundary();
314
335
  }
315
336
  // P2-D × P1-E: re-anchor the frozen-prefix boundary only when the compaction actually broke
316
337
  // the prompt-cache prefix (`result.3` = the planner's per-step `cache_at` cost, `Some` ⇒ a
@@ -335,6 +356,9 @@ impl ContextManager {
335
356
  // Compaction rewrote the history prefix — start a fresh collapse generation so
336
357
  // surviving handles re-evaluate from Resident (P0-C: the one cache-free un-collapse point).
337
358
  self.reset_collapse_generation();
359
+ // K1: the prompt-cache prefix is being rebuilt anyway — the one cache-free moment to
360
+ // apply deferred knowledge upserts/removals (rewriting system[1] bytes).
361
+ self.sweep_knowledge_at_boundary();
338
362
  }
339
363
  // P2-D × P1-E: re-anchor the frozen-prefix boundary only when the compaction actually broke
340
364
  // the prompt-cache prefix (`result.3` = the planner's per-step `cache_at` cost, `Some` ⇒ a
@@ -373,6 +397,9 @@ impl ContextManager {
373
397
  // Compaction rewrote the history prefix — start a fresh collapse generation so
374
398
  // surviving handles re-evaluate from Resident (P0-C: the one cache-free un-collapse point).
375
399
  self.reset_collapse_generation();
400
+ // K1: the prompt-cache prefix is being rebuilt anyway — the one cache-free moment to
401
+ // apply deferred knowledge upserts/removals (rewriting system[1] bytes).
402
+ self.sweep_knowledge_at_boundary();
376
403
  }
377
404
  // P2-D × P1-E: re-anchor the frozen-prefix boundary only when the compaction actually broke
378
405
  // the prompt-cache prefix (`result.3` = the planner's per-step `cache_at` cost, `Some` ⇒ a
@@ -412,6 +439,8 @@ impl ContextManager {
412
439
  // and start a fresh collapse generation (P0-C) since the whole prefix changed.
413
440
  self.prune_orphaned_handles();
414
441
  self.reset_collapse_generation();
442
+ // K1: renewal is a boundary — apply deferred knowledge upserts/removals now.
443
+ self.sweep_knowledge_at_boundary();
415
444
  // P1-E: the renewed history is the new frozen base.
416
445
  self.frozen_history_len = self.partitions.history.messages.len();
417
446
  }
@@ -438,7 +467,7 @@ impl ContextManager {
438
467
  ContextSnapshot {
439
468
  turn,
440
469
  system_messages: self.partitions.system.messages.clone(),
441
- knowledge_messages: self.partitions.knowledge.messages.clone(),
470
+ knowledge_messages: self.partitions.knowledge.messages().cloned().collect(),
442
471
  history_messages: self.partitions.history.messages.clone(),
443
472
  task_state: self.partitions.task_state.clone(),
444
473
  }
@@ -478,6 +507,83 @@ impl ContextManager {
478
507
  self.partitions.knowledge.push(msg, tokens);
479
508
  }
480
509
 
510
+ /// K1: keyed knowledge push — fresh key appends immediately (cache-cheap direction), an
511
+ /// existing key stages a boundary-deferred upsert. `pinned` entries are exempt from the
512
+ /// K2 budget sweep.
513
+ pub fn push_knowledge_entry(
514
+ &mut self,
515
+ key: Option<CompactString>,
516
+ msg: Message,
517
+ tokens: u32,
518
+ pinned: bool,
519
+ ) {
520
+ self.partitions.knowledge.push_entry(key, msg, tokens, pinned);
521
+ }
522
+
523
+ /// K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
524
+ /// Errs-open: unknown key is a no-op (returns false).
525
+ pub fn remove_knowledge(&mut self, key: &str) -> bool {
526
+ self.partitions.knowledge.remove(key)
527
+ }
528
+
529
+ /// K1: run the boundary sweep (apply pending upserts, drop marked entries) and stash the
530
+ /// result for the state machine to drain into a `KnowledgeSwept` observation. Called only
531
+ /// from the compaction/renewal boundary blocks — the one place system[1] bytes may change.
532
+ fn sweep_knowledge_at_boundary(&mut self) {
533
+ let sweep = self.partitions.knowledge.sweep_at_boundary();
534
+ if sweep.changed {
535
+ self.pending_knowledge_sweeps.push(sweep);
536
+ }
537
+ // K2: a boundary starts a fresh cache generation — the budget warning may fire again.
538
+ self.knowledge_budget_warned = false;
539
+ }
540
+
541
+ /// K2: knowledge-budget check, run per turn before render. Over budget ⇒ mark the OLDEST
542
+ /// unpinned, non-skill entries for eviction at the next boundary until the projected usage
543
+ /// (used − already-marked) fits, and return `Some((used, budget))` ONCE per cache generation
544
+ /// for the `KnowledgeBudgetExceeded` observation (marking itself is idempotent and repeats
545
+ /// harmlessly). Skill pins are exempt — deactivation/lease governs them, the budget never
546
+ /// silently unloads a skill the model believes is active. If marking every eligible entry
547
+ /// still exceeds the budget, the warning stands and the overweight remainder is the host's
548
+ /// explicit choice (errs-open). `knowledge_budget_ratio <= 0.0` disables.
549
+ pub fn enforce_knowledge_budget(&mut self) -> Option<(u32, u32)> {
550
+ let ratio = self.config.knowledge_budget_ratio;
551
+ if ratio <= 0.0 {
552
+ return None;
553
+ }
554
+ let budget = (self.max_tokens as f64 * ratio) as u32;
555
+ let used = self.partitions.knowledge.token_count;
556
+ if used <= budget {
557
+ return None;
558
+ }
559
+ let entries = &mut self.partitions.knowledge.entries;
560
+ let marked: u32 = entries.iter().filter(|e| e.evict_at_boundary).map(|e| e.tokens).sum();
561
+ let mut projected = used.saturating_sub(marked);
562
+ for entry in entries.iter_mut() {
563
+ if projected <= budget {
564
+ break;
565
+ }
566
+ if entry.evict_at_boundary || entry.pinned {
567
+ continue;
568
+ }
569
+ if entry.key.as_deref().is_some_and(|k| k.starts_with("skill:")) {
570
+ continue;
571
+ }
572
+ entry.evict_at_boundary = true;
573
+ projected = projected.saturating_sub(entry.tokens);
574
+ }
575
+ if self.knowledge_budget_warned {
576
+ return None;
577
+ }
578
+ self.knowledge_budget_warned = true;
579
+ Some((used, budget))
580
+ }
581
+
582
+ /// K1: drain boundary-sweep results (state-machine side turns these into observations).
583
+ pub fn take_knowledge_sweeps(&mut self) -> Vec<crate::context::partitions::KnowledgeSweep> {
584
+ std::mem::take(&mut self.pending_knowledge_sweeps)
585
+ }
586
+
481
587
  /// Push a runtime signal into the current turn's State slot.
482
588
  /// Signals are ephemeral — cleared after each render.
483
589
  pub fn push_signal(&mut self, text: String) {
@@ -539,9 +645,45 @@ impl ContextManager {
539
645
 
540
646
  /// P1-B: record that the model has loaded a skill (its content is now in context). Returns
541
647
  /// `true` if this changed the active set — an epoch boundary the SDK can use to re-anchor the
542
- /// prompt cache (D). No-op (returns false) when the skill was already active.
648
+ /// prompt cache (D). Re-activating an already-active skill refreshes its lease (K3) but
649
+ /// returns false (no epoch change).
543
650
  pub fn activate_skill(&mut self, name: impl Into<CompactString>) -> bool {
544
- self.active_skills.insert(name.into())
651
+ self.activate_skill_leased(name, None)
652
+ }
653
+
654
+ /// K3: activate with an optional lease expiry turn (`None` = permanent). Same epoch semantics
655
+ /// as [`Self::activate_skill`]; a re-activation overwrites the prior lease (latest wins).
656
+ pub fn activate_skill_leased(
657
+ &mut self,
658
+ name: impl Into<CompactString>,
659
+ expires_at_turn: Option<u32>,
660
+ ) -> bool {
661
+ self.active_skills.insert(name.into(), expires_at_turn).is_none()
662
+ }
663
+
664
+ /// K3: deactivate a skill — the toolset re-widens at the next `emit_call_llm` (an epoch event,
665
+ /// same cache cost class as activation) and the skill's `skill:<name>` knowledge pin is marked
666
+ /// for the next boundary sweep. Errs-open: not-active is a no-op (returns false).
667
+ pub fn deactivate_skill(&mut self, name: &str) -> bool {
668
+ if self.active_skills.remove(name).is_none() {
669
+ return false;
670
+ }
671
+ self.partitions.knowledge.remove(&format!("skill:{name}"));
672
+ true
673
+ }
674
+
675
+ /// K3: expire skill leases whose turn has passed (mirrors the capability lease sweep — runs at
676
+ /// the head of every event). Each expiry takes the same path as an explicit deactivation.
677
+ pub fn sweep_expired_skill_leases(&mut self, current_turn: u32) {
678
+ let expired: Vec<CompactString> = self
679
+ .active_skills
680
+ .iter()
681
+ .filter(|(_, lease)| lease.is_some_and(|t| current_turn >= t))
682
+ .map(|(name, _)| name.clone())
683
+ .collect();
684
+ for name in expired {
685
+ self.deactivate_skill(&name);
686
+ }
545
687
  }
546
688
 
547
689
  /// P1-B: the tool-id allow-set to narrow the exposed toolset to, given the active skills.
@@ -554,7 +696,7 @@ impl ContextManager {
554
696
  return None;
555
697
  }
556
698
  let mut union = std::collections::HashSet::new();
557
- for name in &self.active_skills {
699
+ for name in self.active_skills.keys() {
558
700
  let declared = self.skills.allowed_tools(name);
559
701
  if declared.is_empty() {
560
702
  return None; // an unrestricted active skill ⇒ no narrowing (D3)
@@ -608,10 +750,42 @@ impl ContextManager {
608
750
  if let Some(t) = self.memory_tool_schema() { tools.push(t); }
609
751
  if let Some(t) = self.knowledge_tool_schema() { tools.push(t); }
610
752
  if let Some(t) = self.plan_tool_schema() { tools.push(t); }
753
+ if let Some(t) = self.read_result_tool_schema() { tools.push(t); }
611
754
  tools.sort_by(|a, b| a.name.cmp(&b.name));
612
755
  tools
613
756
  }
614
757
 
758
+ /// O7: the `read_result` meta-tool — re-fetch a tool result the kernel evicted from context
759
+ /// (spooled to disk / collapsed / paged out). Exposed DYNAMICALLY: only once at least one
760
+ /// handle has actually left residency, so runs that never evict see an unchanged toolset
761
+ /// (progressive disclosure; golden fixtures and cache prefixes stay byte-stable). Content is
762
+ /// host-resolved (spool file / session log) — the kernel only advertises the capability.
763
+ pub fn read_result_tool_schema(&self) -> Option<ToolSchema> {
764
+ let any_evicted = self
765
+ .handles
766
+ .all()
767
+ .iter()
768
+ .any(|h| !h.residency.occupies_context());
769
+ if !any_evicted { return None; }
770
+ Some(ToolSchema {
771
+ name: CompactString::new(READ_RESULT_TOOL_NAME),
772
+ description: "Re-read a tool result that was evicted from context (you see a \
773
+ placeholder like '[…tool result spooled…]' or a collapsed entry). \
774
+ Pass the tool call's call_id; use offset/max_bytes to page through \
775
+ large content."
776
+ .to_string(),
777
+ parameters: serde_json::json!({
778
+ "type": "object",
779
+ "properties": {
780
+ "call_id": { "type": "string" },
781
+ "offset": { "type": "integer", "description": "Byte offset to start from (default 0)." },
782
+ "max_bytes": { "type": "integer", "description": "Max bytes to return (default 4000)." }
783
+ },
784
+ "required": ["call_id"]
785
+ }),
786
+ })
787
+ }
788
+
615
789
  pub fn plan_tool_schema(&self) -> Option<ToolSchema> {
616
790
  if !self.plan_tool_enabled { return None; }
617
791
  Some(ToolSchema {
@@ -1213,4 +1387,56 @@ mod tests {
1213
1387
  assert_eq!(mgr.handles.residency_for_source("c0"), Some(&Residency::Collapsed));
1214
1388
  assert_eq!(mgr.handles.residency_for_source("c5"), Some(&Residency::Resident));
1215
1389
  }
1390
+
1391
+ // ── K2: knowledge budget ─────────────────────────────────────────────────
1392
+
1393
+ #[test]
1394
+ fn knowledge_budget_marks_oldest_unpinned_first_and_warns_once() {
1395
+ // max_tokens 100 × default ratio 0.25 ⇒ budget 25. Four 10-token entries (40 used):
1396
+ // two evictable, one pinned, one skill pin.
1397
+ let mut mgr = ContextManager::new(100);
1398
+ mgr.push_knowledge(Message::system("oldest unkeyed"), 10);
1399
+ mgr.push_knowledge_entry(Some("a".into()), Message::system("keyed"), 10, false);
1400
+ mgr.push_knowledge_entry(Some("p".into()), Message::system("pinned"), 10, true);
1401
+ mgr.push_knowledge_entry(Some("skill:x".into()), Message::system("skill"), 10, false);
1402
+
1403
+ let warn = mgr.enforce_knowledge_budget();
1404
+ assert_eq!(warn, Some((40, 25)));
1405
+ // Oldest-first: unkeyed (10) then "a" (10) marked ⇒ projected 20 ≤ 25 stops there.
1406
+ let e = &mgr.partitions.knowledge.entries;
1407
+ assert!(e[0].evict_at_boundary);
1408
+ assert!(e[1].evict_at_boundary);
1409
+ assert!(!e[2].evict_at_boundary, "pinned exempt");
1410
+ assert!(!e[3].evict_at_boundary, "skill pin exempt");
1411
+
1412
+ // Warn-once per generation; marking stays idempotent.
1413
+ assert_eq!(mgr.enforce_knowledge_budget(), None);
1414
+
1415
+ // The boundary sweep drops the marked entries and re-arms the warning.
1416
+ let sweep = mgr.partitions.knowledge.sweep_at_boundary();
1417
+ assert_eq!(sweep.tokens_freed, 20);
1418
+ assert_eq!(mgr.partitions.knowledge.token_count, 20);
1419
+ // Back under budget ⇒ no further warning even though it re-armed.
1420
+ assert_eq!(mgr.enforce_knowledge_budget(), None);
1421
+ }
1422
+
1423
+ #[test]
1424
+ fn knowledge_budget_warning_stands_when_only_exempt_weight_remains() {
1425
+ let mut mgr = ContextManager::new(100);
1426
+ mgr.push_knowledge_entry(Some("p".into()), Message::system("pinned heavy"), 30, true);
1427
+ mgr.push_knowledge_entry(Some("skill:x".into()), Message::system("skill heavy"), 30, false);
1428
+
1429
+ // Over budget (60 > 25) but nothing evictable — warning fires, nothing marked.
1430
+ assert_eq!(mgr.enforce_knowledge_budget(), Some((60, 25)));
1431
+ assert!(mgr.partitions.knowledge.entries.iter().all(|e| !e.evict_at_boundary));
1432
+ }
1433
+
1434
+ #[test]
1435
+ fn knowledge_budget_ratio_zero_disables() {
1436
+ let mut mgr = ContextManager::new(100);
1437
+ mgr.config.knowledge_budget_ratio = 0.0;
1438
+ mgr.push_knowledge(Message::system("huge"), 90);
1439
+ assert_eq!(mgr.enforce_knowledge_budget(), None);
1440
+ assert!(!mgr.partitions.knowledge.entries[0].evict_at_boundary);
1441
+ }
1216
1442
  }