illusion-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
进程内代理执行后端模块
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
本模块实现进程内的代理执行后端。
|
|
6
|
+
使用 :mod:`contextvars` 在当前 Python 进程中将代理作为 asyncio Task 运行,
|
|
7
|
+
实现每个代理的上下文隔离。
|
|
8
|
+
|
|
9
|
+
主要组件:
|
|
10
|
+
- InProcessBackend: 进程内执行后端,实现 TeammateExecutor 协议
|
|
11
|
+
|
|
12
|
+
使用示例:
|
|
13
|
+
>>> from illusion.swarm.in_process import InProcessBackend
|
|
14
|
+
>>> backend = InProcessBackend()
|
|
15
|
+
>>> result = await backend.spawn(config)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import contextlib
|
|
22
|
+
import logging
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
|
|
27
|
+
from illusion.swarm.agent_executor import (
|
|
28
|
+
AgentExecutionContext,
|
|
29
|
+
AgentSpawnConfig,
|
|
30
|
+
AgentAbortController,
|
|
31
|
+
set_agent_context,
|
|
32
|
+
_register_agent,
|
|
33
|
+
_unregister_agent,
|
|
34
|
+
)
|
|
35
|
+
from illusion.swarm.types import (
|
|
36
|
+
BackendType,
|
|
37
|
+
SpawnResult,
|
|
38
|
+
TeammateMessage,
|
|
39
|
+
TeammateSpawnConfig,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# InProcessBackend
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class _AgentEntry:
|
|
52
|
+
"""运行中进程内代理的内部注册表条目。"""
|
|
53
|
+
|
|
54
|
+
task: asyncio.Task[None]
|
|
55
|
+
abort_controller: AgentAbortController
|
|
56
|
+
task_id: str
|
|
57
|
+
started_at: float = field(default_factory=time.time)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InProcessBackend:
|
|
61
|
+
"""将代理作为当前进程中的 asyncio Task 运行的 TeammateExecutor。"""
|
|
62
|
+
|
|
63
|
+
type: BackendType = "in_process"
|
|
64
|
+
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
self._active: dict[str, _AgentEntry] = {}
|
|
67
|
+
|
|
68
|
+
def is_available(self) -> bool:
|
|
69
|
+
"""进程内后端始终可用。"""
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
async def spawn(self, config: TeammateSpawnConfig) -> SpawnResult:
|
|
73
|
+
"""将进程内代理生成为 asyncio Task。"""
|
|
74
|
+
agent_id = f"{config.name}@{config.team}"
|
|
75
|
+
task_id = f"in_process_{uuid.uuid4().hex[:12]}"
|
|
76
|
+
|
|
77
|
+
# 检查是否已存在活跃的同名代理
|
|
78
|
+
if agent_id in self._active:
|
|
79
|
+
entry = self._active[agent_id]
|
|
80
|
+
if not entry.task.done():
|
|
81
|
+
logger.warning("[InProcessBackend] spawn(): %s is already running", agent_id)
|
|
82
|
+
return SpawnResult(
|
|
83
|
+
task_id=task_id,
|
|
84
|
+
agent_id=agent_id,
|
|
85
|
+
backend_type=self.type,
|
|
86
|
+
success=False,
|
|
87
|
+
error=f"Agent {agent_id!r} is already running",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 创建中止控制器
|
|
91
|
+
abort_controller = AgentAbortController()
|
|
92
|
+
|
|
93
|
+
# 创建代理生成配置
|
|
94
|
+
spawn_config = AgentSpawnConfig(
|
|
95
|
+
name=config.name,
|
|
96
|
+
prompt=config.prompt,
|
|
97
|
+
cwd=config.cwd,
|
|
98
|
+
model=config.model,
|
|
99
|
+
system_prompt=config.system_prompt,
|
|
100
|
+
permission_mode=None,
|
|
101
|
+
parent_session_id=config.parent_session_id,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# 预先创建并注册执行上下文,以便 send_message 可以立即找到代理
|
|
105
|
+
from illusion.swarm.agent_executor import AgentExecutionContext, _register_agent
|
|
106
|
+
ctx = AgentExecutionContext(
|
|
107
|
+
agent_id=agent_id,
|
|
108
|
+
agent_name=config.name,
|
|
109
|
+
prompt=config.prompt,
|
|
110
|
+
model=config.model,
|
|
111
|
+
cwd=__import__("pathlib").Path(config.cwd),
|
|
112
|
+
abort_controller=abort_controller,
|
|
113
|
+
)
|
|
114
|
+
_register_agent(ctx)
|
|
115
|
+
|
|
116
|
+
# 创建 asyncio Task
|
|
117
|
+
task = asyncio.create_task(
|
|
118
|
+
self._run_agent(spawn_config, agent_id, abort_controller, ctx),
|
|
119
|
+
name=f"agent-{agent_id}",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
entry = _AgentEntry(
|
|
123
|
+
task=task,
|
|
124
|
+
abort_controller=abort_controller,
|
|
125
|
+
task_id=task_id,
|
|
126
|
+
)
|
|
127
|
+
self._active[agent_id] = entry
|
|
128
|
+
|
|
129
|
+
# 添加完成回调
|
|
130
|
+
def _on_done(t: asyncio.Task[None]) -> None:
|
|
131
|
+
self._active.pop(agent_id, None)
|
|
132
|
+
if not t.cancelled() and t.exception() is not None:
|
|
133
|
+
logger.error("[InProcessBackend] Agent %s raised exception: %s", agent_id, t.exception())
|
|
134
|
+
|
|
135
|
+
task.add_done_callback(_on_done)
|
|
136
|
+
|
|
137
|
+
logger.debug("[InProcessBackend] spawned %s (task_id=%s)", agent_id, task_id)
|
|
138
|
+
return SpawnResult(
|
|
139
|
+
task_id=task_id,
|
|
140
|
+
agent_id=agent_id,
|
|
141
|
+
backend_type=self.type,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def _run_agent(
|
|
145
|
+
self,
|
|
146
|
+
config: AgentSpawnConfig,
|
|
147
|
+
agent_id: str,
|
|
148
|
+
abort_controller: AgentAbortController,
|
|
149
|
+
ctx: AgentExecutionContext | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""运行代理的内部协程。"""
|
|
152
|
+
if ctx is None:
|
|
153
|
+
ctx = AgentExecutionContext(
|
|
154
|
+
agent_id=agent_id,
|
|
155
|
+
agent_name=config.name,
|
|
156
|
+
prompt=config.prompt,
|
|
157
|
+
model=config.model,
|
|
158
|
+
cwd=__import__("pathlib").Path(config.cwd),
|
|
159
|
+
abort_controller=abort_controller,
|
|
160
|
+
)
|
|
161
|
+
_register_agent(ctx)
|
|
162
|
+
set_agent_context(ctx)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
# 注意:这里需要 query_context 和 parent_registry
|
|
166
|
+
# 在实际使用中,这些应该通过某种方式传入
|
|
167
|
+
# 目前这是一个占位实现
|
|
168
|
+
logger.info("[InProcessBackend] %s: agent started (stub)", agent_id)
|
|
169
|
+
ctx.status = "running"
|
|
170
|
+
|
|
171
|
+
# 等待取消或完成
|
|
172
|
+
while not abort_controller.is_cancelled:
|
|
173
|
+
await asyncio.sleep(0.1)
|
|
174
|
+
|
|
175
|
+
except asyncio.CancelledError:
|
|
176
|
+
logger.debug("[InProcessBackend] %s: task cancelled", agent_id)
|
|
177
|
+
raise
|
|
178
|
+
except Exception:
|
|
179
|
+
logger.exception("[InProcessBackend] %s: unhandled exception", agent_id)
|
|
180
|
+
finally:
|
|
181
|
+
ctx.status = "stopped"
|
|
182
|
+
_unregister_agent(agent_id)
|
|
183
|
+
|
|
184
|
+
async def send_message(self, agent_id: str, message: TeammateMessage) -> None:
|
|
185
|
+
"""向运行中的代理发送消息。"""
|
|
186
|
+
# 首先检查 self._active 中是否有该代理
|
|
187
|
+
entry = self._active.get(agent_id)
|
|
188
|
+
if entry is not None and not entry.task.done():
|
|
189
|
+
# 代理正在运行,但需要找到其 AgentExecutionContext
|
|
190
|
+
# 从全局注册表中查找
|
|
191
|
+
from illusion.swarm.agent_executor import get_active_agent
|
|
192
|
+
agent_ctx = get_active_agent(agent_id)
|
|
193
|
+
if agent_ctx is not None:
|
|
194
|
+
await agent_ctx.message_queue.put(message)
|
|
195
|
+
logger.debug("[InProcessBackend] sent message to %s", agent_id)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# 回退:尝试从全局注册表查找
|
|
199
|
+
from illusion.swarm.agent_executor import get_active_agent, get_active_agent_by_name
|
|
200
|
+
agent_name = agent_id.split("@")[0] if "@" in agent_id else agent_id
|
|
201
|
+
|
|
202
|
+
agent_ctx = get_active_agent(agent_id)
|
|
203
|
+
if agent_ctx is None:
|
|
204
|
+
agent_ctx = get_active_agent_by_name(agent_name)
|
|
205
|
+
|
|
206
|
+
if agent_ctx is not None:
|
|
207
|
+
await agent_ctx.message_queue.put(message)
|
|
208
|
+
logger.debug("[InProcessBackend] sent message to %s", agent_id)
|
|
209
|
+
else:
|
|
210
|
+
raise ValueError(f"No active agent found for {agent_id!r}")
|
|
211
|
+
|
|
212
|
+
async def shutdown(self, agent_id: str, *, force: bool = False, timeout: float = 10.0) -> bool:
|
|
213
|
+
"""终止运行中的进程内代理。"""
|
|
214
|
+
entry = self._active.get(agent_id)
|
|
215
|
+
if entry is None:
|
|
216
|
+
logger.debug("[InProcessBackend] shutdown(): %s not found", agent_id)
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
if entry.task.done():
|
|
220
|
+
self._active.pop(agent_id, None)
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
if force:
|
|
224
|
+
entry.abort_controller.request_cancel(reason="force shutdown", force=True)
|
|
225
|
+
entry.task.cancel()
|
|
226
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
227
|
+
await asyncio.wait_for(asyncio.shield(entry.task), timeout=timeout)
|
|
228
|
+
else:
|
|
229
|
+
entry.abort_controller.request_cancel(reason="graceful shutdown")
|
|
230
|
+
try:
|
|
231
|
+
await asyncio.wait_for(asyncio.shield(entry.task), timeout=timeout)
|
|
232
|
+
except asyncio.TimeoutError:
|
|
233
|
+
logger.warning("[InProcessBackend] %s did not exit within %.1fs — forcing", agent_id, timeout)
|
|
234
|
+
entry.abort_controller.request_cancel(reason="timeout — forcing", force=True)
|
|
235
|
+
entry.task.cancel()
|
|
236
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
237
|
+
await entry.task
|
|
238
|
+
|
|
239
|
+
self._active.pop(agent_id, None)
|
|
240
|
+
logger.debug("[InProcessBackend] shut down %s", agent_id)
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
def list_agents(self) -> list[tuple[str, bool, float]]:
|
|
244
|
+
"""返回 (agent_id, is_running, duration_seconds) 元组列表。"""
|
|
245
|
+
now = time.time()
|
|
246
|
+
result = []
|
|
247
|
+
for agent_id, entry in self._active.items():
|
|
248
|
+
is_running = not entry.task.done()
|
|
249
|
+
duration = now - entry.started_at
|
|
250
|
+
result.append((agent_id, is_running, duration))
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
async def shutdown_all(self, *, force: bool = False, timeout: float = 10.0) -> None:
|
|
254
|
+
"""终止所有活跃代理。"""
|
|
255
|
+
agent_ids = list(self._active.keys())
|
|
256
|
+
await asyncio.gather(
|
|
257
|
+
*(self.shutdown(aid, force=force, timeout=timeout) for aid in agent_ids),
|
|
258
|
+
return_exceptions=True,
|
|
259
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
子进程代理执行后端模块
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
本模块实现基于子进程的 TeammateExecutor 接口。
|
|
6
|
+
使用现有的 :class:`~illusion.tasks.manager.BackgroundTaskManager`
|
|
7
|
+
来创建和管理子进程,通过 stdin/stdout 进行通信。
|
|
8
|
+
|
|
9
|
+
主要组件:
|
|
10
|
+
- SubprocessBackend: 子进程执行后端
|
|
11
|
+
|
|
12
|
+
使用示例:
|
|
13
|
+
>>> from illusion.swarm.subprocess_backend import SubprocessBackend
|
|
14
|
+
>>> backend = SubprocessBackend()
|
|
15
|
+
>>> result = await backend.spawn(config)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
from illusion.swarm.agent_executor import (
|
|
24
|
+
_build_agent_cli_flags,
|
|
25
|
+
_build_agent_env_vars,
|
|
26
|
+
_get_agent_command,
|
|
27
|
+
)
|
|
28
|
+
from illusion.swarm.types import (
|
|
29
|
+
BackendType,
|
|
30
|
+
SpawnResult,
|
|
31
|
+
TeammateMessage,
|
|
32
|
+
TeammateSpawnConfig,
|
|
33
|
+
)
|
|
34
|
+
from illusion.tasks.manager import get_task_manager
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SubprocessBackend:
|
|
40
|
+
"""TeammateExecutor 实现,每个代理作为独立子进程运行。"""
|
|
41
|
+
|
|
42
|
+
type: BackendType = "subprocess"
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._agent_tasks: dict[str, str] = {}
|
|
46
|
+
|
|
47
|
+
def is_available(self) -> bool:
|
|
48
|
+
"""子进程后端始终可用。"""
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
async def spawn(self, config: TeammateSpawnConfig) -> SpawnResult:
|
|
52
|
+
"""通过任务管理器作为子进程生成新代理。"""
|
|
53
|
+
agent_id = f"{config.name}@{config.team}"
|
|
54
|
+
|
|
55
|
+
# 构建 CLI 命令
|
|
56
|
+
flags = _build_agent_cli_flags(
|
|
57
|
+
model=config.model,
|
|
58
|
+
permission_mode=None,
|
|
59
|
+
)
|
|
60
|
+
extra_env = _build_agent_env_vars()
|
|
61
|
+
env_prefix = " ".join(f"{k}={v!r}" for k, v in extra_env.items())
|
|
62
|
+
|
|
63
|
+
agent_cmd = _get_agent_command()
|
|
64
|
+
cmd_parts = [agent_cmd, "-m", "illusion"] + flags
|
|
65
|
+
command = f"{env_prefix} {' '.join(cmd_parts)}" if env_prefix else " ".join(cmd_parts)
|
|
66
|
+
|
|
67
|
+
# 创建任务
|
|
68
|
+
manager = get_task_manager()
|
|
69
|
+
try:
|
|
70
|
+
record = await manager.create_agent_task(
|
|
71
|
+
prompt=config.prompt,
|
|
72
|
+
description=f"Agent: {agent_id}",
|
|
73
|
+
cwd=config.cwd,
|
|
74
|
+
task_type="in_process_teammate",
|
|
75
|
+
model=config.model,
|
|
76
|
+
command=command,
|
|
77
|
+
)
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
logger.error("[SubprocessBackend] Failed to spawn agent %s: %s", agent_id, exc)
|
|
80
|
+
return SpawnResult(
|
|
81
|
+
task_id="",
|
|
82
|
+
agent_id=agent_id,
|
|
83
|
+
backend_type=self.type,
|
|
84
|
+
success=False,
|
|
85
|
+
error=str(exc),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self._agent_tasks[agent_id] = record.id
|
|
89
|
+
logger.debug("[SubprocessBackend] Spawned agent %s as task %s", agent_id, record.id)
|
|
90
|
+
return SpawnResult(
|
|
91
|
+
task_id=record.id,
|
|
92
|
+
agent_id=agent_id,
|
|
93
|
+
backend_type=self.type,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def send_message(self, agent_id: str, message: TeammateMessage) -> None:
|
|
97
|
+
"""通过 stdin 向运行中的代理发送消息。"""
|
|
98
|
+
task_id = self._agent_tasks.get(agent_id)
|
|
99
|
+
if task_id is None:
|
|
100
|
+
raise ValueError(f"No active subprocess for agent {agent_id!r}")
|
|
101
|
+
|
|
102
|
+
payload = {
|
|
103
|
+
"text": message.text,
|
|
104
|
+
"from": message.from_agent,
|
|
105
|
+
"timestamp": message.timestamp,
|
|
106
|
+
}
|
|
107
|
+
if message.color:
|
|
108
|
+
payload["color"] = message.color
|
|
109
|
+
if message.summary:
|
|
110
|
+
payload["summary"] = message.summary
|
|
111
|
+
|
|
112
|
+
manager = get_task_manager()
|
|
113
|
+
await manager.write_to_task(task_id, json.dumps(payload))
|
|
114
|
+
logger.debug("[SubprocessBackend] Sent message to %s (task %s)", agent_id, task_id)
|
|
115
|
+
|
|
116
|
+
async def shutdown(self, agent_id: str, *, force: bool = False) -> bool:
|
|
117
|
+
"""终止子进程代理。"""
|
|
118
|
+
task_id = self._agent_tasks.get(agent_id)
|
|
119
|
+
if task_id is None:
|
|
120
|
+
logger.warning("[SubprocessBackend] shutdown() called for unknown agent %s", agent_id)
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
manager = get_task_manager()
|
|
124
|
+
try:
|
|
125
|
+
await manager.stop_task(task_id)
|
|
126
|
+
except ValueError as exc:
|
|
127
|
+
logger.debug("[SubprocessBackend] stop_task for %s: %s", task_id, exc)
|
|
128
|
+
finally:
|
|
129
|
+
self._agent_tasks.pop(agent_id, None)
|
|
130
|
+
|
|
131
|
+
logger.debug("[SubprocessBackend] Shut down agent %s (task %s)", agent_id, task_id)
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
def get_task_id(self, agent_id: str) -> str | None:
|
|
135
|
+
"""返回给定代理的任务管理器任务 ID。"""
|
|
136
|
+
return self._agent_tasks.get(agent_id)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Swarm 团队辅助模块
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
本模块提供 team_create / team_delete 相关的团队文件与目录管理能力。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 团队名称规范化
|
|
9
|
+
- 团队配置文件读写
|
|
10
|
+
- 团队任务目录初始化与重置
|
|
11
|
+
- 会话级团队清理注册
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from illusion.config.paths import get_config_dir, get_data_dir
|
|
23
|
+
|
|
24
|
+
# 团队负责人固定名称,与上游行为对齐
|
|
25
|
+
TEAM_LEAD_NAME = "team-lead"
|
|
26
|
+
|
|
27
|
+
# 当前会话中创建的团队集合(用于会话结束时兜底清理)
|
|
28
|
+
_SESSION_CREATED_TEAMS: set[str] = set()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sanitize_name(name: str) -> str:
|
|
32
|
+
"""将名称转换为适合文件系统与标识符的 slug。"""
|
|
33
|
+
sanitized = re.sub(r"[^a-zA-Z0-9]", "-", name).strip("-").lower()
|
|
34
|
+
return sanitized or "team"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_team_dir(team_name: str) -> Path:
|
|
38
|
+
"""返回团队目录路径(~/.illusion/teams/{team}/)。"""
|
|
39
|
+
return get_config_dir() / "teams" / sanitize_name(team_name)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_team_file_path(team_name: str) -> Path:
|
|
43
|
+
"""返回团队配置文件路径。"""
|
|
44
|
+
return get_team_dir(team_name) / "config.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_team_tasks_dir(task_list_id: str) -> Path:
|
|
48
|
+
"""返回团队任务目录路径(~/.illusion/data/tasks/{taskListId}/)。"""
|
|
49
|
+
return get_data_dir() / "tasks" / sanitize_name(task_list_id)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_team_file(team_name: str) -> dict[str, Any] | None:
|
|
53
|
+
"""读取团队配置文件。"""
|
|
54
|
+
path = get_team_file_path(team_name)
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return None
|
|
57
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
58
|
+
data = json.load(handle)
|
|
59
|
+
if isinstance(data, dict):
|
|
60
|
+
return data
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def write_team_file(team_name: str, team_file: dict[str, Any]) -> None:
|
|
65
|
+
"""写入团队配置文件。"""
|
|
66
|
+
path = get_team_file_path(team_name)
|
|
67
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with path.open("w", encoding="utf-8") as handle:
|
|
69
|
+
json.dump(team_file, handle, ensure_ascii=False, indent=2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ensure_tasks_dir(task_list_id: str) -> None:
|
|
73
|
+
"""确保团队任务目录存在。"""
|
|
74
|
+
get_team_tasks_dir(task_list_id).mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def reset_task_list(task_list_id: str) -> None:
|
|
78
|
+
"""重置团队任务目录内容。"""
|
|
79
|
+
tasks_dir = get_team_tasks_dir(task_list_id)
|
|
80
|
+
tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
for child in tasks_dir.iterdir():
|
|
82
|
+
if child.is_dir():
|
|
83
|
+
shutil.rmtree(child, ignore_errors=True)
|
|
84
|
+
else:
|
|
85
|
+
child.unlink(missing_ok=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def register_team_for_session_cleanup(team_name: str) -> None:
|
|
89
|
+
"""将团队登记到会话结束清理列表。"""
|
|
90
|
+
_SESSION_CREATED_TEAMS.add(team_name)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def unregister_team_for_session_cleanup(team_name: str) -> None:
|
|
94
|
+
"""从会话结束清理列表中移除团队。"""
|
|
95
|
+
_SESSION_CREATED_TEAMS.discard(team_name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cleanup_team_directories(team_name: str) -> None:
|
|
99
|
+
"""清理团队目录和团队任务目录。"""
|
|
100
|
+
team_file = read_team_file(team_name)
|
|
101
|
+
if team_file:
|
|
102
|
+
members = team_file.get("members", [])
|
|
103
|
+
if isinstance(members, list):
|
|
104
|
+
for member in members:
|
|
105
|
+
if not isinstance(member, dict):
|
|
106
|
+
continue
|
|
107
|
+
worktree_path = member.get("worktreePath")
|
|
108
|
+
if isinstance(worktree_path, str) and worktree_path.strip():
|
|
109
|
+
shutil.rmtree(Path(worktree_path), ignore_errors=True)
|
|
110
|
+
|
|
111
|
+
shutil.rmtree(get_team_dir(team_name), ignore_errors=True)
|
|
112
|
+
shutil.rmtree(get_team_tasks_dir(team_name), ignore_errors=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def cleanup_session_teams() -> None:
|
|
116
|
+
"""清理当前会话创建但未显式删除的团队。"""
|
|
117
|
+
if not _SESSION_CREATED_TEAMS:
|
|
118
|
+
return
|
|
119
|
+
teams = list(_SESSION_CREATED_TEAMS)
|
|
120
|
+
for team_name in teams:
|
|
121
|
+
cleanup_team_directories(team_name)
|
|
122
|
+
_SESSION_CREATED_TEAMS.clear()
|
|
123
|
+
|
illusion/swarm/types.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Swarm 后端类型定义模块
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
本模块定义 Agent 派发功能使用的所有类型和协议。
|
|
6
|
+
包括后端类型、代理生成配置、消息类型等。
|
|
7
|
+
|
|
8
|
+
类型定义:
|
|
9
|
+
- BackendType: 支持的后端类型
|
|
10
|
+
- TeammateSpawnConfig: 代理生成配置
|
|
11
|
+
- SpawnResult: 生成结果
|
|
12
|
+
- TeammateMessage: 代理间消息
|
|
13
|
+
|
|
14
|
+
协议:
|
|
15
|
+
- TeammateExecutor: 代理执行器协议
|
|
16
|
+
|
|
17
|
+
使用示例:
|
|
18
|
+
>>> from illusion.swarm.types import BackendType, TeammateExecutor, SpawnResult
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Literal, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# 后端类型字面量
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
BackendType = Literal["subprocess", "in_process"]
|
|
32
|
+
"""所有支持的后端类型。"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# 代理生成配置
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TeammateSpawnConfig:
|
|
42
|
+
"""生成代理的配置。"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
"""人类可读的代理名称(例如 ``"researcher"``)。"""
|
|
46
|
+
|
|
47
|
+
team: str
|
|
48
|
+
"""此代理所属的团队名称。"""
|
|
49
|
+
|
|
50
|
+
prompt: str
|
|
51
|
+
"""代理的初始提示词/任务。"""
|
|
52
|
+
|
|
53
|
+
cwd: str
|
|
54
|
+
"""代理的工作目录。"""
|
|
55
|
+
|
|
56
|
+
parent_session_id: str
|
|
57
|
+
"""父会话 ID(用于转录关联)。"""
|
|
58
|
+
|
|
59
|
+
model: str | None = None
|
|
60
|
+
"""此代理的模型覆盖。"""
|
|
61
|
+
|
|
62
|
+
system_prompt: str | None = None
|
|
63
|
+
"""代理的系统提示词。"""
|
|
64
|
+
|
|
65
|
+
system_prompt_mode: Literal["default", "replace", "append"] | None = None
|
|
66
|
+
"""如何应用系统提示词:替换或追加到默认。"""
|
|
67
|
+
|
|
68
|
+
color: str | None = None
|
|
69
|
+
"""代理的可选 UI 颜色。"""
|
|
70
|
+
|
|
71
|
+
color_override: str | None = None
|
|
72
|
+
"""明确的颜色覆盖(优先于 ``color``)。"""
|
|
73
|
+
|
|
74
|
+
permissions: list[str] = field(default_factory=list)
|
|
75
|
+
"""授予此代理的工具权限。"""
|
|
76
|
+
|
|
77
|
+
plan_mode_required: bool = False
|
|
78
|
+
"""此代理是否必须在实现前进入 plan 模式。"""
|
|
79
|
+
|
|
80
|
+
allow_permission_prompts: bool = False
|
|
81
|
+
"""当为 False(默认)时,未列出的工具被自动拒绝。"""
|
|
82
|
+
|
|
83
|
+
worktree_path: str | None = None
|
|
84
|
+
"""可选的 git worktree 路径,用于隔离的文件系统访问。"""
|
|
85
|
+
|
|
86
|
+
session_id: str | None = None
|
|
87
|
+
"""明确的会话 ID(如果未提供则生成)。"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# 生成结果和消息
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class SpawnResult:
|
|
97
|
+
"""生成代理的结果。"""
|
|
98
|
+
|
|
99
|
+
task_id: str
|
|
100
|
+
"""任务管理器中的任务 ID。"""
|
|
101
|
+
|
|
102
|
+
agent_id: str
|
|
103
|
+
"""唯一代理标识符。"""
|
|
104
|
+
|
|
105
|
+
backend_type: BackendType
|
|
106
|
+
"""用于生成此代理的后端。"""
|
|
107
|
+
|
|
108
|
+
success: bool = True
|
|
109
|
+
error: str | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class TeammateMessage:
|
|
114
|
+
"""发送给代理的消息。"""
|
|
115
|
+
|
|
116
|
+
text: str
|
|
117
|
+
from_agent: str
|
|
118
|
+
color: str | None = None
|
|
119
|
+
timestamp: str | None = None
|
|
120
|
+
summary: str | None = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# TeammateExecutor 协议
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@runtime_checkable
|
|
129
|
+
class TeammateExecutor(Protocol):
|
|
130
|
+
"""代理执行后端的协议。
|
|
131
|
+
|
|
132
|
+
抽象化跨子进程和进程内后端的生成/消息/关闭操作。
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
type: BackendType
|
|
136
|
+
|
|
137
|
+
def is_available(self) -> bool:
|
|
138
|
+
"""检查此后端在系统上是否可用。"""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
async def spawn(self, config: TeammateSpawnConfig) -> SpawnResult:
|
|
142
|
+
"""使用给定配置生成新代理。"""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
async def send_message(self, agent_id: str, message: TeammateMessage) -> None:
|
|
146
|
+
"""向运行中的代理发送消息。"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
async def shutdown(self, agent_id: str, *, force: bool = False) -> bool:
|
|
150
|
+
"""终止代理。
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
agent_id: 要终止的代理。
|
|
154
|
+
force: 如果为 True,立即杀死。如果为 False,尝试优雅关闭。
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
如果代理成功终止返回 True。
|
|
158
|
+
"""
|
|
159
|
+
...
|