deepstrike 0.2.32__tar.gz → 0.2.34__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 (182) hide show
  1. {deepstrike-0.2.32 → deepstrike-0.2.34}/Cargo.lock +5 -5
  2. {deepstrike-0.2.32 → deepstrike-0.2.34}/Cargo.toml +2 -2
  3. {deepstrike-0.2.32 → deepstrike-0.2.34}/PKG-INFO +1 -1
  4. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/config.rs +10 -0
  5. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/manager.rs +66 -0
  6. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/renderer.rs +222 -25
  7. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/task_state.rs +25 -0
  8. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/mod.rs +36 -0
  9. {deepstrike-0.2.32 → deepstrike-0.2.34}/pyproject.toml +1 -1
  10. {deepstrike-0.2.32 → deepstrike-0.2.34}/README.md +0 -0
  11. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/Cargo.toml +0 -0
  12. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/compression.rs +0 -0
  13. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  14. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/mod.rs +0 -0
  15. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  16. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/pressure.rs +0 -0
  17. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  18. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/sections.rs +0 -0
  19. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  20. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
  21. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  22. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/text.rs +0 -0
  23. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  24. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  25. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  26. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  27. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  28. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  29. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  30. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  31. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  32. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  33. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  34. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/harness/eval.rs +0 -0
  35. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  36. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/lib.rs +0 -0
  37. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  38. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  39. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  40. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  41. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  42. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  43. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  44. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/session.rs +0 -0
  45. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  46. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  47. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/memory/working.rs +0 -0
  48. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/mm/handle.rs +0 -0
  49. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  50. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  51. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/orchestration/mod.rs +0 -0
  52. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  53. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
  54. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/orchestration/workflow/mod.rs +0 -0
  55. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/orchestration/workflow/run.rs +0 -0
  56. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/proc/mod.rs +0 -0
  57. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
  58. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/kernel.rs +0 -0
  59. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
  60. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  61. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  62. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/session.rs +0 -0
  63. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/runtime/snapshot.rs +0 -0
  64. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  65. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/mod.rs +0 -0
  66. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  67. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  68. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/capability.rs +0 -0
  69. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/eviction.rs +0 -0
  70. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/gate.rs +0 -0
  71. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/milestone_exec.rs +0 -0
  72. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/process.rs +0 -0
  73. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/signal.rs +0 -0
  74. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/tests.rs +0 -0
  75. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/state_machine/workflow.rs +0 -0
  76. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
  77. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  78. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  79. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  80. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/signals/router.rs +0 -0
  81. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
  82. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/agent.rs +0 -0
  83. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/capability.rs +0 -0
  84. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/contract.rs +0 -0
  85. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/error.rs +0 -0
  86. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/message.rs +0 -0
  87. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  88. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/mod.rs +0 -0
  89. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/model.rs +0 -0
  90. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/policy.rs +0 -0
  91. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/result.rs +0 -0
  92. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/signal.rs +0 -0
  93. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/skill.rs +0 -0
  94. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-core/src/types/task.rs +0 -0
  95. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-py/Cargo.toml +0 -0
  96. {deepstrike-0.2.32 → deepstrike-0.2.34}/crates/deepstrike-py/src/lib.rs +0 -0
  97. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/__init__.py +0 -0
  98. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/collaboration/__init__.py +0 -0
  99. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/collaboration/contract.py +0 -0
  100. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/collaboration/handoff.py +0 -0
  101. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/collaboration/harness.py +0 -0
  102. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/collaboration/modes.py +0 -0
  103. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/collaboration/pool.py +0 -0
  104. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/governance.py +0 -0
  105. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/harness/__init__.py +0 -0
  106. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/harness/harness.py +0 -0
  107. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/harness/judge.py +0 -0
  108. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/kernel/__init__.py +0 -0
  109. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/knowledge/__init__.py +0 -0
  110. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/knowledge/source.py +0 -0
  111. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/memory/__init__.py +0 -0
  112. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/memory/agent.py +0 -0
  113. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/memory/in_memory_store.py +0 -0
  114. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/memory/protocols.py +0 -0
  115. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/memory/working.py +0 -0
  116. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/__init__.py +0 -0
  117. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/anthropic.py +0 -0
  118. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/anthropic_compatible.py +0 -0
  119. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/base.py +0 -0
  120. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/deepseek.py +0 -0
  121. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/factories.py +0 -0
  122. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/gemini.py +0 -0
  123. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/glm.py +0 -0
  124. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/kimi.py +0 -0
  125. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/minimax.py +0 -0
  126. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/ollama.py +0 -0
  127. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/openai.py +0 -0
  128. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/openai_responses.py +0 -0
  129. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/qwen.py +0 -0
  130. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/replay.py +0 -0
  131. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/replay_validator.py +0 -0
  132. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/stream.py +0 -0
  133. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/providers/vendor_profiles.py +0 -0
  134. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/__init__.py +0 -0
  135. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/archive.py +0 -0
  136. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/credential_vault.py +0 -0
  137. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/eval.py +0 -0
  138. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/event_stream.py +0 -0
  139. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/execution_plane.py +0 -0
  140. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/facade.py +0 -0
  141. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/filtered_plane.py +0 -0
  142. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/kernel_event_log.py +0 -0
  143. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/kernel_step.py +0 -0
  144. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/large_result_spool.py +0 -0
  145. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  146. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/os_profile.py +0 -0
  147. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/os_snapshot.py +0 -0
  148. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/output_schema.py +0 -0
  149. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  150. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/provider_replay.py +0 -0
  151. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/reactive_session.py +0 -0
  152. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/reducers.py +0 -0
  153. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  154. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/replay_fixture.py +0 -0
  155. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/replay_provider.py +0 -0
  156. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/replay_sanitize.py +0 -0
  157. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/run_group.py +0 -0
  158. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/runner.py +0 -0
  159. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/session_log.py +0 -0
  160. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/session_repair.py +0 -0
  161. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
  162. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/turn_policy.py +0 -0
  163. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/workflow_control_flow.py +0 -0
  164. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/workflow_store.py +0 -0
  165. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/runtime/worktree_plane.py +0 -0
  166. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/safety/__init__.py +0 -0
  167. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/safety/permissions.py +0 -0
  168. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/signals/__init__.py +0 -0
  169. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/signals/gateway.py +0 -0
  170. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/signals/scheduled.py +0 -0
  171. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/signals/types.py +0 -0
  172. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/skills/__init__.py +0 -0
  173. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/skills/registry.py +0 -0
  174. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/skills/watcher.py +0 -0
  175. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/tools/__init__.py +0 -0
  176. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/tools/builtin/__init__.py +0 -0
  177. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/tools/builtin/read_file.py +0 -0
  178. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/tools/errors.py +0 -0
  179. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/tools/execution.py +0 -0
  180. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/tools/registry.py +0 -0
  181. {deepstrike-0.2.32 → deepstrike-0.2.34}/deepstrike/types/__init__.py +0 -0
  182. {deepstrike-0.2.32 → deepstrike-0.2.34}/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.32"
197
+ version = "0.2.34"
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.32"
209
+ version = "0.2.34"
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.32"
221
+ version = "0.2.34"
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.32"
232
+ version = "0.2.34"
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.32"
266
+ version = "0.2.34"
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.32"
6
+ version = "0.2.34"
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.32" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.34" }
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.32
3
+ Version: 0.2.34
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -49,6 +49,15 @@ pub struct ContextConfig {
49
49
  /// Defaults to false; uses concise natural-language notes instead.
50
50
  pub verbose_control_notes: bool,
51
51
 
52
+ /// Collapse the *narration* text of OLD assistant turns (those past the
53
+ /// `preserve_recent_msgs` window that also carry tool calls) to a short stub at render time —
54
+ /// non-destructively (the full text stays in `partitions.history`). The model's user-facing
55
+ /// preamble ("好的,我来…先X") has no value once it has aged out of the recent window, but
56
+ /// re-feeding it verbatim every turn primes the model to keep emitting the same preamble (an
57
+ /// in-context repetition trap). Tool calls and pairing are untouched; current progress lives in
58
+ /// the TASK STATE turn. Defaults to true.
59
+ pub collapse_assistant_narration: bool,
60
+
52
61
  // ── Layer 3: Time-based decay ───────────────────────────────────────
53
62
 
54
63
  /// Minutes of inactivity before triggering Micro-Compact (Layer 3).
@@ -104,6 +113,7 @@ impl Default for ContextConfig {
104
113
  preserve_recent_turns: 2,
105
114
  render_dashboard: false,
106
115
  verbose_control_notes: false,
116
+ collapse_assistant_narration: true,
107
117
  micro_compact_idle_minutes: 60,
108
118
  preserved_tool_results: 5,
109
119
  autocompact_buffer: 13_000,
@@ -18,6 +18,21 @@ use compact_str::CompactString;
18
18
  pub const MEMORY_TOOL_NAME: &str = "memory";
19
19
  pub const KNOWLEDGE_TOOL_NAME: &str = "knowledge";
20
20
 
21
+ /// Control-plane meta-tools: kernel-handled tools that drive state/capabilities rather than do task
22
+ /// work. Excluded from the `recent_actions` progress log (2b) so the footer reflects real progress.
23
+ const META_TOOL_NAMES: &[&str] = &[
24
+ "update_plan",
25
+ "skill",
26
+ MEMORY_TOOL_NAME,
27
+ KNOWLEDGE_TOOL_NAME,
28
+ "submit_workflow_nodes",
29
+ "start_workflow",
30
+ ];
31
+
32
+ fn is_meta_tool(name: &str) -> bool {
33
+ META_TOOL_NAMES.contains(&name)
34
+ }
35
+
21
36
  /// Internal context engine backing [`crate::runtime::KernelRuntime`].
22
37
  ///
23
38
  /// Exposed for in-crate use and tests; external callers should drive the kernel
@@ -411,6 +426,7 @@ impl ContextManager {
411
426
  self.config.preserve_recent_msgs,
412
427
  &self.handles,
413
428
  self.frozen_history_len,
429
+ self.config.collapse_assistant_narration,
414
430
  )
415
431
  }
416
432
 
@@ -485,6 +501,24 @@ impl ContextManager {
485
501
  self.partitions.task_state.apply(update);
486
502
  }
487
503
 
504
+ /// 2b: record this turn's tool activity into the task-state recency log (kernel-derived progress
505
+ /// that feeds the State-turn footer). Each entry is `(name, compact_args)`; the rendered signature
506
+ /// is `name(args)` (or bare `name` for no-arg calls) so the no-progress STOP keys on the WHOLE
507
+ /// call — same tool with different args (a legit loop over items) reads as distinct progress, not
508
+ /// a repeat. Control-plane meta-tools (plan/skill/memory/knowledge/workflow authoring) are noise,
509
+ /// not task progress — filtered by name. A turn with only meta-tool calls records nothing.
510
+ pub fn note_tool_actions(&mut self, calls: &[(String, String)]) {
511
+ let summary = calls
512
+ .iter()
513
+ .filter(|(name, _)| !is_meta_tool(name))
514
+ .map(|(name, args)| {
515
+ if args.is_empty() { name.clone() } else { format!("{name}({args})") }
516
+ })
517
+ .collect::<Vec<_>>()
518
+ .join(", ");
519
+ self.partitions.task_state.note_actions(summary);
520
+ }
521
+
488
522
  // ── Section pinning ───────────────────────────────────────────────────────
489
523
 
490
524
  pub fn pin_section(&mut self, id: &str) -> bool { self.sections.pin(id) }
@@ -635,6 +669,38 @@ mod tests {
635
669
  use crate::types::message::Message;
636
670
  use crate::types::skill::SkillMetadata;
637
671
 
672
+ #[test]
673
+ fn note_tool_actions_keys_on_name_and_args_so_legit_loops_dont_false_stop() {
674
+ // Same tool, DIFFERENT args across turns = real progress (e.g. process item 1, 2, 3) —
675
+ // must NOT trip the no-progress STOP backstop.
676
+ let mut mgr = ContextManager::new(100_000);
677
+ mgr.init_task("process items".to_string(), vec![]);
678
+ mgr.note_tool_actions(&[("step".to_string(), "{\"n\":1}".to_string())]);
679
+ mgr.note_tool_actions(&[("step".to_string(), "{\"n\":2}".to_string())]);
680
+ mgr.note_tool_actions(&[("step".to_string(), "{\"n\":3}".to_string())]);
681
+ assert_eq!(
682
+ mgr.partitions.task_state.recent_actions,
683
+ ["step({\"n\":1})", "step({\"n\":2})", "step({\"n\":3})"]
684
+ );
685
+ let txt = mgr.render().state_turn.unwrap().content.as_text().unwrap().to_string();
686
+ assert!(!txt.contains("STOP:"), "same-tool/diff-args loop must not trip STOP: {txt}");
687
+
688
+ // Genuine stall — same tool, SAME args repeated — DOES trip the STOP.
689
+ let mut mgr2 = ContextManager::new(100_000);
690
+ mgr2.init_task("g".to_string(), vec![]);
691
+ for _ in 0..3 {
692
+ mgr2.note_tool_actions(&[("document_read".to_string(), "{\"id\":\"x\"}".to_string())]);
693
+ }
694
+ let txt2 = mgr2.render().state_turn.unwrap().content.as_text().unwrap().to_string();
695
+ assert!(txt2.contains("STOP:"), "identical repeated call must trip STOP: {txt2}");
696
+
697
+ // Meta-tools are control plane, not task progress — filtered out entirely.
698
+ let mut mgr3 = ContextManager::new(100_000);
699
+ mgr3.init_task("g".to_string(), vec![]);
700
+ mgr3.note_tool_actions(&[("update_plan".to_string(), "{\"current_step\":1}".to_string())]);
701
+ assert!(mgr3.partitions.task_state.recent_actions.is_empty());
702
+ }
703
+
638
704
  #[test]
639
705
  fn manager_renew_uses_task_state_goal() {
640
706
  let mut mgr = ContextManager::new(1_000);
@@ -124,27 +124,69 @@ fn build_system_knowledge(partitions: &ContextPartitions) -> String {
124
124
  .join("\n\n")
125
125
  }
126
126
 
127
- /// P1-F: a one-line recency footer restating the current focus goal, active plan step, and the
128
- /// most recent standing directive. It is rendered as the *last* content before the "Proceed."
129
- /// anchor, the highest-attention position in the prompt (the model attends most to the final
130
- /// tokens). The full TASK STATE block still leads the turn for primacy + reference; this footer
131
- /// just re-surfaces "what to do right now" where attention peaks. `None` when there is no goal.
127
+ /// P1-F (+ 2b/2c): a one-line recency footer at the *last* content before the "Proceed." anchor
128
+ /// the highest-attention position in the prompt (the model attends most to the final tokens).
129
+ ///
130
+ /// It LEADS WITH FORWARD MOTION (what just happened · what to do next · the standing directive), not
131
+ /// a verbatim restatement of the goal. Re-injecting the bare goal at this peak-attention slot every
132
+ /// turn primes the model to *re-narrate intent* ("好的,我来将<goal>…") instead of acting — an
133
+ /// undamped repetition trap when there is no plan/progress to advance. The full goal still LEADS the
134
+ /// TASK STATE block above (primacy + reference), so goal-adherence is preserved; the footer restates
135
+ /// the goal only when nothing has happened yet (e.g. turn 1, no actions). `None` when there is no goal.
136
+ ///
137
+ /// The "just did" clause is kernel-derived from `recent_actions` (real tool activity), and a trailing
138
+ /// run of an identical action raises an explicit STOP — a cheap no-progress backstop that breaks the
139
+ /// read→re-read→re-narrate loop in-band, at the position the model weights most.
132
140
  fn salience_footer(ts: &TaskState) -> Option<String> {
133
141
  if ts.goal.is_empty() {
134
142
  return None;
135
143
  }
136
- let mut s = format!("→ focus: {}", ts.goal);
137
- if let Some(i) = ts.current_step {
138
- if let Some(step) = ts.plan.get(i) {
139
- if !step.done {
140
- s.push_str(&format!(" · step {}: {}", i + 1, step.label));
141
- }
144
+ let mut clauses: Vec<String> = Vec::new();
145
+
146
+ // What just happened — display tool NAMES only. The full `name(args)` signatures are kept in
147
+ // `recent_actions` for the repeat check below, but rendering them every turn bloats the volatile
148
+ // footer; the names alone show motion at the peak-attention slot.
149
+ let recent = ts.recent_actions.as_slice();
150
+ let action_name = |entry: &str| entry.split('(').next().unwrap_or(entry).to_string();
151
+ if let Some(last) = recent.last() {
152
+ let start = recent.len().saturating_sub(3);
153
+ let names = recent[start..].iter().map(|e| action_name(e)).collect::<Vec<_>>().join(" → ");
154
+ clauses.push(format!("did: {names}"));
155
+
156
+ // No-progress backstop: the SAME call — name AND args — repeated on the last ≥2 turns is a
157
+ // stall (a legit loop varies its args, so it reads as distinct progress, not a repeat).
158
+ let trailing_repeat = recent.iter().rev().take_while(|a| *a == last).count();
159
+ if trailing_repeat >= 2 {
160
+ clauses.push(format!(
161
+ "STOP: `{}` repeated {trailing_repeat}× unchanged — do something different or report",
162
+ action_name(last)
163
+ ));
142
164
  }
143
165
  }
166
+
167
+ // What to do next — the active plan step if the model maintains one, else a short forward nudge.
168
+ let active_step = ts
169
+ .current_step
170
+ .and_then(|i| ts.plan.get(i).map(|s| (i, s)))
171
+ .filter(|(_, s)| !s.done);
172
+ if let Some((i, step)) = active_step {
173
+ clauses.push(format!("next: step {} — {}", i + 1, step.label));
174
+ } else if !recent.is_empty() {
175
+ clauses.push("next: advance the goal".to_string());
176
+ }
177
+
144
178
  if let Some(d) = ts.directives.last() {
145
- s.push_str(&format!(" · must: {d}"));
179
+ clauses.push(format!("must: {d}"));
146
180
  }
147
- Some(s)
181
+
182
+ // Lead with the goal only when no forward clause fills the footer (turn 1, nothing done yet);
183
+ // otherwise the forward clauses carry the salience and the goal stays in the block above.
184
+ let body = if clauses.is_empty() {
185
+ format!("→ focus: {}", ts.goal)
186
+ } else {
187
+ format!("→ {}", clauses.join(" · "))
188
+ };
189
+ Some(body)
148
190
  }
149
191
 
150
192
  /// Build the State turn (the volatile tail): task_state + signals + a recency focus footer +
@@ -194,6 +236,37 @@ fn collapse_preview(output: &str) -> String {
194
236
  )
195
237
  }
196
238
 
239
+ /// Stub substituted for a collapsed assistant preamble. Carries no goal text (that would re-seed the
240
+ /// very repetition this removes) and points the model at the authoritative State turn instead.
241
+ const NARRATION_STUB: &str = "[earlier narration collapsed; tool call(s) preserved below — current progress is in the TASK STATE block]";
242
+
243
+ /// Minimum narration length (chars, CJK-aware) worth collapsing. Short preambles aren't worth a
244
+ /// stub substitution (and the one-time cache churn it costs as the turn ages out of the window).
245
+ const NARRATION_COLLAPSE_MIN_CHARS: usize = 40;
246
+
247
+ /// Method 1: read-time collapse of an OLD assistant turn's narration. Targets exactly the
248
+ /// "preamble before action" turns — `Role::Assistant`, a `Content::Text` body, AND a non-empty
249
+ /// `tool_calls` (the model narrated intent, then acted). Returns a projected copy whose text is
250
+ /// replaced by [`NARRATION_STUB`] while `tool_calls` (and thus tool_use/tool_result pairing) are
251
+ /// left intact; the original full text stays in `partitions.history`, so the projection reverses if
252
+ /// the flag is turned off. `None` when the message isn't a collapsible narration turn or the flag is
253
+ /// off. Caller restricts this to messages already past the protected recent window.
254
+ fn project_assistant_narration(msg: &Message, enabled: bool) -> Option<Message> {
255
+ if !enabled || msg.role != Role::Assistant || msg.tool_calls.is_empty() {
256
+ return None;
257
+ }
258
+ let Content::Text(text) = &msg.content else {
259
+ return None;
260
+ };
261
+ if text == NARRATION_STUB || text.chars().count() < NARRATION_COLLAPSE_MIN_CHARS {
262
+ return None;
263
+ }
264
+ let mut projected = msg.clone();
265
+ projected.content = Content::Text(NARRATION_STUB.to_string());
266
+ projected.token_count = None; // recomputed against the smaller stub
267
+ Some(projected)
268
+ }
269
+
197
270
  /// If any of `msg`'s tool-result parts is `Collapsed` per the handle table, return a projected
198
271
  /// copy with those parts previewed; `None` if nothing is collapsed (render the message as-is).
199
272
  fn project_message(msg: &Message, handles: &HandleTable) -> Option<Message> {
@@ -240,7 +313,9 @@ pub fn render(
240
313
  engine: &ContextTokenEngine,
241
314
  preserve_recent_msgs: usize,
242
315
  ) -> RenderedContext {
243
- render_projected(partitions, budget, engine, preserve_recent_msgs, &HandleTable::new(), 0)
316
+ // The convenience wrapper renders history verbatim (no narration collapse) — callers that want
317
+ // Method-1 collapse drive `render_projected` with the flag (the kernel passes it from config).
318
+ render_projected(partitions, budget, engine, preserve_recent_msgs, &HandleTable::new(), 0, false)
244
319
  }
245
320
 
246
321
  /// Render with Layer-4 read-time projection driven by `handles`: tool results whose handle is
@@ -258,6 +333,7 @@ pub fn render_projected(
258
333
  preserve_recent_msgs: usize,
259
334
  handles: &HandleTable,
260
335
  frozen_history_len: usize,
336
+ collapse_narration: bool,
261
337
  ) -> RenderedContext {
262
338
  let system_stable = build_system_stable(partitions);
263
339
  let system_knowledge = build_system_knowledge(partitions);
@@ -275,8 +351,14 @@ pub fn render_projected(
275
351
  // message: a collapsed tool result renders as a preview and is costed at its reduced size.
276
352
  let mut kept_rev: Vec<Message> = Vec::new();
277
353
  for msg in partitions.history.messages.iter().rev() {
278
- // `projected` is `Some` only when read-time projection shrank the message.
279
- let projected = project_message(msg, handles);
354
+ let is_protected = kept_rev.len() < preserve_recent_msgs;
355
+ // `projected` is `Some` only when read-time projection shrank the message. Two disjoint
356
+ // sources: a `Collapsed` tool-result preview (handle-driven, any age) OR — once the turn has
357
+ // aged past the protected recent window — an assistant-narration stub (Method 1). A message
358
+ // is either a tool-result Parts message or an assistant Text+tool_calls message, never both.
359
+ let projected = project_message(msg, handles).or_else(|| {
360
+ if is_protected { None } else { project_assistant_narration(msg, collapse_narration) }
361
+ });
280
362
  let effective = projected.as_ref().unwrap_or(msg);
281
363
  let tokens = match &projected {
282
364
  Some(p) => engine.count_message(p),
@@ -284,7 +366,6 @@ pub fn render_projected(
284
366
  };
285
367
  if tokens == 0 { continue; }
286
368
 
287
- let is_protected = kept_rev.len() < preserve_recent_msgs;
288
369
  if is_protected {
289
370
  kept_rev.push(effective.clone());
290
371
  remaining = remaining.saturating_sub(tokens);
@@ -455,7 +536,7 @@ mod tests {
455
536
  h.residency = Residency::Collapsed;
456
537
  handles.insert(h);
457
538
 
458
- let rc = render_projected(&c, 10_000, &engine(), 4, &handles, 0);
539
+ let rc = render_projected(&c, 10_000, &engine(), 4, &handles, 0, false);
459
540
  let rendered: String = rc
460
541
  .turns
461
542
  .iter()
@@ -498,7 +579,7 @@ mod tests {
498
579
  let mut handles = HandleTable::new();
499
580
  handles.insert(Handle::resident_for(1, HandleKind::ToolResult, 60, "c2"));
500
581
 
501
- let rc = render_projected(&c, 10_000, &engine(), 4, &handles, 0);
582
+ let rc = render_projected(&c, 10_000, &engine(), 4, &handles, 0, false);
502
583
  let rendered: String = rc
503
584
  .turns
504
585
  .iter()
@@ -518,7 +599,7 @@ mod tests {
518
599
  // ── P1-F: state-turn recency footer ───────────────────────────────────
519
600
 
520
601
  #[test]
521
- fn state_turn_ends_with_salience_footer_before_proceed() {
602
+ fn state_turn_footer_leads_with_next_step_not_bare_goal() {
522
603
  let mut c = ctx();
523
604
  c.task_state = TaskState {
524
605
  goal: "ship the cache work".to_string(),
@@ -530,14 +611,56 @@ mod tests {
530
611
  let rc = render(&c, 100_000, &engine(), 4);
531
612
  let text = rc.state_turn.unwrap().content.as_text().unwrap().to_string();
532
613
 
533
- // The full TASK STATE block still leads (primacy) ...
614
+ // The full TASK STATE block still LEADS (primacy) — goal-adherence preserved ...
534
615
  assert!(text.starts_with("[TASK STATE] goal: ship the cache work"));
535
- // ... and the very last block before "Proceed." is the focus footer (recency).
616
+ // ... but the peak-attention footer leads with the forward action, not a goal restatement.
536
617
  let before_proceed = text.rsplit_once("\n\nProceed.").expect("ends with Proceed").0;
537
618
  let last_block = before_proceed.rsplit("\n\n").next().unwrap();
538
- assert!(last_block.starts_with("→ focus: ship the cache work"), "got: {last_block}");
539
- assert!(last_block.contains("step 1: do E"));
619
+ assert!(last_block.starts_with("→ next: step 1 do E"), "got: {last_block}");
540
620
  assert!(last_block.contains("must: don't break ABI"));
621
+ // The bare goal must NOT be re-injected at the peak-attention tail (the repetition fuel).
622
+ assert!(!last_block.contains("focus: ship the cache work"), "got: {last_block}");
623
+ }
624
+
625
+ #[test]
626
+ fn footer_falls_back_to_focus_goal_when_nothing_done_yet() {
627
+ // Turn 1: no actions, no plan — the footer surfaces the goal so the model knows the objective.
628
+ let mut c = ctx();
629
+ c.task_state = TaskState { goal: "build the thing".to_string(), ..Default::default() };
630
+ let rc = render(&c, 100_000, &engine(), 4);
631
+ let text = rc.state_turn.unwrap().content.as_text().unwrap().to_string();
632
+ let footer = text.rsplit_once("\n\nProceed.").unwrap().0.rsplit("\n\n").next().unwrap();
633
+ assert_eq!(footer, "→ focus: build the thing");
634
+ }
635
+
636
+ #[test]
637
+ fn footer_shows_recent_actions_and_forward_nudge_without_a_plan() {
638
+ // No curated plan, but real tool activity (2b) → the footer shows motion + a forward nudge,
639
+ // and the goal is NOT restated at the tail.
640
+ let mut c = ctx();
641
+ c.task_state = TaskState { goal: "rebuild §4.4 as SVG".to_string(), ..Default::default() };
642
+ c.task_state.note_actions("module_list");
643
+ c.task_state.note_actions("module_read");
644
+ let rc = render(&c, 100_000, &engine(), 4);
645
+ let footer = rc.state_turn.unwrap().content.as_text().unwrap()
646
+ .rsplit_once("\n\nProceed.").unwrap().0.rsplit("\n\n").next().unwrap().to_string();
647
+ assert!(footer.contains("did: module_list → module_read"), "got: {footer}");
648
+ assert!(footer.contains("next: advance the goal"), "got: {footer}");
649
+ assert!(!footer.contains("focus: rebuild §4.4 as SVG"), "goal must not lead the footer");
650
+ }
651
+
652
+ #[test]
653
+ fn footer_raises_stop_on_repeated_action() {
654
+ // The same action on the last ≥2 turns ⇒ explicit STOP backstop (breaks the read-loop in-band).
655
+ let mut c = ctx();
656
+ c.task_state = TaskState { goal: "g".to_string(), ..Default::default() };
657
+ c.task_state.note_actions("document_read");
658
+ c.task_state.note_actions("document_read");
659
+ c.task_state.note_actions("document_read");
660
+ let rc = render(&c, 100_000, &engine(), 4);
661
+ let footer = rc.state_turn.unwrap().content.as_text().unwrap()
662
+ .rsplit_once("\n\nProceed.").unwrap().0.rsplit("\n\n").next().unwrap().to_string();
663
+ assert!(footer.contains("STOP: `document_read` repeated 3×"), "got: {footer}");
541
664
  }
542
665
 
543
666
  #[test]
@@ -627,13 +750,87 @@ mod tests {
627
750
  let mut h = Handle::resident_for(1, HandleKind::ToolResult, 250, "c1");
628
751
  h.residency = Residency::Collapsed;
629
752
  handles.insert(h);
630
- let collapsed = render_projected(&c, 100_000, &engine(), 4, &handles, 0).prefix_fingerprint();
753
+ let collapsed = render_projected(&c, 100_000, &engine(), 4, &handles, 0, false).prefix_fingerprint();
631
754
 
632
755
  // turn 0 ("start") is byte-stable; the collapsed tool result at turn 1 drifts.
633
756
  assert_eq!(collapsed.common_turn_prefix(&resident), 1, "drift begins at the collapsed turn");
634
757
  assert!(!collapsed.extends(&resident));
635
758
  }
636
759
 
760
+ // ── Method 1: assistant-narration collapse ─────────────────────────────
761
+
762
+ fn assistant_with_call(text: &str) -> Message {
763
+ let mut m = Message::assistant(text);
764
+ m.tool_calls = vec![crate::types::message::ToolCall {
765
+ id: "c1".into(),
766
+ name: "module_read".into(),
767
+ arguments: serde_json::json!({}),
768
+ }];
769
+ m
770
+ }
771
+
772
+ #[test]
773
+ fn old_assistant_narration_collapses_keeping_tool_calls() {
774
+ let mut c = ctx();
775
+ // Oldest = a long preamble + a tool call; then enough recent turns to push it past the window.
776
+ c.history.push(assistant_with_call(&"好的,我来将 §4.4 的 Mermaid 部署架构图重新构建为 SVG 版本。先找到当前 Mermaid 模块的位置。".repeat(1)), 60);
777
+ for i in 0..5 { c.history.push(Message::user(format!("recent {i}")), 5); }
778
+
779
+ // collapse ON (preserve window = 4, so the oldest narration turn is past it)
780
+ let rc = render_projected(&c, 100_000, &engine(), 4, &HandleTable::new(), 0, true);
781
+ let narration = rc
782
+ .turns
783
+ .iter()
784
+ .find(|m| m.content.as_text() == Some(NARRATION_STUB))
785
+ .expect("old narration replaced by stub");
786
+ assert_eq!(narration.tool_calls.len(), 1, "tool call (pairing) preserved");
787
+ assert_eq!(narration.tool_calls[0].name, "module_read");
788
+ // No verbatim preamble survives in the rendered prefix.
789
+ assert!(!rc.turns.iter().any(|m| m.content.as_text().map(|t| t.contains("先找到当前 Mermaid")).unwrap_or(false)));
790
+ // Original history is untouched (non-destructive projection).
791
+ assert!(c.history.messages[0].content.as_text().unwrap().contains("先找到当前 Mermaid"));
792
+
793
+ // collapse OFF → verbatim narration survives.
794
+ let rc_off = render_projected(&c, 100_000, &engine(), 4, &HandleTable::new(), 0, false);
795
+ assert!(rc_off.turns.iter().any(|m| m.content.as_text().map(|t| t.contains("先找到当前 Mermaid")).unwrap_or(false)));
796
+ }
797
+
798
+ #[test]
799
+ fn recent_assistant_narration_within_window_is_not_collapsed() {
800
+ let mut c = ctx();
801
+ // Only 2 turns, preserve window = 4 → the narration turn is protected → never collapsed.
802
+ c.history.push(assistant_with_call(&"好的,我来将 §4.4 重新构建为 SVG。先定位模块位置确认范围读取内容。".to_string()), 60);
803
+ c.history.push(Message::user("ok"), 5);
804
+ let rc = render_projected(&c, 100_000, &engine(), 4, &HandleTable::new(), 0, true);
805
+ assert!(rc.turns.iter().any(|m| m.content.as_text().map(|t| t.contains("先定位模块位置")).unwrap_or(false)), "recent narration kept verbatim");
806
+ }
807
+
808
+ #[test]
809
+ fn assistant_without_tool_calls_is_never_collapsed() {
810
+ let mut c = ctx();
811
+ // A pure final answer (no tool calls) is substantive — must survive even when old.
812
+ c.history.push(Message::assistant("这是给用户的最终结论,包含实质内容,不应被折叠掉以免丢信息。"), 40);
813
+ for i in 0..5 { c.history.push(Message::user(format!("r{i}")), 5); }
814
+ let rc = render_projected(&c, 100_000, &engine(), 4, &HandleTable::new(), 0, true);
815
+ assert!(rc.turns.iter().any(|m| m.content.as_text().map(|t| t.contains("最终结论")).unwrap_or(false)), "answer-only turns are not narration");
816
+ }
817
+
818
+ #[test]
819
+ fn collapsing_narration_drifts_only_that_turn_in_the_cache_prefix() {
820
+ // The cost made visible: collapsing rewrites that one turn in place → the prefix hash drifts
821
+ // at its position (one-time, as it ages past the window), but earlier turns stay reusable.
822
+ let mut c = ctx();
823
+ c.history.push(Message::user("start"), 5);
824
+ c.history.push(assistant_with_call(&"好的,我来将 §4.4 重新构建为 SVG 版本。先找到 Mermaid 模块的确切位置再读取其内容。".to_string()), 60);
825
+ for i in 0..4 { c.history.push(Message::user(format!("recent {i}")), 5); }
826
+
827
+ let verbatim = render_projected(&c, 100_000, &engine(), 4, &HandleTable::new(), 0, false).prefix_fingerprint();
828
+ let collapsed = render_projected(&c, 100_000, &engine(), 4, &HandleTable::new(), 0, true).prefix_fingerprint();
829
+ // turn 0 ("start") is byte-stable; drift begins at the collapsed narration turn (index 1).
830
+ assert_eq!(collapsed.common_turn_prefix(&verbatim), 1, "only the collapsed turn drifts");
831
+ assert!(!collapsed.extends(&verbatim));
832
+ }
833
+
637
834
  #[test]
638
835
  fn protected_recent_messages_kept_whole_over_budget() {
639
836
  let mut c = ctx();
@@ -41,6 +41,13 @@ pub struct TaskState {
41
41
  /// Call IDs or artifact hashes that must be preserved from compression.
42
42
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
43
43
  pub preserved_refs: Vec<String>,
44
+ /// Rolling log of recent *task* activity — one entry per turn, each a compact summary of that
45
+ /// turn's tool calls (e.g. "module_read, module_list"). Kernel-maintained from REAL tool
46
+ /// activity (not model-curated), so the State turn always shows forward motion even when the
47
+ /// model never maintains `plan`. Lives in the volatile State turn (out of the cacheable prefix),
48
+ /// so updating it never churns the prompt cache. Bounded + recency-ordered; newest last.
49
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
50
+ pub recent_actions: Vec<String>,
44
51
  /// Append-only log of all compression events. Never overwritten.
45
52
  /// Rendered into systemVolatile so the model always sees compression history.
46
53
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -65,6 +72,9 @@ impl PlanStep {
65
72
  /// Maximum durable directives retained; past this the oldest is dropped (recency window).
66
73
  pub const MAX_DIRECTIVES: usize = 8;
67
74
 
75
+ /// Maximum recent action-turns retained for the recency footer (bounded ring).
76
+ pub const MAX_RECENT_ACTIONS: usize = 6;
77
+
68
78
  /// Partial update applied by the SDK or via `update_plan` meta-tool.
69
79
  #[derive(Debug, Clone, Default, Serialize, Deserialize)]
70
80
  pub struct TaskUpdate {
@@ -161,6 +171,21 @@ impl TaskState {
161
171
  }
162
172
  }
163
173
 
174
+ /// Record one turn's tool activity into the recency log (kernel-driven). `summary` is a compact
175
+ /// string of the turn's task tool names; blank input is ignored. Bounded at
176
+ /// [`MAX_RECENT_ACTIONS`] (oldest dropped past the cap).
177
+ pub fn note_actions(&mut self, summary: impl Into<String>) {
178
+ let summary = summary.into();
179
+ if summary.trim().is_empty() {
180
+ return;
181
+ }
182
+ self.recent_actions.push(summary);
183
+ if self.recent_actions.len() > MAX_RECENT_ACTIONS {
184
+ let overflow = self.recent_actions.len() - MAX_RECENT_ACTIONS;
185
+ self.recent_actions.drain(0..overflow);
186
+ }
187
+ }
188
+
164
189
  /// Append a compression event to the log. Never overwrites existing entries.
165
190
  pub fn log_compression(&mut self, action: &str, summary: String) {
166
191
  self.compression_log.push(CompressionEntry {
@@ -21,6 +21,26 @@ use crate::types::result::{LoopResult, TerminationReason};
21
21
  use crate::types::signal::RuntimeSignal;
22
22
  use crate::types::task::RuntimeTask;
23
23
 
24
+ /// Compact digest of a tool call's arguments for the recency log (2b). Kept short and CJK-safe — it
25
+ /// only needs to make `same-tool / different-args` calls distinguishable (so a legit loop isn't
26
+ /// flagged as a no-progress repeat) and to read sensibly in the "just did: …" footer. Empty for
27
+ /// no-arg / `{}` calls. Lives in the volatile State turn, so length here never churns the cache.
28
+ fn compact_tool_args(args: &serde_json::Value) -> String {
29
+ if args.is_null() {
30
+ return String::new();
31
+ }
32
+ let s = args.to_string();
33
+ if s == "{}" {
34
+ return String::new();
35
+ }
36
+ const MAX: usize = 48;
37
+ if s.chars().count() <= MAX {
38
+ s
39
+ } else {
40
+ format!("{}…", s.chars().take(MAX).collect::<String>())
41
+ }
42
+ }
43
+
24
44
  /// The *turn step* of the L* execution loop (M1d).
25
45
  ///
26
46
  /// Schedulability (`Ready/Running/Blocked/Suspended/Done`) is no longer carried here — it lives
@@ -484,6 +504,22 @@ impl LoopStateMachine {
484
504
  self.ctx.record_activity(now_ms);
485
505
  }
486
506
 
507
+ // 2b: record this turn's tool activity into the task-state recency log (meta-tools
508
+ // filtered inside). The State-turn footer renders it as "just did: …" + a forward
509
+ // nudge / STOP, so progress is kernel-derived and never depends on the model
510
+ // remembering to call `update_plan`. Tool *names* live only on the request (results
511
+ // carry call_id only), so this is the turn to capture them.
512
+ //
513
+ // Capture name AND a compact arg digest: the no-progress STOP keys on whether the
514
+ // SAME call repeats, and a legit loop (same tool, DIFFERENT args — e.g. processing 20
515
+ // items) is real progress, not a stall. Keying on the name alone false-positives those
516
+ // loops; including args distinguishes "step(n=1), step(n=2)…" from a true repeat.
517
+ let action_sigs: Vec<(String, String)> = calls
518
+ .iter()
519
+ .map(|c| (c.name.to_string(), compact_tool_args(&c.arguments)))
520
+ .collect();
521
+ self.ctx.note_tool_actions(&action_sigs);
522
+
487
523
  match self.gate_tool_calls(&calls) {
488
524
  GateToolOutcome::Blocked(action) => return action,
489
525
  GateToolOutcome::Suspended => return LoopAction::AwaitingResume,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "deepstrike"
7
- version = "0.2.32"
7
+ version = "0.2.34"
8
8
  requires-python = ">=3.10"
9
9
  readme = "README.md"
10
10
  dependencies = [
File without changes