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.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. 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
+ ]