steerable-agent-runtime 0.6.7__tar.gz → 0.6.8__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 (152) hide show
  1. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/compaction.py +48 -2
  4. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  5. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_compaction.py +82 -0
  6. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/README.md +0 -0
  7. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/setup.cfg +0 -0
  8. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/__init__.py +0 -0
  9. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/antihallucination.py +0 -0
  10. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/approval.py +0 -0
  11. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/approval_policy.py +0 -0
  12. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/ask_user.py +0 -0
  13. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/branch.py +0 -0
  14. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/cache_control.py +0 -0
  15. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/calibration.py +0 -0
  16. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/config.py +0 -0
  17. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/default.harness.json +0 -0
  18. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  19. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/errors.py +0 -0
  20. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  21. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/handoff.py +0 -0
  22. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/harness.py +0 -0
  23. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/harness_spec.py +0 -0
  24. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/history.py +0 -0
  25. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/hooks.py +0 -0
  26. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  27. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  28. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/compat.py +0 -0
  29. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/errors.py +0 -0
  30. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  31. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  32. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  33. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/parts.py +0 -0
  34. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/presets.py +0 -0
  35. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  36. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/loop.py +0 -0
  37. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/maintenance.py +0 -0
  38. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/mcp.py +0 -0
  39. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/mcp_server.py +0 -0
  40. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/model_catalog.py +0 -0
  41. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/model_info.py +0 -0
  42. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/model_resolve.py +0 -0
  43. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/observation_aging.py +0 -0
  44. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/orchestration.py +0 -0
  45. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/otel.py +0 -0
  46. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/plugins.py +0 -0
  47. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/pool.py +0 -0
  48. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/pricing.py +0 -0
  49. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/pseudo.py +0 -0
  50. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/recording.py +0 -0
  51. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/reminders.py +0 -0
  52. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/replay.py +0 -0
  53. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/resume.py +0 -0
  54. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/retry.py +0 -0
  55. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/sandboxed.py +0 -0
  56. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/skills.py +0 -0
  57. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/spill.py +0 -0
  58. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  59. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  60. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  61. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  62. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  63. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/subagent.py +0 -0
  64. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/todo.py +0 -0
  65. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/tokens.py +0 -0
  66. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/tool_schema.py +0 -0
  67. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/tool_search.py +0 -0
  68. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/tools.py +0 -0
  69. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/tracing.py +0 -0
  70. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  71. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  72. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  73. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime/world_state.py +0 -0
  74. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  75. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  76. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  77. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  78. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_antihallucination.py +0 -0
  79. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_approval.py +0 -0
  80. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_approval_policy.py +0 -0
  81. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_ask_user.py +0 -0
  82. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_branch.py +0 -0
  83. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_cache_control.py +0 -0
  84. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_cache_instrumentation.py +0 -0
  85. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_calibration.py +0 -0
  86. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_config.py +0 -0
  87. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_content_parts.py +0 -0
  88. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_error_taxonomy.py +0 -0
  89. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_fragment_bounds.py +0 -0
  90. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_gateway_catalog.py +0 -0
  91. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_golden.py +0 -0
  92. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_handoff.py +0 -0
  93. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_harness.py +0 -0
  94. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_harness_spec.py +0 -0
  95. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_history.py +0 -0
  96. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_history_persistence.py +0 -0
  97. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_hooks.py +0 -0
  98. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_in_memory_storage.py +0 -0
  99. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_llm_gemini_wire.py +0 -0
  100. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_llm_responses_wire.py +0 -0
  101. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_llm_stream_e2e.py +0 -0
  102. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_llm_wire_helpers.py +0 -0
  103. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_long_session.py +0 -0
  104. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_loop.py +0 -0
  105. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_loop_cancellation.py +0 -0
  106. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_loop_replay.py +0 -0
  107. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_loop_sandbox_event.py +0 -0
  108. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_maintenance.py +0 -0
  109. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_mcp.py +0 -0
  110. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_mcp_server.py +0 -0
  111. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_model_catalog.py +0 -0
  112. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_model_equivalence.py +0 -0
  113. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_model_info.py +0 -0
  114. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_model_resolve.py +0 -0
  115. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_observation_aging.py +0 -0
  116. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_orchestration.py +0 -0
  117. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_otel.py +0 -0
  118. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_parallel_tools.py +0 -0
  119. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_plugins.py +0 -0
  120. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_provider_compat.py +0 -0
  121. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_provider_presets.py +0 -0
  122. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_pseudo.py +0 -0
  123. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_recording.py +0 -0
  124. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_reminders.py +0 -0
  125. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_replay_crosslang.py +0 -0
  126. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_resume.py +0 -0
  127. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_retry_hooks.py +0 -0
  128. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_safety_gate.py +0 -0
  129. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_sandboxed.py +0 -0
  130. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_skills.py +0 -0
  131. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_soft_timeout.py +0 -0
  132. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_spill.py +0 -0
  133. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_sqlite_storage.py +0 -0
  134. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_steer.py +0 -0
  135. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_storage_contract.py +0 -0
  136. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_stream_strip.py +0 -0
  137. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_subagent.py +0 -0
  138. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_system_proxy.py +0 -0
  139. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_todo.py +0 -0
  140. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_todo_gate.py +0 -0
  141. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_tokens.py +0 -0
  142. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_tool_exposure.py +0 -0
  143. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_tool_hygiene.py +0 -0
  144. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_tool_router.py +0 -0
  145. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_tool_schema.py +0 -0
  146. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_tool_timeout.py +0 -0
  147. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_trace_recorder.py +0 -0
  148. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_transport_jsonrpc.py +0 -0
  149. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_transport_sse.py +0 -0
  150. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_usage_attribution.py +0 -0
  151. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_world_state.py +0 -0
  152. {steerable_agent_runtime-0.6.7 → steerable_agent_runtime-0.6.8}/tests/test_write_lease.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.7"
3
+ version = "0.6.8"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -22,7 +22,9 @@ Strategy (deterministic first, LLM optional):
22
22
  message; otherwise drop the middle span behind a marker.
23
23
 
24
24
  System messages and the first user message (the goal) are always kept; the
25
- most recent ``keep_last_messages`` are never touched. Between compactions
25
+ most recent ``keep_last_messages`` are never touched, and the kept tail is
26
+ widened when that count would cut a tool-call group in half (an orphan
27
+ ``tool`` message is a provider-level 400). Between compactions
26
28
  the transcript is append-only, so provider prompt caches keep hitting; a
27
29
  rewrite invalidates the cache once, then the prefix is stable again.
28
30
 
@@ -440,6 +442,50 @@ class CompactionHooks(NoopHooks):
440
442
  for i, m in enumerate(transcript)
441
443
  ]
442
444
 
445
+ @staticmethod
446
+ def _orphan_tool_ids(tail: Sequence[LLMMessage]) -> set[str]:
447
+ """``tool_call_id``s answered inside ``tail`` but issued before it."""
448
+ issued: set[str] = set()
449
+ orphans: set[str] = set()
450
+ for m in tail:
451
+ for call in m.tool_calls or ():
452
+ issued.add(call.id)
453
+ if (
454
+ m.role == "tool"
455
+ and m.tool_call_id is not None
456
+ and m.tool_call_id not in issued
457
+ ):
458
+ orphans.add(m.tool_call_id)
459
+ return orphans
460
+
461
+ def _tail_start(self, transcript: list[LLMMessage], head_end: int) -> int:
462
+ """Where the untouched tail begins: ``keep_last_messages`` back, then
463
+ widened until the tail answers no tool call it does not also issue.
464
+
465
+ OpenAI-compatible providers reject a ``tool`` message whose
466
+ ``tool_call_id`` was never issued by a preceding ``assistant``
467
+ message (DeepSeek answers HTTP 400 ``invalid_request``). A count-only
468
+ cut can land inside a tool-call group, summarizing the issuing
469
+ assistant away while its results stay in the tail. Widening only ever
470
+ keeps more, and is bounded by one round's tool calls — including the
471
+ ``user`` image messages the read path interleaves between results.
472
+
473
+ Only orphans this cut created are worth widening for. A transcript
474
+ that arrives already missing an issuer (a host that resumed from a
475
+ truncated log) would otherwise widen to ``head_end`` and disable
476
+ summarization for the rest of the run, trading a provider 400 for a
477
+ context overflow.
478
+ """
479
+ tail_start = max(head_end, len(transcript) - self._keep_last)
480
+ issuable = {
481
+ call.id for m in transcript[head_end:] for call in m.tool_calls or ()
482
+ }
483
+ while tail_start > head_end and (
484
+ self._orphan_tool_ids(transcript[tail_start:]) & issuable
485
+ ):
486
+ tail_start -= 1
487
+ return tail_start
488
+
443
489
  async def _summarize_middle(self, transcript: list[LLMMessage]) -> list[LLMMessage]:
444
490
  # Keep every system message plus the first user message (the goal) as
445
491
  # the head; keep the recent tail untouched; summarize the middle.
@@ -451,7 +497,7 @@ class CompactionHooks(NoopHooks):
451
497
  head_end = i + 1
452
498
  else:
453
499
  break
454
- tail_start = max(head_end, len(transcript) - self._keep_last)
500
+ tail_start = self._tail_start(transcript, head_end)
455
501
  middle = transcript[head_end:tail_start]
456
502
  if not middle:
457
503
  return transcript
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -437,6 +437,88 @@ async def test_compaction_preserves_system_and_first_user_message() -> None:
437
437
  assert call[1].role == "user" and call[1].content_text == "the original goal"
438
438
 
439
439
 
440
+ def tool_result(call_id: str) -> LLMMessage:
441
+ return LLMMessage.text_of("tool", "ok", name="emit", tool_call_id=call_id)
442
+
443
+
444
+ @pytest.mark.asyncio
445
+ async def test_summarize_middle_widens_the_tail_off_an_orphan_tool_result() -> None:
446
+ # A count-only tail cut can land inside a tool-call group: the issuing
447
+ # assistant goes into the summarized middle while its results stay in the
448
+ # tail, and the request opens on a `tool` message nothing answered.
449
+ # DeepSeek rejects that with HTTP 400 invalid_request.
450
+ calls = [
451
+ ToolCall(id="call_a", name="emit", arguments={}),
452
+ ToolCall(id="call_b", name="emit", arguments={}),
453
+ ]
454
+ transcript = [
455
+ LLMMessage.text_of("system", "you are helpful"),
456
+ LLMMessage.text_of("user", "make a deck"),
457
+ LLMMessage.text_of("assistant", "reading the skill files"),
458
+ LLMMessage.text_of("assistant", "", tool_calls=calls),
459
+ tool_result("call_a"),
460
+ # the read path interleaves image messages between tool results
461
+ LLMMessage.text_of("user", "slide.png:"),
462
+ tool_result("call_b"),
463
+ LLMMessage.text_of("assistant", "wrapping up"),
464
+ ]
465
+ hooks = CompactionHooks(max_context_tokens=1_000, keep_last_messages=3)
466
+
467
+ out = await hooks._summarize_middle(transcript)
468
+
469
+ assert CompactionHooks._orphan_tool_ids(out) == set()
470
+ assert any("[context compacted" in m.content_text for m in out)
471
+ # the group survived whole, in order, with its interleaved image message
472
+ assert [m.role for m in out[-5:]] == [
473
+ "assistant",
474
+ "tool",
475
+ "user",
476
+ "tool",
477
+ "assistant",
478
+ ]
479
+
480
+
481
+ @pytest.mark.asyncio
482
+ async def test_summarize_middle_still_compacts_an_already_orphaned_transcript() -> None:
483
+ # The issuer is missing from the input (a host resuming from a truncated
484
+ # log), so widening could never repair the tail. Compaction proceeds
485
+ # rather than widening to the head and going silent for the rest of the
486
+ # run — the alternative to the provider 400 is a context overflow.
487
+ transcript = [
488
+ LLMMessage.text_of("user", "make a deck"),
489
+ LLMMessage.text_of("assistant", "first"),
490
+ tool_result("call_gone_a"),
491
+ tool_result("call_gone_b"),
492
+ LLMMessage.text_of("assistant", "wrapping up"),
493
+ ]
494
+ hooks = CompactionHooks(max_context_tokens=1_000, keep_last_messages=2)
495
+
496
+ out = await hooks._summarize_middle(transcript)
497
+
498
+ assert len(out) < len(transcript)
499
+ assert any("[context compacted" in m.content_text for m in out)
500
+
501
+
502
+ @pytest.mark.asyncio
503
+ async def test_summarize_middle_keeps_an_aligned_tail_at_keep_last() -> None:
504
+ # No tool-call group is cut, so the tail stays exactly keep_last long —
505
+ # widening must not cost compaction on a clean boundary.
506
+ transcript = [
507
+ LLMMessage.text_of("system", "you are helpful"),
508
+ LLMMessage.text_of("user", "make a deck"),
509
+ LLMMessage.text_of("assistant", "first"),
510
+ LLMMessage.text_of("assistant", "second"),
511
+ LLMMessage.text_of("user", "keep going"),
512
+ LLMMessage.text_of("assistant", "third"),
513
+ ]
514
+ hooks = CompactionHooks(max_context_tokens=1_000, keep_last_messages=3)
515
+
516
+ out = await hooks._summarize_middle(transcript)
517
+
518
+ assert [m.content_text for m in out[-3:]] == ["second", "keep going", "third"]
519
+ assert len(out) == 6 # system + goal + summary + 3 kept
520
+
521
+
440
522
  @pytest.mark.asyncio
441
523
  async def test_micro_compact_trigger_schedule() -> None:
442
524
  """Periodic prune fires on interval multiples (never round 0), and an