voidx 1.1.0__tar.gz → 2.0.0__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 (191) hide show
  1. {voidx-1.1.0 → voidx-2.0.0}/PKG-INFO +30 -6
  2. voidx-2.0.0/README.md +61 -0
  3. {voidx-1.1.0 → voidx-2.0.0}/pyproject.toml +6 -3
  4. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/__init__.py +1 -1
  5. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/agents.py +13 -6
  6. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/compaction.py +12 -3
  7. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/contracts.py +2 -2
  8. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/core.py +37 -6
  9. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/run_loop.py +108 -31
  10. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/subagent.py +10 -0
  11. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/tool_execution.py +115 -11
  12. voidx-2.0.0/src/voidx/agent/intent_refinement.py +241 -0
  13. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/runtime_context.py +46 -17
  14. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/handler.py +3 -2
  15. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/mcp.py +73 -9
  16. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/model.py +43 -29
  17. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/runtime.py +2 -2
  18. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/state.py +9 -3
  19. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/task_state.py +76 -52
  20. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/config.py +53 -31
  21. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/catalog.py +4 -4
  22. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/instruction.py +28 -1
  23. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/main.py +4 -9
  24. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/mcp/__init__.py +2 -1
  25. voidx-2.0.0/src/voidx/mcp/client.py +723 -0
  26. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/mcp/manager.py +104 -50
  27. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/mcp/schema.py +51 -3
  28. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/mcp/tool.py +7 -54
  29. voidx-2.0.0/src/voidx/memory/__init__.py +18 -0
  30. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/memory/context_frames.py +1 -2
  31. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/memory/model_profiles.py +36 -41
  32. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/memory/runtime_state.py +140 -31
  33. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/memory/session.py +46 -55
  34. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/memory/store.py +33 -9
  35. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/memory/transcript.py +1 -6
  36. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/engine.py +168 -31
  37. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/sandbox.py +27 -3
  38. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/skills/__init__.py +5 -0
  39. voidx-2.0.0/src/voidx/skills/bundled/superpowers/brainstorming/SKILL.md +51 -0
  40. voidx-2.0.0/src/voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +60 -0
  41. voidx-2.0.0/src/voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +59 -0
  42. voidx-2.0.0/src/voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +64 -0
  43. voidx-2.0.0/src/voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +61 -0
  44. voidx-2.0.0/src/voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +69 -0
  45. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-design-docs/SKILL.md +87 -0
  46. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-design-docs/templates/api-doc.md +64 -0
  47. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-design-docs/templates/prd.md +117 -0
  48. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-design-docs/templates/readme.md +55 -0
  49. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-design-docs/templates/rfc.md +38 -0
  50. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-design-docs/templates/tech-design.md +68 -0
  51. voidx-2.0.0/src/voidx/skills/bundled/superpowers/writing-plans/SKILL.md +54 -0
  52. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/skills/policy.py +7 -2
  53. voidx-2.0.0/src/voidx/skills/runtime.py +90 -0
  54. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/skills/service.py +35 -0
  55. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/base.py +42 -2
  56. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/bash.py +56 -1
  57. voidx-2.0.0/src/voidx/tools/clarify.py +139 -0
  58. voidx-2.0.0/src/voidx/tools/doc_template.py +96 -0
  59. voidx-2.0.0/src/voidx/tools/on_intent.py +122 -0
  60. voidx-2.0.0/src/voidx/tools/plan_checkpoint.py +161 -0
  61. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/registry.py +5 -0
  62. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/web_mcp.py +1 -1
  63. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/browse.py +2 -2
  64. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/console/app.py +1 -0
  65. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/dock/app.py +72 -1
  66. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/dock/nodes.py +27 -1
  67. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/events/__init__.py +2 -0
  68. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/tree.py +55 -24
  69. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tools/clipboard_image.py +32 -31
  70. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/app.py +54 -15
  71. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/parser.py +17 -1
  72. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/renderer.py +122 -55
  73. {voidx-1.1.0 → voidx-2.0.0}/src/voidx.egg-info/PKG-INFO +30 -6
  74. {voidx-1.1.0 → voidx-2.0.0}/src/voidx.egg-info/SOURCES.txt +13 -0
  75. {voidx-1.1.0 → voidx-2.0.0}/src/voidx.egg-info/requires.txt +1 -1
  76. {voidx-1.1.0 → voidx-2.0.0}/tests/test_compaction.py +9 -12
  77. {voidx-1.1.0 → voidx-2.0.0}/tests/test_config.py +52 -18
  78. voidx-2.0.0/tests/test_main.py +77 -0
  79. {voidx-1.1.0 → voidx-2.0.0}/tests/test_mcp.py +107 -0
  80. voidx-2.0.0/tests/test_npm_package.py +327 -0
  81. {voidx-1.1.0 → voidx-2.0.0}/tests/test_pure_tui.py +207 -0
  82. {voidx-1.1.0 → voidx-2.0.0}/tests/test_skills.py +33 -2
  83. voidx-1.1.0/README.md +0 -37
  84. voidx-1.1.0/src/voidx/mcp/client.py +0 -458
  85. voidx-1.1.0/src/voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +0 -30
  86. voidx-1.1.0/src/voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +0 -27
  87. voidx-1.1.0/src/voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +0 -36
  88. voidx-1.1.0/src/voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +0 -33
  89. voidx-1.1.0/src/voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +0 -31
  90. voidx-1.1.0/src/voidx/skills/bundled/superpowers/writing-plans/SKILL.md +0 -27
  91. voidx-1.1.0/src/voidx/ui/__init__.py +0 -0
  92. voidx-1.1.0/tests/test_main.py +0 -94
  93. voidx-1.1.0/tests/test_npm_package.py +0 -101
  94. {voidx-1.1.0 → voidx-2.0.0}/setup.cfg +0 -0
  95. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/__init__.py +0 -0
  96. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/attachments.py +0 -0
  97. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/__init__.py +0 -0
  98. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/permissions.py +0 -0
  99. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/runtime.py +0 -0
  100. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/graph/streaming.py +0 -0
  101. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/message_rows.py +0 -0
  102. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/__init__.py +0 -0
  103. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/code_ide.py +0 -0
  104. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/lsp.py +0 -0
  105. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/slash/skills.py +0 -0
  106. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/tool_filters.py +0 -0
  107. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/agent/tool_messages.py +0 -0
  108. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/__init__.py +0 -0
  109. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/compaction.py +0 -0
  110. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/context.py +0 -0
  111. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/provider.py +0 -0
  112. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/llm/usage.py +0 -0
  113. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/__init__.py +0 -0
  114. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/client.py +0 -0
  115. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/config.py +0 -0
  116. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/detector.py +0 -0
  117. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/errors.py +0 -0
  118. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/manager.py +0 -0
  119. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/schema.py +0 -0
  120. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/lsp/service.py +0 -0
  121. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/mcp_servers/__init__.py +0 -0
  122. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/mcp_servers/web.py +0 -0
  123. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/__init__.py +0 -0
  124. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/evaluate.py +0 -0
  125. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/schema.py +0 -0
  126. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/service.py +0 -0
  127. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/permission/wildcard.py +0 -0
  128. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/skills/registry.py +0 -0
  129. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/skills/schema.py +0 -0
  130. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/__init__.py +0 -0
  131. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/agent.py +0 -0
  132. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/file_ops.py +0 -0
  133. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/lsp.py +0 -0
  134. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/repomap.py +0 -0
  135. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/search.py +0 -0
  136. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/task_status.py +0 -0
  137. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/task_tracker.py +0 -0
  138. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/todo.py +0 -0
  139. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/web_content.py +0 -0
  140. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/webfetch.py +0 -0
  141. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/tools/websearch.py +0 -0
  142. {voidx-1.1.0/src/voidx/memory → voidx-2.0.0/src/voidx/ui}/__init__.py +0 -0
  143. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/commands.py +0 -0
  144. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/frontend.py +0 -0
  145. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/gateway/__init__.py +0 -0
  146. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/gateway/bootstrap.py +0 -0
  147. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/gateway/server.py +0 -0
  148. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/gateway/session.py +0 -0
  149. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/__init__.py +0 -0
  150. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/capture.py +0 -0
  151. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/console/__init__.py +0 -0
  152. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/console/formatting.py +0 -0
  153. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/console/streaming.py +0 -0
  154. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/diff.py +0 -0
  155. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/dock/__init__.py +0 -0
  156. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/dock/formatting.py +0 -0
  157. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/dock/state.py +0 -0
  158. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/events/schema.py +0 -0
  159. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/output/types.py +0 -0
  160. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/protocol/__init__.py +0 -0
  161. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/protocol/commands.py +0 -0
  162. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/protocol/envelope.py +0 -0
  163. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/protocol/requests.py +0 -0
  164. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/protocol/schema.py +0 -0
  165. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/protocol/transcript.py +0 -0
  166. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/session.py +0 -0
  167. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tools/__init__.py +0 -0
  168. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tools/attachment_tokens.py +0 -0
  169. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tools/code_ide.py +0 -0
  170. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tools/file_picker.py +0 -0
  171. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/transcript.py +0 -0
  172. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/__init__.py +0 -0
  173. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/helpers.py +0 -0
  174. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/input.py +0 -0
  175. {voidx-1.1.0 → voidx-2.0.0}/src/voidx/ui/tui/panels.py +0 -0
  176. {voidx-1.1.0 → voidx-2.0.0}/src/voidx.egg-info/dependency_links.txt +0 -0
  177. {voidx-1.1.0 → voidx-2.0.0}/src/voidx.egg-info/entry_points.txt +0 -0
  178. {voidx-1.1.0 → voidx-2.0.0}/src/voidx.egg-info/top_level.txt +0 -0
  179. {voidx-1.1.0 → voidx-2.0.0}/tests/test_clipboard_image.py +0 -0
  180. {voidx-1.1.0 → voidx-2.0.0}/tests/test_code_ide.py +0 -0
  181. {voidx-1.1.0 → voidx-2.0.0}/tests/test_llm_provider.py +0 -0
  182. {voidx-1.1.0 → voidx-2.0.0}/tests/test_llm_usage.py +0 -0
  183. {voidx-1.1.0 → voidx-2.0.0}/tests/test_lsp.py +0 -0
  184. {voidx-1.1.0 → voidx-2.0.0}/tests/test_scrollback_flush.py +0 -0
  185. {voidx-1.1.0 → voidx-2.0.0}/tests/test_startup.py +0 -0
  186. {voidx-1.1.0 → voidx-2.0.0}/tests/test_tree_smoke.py +0 -0
  187. {voidx-1.1.0 → voidx-2.0.0}/tests/test_ui_diff.py +0 -0
  188. {voidx-1.1.0 → voidx-2.0.0}/tests/test_ui_events.py +0 -0
  189. {voidx-1.1.0 → voidx-2.0.0}/tests/test_ui_frontend_protocol.py +0 -0
  190. {voidx-1.1.0 → voidx-2.0.0}/tests/test_ui_gateway.py +0 -0
  191. {voidx-1.1.0 → voidx-2.0.0}/tests/test_ui_session_changes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidx
3
- Version: 1.1.0
3
+ Version: 2.0.0
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
@@ -14,7 +14,7 @@ Requires-Dist: typer>=0.15.0
14
14
  Requires-Dist: rich>=13.9.0
15
15
  Requires-Dist: tiktoken>=0.8.0
16
16
  Requires-Dist: httpx>=0.28.0
17
- Requires-Dist: websockets>=16.0
17
+ Requires-Dist: websockets>=14
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: build>=1.2.0; extra == "dev"
20
20
  Requires-Dist: pytest>=8.0.0; extra == "dev"
@@ -26,22 +26,46 @@ voidx is a terminal AI coding agent built in Python.
26
26
 
27
27
  ## Install
28
28
 
29
- Python users can install the canonical package from PyPI:
29
+ ### One-line install (no Python or npm required)
30
+
31
+ macOS / Linux:
32
+
33
+ ```bash
34
+ curl -fsSL https://raw.githubusercontent.com/chikhamx/voidx/main/scripts/install.sh | bash
35
+ ```
36
+
37
+ Windows (PowerShell):
38
+
39
+ ```powershell
40
+ irm https://raw.githubusercontent.com/chikhamx/voidx/main/scripts/install.ps1 | iex
41
+ ```
42
+
43
+ The installer downloads a standalone Python runtime and sets up voidx in an
44
+ isolated environment — nothing else is needed on your machine.
45
+
46
+ ### pip
30
47
 
31
48
  ```bash
32
49
  pip install voidx
33
50
  voidx
34
51
  ```
35
52
 
36
- Node users can install the npm launcher. The launcher requires Python 3.11+
37
- on the machine and installs the matching Python package into an isolated
38
- user-local virtual environment on first run:
53
+ ### npm
39
54
 
40
55
  ```bash
41
56
  npm install -g @chikhamx/voidx
42
57
  voidx
43
58
  ```
44
59
 
60
+ ### China / slow network
61
+
62
+ Set mirror environment variables before running any install method:
63
+
64
+ ```bash
65
+ export VOIDX_PYTHON_MIRROR=https://npmmirror.com/mirrors/python-standalone
66
+ export VOIDX_PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
67
+ ```
68
+
45
69
  ## Useful Commands
46
70
 
47
71
  ```bash
voidx-2.0.0/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # voidx
2
+
3
+ voidx is a terminal AI coding agent built in Python.
4
+
5
+ ## Install
6
+
7
+ ### One-line install (no Python or npm required)
8
+
9
+ macOS / Linux:
10
+
11
+ ```bash
12
+ curl -fsSL https://raw.githubusercontent.com/chikhamx/voidx/main/scripts/install.sh | bash
13
+ ```
14
+
15
+ Windows (PowerShell):
16
+
17
+ ```powershell
18
+ irm https://raw.githubusercontent.com/chikhamx/voidx/main/scripts/install.ps1 | iex
19
+ ```
20
+
21
+ The installer downloads a standalone Python runtime and sets up voidx in an
22
+ isolated environment — nothing else is needed on your machine.
23
+
24
+ ### pip
25
+
26
+ ```bash
27
+ pip install voidx
28
+ voidx
29
+ ```
30
+
31
+ ### npm
32
+
33
+ ```bash
34
+ npm install -g @chikhamx/voidx
35
+ voidx
36
+ ```
37
+
38
+ ### China / slow network
39
+
40
+ Set mirror environment variables before running any install method:
41
+
42
+ ```bash
43
+ export VOIDX_PYTHON_MIRROR=https://npmmirror.com/mirrors/python-standalone
44
+ export VOIDX_PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
45
+ ```
46
+
47
+ ## Useful Commands
48
+
49
+ ```bash
50
+ voidx version
51
+ voidx sessions
52
+ voidx -w /path/to/project
53
+ ```
54
+
55
+ ## Development
56
+
57
+ ```bash
58
+ .venv/bin/python -m pytest
59
+ .venv/bin/python scripts/package.py --format all --clean
60
+ npm --prefix npm run check
61
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voidx"
3
- version = "1.1.0"
3
+ version = "2.0.0"
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"
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "rich>=13.9.0",
16
16
  "tiktoken>=0.8.0",
17
17
  "httpx>=0.28.0",
18
- "websockets>=16.0",
18
+ "websockets>=14",
19
19
  ]
20
20
 
21
21
  [project.scripts]
@@ -43,7 +43,10 @@ build-backend = "setuptools.build_meta"
43
43
  where = ["src"]
44
44
 
45
45
  [tool.setuptools.package-data]
46
- "voidx.skills" = ["bundled/superpowers/*/SKILL.md"]
46
+ "voidx.skills" = [
47
+ "bundled/superpowers/*/SKILL.md",
48
+ "bundled/superpowers/*/templates/*.md",
49
+ ]
47
50
 
48
51
  [tool.pytest.ini_options]
49
52
  asyncio_mode = "auto"
@@ -1,3 +1,3 @@
1
1
  """VoidX - A coding agent that quantifies everything."""
2
2
 
3
- __version__ = "1.1.0"
3
+ __version__ = "2.0.0"
@@ -83,8 +83,8 @@ surgical edits directly when that is the shortest safe path.
83
83
  the user explicitly approves.
84
84
  - Fix/implement/modify → edit directly for small scoped changes, or delegate
85
85
  broad/isolated work to implement.
86
- - Ambiguous → continue with read-only investigation when useful. Ask one
87
- clarifying question before edits, unsafe bash, or implement delegation.
86
+ - Ambiguous → continue with read-only investigation when useful. Use clarify
87
+ for one structured question before edits, unsafe bash, or implement delegation.
88
88
 
89
89
  Words like "看看", "分析", "梳理", "有什么建议", "如何设计", "优化方案",
90
90
  "look at", "analyze", "suggest", and "proposal" do NOT imply permission
@@ -92,11 +92,15 @@ surgical edits directly when that is the shortest safe path.
92
92
  says to modify code.
93
93
 
94
94
  1. **Chat / explain** — just answer. No tools unless you need to look something up.
95
+ If Current Task State says intent is chat or ambiguous, but the user request
96
+ appears to require workspace action, call on_intent before other workspace tools.
95
97
 
96
98
  2. **Simple search** — grab read/glob/grep and find it yourself. Only send explore
97
99
  for broad searches across many files.
98
100
 
99
- 3. **Design / plan** — hand off to plan for architecture questions.
101
+ 3. **Design / plan** — hand off to plan for architecture questions. For
102
+ non-trivial implementation plans, call plan_checkpoint before changing files,
103
+ running write-capable commands, or delegating implement.
100
104
 
101
105
  4. **Code changes**
102
106
  - Small, local, or mechanical changes → read first, then call write/edit
@@ -109,9 +113,9 @@ surgical edits directly when that is the shortest safe path.
109
113
  reporting completion.
110
114
  - If review says FAIL or NEEDS_CHANGE → fix, review again.
111
115
 
112
- 5. **Unclear intent** — ask. One specific clarifying question is better than five
113
- assumptions. "When you say 'broken', do you mean it crashes, returns wrong data,
114
- or something else?"
116
+ 5. **Unclear intent** — ask through clarify. One specific clarifying question is
117
+ better than five assumptions. "When you say 'broken', do you mean it crashes,
118
+ returns wrong data, or something else?"
115
119
 
116
120
  ## Rules
117
121
 
@@ -119,6 +123,8 @@ surgical edits directly when that is the shortest safe path.
119
123
  - In plan mode, do not call write/edit/lsp_format, unsafe bash, or implement.
120
124
  - Ambiguous implementation intent is not enough for write/edit/lsp_format,
121
125
  unsafe bash, or implement delegation.
126
+ - Child agents do not interact with the user. If a child plan result needs user
127
+ approval or clarification, call plan_checkpoint or clarify yourself.
122
128
  - Don't tell the user "done" until changes are verified.
123
129
  - Child agents have isolated context — give them complete, self-contained briefs.
124
130
 
@@ -301,6 +307,7 @@ BUILTIN_AGENTS: dict[str, AgentDef] = {
301
307
  "delegates broad work to specialists, reviews results.",
302
308
  when_to_use="Default agent for all user interactions. Always use first.",
303
309
  tools=[
310
+ "on_intent", "clarify", "plan_checkpoint",
304
311
  "read", "glob", "grep", "bash", "agent", "task_status", "todo",
305
312
  "webfetch", "websearch", "repo_map",
306
313
  "lsp_diagnostics", "lsp_symbols", "lsp_definition", "lsp_references",
@@ -25,7 +25,7 @@ class GraphCompactionMixin:
25
25
  async def _maybe_compact(
26
26
  self: GraphCompactionHost,
27
27
  messages: list,
28
- session_msgs: list,
28
+ session_msgs: list | None = None,
29
29
  *,
30
30
  force: bool = False,
31
31
  ask: bool = True,
@@ -218,14 +218,23 @@ class GraphCompactionMixin:
218
218
 
219
219
  await delete_messages_through(self._session.id, last_message_id)
220
220
 
221
+ # Sync in-memory cache: drop compacted rows
222
+ cache = getattr(self, "_session_msg_cache", None)
223
+ if cache is not None:
224
+ self._session_msg_cache = [r for r in cache if r.id is not None and r.id > last_message_id]
225
+
221
226
  async def _compact_session_history(self: GraphCompactionHost, *, force: bool = True) -> bool:
222
227
  if getattr(self, "_session", None) is None:
223
228
  ui.print("[dim]No active session to compact.[/dim]")
224
229
  return False
225
230
 
226
- from voidx.memory.session import load_messages
231
+ cache = getattr(self, "_session_msg_cache", None)
232
+ if cache is not None:
233
+ rows = list(cache)
234
+ else:
235
+ from voidx.memory.session import load_messages
236
+ rows = await load_messages(self._session.id)
227
237
 
228
- rows = await load_messages(self._session.id)
229
238
  messages = messages_from_rows(rows)
230
239
 
231
240
  head, _tail_id = await self._maybe_compact(messages, rows, force=force, ask=False)
@@ -53,6 +53,7 @@ class GraphComponentHost(Protocol):
53
53
  _sub_buffers: dict[str, list[BaseMessage]]
54
54
  _pending_summary: str | None
55
55
  _compaction_summary: str
56
+ _session_msg_cache: list[Any] | None
56
57
  _app: PureTui | None
57
58
  _usage_stats: UsageStats
58
59
  _compaction: CompactionService
@@ -64,7 +65,6 @@ class GraphComponentHost(Protocol):
64
65
  _slash: Any
65
66
 
66
67
  _any_messages_sent: bool
67
- _current_implementation_allowed: bool
68
68
  _needs_failure_check: dict[str, dict]
69
69
 
70
70
  @property
@@ -85,7 +85,7 @@ class GraphComponentHost(Protocol):
85
85
  async def _maybe_compact(
86
86
  self,
87
87
  messages: list[BaseMessage],
88
- session_msgs: list[Any],
88
+ session_msgs: list[Any] | None = None,
89
89
  *,
90
90
  force: bool = False,
91
91
  ask: bool = True,
@@ -37,6 +37,7 @@ from voidx.agent.state import AgentState
37
37
  from voidx.agent.graph.streaming import stream_llm as _stream_llm
38
38
  from voidx.agent.graph.subagent import run_subagent as _run_subagent
39
39
  from voidx.agent.graph.tool_execution import GraphToolExecutionMixin
40
+ from voidx.agent.intent_refinement import refine_intent
40
41
  from voidx.agent.runtime_context import InteractionMode, RuntimeContextBuilder
41
42
  from voidx.agent.task_state import TaskRun, TaskState
42
43
  from voidx.agent.tool_filters import filter_unavailable_lsp_tools
@@ -51,11 +52,11 @@ from voidx.llm.usage import (
51
52
  extract_token_usage,
52
53
  )
53
54
  from voidx.memory.context_frames import save_context_frame_from_messages
54
- from voidx.memory.session import SessionInfo
55
- from voidx.agent.slash import SlashHandler
55
+ from voidx.memory.session import MessageRow, SessionInfo
56
56
  from voidx.permission.service import PermissionService
57
57
  from voidx.tools.registry import ToolRegistry
58
58
  from voidx.tools.agent import AgentTool
59
+ from voidx.tools.on_intent import OnIntentInput, OnIntentTool
59
60
  from voidx.tools.task_tracker import TaskTracker
60
61
  from voidx.ui.output.console import StreamingRenderer
61
62
  from voidx.ui.output.dock import dock
@@ -109,6 +110,8 @@ class VoidXGraph(
109
110
  # Build tool registry, wire tracker through registry
110
111
  self._tracker = TaskTracker()
111
112
  self.tools = ToolRegistry(settings=settings, tracker=self._tracker)
113
+ intent_tool = OnIntentTool(resolver=self._resolve_on_intent)
114
+ self.tools.register("on_intent", intent_tool, intent_tool.description, intent_tool.parameters_schema())
112
115
  agent_tool = AgentTool(runner=self._subagent_runner)
113
116
  self.tools.register("agent", agent_tool, agent_tool.description, agent_tool.parameters_schema())
114
117
 
@@ -137,6 +140,7 @@ class VoidXGraph(
137
140
  # One-turn summary injection vs. persisted summary restored across turns.
138
141
  self._pending_summary: str | None = None
139
142
  self._compaction_summary: str = ""
143
+ self._session_msg_cache: list[MessageRow] | None = None
140
144
  self._app: PureTui | None = None
141
145
  self._next_agent_id: int = 0
142
146
  self._task_state = TaskState()
@@ -153,6 +157,8 @@ class VoidXGraph(
153
157
  )
154
158
 
155
159
  self._build()
160
+ from voidx.agent.slash import SlashHandler
161
+
156
162
  self._slash = SlashHandler(self)
157
163
 
158
164
  # MCP (Model Context Protocol) servers — start on run()
@@ -187,6 +193,15 @@ class VoidXGraph(
187
193
  def interaction_mode(self) -> InteractionMode:
188
194
  return self._interaction_mode
189
195
 
196
+ def _resolve_on_intent(self, inp: OnIntentInput, ctx):
197
+ return refine_intent(
198
+ inp,
199
+ ctx,
200
+ config=self.config,
201
+ settings=self._settings,
202
+ registered_tool_ids=self.tools.ids(),
203
+ )
204
+
190
205
  async def _subagent_runner(self, agent_def: AgentDef, description: str, model_override: str | None) -> str:
191
206
  parent_messages = getattr(self, '_current_messages', None)
192
207
  sub_buffer: list[BaseMessage] = []
@@ -297,6 +312,8 @@ class VoidXGraph(
297
312
  agent=agent_name,
298
313
  task_intent=state.get("task_intent"),
299
314
  interaction_mode=interaction_mode,
315
+ scope=_pending_approval_scope(state.get("pending_approval")) or state.get("goal") or latest_user_text,
316
+ turn_count=state.get("goal_turn_count", 0),
300
317
  )
301
318
  mode_prompt = PLAN_MODE_APPEND if InteractionMode.parse(interaction_mode) == InteractionMode.PLAN else ""
302
319
  summary = self._pending_summary or self._compaction_summary
@@ -313,22 +330,25 @@ class VoidXGraph(
313
330
  interaction_mode=interaction_mode,
314
331
  instructions=instructions,
315
332
  skill_instructions=skill_context.instructions,
333
+ skill_runs=skill_context.runs,
316
334
  active_skill_summaries=skill_context.active,
317
335
  summary=summary,
318
336
  current_user_text=latest_user_text,
319
337
  task_intent=state.get("task_intent"),
320
- implementation_allowed=state.get("implementation_allowed"),
321
338
  intent_resolution_reason=state.get("intent_resolution_reason", ""),
322
- awaiting_implementation_approval=state.get("awaiting_implementation_approval", False),
323
- approved_scope=state.get("approved_scope", ""),
339
+ pending_approval=state.get("pending_approval"),
324
340
  goal=state.get("goal", ""),
325
341
  goal_phase=state.get("goal_phase", ""),
326
342
  goal_status=state.get("goal_status", ""),
327
343
  goal_turn_count=state.get("goal_turn_count", 0),
344
+ available_tool_ids=state.get("available_tool_ids", []),
345
+ intent_confidence=state.get("intent_confidence"),
346
+ intent_source=state.get("intent_source", ""),
347
+ intent_refined=state.get("intent_refined", False),
328
348
  ).build()
329
349
  context.apply_to_messages(state.get("messages", []))
330
350
 
331
- return base
351
+ return {**base, "skill_runs": skill_context.runs}
332
352
 
333
353
  async def _call_llm(self, state: AgentState) -> dict:
334
354
  step = state.get("step_count", 0)
@@ -354,6 +374,9 @@ class VoidXGraph(
354
374
  tool_defs = [t for t in all_tool_defs if t["function"]["name"] in agent_tool_ids]
355
375
  else:
356
376
  tool_defs = all_tool_defs
377
+ if "available_tool_ids" in state:
378
+ visible = set(state.get("available_tool_ids") or [])
379
+ tool_defs = [t for t in tool_defs if t["function"]["name"] in visible]
357
380
  tool_defs = filter_unavailable_lsp_tools(tool_defs, getattr(self, "_lsp_manager", None))
358
381
 
359
382
  has_tool_budget = step < max_s - 1
@@ -447,3 +470,11 @@ def _latest_user_text(messages: list[BaseMessage]) -> str:
447
470
  return "\n".join(parts)
448
471
  return str(content)
449
472
  return ""
473
+
474
+
475
+ def _pending_approval_scope(value: object | None) -> str:
476
+ if value is None:
477
+ return ""
478
+ if isinstance(value, dict):
479
+ return str(value.get("scope") or "").strip()
480
+ return str(getattr(value, "scope", "") or "").strip()
@@ -16,8 +16,9 @@ from voidx.agent.attachments import (
16
16
  )
17
17
  from voidx.agent.graph.runtime import ui
18
18
  from voidx.agent.message_rows import messages_from_rows
19
+ from voidx.agent.runtime_context import TaskIntent
19
20
  from voidx.agent.state import AgentState
20
- from voidx.agent.task_state import resolve_turn_intent
21
+ from voidx.agent.task_state import IntentResolution, PendingApproval, resolve_turn_intent
21
22
  from voidx.llm.provider import get_context_limit
22
23
  from voidx.memory.session import (
23
24
  MessageRow,
@@ -179,7 +180,7 @@ class GraphRunLoopMixin:
179
180
  goal_phase=lambda: getattr(getattr(getattr(self, "_task_run", None), "phase", None), "value", "clarify"),
180
181
  goal_status=lambda: getattr(getattr(getattr(self, "_task_run", None), "status", None), "value", "idle"),
181
182
  goal_turn_count=lambda: getattr(getattr(self, "_task_run", None), "turn_count", 0),
182
- goal_awaiting_approval=lambda: getattr(getattr(self, "_task_run", None), "awaiting_implementation_approval", False),
183
+ goal_awaiting_approval=lambda: bool(getattr(getattr(self, "_task_run", None), "pending_approval", None)),
183
184
  mcp_servers=lambda: [
184
185
  McpServerStatus(
185
186
  name=s.name,
@@ -228,6 +229,11 @@ class GraphRunLoopMixin:
228
229
  dock.append_message("\n".join(lsp_lines), markup=True)
229
230
 
230
231
  if hasattr(self, '_mcp_manager'):
232
+ servers = self._settings.list_mcp_servers() if self._settings else []
233
+ enabled = [s for s in servers if not s.disabled]
234
+ if enabled:
235
+ names = ", ".join(s.name for s in enabled)
236
+ dock.append_message(f"[dim]MCP connecting: {names}…[/dim]", markup=True)
231
237
  await self._mcp_manager.start_all()
232
238
 
233
239
  async def handle_user_input(user_input: str) -> bool:
@@ -310,7 +316,13 @@ class GraphRunLoopMixin:
310
316
  ))
311
317
  else:
312
318
  self._turn_node = dock.start_turn(payload.display_text)
313
- session_msgs = await load_messages(self._session.id) if self._session else []
319
+ # Load session messages use in-memory cache when available
320
+ if self._session_msg_cache is not None:
321
+ session_msgs = list(self._session_msg_cache)
322
+ else:
323
+ session_msgs = (await load_messages(self._session.id)) if self._session else []
324
+ if self._session:
325
+ self._session_msg_cache = list(session_msgs)
314
326
  truncation_notice: str | None = None
315
327
  # Safety: if session is huge, only load recent messages
316
328
  if len(session_msgs) > 500:
@@ -349,13 +361,16 @@ class GraphRunLoopMixin:
349
361
  getattr(self, "_task_state", None),
350
362
  )
351
363
  task_intent = intent_resolution.intent
352
- implementation_allowed = intent_resolution.implementation_allowed
353
- self._current_implementation_allowed = implementation_allowed
354
364
  goal_scope = (
355
365
  task_run.goal
356
366
  if interaction_mode == "goal" and task_run is not None and task_run.goal
357
367
  else payload.title_text
358
368
  )
369
+ pending_approval = _active_pending_approval(
370
+ getattr(self, "_task_state", None),
371
+ task_run,
372
+ interaction_mode,
373
+ )
359
374
 
360
375
  saved_user_content, user_content_format = serialize_message_content(payload.content)
361
376
  user_message_id = await save_message(MessageRow(
@@ -365,6 +380,15 @@ class GraphRunLoopMixin:
365
380
  content_format=user_content_format,
366
381
  created_at=_now(),
367
382
  ))
383
+ if self._session_msg_cache is not None:
384
+ self._session_msg_cache.append(MessageRow(
385
+ id=user_message_id,
386
+ session_id=self._session.id,
387
+ role="user",
388
+ content=saved_user_content,
389
+ content_format=user_content_format,
390
+ created_at=_now(),
391
+ ))
368
392
  self._any_messages_sent = True
369
393
 
370
394
  initial: AgentState = {
@@ -378,10 +402,8 @@ class GraphRunLoopMixin:
378
402
  "plan_mode": self._plan_mode,
379
403
  "interaction_mode": interaction_mode,
380
404
  "task_intent": task_intent.value,
381
- "implementation_allowed": implementation_allowed,
382
405
  "intent_resolution_reason": intent_resolution.reason,
383
- "awaiting_implementation_approval": intent_resolution.awaiting_implementation_approval,
384
- "approved_scope": intent_resolution.approved_scope,
406
+ "pending_approval": _dump_pending_approval(pending_approval),
385
407
  "goal": task_run.goal if task_run is not None else "",
386
408
  "goal_phase": task_run.phase.value if task_run is not None else "",
387
409
  "goal_status": task_run.status.value if task_run is not None else "",
@@ -402,43 +424,51 @@ class GraphRunLoopMixin:
402
424
  await ui_events.emit(StatusFinished(status_id="turn:analyzing"))
403
425
 
404
426
  final = await self.graph.ainvoke(initial, {"recursion_limit": self.config.agent.recursion_limit})
427
+ final_task_intent = TaskIntent(final.get("task_intent", task_intent.value))
428
+ final_intent_resolution_reason = final.get(
429
+ "intent_resolution_reason",
430
+ intent_resolution.reason,
431
+ )
432
+ final_pending_approval = _load_pending_approval(final.get("pending_approval"))
433
+ final_scope = final_pending_approval.scope if final_pending_approval else goal_scope
434
+ final_resolution = IntentResolution(
435
+ intent=final_task_intent,
436
+ reason=final_intent_resolution_reason,
437
+ confirmed_approval=intent_resolution.confirmed_approval,
438
+ )
405
439
  if self.model is not None and hasattr(self, "_task_state"):
406
440
  self._task_state.update_after_turn(
407
- intent_resolution,
441
+ final_resolution,
408
442
  payload.title_text,
409
- scope_text=goal_scope,
443
+ scope_text=final_scope,
410
444
  )
411
445
  if self.model is not None and interaction_mode == "goal" and task_run is not None:
412
446
  task_run.update_after_turn(
413
- intent_resolution,
447
+ final_resolution,
414
448
  payload.title_text,
415
- scope_text=goal_scope,
449
+ scope_text=final_scope,
416
450
  )
451
+ if self.model is not None and task_run is not None:
452
+ task_run.merge_skill_runs(final.get("skill_runs", []))
417
453
  await save_message_runtime_snapshot(MessageRuntimeSnapshot(
418
454
  message_id=user_message_id,
419
455
  session_id=self._session.id,
420
456
  interaction_mode=interaction_mode,
421
- task_intent=task_intent,
422
- implementation_allowed=implementation_allowed,
423
- intent_resolution_reason=intent_resolution.reason,
457
+ task_intent=final_task_intent,
458
+ intent_resolution_reason=final_intent_resolution_reason,
424
459
  goal=task_run.goal if task_run is not None else "",
425
460
  goal_phase=task_run.phase.value if task_run is not None else "",
426
461
  goal_status=task_run.status.value if task_run is not None else "",
427
462
  goal_turn_count=task_run.turn_count if task_run is not None else 0,
428
- awaiting_implementation_approval=(
429
- task_run.awaiting_implementation_approval
430
- if interaction_mode == "goal" and task_run is not None
431
- else getattr(
432
- getattr(self, "_task_state", None),
433
- "awaiting_implementation_approval",
434
- False,
435
- )
436
- ),
437
- approved_scope=(
438
- task_run.approved_scope
439
- if interaction_mode == "goal" and task_run is not None
440
- else getattr(getattr(self, "_task_state", None), "approved_scope", "")
463
+ pending_approval=_active_pending_approval(
464
+ getattr(self, "_task_state", None),
465
+ task_run,
466
+ interaction_mode,
441
467
  ),
468
+ intent_confidence=final.get("intent_confidence"),
469
+ intent_source=final.get("intent_source", ""),
470
+ intent_refined=bool(final.get("intent_refined", False)),
471
+ available_tool_ids=list(final.get("available_tool_ids", []) or []),
442
472
  ))
443
473
  await self._persist_runtime_state()
444
474
 
@@ -469,7 +499,7 @@ class GraphRunLoopMixin:
469
499
  else:
470
500
  saved = str(raw_content)
471
501
  fmt = "text"
472
- await save_message(MessageRow(
502
+ row_id = await save_message(MessageRow(
473
503
  session_id=self._session.id,
474
504
  role="assistant",
475
505
  content=saved,
@@ -477,14 +507,33 @@ class GraphRunLoopMixin:
477
507
  tool_calls=msg.tool_calls if msg.tool_calls else None,
478
508
  created_at=_now(),
479
509
  ))
510
+ if self._session_msg_cache is not None:
511
+ self._session_msg_cache.append(MessageRow(
512
+ id=row_id,
513
+ session_id=self._session.id,
514
+ role="assistant",
515
+ content=saved,
516
+ content_format=fmt,
517
+ tool_calls=msg.tool_calls if msg.tool_calls else None,
518
+ created_at=_now(),
519
+ ))
480
520
  elif isinstance(msg, ToolMessage):
481
- await save_message(MessageRow(
521
+ row_id = await save_message(MessageRow(
482
522
  session_id=self._session.id,
483
523
  role="tool",
484
524
  content=str(msg.content),
485
525
  tool_call_id=getattr(msg, "tool_call_id", None),
486
526
  created_at=_now(),
487
527
  ))
528
+ if self._session_msg_cache is not None:
529
+ self._session_msg_cache.append(MessageRow(
530
+ id=row_id,
531
+ session_id=self._session.id,
532
+ role="tool",
533
+ content=str(msg.content),
534
+ tool_call_id=getattr(msg, "tool_call_id", None),
535
+ created_at=_now(),
536
+ ))
488
537
  await touch_session(self._session.id)
489
538
 
490
539
  # Auto-title on first message
@@ -517,6 +566,11 @@ class GraphRunLoopMixin:
517
566
  except (KeyboardInterrupt, asyncio.CancelledError):
518
567
  if self._session is not None and user_message_id is not None:
519
568
  await delete_messages_from(self._session.id, user_message_id)
569
+ if self._session_msg_cache is not None:
570
+ self._session_msg_cache = [
571
+ r for r in self._session_msg_cache
572
+ if r.id is None or r.id < user_message_id
573
+ ]
520
574
  raise
521
575
  finally:
522
576
  session_tracker.finish_turn()
@@ -528,7 +582,6 @@ class GraphRunLoopMixin:
528
582
  await ui_events.drain()
529
583
  else:
530
584
  dock.set_input("", [])
531
- self._current_implementation_allowed = True
532
585
 
533
586
  async def _dispatch_slash(self: GraphRunLoopHost, inp: str) -> bool:
534
587
  """Try to dispatch a slash command. Returns True if handled."""
@@ -594,3 +647,27 @@ class GraphRunLoopMixin:
594
647
  return False
595
648
  active_dock.restore_tree(transcript_rows_to_tree(rows), append=append)
596
649
  return True
650
+
651
+
652
+ def _active_pending_approval(task_state, task_run, interaction_mode: str) -> PendingApproval | None:
653
+ if interaction_mode == "goal" and task_run is not None:
654
+ return getattr(task_run, "pending_approval", None)
655
+ if task_state is not None:
656
+ return getattr(task_state, "pending_approval", None)
657
+ return None
658
+
659
+
660
+ def _dump_pending_approval(value: PendingApproval | dict | None) -> dict | None:
661
+ if value is None:
662
+ return None
663
+ if isinstance(value, dict):
664
+ return value
665
+ return value.model_dump(mode="json")
666
+
667
+
668
+ def _load_pending_approval(value: PendingApproval | dict | None) -> PendingApproval | None:
669
+ if value is None:
670
+ return None
671
+ if isinstance(value, PendingApproval):
672
+ return value
673
+ return PendingApproval.model_validate(value)