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,514 @@
1
+ """Plan-and-Execute Agent - 先规划后执行"""
2
+
3
+ import json
4
+ import time
5
+ import logging
6
+ from concurrent.futures import ThreadPoolExecutor, Future
7
+ from io import StringIO
8
+ from typing import List, Optional, Dict, Set, Callable
9
+ from enum import Enum
10
+
11
+ from ..llm.base import LlmClient, Message, ToolCall
12
+ from ..memory.manager import MemoryManager
13
+ from ..plan import Planner, ExecutionPlan, PlanStatus, Task, TaskStatus, TaskType
14
+ from ..tool import ToolRegistry, ToolInvocation
15
+ from ..util.ansi import heading, section, subtle
16
+ from .agent_budget import AgentBudget
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _MAX_TASK_ITERATIONS = 5
21
+
22
+ _EXECUTION_PROMPT = """你是一个任务执行专家。请根据当前任务和上下文,选择合适的工具或生成回复。
23
+
24
+ 当前任务类型:%s
25
+ 任务描述:%s
26
+
27
+ 可用工具:
28
+ 1. read_file - 读取文件内容,参数:{"path": "文件路径"}
29
+ 2. write_file - 写入文件内容,参数:{"path": "文件路径", "content": "内容"}
30
+ 3. list_dir - 列出目录内容,参数:{"path": "目录路径"}
31
+ 4. execute_command - 执行命令,参数:{"command": "命令"}
32
+ 5. create_project - 创建项目,参数:{"name": "名称", "type": "java|python|node"}
33
+ 6. search_code - 语义检索代码库,参数:{"query": "自然语言描述", "top_k": 5}
34
+ 7. web_search - 搜索互联网获取实时信息,参数:{"query": "搜索关键词", "top_k": 5}
35
+ 8. web_fetch - 抓取已知 URL 并返回正文 Markdown,参数:{"url": "https://...", "max_chars": 8000}
36
+
37
+ 如果任务涉及理解代码库(如分析代码结构、查找实现位置),请优先使用 search_code 工具。
38
+ 如果任务需要实时互联网信息(如查询框架最新版本、官方文档),请使用 web_search 找入口,
39
+ 拿到具体 URL 后用 web_fetch 抓取全文。已经有 URL 时直接 web_fetch,不要再 web_search 一次。
40
+ web_fetch 拿到空正文(SPA / 防爬墙)时,明确告知用户这是已知边界,不要反复重试。
41
+ 对于当前项目内的文件,请优先使用 read_file 或 list_dir,不要用 execute_command 扫描 /、~ 或整个文件系统。
42
+ execute_command 只适合在当前项目目录执行短时命令。
43
+ 安全策略硬规则(HITL 之外的兜底,无法绕过):read_file / write_file / list_dir / create_project 必须在项目根之内;write_file 单文件 5MB 上限;
44
+ execute_command 禁止 sudo / rm -rf 全盘 / mkfs / dd of=/dev / fork bomb / curl|sh / find / / chmod 777 / / shutdown。
45
+ 被策略拒绝的工具调用("🛡️ 策略拒绝" 开头)不要原样重试,改用项目内相对路径或更安全的命令。
46
+ 同一轮返回多个工具调用时,系统会并行执行这些工具;如果工具之间有依赖关系,请分多轮调用。
47
+ 如果是 ANALYSIS 或 VERIFICATION 类型任务,请直接输出分析结果,不需要调用工具。
48
+
49
+ 请用中文回复。"""
50
+
51
+
52
+ class PlanReviewAction(Enum):
53
+ EXECUTE = "EXECUTE"
54
+ SUPPLEMENT = "SUPPLEMENT"
55
+ CANCEL = "CANCEL"
56
+
57
+
58
+ class PlanReviewDecision:
59
+ def __init__(self, action: PlanReviewAction, feedback: Optional[str] = None):
60
+ self.action = action
61
+ self.feedback = feedback
62
+
63
+ @staticmethod
64
+ def execute() -> "PlanReviewDecision":
65
+ return PlanReviewDecision(PlanReviewAction.EXECUTE)
66
+
67
+ @staticmethod
68
+ def supplement(feedback: str) -> "PlanReviewDecision":
69
+ return PlanReviewDecision(PlanReviewAction.SUPPLEMENT, feedback)
70
+
71
+ @staticmethod
72
+ def cancel() -> "PlanReviewDecision":
73
+ return PlanReviewDecision(PlanReviewAction.CANCEL)
74
+
75
+
76
+ class PlanReviewHandler:
77
+ def review(self, goal: str, plan: ExecutionPlan) -> PlanReviewDecision:
78
+ return PlanReviewDecision.execute()
79
+
80
+
81
+ class PlanExecuteAgent:
82
+ def __init__(self, llm_client: LlmClient,
83
+ tool_registry: Optional[ToolRegistry] = None,
84
+ planner: Optional[Planner] = None,
85
+ memory_manager: Optional[MemoryManager] = None,
86
+ review_handler: Optional[PlanReviewHandler] = None):
87
+ self._llm = llm_client
88
+ self._tool_registry = tool_registry or ToolRegistry()
89
+ self._planner = planner or Planner(llm_client)
90
+ self._memory_manager = memory_manager or MemoryManager(llm_client)
91
+ self._review_handler = review_handler or PlanReviewHandler()
92
+
93
+ @property
94
+ def memory_manager(self) -> MemoryManager:
95
+ return self._memory_manager
96
+
97
+ @property
98
+ def tool_registry(self) -> ToolRegistry:
99
+ return self._tool_registry
100
+
101
+ # ---- Public API ----
102
+
103
+ def run(self, user_input: str) -> str:
104
+ logger.info("Plan run started: inputLength=%d", len(user_input) if user_input else 0)
105
+ self._memory_manager.add_user_message(user_input)
106
+ stream_state = _StreamState()
107
+
108
+ try:
109
+ outcome = self._run_with_plan(user_input, stream_state)
110
+ if outcome.persist and outcome.result:
111
+ self._memory_manager.add_assistant_message("[计划结果] " + outcome.result)
112
+ if stream_state.has_streamed and (not outcome.result):
113
+ return ""
114
+ return outcome.result or ""
115
+ except Exception as e:
116
+ logger.error("Plan run failed", exc_info=True)
117
+ msg = f"❌ 执行失败: {e}"
118
+ self._memory_manager.add_assistant_message(msg)
119
+ return msg
120
+
121
+ # ---- Internal ----
122
+
123
+ def _run_with_plan(self, goal: str, stream_state: "_StreamState"):
124
+ plan = self._planner.create_plan(goal)
125
+ return self._review_and_execute(plan, stream_state)
126
+
127
+ def _review_and_execute(self, plan: ExecutionPlan, stream_state: "_StreamState"):
128
+ while True:
129
+ decision = self._review_handler.review(plan.goal, plan)
130
+ if decision is None or decision.action == PlanReviewAction.EXECUTE:
131
+ return _PlanOutcome.executed(self._execute_plan(plan, stream_state))
132
+ if decision.action == PlanReviewAction.CANCEL:
133
+ return _PlanOutcome.canceled("⏹️ 已取消本次计划执行。")
134
+ feedback = (decision.feedback or "").strip()
135
+ if not feedback:
136
+ return _PlanOutcome.executed(self._execute_plan(plan, stream_state))
137
+ print("📝 已收到补充要求,正在重新规划...\n")
138
+ plan = self._planner.create_plan(f"{plan.goal}\n补充要求:{feedback}")
139
+
140
+ def _execute_plan(self, plan: ExecutionPlan, stream_state: "_StreamState") -> str:
141
+ logger.info("Executing plan: goal='%s', taskCount=%d", plan.goal, len(plan.get_all_tasks()))
142
+ print("🚀 开始执行计划...\n")
143
+ plan.mark_started()
144
+
145
+ final_result_parts = []
146
+ streamed_outputs: Dict[str, bool] = {}
147
+
148
+ while True:
149
+ executable = self._get_executable_in_order(plan)
150
+ if not executable:
151
+ break
152
+
153
+ batch_results = self._execute_task_batch(plan, executable, stream_state)
154
+ for br in batch_results:
155
+ task = br.task
156
+ if not br.failed:
157
+ task.mark_completed(br.result or "")
158
+ streamed_outputs[task.id] = br.streamed
159
+ logger.info("Task completed: %s status=%s resultChars=%d",
160
+ task.id, task.status, len(br.result or ""))
161
+ if br.streamed or not br.result:
162
+ print(f"✅ 完成 [{task.id}]\n")
163
+ else:
164
+ preview = br.result[:100] if len(br.result) > 100 else br.result
165
+ print(f"✅ 完成 [{task.id}]: {preview}\n")
166
+ else:
167
+ err_msg = str(br.error) if br.error else "未知错误"
168
+ task.mark_failed(err_msg)
169
+ logger.warning("Task failed: %s error=%s", task.id, err_msg)
170
+ print(f"❌ 失败 [{task.id}]: {err_msg}\n")
171
+ if plan.get_progress() < 0.5:
172
+ print("🔄 尝试重新规划...\n")
173
+ replanned = self._planner.replan(plan, err_msg)
174
+ return self._review_and_execute(replanned, stream_state).result or ""
175
+ final_result_parts.append(f"任务 {task.id} 失败: {err_msg}")
176
+
177
+ if not plan.is_all_completed() and not plan.has_failed():
178
+ plan.mark_failed()
179
+ return "⚠️ 计划未能继续推进,存在未满足依赖的任务。"
180
+
181
+ summary = "\n".join(final_result_parts) if final_result_parts else \
182
+ self._build_final_result(plan, streamed_outputs)
183
+
184
+ if plan.has_failed():
185
+ plan.mark_failed()
186
+ return f"⚠️ 计划部分完成,有任务失败。\n{summary}" if summary else "⚠️ 计划部分完成,有任务失败。"
187
+
188
+ plan.mark_completed()
189
+ return f"✅ 计划执行完成!\n{summary}" if summary else "✅ 计划执行完成!"
190
+
191
+ def _get_executable_in_order(self, plan: ExecutionPlan) -> List[Task]:
192
+ executable_ids = {t.id for t in plan.get_executable_tasks()}
193
+ return [plan.get_task(tid) for tid in plan.get_execution_order()
194
+ if tid in executable_ids and plan.get_task(tid) is not None]
195
+
196
+ def _execute_task_batch(self, plan: ExecutionPlan, tasks: List[Task],
197
+ stream_state: "_StreamState") -> List["_TaskExecResult"]:
198
+ if len(tasks) == 1:
199
+ task = tasks[0]
200
+ logger.info("Executing single task: %s type=%s", task.id, task.type.value)
201
+ print(f"▶️ 执行任务 [{task.id}]: {task.description}")
202
+ task.mark_started()
203
+ try:
204
+ result = self._execute_single_task(plan.goal, plan, task, stream_state)
205
+ return [_TaskExecResult.success(task, result)]
206
+ except Exception as e:
207
+ return [_TaskExecResult.failure(task, e)]
208
+
209
+ ids = ", ".join(t.id for t in tasks)
210
+ logger.info("Executing parallel batch: %s", ids)
211
+ print(f"⚡ 本轮并行执行 {len(tasks)} 个任务: {ids}")
212
+
213
+ parallelism = min(len(tasks), 4)
214
+ results: List[Optional[_TaskExecResult]] = [None] * len(tasks)
215
+
216
+ with ThreadPoolExecutor(max_workers=parallelism) as executor:
217
+ future_map = {}
218
+ for i, task in enumerate(tasks):
219
+ print(f"▶️ 并行任务 [{task.id}]: {task.description}")
220
+ task.mark_started()
221
+ buf = StringIO()
222
+ future = executor.submit(self._execute_single_task, plan.goal, plan, task, stream_state)
223
+ future_map[future] = i
224
+
225
+ for future in future_map:
226
+ idx = future_map[future]
227
+ task = tasks[idx]
228
+ try:
229
+ result = future.result()
230
+ results[idx] = _TaskExecResult.success(task, result)
231
+ except Exception as e:
232
+ results[idx] = _TaskExecResult.failure(task, e)
233
+
234
+ return [r for r in results if r is not None]
235
+
236
+ def _execute_single_task(self, goal: str, plan: ExecutionPlan, task: Task,
237
+ stream_state: "_StreamState"):
238
+ prompt = _EXECUTION_PROMPT % (task.type.value, task.description)
239
+ memory_context = self._memory_manager.build_context_for_query(task.description, 300)
240
+ task_input = self._build_task_context(goal, plan, task)
241
+ if memory_context:
242
+ task_input += f"\n\n{memory_context}"
243
+
244
+ messages = [
245
+ Message.system(prompt),
246
+ Message.user(task_input),
247
+ ]
248
+
249
+ all_results = []
250
+ renderer = _TaskStreamRenderer(task.id, stream_state)
251
+ total_input = 0
252
+ total_output = 0
253
+ start = time.time()
254
+
255
+ for iteration in range(_MAX_TASK_ITERATIONS):
256
+ response = self._llm.chat(
257
+ messages,
258
+ self._tool_registry.get_tool_definitions(),
259
+ renderer,
260
+ )
261
+ total_input += response.input_tokens or 0
262
+ total_output += response.output_tokens or 0
263
+
264
+ logger.info("Task %s iteration %d: toolCalls=%d, reasoningChars=%d, contentChars=%d",
265
+ task.id, iteration + 1,
266
+ len(response.tool_calls) if response.tool_calls else 0,
267
+ len(response.reasoning_content or ""), len(response.content or ""))
268
+
269
+ if not response.has_tool_calls():
270
+ self._memory_manager.record_token_usage(total_input, total_output)
271
+ if response.content:
272
+ self._memory_manager.add_assistant_message(
273
+ f"[计划任务 {task.id}] {response.content}")
274
+ renderer.finish()
275
+ stats = subtle(
276
+ f"📊 Token: {total_input} 输入 / {total_output} 输出 / "
277
+ f"{total_input + total_output} 合计 | ⏱ {time.time() - start:.1f}s")
278
+ print(stats)
279
+ return _TaskRunResult(response.content or "", renderer.has_streamed_output)
280
+
281
+ all_results.append(response.content or "")
282
+ _print_task_tool_calls(task.id, response.tool_calls)
283
+ messages.append(Message.assistant(
284
+ content=response.content or "",
285
+ reasoning_content=response.reasoning_content,
286
+ tool_calls=response.tool_calls,
287
+ ))
288
+ renderer.reset_between_iterations()
289
+
290
+ tool_results = self._execute_task_tool_calls(task.id, response.tool_calls)
291
+ for tr in tool_results:
292
+ self._memory_manager.add_tool_result(tr.name, tr.result)
293
+ all_results.append(tr.result)
294
+ messages.append(Message.tool(tr.id, tr.result))
295
+
296
+ fallback = "\n".join(r for r in all_results if r).strip()
297
+ if fallback:
298
+ self._memory_manager.add_assistant_message(f"[计划任务 {task.id}] {fallback}")
299
+ renderer.finish()
300
+ stats = subtle(
301
+ f"📊 Token: {total_input} 输入 / {total_output} 输出 / "
302
+ f"{total_input + total_output} 合计 | ⏱ {time.time() - start:.1f}s")
303
+ print(stats)
304
+ return _TaskRunResult(fallback, renderer.has_streamed_output)
305
+
306
+ def _execute_task_tool_calls(self, task_id: str, tool_calls: List[dict]) -> List:
307
+ invocations = []
308
+ for tc in tool_calls:
309
+ func = tc.get("function", {})
310
+ invocations.append(ToolInvocation(
311
+ tc.get("id", ""), func.get("name", ""), func.get("arguments", "{}")
312
+ ))
313
+ if len(invocations) > 1:
314
+ logger.info("Task %s executing %d tool calls in parallel", task_id, len(invocations))
315
+ return self._tool_registry.execute_tools(invocations)
316
+
317
+ @staticmethod
318
+ def _build_task_context(goal: str, plan: ExecutionPlan, task: Task) -> str:
319
+ parts = [f"总目标:{goal}", f"当前任务:{task.description}"]
320
+ if not task.dependencies:
321
+ parts.append("依赖任务:无")
322
+ else:
323
+ parts.append("依赖任务结果:")
324
+ for dep_id in task.dependencies:
325
+ dep = plan.get_task(dep_id)
326
+ if dep is None:
327
+ continue
328
+ parts.append(f"- {dep.id} / {dep.description} / 状态={dep.status.value}")
329
+ if dep.result:
330
+ parts.append(dep.result)
331
+ parts.append("请执行此任务。如果是 ANALYSIS 或 VERIFICATION 类型,请基于以上上下文直接给出结果。")
332
+ return "\n".join(parts)
333
+
334
+ @staticmethod
335
+ def _build_final_result(plan: ExecutionPlan, streamed: Dict[str, bool]) -> str:
336
+ leaf_tasks = [t for t in plan.get_all_tasks() if not t.dependents]
337
+
338
+ results = []
339
+ for t in leaf_tasks:
340
+ if streamed.get(t.id):
341
+ continue
342
+ if t.result:
343
+ results.append(f"[{t.id}] {t.result}")
344
+
345
+ if results:
346
+ return "\n".join(results)
347
+
348
+ for t in plan.get_all_tasks():
349
+ if not streamed.get(t.id) and t.result:
350
+ results.append(f"[{t.id}] {t.result}")
351
+
352
+ return results[-1] if results else ""
353
+
354
+
355
+ class _StreamState:
356
+ def __init__(self):
357
+ self.has_streamed = False
358
+
359
+ def mark(self):
360
+ self.has_streamed = True
361
+
362
+
363
+ class _PlanOutcome:
364
+ def __init__(self, result: Optional[str], persist: bool):
365
+ self.result = result
366
+ self.persist = persist
367
+
368
+ @staticmethod
369
+ def executed(result: str) -> "_PlanOutcome":
370
+ return _PlanOutcome(result, True)
371
+
372
+ @staticmethod
373
+ def canceled(result: str) -> "_PlanOutcome":
374
+ return _PlanOutcome(result, False)
375
+
376
+ @staticmethod
377
+ def failed(result: str) -> "_PlanOutcome":
378
+ return _PlanOutcome(result, True)
379
+
380
+
381
+ class _TaskRunResult:
382
+ def __init__(self, result: str, streamed: bool):
383
+ self.result = result
384
+ self.streamed = streamed
385
+
386
+ @staticmethod
387
+ def of(result: str, streamed: bool) -> "_TaskRunResult":
388
+ return _TaskRunResult(result, streamed)
389
+
390
+
391
+ class _TaskExecResult:
392
+ def __init__(self, task: Task, result: Optional[str] = None,
393
+ streamed: bool = False, error: Optional[Exception] = None):
394
+ self.task = task
395
+ self.result = result
396
+ self.streamed = streamed
397
+ self.error = error
398
+
399
+ @property
400
+ def failed(self) -> bool:
401
+ return self.error is not None
402
+
403
+ @staticmethod
404
+ def success(task: Task, run_result: _TaskRunResult) -> "_TaskExecResult":
405
+ return _TaskExecResult(task, run_result.result, run_result.streamed, None)
406
+
407
+ @staticmethod
408
+ def failure(task: Task, error: Exception) -> "_TaskExecResult":
409
+ return _TaskExecResult(task, None, False, error)
410
+
411
+
412
+ class _TaskStreamRenderer:
413
+ def __init__(self, task_id: str, stream_state: _StreamState):
414
+ self._task_id = task_id
415
+ self._stream_state = stream_state
416
+ self._pending = ""
417
+ self._late = ""
418
+ self._reasoning_started = False
419
+ self._content_started = False
420
+ self._has_streamed = False
421
+
422
+ @property
423
+ def has_streamed_output(self) -> bool:
424
+ return self._has_streamed
425
+
426
+ def __call__(self, delta: str):
427
+ if delta:
428
+ self._on_content(delta)
429
+
430
+ def on_reasoning_delta(self, delta: str):
431
+ if not delta:
432
+ return
433
+ if self._content_started:
434
+ self._late += delta
435
+ return
436
+ if not self._reasoning_started:
437
+ self._pending += delta
438
+ if not self._pending.strip():
439
+ return
440
+ print(heading(f"🧠 任务思考 [{self._task_id}]"))
441
+ print(self._pending, end="", flush=True)
442
+ self._pending = ""
443
+ self._reasoning_started = True
444
+ self._has_streamed = True
445
+ self._stream_state.mark()
446
+ else:
447
+ print(delta, end="", flush=True)
448
+
449
+ def on_content_delta(self, delta: str):
450
+ if not delta:
451
+ return
452
+ if not self._content_started:
453
+ if self._pending.strip():
454
+ print(heading(f"🧠 任务思考 [{self._task_id}]"))
455
+ print(self._pending, end="", flush=True)
456
+ print()
457
+ self._pending = ""
458
+ self._reasoning_started = True
459
+ print(section(f"🤖 任务输出 [{self._task_id}]"))
460
+ self._content_started = True
461
+ self._has_streamed = True
462
+ self._stream_state.mark()
463
+ print(delta, end="", flush=True)
464
+
465
+ def _on_content(self, delta: str):
466
+ self.on_content_delta(delta)
467
+
468
+ def reset_between_iterations(self):
469
+ self._pending = ""
470
+ self._flush_late()
471
+ self._reasoning_started = False
472
+ self._content_started = False
473
+ if self._has_streamed:
474
+ print()
475
+
476
+ def finish(self):
477
+ if self._has_streamed:
478
+ self._flush_late()
479
+ print("\n")
480
+
481
+ def _flush_late(self):
482
+ late = self._late.strip()
483
+ if late:
484
+ print(f"\n{heading(f'🧠 补充思考 [{self._task_id}]')}")
485
+ print(late)
486
+ self._late = ""
487
+
488
+ def has_streamed_output(self) -> bool:
489
+ return self._has_streamed
490
+
491
+
492
+ def _print_task_tool_calls(task_id: str, tool_calls: List[dict]):
493
+ for tc in tool_calls:
494
+ name = tc.get("function", {}).get("name", "")
495
+ args = tc.get("function", {}).get("arguments", "{}")
496
+ detail = _extract_task_param(name, args)
497
+ print(subtle(f" [{task_id}] 🔧 {name}: {detail}"))
498
+
499
+
500
+ def _extract_task_param(tool_name: str, args_json: str) -> str:
501
+ try:
502
+ args = json.loads(args_json)
503
+ key_map = {
504
+ "read_file": "path", "write_file": "path", "list_dir": "path",
505
+ "execute_command": "command", "create_project": "name",
506
+ "search_code": "query", "web_search": "query", "web_fetch": "url",
507
+ }
508
+ key = key_map.get(tool_name)
509
+ if key and key in args:
510
+ val = str(args[key])
511
+ return val if len(val) <= 60 else val[:57] + "..."
512
+ return str(args)[:60]
513
+ except json.JSONDecodeError:
514
+ return args_json[:60]
voxcli/agent/roles.py ADDED
@@ -0,0 +1,80 @@
1
+ """Agent 角色定义与通信消息"""
2
+
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+
7
+ class AgentRole(Enum):
8
+ PLANNER = ("规划者", "负责分析用户任务,制定执行计划,将复杂任务拆解为可执行的子任务")
9
+ WORKER = ("执行者", "负责执行具体任务步骤,调用工具完成文件操作、命令执行等操作")
10
+ REVIEWER = ("检查者", "负责检查执行结果的质量和正确性,提供改进建议")
11
+
12
+ def __init__(self, display_name: str, description: str):
13
+ self._display_name = display_name
14
+ self._description = description
15
+
16
+ @property
17
+ def display_name(self) -> str:
18
+ return self._display_name
19
+
20
+ @property
21
+ def description(self) -> str:
22
+ return self._description
23
+
24
+
25
+ class AgentMessageType(Enum):
26
+ TASK = "TASK"
27
+ RESULT = "RESULT"
28
+ FEEDBACK = "FEEDBACK"
29
+ APPROVAL = "APPROVAL"
30
+ REJECTION = "REJECTION"
31
+ ERROR = "ERROR"
32
+
33
+
34
+ class AgentMessage:
35
+ def __init__(self, from_agent: str, from_role: Optional[AgentRole],
36
+ content: str, type: AgentMessageType):
37
+ self._from_agent = from_agent
38
+ self._from_role = from_role
39
+ self._content = content
40
+ self._type = type
41
+
42
+ @property
43
+ def from_agent(self) -> str:
44
+ return self._from_agent
45
+
46
+ @property
47
+ def from_role(self) -> Optional[AgentRole]:
48
+ return self._from_role
49
+
50
+ @property
51
+ def content(self) -> str:
52
+ return self._content
53
+
54
+ @property
55
+ def type(self) -> AgentMessageType:
56
+ return self._type
57
+
58
+ @staticmethod
59
+ def task(from_agent: str, content: str) -> "AgentMessage":
60
+ return AgentMessage(from_agent, None, content, AgentMessageType.TASK)
61
+
62
+ @staticmethod
63
+ def result(from_agent: str, role: AgentRole, content: str) -> "AgentMessage":
64
+ return AgentMessage(from_agent, role, content, AgentMessageType.RESULT)
65
+
66
+ @staticmethod
67
+ def feedback(from_agent: str, content: str) -> "AgentMessage":
68
+ return AgentMessage(from_agent, AgentRole.REVIEWER, content, AgentMessageType.FEEDBACK)
69
+
70
+ @staticmethod
71
+ def approval(from_agent: str, content: str) -> "AgentMessage":
72
+ return AgentMessage(from_agent, AgentRole.REVIEWER, content, AgentMessageType.APPROVAL)
73
+
74
+ @staticmethod
75
+ def rejection(from_agent: str, content: str) -> "AgentMessage":
76
+ return AgentMessage(from_agent, AgentRole.REVIEWER, content, AgentMessageType.REJECTION)
77
+
78
+ @staticmethod
79
+ def error(from_agent: str, role: AgentRole, content: str) -> "AgentMessage":
80
+ return AgentMessage(from_agent, role, content, AgentMessageType.ERROR)