vox-code 2.0.0__py3-none-any.whl

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 (88) hide show
  1. vox_code-2.0.0.dist-info/METADATA +258 -0
  2. vox_code-2.0.0.dist-info/RECORD +88 -0
  3. vox_code-2.0.0.dist-info/WHEEL +4 -0
  4. vox_code-2.0.0.dist-info/entry_points.txt +3 -0
  5. voxcli/__init__.py +3 -0
  6. voxcli/__main__.py +5 -0
  7. voxcli/agent/__init__.py +12 -0
  8. voxcli/agent/agent.py +449 -0
  9. voxcli/agent/agent_budget.py +133 -0
  10. voxcli/agent/agent_orchestrator.py +414 -0
  11. voxcli/agent/plan_execute_agent.py +514 -0
  12. voxcli/agent/roles.py +80 -0
  13. voxcli/agent/sub_agent.py +351 -0
  14. voxcli/catalog.py +477 -0
  15. voxcli/chat.py +91 -0
  16. voxcli/cli/__init__.py +4 -0
  17. voxcli/cli/main.py +452 -0
  18. voxcli/cli/parser.py +71 -0
  19. voxcli/config.py +518 -0
  20. voxcli/gui/__main__.py +3 -0
  21. voxcli/gui/main.py +22 -0
  22. voxcli/gui/pet/__init__.py +5 -0
  23. voxcli/gui/pet/base.py +62 -0
  24. voxcli/gui/pet/coordinator.py +888 -0
  25. voxcli/gui/pet/data.py +430 -0
  26. voxcli/gui/pet/widgets.py +683 -0
  27. voxcli/gui/pet/windows.py +2298 -0
  28. voxcli/gui/pet/workers.py +54 -0
  29. voxcli/gui/pet_app.py +7 -0
  30. voxcli/hitl/__init__.py +11 -0
  31. voxcli/hitl/handler.py +11 -0
  32. voxcli/hitl/policy.py +32 -0
  33. voxcli/hitl/request.py +13 -0
  34. voxcli/hitl/result.py +11 -0
  35. voxcli/hitl/terminal_handler.py +64 -0
  36. voxcli/hitl/tool_registry.py +64 -0
  37. voxcli/llm/base.py +93 -0
  38. voxcli/llm/factory.py +178 -0
  39. voxcli/llm/ollama_client.py +137 -0
  40. voxcli/llm/openai_compatible.py +249 -0
  41. voxcli/memory/base.py +16 -0
  42. voxcli/memory/budget.py +53 -0
  43. voxcli/memory/compressor.py +198 -0
  44. voxcli/memory/entry.py +36 -0
  45. voxcli/memory/long_term.py +126 -0
  46. voxcli/memory/manager.py +101 -0
  47. voxcli/memory/retriever.py +72 -0
  48. voxcli/memory/short_term.py +84 -0
  49. voxcli/memory/tokenizer.py +21 -0
  50. voxcli/plan/__init__.py +5 -0
  51. voxcli/plan/execution_plan.py +225 -0
  52. voxcli/plan/planner.py +198 -0
  53. voxcli/plan/task.py +123 -0
  54. voxcli/policy/audit_log.py +111 -0
  55. voxcli/policy/command_guard.py +34 -0
  56. voxcli/policy/exception.py +5 -0
  57. voxcli/policy/path_guard.py +32 -0
  58. voxcli/prompting/__init__.py +7 -0
  59. voxcli/prompting/presenter.py +154 -0
  60. voxcli/rag/__init__.py +16 -0
  61. voxcli/rag/analyzer.py +89 -0
  62. voxcli/rag/chunk.py +17 -0
  63. voxcli/rag/chunker.py +137 -0
  64. voxcli/rag/embedding.py +75 -0
  65. voxcli/rag/formatter.py +40 -0
  66. voxcli/rag/index.py +96 -0
  67. voxcli/rag/relation.py +14 -0
  68. voxcli/rag/retriever.py +58 -0
  69. voxcli/rag/store.py +155 -0
  70. voxcli/rag/tokenizer.py +26 -0
  71. voxcli/runtime/__init__.py +6 -0
  72. voxcli/runtime/session_controller.py +386 -0
  73. voxcli/tool/__init__.py +3 -0
  74. voxcli/tool/tool_registry.py +433 -0
  75. voxcli/util/animation.py +219 -0
  76. voxcli/util/ansi.py +82 -0
  77. voxcli/util/markdown.py +98 -0
  78. voxcli/web/__init__.py +17 -0
  79. voxcli/web/base.py +20 -0
  80. voxcli/web/extractor.py +77 -0
  81. voxcli/web/factory.py +38 -0
  82. voxcli/web/fetch_result.py +27 -0
  83. voxcli/web/fetcher.py +42 -0
  84. voxcli/web/network_policy.py +49 -0
  85. voxcli/web/result.py +23 -0
  86. voxcli/web/searxng.py +55 -0
  87. voxcli/web/serpapi.py +53 -0
  88. voxcli/web/zhipu.py +55 -0
@@ -0,0 +1,351 @@
1
+ """子代理 - 可配置角色的轻量 Agent"""
2
+
3
+ import json
4
+ import time
5
+ import logging
6
+ from typing import List, Optional, Dict
7
+
8
+ from ..llm.base import LlmClient, Message, ToolCall
9
+ from ..tool import ToolRegistry, ToolInvocation
10
+ from ..util.ansi import heading, section, subtle
11
+ from .roles import AgentRole, AgentMessage, AgentMessageType
12
+ from .agent_budget import AgentBudget, ExitReason
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _PLANNER_PROMPT = """你是一个任务规划专家。你的职责是分析用户的需求,将其拆解为清晰的执行步骤。
17
+
18
+ 请按以下 JSON 格式输出执行计划:
19
+ {
20
+ "summary": "任务摘要",
21
+ "steps": [
22
+ {
23
+ "id": "step_1",
24
+ "description": "步骤描述,要具体明确",
25
+ "type": "FILE_READ | FILE_WRITE | COMMAND | ANALYSIS | VERIFICATION",
26
+ "dependencies": []
27
+ }
28
+ ]
29
+ }
30
+
31
+ 规则:
32
+ 1. 每个步骤必须有唯一的 id(如 step_1, step_2)
33
+ 2. dependencies 列出依赖的步骤 id
34
+ 3. 步骤描述要具体,让执行者能直接理解要做什么
35
+ 4. 简单任务可以只拆成 1-3 步
36
+ 5. 复杂任务拆成 5-10 步
37
+ 6. 不要为了凑步数引入无关操作
38
+ 7. 如果多个步骤可以独立完成,不要给它们添加依赖;保持 dependencies 为空,让编排器能并行分配给多个 Worker。
39
+ 8. 只有后一步确实需要前一步结果时,才写 dependencies。
40
+
41
+ 只输出 JSON,不要有其他内容。
42
+ 请用中文回复。"""
43
+
44
+ _WORKER_PROMPT = """你是一个任务执行专家。你的职责是根据给定的任务步骤,调用工具完成具体操作。
45
+
46
+ 可用工具:
47
+ 1. read_file - 读取文件内容,参数:{"path": "文件路径"}
48
+ 2. write_file - 写入文件内容,参数:{"path": "文件路径", "content": "内容"}
49
+ 3. list_dir - 列出目录内容,参数:{"path": "目录路径"}
50
+ 4. execute_command - 执行命令,参数:{"command": "命令"}
51
+ 5. create_project - 创建项目,参数:{"name": "名称", "type": "java|python|node"}
52
+ 6. search_code - 语义检索代码库,参数:{"query": "自然语言描述", "top_k": 5}
53
+ 7. web_search - 搜索互联网获取实时信息,参数:{"query": "搜索关键词", "top_k": 5}
54
+ 8. web_fetch - 抓取已知 URL 并返回正文 Markdown,参数:{"url": "https://...", "max_chars": 8000}
55
+
56
+ 如果任务涉及理解代码库,请优先使用 search_code 工具。
57
+ 如果任务涉及实时性或互联网信息,先用 web_search 找入口,拿到 URL 后用 web_fetch 取全文。
58
+ 对于当前项目内的文件,请优先使用 read_file 或 list_dir,不要用 execute_command 扫描 /、~ 或整个文件系统。
59
+ execute_command 只适合在当前项目目录执行短时命令。
60
+ 安全策略硬规则(HITL 之外的兜底,无法绕过):read_file / write_file / list_dir / create_project 必须在项目根之内;
61
+ write_file 单文件 5MB 上限;execute_command 禁止 sudo / rm -rf 全盘 / mkfs / dd of=/dev / fork bomb / curl|sh / find / / chmod 777 / / shutdown。
62
+ 被策略拒绝的工具调用("🛡️ 策略拒绝" 开头)不要原样重试,改用项目内相对路径或更安全的命令。
63
+ 同一轮返回多个工具调用时,系统会并行执行这些工具;如果工具之间有依赖关系,请分多轮调用。
64
+ 如果是 ANALYSIS 或 VERIFICATION 类型任务,请直接输出分析结果。
65
+
66
+ 请用中文回复。"""
67
+
68
+ _REVIEWER_PROMPT = """你是一个质量检查专家。你的职责是检查执行结果是否正确、完整和高质量。
69
+
70
+ 检查要点:
71
+ 1. 任务是否按要求完成
72
+ 2. 结果是否正确,有无明显错误
73
+ 3. 是否遗漏了重要步骤或细节
74
+ 4. 输出格式是否规范
75
+
76
+ 请以 JSON 格式输出检查结果:
77
+ {
78
+ "approved": true 或 false,
79
+ "summary": "检查摘要",
80
+ "issues": ["问题1", "问题2"],
81
+ "suggestions": ["建议1", "建议2"]
82
+ }
83
+
84
+ 如果 approved 为 true,issues 为空即可。
85
+ 如果 approved 为 false,请详细说明问题并给出改进建议。
86
+ 只输出 JSON,不要有其他内容。
87
+ 请用中文回复。"""
88
+
89
+
90
+ _JSON_MAPPER = {}
91
+
92
+
93
+ def _get_role_prompt(role: AgentRole) -> str:
94
+ return {
95
+ AgentRole.PLANNER: _PLANNER_PROMPT,
96
+ AgentRole.WORKER: _WORKER_PROMPT,
97
+ AgentRole.REVIEWER: _REVIEWER_PROMPT,
98
+ }.get(role, _WORKER_PROMPT)
99
+
100
+
101
+ class SubAgent:
102
+ def __init__(self, name: str, role: AgentRole, llm_client: LlmClient, tool_registry: ToolRegistry):
103
+ self._name = name
104
+ self._role = role
105
+ self._llm = llm_client
106
+ self._tool_registry = tool_registry
107
+ self._conversation_history: List[Message] = [Message.system(_get_role_prompt(role))]
108
+
109
+ @property
110
+ def name(self) -> str:
111
+ return self._name
112
+
113
+ @property
114
+ def role(self) -> AgentRole:
115
+ return self._role
116
+
117
+ def execute(self, task: AgentMessage) -> AgentMessage:
118
+ logger.info("[%s] executing task from %s: type=%s", self._name, task.from_agent, task.type.value)
119
+ return self._execute_with_out(task, None)
120
+
121
+ def execute_with_context(self, task: AgentMessage, context: Optional[str] = None) -> AgentMessage:
122
+ if context:
123
+ enriched = f"{context}\n\n当前任务:{task.content}"
124
+ enriched_task = AgentMessage(task.from_agent, task.from_role, enriched, task.type)
125
+ return self._execute_with_out(enriched_task, None)
126
+ return self._execute_with_out(task, None)
127
+
128
+ def review(self, original_task: str, execution_result: str) -> AgentMessage:
129
+ review_input = f"原始任务:{original_task}\n\n执行结果:\n{execution_result}"
130
+ review_task = AgentMessage.task("orchestrator", review_input)
131
+ return self._execute_with_out(review_task, None)
132
+
133
+ def clear_history(self):
134
+ system_msg = self._conversation_history[0]
135
+ self._conversation_history.clear()
136
+ self._conversation_history.append(system_msg)
137
+
138
+ # ---- Internal ----
139
+
140
+ def _execute_with_out(self, task: AgentMessage, _out=None) -> AgentMessage:
141
+ self._conversation_history.append(Message.user(task.content))
142
+ stream_renderer = _SubAgentStreamRenderer(self._name, self._role)
143
+
144
+ start_nanos = time.time()
145
+ budget = AgentBudget.from_env()
146
+ should_use_tools = self._role == AgentRole.WORKER
147
+
148
+ while True:
149
+ exit_reason = budget.check()
150
+ if exit_reason != ExitReason.WITHIN_BUDGET:
151
+ stream_renderer.finish()
152
+ stats = self._format_token_stats(budget.total_input_tokens,
153
+ budget.total_output_tokens, start_nanos)
154
+ print(subtle(stats))
155
+ desc = budget.describe_exit(exit_reason)
156
+ logger.warning("[%s] budget exhausted: reason=%s", self._name, exit_reason)
157
+ return AgentMessage.error(self._name, self._role, desc)
158
+
159
+ budget.begin_iteration()
160
+
161
+ try:
162
+ response = self._llm.chat(
163
+ self._conversation_history,
164
+ self._tool_registry.get_tool_definitions() if should_use_tools else None,
165
+ stream_renderer,
166
+ )
167
+ budget.record_tokens(response.input_tokens or 0, response.output_tokens or 0)
168
+
169
+ if response.has_tool_calls():
170
+ budget.record_tool_calls(response.tool_calls)
171
+ _print_sub_tool_calls(self._name, response.tool_calls)
172
+ self._conversation_history.append(Message.assistant(
173
+ content=response.content or "",
174
+ reasoning_content=response.reasoning_content,
175
+ tool_calls=response.tool_calls,
176
+ ))
177
+ stream_renderer.reset_between_iterations()
178
+ tool_results = self._execute_tool_calls(response.tool_calls)
179
+ for tr in tool_results:
180
+ self._conversation_history.append(Message.tool(tr.id, tr.result))
181
+ continue
182
+
183
+ self._conversation_history.append(Message.assistant(
184
+ content=response.content or "",
185
+ reasoning_content=response.reasoning_content,
186
+ ))
187
+ stream_renderer.finish()
188
+ stats = self._format_token_stats(budget.total_input_tokens,
189
+ budget.total_output_tokens, start_nanos)
190
+ print(subtle(stats))
191
+ return AgentMessage.result(self._name, self._role, response.content or "")
192
+
193
+ except Exception as e:
194
+ logger.error("[%s] LLM call failed", self._name, exc_info=True)
195
+ stream_renderer.finish()
196
+ return AgentMessage.error(self._name, self._role, f"LLM 调用失败: {e}")
197
+
198
+ def _execute_tool_calls(self, tool_calls: List[dict]) -> List:
199
+ invocations = []
200
+ for tc in tool_calls:
201
+ func = tc.get("function", {})
202
+ invocations.append(ToolInvocation(
203
+ tc.get("id", ""), func.get("name", ""), func.get("arguments", "{}")
204
+ ))
205
+ return self._tool_registry.execute_tools(invocations)
206
+
207
+ @staticmethod
208
+ def _format_token_stats(input_tokens: int, output_tokens: int, start: float) -> str:
209
+ elapsed = time.time() - start
210
+ return subtle(
211
+ f"📊 Token: {input_tokens} 输入 / {output_tokens} 输出 / "
212
+ f"{input_tokens + output_tokens} 合计 | ⏱ {elapsed:.1f}s"
213
+ )
214
+
215
+
216
+ def _print_sub_tool_calls(agent_name: str, tool_calls: List[dict]):
217
+ grouped: Dict[str, list] = {}
218
+ for tc in tool_calls:
219
+ name = tc.get("function", {}).get("name", "")
220
+ grouped.setdefault(name, []).append(tc)
221
+ for tool_name, calls in grouped.items():
222
+ label = _sub_tool_label(tool_name, len(calls))
223
+ print(subtle(f" [{agent_name}] {label}"))
224
+ for tc in calls:
225
+ detail = _extract_sub_key_param(tool_name, tc.get("function", {}).get("arguments", "{}"))
226
+ if detail:
227
+ print(subtle(f" └ {detail}"))
228
+
229
+
230
+ def _sub_tool_label(name: str, count: int) -> str:
231
+ labels = {
232
+ "read_file": f"📖 读取 {count} 个文件",
233
+ "write_file": f"✏️ 写入 {count} 个文件",
234
+ "list_dir": f"📂 列出 {count} 个目录",
235
+ "execute_command": f"⚡ 执行 {count} 条命令",
236
+ "create_project": f"🏗️ 创建 {count} 个项目",
237
+ "search_code": f"🔍 搜索代码 {count} 次",
238
+ "web_search": f"🌐 联网搜索 {count} 次",
239
+ "web_fetch": f"📰 抓取 {count} 个网页",
240
+ }
241
+ return labels.get(name, f"🔧 {name} × {count}")
242
+
243
+
244
+ def _extract_sub_key_param(tool_name: str, args_json: str) -> str:
245
+ try:
246
+ args = json.loads(args_json)
247
+ key_map = {
248
+ "read_file": "path", "write_file": "path", "list_dir": "path",
249
+ "execute_command": "command", "create_project": "name",
250
+ "search_code": "query", "web_search": "query", "web_fetch": "url",
251
+ }
252
+ key = key_map.get(tool_name)
253
+ if key and key in args:
254
+ value = str(args[key])
255
+ return value if len(value) <= 80 else value[:77] + "..."
256
+ return ""
257
+ except json.JSONDecodeError:
258
+ return args_json[:80] if len(args_json) > 80 else args_json
259
+
260
+
261
+ class _SubAgentStreamRenderer:
262
+ def __init__(self, agent_name: str, role: AgentRole):
263
+ self._agent_name = agent_name
264
+ self._role = role
265
+ self._pending_reasoning = ""
266
+ self._late_reasoning = ""
267
+ self._reasoning_started = False
268
+ self._content_started = False
269
+ self._streamed_output = False
270
+
271
+ @property
272
+ def _reasoning_label(self) -> str:
273
+ return {
274
+ AgentRole.PLANNER: "规划思考",
275
+ AgentRole.WORKER: "执行思考",
276
+ AgentRole.REVIEWER: "审查思考",
277
+ }.get(self._role, "思考")
278
+
279
+ @property
280
+ def _content_label(self) -> str:
281
+ return {
282
+ AgentRole.PLANNER: "规划结果",
283
+ AgentRole.WORKER: "执行输出",
284
+ AgentRole.REVIEWER: "审查结果",
285
+ }.get(self._role, "输出")
286
+
287
+ def __call__(self, delta: str):
288
+ if delta:
289
+ self._on_content(delta)
290
+
291
+ def on_reasoning_delta(self, delta: str):
292
+ if not delta:
293
+ return
294
+ if self._content_started:
295
+ self._late_reasoning += delta
296
+ return
297
+ if not self._reasoning_started:
298
+ self._pending_reasoning += delta
299
+ if not self._pending_reasoning.strip():
300
+ return
301
+ print(heading(f"🧠 {self._reasoning_label} [{self._agent_name}]"))
302
+ print(self._pending_reasoning, end="", flush=True)
303
+ self._pending_reasoning = ""
304
+ self._reasoning_started = True
305
+ self._streamed_output = True
306
+ else:
307
+ print(delta, end="", flush=True)
308
+
309
+ def on_content_delta(self, delta: str):
310
+ if not delta:
311
+ return
312
+ if not self._content_started:
313
+ if self._pending_reasoning.strip():
314
+ print(heading(f"🧠 {self._reasoning_label} [{self._agent_name}]"))
315
+ print(self._pending_reasoning, end="", flush=True)
316
+ print()
317
+ self._pending_reasoning = ""
318
+ self._reasoning_started = True
319
+ print(section(f"🤖 {self._content_label} [{self._agent_name}]"))
320
+ self._content_started = True
321
+ self._streamed_output = True
322
+ print(delta, end="", flush=True)
323
+
324
+ def _on_content(self, delta: str):
325
+ self.on_content_delta(delta)
326
+
327
+ def reset_between_iterations(self):
328
+ self._pending_reasoning = ""
329
+ late = self._late_reasoning.strip()
330
+ if late:
331
+ print(f"\n{heading(f'🧠 补充思考 [{self._agent_name}]')}")
332
+ print(late)
333
+ self._late_reasoning = ""
334
+ self._streamed_output = True
335
+ self._reasoning_started = False
336
+ self._content_started = False
337
+ if self._streamed_output:
338
+ print()
339
+
340
+ def finish(self):
341
+ late = self._late_reasoning.strip()
342
+ if late:
343
+ print(f"\n{heading(f'🧠 补充思考 [{self._agent_name}]')}")
344
+ print(late)
345
+ self._late_reasoning = ""
346
+ self._streamed_output = True
347
+ if self._streamed_output:
348
+ print("\n")
349
+
350
+ def has_streamed_output(self) -> bool:
351
+ return self._streamed_output