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.
- vox_code-2.0.0.dist-info/METADATA +258 -0
- vox_code-2.0.0.dist-info/RECORD +88 -0
- vox_code-2.0.0.dist-info/WHEEL +4 -0
- vox_code-2.0.0.dist-info/entry_points.txt +3 -0
- voxcli/__init__.py +3 -0
- voxcli/__main__.py +5 -0
- voxcli/agent/__init__.py +12 -0
- voxcli/agent/agent.py +449 -0
- voxcli/agent/agent_budget.py +133 -0
- voxcli/agent/agent_orchestrator.py +414 -0
- voxcli/agent/plan_execute_agent.py +514 -0
- voxcli/agent/roles.py +80 -0
- voxcli/agent/sub_agent.py +351 -0
- voxcli/catalog.py +477 -0
- voxcli/chat.py +91 -0
- voxcli/cli/__init__.py +4 -0
- voxcli/cli/main.py +452 -0
- voxcli/cli/parser.py +71 -0
- voxcli/config.py +518 -0
- voxcli/gui/__main__.py +3 -0
- voxcli/gui/main.py +22 -0
- voxcli/gui/pet/__init__.py +5 -0
- voxcli/gui/pet/base.py +62 -0
- voxcli/gui/pet/coordinator.py +888 -0
- voxcli/gui/pet/data.py +430 -0
- voxcli/gui/pet/widgets.py +683 -0
- voxcli/gui/pet/windows.py +2298 -0
- voxcli/gui/pet/workers.py +54 -0
- voxcli/gui/pet_app.py +7 -0
- voxcli/hitl/__init__.py +11 -0
- voxcli/hitl/handler.py +11 -0
- voxcli/hitl/policy.py +32 -0
- voxcli/hitl/request.py +13 -0
- voxcli/hitl/result.py +11 -0
- voxcli/hitl/terminal_handler.py +64 -0
- voxcli/hitl/tool_registry.py +64 -0
- voxcli/llm/base.py +93 -0
- voxcli/llm/factory.py +178 -0
- voxcli/llm/ollama_client.py +137 -0
- voxcli/llm/openai_compatible.py +249 -0
- voxcli/memory/base.py +16 -0
- voxcli/memory/budget.py +53 -0
- voxcli/memory/compressor.py +198 -0
- voxcli/memory/entry.py +36 -0
- voxcli/memory/long_term.py +126 -0
- voxcli/memory/manager.py +101 -0
- voxcli/memory/retriever.py +72 -0
- voxcli/memory/short_term.py +84 -0
- voxcli/memory/tokenizer.py +21 -0
- voxcli/plan/__init__.py +5 -0
- voxcli/plan/execution_plan.py +225 -0
- voxcli/plan/planner.py +198 -0
- voxcli/plan/task.py +123 -0
- voxcli/policy/audit_log.py +111 -0
- voxcli/policy/command_guard.py +34 -0
- voxcli/policy/exception.py +5 -0
- voxcli/policy/path_guard.py +32 -0
- voxcli/prompting/__init__.py +7 -0
- voxcli/prompting/presenter.py +154 -0
- voxcli/rag/__init__.py +16 -0
- voxcli/rag/analyzer.py +89 -0
- voxcli/rag/chunk.py +17 -0
- voxcli/rag/chunker.py +137 -0
- voxcli/rag/embedding.py +75 -0
- voxcli/rag/formatter.py +40 -0
- voxcli/rag/index.py +96 -0
- voxcli/rag/relation.py +14 -0
- voxcli/rag/retriever.py +58 -0
- voxcli/rag/store.py +155 -0
- voxcli/rag/tokenizer.py +26 -0
- voxcli/runtime/__init__.py +6 -0
- voxcli/runtime/session_controller.py +386 -0
- voxcli/tool/__init__.py +3 -0
- voxcli/tool/tool_registry.py +433 -0
- voxcli/util/animation.py +219 -0
- voxcli/util/ansi.py +82 -0
- voxcli/util/markdown.py +98 -0
- voxcli/web/__init__.py +17 -0
- voxcli/web/base.py +20 -0
- voxcli/web/extractor.py +77 -0
- voxcli/web/factory.py +38 -0
- voxcli/web/fetch_result.py +27 -0
- voxcli/web/fetcher.py +42 -0
- voxcli/web/network_policy.py +49 -0
- voxcli/web/result.py +23 -0
- voxcli/web/searxng.py +55 -0
- voxcli/web/serpapi.py +53 -0
- 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)
|