voidx 3.1.0__tar.gz → 3.1.1__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 (282) hide show
  1. {voidx-3.1.0 → voidx-3.1.1}/PKG-INFO +1 -1
  2. {voidx-3.1.0 → voidx-3.1.1}/pyproject.toml +1 -1
  3. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/__init__.py +1 -1
  4. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/goal_resolver.py +45 -41
  5. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/compaction.py +20 -12
  6. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/compaction_coordinator.py +194 -20
  7. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/contracts.py +13 -4
  8. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/core/_voidx_graph.py +0 -1
  9. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/core/llm.py +123 -19
  10. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/streaming.py +65 -26
  11. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/subagent.py +14 -11
  12. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/executor.py +2 -0
  13. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/guards.py +2 -2
  14. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/helpers.py +1 -0
  15. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/turn_runner.py +10 -2
  16. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/wiring.py +2 -0
  17. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/workflow_utils.py +1 -1
  18. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/message_rows.py +3 -0
  19. voidx-3.1.1/src/voidx/agent/tool_call_ids.py +33 -0
  20. voidx-3.1.1/src/voidx/agent/tool_exchange_sanitizer.py +158 -0
  21. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/models.py +3 -0
  22. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/compaction.py +144 -8
  23. voidx-3.1.1/src/voidx/llm/message_status.py +12 -0
  24. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/session.py +5 -0
  25. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/base.py +42 -5
  26. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/edit_execute.py +22 -1
  27. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/edit_resolve.py +72 -4
  28. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/file.py +7 -0
  29. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/render.py +0 -23
  30. {voidx-3.1.0 → voidx-3.1.1}/src/voidx.egg-info/PKG-INFO +1 -1
  31. {voidx-3.1.0 → voidx-3.1.1}/src/voidx.egg-info/SOURCES.txt +3 -0
  32. {voidx-3.1.0 → voidx-3.1.1}/README.md +0 -0
  33. {voidx-3.1.0 → voidx-3.1.1}/setup.cfg +0 -0
  34. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/__init__.py +0 -0
  35. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/agents.py +0 -0
  36. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/attachments.py +0 -0
  37. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/__init__.py +0 -0
  38. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/convergence.py +0 -0
  39. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/core/__init__.py +0 -0
  40. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/core/helpers.py +0 -0
  41. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/permissions.py +0 -0
  42. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/run_loop.py +0 -0
  43. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/runtime.py +0 -0
  44. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/runtime_guards.py +0 -0
  45. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/session_mixin.py +0 -0
  46. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/session_runtime.py +0 -0
  47. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/title_mixin.py +0 -0
  48. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/todo_events.py +0 -0
  49. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_execution.py +0 -0
  50. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/__init__.py +0 -0
  51. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/types.py +0 -0
  52. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/ui.py +0 -0
  53. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/tool_executor/workflow.py +0 -0
  54. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/topology.py +0 -0
  55. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/transcript_mixin.py +0 -0
  56. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/graph/turn_mixin.py +0 -0
  57. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/prompts.py +0 -0
  58. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/runtime_context.py +0 -0
  59. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/__init__.py +0 -0
  60. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/code_ide.py +0 -0
  61. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/guide.py +0 -0
  62. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/handler.py +0 -0
  63. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/host.py +0 -0
  64. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/init.py +0 -0
  65. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/lsp.py +0 -0
  66. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/mcp.py +0 -0
  67. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/model.py +0 -0
  68. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/profile.py +0 -0
  69. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/runtime.py +0 -0
  70. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/session.py +0 -0
  71. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/skills.py +0 -0
  72. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/slash/upgrade.py +0 -0
  73. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/state.py +0 -0
  74. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/task_state.py +0 -0
  75. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/todo_state.py +0 -0
  76. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/tool_filters.py +0 -0
  77. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/tool_messages.py +0 -0
  78. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/agent/tool_result_storage.py +0 -0
  79. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/__init__.py +0 -0
  80. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/enums.py +0 -0
  81. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/permissions.py +0 -0
  82. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings.py +0 -0
  83. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_agent.py +0 -0
  84. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_api_keys.py +0 -0
  85. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_code_ide.py +0 -0
  86. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_custom.py +0 -0
  87. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_mcp.py +0 -0
  88. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_permissions.py +0 -0
  89. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_skills.py +0 -0
  90. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_update.py +0 -0
  91. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_utils.py +0 -0
  92. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/config/settings_web.py +0 -0
  93. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/data/__init__.py +0 -0
  94. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/data/templates/api-doc.md +0 -0
  95. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/data/templates/prd.md +0 -0
  96. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/data/templates/readme.md +0 -0
  97. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/data/templates/rfc.md +0 -0
  98. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/data/templates/tech-design.md +0 -0
  99. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/diffing.py +0 -0
  100. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/__init__.py +0 -0
  101. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/catalog.py +0 -0
  102. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/context.py +0 -0
  103. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/instruction.py +0 -0
  104. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/message_markers.py +0 -0
  105. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/provider.py +0 -0
  106. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/service.py +0 -0
  107. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/llm/usage.py +0 -0
  108. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/logging/__init__.py +0 -0
  109. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/logging/request_log.py +0 -0
  110. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/logging/tool_log.py +0 -0
  111. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/__init__.py +0 -0
  112. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/client.py +0 -0
  113. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/config.py +0 -0
  114. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/detector.py +0 -0
  115. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/detector_data.py +0 -0
  116. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/errors.py +0 -0
  117. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/manager.py +0 -0
  118. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/schema.py +0 -0
  119. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/lsp/service.py +0 -0
  120. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/main.py +0 -0
  121. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/__init__.py +0 -0
  122. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/client/__init__.py +0 -0
  123. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/client/base.py +0 -0
  124. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/client/errors.py +0 -0
  125. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/client/http_transport.py +0 -0
  126. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/client/sse_transport.py +0 -0
  127. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/client/stdio_transport.py +0 -0
  128. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/manager.py +0 -0
  129. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/schema.py +0 -0
  130. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp/tool.py +0 -0
  131. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp_servers/__init__.py +0 -0
  132. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/mcp_servers/web.py +0 -0
  133. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/__init__.py +0 -0
  134. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/cleanup.py +0 -0
  135. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/context_frames.py +0 -0
  136. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/jsonl_store.py +0 -0
  137. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/model_profiles.py +0 -0
  138. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/runtime_state.py +0 -0
  139. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/service.py +0 -0
  140. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/store.py +0 -0
  141. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/subagents.py +0 -0
  142. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/memory/transcript.py +0 -0
  143. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/__init__.py +0 -0
  144. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/context.py +0 -0
  145. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/engine.py +0 -0
  146. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/evaluate.py +0 -0
  147. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/rules.py +0 -0
  148. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/sandbox.py +0 -0
  149. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/schema.py +0 -0
  150. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/service.py +0 -0
  151. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/permission/wildcard.py +0 -0
  152. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/__init__.py +0 -0
  153. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/attachments.py +0 -0
  154. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/intent.py +0 -0
  155. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/reference_tokens.py +0 -0
  156. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/task_state.py +0 -0
  157. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/todo.py +0 -0
  158. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/ui.py +0 -0
  159. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/runtime/ui_port.py +0 -0
  160. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/selfupdate.py +0 -0
  161. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/skills/__init__.py +0 -0
  162. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/skills/context.py +0 -0
  163. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/skills/references.py +0 -0
  164. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/skills/registry.py +0 -0
  165. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/skills/schema.py +0 -0
  166. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/skills/service.py +0 -0
  167. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/__init__.py +0 -0
  168. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/agent.py +0 -0
  169. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/bash.py +0 -0
  170. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/bash_router.py +0 -0
  171. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/clarify.py +0 -0
  172. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/compact_context.py +0 -0
  173. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/__init__.py +0 -0
  174. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/line.py +0 -0
  175. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/read.py +0 -0
  176. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/types.py +0 -0
  177. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_ops/write.py +0 -0
  178. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/file_state.py +0 -0
  179. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/git.py +0 -0
  180. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/load_doc_template.py +0 -0
  181. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/load_skills.py +0 -0
  182. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/lsp.py +0 -0
  183. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/plan_checkpoint.py +0 -0
  184. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/registry.py +0 -0
  185. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/repomap.py +0 -0
  186. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/search.py +0 -0
  187. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/service.py +0 -0
  188. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/task_status.py +0 -0
  189. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/task_tracker.py +0 -0
  190. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/todo.py +0 -0
  191. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/web_content.py +0 -0
  192. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/web_mcp.py +0 -0
  193. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/webfetch.py +0 -0
  194. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/websearch.py +0 -0
  195. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/tools/workflow.py +0 -0
  196. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/__init__.py +0 -0
  197. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/commands.py +0 -0
  198. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/frontend.py +0 -0
  199. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/gateway/__init__.py +0 -0
  200. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/gateway/bootstrap.py +0 -0
  201. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/gateway/server.py +0 -0
  202. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/gateway/session.py +0 -0
  203. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/__init__.py +0 -0
  204. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/agent_display.py +0 -0
  205. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/browse.py +0 -0
  206. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/capture.py +0 -0
  207. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/console/__init__.py +0 -0
  208. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/console/app.py +0 -0
  209. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/console/formatting.py +0 -0
  210. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/console/streaming.py +0 -0
  211. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/diff.py +0 -0
  212. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/display_policy.py +0 -0
  213. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/__init__.py +0 -0
  214. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/agent_placeholder.py +0 -0
  215. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/app.py +0 -0
  216. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/formatting.py +0 -0
  217. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/nodes.py +0 -0
  218. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/nodes_permission.py +0 -0
  219. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/nodes_startup.py +0 -0
  220. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/nodes_status.py +0 -0
  221. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/state.py +0 -0
  222. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/status.py +0 -0
  223. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/stream.py +0 -0
  224. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/dock/todo.py +0 -0
  225. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/events/__init__.py +0 -0
  226. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/events/bus.py +0 -0
  227. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/events/consumers.py +0 -0
  228. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/events/schema.py +0 -0
  229. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/tree.py +0 -0
  230. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/output/types.py +0 -0
  231. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/protocol/__init__.py +0 -0
  232. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/protocol/commands.py +0 -0
  233. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/protocol/envelope.py +0 -0
  234. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/protocol/requests.py +0 -0
  235. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/protocol/schema.py +0 -0
  236. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/protocol/transcript.py +0 -0
  237. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/session.py +0 -0
  238. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/__init__.py +0 -0
  239. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/attachment_tokens.py +0 -0
  240. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/clipboard_image.py +0 -0
  241. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/clipboard_text.py +0 -0
  242. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/code_ide.py +0 -0
  243. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/file_picker.py +0 -0
  244. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tools/skill_picker.py +0 -0
  245. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/transcript.py +0 -0
  246. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/__init__.py +0 -0
  247. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/activity.py +0 -0
  248. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/app.py +0 -0
  249. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/choice_mixin.py +0 -0
  250. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/clipboard_mixin.py +0 -0
  251. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/helpers.py +0 -0
  252. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/input.py +0 -0
  253. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/overlays.py +0 -0
  254. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/panels.py +0 -0
  255. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/parser.py +0 -0
  256. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/render_activity.py +0 -0
  257. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/render_frame.py +0 -0
  258. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/render_input.py +0 -0
  259. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/render_status.py +0 -0
  260. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/render_todo.py +0 -0
  261. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/renderer.py +0 -0
  262. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/state.py +0 -0
  263. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/terminal_mixin.py +0 -0
  264. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/ui/tui/text_prompt_mixin.py +0 -0
  265. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/__init__.py +0 -0
  266. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/auto_advance.py +0 -0
  267. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/context.py +0 -0
  268. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/dag.py +0 -0
  269. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/nodes.py +0 -0
  270. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/policy.py +0 -0
  271. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/reconcile.py +0 -0
  272. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/route.py +0 -0
  273. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/runtime.py +0 -0
  274. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/schema.py +0 -0
  275. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/service.py +0 -0
  276. {voidx-3.1.0 → voidx-3.1.1}/src/voidx/workflow/types.py +0 -0
  277. {voidx-3.1.0 → voidx-3.1.1}/src/voidx.egg-info/dependency_links.txt +0 -0
  278. {voidx-3.1.0 → voidx-3.1.1}/src/voidx.egg-info/entry_points.txt +0 -0
  279. {voidx-3.1.0 → voidx-3.1.1}/src/voidx.egg-info/requires.txt +0 -0
  280. {voidx-3.1.0 → voidx-3.1.1}/src/voidx.egg-info/top_level.txt +0 -0
  281. {voidx-3.1.0 → voidx-3.1.1}/tests/test_install_sh.py +0 -0
  282. {voidx-3.1.0 → voidx-3.1.1}/tests/test_npm_package.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidx
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: A coding agent that quantifies everything and solves with tools, not fuzzy prompts.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voidx"
3
- version = "3.1.0"
3
+ version = "3.1.1"
4
4
  description = "A coding agent that quantifies everything and solves with tools, not fuzzy prompts."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """VoidX - A coding agent that quantifies everything."""
2
2
 
3
- __version__ = "3.1.0"
3
+ __version__ = "3.1.1"
@@ -230,8 +230,36 @@ def _truncate_error_text(value: str, limit: int = 2000) -> str:
230
230
 
231
231
  def _resolver_system_prompt() -> str:
232
232
  return (
233
- "Resolve this turn into intent, goal, workflow, and kind_hint.\n"
234
- "Read the Markdown request. Return only structured data matching the schema at the end."
233
+ "You are a goal resolver. Classify the user's current turn into intent, goal, workflow, and kind_hint.\n"
234
+ "\n"
235
+ "## Output Schema\n"
236
+ "\n"
237
+ "Return a JSON object matching this template:\n"
238
+ "\n"
239
+ "{\n"
240
+ ' "intent": "coding" or "general",\n'
241
+ ' "goal": null or "<short summary of the user\'s request in their language, 1-2 sentences>",\n'
242
+ ' "workflow": null or "<one of the workflows listed below>",\n'
243
+ ' "kind_hint": null or "<semantic hint: review | debug | feature | inspect | refactor | test | docs>"\n'
244
+ "}\n"
245
+ "\n"
246
+ "## Field Rules\n"
247
+ "\n"
248
+ '- **intent**: "coding" for codebase/workspace work; "general" for non-code conversation.\n'
249
+ "- **goal**: Short user-language summary when a workflow should start; null otherwise. Must be set exactly when workflow is set, and null exactly when workflow is null.\n"
250
+ "- **workflow**: The workflow to start, or null. Must be set exactly when goal is set.\n"
251
+ "- **kind_hint**: Optional semantic hint. Advisory only; never overrides workflow selection.\n"
252
+ "\n"
253
+ "## Available Workflows\n"
254
+ "\n"
255
+ "- brainstorm: Confirm requirements and design, get user approval\n"
256
+ "- debug: Locate root cause and confirm fix direction\n"
257
+ "- design: Produce a structured document that passes the reader test\n"
258
+ "- feedback: Verify and implement valid review feedback\n"
259
+ "- plan: Produce an executable implementation plan, get user approval\n"
260
+ "- review: Initiate structured code review request and collect verdict\n"
261
+ "- tdd: Complete implementation via TDD cycle, all tests green\n"
262
+ "- verify: Prove changes reach expected state with reproducible evidence\n"
235
263
  )
236
264
 
237
265
 
@@ -239,52 +267,22 @@ def _resolver_request_markdown(user_text: str, task_state: TaskState) -> str:
239
267
  recent_content = _recent_exchanges_content(task_state)
240
268
  active = ", ".join(_active_workflow_names(task_state)) or "none"
241
269
  goal = task_state.current_goal.label if task_state.current_goal is not None else "none"
242
- return "\n".join([
243
- "# Goal Resolver Request",
244
- "",
245
- "## Current State",
270
+ sections = [
271
+ "# Context",
246
272
  "",
247
273
  f"- intent: {task_state.current_intent.value}",
248
274
  f"- goal: {goal}",
249
275
  f"- active workflows: {active}",
250
276
  "",
251
- "## Recent Conversation Content",
277
+ "# Recent Conversation",
252
278
  "",
253
- "```text",
254
279
  recent_content,
255
- "```",
256
280
  "",
257
- "## Current User Content",
281
+ "# Current User Question",
258
282
  "",
259
- "```text",
260
283
  user_text,
261
- "```",
262
- "",
263
- "## Available Workflows",
264
- "",
265
- "- brainstorm: Confirm requirements and design, get user approval",
266
- "- debug: Locate root cause and confirm fix direction",
267
- "- design: Produce a structured document that passes the reader test",
268
- "- feedback: Verify and implement valid review feedback",
269
- "- plan: Produce an executable implementation plan, get user approval",
270
- "- review: Initiate structured code review request and collect verdict",
271
- "- tdd: Complete implementation via TDD cycle, all tests green",
272
- "- verify: Prove changes reach expected state with reproducible evidence",
273
- "",
274
- "## Return Fields",
275
- "",
276
- '- intent: "coding" for codebase/workspace work; "general" for non-code conversation.',
277
- "- goal: short user-language summary, or null when no workflow should start.",
278
- "- workflow: workflow to start, or null. Must be set exactly when goal is set.",
279
- "- kind_hint: optional semantic hint such as review/debug/feature/inspect. Advisory only; never overrides workflow.",
280
- "",
281
- "## ResolverGoal Schema",
282
- "",
283
- "- intent: 'coding' | 'general'",
284
- "- goal: null or string (short summary of the user's request in their language, 1-2 sentences)",
285
- "- workflow: null or one of [brainstorm, debug, design, feedback, plan, review, tdd, verify]",
286
- "- kind_hint: null or string (non-authoritative semantic hint; not used for routing)",
287
- ])
284
+ ]
285
+ return "\n".join(sections)
288
286
 
289
287
 
290
288
  _ALLOWED_JOIN_NODES = {"debug", "brainstorm", "design", "plan", "tdd", "review", "feedback", "verify"}
@@ -405,10 +403,16 @@ def _normalize_resolution(
405
403
  def _recent_exchanges_content(task_state: TaskState) -> str:
406
404
  blocks: list[str] = []
407
405
  for index, exchange in enumerate(task_state.recent_exchanges, start=1):
408
- parts = [part for part in (exchange.user_text.strip(), exchange.assistant_text.strip()) if part]
409
- if not parts:
406
+ lines: list[str] = [f"## Turn {index}", ""]
407
+ user_text = exchange.user_text.strip()
408
+ assistant_text = exchange.assistant_text.strip()
409
+ if user_text:
410
+ lines.append(f"**Human**: {user_text}")
411
+ if assistant_text:
412
+ lines.append(f"**Assistant**: {assistant_text}")
413
+ if len(lines) <= 2:
410
414
  continue
411
- blocks.append(f"### Content {index}\n\n" + "\n\n".join(parts))
415
+ blocks.append("\n".join(lines))
412
416
  return "\n\n".join(blocks)
413
417
 
414
418
 
@@ -4,7 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from voidx.agent.graph.compaction_coordinator import CompactionResult, GraphCompactionCoordinator
7
+ from voidx.agent.graph.compaction_coordinator import (
8
+ CompactionResult,
9
+ GraphCompactionCoordinator,
10
+ PreflightCompactionResult,
11
+ )
8
12
 
9
13
  if TYPE_CHECKING:
10
14
  from voidx.agent.graph.contracts import GraphCompactionHost
@@ -30,36 +34,40 @@ class GraphCompactionMixin:
30
34
  *,
31
35
  force: bool = False,
32
36
  ask: bool = True,
37
+ preflight: bool = False,
33
38
  ) -> tuple[list | None, str | None]:
34
39
  return await _compaction_component_for(self).maybe_compact(
35
40
  messages,
36
41
  session_msgs,
37
42
  force=force,
38
43
  ask=ask,
44
+ preflight=preflight,
39
45
  run_compaction_agent=self._run_compaction_agent,
40
46
  persist_compaction=self._persist_compaction,
41
47
  )
42
48
 
43
- async def _in_turn_compact(
49
+ async def _preflight_compact_if_needed(
44
50
  self: GraphCompactionHost,
45
51
  messages: list,
46
- ) -> CompactionResult | None:
47
- count = getattr(self, "_in_turn_compaction_count", 0) + 1
48
- self._in_turn_compaction_count = count
49
- if count > 2:
50
- return None
51
- result = await _compaction_component_for(self).compact_for_live_state(
52
+ session_msgs: list | None = None,
53
+ *,
54
+ force: bool = False,
55
+ reason: str = "threshold",
56
+ ask: bool = False,
57
+ ) -> tuple[CompactionResult | None, PreflightCompactionResult]:
58
+ result, preflight_result = await _compaction_component_for(self).preflight_compact_if_needed(
52
59
  messages,
53
- force=True,
54
- ask=False,
55
- include_summary_message=True,
60
+ session_msgs,
61
+ force=force,
62
+ reason=reason,
63
+ ask=ask,
56
64
  run_compaction_agent=self._run_compaction_agent,
57
65
  persist_compaction=self._persist_compaction,
58
66
  )
59
67
  if result is not None:
60
68
  self._file_read_coverage.clear()
61
69
  self._file_mtimes.clear()
62
- return result
70
+ return result, preflight_result
63
71
 
64
72
  async def _ask_compact(self: GraphCompactionHost, total_tokens: int) -> bool:
65
73
  return await _compaction_component_for(self).ask_compact(total_tokens)
@@ -4,9 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from collections.abc import Awaitable, Callable
7
- from dataclasses import dataclass
7
+ from dataclasses import dataclass, field
8
8
  from typing import TYPE_CHECKING
9
9
 
10
+ from pydantic import BaseModel
11
+
10
12
  from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
11
13
 
12
14
  from voidx.agent.graph.streaming import extract_text, stream_llm
@@ -47,6 +49,38 @@ class CompactionResult:
47
49
  live_messages: list[BaseMessage]
48
50
  tail_id: str | None
49
51
  fallback: bool = False
52
+ metadata: dict[str, object] = field(default_factory=dict)
53
+
54
+
55
+ class PreflightCompactionResult(BaseModel):
56
+ compacted: bool
57
+ summary: str = ""
58
+ removed_message_count: int = 0
59
+ retained_turn_count: int = 0
60
+ pre_tokens: int = 0
61
+ post_tokens: int = 0
62
+ post_target_tokens: int = 0
63
+ tail_anchor_id: str = ""
64
+ fallback: bool = False
65
+ reason: str = ""
66
+
67
+ @classmethod
68
+ def from_compaction_result(cls, result: CompactionResult | None) -> "PreflightCompactionResult":
69
+ if result is None:
70
+ return cls(compacted=False)
71
+ metadata = result.metadata or {}
72
+ return cls(
73
+ compacted=True,
74
+ summary=result.summary,
75
+ removed_message_count=int(metadata.get("removed_message_count") or len(result.removed_messages)),
76
+ retained_turn_count=int(metadata.get("retained_turn_count") or 0),
77
+ pre_tokens=int(metadata.get("pre_tokens") or 0),
78
+ post_tokens=int(metadata.get("post_tokens") or 0),
79
+ post_target_tokens=int(metadata.get("post_compaction_target") or 0),
80
+ tail_anchor_id=str(metadata.get("tail_anchor_id") or result.tail_id or ""),
81
+ fallback=bool(metadata.get("fallback") or result.fallback),
82
+ reason=str(metadata.get("compaction_reason") or ""),
83
+ )
50
84
 
51
85
 
52
86
  class GraphCompactionCoordinator:
@@ -62,6 +96,7 @@ class GraphCompactionCoordinator:
62
96
  *,
63
97
  force: bool = False,
64
98
  ask: bool = True,
99
+ preflight: bool = False,
65
100
  run_compaction_agent: RunCompactionAgent | None = None,
66
101
  persist_compaction: PersistCompaction | None = None,
67
102
  ) -> tuple[list | None, str | None]:
@@ -75,6 +110,7 @@ class GraphCompactionCoordinator:
75
110
  session_msgs,
76
111
  force=force,
77
112
  ask=ask,
113
+ preflight=preflight,
78
114
  run_compaction_agent=run_compaction_agent,
79
115
  persist_compaction=persist_compaction,
80
116
  )
@@ -85,6 +121,34 @@ class GraphCompactionCoordinator:
85
121
  messages.extend(result.live_messages)
86
122
  return result.removed_messages, result.tail_id
87
123
 
124
+ async def preflight_compact_if_needed(
125
+ self,
126
+ messages: list[BaseMessage],
127
+ session_msgs: list | None = None,
128
+ *,
129
+ force: bool = False,
130
+ reason: str = "threshold",
131
+ ask: bool = False,
132
+ run_compaction_agent: RunCompactionAgent | None = None,
133
+ persist_compaction: PersistCompaction | None = None,
134
+ ) -> tuple[CompactionResult | None, PreflightCompactionResult]:
135
+ result = await self.compact_for_live_state(
136
+ messages,
137
+ session_msgs,
138
+ force=force,
139
+ ask=ask,
140
+ preflight=True,
141
+ include_summary_message=False,
142
+ run_compaction_agent=run_compaction_agent,
143
+ persist_compaction=persist_compaction,
144
+ )
145
+ preflight_result = PreflightCompactionResult.from_compaction_result(result)
146
+ if preflight_result.compacted and reason and preflight_result.reason in {"", "threshold", "force"}:
147
+ preflight_result.reason = reason
148
+ if result is not None:
149
+ result.metadata["compaction_reason"] = reason
150
+ return result, preflight_result
151
+
88
152
  async def compact_for_live_state(
89
153
  self,
90
154
  messages: list[BaseMessage],
@@ -92,6 +156,7 @@ class GraphCompactionCoordinator:
92
156
  *,
93
157
  force: bool = False,
94
158
  ask: bool = True,
159
+ preflight: bool = False,
95
160
  include_summary_message: bool = False,
96
161
  run_compaction_agent: RunCompactionAgent | None = None,
97
162
  persist_compaction: PersistCompaction | None = None,
@@ -103,7 +168,9 @@ class GraphCompactionCoordinator:
103
168
  total_tokens = estimate_context_tokens(messages, host.config.model.model)
104
169
  tokens = {"total": total_tokens, "input": total_tokens, "output": 0, "reasoning": 0}
105
170
 
106
- if not force and not host._compaction.is_overflow(tokens):
171
+ over_hard = host._compaction.is_overflow(tokens)
172
+ over_soft = preflight and host._compaction.is_soft_overflow(tokens)
173
+ if not force and not over_hard and not over_soft:
107
174
  return None
108
175
 
109
176
  if not force and ask and getattr(host.config, "ask_compact", False):
@@ -123,11 +190,7 @@ class GraphCompactionCoordinator:
123
190
  await host._ui.events.emit(StatusUpdated(
124
191
  status_id="compaction",
125
192
  label="Compacting context",
126
- detail=(
127
- f"{total_tokens} tokens exceed the active context budget"
128
- if not force
129
- else f"manual compaction of {len(messages)} messages"
130
- ),
193
+ detail=_compaction_status_detail(total_tokens, force=force, preflight=preflight),
131
194
  stage="compacting",
132
195
  ))
133
196
  else:
@@ -142,7 +205,15 @@ class GraphCompactionCoordinator:
142
205
  raw_semantic_messages(messages),
143
206
  preserve_trailing_ai_tool_calls=True,
144
207
  )
145
- selection = host._compaction.select_details(semantic_messages)
208
+ if preflight:
209
+ selection = host._compaction.select_preflight_details(
210
+ semantic_messages,
211
+ model=host.config.model.model,
212
+ )
213
+ if not selection.should_compact and (force or over_hard):
214
+ selection = host._compaction.select_details(semantic_messages)
215
+ else:
216
+ selection = host._compaction.select_details(semantic_messages)
146
217
  head_msgs, tail_id = selection.head, selection.tail_id
147
218
  semantic_tail = semantic_messages[selection.keep_from:]
148
219
 
@@ -155,6 +226,18 @@ class GraphCompactionCoordinator:
155
226
  ))
156
227
  return None
157
228
 
229
+ base_metadata = _compaction_metadata(
230
+ host,
231
+ semantic_messages=semantic_messages,
232
+ semantic_tail=semantic_tail,
233
+ total_tokens=total_tokens,
234
+ force=force,
235
+ preflight=preflight,
236
+ over_soft=over_soft,
237
+ over_hard=over_hard,
238
+ removed_message_count=len(head_msgs),
239
+ tail_id=tail_id,
240
+ )
158
241
  summary = None
159
242
  previous_summary = getattr(host, "_compaction_summary", "") or None
160
243
  last_error: Exception | None = None
@@ -213,6 +296,18 @@ class GraphCompactionCoordinator:
213
296
  host._compaction_summary = fallback
214
297
  host._compaction.compaction_count += 1
215
298
  await persist(head_msgs)
299
+ live_messages = _live_messages(
300
+ runtime_prefix,
301
+ semantic_tail,
302
+ fallback,
303
+ include_summary_message=include_summary_message,
304
+ )
305
+ metadata = _finish_compaction_metadata(
306
+ base_metadata,
307
+ live_messages=live_messages,
308
+ model=host.config.model.model,
309
+ fallback=True,
310
+ )
216
311
  if host._ui.via_events():
217
312
  await host._ui.events.emit(StatusFinished(
218
313
  status_id="compaction",
@@ -224,14 +319,10 @@ class GraphCompactionCoordinator:
224
319
  return CompactionResult(
225
320
  summary=fallback,
226
321
  removed_messages=list(head_msgs),
227
- live_messages=_live_messages(
228
- runtime_prefix,
229
- semantic_tail,
230
- fallback,
231
- include_summary_message=include_summary_message,
232
- ),
322
+ live_messages=live_messages,
233
323
  tail_id=tail_id,
234
324
  fallback=True,
325
+ metadata=metadata,
235
326
  )
236
327
 
237
328
  if summary:
@@ -258,17 +349,25 @@ class GraphCompactionCoordinator:
258
349
  else:
259
350
  return None
260
351
 
352
+ live_messages = _live_messages(
353
+ runtime_prefix,
354
+ semantic_tail,
355
+ summary,
356
+ include_summary_message=include_summary_message,
357
+ )
358
+ metadata = _finish_compaction_metadata(
359
+ base_metadata,
360
+ live_messages=live_messages,
361
+ model=host.config.model.model,
362
+ fallback=False,
363
+ )
261
364
  return CompactionResult(
262
365
  summary=summary,
263
366
  removed_messages=list(head_msgs),
264
- live_messages=_live_messages(
265
- runtime_prefix,
266
- semantic_tail,
267
- summary,
268
- include_summary_message=include_summary_message,
269
- ),
367
+ live_messages=live_messages,
270
368
  tail_id=tail_id,
271
369
  fallback=False,
370
+ metadata=metadata,
272
371
  )
273
372
 
274
373
  async def ask_compact(self, total_tokens: int) -> bool:
@@ -450,6 +549,81 @@ def _content_type_summary(content: object) -> str:
450
549
  return type(content).__name__
451
550
 
452
551
 
552
+ def _compaction_status_detail(total_tokens: int, *, force: bool, preflight: bool) -> str:
553
+ if force:
554
+ return "manual compaction"
555
+ if preflight:
556
+ return f"{total_tokens} tokens reached the preflight compaction threshold"
557
+ return f"{total_tokens} tokens exceed the active context budget"
558
+
559
+
560
+ def _compaction_metadata(
561
+ host: GraphCompactionHost,
562
+ *,
563
+ semantic_messages: list[BaseMessage],
564
+ semantic_tail: list[BaseMessage],
565
+ total_tokens: int,
566
+ force: bool,
567
+ preflight: bool,
568
+ over_soft: bool,
569
+ over_hard: bool,
570
+ removed_message_count: int,
571
+ tail_id: str | None,
572
+ ) -> dict[str, object]:
573
+ return {
574
+ "compaction_reason": _compaction_reason(
575
+ force=force,
576
+ preflight=preflight,
577
+ over_soft=over_soft,
578
+ over_hard=over_hard,
579
+ ),
580
+ "pre_tokens": total_tokens,
581
+ "soft_threshold": host._compaction.soft_threshold(),
582
+ "hard_threshold": int(host._compaction.context_limit * 0.90),
583
+ "post_compaction_target": host._compaction.post_compaction_target(),
584
+ "removed_message_count": removed_message_count,
585
+ "retained_turn_count": len(host._compaction._turns(semantic_tail)),
586
+ "current_user_preserved": _latest_user_preserved(semantic_messages, semantic_tail),
587
+ "tail_anchor_id": tail_id or "",
588
+ "inline_compaction_enabled": bool(getattr(host.config, "inline_compaction_enabled", False)),
589
+ }
590
+
591
+
592
+ def _finish_compaction_metadata(
593
+ metadata: dict[str, object],
594
+ *,
595
+ live_messages: list[BaseMessage],
596
+ model: str,
597
+ fallback: bool,
598
+ ) -> dict[str, object]:
599
+ return {
600
+ **metadata,
601
+ "post_tokens": estimate_context_tokens(live_messages, model),
602
+ "fallback": fallback,
603
+ }
604
+
605
+
606
+ def _compaction_reason(*, force: bool, preflight: bool, over_soft: bool, over_hard: bool) -> str:
607
+ if force:
608
+ return "force"
609
+ if over_hard:
610
+ return "hard_threshold"
611
+ if preflight and over_soft:
612
+ return "soft_threshold"
613
+ return "threshold"
614
+
615
+
616
+ def _latest_user_preserved(
617
+ semantic_messages: list[BaseMessage],
618
+ semantic_tail: list[BaseMessage],
619
+ ) -> bool:
620
+ latest_user = next(
621
+ (message for message in reversed(semantic_messages) if isinstance(message, HumanMessage)),
622
+ None,
623
+ )
624
+ return latest_user is None or any(message is latest_user for message in semantic_tail)
625
+
626
+
453
627
  def _compaction_request_text(previous_summary: str | None) -> str:
454
628
  previous_summary_section = ""
455
629
  if previous_summary:
@@ -20,7 +20,11 @@ from voidx.runtime.ui_port import AgentUiPort
20
20
  from voidx.tools.service import ToolRegistry, TaskTracker
21
21
 
22
22
  if TYPE_CHECKING:
23
- from voidx.agent.graph.compaction_coordinator import CompactionResult, GraphCompactionCoordinator
23
+ from voidx.agent.graph.compaction_coordinator import (
24
+ CompactionResult,
25
+ GraphCompactionCoordinator,
26
+ PreflightCompactionResult,
27
+ )
24
28
  from voidx.agent.graph.runtime_guards import RuntimeGuardState
25
29
  from voidx.agent.graph.session_runtime import GraphSessionRuntime
26
30
  from voidx.agent.graph.tool_executor import GraphToolExecutor
@@ -39,7 +43,6 @@ class GraphCompactionHost(Protocol):
39
43
  _usage_stats: UsageStats
40
44
  _compaction: CompactionService
41
45
  _compaction_coordinator: GraphCompactionCoordinator
42
- _in_turn_compaction_count: int
43
46
  _pending_summary: str | None
44
47
  _compaction_summary: str
45
48
  _session_msg_cache: list[Any] | None
@@ -53,11 +56,17 @@ class GraphCompactionHost(Protocol):
53
56
  *,
54
57
  force: bool = False,
55
58
  ask: bool = True,
59
+ preflight: bool = False,
56
60
  ) -> tuple[list[BaseMessage] | None, str | None]: ...
57
- async def _in_turn_compact(
61
+ async def _preflight_compact_if_needed(
58
62
  self,
59
63
  messages: list[BaseMessage],
60
- ) -> CompactionResult | None: ...
64
+ session_msgs: list[Any] | None = None,
65
+ *,
66
+ force: bool = False,
67
+ reason: str = "threshold",
68
+ ask: bool = False,
69
+ ) -> tuple[CompactionResult | None, PreflightCompactionResult]: ...
61
70
  async def _ask_compact(self, total_tokens: int) -> bool: ...
62
71
  async def _persist_compaction(self, head_messages: list[BaseMessage]) -> None: ...
63
72
  async def _compact_session_history(self, *, force: bool = True) -> bool: ...
@@ -114,7 +114,6 @@ class VoidXGraph(
114
114
  self._current_messages: list[BaseMessage] | None = None
115
115
  self._pending_summary: str | None = None
116
116
  self._compaction_summary: str = ""
117
- self._in_turn_compaction_count: int = 0
118
117
  self._session_date: str = session_date(session)
119
118
  self._session_msg_cache: list | None = None
120
119
  self._context_cache = ContextCompilerCache()