deepstrike 0.2.34__tar.gz → 0.2.35__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.34 → deepstrike-0.2.35}/Cargo.lock +5 -5
  2. {deepstrike-0.2.34 → deepstrike-0.2.35}/Cargo.toml +2 -2
  3. {deepstrike-0.2.34 → deepstrike-0.2.35}/PKG-INFO +7 -1
  4. {deepstrike-0.2.34 → deepstrike-0.2.35}/README.md +6 -0
  5. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/proc/mod.rs +1 -0
  6. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/kernel.rs +28 -0
  7. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/eviction.rs +52 -1
  8. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/mod.rs +54 -0
  9. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/tests.rs +139 -0
  10. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/result.rs +5 -0
  11. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/anthropic.py +4 -0
  12. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/openai.py +7 -0
  13. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/stream.py +3 -0
  14. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/runner.py +28 -27
  15. {deepstrike-0.2.34 → deepstrike-0.2.35}/pyproject.toml +1 -1
  16. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/Cargo.toml +0 -0
  17. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/compression.rs +0 -0
  18. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/config.rs +0 -0
  19. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  20. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/manager.rs +0 -0
  21. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/mod.rs +0 -0
  22. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  23. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/pressure.rs +0 -0
  24. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/renderer.rs +0 -0
  25. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  26. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/sections.rs +0 -0
  27. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  28. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
  29. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  30. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/task_state.rs +0 -0
  31. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/text.rs +0 -0
  32. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  33. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  34. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  35. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  36. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  37. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  38. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  39. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  40. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  41. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  42. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  43. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/harness/eval.rs +0 -0
  44. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  45. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/lib.rs +0 -0
  46. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  47. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  48. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  49. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  50. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  51. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  52. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  53. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/session.rs +0 -0
  54. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  55. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  56. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/memory/working.rs +0 -0
  57. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/mm/handle.rs +0 -0
  58. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  59. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  60. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/orchestration/mod.rs +0 -0
  61. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
  62. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
  63. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/orchestration/workflow/mod.rs +0 -0
  64. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/orchestration/workflow/run.rs +0 -0
  65. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
  66. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/mod.rs +0 -0
  67. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  68. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  69. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/session.rs +0 -0
  70. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/runtime/snapshot.rs +0 -0
  71. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  72. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/mod.rs +0 -0
  73. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  74. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  75. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/capability.rs +0 -0
  76. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/gate.rs +0 -0
  77. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/milestone_exec.rs +0 -0
  78. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/process.rs +0 -0
  79. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/signal.rs +0 -0
  80. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/state_machine/workflow.rs +0 -0
  81. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
  82. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  83. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  84. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  85. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/signals/router.rs +0 -0
  86. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
  87. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/agent.rs +0 -0
  88. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/capability.rs +0 -0
  89. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/contract.rs +0 -0
  90. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/error.rs +0 -0
  91. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/message.rs +0 -0
  92. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  93. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/mod.rs +0 -0
  94. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/model.rs +0 -0
  95. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/policy.rs +0 -0
  96. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/signal.rs +0 -0
  97. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/skill.rs +0 -0
  98. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-core/src/types/task.rs +0 -0
  99. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-py/Cargo.toml +0 -0
  100. {deepstrike-0.2.34 → deepstrike-0.2.35}/crates/deepstrike-py/src/lib.rs +0 -0
  101. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/__init__.py +0 -0
  102. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/collaboration/__init__.py +0 -0
  103. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/collaboration/contract.py +0 -0
  104. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/collaboration/handoff.py +0 -0
  105. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/collaboration/harness.py +0 -0
  106. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/collaboration/modes.py +0 -0
  107. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/collaboration/pool.py +0 -0
  108. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/governance.py +0 -0
  109. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/harness/__init__.py +0 -0
  110. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/harness/harness.py +0 -0
  111. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/harness/judge.py +0 -0
  112. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/kernel/__init__.py +0 -0
  113. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/knowledge/__init__.py +0 -0
  114. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/knowledge/source.py +0 -0
  115. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/memory/__init__.py +0 -0
  116. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/memory/agent.py +0 -0
  117. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/memory/in_memory_store.py +0 -0
  118. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/memory/protocols.py +0 -0
  119. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/memory/working.py +0 -0
  120. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/__init__.py +0 -0
  121. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/anthropic_compatible.py +0 -0
  122. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/base.py +0 -0
  123. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/deepseek.py +0 -0
  124. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/factories.py +0 -0
  125. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/gemini.py +0 -0
  126. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/glm.py +0 -0
  127. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/kimi.py +0 -0
  128. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/minimax.py +0 -0
  129. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/ollama.py +0 -0
  130. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/openai_responses.py +0 -0
  131. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/qwen.py +0 -0
  132. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/replay.py +0 -0
  133. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/replay_validator.py +0 -0
  134. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/providers/vendor_profiles.py +0 -0
  135. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/__init__.py +0 -0
  136. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/archive.py +0 -0
  137. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/credential_vault.py +0 -0
  138. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/eval.py +0 -0
  139. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/event_stream.py +0 -0
  140. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/execution_plane.py +0 -0
  141. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/facade.py +0 -0
  142. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/filtered_plane.py +0 -0
  143. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/kernel_event_log.py +0 -0
  144. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/kernel_step.py +0 -0
  145. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/large_result_spool.py +0 -0
  146. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  147. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/os_profile.py +0 -0
  148. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/os_snapshot.py +0 -0
  149. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/output_schema.py +0 -0
  150. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  151. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/provider_replay.py +0 -0
  152. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/reactive_session.py +0 -0
  153. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/reducers.py +0 -0
  154. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  155. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/replay_fixture.py +0 -0
  156. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/replay_provider.py +0 -0
  157. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/replay_sanitize.py +0 -0
  158. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/run_group.py +0 -0
  159. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/session_log.py +0 -0
  160. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/session_repair.py +0 -0
  161. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
  162. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/turn_policy.py +0 -0
  163. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/workflow_control_flow.py +0 -0
  164. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/workflow_store.py +0 -0
  165. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/runtime/worktree_plane.py +0 -0
  166. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/safety/__init__.py +0 -0
  167. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/safety/permissions.py +0 -0
  168. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/signals/__init__.py +0 -0
  169. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/signals/gateway.py +0 -0
  170. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/signals/scheduled.py +0 -0
  171. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/signals/types.py +0 -0
  172. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/skills/__init__.py +0 -0
  173. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/skills/registry.py +0 -0
  174. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/skills/watcher.py +0 -0
  175. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/tools/__init__.py +0 -0
  176. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/tools/builtin/__init__.py +0 -0
  177. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/tools/builtin/read_file.py +0 -0
  178. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/tools/errors.py +0 -0
  179. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/tools/execution.py +0 -0
  180. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/tools/registry.py +0 -0
  181. {deepstrike-0.2.34 → deepstrike-0.2.35}/deepstrike/types/__init__.py +0 -0
  182. {deepstrike-0.2.34 → deepstrike-0.2.35}/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.34"
197
+ version = "0.2.35"
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.34"
209
+ version = "0.2.35"
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.34"
221
+ version = "0.2.35"
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.34"
232
+ version = "0.2.35"
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.34"
266
+ version = "0.2.35"
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.34"
6
+ version = "0.2.35"
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.34" }
12
+ deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.35" }
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.34
3
+ Version: 0.2.35
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -11,6 +11,12 @@ Requires-Dist: dashscope>=1.20
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
13
13
 
14
+ <p align="center">
15
+ <a href="https://github.com/kongusen/deepstrike">
16
+ <img src="https://raw.githubusercontent.com/kongusen/deepstrike/main/docs/public/banner.png" alt="DeepStrike" width="420" />
17
+ </a>
18
+ </p>
19
+
14
20
  # DeepStrike Python SDK
15
21
 
16
22
  Runtime framework built on a Rust kernel. The kernel owns loop control, context compression, governance, signal routing, and memory paging — the SDK owns all I/O (LLM calls, tool execution, disk, long-term memory).
@@ -1,3 +1,9 @@
1
+ <p align="center">
2
+ <a href="https://github.com/kongusen/deepstrike">
3
+ <img src="https://raw.githubusercontent.com/kongusen/deepstrike/main/docs/public/banner.png" alt="DeepStrike" width="420" />
4
+ </a>
5
+ </p>
6
+
1
7
  # DeepStrike Python SDK
2
8
 
3
9
  Runtime framework built on a Rust kernel. The kernel owns loop control, context compression, governance, signal routing, and memory paging — the SDK owns all I/O (LLM calls, tool execution, disk, long-term memory).
@@ -84,6 +84,7 @@ impl AgentProcess {
84
84
  TerminationReason::UserAbort => "user_abort",
85
85
  TerminationReason::Error => "error",
86
86
  TerminationReason::MilestoneExceeded => "milestone_exceeded",
87
+ TerminationReason::ContextOverflow => "context_overflow",
87
88
  })
88
89
  }
89
90
  }
@@ -346,10 +346,24 @@ pub enum KernelInputEvent {
346
346
  // on a 0 clock (effectively unlimited). Can become required once all SDKs feed time.
347
347
  #[serde(default, skip_serializing_if = "Option::is_none")]
348
348
  now_ms: Option<u64>,
349
+ /// Provider stop_reason for this response — `max_tokens` (Anthropic) / `length` (OpenAI)
350
+ /// signal an output-cap truncation, which drives the kernel's max-output-tokens recovery.
351
+ /// Additive: omitted by providers/SDKs that don't report it (no-op recovery).
352
+ #[serde(default, skip_serializing_if = "Option::is_none")]
353
+ stop_reason: Option<String>,
349
354
  },
350
355
  ToolResults {
351
356
  results: Vec<ToolResult>,
352
357
  },
358
+ /// Reactive recovery entry point: the SDK's provider stream failed. The kernel classifies the
359
+ /// error (context-overflow vs other) and runs the bounded compact-and-retry recovery ladder,
360
+ /// returning `CallProvider` to retry with a freshly compacted context or `Done` to terminate.
361
+ /// The runners forward the raw provider error text and dispatch the result, instead of each
362
+ /// owning the classify + compact + retry + give-up policy. Additive ABI: a brand-new variant,
363
+ /// byte-identical on the wire for SDKs that never send it.
364
+ ProviderError {
365
+ message: String,
366
+ },
353
367
  Signal {
354
368
  signal: RuntimeSignal,
355
369
  },
@@ -994,6 +1008,7 @@ impl KernelRuntime {
994
1008
  observed_input_tokens,
995
1009
  observed_output_tokens: _,
996
1010
  now_ms,
1011
+ stop_reason,
997
1012
  } => {
998
1013
  if let Some(tokens) = observed_input_tokens {
999
1014
  self.sm.ctx.set_observed_prompt_tokens(tokens);
@@ -1003,11 +1018,18 @@ impl KernelRuntime {
1003
1018
  if let Some(ms) = now_ms {
1004
1019
  self.sm.set_observed_time(ms);
1005
1020
  }
1021
+ // Stash stop_reason so `feed` can detect an output-cap truncation and drive recovery.
1022
+ self.sm.set_pending_stop_reason(stop_reason);
1006
1023
  self.sm.feed(LoopEvent::LLMResponse { message })
1007
1024
  }
1008
1025
  KernelInputEvent::ToolResults { results } => {
1009
1026
  self.sm.feed(LoopEvent::ToolResults { results })
1010
1027
  }
1028
+ KernelInputEvent::ProviderError { message } => {
1029
+ // Reactive recovery is a kernel decision: classify + bounded compact-and-retry,
1030
+ // returning the next action (retry or honest terminal) through the common tail.
1031
+ self.sm.recover_from_provider_error(&message)
1032
+ }
1011
1033
  KernelInputEvent::Signal { signal } => match self.sm.signal_event(signal) {
1012
1034
  Some(action) => action,
1013
1035
  // Non-actionable disposition (queued / observed / ignored / dropped):
@@ -1232,6 +1254,7 @@ mod tests {
1232
1254
  message: Message::assistant("done"),
1233
1255
  observed_input_tokens: None,
1234
1256
  observed_output_tokens: None,
1257
+ stop_reason: None,
1235
1258
  now_ms: None,
1236
1259
  }));
1237
1260
 
@@ -1450,6 +1473,7 @@ mod tests {
1450
1473
  message: msg,
1451
1474
  observed_input_tokens: None,
1452
1475
  observed_output_tokens: None,
1476
+ stop_reason: None,
1453
1477
  now_ms: None,
1454
1478
  }));
1455
1479
  runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
@@ -1717,6 +1741,7 @@ mod tests {
1717
1741
  message: msg,
1718
1742
  observed_input_tokens: None,
1719
1743
  observed_output_tokens: None,
1744
+ stop_reason: None,
1720
1745
  now_ms: Some(100),
1721
1746
  }));
1722
1747
  let step = runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
@@ -1767,6 +1792,7 @@ mod tests {
1767
1792
  message: assistant_calling(tool),
1768
1793
  observed_input_tokens: None,
1769
1794
  observed_output_tokens: None,
1795
+ stop_reason: None,
1770
1796
  now_ms: None,
1771
1797
  }))
1772
1798
  }
@@ -1992,6 +2018,7 @@ mod tests {
1992
2018
  message: assistant_calling("fetch"),
1993
2019
  observed_input_tokens: None,
1994
2020
  observed_output_tokens: None,
2021
+ stop_reason: None,
1995
2022
  now_ms: Some(1_000),
1996
2023
  }));
1997
2024
  assert!(
@@ -2011,6 +2038,7 @@ mod tests {
2011
2038
  message: assistant_calling("fetch"),
2012
2039
  observed_input_tokens: None,
2013
2040
  observed_output_tokens: None,
2041
+ stop_reason: None,
2014
2042
  now_ms: Some(1_001),
2015
2043
  }));
2016
2044
  assert!(
@@ -1,12 +1,63 @@
1
1
  //! MM / eviction execution impl for [`super::LoopStateMachine`].
2
2
 
3
- use super::{KernelObservation, LoopStateMachine};
3
+ use super::{KernelObservation, LoopAction, LoopPhase, LoopStateMachine};
4
4
  use crate::context::pressure::PressureAction;
5
5
  use crate::mm::{page_in_requests_from_calls, tier_hint_for_compress};
6
6
  use crate::runtime::kernel::KernelPressureAction;
7
7
  use crate::types::message::{Message, ToolCall};
8
+ use crate::types::result::TerminationReason;
9
+
10
+ /// Max consecutive compact-and-retry attempts before a context overflow is declared
11
+ /// unrecoverable. Bounds the reactive recovery ladder (anti-spiral); resets on any successful
12
+ /// provider turn. The `force_compress` "nothing left to save" check is the real terminator —
13
+ /// this is the belt-and-suspenders cap so a degenerate provider that 413s forever still ends.
14
+ const MAX_RECOVERY_ATTEMPTS: u8 = 2;
15
+
16
+ /// Classify a provider error message as a context-overflow (prompt-too-long / 413). Centralizes
17
+ /// the case-insensitive string match the four SDK runners (node/python/rust/wasm) each used to
18
+ /// own, so the recovery vocabulary lives in exactly one place.
19
+ pub(crate) fn is_prompt_too_long(message: &str) -> bool {
20
+ let msg = message.to_lowercase();
21
+ msg.contains("413")
22
+ || msg.contains("too long")
23
+ || msg.contains("prompt too long")
24
+ || msg.contains("context length exceeded")
25
+ || msg.contains("context_length_exceeded")
26
+ }
8
27
 
9
28
  impl LoopStateMachine {
29
+ /// Reactive recovery for a provider error the SDK reports via [`KernelInputEvent::ProviderError`].
30
+ /// Owns the policy the SDK runners used to duplicate: classify → compact-and-retry on overflow
31
+ /// (bounded, anti-spiral) → honest terminal when the ladder is exhausted. Returns the next
32
+ /// [`LoopAction`] so the kernel's normal action tail dispatches it: `CallLLM` to retry the
33
+ /// provider with a freshly compacted context, or `Done { ContextOverflow }` to give up.
34
+ ///
35
+ /// This is the reactive twin of the proactive eviction checkpoint in `feed` — same execution
36
+ /// funnel (`force_compact` → `EvictionOp`s), now driven by a real provider 413 instead of an
37
+ /// rho threshold, and surfaced as a kernel decision rather than SDK control flow.
38
+ pub fn recover_from_provider_error(&mut self, message: &str) -> LoopAction {
39
+ self.observations.clear();
40
+ if !is_prompt_too_long(message) {
41
+ // Non-overflow provider failures aren't recoverable here — terminate with `Error`,
42
+ // the same outcome the runners produced, minus the fabricated `timeout`.
43
+ return self.terminate(TerminationReason::Error, None);
44
+ }
45
+ if self.recovery_attempts >= MAX_RECOVERY_ATTEMPTS {
46
+ return self.terminate(TerminationReason::ContextOverflow, None);
47
+ }
48
+ self.recovery_attempts += 1;
49
+ if self.force_compact() {
50
+ // Recovered headroom: re-render and retry as a normal turn (tools intact). The
51
+ // Compressed/PageOut observations pushed by `force_compact` ride out in this same
52
+ // step, so the SDK still archives the evicted messages.
53
+ self.phase = LoopPhase::Reason;
54
+ self.emit_call_llm()
55
+ } else {
56
+ // Nothing left to compact — the prompt genuinely won't fit. Honest terminal.
57
+ self.terminate(TerminationReason::ContextOverflow, None)
58
+ }
59
+ }
60
+
10
61
  /// 强行进行一次最大力度的压缩归档。通常用于收到模型 API 413 (Prompt too long) 时做兜底重试。
11
62
  pub fn force_compact(&mut self) -> bool {
12
63
  let action = PressureAction::AutoCompact;
@@ -188,6 +188,21 @@ pub struct LoopStateMachine {
188
188
  /// When set, the next LLM call strips tools to force a text response,
189
189
  /// then terminates with this reason once the response arrives.
190
190
  pub(super) pending_termination: Option<TerminationReason>,
191
+ /// Reactive context-overflow recovery: consecutive compact-and-retry attempts since the last
192
+ /// successful provider turn. Bounds the recovery ladder (anti-spiral) and resets to 0 on any
193
+ /// `LLMResponse`, mirroring the per-turn `hasAttemptedReactiveCompact` reset the SDK runners
194
+ /// used to own. See `recover_from_provider_error`.
195
+ pub(super) recovery_attempts: u8,
196
+ /// Max-output-tokens recovery: consecutive continue-and-retry turns since the model last
197
+ /// finished a response WITHOUT hitting the output cap. When a turn is cut off at the cap
198
+ /// (provider `stop_reason` = max_tokens/length) the kernel keeps the partial, nudges the model
199
+ /// to resume mid-thought, and re-calls — bounded by `MAX_OUTPUT_RECOVERY` (mirrors query.ts's
200
+ /// MAX_OUTPUT_TOKENS_RECOVERY_LIMIT). Resets to 0 on any non-truncated response.
201
+ pub(super) output_recovery_attempts: u8,
202
+ /// Transient carrier for the provider `stop_reason` of the in-flight response, set by the
203
+ /// kernel ABI just before `feed(LLMResponse)` and taken (cleared) inside it. `None` when the
204
+ /// SDK/provider doesn't report one (every non-Anthropic provider today ⇒ no-op).
205
+ pub(super) pending_stop_reason: Option<String>,
191
206
  /// Number of history messages present at session start (after preload_history).
192
207
  /// drain_new_messages() returns the slice from this offset onward.
193
208
  pub(super) session_history_baseline: usize,
@@ -265,6 +280,9 @@ impl LoopStateMachine {
265
280
  group_tokens_base: 0,
266
281
  group_spawns_base: 0,
267
282
  pending_termination: None,
283
+ recovery_attempts: 0,
284
+ output_recovery_attempts: 0,
285
+ pending_stop_reason: None,
268
286
  session_history_baseline: 0,
269
287
  checkpoint: TurnCheckpoint::default(),
270
288
  milestone: MilestoneTracker::new(),
@@ -393,6 +411,12 @@ impl LoopStateMachine {
393
411
  }
394
412
  }
395
413
 
414
+ /// Stash the in-flight response's provider `stop_reason` so `feed(LLMResponse)` can detect an
415
+ /// output-cap truncation. Set by the kernel ABI right before feeding the result; `None` clears it.
416
+ pub fn set_pending_stop_reason(&mut self, stop_reason: Option<String>) {
417
+ self.pending_stop_reason = stop_reason;
418
+ }
419
+
396
420
  /// Pre-populate the history partition with messages from a prior session.
397
421
  ///
398
422
  /// Call **before** `start()` when resuming a conversation. Sets the baseline
@@ -464,14 +488,41 @@ impl LoopStateMachine {
464
488
  LoopEvent::Start { task } => self.start(task),
465
489
 
466
490
  LoopEvent::LLMResponse { message } => {
491
+ // A response arrived ⇒ the prompt fit ⇒ the overflow recovery ladder is reset.
492
+ self.recovery_attempts = 0;
467
493
  let tokens = self.message_tokens(&message);
468
494
  self.total_tokens += tokens as u64;
469
495
 
496
+ // Max-output-tokens recovery (mirrors query.ts): a response cut off at the output
497
+ // cap reports stop_reason = max_tokens (Anthropic) / length (OpenAI). A clean finish
498
+ // resets the ladder.
499
+ const MAX_OUTPUT_RECOVERY: u8 = 3;
500
+ const OUTPUT_TRUNCATION_NUDGE: &str = "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.";
501
+ let truncated = matches!(
502
+ self.pending_stop_reason.take().as_deref(),
503
+ Some("max_tokens") | Some("length"),
504
+ );
505
+ if !truncated {
506
+ self.output_recovery_attempts = 0;
507
+ }
508
+
470
509
  if let Some(reason) = self.pending_termination.take() {
471
510
  return self.terminate(reason, Some(message));
472
511
  }
473
512
 
474
513
  if message.tool_calls.is_empty() {
514
+ // The model was cut off at the output cap with no tool call. Keep the partial,
515
+ // nudge it to resume mid-thought, and re-call — instead of mistaking the
516
+ // truncation for a finished turn. Bounded by MAX_OUTPUT_RECOVERY; once exhausted
517
+ // the partial stands and the turn terminates normally below. (A truncated
518
+ // *tool-call* turn isn't handled here — it falls through to tool execution.)
519
+ if truncated && self.output_recovery_attempts < MAX_OUTPUT_RECOVERY {
520
+ self.output_recovery_attempts += 1;
521
+ self.ctx.push_history(message, tokens);
522
+ self.ctx.push_signal(OUTPUT_TRUNCATION_NUDGE.to_string());
523
+ self.phase = LoopPhase::Reason;
524
+ return self.emit_call_llm();
525
+ }
475
526
  // When a milestone contract is active and not yet complete,
476
527
  // request evaluation instead of terminating.
477
528
  if !self.milestone.is_complete() {
@@ -795,6 +846,9 @@ impl LoopStateMachine {
795
846
  group_tokens_base: 0,
796
847
  group_spawns_base: 0,
797
848
  pending_termination: None,
849
+ recovery_attempts: 0,
850
+ output_recovery_attempts: 0,
851
+ pending_stop_reason: None,
798
852
  session_history_baseline: 0,
799
853
  checkpoint: TurnCheckpoint::default(),
800
854
  milestone: crate::scheduler::milestone::MilestoneTracker::new(),
@@ -537,6 +537,145 @@
537
537
  assert!(obs.iter().any(|o| matches!(o, KernelObservation::PageOut { .. })));
538
538
  }
539
539
 
540
+ // ---- Reactive recovery ladder (lifted from the SDK runners into the kernel) ----
541
+
542
+ fn compactible_machine() -> LoopStateMachine {
543
+ let mut sm = LoopStateMachine::new(LoopPolicy {
544
+ max_tokens: 100,
545
+ max_turns: 100,
546
+ ..LoopPolicy::default()
547
+ });
548
+ sm.start(RuntimeTask::new("test"));
549
+ for i in 0..10 {
550
+ sm.ctx.push_history(Message::user(format!("filler {i}")), 50);
551
+ }
552
+ sm
553
+ }
554
+
555
+ #[test]
556
+ fn recover_overflow_compacts_and_retries() {
557
+ let mut sm = compactible_machine();
558
+ let action = sm.recover_from_provider_error("HTTP 413: prompt is too long");
559
+ // Recovered headroom ⇒ retry the provider with a freshly compacted context.
560
+ assert!(matches!(action, LoopAction::CallLLM { .. }));
561
+ assert_eq!(sm.recovery_attempts, 1);
562
+ // The eviction rode out as observations so the SDK still archives the evicted messages.
563
+ let obs = sm.take_observations();
564
+ assert!(obs.iter().any(|o| matches!(o, KernelObservation::Compressed { .. })));
565
+ }
566
+
567
+ #[test]
568
+ fn recover_overflow_exhausted_terminates_context_overflow() {
569
+ // Fresh machine with nothing compactible ⇒ force_compact saves 0 ⇒ honest terminal.
570
+ let mut sm = sm();
571
+ sm.start(RuntimeTask::new("test"));
572
+ let action = sm.recover_from_provider_error("context_length_exceeded");
573
+ match action {
574
+ LoopAction::Done { result } => {
575
+ assert_eq!(result.termination, TerminationReason::ContextOverflow);
576
+ }
577
+ other => panic!("expected Done(ContextOverflow), got {other:?}"),
578
+ }
579
+ }
580
+
581
+ #[test]
582
+ fn recover_non_overflow_terminates_error() {
583
+ let mut sm = compactible_machine();
584
+ let action = sm.recover_from_provider_error("500 Internal Server Error");
585
+ match action {
586
+ LoopAction::Done { result } => {
587
+ assert_eq!(result.termination, TerminationReason::Error);
588
+ }
589
+ other => panic!("expected Done(Error), got {other:?}"),
590
+ }
591
+ }
592
+
593
+ #[test]
594
+ fn recover_attempt_cap_is_bounded() {
595
+ // Even a provider that 413s forever must terminate, not loop. Drive past the cap and
596
+ // assert a terminal appears within a bounded number of attempts.
597
+ let mut sm = compactible_machine();
598
+ let mut terminated = false;
599
+ for _ in 0..6 {
600
+ if let LoopAction::Done { result } = sm.recover_from_provider_error("413 too long") {
601
+ assert_eq!(result.termination, TerminationReason::ContextOverflow);
602
+ terminated = true;
603
+ break;
604
+ }
605
+ }
606
+ assert!(terminated, "recovery ladder must terminate under repeated overflow");
607
+ }
608
+
609
+ #[test]
610
+ fn recovery_attempts_reset_on_successful_response() {
611
+ let mut sm = compactible_machine();
612
+ let action = sm.recover_from_provider_error("413 too long");
613
+ assert!(matches!(action, LoopAction::CallLLM { .. }));
614
+ assert_eq!(sm.recovery_attempts, 1);
615
+ // A response that fits resets the ladder (mirrors the per-turn SDK guard reset).
616
+ sm.feed(LoopEvent::LLMResponse { message: Message::assistant("recovered") });
617
+ assert_eq!(sm.recovery_attempts, 0);
618
+ }
619
+
620
+ #[test]
621
+ fn is_prompt_too_long_classifier() {
622
+ use super::eviction::is_prompt_too_long;
623
+ assert!(is_prompt_too_long("Error 413"));
624
+ assert!(is_prompt_too_long("prompt is TOO LONG"));
625
+ assert!(is_prompt_too_long("context_length_exceeded"));
626
+ assert!(!is_prompt_too_long("429 rate limited"));
627
+ assert!(!is_prompt_too_long("connection reset"));
628
+ }
629
+
630
+ // ---- Max-output-tokens recovery (Phase 4) ----
631
+
632
+ #[test]
633
+ fn truncated_response_continues_then_resets() {
634
+ let mut sm = sm();
635
+ sm.start(RuntimeTask::new("write a long thing"));
636
+ // Cut off at the output cap with no tool call ⇒ keep the partial and re-call (don't finish).
637
+ sm.set_pending_stop_reason(Some("max_tokens".into()));
638
+ let action = sm.feed(LoopEvent::LLMResponse { message: Message::assistant("partial...") });
639
+ assert!(matches!(action, LoopAction::CallLLM { .. }));
640
+ assert_eq!(sm.output_recovery_attempts, 1);
641
+ // A clean finish (no stop_reason) terminates normally AND resets the ladder.
642
+ let action = sm.feed(LoopEvent::LLMResponse { message: Message::assistant("...the rest. done") });
643
+ match action {
644
+ LoopAction::Done { result } => assert_eq!(result.termination, TerminationReason::Completed),
645
+ other => panic!("expected Done(Completed), got {other:?}"),
646
+ }
647
+ assert_eq!(sm.output_recovery_attempts, 0);
648
+ }
649
+
650
+ #[test]
651
+ fn truncation_recovery_is_bounded() {
652
+ let mut sm = sm();
653
+ sm.start(RuntimeTask::new("write forever"));
654
+ // 3 attempts continue; the 4th consecutive truncation gives up and accepts the partial.
655
+ for _ in 0..3 {
656
+ sm.set_pending_stop_reason(Some("length".into()));
657
+ let action = sm.feed(LoopEvent::LLMResponse { message: Message::assistant("more") });
658
+ assert!(matches!(action, LoopAction::CallLLM { .. }));
659
+ }
660
+ sm.set_pending_stop_reason(Some("length".into()));
661
+ let action = sm.feed(LoopEvent::LLMResponse { message: Message::assistant("more") });
662
+ match action {
663
+ LoopAction::Done { result } => assert_eq!(result.termination, TerminationReason::Completed),
664
+ other => panic!("expected Done(Completed) after the cap, got {other:?}"),
665
+ }
666
+ }
667
+
668
+ #[test]
669
+ fn no_stop_reason_terminates_normally() {
670
+ // The no-op safety path: a provider that never reports stop_reason (every non-Anthropic
671
+ // provider today) finishes the turn as before — no spurious continue.
672
+ let mut sm = sm();
673
+ sm.start(RuntimeTask::new("answer"));
674
+ let action = sm.feed(LoopEvent::LLMResponse { message: Message::assistant("the answer") });
675
+ assert!(matches!(action, LoopAction::Done { .. }));
676
+ assert_eq!(sm.output_recovery_attempts, 0);
677
+ }
678
+
540
679
  // ---- Layer 5: AutoCompact → semantic page-out (SDK does the LLM summary) ----
541
680
  //
542
681
  // Contract: AutoCompact keeps a structural summary in-context (sync, zero-I/O) and pages the
@@ -14,6 +14,11 @@ pub enum TerminationReason {
14
14
  Error,
15
15
  /// Milestone phase retry budget exhausted and rollback_policy = Terminate.
16
16
  MilestoneExceeded,
17
+ /// Reactive recovery ladder exhausted on a provider context-overflow (prompt-too-long /
18
+ /// 413): the kernel compacted as hard as it could and the prompt still won't fit. Distinct
19
+ /// from `Timeout` (which the SDK used to fabricate for this case) so embedders can tell an
20
+ /// unrecoverable overflow from a wall-clock deadline.
21
+ ContextOverflow,
17
22
  }
18
23
 
19
24
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -259,6 +259,9 @@ class AnthropicProvider:
259
259
  # read + cache write) for accurate context accounting.
260
260
  full_input = uncached_input + cache_read + cache_creation
261
261
  by_slot = _estimate_cache_read_by_slot(cache_read, slot_bp)
262
+ # stop_reason is only present on message_delta (the closing frame);
263
+ # "max_tokens" drives the kernel's output-cap recovery.
264
+ stop_reason = getattr(getattr(event, "delta", None), "stop_reason", None)
262
265
  yield UsageEvent(
263
266
  total_tokens=full_input + output_tokens,
264
267
  input_tokens=full_input,
@@ -266,6 +269,7 @@ class AnthropicProvider:
266
269
  cache_read_input_tokens=cache_read,
267
270
  cache_creation_input_tokens=cache_creation,
268
271
  cache_read_input_tokens_by_slot=by_slot,
272
+ stop_reason=stop_reason,
269
273
  )
270
274
  elif event.type == "content_block_start":
271
275
  idx = event.index
@@ -304,6 +304,10 @@ class OpenAIProvider(ReasoningReplayMixin):
304
304
  stream_options={"include_usage": True},
305
305
  )
306
306
 
307
+ # Phase 4: OpenAI signals an output-cap truncation via finish_reason="length", which arrives
308
+ # on a choices frame BEFORE the trailing usage frame — capture it and attach it to the usage
309
+ # event. The kernel treats "length" as a truncation (== Anthropic "max_tokens").
310
+ finish_reason_seen: "str | None" = None
307
311
  async for chunk in stream:
308
312
  usage = getattr(chunk, "usage", None)
309
313
  if usage:
@@ -314,11 +318,14 @@ class OpenAIProvider(ReasoningReplayMixin):
314
318
  input_tokens=getattr(usage, "prompt_tokens", 0) or 0,
315
319
  output_tokens=getattr(usage, "completion_tokens", 0) or 0,
316
320
  cache_read_input_tokens=openai_cached_prompt_tokens(usage),
321
+ stop_reason=finish_reason_seen,
317
322
  )
318
323
  continue
319
324
  choice = chunk.choices[0] if chunk.choices else None
320
325
  if not choice:
321
326
  continue
327
+ if choice.finish_reason:
328
+ finish_reason_seen = choice.finish_reason
322
329
 
323
330
  delta = getattr(choice, "delta", None)
324
331
  if not delta:
@@ -66,6 +66,9 @@ class UsageEvent:
66
66
  # I1: pro-rata per-slot attribution of cache_read_input_tokens (Anthropic only). None when the
67
67
  # provider doesn't honor cache_control. Shape: {"system": int?, "tools": int?, "messages": int?}.
68
68
  cache_read_input_tokens_by_slot: "dict | None" = None
69
+ # Provider stop reason — "max_tokens" (Anthropic) / "length" (OpenAI) flag an output-cap
70
+ # truncation that drives the kernel's max-output-tokens recovery. None when not reported.
71
+ stop_reason: "str | None" = None
69
72
 
70
73
 
71
74
  @dataclass