illusion-code 0.1.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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
"""
|
|
2
|
+
代理执行器模块
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
本模块提供子代理派发和执行的核心功能,对齐标准 AgentTool 架构。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- AgentExecutionContext: 代理运行时上下文
|
|
9
|
+
- AgentAbortController: 代理中止控制器
|
|
10
|
+
- TaskNotification: 任务通知数据类
|
|
11
|
+
- run_agent_in_process: 进程内代理执行
|
|
12
|
+
- run_agent_subprocess: 子进程代理执行
|
|
13
|
+
- resolve_agent_tools: 根据代理定义组装工具池
|
|
14
|
+
- format_task_notification / parse_task_notification: XML 序列化
|
|
15
|
+
|
|
16
|
+
架构概述:
|
|
17
|
+
代理通过 AgentTool 派发,分为同步(前台)和异步(后台)两种模式。
|
|
18
|
+
同步模式直接返回代理最终文本;异步模式通过 task-notification XML 通知完成。
|
|
19
|
+
代理间通信通过内存中的 asyncio.Queue 实现。
|
|
20
|
+
|
|
21
|
+
使用示例:
|
|
22
|
+
>>> from illusion.swarm.agent_executor import run_agent_in_process, AgentSpawnConfig
|
|
23
|
+
>>> config = AgentSpawnConfig(...)
|
|
24
|
+
>>> result = await run_agent_in_process(config, query_context)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import contextlib
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import shlex
|
|
35
|
+
import shutil
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
import uuid
|
|
39
|
+
from contextvars import ContextVar
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any, Literal
|
|
43
|
+
|
|
44
|
+
from illusion.coordinator.agent_definitions import AgentDefinition
|
|
45
|
+
from illusion.engine.messages import ConversationMessage
|
|
46
|
+
from illusion.tools.base import ToolRegistry
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# 代理中止控制器
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AgentAbortController:
|
|
57
|
+
"""代理的双重信号中止控制器。
|
|
58
|
+
|
|
59
|
+
提供 *优雅* 取消(设置 ``cancel_event``;代理完成当前工具使用后退出)
|
|
60
|
+
和 *强制* 终止(设置 ``force_cancel``;立即取消)。
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self.cancel_event: asyncio.Event = asyncio.Event()
|
|
65
|
+
"""设置为请求代理循环的优雅取消。"""
|
|
66
|
+
|
|
67
|
+
self.force_cancel: asyncio.Event = asyncio.Event()
|
|
68
|
+
"""设置为请求立即(强制)终止。"""
|
|
69
|
+
|
|
70
|
+
self._reason: str | None = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_cancelled(self) -> bool:
|
|
74
|
+
"""如果任一取消信号已设置则返回 True。"""
|
|
75
|
+
return self.cancel_event.is_set() or self.force_cancel.is_set()
|
|
76
|
+
|
|
77
|
+
def request_cancel(self, reason: str | None = None, *, force: bool = False) -> None:
|
|
78
|
+
"""请求取消代理。
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
reason: 取消的人类可读原因。
|
|
82
|
+
force: 当为 True 时,设置 ``force_cancel`` 以立即终止。
|
|
83
|
+
"""
|
|
84
|
+
self._reason = reason
|
|
85
|
+
if force:
|
|
86
|
+
self.force_cancel.set()
|
|
87
|
+
self.cancel_event.set()
|
|
88
|
+
else:
|
|
89
|
+
self.cancel_event.set()
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def reason(self) -> str | None:
|
|
93
|
+
"""最近一次取消请求的原因。"""
|
|
94
|
+
return self._reason
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# 代理执行上下文
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
# 代理状态类型
|
|
102
|
+
AgentStatus = Literal["starting", "running", "idle", "stopped"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class AgentExecutionContext:
|
|
107
|
+
"""代理运行时状态,存储在 ContextVar 中实现每个 asyncio Task 隔离。"""
|
|
108
|
+
|
|
109
|
+
agent_id: str
|
|
110
|
+
"""唯一代理标识符。"""
|
|
111
|
+
|
|
112
|
+
agent_name: str
|
|
113
|
+
"""人类可读名称,例如 ``"researcher"``。"""
|
|
114
|
+
|
|
115
|
+
agent_definition: AgentDefinition | None = None
|
|
116
|
+
"""代理定义(如果使用 subagent_type 派发)。"""
|
|
117
|
+
|
|
118
|
+
prompt: str = ""
|
|
119
|
+
"""代理的初始提示词。"""
|
|
120
|
+
|
|
121
|
+
model: str | None = None
|
|
122
|
+
"""模型覆盖。"""
|
|
123
|
+
|
|
124
|
+
cwd: Path = field(default_factory=lambda: Path.cwd())
|
|
125
|
+
"""工作目录。"""
|
|
126
|
+
|
|
127
|
+
permission_mode: str | None = None
|
|
128
|
+
"""权限模式覆盖。"""
|
|
129
|
+
|
|
130
|
+
abort_controller: AgentAbortController = field(default_factory=AgentAbortController)
|
|
131
|
+
"""中止控制器。"""
|
|
132
|
+
|
|
133
|
+
message_queue: asyncio.Queue[TeammateMessage] = field(default_factory=asyncio.Queue)
|
|
134
|
+
"""回合之间传递的待处理消息队列。"""
|
|
135
|
+
|
|
136
|
+
status: AgentStatus = "starting"
|
|
137
|
+
"""此代理的生命周期状态。"""
|
|
138
|
+
|
|
139
|
+
started_at: float = field(default_factory=time.time)
|
|
140
|
+
"""代理生成时的 Unix 时间戳。"""
|
|
141
|
+
|
|
142
|
+
tool_use_count: int = 0
|
|
143
|
+
"""此代理生命周期内调用的工具数量。"""
|
|
144
|
+
|
|
145
|
+
total_tokens: int = 0
|
|
146
|
+
"""所有查询回合的累计 token 计数。"""
|
|
147
|
+
|
|
148
|
+
output_file: Path | None = None
|
|
149
|
+
"""后台任务的输出文件路径。"""
|
|
150
|
+
|
|
151
|
+
task_id: str | None = None
|
|
152
|
+
"""任务管理器中的任务 ID。"""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# 代理上下文变量
|
|
156
|
+
_agent_context_var: ContextVar[AgentExecutionContext | None] = ContextVar(
|
|
157
|
+
"_agent_context_var", default=None
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_agent_context() -> AgentExecutionContext | None:
|
|
162
|
+
"""返回当前运行的代理的 :class:`AgentExecutionContext`。"""
|
|
163
|
+
return _agent_context_var.get()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def set_agent_context(ctx: AgentExecutionContext) -> None:
|
|
167
|
+
"""将 *ctx* 绑定到当前异步上下文。"""
|
|
168
|
+
_agent_context_var.set(ctx)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# 活跃代理注册表(内存)
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
# 映射 agent_id -> AgentExecutionContext
|
|
176
|
+
_active_agents: dict[str, AgentExecutionContext] = {}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_active_agent(agent_id: str) -> AgentExecutionContext | None:
|
|
180
|
+
"""按 ID 查找活跃代理。"""
|
|
181
|
+
return _active_agents.get(agent_id)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_active_agent_by_name(name: str) -> AgentExecutionContext | None:
|
|
185
|
+
"""按名称查找活跃代理。"""
|
|
186
|
+
for ctx in _active_agents.values():
|
|
187
|
+
if ctx.agent_name == name:
|
|
188
|
+
return ctx
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def list_active_agents() -> list[AgentExecutionContext]:
|
|
193
|
+
"""返回所有活跃代理。"""
|
|
194
|
+
return list(_active_agents.values())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _register_agent(ctx: AgentExecutionContext) -> None:
|
|
198
|
+
"""注册代理到活跃注册表。"""
|
|
199
|
+
_active_agents[ctx.agent_id] = ctx
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _unregister_agent(agent_id: str) -> None:
|
|
203
|
+
"""从活跃注册表中移除代理。"""
|
|
204
|
+
_active_agents.pop(agent_id, None)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# 消息类型
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class TeammateMessage:
|
|
214
|
+
"""发送给代理的消息。"""
|
|
215
|
+
|
|
216
|
+
text: str
|
|
217
|
+
from_agent: str
|
|
218
|
+
color: str | None = None
|
|
219
|
+
timestamp: str | None = None
|
|
220
|
+
summary: str | None = None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# 任务通知
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class TaskNotification:
|
|
230
|
+
"""已完成代理任务的结构化结果。"""
|
|
231
|
+
|
|
232
|
+
task_id: str
|
|
233
|
+
"""任务 ID。"""
|
|
234
|
+
|
|
235
|
+
status: str
|
|
236
|
+
"""状态 (completed/failed/killed)。"""
|
|
237
|
+
|
|
238
|
+
summary: str
|
|
239
|
+
"""人类可读的状态摘要。"""
|
|
240
|
+
|
|
241
|
+
result: str | None = None
|
|
242
|
+
"""代理的最终文本响应。"""
|
|
243
|
+
|
|
244
|
+
usage: dict[str, int] | None = None
|
|
245
|
+
"""使用统计信息。"""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# 使用统计字段名
|
|
249
|
+
_USAGE_FIELDS = ("total_tokens", "tool_uses", "duration_ms")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def format_task_notification(n: TaskNotification) -> str:
|
|
253
|
+
"""将 TaskNotification 序列化为标准 XML envelope。"""
|
|
254
|
+
parts = [
|
|
255
|
+
"<task-notification>",
|
|
256
|
+
f"<task-id>{n.task_id}</task-id>",
|
|
257
|
+
f"<status>{n.status}</status>",
|
|
258
|
+
f"<summary>{n.summary}</summary>",
|
|
259
|
+
]
|
|
260
|
+
if n.result is not None:
|
|
261
|
+
parts.append(f"<result>{n.result}</result>")
|
|
262
|
+
if n.usage:
|
|
263
|
+
parts.append("<usage>")
|
|
264
|
+
for key in _USAGE_FIELDS:
|
|
265
|
+
if key in n.usage:
|
|
266
|
+
parts.append(f" <{key}>{n.usage[key]}</{key}>")
|
|
267
|
+
parts.append("</usage>")
|
|
268
|
+
parts.append("</task-notification>")
|
|
269
|
+
return "\n".join(parts)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def parse_task_notification(xml: str) -> TaskNotification:
|
|
273
|
+
"""从 XML 字符串解析 TaskNotification。"""
|
|
274
|
+
|
|
275
|
+
def _extract(tag: str) -> str | None:
|
|
276
|
+
m = re.search(rf"<{tag}>(.*?)</{tag}>", xml, re.DOTALL)
|
|
277
|
+
return m.group(1).strip() if m else None
|
|
278
|
+
|
|
279
|
+
task_id = _extract("task-id") or ""
|
|
280
|
+
status = _extract("status") or ""
|
|
281
|
+
summary = _extract("summary") or ""
|
|
282
|
+
result = _extract("result")
|
|
283
|
+
|
|
284
|
+
usage: dict[str, int] | None = None
|
|
285
|
+
usage_block = re.search(r"<usage>(.*?)</usage>", xml, re.DOTALL)
|
|
286
|
+
if usage_block:
|
|
287
|
+
usage = {}
|
|
288
|
+
for key in _USAGE_FIELDS:
|
|
289
|
+
m = re.search(rf"<{key}>(\d+)</{key}>", usage_block.group(1))
|
|
290
|
+
if m:
|
|
291
|
+
usage[key] = int(m.group(1))
|
|
292
|
+
|
|
293
|
+
return TaskNotification(
|
|
294
|
+
task_id=task_id,
|
|
295
|
+
status=status,
|
|
296
|
+
summary=summary,
|
|
297
|
+
result=result,
|
|
298
|
+
usage=usage,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# 代理生成配置
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class AgentSpawnConfig:
|
|
309
|
+
"""生成代理的配置。"""
|
|
310
|
+
|
|
311
|
+
name: str
|
|
312
|
+
"""代理名称。"""
|
|
313
|
+
|
|
314
|
+
prompt: str
|
|
315
|
+
"""代理的初始提示词。"""
|
|
316
|
+
|
|
317
|
+
cwd: str
|
|
318
|
+
"""工作目录。"""
|
|
319
|
+
|
|
320
|
+
agent_definition: AgentDefinition | None = None
|
|
321
|
+
"""代理定义。"""
|
|
322
|
+
|
|
323
|
+
model: str | None = None
|
|
324
|
+
"""模型覆盖。"""
|
|
325
|
+
|
|
326
|
+
parent_session_id: str = "main"
|
|
327
|
+
"""父会话 ID。"""
|
|
328
|
+
|
|
329
|
+
permission_mode: str | None = None
|
|
330
|
+
"""权限模式覆盖。"""
|
|
331
|
+
|
|
332
|
+
system_prompt: str | None = None
|
|
333
|
+
"""系统提示词覆盖。"""
|
|
334
|
+
|
|
335
|
+
color: str | None = None
|
|
336
|
+
"""UI 颜色。"""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# 代理执行结果
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass
|
|
345
|
+
class AgentResult:
|
|
346
|
+
"""代理执行的结果。"""
|
|
347
|
+
|
|
348
|
+
agent_id: str
|
|
349
|
+
"""代理 ID。"""
|
|
350
|
+
|
|
351
|
+
success: bool = True
|
|
352
|
+
"""是否成功完成。"""
|
|
353
|
+
|
|
354
|
+
result_text: str = ""
|
|
355
|
+
"""代理的最终文本响应。"""
|
|
356
|
+
|
|
357
|
+
error: str | None = None
|
|
358
|
+
"""错误信息(如果失败)。"""
|
|
359
|
+
|
|
360
|
+
notification: TaskNotification | None = None
|
|
361
|
+
"""任务通知(用于异步模式)。"""
|
|
362
|
+
|
|
363
|
+
total_tokens: int = 0
|
|
364
|
+
"""总 token 使用量。"""
|
|
365
|
+
|
|
366
|
+
tool_use_count: int = 0
|
|
367
|
+
"""工具调用次数。"""
|
|
368
|
+
|
|
369
|
+
duration_ms: int = 0
|
|
370
|
+
"""执行时长(毫秒)。"""
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
# 工具池解析
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
# 子代理默认禁止的工具
|
|
378
|
+
_AGENT_DISALLOWED_TOOLS = frozenset({
|
|
379
|
+
"agent", # 禁止递归派发
|
|
380
|
+
"enter_plan_mode",
|
|
381
|
+
"exit_plan_mode",
|
|
382
|
+
"ask_user_question",
|
|
383
|
+
"task_stop",
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def resolve_agent_tools(
|
|
388
|
+
agent_def: AgentDefinition | None,
|
|
389
|
+
parent_registry: ToolRegistry,
|
|
390
|
+
) -> ToolRegistry:
|
|
391
|
+
"""根据代理定义组装工具池。
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
agent_def: 代理定义。如果为 None,使用所有工具。
|
|
395
|
+
parent_registry: 父级工具注册表。
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
ToolRegistry: 代理专用的工具注册表。
|
|
399
|
+
"""
|
|
400
|
+
registry = ToolRegistry()
|
|
401
|
+
|
|
402
|
+
# 确定允许的工具集
|
|
403
|
+
if agent_def is None or agent_def.tools is None or agent_def.tools == ["*"]:
|
|
404
|
+
# 使用所有工具
|
|
405
|
+
allowed_names = None # None 表示全部
|
|
406
|
+
else:
|
|
407
|
+
allowed_names = set(agent_def.tools)
|
|
408
|
+
|
|
409
|
+
# 确定禁止的工具集
|
|
410
|
+
disallowed = set(_AGENT_DISALLOWED_TOOLS)
|
|
411
|
+
if agent_def and agent_def.disallowed_tools:
|
|
412
|
+
disallowed.update(agent_def.disallowed_tools)
|
|
413
|
+
|
|
414
|
+
# 从父注册表中筛选工具
|
|
415
|
+
for tool in parent_registry.list_tools():
|
|
416
|
+
# 跳过禁止的工具
|
|
417
|
+
if tool.name in disallowed:
|
|
418
|
+
continue
|
|
419
|
+
# 如果指定了允许列表,只包含列表中的工具
|
|
420
|
+
if allowed_names is not None and tool.name not in allowed_names:
|
|
421
|
+
continue
|
|
422
|
+
registry.register(tool)
|
|
423
|
+
|
|
424
|
+
return registry
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ---------------------------------------------------------------------------
|
|
428
|
+
# 子进程命令构建
|
|
429
|
+
# ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
# 环境变量:覆盖代理命令
|
|
432
|
+
_AGENT_COMMAND_ENV_VAR = "ILLUSION_TEAMMATE_COMMAND"
|
|
433
|
+
|
|
434
|
+
# 要转发到子进程的环境变量
|
|
435
|
+
_AGENT_ENV_VARS = [
|
|
436
|
+
"ANTHROPIC_API_KEY",
|
|
437
|
+
"ANTHROPIC_BASE_URL",
|
|
438
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
439
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
440
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
441
|
+
"CLAUDE_CONFIG_DIR",
|
|
442
|
+
"CLAUDE_CODE_REMOTE",
|
|
443
|
+
"CLAUDE_CODE_REMOTE_MEMORY_DIR",
|
|
444
|
+
"HTTPS_PROXY",
|
|
445
|
+
"https_proxy",
|
|
446
|
+
"HTTP_PROXY",
|
|
447
|
+
"http_proxy",
|
|
448
|
+
"NO_PROXY",
|
|
449
|
+
"no_proxy",
|
|
450
|
+
"SSL_CERT_FILE",
|
|
451
|
+
"NODE_EXTRA_CA_CERTS",
|
|
452
|
+
"REQUESTS_CA_BUNDLE",
|
|
453
|
+
"CURL_CA_BUNDLE",
|
|
454
|
+
"ILLUSION_API_FORMAT",
|
|
455
|
+
"ILLUSION_BASE_URL",
|
|
456
|
+
"ILLUSION_MODEL",
|
|
457
|
+
"OPENAI_API_KEY",
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _get_agent_command() -> str:
|
|
462
|
+
"""返回用于生成代理子进程的可执行文件。"""
|
|
463
|
+
override = os.environ.get(_AGENT_COMMAND_ENV_VAR)
|
|
464
|
+
if override:
|
|
465
|
+
return override
|
|
466
|
+
|
|
467
|
+
entry_point = shutil.which("illusion")
|
|
468
|
+
if entry_point:
|
|
469
|
+
return entry_point
|
|
470
|
+
|
|
471
|
+
return sys.executable
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _build_agent_cli_flags(
|
|
475
|
+
*,
|
|
476
|
+
model: str | None = None,
|
|
477
|
+
permission_mode: str | None = None,
|
|
478
|
+
) -> list[str]:
|
|
479
|
+
"""构建从当前会话继承到子代理的 CLI 标志。"""
|
|
480
|
+
flags: list[str] = ["--headless"]
|
|
481
|
+
|
|
482
|
+
if permission_mode == "bypassPermissions":
|
|
483
|
+
flags.append("--dangerously-skip-permissions")
|
|
484
|
+
elif permission_mode == "acceptEdits":
|
|
485
|
+
flags.extend(["--permission-mode", "acceptEdits"])
|
|
486
|
+
|
|
487
|
+
if model:
|
|
488
|
+
flags.extend(["--model", shlex.quote(model)])
|
|
489
|
+
|
|
490
|
+
return flags
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _build_agent_env_vars() -> dict[str, str]:
|
|
494
|
+
"""构建要转发到子代理的环境变量。"""
|
|
495
|
+
env: dict[str, str] = {
|
|
496
|
+
"ILLUSION_AGENT_TEAMS": "1",
|
|
497
|
+
}
|
|
498
|
+
for key in _AGENT_ENV_VARS:
|
|
499
|
+
value = os.environ.get(key)
|
|
500
|
+
if value:
|
|
501
|
+
env[key] = value
|
|
502
|
+
return env
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ---------------------------------------------------------------------------
|
|
506
|
+
# 进程内代理执行
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
async def run_agent_in_process(
|
|
511
|
+
config: AgentSpawnConfig,
|
|
512
|
+
query_context: Any,
|
|
513
|
+
parent_registry: ToolRegistry,
|
|
514
|
+
*,
|
|
515
|
+
is_async: bool = False,
|
|
516
|
+
existing_context: AgentExecutionContext | None = None,
|
|
517
|
+
) -> AgentResult:
|
|
518
|
+
"""在当前进程中运行代理。
|
|
519
|
+
|
|
520
|
+
此协程驱动查询引擎循环,直到代理完成或被取消。
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
config: 代理生成配置。
|
|
524
|
+
query_context: 预构建的 QueryContext。
|
|
525
|
+
parent_registry: 父级工具注册表(用于解析代理工具)。
|
|
526
|
+
is_async: 是否为异步(后台)模式。
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
AgentResult: 代理执行结果。
|
|
530
|
+
"""
|
|
531
|
+
from illusion.engine.query import QueryContext
|
|
532
|
+
from illusion.engine.stream_events import AssistantTextDelta, AssistantTurnComplete, ErrorEvent
|
|
533
|
+
|
|
534
|
+
# 解析代理定义
|
|
535
|
+
agent_def = config.agent_definition
|
|
536
|
+
|
|
537
|
+
# 使用已有的上下文或创建新的
|
|
538
|
+
if existing_context is not None:
|
|
539
|
+
ctx = existing_context
|
|
540
|
+
agent_id = ctx.agent_id
|
|
541
|
+
else:
|
|
542
|
+
agent_id = f"agent_{uuid.uuid4().hex[:12]}"
|
|
543
|
+
ctx = AgentExecutionContext(
|
|
544
|
+
agent_id=agent_id,
|
|
545
|
+
agent_name=config.name,
|
|
546
|
+
agent_definition=agent_def,
|
|
547
|
+
prompt=config.prompt,
|
|
548
|
+
model=config.model,
|
|
549
|
+
cwd=Path(config.cwd),
|
|
550
|
+
permission_mode=config.permission_mode or (agent_def.permission_mode if agent_def else None),
|
|
551
|
+
)
|
|
552
|
+
_register_agent(ctx)
|
|
553
|
+
|
|
554
|
+
set_agent_context(ctx)
|
|
555
|
+
|
|
556
|
+
# 解析工具池
|
|
557
|
+
agent_tools = resolve_agent_tools(agent_def, parent_registry)
|
|
558
|
+
|
|
559
|
+
# 构建系统提示词
|
|
560
|
+
system_prompt = config.system_prompt
|
|
561
|
+
if system_prompt is None and agent_def and agent_def.system_prompt:
|
|
562
|
+
system_prompt = agent_def.system_prompt
|
|
563
|
+
if system_prompt is None:
|
|
564
|
+
system_prompt = query_context.system_prompt
|
|
565
|
+
|
|
566
|
+
# 构建模型
|
|
567
|
+
model = config.model
|
|
568
|
+
if model is None and agent_def and agent_def.model:
|
|
569
|
+
if agent_def.model == "inherit":
|
|
570
|
+
model = query_context.model
|
|
571
|
+
else:
|
|
572
|
+
model = agent_def.model
|
|
573
|
+
if model is None:
|
|
574
|
+
model = query_context.model
|
|
575
|
+
|
|
576
|
+
# 使用父级的权限检查器(agent 继承父级权限设置)
|
|
577
|
+
permission_checker = query_context.permission_checker
|
|
578
|
+
|
|
579
|
+
# 为 agent 创建无操作的回调(agent 无法与用户交互)
|
|
580
|
+
async def _noop_permission_prompt(tool_name: str, reason: str) -> bool:
|
|
581
|
+
logger.debug("[agent_executor] %s: auto-approving %s (reason: %s)", agent_id, tool_name, reason)
|
|
582
|
+
return True
|
|
583
|
+
|
|
584
|
+
async def _noop_ask_user(question: str, questions_data: object = None) -> str:
|
|
585
|
+
logger.debug("[agent_executor] %s: auto-answering: %s", agent_id, question)
|
|
586
|
+
return ""
|
|
587
|
+
|
|
588
|
+
# 创建代理专用的 QueryContext
|
|
589
|
+
agent_query_context = QueryContext(
|
|
590
|
+
api_client=query_context.api_client,
|
|
591
|
+
tool_registry=agent_tools,
|
|
592
|
+
permission_checker=permission_checker,
|
|
593
|
+
cwd=ctx.cwd,
|
|
594
|
+
model=model,
|
|
595
|
+
system_prompt=system_prompt,
|
|
596
|
+
max_tokens=query_context.max_tokens,
|
|
597
|
+
permission_prompt=_noop_permission_prompt,
|
|
598
|
+
ask_user_prompt=_noop_ask_user,
|
|
599
|
+
max_turns=agent_def.max_turns if agent_def and agent_def.max_turns else query_context.max_turns,
|
|
600
|
+
hook_executor=None, # agent 不执行 hooks
|
|
601
|
+
effort=query_context.effort,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# 初始化消息列表
|
|
605
|
+
messages: list[ConversationMessage] = [
|
|
606
|
+
ConversationMessage.from_user_text(config.prompt)
|
|
607
|
+
]
|
|
608
|
+
|
|
609
|
+
start_time = time.time()
|
|
610
|
+
final_text = ""
|
|
611
|
+
error_text = ""
|
|
612
|
+
ctx.status = "running"
|
|
613
|
+
|
|
614
|
+
logger.warning(
|
|
615
|
+
"[agent_executor] %s: STARTING agent '%s' (model=%s, tools=%d, max_turns=%s, prompt=%.80s)",
|
|
616
|
+
agent_id, config.name, model, len(agent_tools.list_tools()),
|
|
617
|
+
agent_query_context.max_turns, config.prompt,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Agent 超时时间(秒)
|
|
621
|
+
AGENT_TIMEOUT = 300 # 5 分钟
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
from illusion.engine.query import run_query
|
|
625
|
+
|
|
626
|
+
async def _run_query_loop():
|
|
627
|
+
"""执行查询循环的内部协程。"""
|
|
628
|
+
logger.warning("[agent_executor] %s: entering query loop", agent_id)
|
|
629
|
+
event_count = 0
|
|
630
|
+
async for event, usage in run_query(agent_query_context, messages):
|
|
631
|
+
event_count += 1
|
|
632
|
+
if event_count <= 3:
|
|
633
|
+
logger.warning("[agent_executor] %s: event #%d: %s", agent_id, event_count, type(event).__name__)
|
|
634
|
+
# 检测错误事件
|
|
635
|
+
if isinstance(event, ErrorEvent):
|
|
636
|
+
nonlocal error_text
|
|
637
|
+
error_text = event.message
|
|
638
|
+
logger.error("[agent_executor] %s: API error: %s", agent_id, error_text)
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
# 跟踪文本增量(用于调试)
|
|
642
|
+
if isinstance(event, AssistantTextDelta):
|
|
643
|
+
if not final_text:
|
|
644
|
+
logger.debug("[agent_executor] %s: received first text delta", agent_id)
|
|
645
|
+
|
|
646
|
+
# 跟踪 token 使用
|
|
647
|
+
if usage is not None:
|
|
648
|
+
with contextlib.suppress(AttributeError, TypeError):
|
|
649
|
+
ctx.total_tokens += getattr(usage, "input_tokens", 0)
|
|
650
|
+
ctx.total_tokens += getattr(usage, "output_tokens", 0)
|
|
651
|
+
|
|
652
|
+
# 跟踪工具使用
|
|
653
|
+
if isinstance(event, AssistantTurnComplete):
|
|
654
|
+
logger.debug(
|
|
655
|
+
"[agent_executor] %s: turn complete (tool_uses=%d)",
|
|
656
|
+
agent_id, len(event.message.tool_uses),
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
with contextlib.suppress(AttributeError, TypeError):
|
|
660
|
+
if getattr(event, "type", None) in ("tool_use", "tool_call", "ToolExecutionCompleted"):
|
|
661
|
+
ctx.tool_use_count += 1
|
|
662
|
+
|
|
663
|
+
# 检查取消
|
|
664
|
+
if ctx.abort_controller.is_cancelled:
|
|
665
|
+
logger.debug("[agent_executor] %s: cancelled", agent_id)
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
# 耗尽消息队列
|
|
669
|
+
while not ctx.message_queue.empty():
|
|
670
|
+
try:
|
|
671
|
+
queued = ctx.message_queue.get_nowait()
|
|
672
|
+
logger.debug("[agent_executor] %s: injecting message from %s", agent_id, queued.from_agent)
|
|
673
|
+
messages.append(ConversationMessage(role="user", content=queued.text))
|
|
674
|
+
except asyncio.QueueEmpty:
|
|
675
|
+
break
|
|
676
|
+
|
|
677
|
+
# 带超时执行查询循环
|
|
678
|
+
try:
|
|
679
|
+
logger.warning("[agent_executor] %s: about to await query loop", agent_id)
|
|
680
|
+
await asyncio.wait_for(_run_query_loop(), timeout=AGENT_TIMEOUT)
|
|
681
|
+
logger.warning("[agent_executor] %s: query loop completed", agent_id)
|
|
682
|
+
except asyncio.TimeoutError:
|
|
683
|
+
logger.error("[agent_executor] %s: agent timed out after %ds", agent_id, AGENT_TIMEOUT)
|
|
684
|
+
error_text = f"Agent timed out after {AGENT_TIMEOUT} seconds"
|
|
685
|
+
|
|
686
|
+
# 从消息中提取最终文本
|
|
687
|
+
for msg in reversed(messages):
|
|
688
|
+
if msg.role == "assistant" and msg.content:
|
|
689
|
+
text = msg.text
|
|
690
|
+
if text:
|
|
691
|
+
final_text = text
|
|
692
|
+
break
|
|
693
|
+
|
|
694
|
+
# 如果没有提取到文本,记录调试信息
|
|
695
|
+
if not final_text and not error_text:
|
|
696
|
+
assistant_count = sum(1 for m in messages if m.role == "assistant")
|
|
697
|
+
logger.warning(
|
|
698
|
+
"[agent_executor] %s: no text extracted (messages=%d, assistant_msgs=%d)",
|
|
699
|
+
agent_id, len(messages), assistant_count,
|
|
700
|
+
)
|
|
701
|
+
# 尝试从所有助手消息中提取文本
|
|
702
|
+
for msg in messages:
|
|
703
|
+
if msg.role == "assistant":
|
|
704
|
+
text = msg.text
|
|
705
|
+
if text:
|
|
706
|
+
final_text = text
|
|
707
|
+
break
|
|
708
|
+
|
|
709
|
+
ctx.status = "idle"
|
|
710
|
+
|
|
711
|
+
except asyncio.CancelledError:
|
|
712
|
+
logger.debug("[agent_executor] %s: task cancelled", agent_id)
|
|
713
|
+
ctx.status = "stopped"
|
|
714
|
+
raise
|
|
715
|
+
except Exception as exc:
|
|
716
|
+
logger.exception("[agent_executor] %s: unhandled exception", agent_id)
|
|
717
|
+
ctx.status = "stopped"
|
|
718
|
+
return AgentResult(
|
|
719
|
+
agent_id=agent_id,
|
|
720
|
+
success=False,
|
|
721
|
+
error=str(exc),
|
|
722
|
+
total_tokens=ctx.total_tokens,
|
|
723
|
+
tool_use_count=ctx.tool_use_count,
|
|
724
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
725
|
+
)
|
|
726
|
+
finally:
|
|
727
|
+
# 只有自己创建的 context 才注销,外部传入的由调用方负责注销
|
|
728
|
+
if existing_context is None:
|
|
729
|
+
_unregister_agent(agent_id)
|
|
730
|
+
ctx.status = "stopped"
|
|
731
|
+
|
|
732
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
733
|
+
|
|
734
|
+
# 如果有错误,返回错误结果
|
|
735
|
+
if error_text and not final_text:
|
|
736
|
+
return AgentResult(
|
|
737
|
+
agent_id=agent_id,
|
|
738
|
+
success=False,
|
|
739
|
+
error=error_text,
|
|
740
|
+
total_tokens=ctx.total_tokens,
|
|
741
|
+
tool_use_count=ctx.tool_use_count,
|
|
742
|
+
duration_ms=duration_ms,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
logger.info(
|
|
746
|
+
"[agent_executor] %s: completed (text_len=%d, tokens=%d, tools=%d, duration=%dms)",
|
|
747
|
+
agent_id, len(final_text), ctx.total_tokens, ctx.tool_use_count, duration_ms,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# 构建任务通知
|
|
751
|
+
notification = TaskNotification(
|
|
752
|
+
task_id=agent_id,
|
|
753
|
+
status="completed" if not ctx.abort_controller.is_cancelled else "killed",
|
|
754
|
+
summary=f"Agent '{config.name}' completed",
|
|
755
|
+
result=final_text,
|
|
756
|
+
usage={
|
|
757
|
+
"total_tokens": ctx.total_tokens,
|
|
758
|
+
"tool_uses": ctx.tool_use_count,
|
|
759
|
+
"duration_ms": duration_ms,
|
|
760
|
+
},
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
return AgentResult(
|
|
764
|
+
agent_id=agent_id,
|
|
765
|
+
success=True,
|
|
766
|
+
result_text=final_text,
|
|
767
|
+
notification=notification,
|
|
768
|
+
total_tokens=ctx.total_tokens,
|
|
769
|
+
tool_use_count=ctx.tool_use_count,
|
|
770
|
+
duration_ms=duration_ms,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# ---------------------------------------------------------------------------
|
|
775
|
+
# 子进程代理执行
|
|
776
|
+
# ---------------------------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
async def run_agent_subprocess(
|
|
780
|
+
config: AgentSpawnConfig,
|
|
781
|
+
) -> AgentResult:
|
|
782
|
+
"""作为子进程运行代理。
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
config: 代理生成配置。
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
AgentResult: 代理执行结果(立即返回,代理在后台运行)。
|
|
789
|
+
"""
|
|
790
|
+
agent_id = f"agent_{uuid.uuid4().hex[:12]}"
|
|
791
|
+
agent_def = config.agent_definition
|
|
792
|
+
|
|
793
|
+
# 构建 CLI 命令
|
|
794
|
+
flags = _build_agent_cli_flags(
|
|
795
|
+
model=config.model,
|
|
796
|
+
permission_mode=config.permission_mode or (agent_def.permission_mode if agent_def else None),
|
|
797
|
+
)
|
|
798
|
+
extra_env = _build_agent_env_vars()
|
|
799
|
+
env_prefix = " ".join(f"{k}={v!r}" for k, v in extra_env.items())
|
|
800
|
+
|
|
801
|
+
agent_cmd = _get_agent_command()
|
|
802
|
+
cmd_parts = [agent_cmd, "-m", "illusion"] + flags
|
|
803
|
+
command = f"{env_prefix} {' '.join(cmd_parts)}" if env_prefix else " ".join(cmd_parts)
|
|
804
|
+
|
|
805
|
+
# 创建任务
|
|
806
|
+
from illusion.tasks.manager import get_task_manager
|
|
807
|
+
|
|
808
|
+
manager = get_task_manager()
|
|
809
|
+
try:
|
|
810
|
+
record = await manager.create_agent_task(
|
|
811
|
+
prompt=config.prompt,
|
|
812
|
+
description=f"Agent: {config.name} ({agent_id})",
|
|
813
|
+
cwd=config.cwd,
|
|
814
|
+
task_type="local_agent",
|
|
815
|
+
model=config.model,
|
|
816
|
+
command=command,
|
|
817
|
+
)
|
|
818
|
+
except Exception as exc:
|
|
819
|
+
logger.error("[agent_executor] Failed to spawn subprocess agent %s: %s", agent_id, exc)
|
|
820
|
+
return AgentResult(
|
|
821
|
+
agent_id=agent_id,
|
|
822
|
+
success=False,
|
|
823
|
+
error=str(exc),
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
logger.debug("[agent_executor] Spawned subprocess agent %s as task %s", agent_id, record.id)
|
|
827
|
+
|
|
828
|
+
return AgentResult(
|
|
829
|
+
agent_id=agent_id,
|
|
830
|
+
success=True,
|
|
831
|
+
# 子进程代理的结果通过 task notification 异步传递
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
# ---------------------------------------------------------------------------
|
|
836
|
+
# 导出 TeammateMessage 供 send_message_tool 使用
|
|
837
|
+
# ---------------------------------------------------------------------------
|
|
838
|
+
|
|
839
|
+
__all__ = [
|
|
840
|
+
"AgentAbortController",
|
|
841
|
+
"AgentExecutionContext",
|
|
842
|
+
"AgentResult",
|
|
843
|
+
"AgentSpawnConfig",
|
|
844
|
+
"AgentStatus",
|
|
845
|
+
"TaskNotification",
|
|
846
|
+
"TeammateMessage",
|
|
847
|
+
"format_task_notification",
|
|
848
|
+
"get_active_agent",
|
|
849
|
+
"get_active_agent_by_name",
|
|
850
|
+
"get_agent_context",
|
|
851
|
+
"list_active_agents",
|
|
852
|
+
"parse_task_notification",
|
|
853
|
+
"resolve_agent_tools",
|
|
854
|
+
"run_agent_in_process",
|
|
855
|
+
"run_agent_subprocess",
|
|
856
|
+
"set_agent_context",
|
|
857
|
+
]
|