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
illusion/ui/runtime.py ADDED
@@ -0,0 +1,787 @@
1
+ """
2
+ Runtime 运行时模块
3
+ ================
4
+
5
+ 本模块实现无 UI 和 Textual UI 共享的运行时程序集。
6
+
7
+ 主要功能:
8
+ - 运行时数据 bundle 管理
9
+ - API 客户端初始化和配置
10
+ - 工具注册和权限检查
11
+ - 会话状态管理
12
+ - 命令处理和执行
13
+ - 会话快照保存
14
+
15
+ 类说明:
16
+ - RuntimeBundle: 共享运行时数据bundle
17
+ - build_runtime: 构建运行时
18
+ - start_runtime: 启动运行时(执行会话开始钩子)
19
+ - close_runtime: 关闭运行时并清理资源
20
+ - handle_line: 处理用户输入行
21
+ - sync_app_state: 同步应用状态
22
+
23
+ 使用示例:
24
+ >>> from illusion.ui.runtime import build_runtime, handle_line, start_runtime, close_runtime
25
+ >>>
26
+ >>> # 构建运行时
27
+ >>> bundle = await build_runtime(model="claude-sonnet-4-20250514")
28
+ >>> await start_runtime(bundle)
29
+ >>>
30
+ >>> # 处理输入行
31
+ >>> await handle_line(
32
+ ... bundle,
33
+ ... "帮我写一个 hello world 程序",
34
+ ... print_system=print_system,
35
+ ... render_event=render_event,
36
+ ... clear_output=clear_output,
37
+ ... )
38
+ >>>
39
+ >>> # 关闭运行时
40
+ >>> await close_runtime(bundle)
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import json
46
+ from dataclasses import dataclass, field
47
+ from pathlib import Path
48
+ from typing import Any, Awaitable, Callable
49
+ from uuid import uuid4
50
+
51
+ from illusion.api.client import AnthropicApiClient, SupportsStreamingMessages
52
+ from illusion.api.effort import EffortMapper
53
+ from illusion.api.openai_client import OpenAICompatibleClient
54
+ from illusion.api.provider import auth_status, detect_provider
55
+ from illusion.bridge import get_bridge_manager
56
+ from illusion.commands import CommandContext, CommandResult, create_default_command_registry
57
+ from illusion.config import get_config_file_path, load_settings
58
+ from illusion.config.settings import Settings
59
+ from illusion.engine import QueryEngine
60
+ from illusion.engine.messages import ConversationMessage, ToolResultBlock, ToolUseBlock
61
+ from illusion.engine.query import MaxTurnsExceeded
62
+ from illusion.engine.stream_events import StreamEvent
63
+ from illusion.hooks import HookEvent, HookExecutionContext, HookExecutor, load_hook_registry
64
+ from illusion.hooks.hot_reload import HookReloader
65
+ from illusion.mcp.client import McpClientManager
66
+ from illusion.mcp.config import load_mcp_server_configs
67
+ from illusion.permissions import PermissionChecker
68
+ from illusion.plugins import load_plugins
69
+ from illusion.prompts import build_runtime_system_prompt
70
+ from illusion.state import AppState, AppStateStore
71
+ from illusion.services.session_storage import save_session_snapshot
72
+ from illusion.tools import ToolRegistry, create_default_tool_registry
73
+
74
+ # 类型别名定义
75
+ PermissionPrompt = Callable[[str, str], Awaitable[bool]] # 权限确认回调
76
+ AskUserPrompt = Callable[[str], Awaitable[str]] # 用户问答回调
77
+ SystemPrinter = Callable[[str], Awaitable[None]] # 系统消息打印回调
78
+ StreamRenderer = Callable[[StreamEvent], Awaitable[None]] # 流式事件渲染回调
79
+ ClearHandler = Callable[[], Awaitable[None]] # 清空输出回调
80
+ TranscriptItemSender = Callable[[dict], Awaitable[None]] # 发送 transcript_item 的回调
81
+ CommandResultEmitter = Callable[[str, str], Awaitable[None]] # 指令结果发射回调(message, type)
82
+ ReplaceTranscriptItems = Callable[[list[dict]], Awaitable[None]] # 替换转录项列表的回调
83
+
84
+
85
+ @dataclass
86
+ class RuntimeBundle:
87
+ """共享运行时数据bundle。
88
+
89
+ 用于存储一次交互式会话的所有运行时对象。
90
+ 包括 API 客户端、工具注册器、引擎、状态管理等。
91
+
92
+ Attributes:
93
+ api_client: 流式 API 客户端实例
94
+ cwd: 当前工作目录
95
+ mcp_manager: MCP 客户端管理器
96
+ tool_registry: 工具注册器
97
+ app_state: 应用状态存储
98
+ hook_executor: 钩子执行器
99
+ engine: 查询引擎
100
+ commands: 命令注册表
101
+ external_api_client: 是否使用外部 API 客户端
102
+ session_id: 会话 ID
103
+ settings_overrides: 设置覆盖字典
104
+ """
105
+
106
+ api_client: SupportsStreamingMessages
107
+ cwd: str
108
+ mcp_manager: McpClientManager
109
+ tool_registry: ToolRegistry
110
+ app_state: AppStateStore
111
+ hook_executor: HookExecutor
112
+ engine: QueryEngine
113
+ commands: object
114
+ external_api_client: bool
115
+ session_id: str = ""
116
+ settings_overrides: dict[str, Any] = field(default_factory=dict)
117
+
118
+ def current_settings(self):
119
+ """返回会话的有效设置。
120
+
121
+ 大多数设置持久化到磁盘(~/.illusion/settings.json),
122
+ 但 CLI 选项如 --model/--api-format 在进程生命周期内保持有效。
123
+ 没有此覆盖,发送任何斜杠命令(如 /fast)会从磁盘刷新 UI 状态,
124
+ 并将 model/provider " snap back" 到配置文件中的值。
125
+ """
126
+ return load_settings().merge_cli_overrides(**self.settings_overrides)
127
+
128
+ def current_plugins(self):
129
+ """返回当前工作树的可见插件。"""
130
+ return load_plugins(self.current_settings(), self.cwd)
131
+
132
+ def hook_summary(self) -> str:
133
+ """返回当前钩子摘要。"""
134
+ return load_hook_registry(self.current_settings(), self.current_plugins()).summary()
135
+
136
+ def plugin_summary(self) -> str:
137
+ """返回当前插件摘要。"""
138
+ plugins = self.current_plugins()
139
+ if not plugins:
140
+ return "No plugins discovered."
141
+ lines = ["Plugins:"]
142
+ for plugin in plugins:
143
+ state = "enabled" if plugin.enabled else "disabled"
144
+ lines.append(f"- {plugin.manifest.name} [{state}] {plugin.manifest.description}")
145
+ return "\n".join(lines)
146
+
147
+ def mcp_summary(self) -> str:
148
+ """返回当前 MCP 摘要。"""
149
+ statuses = self.mcp_manager.list_statuses()
150
+ if not statuses:
151
+ return "No MCP servers configured."
152
+ lines = ["MCP servers:"]
153
+ for status in statuses:
154
+ suffix = f" - {status.detail}" if status.detail else ""
155
+ lines.append(f"- {status.name}: {status.state}{suffix}")
156
+ if status.tools:
157
+ lines.append(f" tools: {', '.join(tool.name for tool in status.tools)}")
158
+ if status.resources:
159
+ lines.append(f" resources: {', '.join(resource.uri for resource in status.resources)}")
160
+ return "\n".join(lines)
161
+
162
+
163
+ async def build_runtime(
164
+ *,
165
+ prompt: str | None = None,
166
+ model: str | None = None,
167
+ max_turns: int | None = None,
168
+ base_url: str | None = None,
169
+ system_prompt: str | None = None,
170
+ api_key: str | None = None,
171
+ api_format: str | None = None,
172
+ api_client: SupportsStreamingMessages | None = None,
173
+ permission_prompt: PermissionPrompt | None = None,
174
+ ask_user_prompt: AskUserPrompt | None = None,
175
+ restore_messages: list[dict] | None = None,
176
+ restore_session_id: str | None = None,
177
+ effort: str | None = None,
178
+ is_interactive: bool = True,
179
+ ) -> RuntimeBundle:
180
+ """构建 IllusionCode 会话的共享运行时。
181
+
182
+ 初始化所有运行时对象,包括 API 客户端、插件、工具注册器、引擎等。
183
+
184
+ Args:
185
+ prompt: 初始用户提示词
186
+ model: 使用的模型名称
187
+ max_turns: 最大对话轮次
188
+ base_url: API 基础 URL
189
+ system_prompt: 系统提示词
190
+ api_key: API 密钥
191
+ api_format: API 格式(openai/anthropic)
192
+ api_client: 流式 API 客户端实例
193
+ permission_prompt: 权限确认回调函数
194
+ ask_user_prompt: 用户问答回调函数
195
+ restore_messages: 恢复的会话消息列表
196
+ effort: 推理强度级别(low/medium/high/xhigh/max)
197
+ is_interactive: 是否为交互模式(默认True)。非交互模式下会加载StructuredOutputTool。
198
+
199
+ Returns:
200
+ RuntimeBundle: 运行时数据 bundle
201
+ """
202
+ # 构建设置覆盖字典
203
+ settings_overrides: dict[str, Any] = {
204
+ "model": model,
205
+ "max_turns": max_turns,
206
+ "base_url": base_url,
207
+ "system_prompt": system_prompt,
208
+ "api_key": api_key,
209
+ "api_format": api_format,
210
+ "effort": effort,
211
+ }
212
+ settings = load_settings().merge_cli_overrides(**settings_overrides)
213
+ session_id = restore_session_id or uuid4().hex[:12]
214
+ # 获取当前工作目录
215
+ cwd = str(Path.cwd())
216
+ # 加载插件
217
+ plugins = load_plugins(settings, cwd)
218
+ # 解析 API 客户端
219
+ if api_client:
220
+ resolved_api_client = api_client
221
+ elif settings.api_format == "openai":
222
+ # 检测是否为 Copilot 或 Codex 提供商
223
+ _provider_info = detect_provider(settings)
224
+ if _provider_info.name == "copilot":
225
+ from illusion.auth.copilot import CopilotAuth, copilot_extra_headers
226
+ _copilot = CopilotAuth()
227
+ _copilot_token = _copilot.get_valid_token()
228
+ resolved_api_client = OpenAICompatibleClient(
229
+ api_key=_copilot_token,
230
+ base_url=settings.base_url or "https://api.githubcopilot.com",
231
+ extra_headers=copilot_extra_headers(),
232
+ )
233
+ elif _provider_info.name == "codex":
234
+ from illusion.auth.external import default_binding_for_provider, load_external_credential
235
+ from illusion.api.codex_client import CodexApiClient
236
+ _binding = default_binding_for_provider("openai_codex")
237
+ _cred = load_external_credential(_binding)
238
+ resolved_api_client = CodexApiClient(
239
+ auth_token=_cred.value,
240
+ base_url=settings.base_url,
241
+ )
242
+ else:
243
+ resolved_api_client = OpenAICompatibleClient(
244
+ api_key=settings.resolve_api_key(),
245
+ base_url=settings.base_url,
246
+ )
247
+ else:
248
+ resolved_api_client = AnthropicApiClient(
249
+ api_key=settings.resolve_api_key(),
250
+ base_url=settings.base_url,
251
+ )
252
+ # 创建 MCP 客户端管理器
253
+ mcp_manager = McpClientManager(load_mcp_server_configs(settings, plugins, cwd))
254
+ await mcp_manager.connect_all()
255
+ # 创建工具注册器
256
+ tool_registry = create_default_tool_registry(mcp_manager, is_interactive=is_interactive)
257
+ # 检测提供者
258
+ provider = detect_provider(settings)
259
+ # 获取桥接管理器
260
+ bridge_manager = get_bridge_manager()
261
+ # 创建应用状态存储
262
+ app_state = AppStateStore(
263
+ AppState(
264
+ model=settings.active_model_name,
265
+ permission_mode=settings.permission.mode.value,
266
+ ui_language=settings.ui_language,
267
+ cwd=cwd,
268
+ provider=provider.name,
269
+ auth_status=auth_status(settings),
270
+ base_url=settings.base_url or "",
271
+ fast_mode=settings.fast_mode,
272
+ effort=settings.effort,
273
+ passes=settings.passes,
274
+ mcp_connected=sum(1 for status in mcp_manager.list_statuses() if status.state == "connected"),
275
+ mcp_failed=sum(1 for status in mcp_manager.list_statuses() if status.state == "failed"),
276
+ bridge_sessions=len(bridge_manager.list_sessions()),
277
+ output_style=settings.output_style,
278
+ show_thinking=settings.show_thinking,
279
+ phase="idle",
280
+ session_id=session_id,
281
+ )
282
+ )
283
+ # 创建钩子重载器和执行器
284
+ hook_reloader = HookReloader(get_config_file_path())
285
+ hook_executor = HookExecutor(
286
+ hook_reloader.current_registry() if api_client is None else load_hook_registry(settings, plugins),
287
+ HookExecutionContext(
288
+ cwd=Path(cwd).resolve(),
289
+ api_client=resolved_api_client,
290
+ default_model=settings.active_model_name,
291
+ ),
292
+ )
293
+ # 创建查询引擎
294
+ engine = QueryEngine(
295
+ api_client=resolved_api_client,
296
+ tool_registry=tool_registry,
297
+ permission_checker=PermissionChecker(settings.permission),
298
+ cwd=cwd,
299
+ model=settings.active_model_name,
300
+ system_prompt=build_runtime_system_prompt(settings, cwd=cwd, latest_user_prompt=prompt),
301
+ max_tokens=settings.max_tokens,
302
+ max_turns=settings.max_turns,
303
+ permission_prompt=permission_prompt,
304
+ ask_user_prompt=ask_user_prompt,
305
+ hook_executor=hook_executor,
306
+ tool_metadata={
307
+ "mcp_manager": mcp_manager,
308
+ "bridge_manager": bridge_manager,
309
+ "app_state_store": app_state,
310
+ "session_id": session_id,
311
+ },
312
+ effort=EffortMapper.normalize(settings.effort),
313
+ session_id=session_id,
314
+ )
315
+ # 将引擎自身添加到工具元数据中,供子 agent 使用
316
+ engine._tool_metadata["query_engine"] = engine
317
+ # 将后台代理追踪器添加到工具元数据中,供 AgentTool 使用
318
+ engine._tool_metadata["bg_agent_tracker"] = engine._bg_agent_tracker
319
+ # 从保存的会话恢复消息(如果提供)
320
+ if restore_messages:
321
+ restored = [
322
+ ConversationMessage.model_validate(m) for m in restore_messages
323
+ ]
324
+ engine.load_messages(restored)
325
+
326
+ return RuntimeBundle(
327
+ api_client=resolved_api_client,
328
+ cwd=cwd,
329
+ mcp_manager=mcp_manager,
330
+ tool_registry=tool_registry,
331
+ app_state=app_state,
332
+ hook_executor=hook_executor,
333
+ engine=engine,
334
+ commands=create_default_command_registry(),
335
+ external_api_client=api_client is not None,
336
+ session_id=session_id,
337
+ settings_overrides=settings_overrides,
338
+ )
339
+
340
+
341
+ async def start_runtime(bundle: RuntimeBundle) -> None:
342
+ """运行会话开始钩子。
343
+
344
+ 执行 SESSION_START 钩子事件。
345
+
346
+ Args:
347
+ bundle: 运行时数据 bundle
348
+ """
349
+ await bundle.hook_executor.execute(
350
+ HookEvent.SESSION_START,
351
+ {"cwd": bundle.cwd, "event": HookEvent.SESSION_START.value},
352
+ )
353
+
354
+
355
+ async def close_runtime(bundle: RuntimeBundle) -> None:
356
+ """关闭运行时拥有的资源。
357
+
358
+ 关闭 MCP 管理器并执行 SESSION_END 钩子。
359
+
360
+ Args:
361
+ bundle: 运行时数据 bundle
362
+ """
363
+ from illusion.swarm.team_helpers import cleanup_session_teams
364
+
365
+ await cleanup_session_teams()
366
+ # 关闭 MCP 管理器
367
+ await bundle.mcp_manager.close()
368
+ # 执行会话结束钩子
369
+ await bundle.hook_executor.execute(
370
+ HookEvent.SESSION_END,
371
+ {"cwd": bundle.cwd, "event": HookEvent.SESSION_END.value},
372
+ )
373
+
374
+
375
+ def _last_user_text(messages: list[ConversationMessage]) -> str:
376
+ """获取最后一条用户消息的文本。
377
+
378
+ Args:
379
+ messages: 会话消息列表
380
+
381
+ Returns:
382
+ str: 最后一条用户消息文本(如果不存在则返回空字符串)
383
+ """
384
+ for msg in reversed(messages):
385
+ if msg.role == "user" and msg.text.strip():
386
+ return msg.text.strip()
387
+ return ""
388
+
389
+
390
+ def _truncate(text: str, limit: int) -> str:
391
+ """截断文本到指定长度。
392
+
393
+ Args:
394
+ text: 要截断的文本
395
+ limit: 最大长度
396
+
397
+ Returns:
398
+ str: 截断后的文本
399
+ """
400
+ if len(text) <= limit:
401
+ return text
402
+ return text[:limit] + "…"
403
+
404
+
405
+ def _format_pending_tool_results(messages: list[ConversationMessage]) -> str | None:
406
+ """在工具执行后停止时呈现紧凑摘要。
407
+
408
+ 在模型有机会响应之前呈现待处理结果的摘要。
409
+
410
+ Args:
411
+ messages: 会话消息列表
412
+
413
+ Returns:
414
+ str | None: 摘要文本(如果没有待处理结果则返回 None)
415
+ """
416
+ if not messages:
417
+ return None
418
+
419
+ last = messages[-1]
420
+ if last.role != "user":
421
+ return None
422
+ tool_results = [block for block in last.content if isinstance(block, ToolResultBlock)]
423
+ if not tool_results:
424
+ return None
425
+
426
+ # 构建工具使用 ID 到工具使用的映射
427
+ tool_uses_by_id: dict[str, ToolUseBlock] = {}
428
+ assistant_text = ""
429
+ for msg in reversed(messages[:-1]):
430
+ if msg.role != "assistant":
431
+ continue
432
+ if not msg.tool_uses:
433
+ continue
434
+ assistant_text = msg.text.strip()
435
+ for tu in msg.tool_uses:
436
+ tool_uses_by_id[tu.id] = tu
437
+ break
438
+
439
+ lines: list[str] = [
440
+ "Pending continuation: tool results were produced, but the model did not get a chance to respond yet."
441
+ ]
442
+ if assistant_text:
443
+ lines.append(f"Last assistant message: {_truncate(assistant_text, 400)}")
444
+
445
+ max_results = 3
446
+ for tr in tool_results[:max_results]:
447
+ tu = tool_uses_by_id.get(tr.tool_use_id)
448
+ if tu is not None:
449
+ raw_input = json.dumps(tu.input, ensure_ascii=True, sort_keys=True)
450
+ lines.append(
451
+ f"- {tu.name} {_truncate(raw_input, 200)} -> {_truncate(tr.content.strip(), 400)}"
452
+ )
453
+ else:
454
+ lines.append(
455
+ f"- tool_result[{tr.tool_use_id}] -> {_truncate(tr.content.strip(), 400)}"
456
+ )
457
+
458
+ if len(tool_results) > max_results:
459
+ lines.append(f"(+{len(tool_results) - max_results} more tool results)")
460
+
461
+ lines.append("To continue from these results, run: /continue 32 (or any count).")
462
+ return "\n".join(lines)
463
+
464
+
465
+ def sync_app_state(bundle: RuntimeBundle) -> None:
466
+ """从当前设置和动态键绑定刷新 UI 状态。
467
+
468
+ Args:
469
+ bundle: 运行时数据 bundle
470
+ """
471
+ from illusion.services.compact import estimate_conversation_tokens
472
+ settings = bundle.current_settings()
473
+ bundle.engine.set_max_turns(settings.max_turns)
474
+ provider = detect_provider(settings)
475
+ bundle.app_state.set(
476
+ model=settings.active_model_name,
477
+ permission_mode=settings.permission.mode.value,
478
+ ui_language=settings.ui_language,
479
+ cwd=bundle.cwd,
480
+ provider=provider.name,
481
+ auth_status=auth_status(settings),
482
+ base_url=settings.base_url or "",
483
+ fast_mode=settings.fast_mode,
484
+ effort=settings.effort,
485
+ passes=settings.passes,
486
+ mcp_connected=sum(1 for status in bundle.mcp_manager.list_statuses() if status.state == "connected"),
487
+ mcp_failed=sum(1 for status in bundle.mcp_manager.list_statuses() if status.state == "failed"),
488
+ bridge_sessions=len(get_bridge_manager().list_sessions()),
489
+ output_style=settings.output_style,
490
+ show_thinking=settings.show_thinking,
491
+ phase=bundle.app_state.get().phase,
492
+ session_id=bundle.session_id,
493
+ context_window=settings.context_window,
494
+ context_tokens=estimate_conversation_tokens(bundle.engine.messages),
495
+ )
496
+
497
+
498
+ def _rebuild_api_client(bundle: RuntimeBundle, settings: Settings) -> None:
499
+ """根据当前设置重建 API 客户端(跨 env 切换模型时调用)
500
+
501
+ Args:
502
+ bundle: 运行时数据 bundle
503
+ settings: 当前设置
504
+ """
505
+ from illusion.api.provider import detect_provider as _detect
506
+
507
+ _provider_info = _detect(settings)
508
+ if _provider_info.name == "copilot":
509
+ from illusion.auth.copilot import CopilotAuth, copilot_extra_headers
510
+ _copilot = CopilotAuth()
511
+ _copilot_token = _copilot.get_valid_token()
512
+ new_client = OpenAICompatibleClient(
513
+ api_key=_copilot_token,
514
+ base_url=settings.base_url or "https://api.githubcopilot.com",
515
+ extra_headers=copilot_extra_headers(),
516
+ )
517
+ elif _provider_info.name == "codex":
518
+ from illusion.auth.external import default_binding_for_provider, load_external_credential
519
+ from illusion.api.codex_client import CodexApiClient
520
+ _binding = default_binding_for_provider("openai_codex")
521
+ _cred = load_external_credential(_binding)
522
+ new_client = CodexApiClient(
523
+ auth_token=_cred.value,
524
+ base_url=settings.base_url,
525
+ )
526
+ elif settings.api_format == "openai":
527
+ new_client = OpenAICompatibleClient(
528
+ api_key=settings.resolve_api_key(),
529
+ base_url=settings.base_url,
530
+ )
531
+ else:
532
+ new_client = AnthropicApiClient(
533
+ api_key=settings.resolve_api_key(),
534
+ base_url=settings.base_url,
535
+ )
536
+
537
+ bundle.api_client = new_client
538
+ bundle.engine.set_api_client(new_client)
539
+ bundle.hook_executor._context.api_client = new_client # type: ignore[attr-defined]
540
+
541
+
542
+ async def handle_line(
543
+ bundle: RuntimeBundle,
544
+ line: str,
545
+ *,
546
+ print_system: SystemPrinter,
547
+ render_event: StreamRenderer,
548
+ clear_output: ClearHandler,
549
+ replay_transcript_item: TranscriptItemSender | None = None,
550
+ command_result_emitter: CommandResultEmitter | None = None,
551
+ replace_transcript_items: ReplaceTranscriptItems | None = None,
552
+ ) -> bool:
553
+ """处理提交的一行输入(用于无头或 TUI 渲染)。
554
+
555
+ 处理命令或用户消息,更新引擎,渲染事件,并保存会话快照。
556
+
557
+ Args:
558
+ bundle: 运行时数据 bundle
559
+ line: 用户输入的行
560
+ print_system: 系统消息打印回调
561
+ render_event: 流式事件渲染回调
562
+ clear_output: 清空输出回调
563
+ replay_transcript_item: 重播 transcript_item 的回调(用于 /resume)
564
+ command_result_emitter: 指令结果发射回调
565
+ replace_transcript_items: 替换转录项列表的回调(用于 /rewind 等,避免 Ink Static 重复渲染)
566
+
567
+ Returns:
568
+ bool: 是否继续会话
569
+ """
570
+ # 更新钩子注册表(如果不是外部 API 客户端)
571
+ if not bundle.external_api_client:
572
+ bundle.hook_executor.update_registry(
573
+ load_hook_registry(bundle.current_settings(), bundle.current_plugins())
574
+ )
575
+
576
+ # 解析命令
577
+ parsed = bundle.commands.lookup(line)
578
+ if parsed is not None:
579
+ command, args = parsed
580
+ result = await command.handler(
581
+ args,
582
+ CommandContext(
583
+ engine=bundle.engine,
584
+ hooks_summary=bundle.hook_summary(),
585
+ mcp_summary=bundle.mcp_summary(),
586
+ plugin_summary=bundle.plugin_summary(),
587
+ cwd=bundle.cwd,
588
+ tool_registry=bundle.tool_registry,
589
+ app_state=bundle.app_state,
590
+ session_id=bundle.session_id,
591
+ ),
592
+ )
593
+ if result.reset_session:
594
+ bundle.session_id = uuid4().hex[:12]
595
+ locale = str(bundle.app_state.get().ui_language or bundle.current_settings().ui_language)
596
+ prefix = "新会话已开启,任务 ID:" if locale.lower().startswith("zh") else "Started new session. Task ID: "
597
+ suffix = result.message or ""
598
+ detail = f"\n{suffix}" if suffix else ""
599
+ result.message = f"{prefix}{bundle.session_id}{detail}"
600
+ await _render_command_result(result, print_system, clear_output, render_event, replay_transcript_item, command_result_emitter, replace_transcript_items)
601
+ if result.restored_session_id:
602
+ bundle.session_id = result.restored_session_id
603
+ # 跨 env 切换模型时重建 API 客户端
604
+ if result.needs_api_rebuild:
605
+ _rebuild_api_client(bundle, bundle.current_settings())
606
+ # 处理待继续标志
607
+ if result.continue_pending:
608
+ settings = bundle.current_settings()
609
+ bundle.engine.set_max_turns(settings.max_turns)
610
+ system_prompt = build_runtime_system_prompt(
611
+ settings,
612
+ cwd=bundle.cwd,
613
+ latest_user_prompt=_last_user_text(bundle.engine.messages),
614
+ )
615
+ bundle.engine.set_system_prompt(system_prompt)
616
+ turns = result.continue_turns if result.continue_turns is not None else bundle.engine.max_turns
617
+ try:
618
+ async for event in bundle.engine.continue_pending(max_turns=turns):
619
+ await render_event(event)
620
+ except MaxTurnsExceeded as exc:
621
+ await print_system(f"Stopped after {exc.max_turns} turns (max_turns).")
622
+ pending = _format_pending_tool_results(bundle.engine.messages)
623
+ if pending:
624
+ await print_system(pending)
625
+ # 保存会话快照
626
+ save_session_snapshot(
627
+ cwd=bundle.cwd,
628
+ model=settings.active_model_name,
629
+ system_prompt=system_prompt,
630
+ messages=bundle.engine.messages,
631
+ usage=bundle.engine.total_usage,
632
+ session_id=bundle.session_id,
633
+ )
634
+ sync_app_state(bundle)
635
+ return not result.should_exit
636
+
637
+ # 处理普通用户消息
638
+ settings = bundle.current_settings()
639
+ bundle.engine.set_max_turns(settings.max_turns)
640
+ system_prompt = build_runtime_system_prompt(settings, cwd=bundle.cwd, latest_user_prompt=line)
641
+ bundle.engine.set_system_prompt(system_prompt)
642
+ try:
643
+ async for event in bundle.engine.submit_message(line):
644
+ await render_event(event)
645
+ except MaxTurnsExceeded as exc:
646
+ await print_system(f"Stopped after {exc.max_turns} turns (max_turns).")
647
+ pending = _format_pending_tool_results(bundle.engine.messages)
648
+ if pending:
649
+ await print_system(pending)
650
+ save_session_snapshot(
651
+ cwd=bundle.cwd,
652
+ model=settings.model,
653
+ system_prompt=system_prompt,
654
+ messages=bundle.engine.messages,
655
+ usage=bundle.engine.total_usage,
656
+ session_id=bundle.session_id,
657
+ )
658
+ sync_app_state(bundle)
659
+ return True
660
+ # 保存会话快照
661
+ save_session_snapshot(
662
+ cwd=bundle.cwd,
663
+ model=settings.model,
664
+ system_prompt=system_prompt,
665
+ messages=bundle.engine.messages,
666
+ usage=bundle.engine.total_usage,
667
+ session_id=bundle.session_id,
668
+ )
669
+ sync_app_state(bundle)
670
+ return True
671
+
672
+
673
+ async def _render_command_result(
674
+ result: CommandResult,
675
+ print_system: SystemPrinter,
676
+ clear_output: ClearHandler,
677
+ render_event: StreamRenderer | None = None,
678
+ replay_transcript_item: TranscriptItemSender | None = None,
679
+ command_result_emitter: CommandResultEmitter | None = None,
680
+ replace_transcript_items: ReplaceTranscriptItems | None = None,
681
+ ) -> None:
682
+ """渲染命令执行结果。
683
+
684
+ Args:
685
+ result: 命令执行结果
686
+ print_system: 系统消息打印回调
687
+ clear_output: 清空输出回调
688
+ render_event: 流式事件渲染回调
689
+ replay_transcript_item: 重播 transcript_item 的回调
690
+ command_result_emitter: 指令结果发射回调
691
+ replace_transcript_items: 替换转录项列表的回调
692
+ """
693
+ if result.replay_messages and replace_transcript_items is not None:
694
+ from illusion.engine.messages import ToolUseBlock, ToolResultBlock
695
+
696
+ tool_uses_by_id: dict[str, dict] = {}
697
+ replay_items: list[dict] = []
698
+ for msg in result.replay_messages:
699
+ if msg.role == "user":
700
+ if msg.text.strip():
701
+ replay_items.append({"role": "user", "text": msg.text})
702
+ for block in msg.content:
703
+ if isinstance(block, ToolResultBlock):
704
+ tool_info = tool_uses_by_id.get(block.tool_use_id, {})
705
+ replay_items.append({
706
+ "role": "tool_result",
707
+ "text": block.text_content,
708
+ "tool_name": tool_info.get("name"),
709
+ "tool_use_id": block.tool_use_id,
710
+ "is_error": block.is_error,
711
+ })
712
+ elif msg.role == "assistant":
713
+ reasoning = msg.thinking_text.strip()
714
+ assistant_text = msg.text.strip()
715
+ if assistant_text or reasoning:
716
+ item = {"role": "assistant", "text": assistant_text}
717
+ if reasoning:
718
+ item["reasoning"] = reasoning
719
+ replay_items.append(item)
720
+ for block in msg.content:
721
+ if isinstance(block, ToolUseBlock):
722
+ tool_uses_by_id[block.id] = {"name": block.name, "input": block.input}
723
+ replay_items.append({
724
+ "role": "tool",
725
+ "text": f"{block.name} {json.dumps(block.input, ensure_ascii=True)}",
726
+ "tool_name": block.name,
727
+ "tool_input": block.input,
728
+ "tool_use_id": block.id,
729
+ })
730
+ await replace_transcript_items(replay_items)
731
+ if result.message and command_result_emitter is not None:
732
+ await command_result_emitter(result.message, "info")
733
+ return
734
+ elif result.clear_screen:
735
+ await clear_output()
736
+ if result.replay_messages and render_event is not None:
737
+ from illusion.engine.stream_events import AssistantTurnComplete
738
+ from illusion.api.usage import UsageSnapshot
739
+ from illusion.engine.messages import ToolUseBlock, ToolResultBlock
740
+
741
+ await clear_output()
742
+ tool_uses_by_id: dict[str, dict] = {}
743
+ for msg in result.replay_messages:
744
+ if msg.role == "user":
745
+ if msg.text.strip():
746
+ if replay_transcript_item is not None:
747
+ await replay_transcript_item({"role": "user", "text": msg.text})
748
+ else:
749
+ await print_system(f"> {msg.text}")
750
+ for block in msg.content:
751
+ if isinstance(block, ToolResultBlock) and replay_transcript_item is not None:
752
+ tool_info = tool_uses_by_id.get(block.tool_use_id, {})
753
+ await replay_transcript_item({
754
+ "role": "tool_result",
755
+ "text": block.text_content,
756
+ "tool_name": tool_info.get("name"),
757
+ "tool_use_id": block.tool_use_id,
758
+ "is_error": block.is_error,
759
+ })
760
+ elif msg.role == "assistant":
761
+ reasoning = msg.thinking_text.strip()
762
+ assistant_text = msg.text.strip()
763
+ if replay_transcript_item is not None and (assistant_text or reasoning):
764
+ item = {"role": "assistant", "text": assistant_text}
765
+ if reasoning:
766
+ item["reasoning"] = reasoning
767
+ await replay_transcript_item(item)
768
+ elif assistant_text:
769
+ await render_event(AssistantTurnComplete(message=msg, usage=UsageSnapshot()))
770
+ for block in msg.content:
771
+ if isinstance(block, ToolUseBlock):
772
+ tool_uses_by_id[block.id] = {"name": block.name, "input": block.input}
773
+ if replay_transcript_item is not None:
774
+ await replay_transcript_item({
775
+ "role": "tool",
776
+ "text": f"{block.name} {json.dumps(block.input, ensure_ascii=True)}",
777
+ "tool_name": block.name,
778
+ "tool_input": block.input,
779
+ "tool_use_id": block.id,
780
+ })
781
+ elif result.clear_screen:
782
+ await clear_output()
783
+ if result.message and not result.replay_messages:
784
+ if command_result_emitter is not None:
785
+ await command_result_emitter(result.message, "info")
786
+ else:
787
+ await print_system(result.message)