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,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
后台任务管理器模块
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
本模块管理后台 shell 和 agent 子进程任务。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 创建待处理任务
|
|
9
|
+
- 创建 Shell 任务
|
|
10
|
+
- 创建 Agent 任务
|
|
11
|
+
- 更新任务
|
|
12
|
+
- 停止任务
|
|
13
|
+
- 读写任务输出
|
|
14
|
+
|
|
15
|
+
类说明:
|
|
16
|
+
- BackgroundTaskManager: 后台任务管理器类
|
|
17
|
+
- create_pending_task: 创建待处理任务
|
|
18
|
+
- create_shell_task: 创建 Shell 任务
|
|
19
|
+
- create_agent_task: 创建 Agent 任务
|
|
20
|
+
- update_task: 更新任务
|
|
21
|
+
- stop_task: 停止任务
|
|
22
|
+
|
|
23
|
+
使用示例:
|
|
24
|
+
>>> from illusion.tasks.manager import BackgroundTaskManager, get_task_manager
|
|
25
|
+
>>> # 获取任务管理器
|
|
26
|
+
>>> manager = get_task_manager()
|
|
27
|
+
>>> # 创建 Shell 任务
|
|
28
|
+
>>> record = await manager.create_shell_task(command="ls -la", description="列出文件", cwd=".")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
import os
|
|
35
|
+
import shlex
|
|
36
|
+
import time
|
|
37
|
+
from dataclasses import replace
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from uuid import uuid4
|
|
40
|
+
|
|
41
|
+
from illusion.config.paths import get_tasks_dir
|
|
42
|
+
from illusion.tasks.types import TaskRecord, TaskStatus, TaskType, to_task_internal_status
|
|
43
|
+
from illusion.utils.shell import create_shell_subprocess
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BackgroundTaskManager:
|
|
47
|
+
"""管理 shell 和 agent 子进程任务。"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._tasks: dict[str, TaskRecord] = {}
|
|
51
|
+
self._processes: dict[str, asyncio.subprocess.Process] = {}
|
|
52
|
+
self._waiters: dict[str, asyncio.Task[None]] = {}
|
|
53
|
+
self._output_locks: dict[str, asyncio.Lock] = {}
|
|
54
|
+
self._input_locks: dict[str, asyncio.Lock] = {}
|
|
55
|
+
self._generations: dict[str, int] = {}
|
|
56
|
+
|
|
57
|
+
def create_pending_task(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
subject: str,
|
|
61
|
+
description: str,
|
|
62
|
+
active_form: str | None = None,
|
|
63
|
+
) -> TaskRecord:
|
|
64
|
+
"""创建用于跟踪的待处理任务(非后台进程)。"""
|
|
65
|
+
task_id = _task_id("in_process_teammate")
|
|
66
|
+
output_path = get_tasks_dir() / f"{task_id}.log"
|
|
67
|
+
record = TaskRecord(
|
|
68
|
+
id=task_id,
|
|
69
|
+
type="in_process_teammate",
|
|
70
|
+
status="pending",
|
|
71
|
+
description=description,
|
|
72
|
+
subject=subject,
|
|
73
|
+
active_form=active_form,
|
|
74
|
+
cwd=str(Path.cwd().resolve()),
|
|
75
|
+
output_file=output_path,
|
|
76
|
+
created_at=time.time(),
|
|
77
|
+
)
|
|
78
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
output_path.write_text("", encoding="utf-8")
|
|
80
|
+
self._tasks[task_id] = record
|
|
81
|
+
return record
|
|
82
|
+
|
|
83
|
+
async def create_shell_task(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
command: str,
|
|
87
|
+
description: str,
|
|
88
|
+
cwd: str | Path,
|
|
89
|
+
task_type: TaskType = "local_bash",
|
|
90
|
+
) -> TaskRecord:
|
|
91
|
+
"""启动后台 shell 命令。"""
|
|
92
|
+
task_id = _task_id(task_type)
|
|
93
|
+
output_path = get_tasks_dir() / f"{task_id}.log"
|
|
94
|
+
record = TaskRecord(
|
|
95
|
+
id=task_id,
|
|
96
|
+
type=task_type,
|
|
97
|
+
status="running",
|
|
98
|
+
description=description,
|
|
99
|
+
cwd=str(Path(cwd).resolve()),
|
|
100
|
+
output_file=output_path,
|
|
101
|
+
command=command,
|
|
102
|
+
created_at=time.time(),
|
|
103
|
+
started_at=time.time(),
|
|
104
|
+
)
|
|
105
|
+
output_path.write_text("", encoding="utf-8")
|
|
106
|
+
self._tasks[task_id] = record
|
|
107
|
+
self._output_locks[task_id] = asyncio.Lock()
|
|
108
|
+
self._input_locks[task_id] = asyncio.Lock()
|
|
109
|
+
await self._start_process(task_id)
|
|
110
|
+
return record
|
|
111
|
+
|
|
112
|
+
async def create_agent_task(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
prompt: str,
|
|
116
|
+
description: str,
|
|
117
|
+
cwd: str | Path,
|
|
118
|
+
task_type: TaskType = "local_agent",
|
|
119
|
+
model: str | None = None,
|
|
120
|
+
api_key: str | None = None,
|
|
121
|
+
command: str | None = None,
|
|
122
|
+
) -> TaskRecord:
|
|
123
|
+
"""作为子进程启动本地 agent 任务。"""
|
|
124
|
+
if command is None:
|
|
125
|
+
effective_api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
|
126
|
+
if not effective_api_key:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"Local agent tasks require ANTHROPIC_API_KEY or an explicit command override"
|
|
129
|
+
)
|
|
130
|
+
cmd = ["python", "-m", "illusion", "--headless", "--api-key", effective_api_key]
|
|
131
|
+
if model:
|
|
132
|
+
cmd.extend(["--model", model])
|
|
133
|
+
command = " ".join(shlex.quote(part) for part in cmd)
|
|
134
|
+
|
|
135
|
+
record = await self.create_shell_task(
|
|
136
|
+
command=command,
|
|
137
|
+
description=description,
|
|
138
|
+
cwd=cwd,
|
|
139
|
+
task_type=task_type,
|
|
140
|
+
)
|
|
141
|
+
updated = replace(record, prompt=prompt)
|
|
142
|
+
if task_type != "local_agent":
|
|
143
|
+
updated.metadata["agent_mode"] = task_type
|
|
144
|
+
self._tasks[record.id] = updated
|
|
145
|
+
await self.write_to_task(record.id, prompt)
|
|
146
|
+
return updated
|
|
147
|
+
|
|
148
|
+
def get_task(self, task_id: str) -> TaskRecord | None:
|
|
149
|
+
"""返回一个任务记录。"""
|
|
150
|
+
return self._tasks.get(task_id)
|
|
151
|
+
|
|
152
|
+
def list_tasks(self, *, status: TaskStatus | None = None) -> list[TaskRecord]:
|
|
153
|
+
"""返回所有任务,可选按状态过滤。"""
|
|
154
|
+
tasks = list(self._tasks.values())
|
|
155
|
+
if status is not None:
|
|
156
|
+
tasks = [task for task in tasks if task.status == status]
|
|
157
|
+
return sorted(tasks, key=lambda item: item.created_at, reverse=True)
|
|
158
|
+
|
|
159
|
+
def update_task(
|
|
160
|
+
self,
|
|
161
|
+
task_id: str,
|
|
162
|
+
*,
|
|
163
|
+
subject: str | None = None,
|
|
164
|
+
description: str | None = None,
|
|
165
|
+
active_form: str | None = None,
|
|
166
|
+
status: str | None = None,
|
|
167
|
+
owner: str | None = None,
|
|
168
|
+
progress: int | None = None,
|
|
169
|
+
status_note: str | None = None,
|
|
170
|
+
metadata: dict | None = None,
|
|
171
|
+
add_blocks: list[str] | None = None,
|
|
172
|
+
add_blocked_by: list[str] | None = None,
|
|
173
|
+
comments: str | None = None,
|
|
174
|
+
) -> TaskRecord:
|
|
175
|
+
"""更新用于协调和 UI 显示的可变任务元数据。"""
|
|
176
|
+
task = self._require_task(task_id)
|
|
177
|
+
|
|
178
|
+
# 处理删除
|
|
179
|
+
if status == "deleted":
|
|
180
|
+
self._tasks.pop(task_id, None)
|
|
181
|
+
return task
|
|
182
|
+
|
|
183
|
+
if subject is not None:
|
|
184
|
+
task.subject = subject
|
|
185
|
+
if description is not None and description.strip():
|
|
186
|
+
task.description = description.strip()
|
|
187
|
+
if active_form is not None:
|
|
188
|
+
task.active_form = active_form
|
|
189
|
+
if status is not None:
|
|
190
|
+
task.status = to_task_internal_status(status)
|
|
191
|
+
if owner is not None:
|
|
192
|
+
task.owner = owner
|
|
193
|
+
if progress is not None:
|
|
194
|
+
task.metadata["progress"] = str(progress)
|
|
195
|
+
if status_note is not None:
|
|
196
|
+
note = status_note.strip()
|
|
197
|
+
if note:
|
|
198
|
+
task.metadata["status_note"] = note
|
|
199
|
+
else:
|
|
200
|
+
task.metadata.pop("status_note", None)
|
|
201
|
+
if metadata is not None:
|
|
202
|
+
for key, value in metadata.items():
|
|
203
|
+
if value is None:
|
|
204
|
+
task.metadata.pop(key, None)
|
|
205
|
+
else:
|
|
206
|
+
task.metadata[key] = str(value)
|
|
207
|
+
if add_blocks is not None:
|
|
208
|
+
for block_id in add_blocks:
|
|
209
|
+
if block_id not in task.blocks:
|
|
210
|
+
task.blocks.append(block_id)
|
|
211
|
+
if add_blocked_by is not None:
|
|
212
|
+
for blocker_id in add_blocked_by:
|
|
213
|
+
if blocker_id not in task.blocked_by:
|
|
214
|
+
task.blocked_by.append(blocker_id)
|
|
215
|
+
if comments is not None:
|
|
216
|
+
task.comments.append(comments)
|
|
217
|
+
return task
|
|
218
|
+
|
|
219
|
+
async def stop_task(self, task_id: str) -> TaskRecord:
|
|
220
|
+
"""终止运行中的任务。"""
|
|
221
|
+
task = self._require_task(task_id)
|
|
222
|
+
process = self._processes.get(task_id)
|
|
223
|
+
if process is None:
|
|
224
|
+
if task.status in {"completed", "failed", "killed"}:
|
|
225
|
+
return task
|
|
226
|
+
raise ValueError(f"Task {task_id} is not running")
|
|
227
|
+
|
|
228
|
+
process.terminate()
|
|
229
|
+
try:
|
|
230
|
+
await asyncio.wait_for(process.wait(), timeout=3)
|
|
231
|
+
except asyncio.TimeoutError:
|
|
232
|
+
process.kill()
|
|
233
|
+
await process.wait()
|
|
234
|
+
|
|
235
|
+
task.status = "killed"
|
|
236
|
+
task.ended_at = time.time()
|
|
237
|
+
return task
|
|
238
|
+
|
|
239
|
+
async def write_to_task(self, task_id: str, data: str) -> None:
|
|
240
|
+
"""向任务 stdin 写入一行,需要时自动恢复本地 agent。"""
|
|
241
|
+
task = self._require_task(task_id)
|
|
242
|
+
async with self._input_locks[task_id]:
|
|
243
|
+
process = await self._ensure_writable_process(task)
|
|
244
|
+
process.stdin.write((data.rstrip("\n") + "\n").encode("utf-8"))
|
|
245
|
+
try:
|
|
246
|
+
await process.stdin.drain()
|
|
247
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
248
|
+
if task.type not in {"local_agent", "remote_agent", "in_process_teammate"}:
|
|
249
|
+
raise ValueError(f"Task {task_id} does not accept input") from None
|
|
250
|
+
process = await self._restart_agent_task(task)
|
|
251
|
+
process.stdin.write((data.rstrip("\n") + "\n").encode("utf-8"))
|
|
252
|
+
await process.stdin.drain()
|
|
253
|
+
|
|
254
|
+
def read_task_output(self, task_id: str, *, max_bytes: int = 12000) -> str:
|
|
255
|
+
"""返回任务输出文件的尾部。"""
|
|
256
|
+
task = self._require_task(task_id)
|
|
257
|
+
content = task.output_file.read_text(encoding="utf-8", errors="replace")
|
|
258
|
+
if len(content) > max_bytes:
|
|
259
|
+
return content[-max_bytes:]
|
|
260
|
+
return content
|
|
261
|
+
|
|
262
|
+
async def _watch_process(
|
|
263
|
+
self,
|
|
264
|
+
task_id: str,
|
|
265
|
+
process: asyncio.subprocess.Process,
|
|
266
|
+
generation: int,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""监视子进程直到完成。"""
|
|
269
|
+
reader = asyncio.create_task(self._copy_output(task_id, process))
|
|
270
|
+
return_code = await process.wait()
|
|
271
|
+
await reader
|
|
272
|
+
|
|
273
|
+
current_generation = self._generations.get(task_id)
|
|
274
|
+
if current_generation != generation:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
task = self._tasks[task_id]
|
|
278
|
+
task.return_code = return_code
|
|
279
|
+
if task.status != "killed":
|
|
280
|
+
task.status = "completed" if return_code == 0 else "failed"
|
|
281
|
+
task.ended_at = time.time()
|
|
282
|
+
self._processes.pop(task_id, None)
|
|
283
|
+
self._waiters.pop(task_id, None)
|
|
284
|
+
|
|
285
|
+
async def _copy_output(self, task_id: str, process: asyncio.subprocess.Process) -> None:
|
|
286
|
+
"""将进程输出复制到任务输出文件。"""
|
|
287
|
+
if process.stdout is None:
|
|
288
|
+
return
|
|
289
|
+
while True:
|
|
290
|
+
chunk = await process.stdout.read(4096)
|
|
291
|
+
if not chunk:
|
|
292
|
+
return
|
|
293
|
+
async with self._output_locks[task_id]:
|
|
294
|
+
with self._tasks[task_id].output_file.open("ab") as handle:
|
|
295
|
+
handle.write(chunk)
|
|
296
|
+
|
|
297
|
+
def _require_task(self, task_id: str) -> TaskRecord:
|
|
298
|
+
"""返回任务记录,不存在则抛出异常。"""
|
|
299
|
+
task = self._tasks.get(task_id)
|
|
300
|
+
if task is None:
|
|
301
|
+
raise ValueError(f"No task found with ID: {task_id}")
|
|
302
|
+
return task
|
|
303
|
+
|
|
304
|
+
async def _start_process(self, task_id: str) -> asyncio.subprocess.Process:
|
|
305
|
+
"""启动任务进程。"""
|
|
306
|
+
task = self._require_task(task_id)
|
|
307
|
+
if task.command is None:
|
|
308
|
+
raise ValueError(f"Task {task_id} does not have a command to run")
|
|
309
|
+
|
|
310
|
+
generation = self._generations.get(task_id, 0) + 1
|
|
311
|
+
self._generations[task_id] = generation
|
|
312
|
+
process = await create_shell_subprocess(
|
|
313
|
+
task.command,
|
|
314
|
+
cwd=task.cwd,
|
|
315
|
+
stdin=asyncio.subprocess.PIPE,
|
|
316
|
+
stdout=asyncio.subprocess.PIPE,
|
|
317
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
318
|
+
)
|
|
319
|
+
self._processes[task_id] = process
|
|
320
|
+
self._waiters[task_id] = asyncio.create_task(
|
|
321
|
+
self._watch_process(task_id, process, generation)
|
|
322
|
+
)
|
|
323
|
+
return process
|
|
324
|
+
|
|
325
|
+
async def _ensure_writable_process(
|
|
326
|
+
self,
|
|
327
|
+
task: TaskRecord,
|
|
328
|
+
) -> asyncio.subprocess.Process:
|
|
329
|
+
"""确保任务可写入,必要时重启。"""
|
|
330
|
+
process = self._processes.get(task.id)
|
|
331
|
+
if process is not None and process.stdin is not None and process.returncode is None:
|
|
332
|
+
return process
|
|
333
|
+
if task.type not in {"local_agent", "remote_agent", "in_process_teammate"}:
|
|
334
|
+
raise ValueError(f"Task {task.id} does not accept input")
|
|
335
|
+
return await self._restart_agent_task(task)
|
|
336
|
+
|
|
337
|
+
async def _restart_agent_task(self, task: TaskRecord) -> asyncio.subprocess.Process:
|
|
338
|
+
"""重启 agent 任务。"""
|
|
339
|
+
if task.command is None:
|
|
340
|
+
raise ValueError(f"Task {task.id} does not have a restart command")
|
|
341
|
+
|
|
342
|
+
waiter = self._waiters.get(task.id)
|
|
343
|
+
if waiter is not None and not waiter.done():
|
|
344
|
+
await waiter
|
|
345
|
+
|
|
346
|
+
restart_count = int(task.metadata.get("restart_count", "0")) + 1
|
|
347
|
+
task.metadata["restart_count"] = str(restart_count)
|
|
348
|
+
task.status = "running"
|
|
349
|
+
task.started_at = time.time()
|
|
350
|
+
task.ended_at = None
|
|
351
|
+
task.return_code = None
|
|
352
|
+
return await self._start_process(task.id)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# 按任务目录隔离的任务管理器缓存
|
|
356
|
+
_MANAGERS_BY_KEY: dict[str, BackgroundTaskManager] = {}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_task_manager() -> BackgroundTaskManager:
|
|
360
|
+
"""返回单例任务管理器。"""
|
|
361
|
+
current_key = str(get_tasks_dir().resolve())
|
|
362
|
+
manager = _MANAGERS_BY_KEY.get(current_key)
|
|
363
|
+
if manager is None:
|
|
364
|
+
manager = BackgroundTaskManager()
|
|
365
|
+
_MANAGERS_BY_KEY[current_key] = manager
|
|
366
|
+
return manager
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _task_id(task_type: TaskType) -> str:
|
|
370
|
+
"""生成任务 ID 前缀。"""
|
|
371
|
+
prefixes = {
|
|
372
|
+
"local_bash": "b",
|
|
373
|
+
"local_agent": "a",
|
|
374
|
+
"remote_agent": "r",
|
|
375
|
+
"in_process_teammate": "t",
|
|
376
|
+
}
|
|
377
|
+
return f"{prefixes[task_type]}{uuid4().hex[:8]}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
停止任务辅助模块
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供停止运行中任务的辅助函数。
|
|
6
|
+
|
|
7
|
+
使用示例:
|
|
8
|
+
>>> from illusion.tasks.stop_task import stop_task
|
|
9
|
+
>>> # 停止任务
|
|
10
|
+
>>> await stop_task("task_id")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from illusion.tasks.manager import get_task_manager
|
|
16
|
+
from illusion.tasks.types import TaskRecord
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def stop_task(task_id: str) -> TaskRecord:
|
|
20
|
+
"""通过默认任务管理器停止运行中的任务。"""
|
|
21
|
+
return await get_task_manager().stop_task(task_id)
|
illusion/tasks/types.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
任务数据模型模块
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块定义任务相关的数据类型。
|
|
6
|
+
|
|
7
|
+
类型说明:
|
|
8
|
+
- TaskType: 任务类型
|
|
9
|
+
- TaskStatus: 任务状态
|
|
10
|
+
- TaskUpdateStatus: 任务更新状态(含 deleted)
|
|
11
|
+
|
|
12
|
+
类说明:
|
|
13
|
+
- TaskRecord: 后台任务的运行时表示
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Literal, cast
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# 任务类型
|
|
24
|
+
TaskType = Literal["local_bash", "local_agent", "remote_agent", "in_process_teammate"]
|
|
25
|
+
# 任务状态
|
|
26
|
+
TaskStatus = Literal["pending", "running", "completed", "failed", "killed"]
|
|
27
|
+
# 对外显示状态
|
|
28
|
+
TaskDisplayStatus = Literal["pending", "in_progress", "completed", "failed", "killed"]
|
|
29
|
+
|
|
30
|
+
# 扩展状态,包含任务更新操作的 deleted
|
|
31
|
+
TaskUpdateStatus = Literal["pending", "in_progress", "completed", "deleted"]
|
|
32
|
+
|
|
33
|
+
_INTERNAL_TO_DISPLAY_STATUS: dict[str, str] = {
|
|
34
|
+
"pending": "pending",
|
|
35
|
+
"running": "in_progress",
|
|
36
|
+
"completed": "completed",
|
|
37
|
+
"failed": "failed",
|
|
38
|
+
"killed": "killed",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_DISPLAY_TO_INTERNAL_STATUS: dict[str, str] = {
|
|
42
|
+
"pending": "pending",
|
|
43
|
+
"in_progress": "running",
|
|
44
|
+
"completed": "completed",
|
|
45
|
+
"failed": "failed",
|
|
46
|
+
"killed": "killed",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_task_display_status(status: TaskStatus | str) -> TaskDisplayStatus | str:
|
|
51
|
+
"""将内部任务状态转换为对外显示状态。"""
|
|
52
|
+
mapped = _INTERNAL_TO_DISPLAY_STATUS.get(status)
|
|
53
|
+
if mapped is None:
|
|
54
|
+
return status
|
|
55
|
+
return cast(TaskDisplayStatus, mapped)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def to_task_internal_status(status: TaskUpdateStatus | TaskDisplayStatus | TaskStatus | str) -> TaskStatus:
|
|
59
|
+
"""将对外状态转换为内部任务状态。"""
|
|
60
|
+
mapped = _DISPLAY_TO_INTERNAL_STATUS.get(status, status)
|
|
61
|
+
if mapped not in {"pending", "running", "completed", "failed", "killed"}:
|
|
62
|
+
raise ValueError(f"Unsupported task status: {status}")
|
|
63
|
+
return cast(TaskStatus, mapped)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class TaskRecord:
|
|
68
|
+
"""后台任务的运行时表示。"""
|
|
69
|
+
|
|
70
|
+
id: str
|
|
71
|
+
type: TaskType
|
|
72
|
+
status: TaskStatus
|
|
73
|
+
description: str
|
|
74
|
+
cwd: str
|
|
75
|
+
output_file: Path
|
|
76
|
+
command: str | None = None
|
|
77
|
+
prompt: str | None = None
|
|
78
|
+
subject: str | None = None
|
|
79
|
+
active_form: str | None = None
|
|
80
|
+
owner: str | None = None
|
|
81
|
+
blocked_by: list[str] = field(default_factory=list)
|
|
82
|
+
blocks: list[str] = field(default_factory=list)
|
|
83
|
+
created_at: float = 0.0
|
|
84
|
+
started_at: float | None = None
|
|
85
|
+
ended_at: float | None = None
|
|
86
|
+
return_code: int | None = None
|
|
87
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
88
|
+
comments: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
内置工具注册模块
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供 IllusionCode 内置工具的注册和管理功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- BaseTool: 工具抽象基类
|
|
9
|
+
- ToolExecutionContext: 工具执行上下文
|
|
10
|
+
- ToolResult: 工具执行结果
|
|
11
|
+
- ToolRegistry: 工具注册表
|
|
12
|
+
- create_default_tool_registry: 创建默认工具注册表
|
|
13
|
+
|
|
14
|
+
使用示例:
|
|
15
|
+
>>> from illusion.tools import create_default_tool_registry, ToolRegistry
|
|
16
|
+
>>> registry = create_default_tool_registry()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from illusion.tools.ask_user_question_tool import AskUserQuestionTool
|
|
20
|
+
from illusion.tools.agent_tool import AgentTool
|
|
21
|
+
from illusion.tools.bash_tool import BashTool
|
|
22
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolRegistry, ToolResult
|
|
23
|
+
from illusion.tools.config_tool import ConfigTool
|
|
24
|
+
from illusion.tools.cron_tool import CronTool
|
|
25
|
+
from illusion.tools.enter_plan_mode_tool import EnterPlanModeTool
|
|
26
|
+
from illusion.tools.enter_worktree_tool import EnterWorktreeTool
|
|
27
|
+
from illusion.tools.exit_plan_mode_tool import ExitPlanModeTool
|
|
28
|
+
from illusion.tools.exit_worktree_tool import ExitWorktreeTool
|
|
29
|
+
from illusion.tools.file_edit_tool import FileEditTool
|
|
30
|
+
from illusion.tools.file_read_tool import FileReadTool
|
|
31
|
+
from illusion.tools.file_write_tool import FileWriteTool
|
|
32
|
+
from illusion.tools.glob_tool import GlobTool
|
|
33
|
+
from illusion.tools.grep_tool import GrepTool
|
|
34
|
+
from illusion.tools.list_mcp_resources_tool import ListMcpResourcesTool
|
|
35
|
+
from illusion.tools.lsp_tool import LspTool
|
|
36
|
+
from illusion.tools.mcp_auth_tool import McpAuthTool
|
|
37
|
+
from illusion.tools.mcp_tool import McpToolAdapter
|
|
38
|
+
from illusion.tools.notebook_edit_tool import NotebookEditTool
|
|
39
|
+
from illusion.tools.powershell_tool import PowerShellTool
|
|
40
|
+
from illusion.tools.read_mcp_resource_tool import ReadMcpResourceTool
|
|
41
|
+
from illusion.tools.repl_tool import ReplTool
|
|
42
|
+
from illusion.tools.send_message_tool import SendMessageTool
|
|
43
|
+
from illusion.tools.skill_tool import SkillTool
|
|
44
|
+
from illusion.tools.sleep_tool import SleepTool
|
|
45
|
+
from illusion.tools.structured_output_tool import StructuredOutputTool
|
|
46
|
+
from illusion.tools.team_create_tool import TeamCreateTool
|
|
47
|
+
from illusion.tools.team_delete_tool import TeamDeleteTool
|
|
48
|
+
from illusion.tools.task_create_tool import TaskCreateTool
|
|
49
|
+
from illusion.tools.task_get_tool import TaskGetTool
|
|
50
|
+
from illusion.tools.task_list_tool import TaskListTool
|
|
51
|
+
from illusion.tools.task_output_tool import TaskOutputTool
|
|
52
|
+
from illusion.tools.task_stop_tool import TaskStopTool
|
|
53
|
+
from illusion.tools.task_update_tool import TaskUpdateTool
|
|
54
|
+
from illusion.tools.todo_write_tool import TodoWriteTool
|
|
55
|
+
from illusion.tools.tool_search_tool import ToolSearchTool
|
|
56
|
+
from illusion.tools.web_fetch_tool import WebFetchTool
|
|
57
|
+
from illusion.tools.web_search_tool import WebSearchTool
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_default_tool_registry(mcp_manager=None, is_interactive: bool = True) -> ToolRegistry:
|
|
61
|
+
"""返回默认内置工具注册表
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
mcp_manager: MCP 管理器(可选)
|
|
65
|
+
is_interactive: 是否为交互模式(默认True)。非交互模式下会加载StructuredOutputTool。
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ToolRegistry: 工具注册表
|
|
69
|
+
"""
|
|
70
|
+
registry = ToolRegistry()
|
|
71
|
+
tools = [
|
|
72
|
+
BashTool(),
|
|
73
|
+
PowerShellTool(),
|
|
74
|
+
ReplTool(),
|
|
75
|
+
AskUserQuestionTool(),
|
|
76
|
+
FileReadTool(),
|
|
77
|
+
FileWriteTool(),
|
|
78
|
+
FileEditTool(),
|
|
79
|
+
NotebookEditTool(),
|
|
80
|
+
LspTool(),
|
|
81
|
+
McpAuthTool(),
|
|
82
|
+
GlobTool(),
|
|
83
|
+
GrepTool(),
|
|
84
|
+
SkillTool(),
|
|
85
|
+
ToolSearchTool(),
|
|
86
|
+
WebFetchTool(),
|
|
87
|
+
WebSearchTool(),
|
|
88
|
+
ConfigTool(),
|
|
89
|
+
SleepTool(),
|
|
90
|
+
EnterWorktreeTool(),
|
|
91
|
+
ExitWorktreeTool(),
|
|
92
|
+
TodoWriteTool(),
|
|
93
|
+
EnterPlanModeTool(),
|
|
94
|
+
ExitPlanModeTool(),
|
|
95
|
+
CronTool(),
|
|
96
|
+
TaskCreateTool(),
|
|
97
|
+
TaskGetTool(),
|
|
98
|
+
TaskListTool(),
|
|
99
|
+
TaskStopTool(),
|
|
100
|
+
TaskOutputTool(),
|
|
101
|
+
TaskUpdateTool(),
|
|
102
|
+
AgentTool(),
|
|
103
|
+
SendMessageTool(),
|
|
104
|
+
TeamCreateTool(),
|
|
105
|
+
TeamDeleteTool(),
|
|
106
|
+
]
|
|
107
|
+
# StructuredOutputTool 只在非交互模式下加载
|
|
108
|
+
if not is_interactive:
|
|
109
|
+
tools.append(StructuredOutputTool())
|
|
110
|
+
for tool in tools:
|
|
111
|
+
registry.register(tool)
|
|
112
|
+
if mcp_manager is not None:
|
|
113
|
+
registry.register(ListMcpResourcesTool(mcp_manager))
|
|
114
|
+
registry.register(ReadMcpResourceTool(mcp_manager))
|
|
115
|
+
for tool_info in mcp_manager.list_tools():
|
|
116
|
+
registry.register(McpToolAdapter(mcp_manager, tool_info))
|
|
117
|
+
return registry
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"BaseTool",
|
|
122
|
+
"ToolExecutionContext",
|
|
123
|
+
"ToolRegistry",
|
|
124
|
+
"ToolResult",
|
|
125
|
+
"create_default_tool_registry",
|
|
126
|
+
]
|