voidx 2.2.1__tar.gz → 3.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 (345) hide show
  1. {voidx-2.2.1 → voidx-3.0.0}/PKG-INFO +1 -1
  2. {voidx-2.2.1 → voidx-3.0.0}/pyproject.toml +2 -1
  3. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/__init__.py +1 -1
  4. voidx-3.0.0/src/voidx/agent/agents.py +266 -0
  5. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/attachments.py +25 -4
  6. voidx-3.0.0/src/voidx/agent/goal_resolver.py +228 -0
  7. voidx-3.0.0/src/voidx/agent/graph/compaction.py +81 -0
  8. voidx-3.0.0/src/voidx/agent/graph/compaction_coordinator.py +502 -0
  9. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/contracts.py +145 -72
  10. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/convergence.py +2 -1
  11. voidx-3.0.0/src/voidx/agent/graph/core.py +954 -0
  12. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/permissions.py +75 -11
  13. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/run_loop.py +110 -41
  14. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/runtime.py +0 -2
  15. voidx-3.0.0/src/voidx/agent/graph/runtime_guards.py +465 -0
  16. voidx-3.0.0/src/voidx/agent/graph/session_mixin.py +38 -0
  17. voidx-3.0.0/src/voidx/agent/graph/session_runtime.py +282 -0
  18. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/streaming.py +90 -21
  19. voidx-3.0.0/src/voidx/agent/graph/subagent.py +448 -0
  20. voidx-3.0.0/src/voidx/agent/graph/title_mixin.py +100 -0
  21. voidx-3.0.0/src/voidx/agent/graph/todo_events.py +17 -0
  22. voidx-3.0.0/src/voidx/agent/graph/tool_execution.py +40 -0
  23. voidx-3.0.0/src/voidx/agent/graph/tool_executor.py +1226 -0
  24. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/topology.py +11 -18
  25. voidx-3.0.0/src/voidx/agent/graph/transcript_mixin.py +18 -0
  26. voidx-3.0.0/src/voidx/agent/graph/turn_mixin.py +36 -0
  27. voidx-3.0.0/src/voidx/agent/graph/turn_runner.py +425 -0
  28. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/wiring.py +2 -9
  29. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/message_rows.py +1 -1
  30. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/runtime_context.py +276 -126
  31. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/code_ide.py +3 -4
  32. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/guide.py +3 -4
  33. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/handler.py +54 -176
  34. voidx-3.0.0/src/voidx/agent/slash/host.py +359 -0
  35. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/init.py +5 -5
  36. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/lsp.py +6 -6
  37. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/mcp.py +22 -21
  38. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/model.py +86 -83
  39. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/profile.py +9 -8
  40. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/runtime.py +0 -2
  41. voidx-3.0.0/src/voidx/agent/slash/session.py +253 -0
  42. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/skills.py +36 -13
  43. voidx-3.0.0/src/voidx/agent/slash/upgrade.py +98 -0
  44. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/state.py +4 -16
  45. voidx-3.0.0/src/voidx/agent/task_state.py +33 -0
  46. voidx-3.0.0/src/voidx/agent/todo_state.py +259 -0
  47. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/tool_filters.py +2 -2
  48. voidx-3.0.0/src/voidx/agent/tool_result_storage.py +73 -0
  49. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/__init__.py +0 -2
  50. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/enums.py +1 -1
  51. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/models.py +1 -39
  52. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings.py +154 -37
  53. voidx-3.0.0/src/voidx/config/settings_agent.py +21 -0
  54. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_api_keys.py +3 -11
  55. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_code_ide.py +2 -4
  56. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_custom.py +3 -21
  57. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_mcp.py +23 -19
  58. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_permissions.py +7 -7
  59. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_skills.py +29 -2
  60. voidx-3.0.0/src/voidx/config/settings_update.py +68 -0
  61. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_web.py +15 -10
  62. voidx-3.0.0/src/voidx/data/intent_classifier.json +1 -0
  63. voidx-3.0.0/src/voidx/data/templates/api-doc.md +64 -0
  64. voidx-3.0.0/src/voidx/data/templates/prd.md +117 -0
  65. voidx-3.0.0/src/voidx/data/templates/readme.md +55 -0
  66. voidx-3.0.0/src/voidx/data/templates/rfc.md +38 -0
  67. voidx-3.0.0/src/voidx/data/templates/tech-design.md +68 -0
  68. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/compaction.py +31 -0
  69. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/instruction.py +59 -63
  70. voidx-3.0.0/src/voidx/llm/provider.py +545 -0
  71. voidx-3.0.0/src/voidx/llm/service.py +32 -0
  72. voidx-3.0.0/src/voidx/logging/__init__.py +1 -0
  73. voidx-3.0.0/src/voidx/logging/request_log.py +123 -0
  74. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/__init__.py +9 -1
  75. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/client.py +9 -2
  76. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/manager.py +70 -22
  77. voidx-3.0.0/src/voidx/memory/cleanup.py +122 -0
  78. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/memory/context_frames.py +80 -20
  79. voidx-3.0.0/src/voidx/memory/jsonl_store.py +186 -0
  80. voidx-3.0.0/src/voidx/memory/runtime_state.py +366 -0
  81. voidx-3.0.0/src/voidx/memory/service.py +99 -0
  82. voidx-3.0.0/src/voidx/memory/session.py +403 -0
  83. voidx-3.0.0/src/voidx/memory/store.py +242 -0
  84. voidx-3.0.0/src/voidx/memory/subagents.py +21 -0
  85. voidx-3.0.0/src/voidx/memory/transcript.py +495 -0
  86. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/evaluate.py +1 -1
  87. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/rules.py +35 -21
  88. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/service.py +13 -2
  89. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/runtime/__init__.py +21 -11
  90. voidx-3.0.0/src/voidx/runtime/attachments.py +7 -0
  91. voidx-3.0.0/src/voidx/runtime/intent.py +163 -0
  92. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/runtime/intent_classifier.py +8 -11
  93. voidx-3.0.0/src/voidx/runtime/reference_tokens.py +9 -0
  94. voidx-3.0.0/src/voidx/runtime/task_state.py +338 -0
  95. voidx-3.0.0/src/voidx/runtime/todo.py +9 -0
  96. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/runtime/ui.py +3 -0
  97. voidx-3.0.0/src/voidx/runtime/ui_port.py +178 -0
  98. voidx-3.0.0/src/voidx/selfupdate.py +207 -0
  99. voidx-3.0.0/src/voidx/skills/__init__.py +23 -0
  100. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/skills/context.py +4 -5
  101. voidx-3.0.0/src/voidx/skills/policy.py +46 -0
  102. voidx-3.0.0/src/voidx/skills/references.py +73 -0
  103. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/skills/registry.py +2 -1
  104. voidx-3.0.0/src/voidx/skills/runtime.py +25 -0
  105. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/skills/schema.py +2 -0
  106. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/skills/service.py +32 -115
  107. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/__init__.py +8 -1
  108. voidx-3.0.0/src/voidx/tools/advance_workflow.py +253 -0
  109. voidx-3.0.0/src/voidx/tools/agent.py +187 -0
  110. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/base.py +10 -6
  111. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/bash.py +4 -4
  112. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/clarify.py +24 -15
  113. voidx-3.0.0/src/voidx/tools/compact_context.py +53 -0
  114. voidx-3.0.0/src/voidx/tools/file_ops.py +208 -0
  115. voidx-3.0.0/src/voidx/tools/file_state.py +134 -0
  116. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/git.py +1 -0
  117. voidx-3.0.0/src/voidx/tools/load_doc_template.py +63 -0
  118. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/load_skills.py +11 -9
  119. voidx-3.0.0/src/voidx/tools/lsp.py +131 -0
  120. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/plan_checkpoint.py +8 -15
  121. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/registry.py +8 -12
  122. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/repomap.py +1 -0
  123. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/search.py +2 -0
  124. voidx-3.0.0/src/voidx/tools/service.py +32 -0
  125. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/task_status.py +3 -1
  126. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/task_tracker.py +3 -3
  127. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/todo.py +2 -5
  128. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/web_mcp.py +1 -0
  129. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/webfetch.py +1 -0
  130. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/websearch.py +1 -0
  131. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/commands.py +11 -0
  132. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/agent_display.py +2 -2
  133. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/capture.py +1 -1
  134. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/console/app.py +40 -8
  135. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/console/formatting.py +1 -1
  136. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/console/streaming.py +6 -3
  137. voidx-3.0.0/src/voidx/ui/output/display_policy.py +133 -0
  138. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/app.py +12 -6
  139. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/formatting.py +38 -17
  140. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/nodes.py +30 -16
  141. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/stream.py +15 -58
  142. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/todo.py +1 -5
  143. voidx-3.0.0/src/voidx/ui/output/events/__init__.py +102 -0
  144. voidx-3.0.0/src/voidx/ui/output/events/bus.py +119 -0
  145. voidx-2.2.1/src/voidx/ui/output/events/__init__.py → voidx-3.0.0/src/voidx/ui/output/events/consumers.py +42 -131
  146. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/events/schema.py +11 -1
  147. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/tree.py +186 -18
  148. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/types.py +2 -3
  149. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/session.py +2 -8
  150. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tools/__init__.py +5 -0
  151. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tools/clipboard_image.py +1 -1
  152. voidx-3.0.0/src/voidx/ui/tools/skill_picker.py +83 -0
  153. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/transcript.py +1 -1
  154. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/app.py +26 -2
  155. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/helpers.py +1 -4
  156. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/input.py +7 -0
  157. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/overlays.py +43 -1
  158. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/panels.py +125 -3
  159. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/parser.py +4 -0
  160. voidx-3.0.0/src/voidx/ui/tui/render_activity.py +77 -0
  161. voidx-3.0.0/src/voidx/ui/tui/render_frame.py +587 -0
  162. voidx-3.0.0/src/voidx/ui/tui/render_input.py +154 -0
  163. voidx-3.0.0/src/voidx/ui/tui/render_status.py +269 -0
  164. voidx-3.0.0/src/voidx/ui/tui/render_todo.py +81 -0
  165. voidx-3.0.0/src/voidx/ui/tui/renderer.py +24 -0
  166. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/state.py +36 -0
  167. voidx-3.0.0/src/voidx/workflow/__init__.py +74 -0
  168. voidx-3.0.0/src/voidx/workflow/auto_advance.py +162 -0
  169. voidx-3.0.0/src/voidx/workflow/context.py +64 -0
  170. voidx-3.0.0/src/voidx/workflow/dag.py +40 -0
  171. voidx-3.0.0/src/voidx/workflow/nodes.py +417 -0
  172. voidx-3.0.0/src/voidx/workflow/policy.py +205 -0
  173. voidx-3.0.0/src/voidx/workflow/reconcile.py +251 -0
  174. voidx-3.0.0/src/voidx/workflow/render.py +114 -0
  175. voidx-3.0.0/src/voidx/workflow/route.py +49 -0
  176. voidx-3.0.0/src/voidx/workflow/runtime.py +285 -0
  177. voidx-3.0.0/src/voidx/workflow/schema.py +177 -0
  178. voidx-3.0.0/src/voidx/workflow/service.py +391 -0
  179. voidx-3.0.0/src/voidx/workflow/types.py +100 -0
  180. {voidx-2.2.1 → voidx-3.0.0}/src/voidx.egg-info/PKG-INFO +1 -1
  181. {voidx-2.2.1 → voidx-3.0.0}/src/voidx.egg-info/SOURCES.txt +71 -12
  182. voidx-3.0.0/tests/test_auto_advance.py +410 -0
  183. {voidx-2.2.1 → voidx-3.0.0}/tests/test_compaction.py +274 -36
  184. {voidx-2.2.1 → voidx-3.0.0}/tests/test_config.py +289 -31
  185. voidx-3.0.0/tests/test_dock_formatting.py +48 -0
  186. voidx-3.0.0/tests/test_goal_resolution_refactor.py +128 -0
  187. voidx-3.0.0/tests/test_install_sh.py +162 -0
  188. {voidx-2.2.1 → voidx-3.0.0}/tests/test_instruction_cache.py +1 -1
  189. {voidx-2.2.1 → voidx-3.0.0}/tests/test_intent_classifier_phase_a.py +13 -14
  190. voidx-3.0.0/tests/test_llm_provider.py +632 -0
  191. {voidx-2.2.1 → voidx-3.0.0}/tests/test_lsp.py +180 -7
  192. voidx-3.0.0/tests/test_module_boundaries.py +242 -0
  193. {voidx-2.2.1 → voidx-3.0.0}/tests/test_npm_package.py +7 -0
  194. voidx-3.0.0/tests/test_runtime_intent_classifier.py +131 -0
  195. {voidx-2.2.1 → voidx-3.0.0}/tests/test_scrollback_flush.py +2 -2
  196. voidx-3.0.0/tests/test_selfupdate.py +103 -0
  197. voidx-3.0.0/tests/test_skills.py +1236 -0
  198. voidx-3.0.0/tests/test_streaming_sanitize.py +103 -0
  199. {voidx-2.2.1 → voidx-3.0.0}/tests/test_tree_smoke.py +30 -0
  200. voidx-3.0.0/tests/test_tui_frame_rendering.py +555 -0
  201. voidx-3.0.0/tests/test_tui_input_handling.py +865 -0
  202. voidx-3.0.0/tests/test_tui_output_tree.py +750 -0
  203. voidx-3.0.0/tests/test_tui_paste_handling.py +280 -0
  204. voidx-3.0.0/tests/test_tui_status_activity.py +778 -0
  205. voidx-3.0.0/tests/test_tui_terminal_panels.py +776 -0
  206. {voidx-2.2.1 → voidx-3.0.0}/tests/test_ui_events.py +64 -2
  207. {voidx-2.2.1 → voidx-3.0.0}/tests/test_ui_session_changes.py +0 -49
  208. voidx-3.0.0/tests/test_workflow_reconcile.py +309 -0
  209. voidx-2.2.1/src/voidx/agent/agents.py +0 -477
  210. voidx-2.2.1/src/voidx/agent/graph/compaction.py +0 -323
  211. voidx-2.2.1/src/voidx/agent/graph/core.py +0 -676
  212. voidx-2.2.1/src/voidx/agent/graph/session_mixin.py +0 -60
  213. voidx-2.2.1/src/voidx/agent/graph/subagent.py +0 -322
  214. voidx-2.2.1/src/voidx/agent/graph/title_mixin.py +0 -211
  215. voidx-2.2.1/src/voidx/agent/graph/todo_events.py +0 -21
  216. voidx-2.2.1/src/voidx/agent/graph/tool_execution.py +0 -475
  217. voidx-2.2.1/src/voidx/agent/graph/transcript_mixin.py +0 -34
  218. voidx-2.2.1/src/voidx/agent/graph/turn_mixin.py +0 -430
  219. voidx-2.2.1/src/voidx/agent/intent_refinement.py +0 -243
  220. voidx-2.2.1/src/voidx/agent/slash/session.py +0 -239
  221. voidx-2.2.1/src/voidx/agent/task_state.py +0 -23
  222. voidx-2.2.1/src/voidx/config/settings_agent.py +0 -34
  223. voidx-2.2.1/src/voidx/data/intent_classifier.json +0 -1
  224. voidx-2.2.1/src/voidx/llm/provider.py +0 -320
  225. voidx-2.2.1/src/voidx/memory/runtime_state.py +0 -366
  226. voidx-2.2.1/src/voidx/memory/session.py +0 -298
  227. voidx-2.2.1/src/voidx/memory/store.py +0 -271
  228. voidx-2.2.1/src/voidx/memory/transcript.py +0 -132
  229. voidx-2.2.1/src/voidx/runtime/intent.py +0 -96
  230. voidx-2.2.1/src/voidx/runtime/task_state.py +0 -364
  231. voidx-2.2.1/src/voidx/skills/__init__.py +0 -34
  232. voidx-2.2.1/src/voidx/skills/bundled/brainstorming/SKILL.md +0 -51
  233. voidx-2.2.1/src/voidx/skills/bundled/receiving-code-review/SKILL.md +0 -60
  234. voidx-2.2.1/src/voidx/skills/bundled/requesting-code-review/SKILL.md +0 -59
  235. voidx-2.2.1/src/voidx/skills/bundled/systematic-debugging/SKILL.md +0 -64
  236. voidx-2.2.1/src/voidx/skills/bundled/test-driven-development/SKILL.md +0 -61
  237. voidx-2.2.1/src/voidx/skills/bundled/verification-before-completion/SKILL.md +0 -69
  238. voidx-2.2.1/src/voidx/skills/bundled/writing-design-docs/SKILL.md +0 -87
  239. voidx-2.2.1/src/voidx/skills/bundled/writing-plans/SKILL.md +0 -54
  240. voidx-2.2.1/src/voidx/skills/policy.py +0 -121
  241. voidx-2.2.1/src/voidx/skills/runtime.py +0 -252
  242. voidx-2.2.1/src/voidx/tools/agent.py +0 -110
  243. voidx-2.2.1/src/voidx/tools/file_ops.py +0 -618
  244. voidx-2.2.1/src/voidx/tools/lsp.py +0 -155
  245. voidx-2.2.1/src/voidx/tools/on_intent.py +0 -116
  246. voidx-2.2.1/src/voidx/ui/tui/renderer.py +0 -905
  247. voidx-2.2.1/tests/test_llm_provider.py +0 -225
  248. voidx-2.2.1/tests/test_pure_tui.py +0 -3204
  249. voidx-2.2.1/tests/test_runtime_intent_classifier.py +0 -191
  250. voidx-2.2.1/tests/test_skills.py +0 -890
  251. {voidx-2.2.1 → voidx-3.0.0}/README.md +0 -0
  252. {voidx-2.2.1 → voidx-3.0.0}/setup.cfg +0 -0
  253. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/__init__.py +0 -0
  254. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/graph/__init__.py +0 -0
  255. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/slash/__init__.py +0 -0
  256. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/agent/tool_messages.py +0 -0
  257. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/cli.py +0 -0
  258. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/permissions.py +0 -0
  259. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/config/settings_utils.py +0 -0
  260. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/data/__init__.py +0 -0
  261. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/diffing.py +0 -0
  262. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/__init__.py +0 -0
  263. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/catalog.py +0 -0
  264. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/context.py +0 -0
  265. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/message_markers.py +0 -0
  266. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/llm/usage.py +0 -0
  267. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/config.py +0 -0
  268. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/detector.py +0 -0
  269. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/detector_data.py +0 -0
  270. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/errors.py +0 -0
  271. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/schema.py +0 -0
  272. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/lsp/service.py +0 -0
  273. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/main.py +0 -0
  274. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/__init__.py +0 -0
  275. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/client/__init__.py +0 -0
  276. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/client/base.py +0 -0
  277. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/client/errors.py +0 -0
  278. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/client/http_transport.py +0 -0
  279. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/client/sse_transport.py +0 -0
  280. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/client/stdio_transport.py +0 -0
  281. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/manager.py +0 -0
  282. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/schema.py +0 -0
  283. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp/tool.py +0 -0
  284. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp_servers/__init__.py +0 -0
  285. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/mcp_servers/web.py +0 -0
  286. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/memory/__init__.py +0 -0
  287. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/memory/model_profiles.py +0 -0
  288. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/__init__.py +0 -0
  289. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/context.py +0 -0
  290. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/engine.py +0 -0
  291. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/sandbox.py +0 -0
  292. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/schema.py +0 -0
  293. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/permission/wildcard.py +0 -0
  294. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/tools/web_content.py +0 -0
  295. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/__init__.py +0 -0
  296. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/frontend.py +0 -0
  297. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/gateway/__init__.py +0 -0
  298. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/gateway/bootstrap.py +0 -0
  299. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/gateway/server.py +0 -0
  300. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/gateway/session.py +0 -0
  301. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/__init__.py +0 -0
  302. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/browse.py +0 -0
  303. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/console/__init__.py +0 -0
  304. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/diff.py +0 -0
  305. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/__init__.py +0 -0
  306. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/agent_placeholder.py +0 -0
  307. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/nodes_permission.py +0 -0
  308. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/nodes_startup.py +0 -0
  309. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/nodes_status.py +0 -0
  310. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/state.py +0 -0
  311. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/output/dock/status.py +0 -0
  312. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/protocol/__init__.py +0 -0
  313. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/protocol/commands.py +0 -0
  314. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/protocol/envelope.py +0 -0
  315. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/protocol/requests.py +0 -0
  316. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/protocol/schema.py +0 -0
  317. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/protocol/transcript.py +0 -0
  318. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tools/attachment_tokens.py +0 -0
  319. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tools/clipboard_text.py +0 -0
  320. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tools/code_ide.py +0 -0
  321. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tools/file_picker.py +0 -0
  322. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/__init__.py +0 -0
  323. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/activity.py +0 -0
  324. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/choice_mixin.py +0 -0
  325. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/clipboard_mixin.py +0 -0
  326. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/terminal_mixin.py +0 -0
  327. {voidx-2.2.1 → voidx-3.0.0}/src/voidx/ui/tui/text_prompt_mixin.py +0 -0
  328. {voidx-2.2.1 → voidx-3.0.0}/src/voidx.egg-info/dependency_links.txt +0 -0
  329. {voidx-2.2.1 → voidx-3.0.0}/src/voidx.egg-info/entry_points.txt +0 -0
  330. {voidx-2.2.1 → voidx-3.0.0}/src/voidx.egg-info/requires.txt +0 -0
  331. {voidx-2.2.1 → voidx-3.0.0}/src/voidx.egg-info/top_level.txt +0 -0
  332. {voidx-2.2.1 → voidx-3.0.0}/tests/test_clipboard_image.py +0 -0
  333. {voidx-2.2.1 → voidx-3.0.0}/tests/test_clipboard_text.py +0 -0
  334. {voidx-2.2.1 → voidx-3.0.0}/tests/test_code_ide.py +0 -0
  335. {voidx-2.2.1 → voidx-3.0.0}/tests/test_llm_catalog.py +0 -0
  336. {voidx-2.2.1 → voidx-3.0.0}/tests/test_llm_usage.py +0 -0
  337. {voidx-2.2.1 → voidx-3.0.0}/tests/test_main.py +0 -0
  338. {voidx-2.2.1 → voidx-3.0.0}/tests/test_main_startup.py +0 -0
  339. {voidx-2.2.1 → voidx-3.0.0}/tests/test_mcp.py +0 -0
  340. {voidx-2.2.1 → voidx-3.0.0}/tests/test_output_browse.py +0 -0
  341. {voidx-2.2.1 → voidx-3.0.0}/tests/test_runtime_ui.py +0 -0
  342. {voidx-2.2.1 → voidx-3.0.0}/tests/test_startup.py +0 -0
  343. {voidx-2.2.1 → voidx-3.0.0}/tests/test_ui_diff.py +0 -0
  344. {voidx-2.2.1 → voidx-3.0.0}/tests/test_ui_frontend_protocol.py +0 -0
  345. {voidx-2.2.1 → voidx-3.0.0}/tests/test_ui_gateway.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidx
3
- Version: 2.2.1
3
+ Version: 3.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voidx"
3
- version = "2.2.1"
3
+ version = "3.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"
@@ -48,6 +48,7 @@ where = ["src"]
48
48
  ]
49
49
  "voidx.data" = [
50
50
  "intent_classifier.json",
51
+ "templates/*.md",
51
52
  ]
52
53
 
53
54
  [tool.pytest.ini_options]
@@ -1,3 +1,3 @@
1
1
  """VoidX - A coding agent that quantifies everything."""
2
2
 
3
- __version__ = "2.2.1"
3
+ __version__ = "3.0.0"
@@ -0,0 +1,266 @@
1
+ """Agent definitions — typed config, whenToUse descriptions, prompts.
2
+
3
+ voidx uses one agent identity:
4
+ voidx — primary identity, also used for isolated child runs
5
+
6
+ Runtime personas (coordinate/explore/plan/implement/review) are thinking-mode
7
+ labels, not AgentDef ids.
8
+ """
9
+
10
+
11
+ from __future__ import annotations
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ # ── prompts ───────────────────────────────────────────────────────────────
17
+
18
+ BASE_SYSTEM_PROMPT = """You are voidx, a coding agent that lives in the terminal.
19
+
20
+ ## Communication Style
21
+
22
+ - **Natural and warm.** Write like a skilled colleague, not a robot.
23
+ Use contractions, vary sentence length, show personality.
24
+ - **Match the user's language.** If the user writes in Chinese, respond in Chinese.
25
+ If they write in English, respond in English. Mirror their tone.
26
+ - **Be concise.** One good sentence beats three mediocre ones. The user can ask
27
+ follow-ups if they want more detail.
28
+ - **Don't explain your internals.** The user doesn't need to know about agents,
29
+ personas, explore/plan/implement/review, or your architecture. Just help them.
30
+ If asked "who are you", say "I'm voidx, a coding assistant" — one sentence max.
31
+ - **Say what you're about to do.** Brief heads-up before searching or editing:
32
+ "Let me check the auth module." — not "I will now delegate to the explore agent."
33
+ - **Summarize results, not process.** After completing work, tell the user what
34
+ changed and where. Don't narrate which agents you used or how many steps it took.
35
+ - **Acknowledge uncertainty.** If you're not sure, say so. "I think it's auth.py:42,
36
+ but let me verify" — not "I have medium confidence in this assessment."
37
+ - **Show progress via todo.** Update the todo list so progress is visible.
38
+ But don't narrate todo updates in your text.
39
+
40
+ ## Global Rules
41
+
42
+ - Use tools for facts about the workspace; do not guess file contents.
43
+ - Read before editing. Make minimal, precise changes.
44
+ - Keep user-facing responses concise and focused on outcomes.
45
+ - Do not expose internal persona names unless the user asks about architecture.
46
+ - Never claim work is complete until it has been verified.
47
+ - When Current Task State lists an active workflow gate, that workflow gate takes precedence
48
+ over persona prompts, delegation rules, and the decision flow below.
49
+
50
+ ## Workflow Runtime
51
+
52
+ - voidx has a structured workflow runtime.
53
+ - Current Task State is the activation source for this turn's workflow nodes.
54
+ - Workflow Context messages contain structured workflow node definitions as a
55
+ stable reference library. Follow ONLY nodes listed as active in Current Task
56
+ State, unless the user explicitly references another node by name.
57
+ - When a node is not listed as active, its definition is reference only. Do not
58
+ follow its gate, internal workflow steps, or transition instructions.
59
+ - load_skills can return project/global skill bodies for the current turn.
60
+ """
61
+
62
+
63
+ VOIDX_PROMPT = """## Coordination
64
+
65
+ - Assess before acting.
66
+ - Stay aligned with the user's actual goal.
67
+ - Delegate only when you need to run multiple independent tasks in parallel, or the user explicitly asks for a child agent. Do not delegate single-file reads, simple searches, or straightforward tasks you can do directly.
68
+ - Coordinate the work without exposing internal persona names to the user.
69
+
70
+ ## Persona Model
71
+
72
+ voidx has five thinking modes (personas). The active persona is shown in Current Task State.
73
+ Switch persona automatically when entering a workflow node.
74
+
75
+ - **coordinate**: Default. Assess, plan next steps, coordinate work, delegate when parallel speedup is needed.
76
+ - **explore**: Evidence gathering and codebase search. Search broadly, report with concrete paths and lines.
77
+ - **plan**: Design and architecture. Study existing patterns, output structured implementable plans.
78
+ - **implement**: Build and execute. Write minimal precise edits, run tests to verify.
79
+ - **review**: Verify and critique. Check correctness, completeness, style, security. Produce PASS/FAIL verdicts.
80
+
81
+ ## Responsibilities
82
+
83
+ - Before acting, assess what's already known and what's still needed.
84
+ - Pick the smallest next action that makes progress toward the goal.
85
+ - Only delegate to a child agent when you have multiple independent tasks or the user asks.
86
+ - Only declare work done after running verification (tests, reads, diagnostics).
87
+
88
+ ## Rules
89
+
90
+ - Subagents do not interact with the user.
91
+ - Runtime workflow gates take precedence over persona prompts and delegation rules.
92
+ """
93
+
94
+ CHILD_RUN_CONSTRAINTS = """- Follow the runtime persona shown in Current Task State.
95
+ - Execute only the delegated task in this isolated child run.
96
+ - Do not interact with the user directly.
97
+ - Do not start another child agent.
98
+ - If a tool call fails, report the error clearly and attempt an alternative approach if one exists.
99
+ - Structure your final output with clear sections: what you found, what you did, and what remains uncertain.
100
+ - Runtime workflow gates take precedence over persona prompts and delegation rules."""
101
+
102
+ # Plan mode prompt — injected when plan_mode=True
103
+ PLAN_MODE_APPEND = """
104
+ ## PLAN MODE ACTIVE
105
+ You are in plan mode. Write/edit tools are BLOCKED at the permission level.
106
+ - You CAN: read, glob, grep, bash (non-destructive commands only; no file writes, installs, or system changes), agent(plan/explore/review)
107
+ - You CANNOT: write, edit, agent(implement), bash (destructive)
108
+ - Focus on analysis, design, and creating structured plans.
109
+ - When ready to implement, tell the user to exit plan mode.
110
+ """
111
+
112
+ # ── agent definitions ─────────────────────────────────────────────────────
113
+
114
+ class AgentDef(BaseModel):
115
+ """An agent's complete definition — typed, no loose config."""
116
+ name: str
117
+ description: str
118
+ when_to_use: str
119
+ tools: list[str] # tool IDs this agent can use
120
+ can_write: bool
121
+ can_delegate: bool # can it start child agents via the agent tool?
122
+ hidden: bool = False # hidden from user-facing lists?
123
+ model: str | None = None # None = inherit from parent
124
+ mcp_tools: bool = False # can see registered MCP tools
125
+
126
+ @property
127
+ def persona_prompt(self) -> str:
128
+ try:
129
+ return PERSONA_PROMPTS[self.name]
130
+ except KeyError as exc:
131
+ raise ValueError(f"No persona prompt registered for agent: {self.name}") from exc
132
+
133
+ @property
134
+ def tool_contract(self) -> str:
135
+ lines = [
136
+ f"- Agent identity: {self.name}",
137
+ f"- Can write files: {str(self.can_write).lower()}",
138
+ f"- Can start child agents: {str(self.can_delegate).lower()}",
139
+ ]
140
+ if self.tools:
141
+ lines.append(f"- Available tools: {', '.join(self.tools)}")
142
+ else:
143
+ lines.append("- Available tools: none")
144
+ if self.mcp_tools:
145
+ lines.append("- MCP tools: available when configured; each call is permission-gated")
146
+ if not self.can_delegate:
147
+ lines.append("- Constraint: this agent must not start another child agent.")
148
+ return "\n".join(lines)
149
+
150
+ def persona_prompt_for_llm(agent: AgentDef, *, parallel_subagents_enabled: bool = False) -> str:
151
+ """Return the persona prompt with runtime-gated child-agent scheduling rules."""
152
+
153
+ prompt = agent.persona_prompt
154
+ if agent.name != "voidx":
155
+ return prompt
156
+ child_agent_prompt = _parallel_subagents_prompt(
157
+ enabled=parallel_subagents_enabled,
158
+ )
159
+ if not prompt:
160
+ return child_agent_prompt
161
+ return f"{prompt.rstrip()}\n\n{child_agent_prompt}"
162
+
163
+
164
+ _SCHEDULING_COMMON = """- Each child-agent brief must be complete and self-contained.
165
+ - Keep dependent child-agent work sequential: wait for the result before
166
+ delegating follow-up work that depends on it.
167
+ - Batch independent read/search tools when useful; keep dependent tool work
168
+ sequential."""
169
+
170
+
171
+ def _parallel_subagents_prompt(*, enabled: bool) -> str:
172
+ if enabled:
173
+ return (
174
+ "## Child-Agent Scheduling\n\n"
175
+ "- Only start a child agent when you have multiple independent tasks to run in\n"
176
+ " parallel, or the user explicitly requests it. Do not delegate work you can do\n"
177
+ " directly.\n"
178
+ "- For independent child-agent tasks, you may issue multiple `agent` tool calls\n"
179
+ " in one response. They will run concurrently up to the configured limit.\n"
180
+ + _SCHEDULING_COMMON
181
+ )
182
+ return (
183
+ "## Child-Agent Scheduling\n\n"
184
+ "- Only start a child agent when you have multiple independent tasks to run in\n"
185
+ " parallel, or the user explicitly requests it. Do not delegate work you can do\n"
186
+ " directly.\n"
187
+ "- Delegate at most one child agent in a response. Wait for that result before\n"
188
+ " deciding whether another child agent is needed.\n"
189
+ + _SCHEDULING_COMMON
190
+ )
191
+
192
+
193
+ # ── built-in agents ────────────────────────────────────────────────────────
194
+
195
+ BUILTIN_AGENTS: dict[str, AgentDef] = {
196
+ "voidx": AgentDef(
197
+ name="voidx",
198
+ description="Primary agent. Understands intent, edits small scoped changes directly, "
199
+ "delegates broad work to specialists, reviews results.",
200
+ when_to_use="Default agent for all user interactions. Always use first.",
201
+ tools=[
202
+ "clarify", "plan_checkpoint", "advance_workflow", "compact_context",
203
+ "read", "glob", "grep", "bash", "agent", "task_status", "todo", "load_skills",
204
+ "load_doc_template",
205
+ "webfetch", "websearch", "repo_map",
206
+ "lsp",
207
+ "write", "edit",
208
+ ],
209
+ can_write=True,
210
+ can_delegate=True,
211
+ hidden=False,
212
+ mcp_tools=True,
213
+ ),
214
+ }
215
+
216
+
217
+ PERSONA_PROMPTS = {
218
+ "voidx": VOIDX_PROMPT,
219
+ }
220
+
221
+
222
+ CHILD_RUN_TOOLS = [
223
+ "read", "write", "edit", "glob", "grep", "bash", "todo", "load_skills", "repo_map",
224
+ "lsp",
225
+ ]
226
+
227
+
228
+ def get_agent(name: str) -> AgentDef | None:
229
+ return BUILTIN_AGENTS.get(name)
230
+
231
+
232
+ def get_visible_agents() -> list[AgentDef]:
233
+ return [a for a in BUILTIN_AGENTS.values() if not a.hidden]
234
+
235
+
236
+ def get_subagents() -> list[AgentDef]:
237
+ """Child-run identities voidx can delegate to."""
238
+ agent = get_agent("voidx")
239
+ return [child_run_agent_def(agent)] if agent is not None else []
240
+
241
+
242
+ def child_run_agent_def(agent: AgentDef) -> AgentDef:
243
+ """Return the child-run view of the public voidx identity."""
244
+ tools = CHILD_RUN_TOOLS if agent.name == "voidx" else agent.tools
245
+ return agent.model_copy(update={
246
+ "name": "voidx",
247
+ "description": "Isolated child run of voidx with a requested runtime persona.",
248
+ "when_to_use": "Use for delegated child work that benefits from isolated context.",
249
+ "tools": tools,
250
+ "can_delegate": False,
251
+ })
252
+
253
+
254
+ def child_agent_descriptions_for_llm() -> str:
255
+ """Generate child-agent descriptions for the agent tool."""
256
+ lines = ["Available child agents and the tools they have access to:"]
257
+ for agent in get_subagents():
258
+ tools_str = ", ".join(agent.tools)
259
+ if agent.mcp_tools:
260
+ tools_str = f"{tools_str}, MCP tools" if tools_str else "MCP tools"
261
+ lines.append(
262
+ f"- {agent.name}: {agent.description}\n"
263
+ f" Tools: {tools_str}\n"
264
+ f" Write access: {agent.can_write}"
265
+ )
266
+ return "\n".join(lines)
@@ -10,9 +10,10 @@ from dataclasses import dataclass, field
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
+ from voidx.runtime.attachments import MAX_IMAGE_ATTACHMENT_BYTES
14
+
13
15
  IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
14
16
  MAX_TEXT_ATTACHMENT_BYTES = 200_000
15
- MAX_IMAGE_ATTACHMENT_BYTES = 5_000_000
16
17
  MAX_DIR_LISTING_ITEMS = 500
17
18
  _DIR_TREE_SKIP = {"__pycache__", ".git", ".hg", ".svn", "node_modules", ".venv", "venv", "dist", "build", ".pytest_cache", ".mypy_cache"}
18
19
  _ATTACHMENT_RE = re.compile(r'(?<!\S)(?:@(?:"([^"]+)"|(\S+))|\[image-([^\]]+)\])')
@@ -56,7 +57,13 @@ def serialize_message_content(content: str | list[dict[str, Any]]) -> tuple[str,
56
57
  return content, "text"
57
58
 
58
59
 
59
- def build_user_message_payload(user_text: str, workspace: str) -> UserMessagePayload:
60
+ def build_user_message_payload(
61
+ user_text: str,
62
+ workspace: str,
63
+ *,
64
+ text_prefix: str = "",
65
+ extra_removed_spans: list[tuple[int, int]] | None = None,
66
+ ) -> UserMessagePayload:
60
67
  workspace_path = Path(workspace).resolve()
61
68
  tokens = _attachment_tokens(user_text)
62
69
  removed_spans: list[tuple[int, int]] = []
@@ -112,8 +119,13 @@ def build_user_message_payload(user_text: str, workspace: str) -> UserMessagePay
112
119
  if section:
113
120
  text_sections.append(section)
114
121
 
122
+ display_clean_text = _normalize_text(_remove_spans(user_text, removed_spans))
123
+ if extra_removed_spans:
124
+ removed_spans.extend(extra_removed_spans)
115
125
  clean_text = _normalize_text(_remove_spans(user_text, removed_spans))
116
126
  text_content = _build_text_content(clean_text, attachments, text_sections)
127
+ if text_prefix.strip():
128
+ text_content = _prefix_text_content(text_prefix.strip(), text_content)
117
129
  content: str | list[dict[str, Any]]
118
130
  content_format = "text"
119
131
  if image_parts:
@@ -122,7 +134,7 @@ def build_user_message_payload(user_text: str, workspace: str) -> UserMessagePay
122
134
  else:
123
135
  content = text_content
124
136
 
125
- display_text = _display_text(clean_text, attachments)
137
+ display_text = _display_text(display_clean_text, attachments)
126
138
  title_text = clean_text or (f"Attached {attachments[0].rel_path}" if attachments else user_text)
127
139
  return UserMessagePayload(
128
140
  raw_text=user_text,
@@ -158,7 +170,9 @@ def _remove_spans(text: str, spans: list[tuple[int, int]]) -> str:
158
170
  return text
159
171
  result: list[str] = []
160
172
  last = 0
161
- for start, end in spans:
173
+ for start, end in sorted(spans):
174
+ if start < last:
175
+ continue
162
176
  result.append(text[last:start])
163
177
  result.append(" ")
164
178
  last = end
@@ -166,6 +180,13 @@ def _remove_spans(text: str, spans: list[tuple[int, int]]) -> str:
166
180
  return "".join(result)
167
181
 
168
182
 
183
+ def _prefix_text_content(prefix: str, text: str) -> str:
184
+ text = text.strip()
185
+ if not text:
186
+ return prefix
187
+ return f"{prefix}\n\n{text}"
188
+
189
+
169
190
  def _resolve_workspace_path(workspace: Path, raw_path: str) -> Path | None:
170
191
  candidate = Path(raw_path).expanduser()
171
192
  resolved = candidate.resolve() if candidate.is_absolute() else (workspace / candidate).resolve()
@@ -0,0 +1,228 @@
1
+ """Runtime-owned structured goal resolution for top-level turns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from typing import Any
8
+
9
+ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
10
+
11
+ from voidx.runtime.intent import InteractionMode, TaskIntent
12
+ from voidx.runtime.task_state import (
13
+ GoalResolution,
14
+ GoalSpec,
15
+ GoalType,
16
+ IntentResolution,
17
+ PlanResolution,
18
+ TaskState,
19
+ _default_join_for_goal_type,
20
+ _default_leave_for_goal_type,
21
+ )
22
+ from voidx.workflow.dag import DEFAULT_WORKFLOW_DAG
23
+
24
+
25
+ GOAL_RESOLVER_TIMEOUT_SECONDS = 20
26
+
27
+
28
+ async def resolve_goal_for_turn(
29
+ *,
30
+ model: Any | None,
31
+ user_text: str,
32
+ interaction_mode: str | InteractionMode | None,
33
+ task_state: TaskState,
34
+ workspace: str,
35
+ session_time: str,
36
+ ) -> GoalResolution:
37
+ fallback = GoalResolution(
38
+ intent=IntentResolution(type=TaskIntent.GENERAL, desc=""),
39
+ goal=None,
40
+ plan=None,
41
+ )
42
+ if model is None:
43
+ return _normalize_resolution(fallback, user_text, interaction_mode, task_state)
44
+
45
+ structured = getattr(model, "with_structured_output", None)
46
+ if not callable(structured):
47
+ return _normalize_resolution(fallback, user_text, interaction_mode, task_state)
48
+
49
+ try:
50
+ runnable = structured(GoalResolution)
51
+ raw = await asyncio.wait_for(
52
+ runnable.ainvoke(_resolver_messages(
53
+ user_text,
54
+ interaction_mode,
55
+ task_state,
56
+ workspace,
57
+ session_time,
58
+ )),
59
+ timeout=GOAL_RESOLVER_TIMEOUT_SECONDS,
60
+ )
61
+ resolution = _coerce_resolution(raw)
62
+ except Exception:
63
+ resolution = None
64
+
65
+ if resolution is None:
66
+ return _normalize_resolution(fallback, user_text, interaction_mode, task_state)
67
+ return _normalize_resolution(resolution, user_text, interaction_mode, task_state)
68
+
69
+
70
+ def _resolver_messages(
71
+ user_text: str,
72
+ interaction_mode: str | InteractionMode | None,
73
+ task_state: TaskState,
74
+ workspace: str,
75
+ session_time: str,
76
+ ) -> list:
77
+ schema = json.dumps(GoalResolution.model_json_schema(), ensure_ascii=False)
78
+ current_goal = (
79
+ task_state.current_goal.model_dump(mode="json")
80
+ if task_state.current_goal is not None
81
+ else None
82
+ )
83
+ context = {
84
+ "workspace": workspace,
85
+ "session_time": session_time,
86
+ "interaction_mode": InteractionMode.parse(interaction_mode).value,
87
+ "current_intent": task_state.current_intent.value,
88
+ "current_goal": current_goal,
89
+
90
+ "recent_user_texts": task_state.recent_user_texts[-2:],
91
+ "latest_user_text": user_text,
92
+ }
93
+ available_joins = ", ".join(sorted(_ALLOWED_JOIN_NODES))
94
+ system = (
95
+ "You are voidx resolving the current user's goal before normal work begins.\n"
96
+ "Return only structured data matching the GoalResolution schema.\n\n"
97
+ "Rules:\n"
98
+ "- intent.type=general only for non-code, non-workspace conversation.\n"
99
+ "- intent.type=coding for codebase inspection, design, docs, review, debugging, or edits.\n"
100
+ "- Pick exactly one goal.type when intent is coding and a concrete workspace goal exists.\n"
101
+ "- plan.join is the workflow node the agent should enter. Required when goal is set; null when goal is null.\n"
102
+ "- plan.leave is the workflow node after which automatic progression stops. Optional.\n"
103
+ f"- Available join values: {available_joins}.\n"
104
+ "- Choose join based on the user's primary intent:\n"
105
+ " - debug: user reports a bug, error, crash, or unexpected behavior to investigate.\n"
106
+ " - brainstorm: user wants to explore requirements, design a feature, or discuss approach before coding.\n"
107
+ " - design-doc: user asks to write or revise a design/spec/PRD/RFC/API doc.\n"
108
+ " - plan: user asks to turn a spec or requirements into an implementation plan.\n"
109
+ " - tdd: user explicitly asks to implement an already detailed spec or continue an approved implementation.\n"
110
+ " - review: user asks for code review or pre-merge review.\n"
111
+ " - feedback: user provides review feedback or reviewer comments to act on.\n"
112
+ "- If the user's intent does not clearly match any join value, set goal to null and plan to null. The agent will work without workflow constraints.\n"
113
+ "- Do not choose brainstorm when the request already contains an approved or sufficiently detailed spec.\n"
114
+ "- Do not set join or leave based on vague or ambiguous approval.\n"
115
+ "- goal and plan are bound: if goal is set, plan must be set with join; if goal is null, plan must be null.\n"
116
+ f"GoalResolution JSON schema:\n{schema}"
117
+ )
118
+ return [
119
+ SystemMessage(content=system),
120
+ HumanMessage(content=json.dumps(context, ensure_ascii=False, indent=2)),
121
+ ]
122
+
123
+
124
+ _ALLOWED_JOIN_NODES = {"debug", "brainstorm", "design-doc", "plan", "tdd", "review", "feedback"}
125
+
126
+
127
+ def _coerce_resolution(value: object) -> GoalResolution | None:
128
+ if isinstance(value, GoalResolution):
129
+ return value
130
+ if isinstance(value, AIMessage):
131
+ value = value.content
132
+ if isinstance(value, str):
133
+ try:
134
+ value = json.loads(value)
135
+ except json.JSONDecodeError:
136
+ return None
137
+ if isinstance(value, dict) and "parsed" in value:
138
+ return _coerce_resolution(value.get("parsed"))
139
+ if isinstance(value, dict):
140
+ try:
141
+ return GoalResolution.model_validate(value)
142
+ except ValueError:
143
+ return None
144
+ return None
145
+
146
+
147
+ def _normalize_resolution(
148
+ resolution: GoalResolution,
149
+ user_text: str,
150
+ interaction_mode: str | InteractionMode | None,
151
+ task_state: TaskState,
152
+ ) -> GoalResolution:
153
+ mode = InteractionMode.parse(interaction_mode)
154
+
155
+ # Validate join against resolver entry points, and leave against DAG nodes.
156
+ plan = resolution.plan
157
+ if plan is not None:
158
+ if plan.join and plan.join not in _ALLOWED_JOIN_NODES:
159
+ plan = None
160
+ elif plan.leave and plan.leave not in DEFAULT_WORKFLOW_DAG.nodes:
161
+ plan = PlanResolution(join=plan.join, leave=None)
162
+
163
+ # plan mode: force design goal + brainstorm
164
+ if mode == InteractionMode.PLAN:
165
+ desc = (
166
+ resolution.goal.desc
167
+ if resolution.goal is not None and resolution.goal.desc.strip()
168
+ else user_text
169
+ )
170
+ return GoalResolution(
171
+ intent=IntentResolution(type=TaskIntent.CODING, desc="plan mode forces design goal"),
172
+ goal=GoalSpec(type=GoalType.DESIGN, desc=desc),
173
+ plan=PlanResolution(
174
+ join="brainstorm",
175
+ leave=plan.leave if plan is not None else None,
176
+ ),
177
+ )
178
+
179
+ # goal mode: keep current_goal unchanged
180
+ if mode == InteractionMode.GOAL and task_state.current_goal is not None:
181
+ current = task_state.current_goal
182
+ goal = GoalSpec(type=current.type, desc=current.desc)
183
+ if plan is None:
184
+ plan = PlanResolution(
185
+ join=_default_join_for_goal_type(current.type),
186
+ leave=_default_leave_for_goal_type(current.type),
187
+ )
188
+ return GoalResolution(
189
+ intent=IntentResolution(
190
+ type=TaskIntent.CODING,
191
+ desc="goal mode keeps the turn scoped to the current goal",
192
+ ),
193
+ goal=goal,
194
+ plan=plan,
195
+ )
196
+
197
+ # general intent: no goal, no plan
198
+ if resolution.intent.type == TaskIntent.GENERAL:
199
+ return GoalResolution(
200
+ intent=resolution.intent,
201
+ goal=None,
202
+ plan=None,
203
+ )
204
+
205
+ # coding intent: fill default join/leave when needed
206
+ goal = resolution.goal
207
+ if goal is not None:
208
+ if plan is None:
209
+ plan = PlanResolution(
210
+ join=_default_join_for_goal_type(goal.type),
211
+ leave=_default_leave_for_goal_type(goal.type),
212
+ )
213
+ elif not plan.join:
214
+ plan = PlanResolution(
215
+ join=_default_join_for_goal_type(goal.type),
216
+ leave=plan.leave or _default_leave_for_goal_type(goal.type),
217
+ )
218
+ elif not plan.leave:
219
+ plan = PlanResolution(
220
+ join=plan.join,
221
+ leave=_default_leave_for_goal_type(goal.type),
222
+ )
223
+
224
+ return GoalResolution(
225
+ intent=resolution.intent,
226
+ goal=goal,
227
+ plan=plan,
228
+ )
@@ -0,0 +1,81 @@
1
+ """Context compaction method proxies for the agent graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from voidx.agent.graph.compaction_coordinator import CompactionResult, GraphCompactionCoordinator
8
+
9
+ if TYPE_CHECKING:
10
+ from voidx.agent.graph.contracts import GraphCompactionHost
11
+
12
+
13
+ def _compaction_component_for(host: GraphCompactionHost) -> GraphCompactionCoordinator:
14
+ coordinator = getattr(host, "_compaction_coordinator", None)
15
+ if coordinator is None:
16
+ # Bare mixin tests can instantiate a host without VoidXGraph.__init__.
17
+ coordinator = GraphCompactionCoordinator(host)
18
+ host._compaction_coordinator = coordinator
19
+ return coordinator
20
+
21
+
22
+ class GraphCompactionMixin:
23
+ def _compaction_component(self: GraphCompactionHost) -> GraphCompactionCoordinator:
24
+ return _compaction_component_for(self)
25
+
26
+ async def _maybe_compact(
27
+ self: GraphCompactionHost,
28
+ messages: list,
29
+ session_msgs: list | None = None,
30
+ *,
31
+ force: bool = False,
32
+ ask: bool = True,
33
+ ) -> tuple[list | None, str | None]:
34
+ return await _compaction_component_for(self).maybe_compact(
35
+ messages,
36
+ session_msgs,
37
+ force=force,
38
+ ask=ask,
39
+ run_compaction_agent=self._run_compaction_agent,
40
+ persist_compaction=self._persist_compaction,
41
+ )
42
+
43
+ async def _in_turn_compact(
44
+ self: GraphCompactionHost,
45
+ 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
+ return await _compaction_component_for(self).compact_for_live_state(
52
+ messages,
53
+ force=True,
54
+ ask=False,
55
+ include_summary_message=True,
56
+ run_compaction_agent=self._run_compaction_agent,
57
+ persist_compaction=self._persist_compaction,
58
+ )
59
+
60
+ async def _ask_compact(self: GraphCompactionHost, total_tokens: int) -> bool:
61
+ return await _compaction_component_for(self).ask_compact(total_tokens)
62
+
63
+ async def _persist_compaction(self: GraphCompactionHost, head_messages: list) -> None:
64
+ await _compaction_component_for(self).persist_compaction(head_messages)
65
+
66
+ async def _compact_session_history(self: GraphCompactionHost, *, force: bool = True) -> bool:
67
+ return await _compaction_component_for(self).compact_session_history(
68
+ force=force,
69
+ run_compaction_agent=self._run_compaction_agent,
70
+ persist_compaction=self._persist_compaction,
71
+ )
72
+
73
+ async def _run_compaction_agent(
74
+ self: GraphCompactionHost,
75
+ head_messages: list,
76
+ previous_summary: str | None,
77
+ ) -> str | None:
78
+ return await _compaction_component_for(self).run_compaction_agent(
79
+ head_messages,
80
+ previous_summary,
81
+ )