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,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
最小 REPL shell 执行工具
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
本模块提供在类 REPL 环境中执行 shell 命令的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- ReplTool: REPL 风格的 shell 执行工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import ReplTool
|
|
12
|
+
>>> tool = ReplTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
from illusion.sandbox import SandboxUnavailableError
|
|
23
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
24
|
+
from illusion.utils.shell import create_shell_subprocess
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReplToolInput(BaseModel):
|
|
28
|
+
"""REPL 工具参数。
|
|
29
|
+
|
|
30
|
+
属性:
|
|
31
|
+
command: 要执行的 shell 命令
|
|
32
|
+
cwd: 可选的工作目录覆盖
|
|
33
|
+
timeout_seconds: 超时秒数(1-600)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
command: str = Field(description="Shell command to execute")
|
|
37
|
+
cwd: str | None = Field(default=None, description="Working directory override")
|
|
38
|
+
timeout_seconds: int = Field(default=120, ge=1, le=600)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ReplTool(BaseTool):
|
|
42
|
+
"""使用类 REPL 的执行界面执行 shell 命令。
|
|
43
|
+
|
|
44
|
+
用于在交互式环境中执行命令。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name = "repl"
|
|
48
|
+
description = "Run shell commands using a REPL-style execution surface."
|
|
49
|
+
input_model = ReplToolInput
|
|
50
|
+
|
|
51
|
+
async def execute(self, arguments: ReplToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
52
|
+
# 解析工作目录
|
|
53
|
+
cwd = Path(arguments.cwd).expanduser() if arguments.cwd else context.cwd
|
|
54
|
+
try:
|
|
55
|
+
# 创建 shell 子进程
|
|
56
|
+
process = await create_shell_subprocess(
|
|
57
|
+
arguments.command,
|
|
58
|
+
cwd=cwd,
|
|
59
|
+
stdin=asyncio.subprocess.DEVNULL, # 防止 Windows 上的句柄继承死锁
|
|
60
|
+
stdout=asyncio.subprocess.PIPE,
|
|
61
|
+
stderr=asyncio.subprocess.PIPE,
|
|
62
|
+
)
|
|
63
|
+
except SandboxUnavailableError as exc:
|
|
64
|
+
return ToolResult(output=str(exc), is_error=True)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# 等待命令完成
|
|
68
|
+
stdout, stderr = await asyncio.wait_for(
|
|
69
|
+
process.communicate(),
|
|
70
|
+
timeout=arguments.timeout_seconds,
|
|
71
|
+
)
|
|
72
|
+
except asyncio.TimeoutError:
|
|
73
|
+
# 超时后终止进程
|
|
74
|
+
process.kill()
|
|
75
|
+
await process.wait()
|
|
76
|
+
return ToolResult(
|
|
77
|
+
output=f"Command timed out after {arguments.timeout_seconds} seconds",
|
|
78
|
+
is_error=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# 收集输出
|
|
82
|
+
parts = []
|
|
83
|
+
if stdout:
|
|
84
|
+
parts.append(stdout.decode("utf-8", errors="replace").rstrip())
|
|
85
|
+
if stderr:
|
|
86
|
+
parts.append(stderr.decode("utf-8", errors="replace").rstrip())
|
|
87
|
+
|
|
88
|
+
text = "\n".join(part for part in parts if part).strip()
|
|
89
|
+
if not text:
|
|
90
|
+
text = "(no output)"
|
|
91
|
+
|
|
92
|
+
# 截断过长的输出
|
|
93
|
+
if len(text) > 12000:
|
|
94
|
+
text = f"{text[:12000]}\n...[truncated]..."
|
|
95
|
+
|
|
96
|
+
return ToolResult(
|
|
97
|
+
output=text,
|
|
98
|
+
is_error=process.returncode != 0,
|
|
99
|
+
metadata={"returncode": process.returncode},
|
|
100
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
发送消息工具
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
本模块提供向运行中的代理发送消息的功能,对齐标准 SendMessageTool 架构。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- SendMessageTool: 向代理发送消息的工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import SendMessageTool
|
|
12
|
+
>>> tool = SendMessageTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SendMessageToolInput(BaseModel):
|
|
27
|
+
"""发送消息参数。
|
|
28
|
+
|
|
29
|
+
属性:
|
|
30
|
+
to: 目标代理名称或 ID
|
|
31
|
+
message: 消息内容
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
to: str = Field(description="Agent name or ID to send the message to")
|
|
35
|
+
message: str = Field(description="Message content to send")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SendMessageTool(BaseTool):
|
|
39
|
+
"""向运行中的代理发送消息。
|
|
40
|
+
|
|
41
|
+
用于与代理通信或发送继续指令。
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name = "send_message"
|
|
45
|
+
description = """Send a message to another agent.
|
|
46
|
+
|
|
47
|
+
Use this tool to communicate with running agents. Messages from teammates are delivered automatically; you don't check an inbox. Refer to teammates by name, never by UUID.
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
- Send to agent by name: `SendMessage({ to: "researcher", message: "..." })`
|
|
51
|
+
- Send to agent by ID: `SendMessage({ to: "agent_abc123", message: "..." })`
|
|
52
|
+
|
|
53
|
+
When continuing a completed agent, the agent resumes with its full context preserved.
|
|
54
|
+
"""
|
|
55
|
+
input_model = SendMessageToolInput
|
|
56
|
+
|
|
57
|
+
async def execute(self, arguments: SendMessageToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
58
|
+
"""执行发送消息。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
arguments: 工具输入参数。
|
|
62
|
+
context: 工具执行上下文。
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
ToolResult: 工具执行结果。
|
|
66
|
+
"""
|
|
67
|
+
# 延迟导入以避免循环依赖
|
|
68
|
+
from illusion.swarm.agent_executor import (
|
|
69
|
+
TeammateMessage,
|
|
70
|
+
get_active_agent,
|
|
71
|
+
get_active_agent_by_name,
|
|
72
|
+
)
|
|
73
|
+
from illusion.tasks.manager import get_task_manager
|
|
74
|
+
|
|
75
|
+
target = arguments.to
|
|
76
|
+
message_text = arguments.message
|
|
77
|
+
|
|
78
|
+
# 首先尝试通过名称查找活跃的进程内代理
|
|
79
|
+
agent_ctx = get_active_agent_by_name(target)
|
|
80
|
+
if agent_ctx is None:
|
|
81
|
+
agent_ctx = get_active_agent(target)
|
|
82
|
+
|
|
83
|
+
if agent_ctx is not None:
|
|
84
|
+
msg = TeammateMessage(
|
|
85
|
+
text=message_text,
|
|
86
|
+
from_agent="coordinator",
|
|
87
|
+
)
|
|
88
|
+
await agent_ctx.message_queue.put(msg)
|
|
89
|
+
logger.debug("[SendMessage] Sent message to in-process agent %s", agent_ctx.agent_id)
|
|
90
|
+
return ToolResult(output=f"Sent message to agent '{target}'")
|
|
91
|
+
|
|
92
|
+
# 尝试通过任务管理器写入(子进程代理)
|
|
93
|
+
try:
|
|
94
|
+
await get_task_manager().write_to_task(target, message_text)
|
|
95
|
+
return ToolResult(output=f"Sent message to task '{target}'")
|
|
96
|
+
except ValueError:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# 尝试查找匹配的任务
|
|
100
|
+
task_manager = get_task_manager()
|
|
101
|
+
for task in task_manager.list_tasks(status="running"):
|
|
102
|
+
if task.description and target in task.description:
|
|
103
|
+
try:
|
|
104
|
+
await task_manager.write_to_task(task.id, message_text)
|
|
105
|
+
return ToolResult(output=f"Sent message to task '{task.id}' (matched '{target}')")
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
return ToolResult(output=str(exc), is_error=True)
|
|
108
|
+
|
|
109
|
+
return ToolResult(
|
|
110
|
+
output=f"No active agent or task found matching '{target}'",
|
|
111
|
+
is_error=True,
|
|
112
|
+
)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell 工具共享模块
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
本模块提供 shell 命令执行的共享工具,包括错误码标准化、输出归一化和命令执行器。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- ShellErrorCode: 标准化 shell 退出码常量
|
|
9
|
+
- NormalizedResult: 标准化命令执行结果数据类
|
|
10
|
+
- OutputNormalizer: 输出解码与归一化处理
|
|
11
|
+
- CommandExecutor: 统一命令执行器,处理超时、解码、归一化
|
|
12
|
+
|
|
13
|
+
类说明:
|
|
14
|
+
- ShellErrorCode: 标准化 shell 退出码常量类
|
|
15
|
+
- SUCCESS: 成功 (0)
|
|
16
|
+
- GENERAL_ERROR: 一般错误 (1)
|
|
17
|
+
- COMMAND_NOT_FOUND: 命令未找到 (127)
|
|
18
|
+
- TIMEOUT: 超时 (-1)
|
|
19
|
+
- PERMISSION_DENIED: 权限拒绝 (126)
|
|
20
|
+
- SIGNAL_BASE: 信号基准 (128 + signal_number)
|
|
21
|
+
|
|
22
|
+
- NormalizedResult: 标准化命令执行结果
|
|
23
|
+
- output: 输出文本
|
|
24
|
+
- is_error: 是否为错误
|
|
25
|
+
- return_code: 返回码
|
|
26
|
+
- metadata: 元数据字典
|
|
27
|
+
|
|
28
|
+
- OutputNormalizer: 输出解码与归一化处理
|
|
29
|
+
- decode_output: 健壮解码(UTF-8 → UTF-16LE → locale → replace)
|
|
30
|
+
- format_result: 生成上下文相关的输出消息
|
|
31
|
+
|
|
32
|
+
- CommandExecutor: 统一命令执行器
|
|
33
|
+
- run_and_normalize: 等待进程完成,捕获输出,归一化结果
|
|
34
|
+
|
|
35
|
+
使用示例:
|
|
36
|
+
>>> from illusion.tools.shell_common import ShellErrorCode, NormalizedResult
|
|
37
|
+
>>> print(ShellErrorCode.SUCCESS) # 输出: 0
|
|
38
|
+
>>> result = NormalizedResult(output="test", is_error=False, return_code=0)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import locale
|
|
45
|
+
from dataclasses import dataclass, field
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# 输出截断阈值(描述中对外暴露此值,修改时需同步更新工具描述文本)
|
|
50
|
+
MAX_OUTPUT_LENGTH = 30_000
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ShellErrorCode:
|
|
54
|
+
"""标准化 shell 退出码常量。"""
|
|
55
|
+
|
|
56
|
+
SUCCESS = 0
|
|
57
|
+
GENERAL_ERROR = 1
|
|
58
|
+
COMMAND_NOT_FOUND = 127
|
|
59
|
+
TIMEOUT = -1
|
|
60
|
+
PERMISSION_DENIED = 126
|
|
61
|
+
SIGNAL_BASE = 128 # 128 + signal_number
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class NormalizedResult:
|
|
66
|
+
"""标准化命令执行结果。"""
|
|
67
|
+
|
|
68
|
+
output: str
|
|
69
|
+
is_error: bool
|
|
70
|
+
return_code: int
|
|
71
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OutputNormalizer:
|
|
75
|
+
"""输出解码与归一化处理。"""
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def decode_output(data: bytes) -> str:
|
|
79
|
+
"""健壮解码:UTF-8 → UTF-16LE(如含 null 字节)→ locale → replace。"""
|
|
80
|
+
if not data:
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
encodings: list[str] = ["utf-8"]
|
|
84
|
+
|
|
85
|
+
# Windows PowerShell 经常输出 UTF-16LE —— 含 null 字节时优先尝试
|
|
86
|
+
if b"\x00" in data:
|
|
87
|
+
encodings.append("utf-16-le")
|
|
88
|
+
|
|
89
|
+
preferred = locale.getpreferredencoding(False)
|
|
90
|
+
if preferred and preferred.lower() not in {"utf-8", "utf8"}:
|
|
91
|
+
encodings.append(preferred)
|
|
92
|
+
|
|
93
|
+
for encoding in encodings:
|
|
94
|
+
try:
|
|
95
|
+
return data.decode(encoding)
|
|
96
|
+
except UnicodeDecodeError:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
return data.decode("utf-8", errors="replace")
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def format_result(
|
|
103
|
+
*,
|
|
104
|
+
stdout: bytes,
|
|
105
|
+
stderr: bytes,
|
|
106
|
+
return_code: int,
|
|
107
|
+
timed_out: bool,
|
|
108
|
+
timeout_seconds: int,
|
|
109
|
+
) -> NormalizedResult:
|
|
110
|
+
"""生成上下文相关的输出消息,消除 '(no output)' 歧义。"""
|
|
111
|
+
if timed_out:
|
|
112
|
+
output = f"Command timed out after {timeout_seconds}s"
|
|
113
|
+
return NormalizedResult(
|
|
114
|
+
output=output,
|
|
115
|
+
is_error=True,
|
|
116
|
+
return_code=-1,
|
|
117
|
+
metadata={"returncode": -1, "timed_out": True},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
decoded_stdout = OutputNormalizer.decode_output(stdout).rstrip()
|
|
121
|
+
decoded_stderr = OutputNormalizer.decode_output(stderr).rstrip()
|
|
122
|
+
|
|
123
|
+
parts = []
|
|
124
|
+
if decoded_stdout:
|
|
125
|
+
parts.append(decoded_stdout)
|
|
126
|
+
if decoded_stderr:
|
|
127
|
+
parts.append(decoded_stderr)
|
|
128
|
+
|
|
129
|
+
text = "\n".join(parts).strip()
|
|
130
|
+
|
|
131
|
+
if not text:
|
|
132
|
+
# 上下文相关的空输出消息
|
|
133
|
+
if return_code == 0:
|
|
134
|
+
text = "Command completed successfully (no output produced)\nExit code: 0"
|
|
135
|
+
else:
|
|
136
|
+
text = (
|
|
137
|
+
f"Process exited with code {return_code} but produced no output\n"
|
|
138
|
+
f"Exit code: {return_code}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if len(text) > MAX_OUTPUT_LENGTH:
|
|
142
|
+
text = f"{text[:MAX_OUTPUT_LENGTH]}\n...[truncated]..."
|
|
143
|
+
|
|
144
|
+
return NormalizedResult(
|
|
145
|
+
output=text,
|
|
146
|
+
is_error=return_code != 0,
|
|
147
|
+
return_code=return_code,
|
|
148
|
+
metadata={"returncode": return_code},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CommandExecutor:
|
|
153
|
+
"""统一命令执行器,处理超时、解码、归一化。"""
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
async def run_and_normalize(
|
|
157
|
+
process: asyncio.subprocess.Process,
|
|
158
|
+
*,
|
|
159
|
+
timeout: int,
|
|
160
|
+
) -> NormalizedResult:
|
|
161
|
+
"""等待进程完成,捕获输出,归一化结果。
|
|
162
|
+
|
|
163
|
+
调用方负责创建 process(保留各自的 shell/sandbox 逻辑),
|
|
164
|
+
本方法负责统一的超时、解码、截断和上下文化消息。
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
stdout, stderr = await asyncio.wait_for(
|
|
168
|
+
process.communicate(),
|
|
169
|
+
timeout=timeout,
|
|
170
|
+
)
|
|
171
|
+
except asyncio.TimeoutError:
|
|
172
|
+
process.kill()
|
|
173
|
+
await process.wait()
|
|
174
|
+
return NormalizedResult(
|
|
175
|
+
output=f"Command timed out after {timeout}s",
|
|
176
|
+
is_error=True,
|
|
177
|
+
return_code=-1,
|
|
178
|
+
metadata={"returncode": -1, "timed_out": True},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return OutputNormalizer.format_result(
|
|
182
|
+
stdout=stdout or b"",
|
|
183
|
+
stderr=stderr or b"",
|
|
184
|
+
return_code=process.returncode if process.returncode is not None else -1,
|
|
185
|
+
timed_out=False,
|
|
186
|
+
timeout_seconds=timeout,
|
|
187
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
技能内容读取工具
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供读取已加载技能内容的功能,用于执行斜杠命令和自定义技能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- SkillTool: 读取技能内容的工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import SkillTool
|
|
12
|
+
>>> tool = SkillTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from illusion.skills import load_skill_registry
|
|
20
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SkillToolInput(BaseModel):
|
|
24
|
+
"""技能查找参数。
|
|
25
|
+
|
|
26
|
+
属性:
|
|
27
|
+
name: 技能名称
|
|
28
|
+
args: 技能的可选参数
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str = Field(description="Skill name")
|
|
32
|
+
args: str | None = Field(default=None, description="Optional arguments for the skill")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SkillTool(BaseTool):
|
|
36
|
+
"""返回已加载技能的内容。
|
|
37
|
+
|
|
38
|
+
用于执行斜杠命令(/command)或调用自定义技能。
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name = "skill"
|
|
42
|
+
description = """Load a skill's instructions so you can then execute them yourself.
|
|
43
|
+
|
|
44
|
+
This tool does NOT execute anything — it only returns the skill's content (instructions, workflows, examples). You must read the returned content and follow it step by step to complete the task.
|
|
45
|
+
|
|
46
|
+
When users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge.
|
|
47
|
+
|
|
48
|
+
When users reference a "slash command" or "/<something>" (e.g., "/commit", "/review-pr"), they are referring to a skill. Use this tool to load its instructions.
|
|
49
|
+
|
|
50
|
+
How to use:
|
|
51
|
+
- Call this tool with the skill name and optional arguments
|
|
52
|
+
- The tool returns the skill's instructions — you then execute them
|
|
53
|
+
- Examples:
|
|
54
|
+
- `skill: "pdf"` — loads the pdf skill's instructions
|
|
55
|
+
- `skill: "commit", args: "-m 'Fix bug'"` — loads with arguments
|
|
56
|
+
- `skill: "review-pr", args: "123"` — loads with arguments
|
|
57
|
+
- `skill: "ms-office-suite:pdf"` — loads using fully qualified name
|
|
58
|
+
|
|
59
|
+
Important:
|
|
60
|
+
- Available skills are listed in <system-reminder> messages in the conversation
|
|
61
|
+
- When a skill matches the user's request, this is a BLOCKING REQUIREMENT: call this Skill tool to load its instructions BEFORE generating any other response about the task
|
|
62
|
+
- NEVER mention a skill without actually calling this tool
|
|
63
|
+
- Do not load a skill that is already active in the current context
|
|
64
|
+
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
|
|
65
|
+
- If you see a <command-name> tag in the current conversation turn, the skill's instructions have ALREADY been loaded — follow the instructions directly instead of calling this tool again"""
|
|
66
|
+
input_model = SkillToolInput
|
|
67
|
+
|
|
68
|
+
def is_read_only(self, arguments: SkillToolInput) -> bool:
|
|
69
|
+
del arguments
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
async def execute(self, arguments: SkillToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
73
|
+
# 加载技能注册表
|
|
74
|
+
registry = load_skill_registry(context.cwd)
|
|
75
|
+
# 尝试多种名称格式匹配
|
|
76
|
+
skill = registry.get(arguments.name) or registry.get(arguments.name.lower()) or registry.get(arguments.name.title())
|
|
77
|
+
if skill is None:
|
|
78
|
+
return ToolResult(output=f"Skill not found: {arguments.name}", is_error=True)
|
|
79
|
+
|
|
80
|
+
# 获取技能内容
|
|
81
|
+
content = skill.content
|
|
82
|
+
# 如果提供了参数,替换 $ARGUMENTS 占位符
|
|
83
|
+
if arguments.args and "$ARGUMENTS" in content:
|
|
84
|
+
content = content.replace("$ARGUMENTS", arguments.args)
|
|
85
|
+
|
|
86
|
+
return ToolResult(output=content)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
休眠工具
|
|
3
|
+
========
|
|
4
|
+
|
|
5
|
+
本模块提供暂停执行的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- SleepTool: 暂停执行的工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import SleepTool
|
|
12
|
+
>>> tool = SleepTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SleepToolInput(BaseModel):
|
|
25
|
+
"""休眠参数。
|
|
26
|
+
|
|
27
|
+
属性:
|
|
28
|
+
seconds: 休眠秒数
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
seconds: float = Field(default=1.0, ge=0.0, le=30.0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SleepTool(BaseTool):
|
|
35
|
+
"""短暂暂停执行。
|
|
36
|
+
|
|
37
|
+
用于等待指定时间后继续执行。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name = "sleep"
|
|
41
|
+
description = """Wait for a specified duration. The user can interrupt the sleep at any time.
|
|
42
|
+
|
|
43
|
+
Use this when the user tells you to sleep or rest, when you have nothing to do, or when you're waiting for something.
|
|
44
|
+
|
|
45
|
+
You may receive <tick> prompts - these are periodic check-ins. Look for useful work to do before sleeping.
|
|
46
|
+
|
|
47
|
+
You can call this concurrently with other tools - it won't interfere with them.
|
|
48
|
+
|
|
49
|
+
Prefer this over `Bash(sleep ...)` - it doesn't hold a shell process.
|
|
50
|
+
|
|
51
|
+
Each wake-up costs an API call, but the prompt cache expires after 5 minutes of inactivity - balance accordingly."""
|
|
52
|
+
input_model = SleepToolInput
|
|
53
|
+
|
|
54
|
+
def is_read_only(self, arguments: SleepToolInput) -> bool:
|
|
55
|
+
del arguments
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
async def execute(self, arguments: SleepToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
59
|
+
del context
|
|
60
|
+
# 异步休眠
|
|
61
|
+
await asyncio.sleep(arguments.seconds)
|
|
62
|
+
return ToolResult(output=f"Slept for {arguments.seconds} seconds")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
结构化输出工具
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
本模块提供以模式验证的 JSON 格式返回最终响应的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- StructuredOutputTool: 结构化输出工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import StructuredOutputTool
|
|
12
|
+
>>> tool = StructuredOutputTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StructuredOutputToolInput(BaseModel):
|
|
25
|
+
"""任意结构化 payload。
|
|
26
|
+
|
|
27
|
+
属性:
|
|
28
|
+
structured_output: 结构化输出 payload
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
structured_output: dict[str, object] = Field(
|
|
32
|
+
default_factory=dict,
|
|
33
|
+
description="Structured output payload",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StructuredOutputTool(BaseTool):
|
|
38
|
+
"""以结构化 JSON 形式返回最终响应。
|
|
39
|
+
|
|
40
|
+
用于按照请求的格式返回结构化输出。
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name = "structured_output"
|
|
44
|
+
description = """Use this tool to return your final response in the requested structured format. You MUST call this tool exactly once at the end of your response to provide the structured output."""
|
|
45
|
+
input_model = StructuredOutputToolInput
|
|
46
|
+
|
|
47
|
+
def is_read_only(self, arguments: StructuredOutputToolInput) -> bool:
|
|
48
|
+
del arguments
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
async def execute(
|
|
52
|
+
self,
|
|
53
|
+
arguments: StructuredOutputToolInput,
|
|
54
|
+
context: ToolExecutionContext,
|
|
55
|
+
) -> ToolResult:
|
|
56
|
+
del context
|
|
57
|
+
output = json.dumps(arguments.structured_output, ensure_ascii=False)
|
|
58
|
+
return ToolResult(output=output)
|