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,388 @@
1
+ """
2
+ 代理工具模块
3
+ ============
4
+
5
+ 本模块提供子代理派发工具,对齐标准 AgentTool 架构。
6
+
7
+ 主要组件:
8
+ - AgentTool: 启动子代理的工具
9
+ - AgentToolInput: 工具输入参数模型
10
+
11
+ 使用示例:
12
+ >>> from illusion.tools import AgentTool
13
+ >>> tool = AgentTool()
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import logging
20
+ import uuid
21
+ from pathlib import Path
22
+
23
+ from pydantic import BaseModel, Field
24
+
25
+ from illusion.state import AppStateStore
26
+ from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class AgentToolInput(BaseModel):
32
+ """代理工具输入参数。
33
+
34
+ 属性:
35
+ description: 任务的简短描述(3-5 个词)
36
+ prompt: 代理要执行的完整任务
37
+ subagent_type: 代理类型(如 'general-purpose', 'Explore', 'worker')
38
+ model: 可选的模型覆盖
39
+ run_in_background: 是否在后台运行
40
+ name: 代理名称,用于通过 SendMessage 寻址
41
+ team_name: 团队名称(保留字段)
42
+ mode: 权限模式覆盖
43
+ isolation: 隔离模式('worktree')
44
+ cwd: 工作目录覆盖
45
+ """
46
+
47
+ description: str = Field(description="A short (3-5 word) description of the task")
48
+ prompt: str = Field(description="The task for the agent to perform")
49
+ subagent_type: str | None = Field(
50
+ default=None,
51
+ description="The type of specialized agent to use for this task",
52
+ )
53
+ model: str | None = Field(
54
+ default=None,
55
+ description="Optional model override for this agent",
56
+ )
57
+ run_in_background: bool = Field(
58
+ default=False,
59
+ description="Set to true to run this agent in the background",
60
+ )
61
+ name: str | None = Field(
62
+ default=None,
63
+ description="Name for the spawned agent. Makes it addressable via SendMessage",
64
+ )
65
+ team_name: str | None = Field(
66
+ default=None,
67
+ description="Team name for spawning (reserved)",
68
+ )
69
+ mode: str | None = Field(
70
+ default=None,
71
+ description="Permission mode override for the agent",
72
+ )
73
+ isolation: str | None = Field(
74
+ default=None,
75
+ description='Isolation mode. "worktree" creates a temporary git worktree',
76
+ )
77
+ cwd: str | None = Field(
78
+ default=None,
79
+ description="Absolute path to run the agent in",
80
+ )
81
+
82
+
83
+ class AgentTool(BaseTool):
84
+ """启动子代理处理复杂、多步骤任务。
85
+
86
+ 用于启动专门的代理来自动处理复杂任务。每个代理类型都有特定的能力和工具。
87
+ """
88
+
89
+ name = "agent"
90
+ description = """Launch a new agent to handle complex, multi-step tasks autonomously.
91
+
92
+ The Agent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
93
+
94
+ Available agent types and the tools they have access to:
95
+ - general-purpose: All tools available. General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks.
96
+ - Explore: Fast read-only codebase exploration (Tools: Glob, Grep, Read, Bash). Disallows editing tools.
97
+ - Plan: Software architect for designing implementation plans (Tools: Glob, Grep, Read, Bash). Disallows editing tools.
98
+ - verification: Verification specialist for checking implementation correctness (Tools: Glob, Grep, Read, Bash). Disallows editing tools.
99
+ - illusion-guide: Documentation lookup for Illusion Code/SDK/API (Tools: Glob, Grep, Read, WebFetch, WebSearch).
100
+ - worker: Implementation-focused worker agent. All tools available.
101
+ - statusline-setup: Status line configuration agent (Tools: Read, Edit).
102
+
103
+ When using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.
104
+
105
+ When NOT to use the Agent tool:
106
+ - If you want to read a specific file path, use the Read tool or the Glob tool instead
107
+ - If you are searching for a specific class definition, use the Glob tool instead
108
+ - If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead
109
+
110
+ Usage notes:
111
+ - Always include a short description (3-5 words) summarizing what the agent will do
112
+ - Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
113
+ - When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
114
+ - You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will be automatically notified when it completes — do NOT sleep, poll, or proactively check on its progress. Continue with other work or respond to the user instead.
115
+ - **Foreground vs background**: Use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
116
+ - To continue a previously spawned agent, use SendMessage with the agent's ID or name as the `to` field.
117
+ - The agent's outputs should generally be trusted
118
+ - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
119
+ - If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Agent tool use content blocks.
120
+ - Use `isolation="worktree"` to run the agent in an isolated git worktree directory. This prevents the agent from affecting the main workspace. The worktree is automatically cleaned up when the agent completes.
121
+
122
+ ## Writing the prompt
123
+
124
+ When spawning a fresh agent (with a `subagent_type`), it starts with zero context. Brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
125
+ - Explain what you're trying to accomplish and why.
126
+ - Describe what you've already learned or ruled out.
127
+ - Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
128
+ - If you need a short response, say so ("report in under 200 words").
129
+ - Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong.
130
+
131
+ Terse command-style prompts produce shallow, generic work.
132
+
133
+ **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change."""
134
+
135
+ input_model = AgentToolInput
136
+
137
+ async def execute(self, arguments: AgentToolInput, context: ToolExecutionContext) -> ToolResult:
138
+ """执行代理工具。
139
+
140
+ Args:
141
+ arguments: 工具输入参数。
142
+ context: 工具执行上下文。
143
+
144
+ Returns:
145
+ ToolResult: 工具执行结果。
146
+ """
147
+ # 延迟导入以避免循环依赖
148
+ from illusion.coordinator.agent_definitions import get_agent_definition, get_all_agent_definitions
149
+ from illusion.swarm.agent_executor import (
150
+ AgentSpawnConfig,
151
+ format_task_notification,
152
+ run_agent_in_process,
153
+ run_agent_subprocess,
154
+ )
155
+
156
+ # 解析代理定义
157
+ agent_def = None
158
+ if arguments.subagent_type:
159
+ agent_def = get_agent_definition(arguments.subagent_type)
160
+ if agent_def is None:
161
+ available = [a.name for a in get_all_agent_definitions()]
162
+ return ToolResult(
163
+ output=f"Agent type '{arguments.subagent_type}' not found. Available agents: {', '.join(available)}",
164
+ is_error=True,
165
+ )
166
+
167
+ # 确定工作目录
168
+ cwd = arguments.cwd or str(context.cwd)
169
+
170
+ # ------------------------------------------------------------------
171
+ # 处理 worktree 隔离
172
+ # ------------------------------------------------------------------
173
+ worktree_manager = None
174
+ worktree_info = None
175
+ isolation = arguments.isolation
176
+
177
+ if isolation == "worktree":
178
+ from illusion.swarm.worktree import WorktreeManager, validate_worktree_slug
179
+
180
+ worktree_manager = WorktreeManager()
181
+
182
+ # 生成唯一的 worktree slug
183
+ slug_name = arguments.name or arguments.subagent_type or "agent"
184
+ slug_name = slug_name.replace(" ", "-").lower()
185
+ slug = f"{slug_name}-{uuid.uuid4().hex[:8]}"
186
+ try:
187
+ validate_worktree_slug(slug)
188
+ except ValueError:
189
+ # 降级:清理非兼容字符
190
+ import re as _re
191
+ slug = _re.sub(r"[^a-zA-Z0-9._-]", "-", slug).strip("-") or "agent-worktree"
192
+
193
+ try:
194
+ worktree_info = await worktree_manager.create_worktree(
195
+ repo_path=Path(cwd),
196
+ slug=slug,
197
+ )
198
+ cwd = str(worktree_info.path)
199
+ logger.info(
200
+ "[AgentTool] Created worktree for agent at: %s", worktree_info.path
201
+ )
202
+ except Exception as exc:
203
+ return ToolResult(
204
+ output=f"Failed to create isolated worktree: {exc}",
205
+ is_error=True,
206
+ )
207
+
208
+ # 构建生成配置
209
+ config = AgentSpawnConfig(
210
+ name=arguments.name or arguments.subagent_type or "agent",
211
+ prompt=arguments.prompt,
212
+ cwd=cwd,
213
+ agent_definition=agent_def,
214
+ model=arguments.model,
215
+ permission_mode=arguments.mode,
216
+ )
217
+
218
+ # 获取父级工具注册表
219
+ parent_registry = context.metadata.get("tool_registry")
220
+ if parent_registry is None:
221
+ return ToolResult(
222
+ output="Tool registry not available in execution context",
223
+ is_error=True,
224
+ )
225
+
226
+ # 获取查询引擎(用于进程内执行)
227
+ query_engine = context.metadata.get("query_engine")
228
+
229
+ if query_engine is not None:
230
+ # 从引擎构建 QueryContext
231
+ from illusion.engine.query import QueryContext
232
+ query_context = QueryContext(
233
+ api_client=query_engine._api_client,
234
+ tool_registry=query_engine._tool_registry,
235
+ permission_checker=query_engine._permission_checker,
236
+ cwd=query_engine._cwd,
237
+ model=query_engine._model,
238
+ system_prompt=query_engine._system_prompt,
239
+ max_tokens=query_engine._max_tokens,
240
+ max_turns=query_engine._max_turns,
241
+ permission_prompt=query_engine._permission_prompt,
242
+ ask_user_prompt=query_engine._ask_user_prompt,
243
+ hook_executor=query_engine._hook_executor,
244
+ effort=query_engine._effort,
245
+ )
246
+ else:
247
+ query_context = None
248
+
249
+ app_state_store = context.metadata.get("app_state_store")
250
+ in_team_context = False
251
+ if isinstance(app_state_store, AppStateStore):
252
+ team_context = app_state_store.get().team_context
253
+ if isinstance(team_context, dict) and team_context.get("teamName"):
254
+ in_team_context = True
255
+
256
+ has_parent_queue = context.metadata.get("parent_message_queue") is not None
257
+ effective_run_in_background = arguments.run_in_background
258
+
259
+ # 团队上下文中后台模式的通知链路可能不可用,仅记录日志提醒
260
+ if effective_run_in_background and in_team_context and not has_parent_queue:
261
+ logger.info(
262
+ "[AgentTool] Background agent in team context without parent queue; "
263
+ "completion notification will not be delivered to caller"
264
+ )
265
+
266
+ # 辅助函数:清理 worktree
267
+ async def _cleanup_worktree():
268
+ if worktree_info is not None and worktree_manager is not None:
269
+ try:
270
+ await worktree_manager.remove_worktree(worktree_info.slug)
271
+ logger.info("[AgentTool] Cleaned up worktree: %s", worktree_info.slug)
272
+ except Exception:
273
+ logger.exception("[AgentTool] Failed to cleanup worktree: %s", worktree_info.slug)
274
+
275
+ if effective_run_in_background:
276
+ # 异步模式:后台执行
277
+ # 获取后台代理追踪器
278
+ bg_tracker = context.metadata.get("bg_agent_tracker")
279
+
280
+ if query_context is not None:
281
+ # 进程内后台执行
282
+ agent_id = f"agent_{uuid.uuid4().hex[:12]}"
283
+
284
+ # 注册到追踪器
285
+ if bg_tracker is not None:
286
+ bg_tracker.register(agent_id)
287
+
288
+ async def _run_background():
289
+ from illusion.swarm.agent_executor import (
290
+ AgentExecutionContext,
291
+ _register_agent,
292
+ _unregister_agent,
293
+ TeammateMessage,
294
+ )
295
+
296
+ bg_ctx = AgentExecutionContext(
297
+ agent_id=agent_id,
298
+ agent_name=config.name,
299
+ agent_definition=agent_def,
300
+ prompt=config.prompt,
301
+ model=config.model,
302
+ cwd=Path(cwd),
303
+ permission_mode=config.permission_mode,
304
+ )
305
+ _register_agent(bg_ctx)
306
+
307
+ try:
308
+ result = await run_agent_in_process(config, query_context, parent_registry, is_async=True, existing_context=bg_ctx)
309
+ # 构建通知 XML
310
+ if result.notification:
311
+ notification_xml = format_task_notification(result.notification)
312
+ else:
313
+ status = "completed" if result.success else "failed"
314
+ summary = result.result_text or result.error or "Agent completed"
315
+ notification_xml = (
316
+ f"<task-notification><task-id>{agent_id}</task-id>"
317
+ f"<status>{status}</status><summary>{summary}</summary></task-notification>"
318
+ )
319
+ # 通知后台代理追踪器(唤醒主 agent)
320
+ if bg_tracker is not None:
321
+ bg_tracker.notify_completed(agent_id, notification_xml)
322
+ # 通知父代理(团队上下文)
323
+ parent_queue = context.metadata.get("parent_message_queue")
324
+ if parent_queue:
325
+ await parent_queue.put(TeammateMessage(
326
+ text=notification_xml,
327
+ from_agent="system",
328
+ ))
329
+ except Exception:
330
+ logger.exception("[AgentTool] Background agent %s failed", agent_id)
331
+ # 即使异常也通知追踪器,避免主 agent 永远等待
332
+ if bg_tracker is not None:
333
+ bg_tracker.notify_completed(
334
+ agent_id,
335
+ f"<task-notification><task-id>{agent_id}</task-id>"
336
+ f"<status>failed</status><summary>Agent crashed with unhandled exception</summary></task-notification>",
337
+ )
338
+ finally:
339
+ _unregister_agent(agent_id)
340
+ await _cleanup_worktree()
341
+
342
+ asyncio.create_task(_run_background(), name=f"agent-{agent_id}")
343
+
344
+ return ToolResult(
345
+ output=(
346
+ f"Agent '{config.name}' launched in background (agent_id={agent_id}). "
347
+ f"You will be notified when it completes."
348
+ ),
349
+ )
350
+ else:
351
+ # 子进程后台执行 - worktree 清理由 WorktreeManager.cleanup_stale 负责
352
+ if worktree_info is not None:
353
+ logger.info(
354
+ "[AgentTool] Worktree %s will persist for subprocess agent; "
355
+ "cleanup deferred to stale worktree cleanup",
356
+ worktree_info.slug,
357
+ )
358
+ result = await run_agent_subprocess(config)
359
+ if not result.success:
360
+ await _cleanup_worktree()
361
+ return ToolResult(output=result.error or "Failed to spawn agent", is_error=True)
362
+ return ToolResult(
363
+ output=(
364
+ f"Agent '{config.name}' launched as subprocess (agent_id={result.agent_id}). "
365
+ f"You will be notified when it completes."
366
+ ),
367
+ )
368
+ else:
369
+ # 同步模式:前台执行
370
+ try:
371
+ if query_context is not None:
372
+ # 进程内同步执行
373
+ result = await run_agent_in_process(config, query_context, parent_registry)
374
+
375
+ if not result.success:
376
+ return ToolResult(output=result.error or "Agent execution failed", is_error=True)
377
+
378
+ return ToolResult(output=result.result_text)
379
+ else:
380
+ # 子进程同步执行(不常见,但支持)
381
+ result = await run_agent_subprocess(config)
382
+ if not result.success:
383
+ return ToolResult(output=result.error or "Failed to spawn agent", is_error=True)
384
+ return ToolResult(
385
+ output=f"Agent '{config.name}' launched as subprocess (agent_id={result.agent_id}).",
386
+ )
387
+ finally:
388
+ await _cleanup_worktree()
@@ -0,0 +1,186 @@
1
+ """
2
+ 交互式用户问题工具
3
+ ==================
4
+
5
+ 本模块提供向交互式用户提问并获取答案的功能,用于收集用户偏好和需求。
6
+
7
+ 主要组件:
8
+ - AskUserQuestionTool: 向用户提问的工具
9
+
10
+ 使用示例:
11
+ >>> from illusion.tools import AskUserQuestionTool
12
+ >>> tool = AskUserQuestionTool()
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Awaitable, Callable
18
+ from typing import Any
19
+
20
+ from pydantic import BaseModel, Field, model_validator
21
+
22
+ from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
23
+
24
+
25
+ # 用户提示回调函数类型
26
+ # 回调签名: (question_text: str, questions: list[QuestionItem]) -> str | dict[str, str | list[str]]
27
+ # 返回 str 为兼容旧模式,返回 dict 时多选值为 list[str]
28
+ AskUserPrompt = Callable[..., Awaitable[Any]]
29
+
30
+
31
+ class QuestionOption(BaseModel):
32
+ """问题的单个选项。
33
+
34
+ 属性:
35
+ label: 选项的显示文本(1-5 个词)
36
+ description: 选项的解释或选择后会发生什么
37
+ preview: 可选的预览内容(markdown 格式)
38
+ """
39
+
40
+ label: str = Field(description="Display text for this option (1-5 words)")
41
+ description: str = Field(description="Explanation of what this option means or what will happen if chosen")
42
+ preview: str | None = Field(default=None, description="Optional preview content (markdown)")
43
+
44
+
45
+ class QuestionItem(BaseModel):
46
+ """单个问题项。
47
+
48
+ 属性:
49
+ question: 完整的问题文本
50
+ header: 简短的标签,显示为 chip/tag(最多 12 个字符)
51
+ options: 可用选项列表(2-4 个)
52
+ multiSelect: 是否允许多选
53
+ """
54
+
55
+ question: str = Field(description="The complete question to ask the user. Should be clear, specific, and end with a question mark.")
56
+ header: str = Field(description="Very short label displayed as a chip/tag (max 12 chars). Examples: 'Auth method', 'Library', 'Approach'.")
57
+ options: list[QuestionOption] = Field(
58
+ description="The available choices for this question. Must have 2-4 options.",
59
+ min_length=2,
60
+ max_length=4,
61
+ )
62
+ multiSelect: bool = Field(
63
+ default=False,
64
+ description="Set to true to allow the user to select multiple options instead of just one.",
65
+ )
66
+
67
+ @model_validator(mode="after")
68
+ def _check_preview_multiselect(self) -> "QuestionItem":
69
+ """preview 仅支持单选,多选时不能有选项携带 preview。"""
70
+ if self.multiSelect:
71
+ for opt in self.options:
72
+ if opt.preview is not None:
73
+ raise ValueError(
74
+ f"Option '{opt.label}' has a preview, but previews are only "
75
+ "supported for single-select questions (multiSelect must be False)."
76
+ )
77
+ return self
78
+
79
+
80
+ class AskUserQuestionToolInput(BaseModel):
81
+ """向用户提问的参数。
82
+
83
+ 属性:
84
+ questions: 要问的问题列表(1-4 个)
85
+ answers: 权限组件收集的用户答案
86
+ annotations: 来自用户的可选的每问题注解
87
+ metadata: 用于跟踪和分析的可选元数据
88
+ """
89
+
90
+ questions: list[QuestionItem] = Field(
91
+ description="Questions to ask the user (1-4 questions)",
92
+ min_length=1,
93
+ max_length=4,
94
+ )
95
+ answers: dict[str, str] | None = Field(
96
+ default=None,
97
+ description="Reserved for the permission component to inject user answers. Do not provide.",
98
+ )
99
+ annotations: dict[str, Any] | None = Field(
100
+ default=None,
101
+ description="Reserved for the permission component. Do not provide.",
102
+ )
103
+ metadata: dict[str, Any] | None = Field(
104
+ default=None,
105
+ description="Reserved for the permission component. Do not provide.",
106
+ )
107
+
108
+
109
+ class AskUserQuestionTool(BaseTool):
110
+ """向交互式用户提问并返回答案。
111
+
112
+ 用于收集用户偏好、澄清模糊指令、获取实现选择决策等。
113
+ """
114
+
115
+ name = "ask_user_question"
116
+ description = """Use this tool when you need to ask the user questions during execution. This allows you to:
117
+ 1. Gather user preferences or requirements
118
+ 2. Clarify ambiguous instructions
119
+ 3. Get decisions on implementation choices as you work
120
+ 4. Offer choices to the user about what direction to take.
121
+
122
+ Usage notes:
123
+ - Users will always be able to select "Other" to provide custom text input
124
+ - Use multiSelect: true to allow multiple answers to be selected for a question
125
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
126
+
127
+ Plan mode note: In plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?" or "Should I proceed?" - use ExitPlanMode for plan approval. IMPORTANT: Do not reference "the plan" in your questions (e.g., "Do you have feedback about the plan?", "Does the plan look good?") because the user cannot see the plan in the UI until you call ExitPlanMode. If you need plan approval, use ExitPlanMode instead.
128
+
129
+ Preview feature:
130
+ Use the optional `preview` field on options when presenting concrete artifacts that users need to visually compare:
131
+ - ASCII mockups of UI layouts or components
132
+ - Code snippets showing different implementations
133
+ - Diagram variations
134
+ - Configuration examples
135
+
136
+ Preview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect)."""
137
+ input_model = AskUserQuestionToolInput
138
+
139
+ def is_read_only(self, arguments: AskUserQuestionToolInput) -> bool:
140
+ del arguments
141
+ return True
142
+
143
+ async def execute(
144
+ self,
145
+ arguments: AskUserQuestionToolInput,
146
+ context: ToolExecutionContext,
147
+ ) -> ToolResult:
148
+ # 获取用户提示回调函数
149
+ prompt = context.metadata.get("ask_user_prompt")
150
+ if not callable(prompt):
151
+ return ToolResult(
152
+ output="ask_user_question is unavailable in this session",
153
+ is_error=True,
154
+ )
155
+
156
+ # 构建问题显示文本
157
+ parts: list[str] = []
158
+ for i, q in enumerate(arguments.questions, 1):
159
+ header = f"[{q.header}]" if q.header else ""
160
+ parts.append(f"{header} {q.question}")
161
+ for j, opt in enumerate(q.options, 1):
162
+ preview_note = " (has preview)" if opt.preview else ""
163
+ parts.append(f" {j}. {opt.label} - {opt.description}{preview_note}")
164
+ if q.multiSelect:
165
+ parts.append(" (multi-select)")
166
+
167
+ question_text = "\n".join(parts)
168
+
169
+ # 将结构化问题数据传给回调,使其能正确渲染单选/多选UI
170
+ answers = await prompt(question_text, arguments.questions)
171
+
172
+ if not answers:
173
+ return ToolResult(output="(no response)")
174
+
175
+ # 格式化答案:dict 模式支持 list 值(多选),逐项展开
176
+ if isinstance(answers, dict):
177
+ lines: list[str] = []
178
+ for k, v in answers.items():
179
+ if isinstance(v, list):
180
+ for item in v:
181
+ lines.append(f"{k}: {item}")
182
+ else:
183
+ lines.append(f"{k}: {v}")
184
+ return ToolResult(output="\n".join(lines))
185
+
186
+ return ToolResult(output=str(answers))