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,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
会话持久化辅助模块
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供会话状态持久化功能,支持保存和加载会话快照。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 获取项目会话目录
|
|
9
|
+
- 保存会话快照
|
|
10
|
+
- 加载会话快照
|
|
11
|
+
- 列出会话快照
|
|
12
|
+
- 导出会话记录为 Markdown
|
|
13
|
+
|
|
14
|
+
类说明:
|
|
15
|
+
- get_project_session_dir: 获取项目会话目录
|
|
16
|
+
- save_session_snapshot: 保存会话快照
|
|
17
|
+
- load_session_snapshot: 加载会话快照
|
|
18
|
+
- list_session_snapshots: 列出会话快照
|
|
19
|
+
- export_session_markdown: 导出为 Markdown
|
|
20
|
+
|
|
21
|
+
使用示例:
|
|
22
|
+
>>> from illusion.services.session_storage import get_project_session_dir, save_session_snapshot
|
|
23
|
+
>>> # 获取项目会话目录
|
|
24
|
+
>>> session_dir = get_project_session_dir("/path/to/project")
|
|
25
|
+
>>> # 保存会话快照
|
|
26
|
+
>>> save_session_snapshot(cwd="/path/to/project", model="claude-3", messages=[...], usage=...)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import time
|
|
33
|
+
from hashlib import sha1
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
from uuid import uuid4
|
|
37
|
+
|
|
38
|
+
from illusion.api.usage import UsageSnapshot
|
|
39
|
+
from illusion.config.paths import get_sessions_dir
|
|
40
|
+
from illusion.engine.messages import ConversationMessage
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_project_session_dir(cwd: str | Path) -> Path:
|
|
44
|
+
"""返回项目的会话目录。"""
|
|
45
|
+
path = Path(cwd).resolve()
|
|
46
|
+
# 使用路径的 SHA1 哈希前 12 位作为目录名的一部分
|
|
47
|
+
digest = sha1(str(path).encode("utf-8")).hexdigest()[:12]
|
|
48
|
+
session_dir = get_sessions_dir() / f"{path.name}-{digest}"
|
|
49
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
return session_dir
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_session_snapshot(
|
|
54
|
+
*,
|
|
55
|
+
cwd: str | Path,
|
|
56
|
+
model: str,
|
|
57
|
+
system_prompt: str,
|
|
58
|
+
messages: list[ConversationMessage],
|
|
59
|
+
usage: UsageSnapshot,
|
|
60
|
+
session_id: str | None = None,
|
|
61
|
+
) -> Path:
|
|
62
|
+
"""持久化会话快照。同时按 ID 保存和保存为 latest。"""
|
|
63
|
+
session_dir = get_project_session_dir(cwd)
|
|
64
|
+
sid = session_id or uuid4().hex[:12]
|
|
65
|
+
now = time.time()
|
|
66
|
+
# 从第一个用户消息提取摘要
|
|
67
|
+
summary = ""
|
|
68
|
+
for msg in messages:
|
|
69
|
+
if msg.role == "user" and msg.text.strip():
|
|
70
|
+
summary = msg.text.strip()[:80]
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
payload = {
|
|
74
|
+
"session_id": sid,
|
|
75
|
+
"cwd": str(Path(cwd).resolve()),
|
|
76
|
+
"model": model,
|
|
77
|
+
"system_prompt": system_prompt,
|
|
78
|
+
"messages": [message.model_dump(mode="json") for message in messages],
|
|
79
|
+
"usage": usage.model_dump(),
|
|
80
|
+
"created_at": now,
|
|
81
|
+
"summary": summary,
|
|
82
|
+
"message_count": len(messages),
|
|
83
|
+
}
|
|
84
|
+
data = json.dumps(payload, indent=2) + "\n"
|
|
85
|
+
|
|
86
|
+
# 保存为 latest
|
|
87
|
+
latest_path = session_dir / "latest.json"
|
|
88
|
+
latest_path.write_text(data, encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
# 按会话 ID 保存
|
|
91
|
+
session_path = session_dir / f"session-{sid}.json"
|
|
92
|
+
session_path.write_text(data, encoding="utf-8")
|
|
93
|
+
|
|
94
|
+
return latest_path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_session_snapshot(cwd: str | Path) -> dict[str, Any] | None:
|
|
98
|
+
"""加载项目的最新会话快照。"""
|
|
99
|
+
path = get_project_session_dir(cwd) / "latest.json"
|
|
100
|
+
if not path.exists():
|
|
101
|
+
return None
|
|
102
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def list_session_snapshots(cwd: str | Path, limit: int = 20) -> list[dict[str, Any]]:
|
|
106
|
+
"""列出项目的已保存会话,按最新优先排序。"""
|
|
107
|
+
session_dir = get_project_session_dir(cwd)
|
|
108
|
+
sessions: list[dict[str, Any]] = []
|
|
109
|
+
seen_ids: set[str] = set()
|
|
110
|
+
|
|
111
|
+
# 命名会话文件
|
|
112
|
+
for path in sorted(session_dir.glob("session-*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
113
|
+
try:
|
|
114
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
115
|
+
sid = data.get("session_id", path.stem.replace("session-", ""))
|
|
116
|
+
seen_ids.add(sid)
|
|
117
|
+
summary = data.get("summary", "")
|
|
118
|
+
if not summary:
|
|
119
|
+
# 从消息中提取
|
|
120
|
+
for msg in data.get("messages", []):
|
|
121
|
+
if msg.get("role") == "user":
|
|
122
|
+
texts = [b.get("text", "") for b in msg.get("content", []) if b.get("type") == "text"]
|
|
123
|
+
summary = " ".join(texts).strip()[:80]
|
|
124
|
+
if summary:
|
|
125
|
+
break
|
|
126
|
+
sessions.append({
|
|
127
|
+
"session_id": sid,
|
|
128
|
+
"summary": summary,
|
|
129
|
+
"message_count": data.get("message_count", len(data.get("messages", []))),
|
|
130
|
+
"model": data.get("model", ""),
|
|
131
|
+
"created_at": data.get("created_at", path.stat().st_mtime),
|
|
132
|
+
})
|
|
133
|
+
except (json.JSONDecodeError, OSError):
|
|
134
|
+
continue
|
|
135
|
+
if len(sessions) >= limit:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
# 也包含 latest.json(如果没有对应的会话文件)
|
|
139
|
+
latest_path = session_dir / "latest.json"
|
|
140
|
+
if latest_path.exists() and len(sessions) < limit:
|
|
141
|
+
try:
|
|
142
|
+
data = json.loads(latest_path.read_text(encoding="utf-8"))
|
|
143
|
+
sid = data.get("session_id", "latest")
|
|
144
|
+
if sid not in seen_ids:
|
|
145
|
+
summary = data.get("summary", "")
|
|
146
|
+
if not summary:
|
|
147
|
+
for msg in data.get("messages", []):
|
|
148
|
+
if msg.get("role") == "user":
|
|
149
|
+
texts = [b.get("text", "") for b in msg.get("content", []) if b.get("type") == "text"]
|
|
150
|
+
summary = " ".join(texts).strip()[:80]
|
|
151
|
+
if summary:
|
|
152
|
+
break
|
|
153
|
+
sessions.append({
|
|
154
|
+
"session_id": sid,
|
|
155
|
+
"summary": summary or "(latest session)",
|
|
156
|
+
"message_count": data.get("message_count", len(data.get("messages", []))),
|
|
157
|
+
"model": data.get("model", ""),
|
|
158
|
+
"created_at": data.get("created_at", latest_path.stat().st_mtime),
|
|
159
|
+
})
|
|
160
|
+
except (json.JSONDecodeError, OSError):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
# 按 created_at 降序排序
|
|
164
|
+
sessions.sort(key=lambda s: s.get("created_at", 0), reverse=True)
|
|
165
|
+
return sessions[:limit]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_session_by_id(cwd: str | Path, session_id: str) -> dict[str, Any] | None:
|
|
169
|
+
"""按 ID 加载特定会话。"""
|
|
170
|
+
session_dir = get_project_session_dir(cwd)
|
|
171
|
+
# 先尝试命名会话
|
|
172
|
+
path = session_dir / f"session-{session_id}.json"
|
|
173
|
+
if path.exists():
|
|
174
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
175
|
+
# 回退到 latest.json(如果 session_id 匹配)
|
|
176
|
+
latest = session_dir / "latest.json"
|
|
177
|
+
if latest.exists():
|
|
178
|
+
data = json.loads(latest.read_text(encoding="utf-8"))
|
|
179
|
+
if data.get("session_id") == session_id or session_id == "latest":
|
|
180
|
+
return data
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def delete_session_by_id(cwd: str | Path, session_id: str) -> bool:
|
|
185
|
+
"""按 ID 删除特定会话。返回是否成功删除。"""
|
|
186
|
+
session_dir = get_project_session_dir(cwd)
|
|
187
|
+
path = session_dir / f"session-{session_id}.json"
|
|
188
|
+
if path.exists():
|
|
189
|
+
path.unlink()
|
|
190
|
+
# 如果删除的是 latest.json 对应的会话,也删除 latest.json
|
|
191
|
+
latest = session_dir / "latest.json"
|
|
192
|
+
if latest.exists():
|
|
193
|
+
try:
|
|
194
|
+
data = json.loads(latest.read_text(encoding="utf-8"))
|
|
195
|
+
if data.get("session_id") == session_id:
|
|
196
|
+
latest.unlink()
|
|
197
|
+
except (json.JSONDecodeError, OSError):
|
|
198
|
+
pass
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def delete_all_sessions(cwd: str | Path) -> int:
|
|
204
|
+
"""删除项目的所有会话快照。返回删除的文件数量。"""
|
|
205
|
+
session_dir = get_project_session_dir(cwd)
|
|
206
|
+
count = 0
|
|
207
|
+
for path in session_dir.glob("session-*.json"):
|
|
208
|
+
path.unlink()
|
|
209
|
+
count += 1
|
|
210
|
+
latest = session_dir / "latest.json"
|
|
211
|
+
if latest.exists():
|
|
212
|
+
latest.unlink()
|
|
213
|
+
count += 1
|
|
214
|
+
return count
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def export_session_markdown(
|
|
218
|
+
*,
|
|
219
|
+
cwd: str | Path,
|
|
220
|
+
messages: list[ConversationMessage],
|
|
221
|
+
) -> Path:
|
|
222
|
+
"""将会话记录导出为 Markdown。"""
|
|
223
|
+
session_dir = get_project_session_dir(cwd)
|
|
224
|
+
path = session_dir / "transcript.md"
|
|
225
|
+
parts: list[str] = ["# IllusionCode Session Transcript"]
|
|
226
|
+
for message in messages:
|
|
227
|
+
parts.append(f"\n## {message.role.capitalize()}\n")
|
|
228
|
+
text = message.text.strip()
|
|
229
|
+
if text:
|
|
230
|
+
parts.append(text)
|
|
231
|
+
for block in message.tool_uses:
|
|
232
|
+
parts.append(f"\n```tool\n{block.name} {json.dumps(block.input, ensure_ascii=True)}\n```")
|
|
233
|
+
for block in message.content:
|
|
234
|
+
if getattr(block, "type", "") == "tool_result":
|
|
235
|
+
parts.append(f"\n```tool-result\n{block.content}\n```")
|
|
236
|
+
path.write_text("\n".join(parts).strip() + "\n", encoding="utf-8")
|
|
237
|
+
return path
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token 估算工具
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
本模块提供 Token 估算功能,使用改进的启发式方法:
|
|
6
|
+
- 基础估算:字符数 / 4(适用于英文为主的文本)
|
|
7
|
+
- CJK 优化:中日韩文字按 2 字符/token 估算(更密集)
|
|
8
|
+
- JSON 优化:JSON 内容按 2 字节/token 估算
|
|
9
|
+
- 混合文本自动检测
|
|
10
|
+
|
|
11
|
+
主要功能:
|
|
12
|
+
- 估算单个文本的 Token 数量
|
|
13
|
+
- 估算消息列表的总 Token 数量
|
|
14
|
+
- 检测文本是否包含 CJK 字符
|
|
15
|
+
|
|
16
|
+
使用示例:
|
|
17
|
+
>>> from illusion.services.token_estimation import estimate_tokens
|
|
18
|
+
>>> tokens = estimate_tokens("Hello, world!")
|
|
19
|
+
>>> print(tokens) # 输出约 4
|
|
20
|
+
>>> tokens = estimate_tokens("你好世界")
|
|
21
|
+
>>> print(tokens) # 输出约 4
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
# CJK 字符范围的正则表达式(匹配中日韩统一表意文字等)
|
|
29
|
+
_CJK_PATTERN = re.compile(
|
|
30
|
+
r"[\u4e00-\u9fff" # CJK Unified Ideographs
|
|
31
|
+
r"\u3400-\u4dbf" # CJK Unified Ideographs Extension A
|
|
32
|
+
r"\uf900-\ufaff" # CJK Compatibility Ideographs
|
|
33
|
+
r"\u3040-\u309f" # Hiragana
|
|
34
|
+
r"\u30a0-\u30ff" # Katakana
|
|
35
|
+
r"\uac00-\ud7af" # Hangul Syllables
|
|
36
|
+
r"]"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _has_cjk(text: str) -> bool:
|
|
41
|
+
"""检查文本是否包含 CJK 字符。"""
|
|
42
|
+
return bool(_CJK_PATTERN.search(text))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def estimate_tokens(text: str) -> int:
|
|
46
|
+
"""使用改进的启发式方法估算纯文本的 Token 数。
|
|
47
|
+
|
|
48
|
+
对于英文为主的文本,使用字符数 / 4 的估算。
|
|
49
|
+
对于包含 CJK 字符的文本,使用字符数 / 2 的估算(CJK 文字更密集)。
|
|
50
|
+
"""
|
|
51
|
+
if not text:
|
|
52
|
+
return 0
|
|
53
|
+
if _has_cjk(text):
|
|
54
|
+
# CJK 文字:每个字符约 0.5-1 token,使用 /2 估算
|
|
55
|
+
return max(1, (len(text) + 1) // 2)
|
|
56
|
+
# 英文为主:约 4 字符/token
|
|
57
|
+
return max(1, (len(text) + 3) // 4)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def estimate_tokens_for_json(text: str) -> int:
|
|
61
|
+
"""估算 JSON 文本的 Token 数。
|
|
62
|
+
|
|
63
|
+
JSON 内容比普通文本更密集,使用 2 字节/token 估算。
|
|
64
|
+
"""
|
|
65
|
+
if not text:
|
|
66
|
+
return 0
|
|
67
|
+
return max(1, len(text.encode("utf-8")) // 2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def estimate_message_tokens(messages: list[str]) -> int:
|
|
71
|
+
"""估算消息字符串集合的 Token 总数。"""
|
|
72
|
+
return sum(estimate_tokens(message) for message in messages)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill 模块导出
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
本模块导出 skills 子目录中的公共接口。
|
|
6
|
+
|
|
7
|
+
导出内容:
|
|
8
|
+
- SkillDefinition: Skill 定义数据类
|
|
9
|
+
- SkillRegistry: Skill 注册表
|
|
10
|
+
- get_user_skills_dir: 用户 skills 目录
|
|
11
|
+
- load_skill_registry: 加载 skill 注册表
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
19
|
+
from illusion.skills.registry import SkillRegistry
|
|
20
|
+
from illusion.skills.types import SkillDefinition
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SkillDefinition",
|
|
24
|
+
"SkillRegistry",
|
|
25
|
+
"get_project_rules_dir",
|
|
26
|
+
"get_project_skills_dir",
|
|
27
|
+
"get_user_skills_dir",
|
|
28
|
+
"load_skill_registry",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __getattr__(name: str):
|
|
33
|
+
if name in {
|
|
34
|
+
"get_user_skills_dir",
|
|
35
|
+
"get_project_skills_dir",
|
|
36
|
+
"get_project_rules_dir",
|
|
37
|
+
"load_skill_registry",
|
|
38
|
+
}:
|
|
39
|
+
from illusion.skills.loader import (
|
|
40
|
+
get_project_rules_dir,
|
|
41
|
+
get_project_skills_dir,
|
|
42
|
+
get_user_skills_dir,
|
|
43
|
+
load_skill_registry,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"get_user_skills_dir": get_user_skills_dir,
|
|
48
|
+
"get_project_skills_dir": get_project_skills_dir,
|
|
49
|
+
"get_project_rules_dir": get_project_rules_dir,
|
|
50
|
+
"load_skill_registry": load_skill_registry,
|
|
51
|
+
}[name]
|
|
52
|
+
if name == "SkillRegistry":
|
|
53
|
+
from illusion.skills.registry import SkillRegistry
|
|
54
|
+
|
|
55
|
+
return SkillRegistry
|
|
56
|
+
if name == "SkillDefinition":
|
|
57
|
+
from illusion.skills.types import SkillDefinition
|
|
58
|
+
|
|
59
|
+
return SkillDefinition
|
|
60
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
内置 Skill 定义模块
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
本模块从 .md 文件加载内置 skill 定义。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 从 content 目录加载所有内置 skills
|
|
9
|
+
- 解析 Markdown 文件的前 matter
|
|
10
|
+
|
|
11
|
+
类说明:
|
|
12
|
+
- get_bundled_skills: 加载所有内置 skills
|
|
13
|
+
|
|
14
|
+
使用示例:
|
|
15
|
+
>>> from illusion.skills.bundled import get_bundled_skills
|
|
16
|
+
>>> # 加载内置 skills
|
|
17
|
+
>>> skills = get_bundled_skills()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from illusion.skills.types import SkillDefinition
|
|
25
|
+
|
|
26
|
+
# 内置 skill 内容目录
|
|
27
|
+
_CONTENT_DIR = Path(__file__).parent / "content"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_bundled_skills() -> list[SkillDefinition]:
|
|
31
|
+
"""从 content 目录加载所有内置 skills(支持 .md、.yaml、.yml)。"""
|
|
32
|
+
skills: list[SkillDefinition] = []
|
|
33
|
+
if not _CONTENT_DIR.exists():
|
|
34
|
+
return skills
|
|
35
|
+
from illusion.skills.loader import _load_yaml_skill
|
|
36
|
+
|
|
37
|
+
for path in sorted(_CONTENT_DIR.iterdir()):
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
continue
|
|
40
|
+
if path.suffix in (".yaml", ".yml"):
|
|
41
|
+
skill = _load_yaml_skill(path, source="bundled")
|
|
42
|
+
if skill:
|
|
43
|
+
skills.append(skill)
|
|
44
|
+
elif path.suffix == ".md":
|
|
45
|
+
content = path.read_text(encoding="utf-8")
|
|
46
|
+
name, description = _parse_frontmatter(path.stem, content)
|
|
47
|
+
skills.append(
|
|
48
|
+
SkillDefinition(
|
|
49
|
+
name=name,
|
|
50
|
+
description=description,
|
|
51
|
+
content=content,
|
|
52
|
+
source="bundled",
|
|
53
|
+
path=str(path),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
return skills
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_frontmatter(default_name: str, content: str) -> tuple[str, str]:
|
|
60
|
+
"""从 skill markdown 文件中提取名称和描述。
|
|
61
|
+
|
|
62
|
+
支持 YAML frontmatter(--- 分隔),并回退到标题/段落解析。
|
|
63
|
+
"""
|
|
64
|
+
import yaml
|
|
65
|
+
|
|
66
|
+
name = default_name
|
|
67
|
+
description = ""
|
|
68
|
+
lines = content.splitlines()
|
|
69
|
+
|
|
70
|
+
# 先尝试 YAML frontmatter
|
|
71
|
+
if lines and lines[0].strip() == "---":
|
|
72
|
+
end_idx = -1
|
|
73
|
+
for i, line in enumerate(lines[1:], 1):
|
|
74
|
+
if line.strip() == "---":
|
|
75
|
+
end_idx = i
|
|
76
|
+
break
|
|
77
|
+
if end_idx > 0:
|
|
78
|
+
fm_text = "\n".join(lines[1:end_idx])
|
|
79
|
+
try:
|
|
80
|
+
data = yaml.safe_load(fm_text)
|
|
81
|
+
if isinstance(data, dict):
|
|
82
|
+
if data.get("name"):
|
|
83
|
+
name = str(data["name"]).strip()
|
|
84
|
+
if data.get("description"):
|
|
85
|
+
description = str(data["description"]).strip()
|
|
86
|
+
except Exception:
|
|
87
|
+
# YAML 解析失败,回退到手动解析
|
|
88
|
+
for fm_line in lines[1:end_idx]:
|
|
89
|
+
fm = fm_line.strip()
|
|
90
|
+
if fm.startswith("name:"):
|
|
91
|
+
val = fm[5:].strip().strip("'\"")
|
|
92
|
+
if val:
|
|
93
|
+
name = val
|
|
94
|
+
elif fm.startswith("description:"):
|
|
95
|
+
val = fm[12:].strip().strip("'\"")
|
|
96
|
+
if val:
|
|
97
|
+
description = val
|
|
98
|
+
if description:
|
|
99
|
+
return name, description
|
|
100
|
+
|
|
101
|
+
# 回退:标题 + 第一段
|
|
102
|
+
for line in lines:
|
|
103
|
+
stripped = line.strip()
|
|
104
|
+
if stripped.startswith("# "):
|
|
105
|
+
name = stripped[2:].strip() or default_name
|
|
106
|
+
continue
|
|
107
|
+
if stripped and not stripped.startswith("---") and not stripped.startswith("#"):
|
|
108
|
+
description = stripped[:200]
|
|
109
|
+
break
|
|
110
|
+
return name, description or f"Bundled skill: {name}"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: batch
|
|
3
|
+
description: Research and plan a large-scale change, then execute it in parallel across 5-30 isolated worktree agents that each open a PR.
|
|
4
|
+
argument-hint: "<instruction>"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Batch: Parallel Work Orchestration
|
|
8
|
+
|
|
9
|
+
You are orchestrating a large, parallelizable change across this codebase.
|
|
10
|
+
|
|
11
|
+
## User Instruction
|
|
12
|
+
|
|
13
|
+
The instruction will be provided as arguments.
|
|
14
|
+
|
|
15
|
+
## Phase 1: Research and Plan (Plan Mode)
|
|
16
|
+
|
|
17
|
+
Call the `enter_plan_mode` tool now to enter plan mode, then:
|
|
18
|
+
|
|
19
|
+
1. **Understand the scope.** Launch one or more subagents (in the foreground — you need their results) to deeply research what this instruction touches. Find all the files, patterns, and call sites that need to change. Understand the existing conventions so the migration is consistent.
|
|
20
|
+
|
|
21
|
+
2. **Decompose into independent units.** Break the work into 5–30 self-contained units. Each unit must:
|
|
22
|
+
- Be independently implementable in an isolated git worktree (no shared state with sibling units)
|
|
23
|
+
- Be mergeable on its own without depending on another unit's PR landing first
|
|
24
|
+
- Be roughly uniform in size (split large units, merge trivial ones)
|
|
25
|
+
|
|
26
|
+
Scale the count to the actual work: few files → closer to 5; hundreds of files → closer to 30. Prefer per-directory or per-module slicing over arbitrary file lists.
|
|
27
|
+
|
|
28
|
+
3. **Determine the e2e test recipe.** Figure out how a worker can verify its change actually works end-to-end — not just that unit tests pass. Look for:
|
|
29
|
+
- A browser-automation tool (for UI changes: click through the affected flow, screenshot the result)
|
|
30
|
+
- A CLI-verifier skill (for CLI changes: launch the app interactively, exercise the changed behavior)
|
|
31
|
+
- A dev-server + curl pattern (for API changes: start the server, hit the affected endpoints)
|
|
32
|
+
- An existing e2e/integration test suite the worker can run
|
|
33
|
+
|
|
34
|
+
If you cannot find a concrete e2e path, use the `ask_user_question` tool to ask the user how to verify this change end-to-end. Offer 2–3 specific options based on what you found.
|
|
35
|
+
|
|
36
|
+
Write the recipe as a short, concrete set of steps that a worker can execute autonomously.
|
|
37
|
+
|
|
38
|
+
4. **Write the plan.** In your plan file, include:
|
|
39
|
+
- A summary of what you found during research
|
|
40
|
+
- A numbered list of work units — for each: a short title, the list of files/directories it covers, and a one-line description of the change
|
|
41
|
+
- The e2e test recipe (or "skip e2e because …" if the user chose that)
|
|
42
|
+
- The exact worker instructions you will give each agent (the shared template)
|
|
43
|
+
|
|
44
|
+
5. Call `exit_plan_mode` to present the plan for approval.
|
|
45
|
+
|
|
46
|
+
## Phase 2: Spawn Workers (After Plan Approval)
|
|
47
|
+
|
|
48
|
+
Once the plan is approved, spawn one background agent per work unit using the `agent` tool. **All agents must use `isolation: "worktree"` and `run_in_background: true`.** Launch them all in a single message block so they run in parallel.
|
|
49
|
+
|
|
50
|
+
For each agent, the prompt must be fully self-contained. Include:
|
|
51
|
+
- The overall goal (the user's instruction)
|
|
52
|
+
- This unit's specific task (title, file list, change description — copied verbatim from your plan)
|
|
53
|
+
- Any codebase conventions you discovered that the worker needs to follow
|
|
54
|
+
- The e2e test recipe from your plan (or "skip e2e because …")
|
|
55
|
+
- The worker instructions below, copied verbatim:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
After you finish implementing the change:
|
|
59
|
+
1. **Simplify** — Invoke the `skill` tool with `name: "simplify"` to review and clean up your changes.
|
|
60
|
+
2. **Run unit tests** — Run the project's test suite (check for package.json scripts, Makefile targets, or common commands like `npm test`, `bun test`, `pytest`, `go test`). If tests fail, fix them.
|
|
61
|
+
3. **Test end-to-end** — Follow the e2e test recipe from the coordinator's prompt. If the recipe says to skip e2e for this unit, skip it.
|
|
62
|
+
4. **Commit and push** — Commit all changes with a clear message, push the branch, and create a PR with `gh pr create`. Use a descriptive title. If `gh` is not available or the push fails, note it in your final message.
|
|
63
|
+
5. **Report** — End with a single line: `PR: <url>` so the coordinator can track it. If no PR was created, end with `PR: none — <reason>`.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `subagent_type: "general-purpose"` unless a more specific agent type fits.
|
|
67
|
+
|
|
68
|
+
## Phase 3: Track Progress
|
|
69
|
+
|
|
70
|
+
After launching all workers, render an initial status table:
|
|
71
|
+
|
|
72
|
+
| # | Unit | Status | PR |
|
|
73
|
+
|---|------|--------|----|
|
|
74
|
+
| 1 | <title> | running | — |
|
|
75
|
+
| 2 | <title> | running | — |
|
|
76
|
+
|
|
77
|
+
As background-agent completion notifications arrive, parse the `PR: <url>` line from each agent's result and re-render the table with updated status (`done` / `failed`) and PR links. Keep a brief failure note for any agent that did not produce a PR.
|
|
78
|
+
|
|
79
|
+
When all agents have reported, render the final table and a one-line summary (e.g., "22/24 units landed as PRs").
|
|
80
|
+
|
|
81
|
+
## Rules
|
|
82
|
+
|
|
83
|
+
- This command requires a git repo because it spawns agents in isolated git worktrees and creates PRs from each. Initialize a repo first if needed.
|
|
84
|
+
- Each agent must be fully self-contained — no shared state between agents
|
|
85
|
+
- All agents must use worktree isolation
|
|
86
|
+
- Track progress and report results clearly
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coding-guidelines
|
|
3
|
+
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coding Guidelines
|
|
7
|
+
|
|
8
|
+
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
|
9
|
+
|
|
10
|
+
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
|
11
|
+
|
|
12
|
+
## 1. Think Before Coding
|
|
13
|
+
|
|
14
|
+
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
|
15
|
+
|
|
16
|
+
Before implementing:
|
|
17
|
+
- State your assumptions explicitly. If uncertain, ask.
|
|
18
|
+
- If multiple interpretations exist, present them - don't pick silently.
|
|
19
|
+
- If a simpler approach exists, say so. Push back when warranted.
|
|
20
|
+
- If something is unclear, stop. Name what's confusing. Ask.
|
|
21
|
+
|
|
22
|
+
## 2. Simplicity First
|
|
23
|
+
|
|
24
|
+
**Minimum code that solves the problem. Nothing speculative.**
|
|
25
|
+
|
|
26
|
+
- No features beyond what was asked.
|
|
27
|
+
- No abstractions for single-use code.
|
|
28
|
+
- No "flexibility" or "configurability" that wasn't requested.
|
|
29
|
+
- No error handling for impossible scenarios.
|
|
30
|
+
- If you write 200 lines and it could be 50, rewrite it.
|
|
31
|
+
|
|
32
|
+
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
|
33
|
+
|
|
34
|
+
## 3. Surgical Changes
|
|
35
|
+
|
|
36
|
+
**Touch only what you must. Clean up only your own mess.**
|
|
37
|
+
|
|
38
|
+
When editing existing code:
|
|
39
|
+
- Don't "improve" adjacent code, comments, or formatting.
|
|
40
|
+
- Don't refactor things that aren't broken.
|
|
41
|
+
- Match existing style, even if you'd do it differently.
|
|
42
|
+
- If you notice unrelated dead code, mention it - don't delete it.
|
|
43
|
+
|
|
44
|
+
When your changes create orphans:
|
|
45
|
+
- Remove imports/variables/functions that YOUR changes made unused.
|
|
46
|
+
- Don't remove pre-existing dead code unless asked.
|
|
47
|
+
|
|
48
|
+
The test: Every changed line should trace directly to the user's request.
|
|
49
|
+
|
|
50
|
+
## 4. Goal-Driven Execution
|
|
51
|
+
|
|
52
|
+
**Define success criteria. Loop until verified.**
|
|
53
|
+
|
|
54
|
+
Transform tasks into verifiable goals:
|
|
55
|
+
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
|
56
|
+
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
|
57
|
+
- "Refactor X" → "Ensure tests pass before and after"
|
|
58
|
+
|
|
59
|
+
For multi-step tasks, state a brief plan:
|
|
60
|
+
```
|
|
61
|
+
1. [Step] → verify: [check]
|
|
62
|
+
2. [Step] → verify: [check]
|
|
63
|
+
3. [Step] → verify: [check]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: debug
|
|
3
|
+
description: Enable debug logging for this session and help diagnose issues
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Debug Skill
|
|
7
|
+
|
|
8
|
+
Help the user debug an issue they're encountering in this current Illusion Code session.
|
|
9
|
+
|
|
10
|
+
## Session Debug Log
|
|
11
|
+
|
|
12
|
+
The debug log for the current session is located at the path shown in the session startup output.
|
|
13
|
+
|
|
14
|
+
## Issue Description
|
|
15
|
+
|
|
16
|
+
The user's issue description will be provided as arguments. If no description is given, read the debug log and summarize any errors, warnings, or notable issues.
|
|
17
|
+
|
|
18
|
+
## Settings
|
|
19
|
+
|
|
20
|
+
Remember that settings are in:
|
|
21
|
+
* user - ~/.illusion/settings.json
|
|
22
|
+
* project - .illusion/settings.json
|
|
23
|
+
|
|
24
|
+
## Instructions
|
|
25
|
+
|
|
26
|
+
1. Review the user's issue description
|
|
27
|
+
2. Look for [ERROR] and [WARN] entries, stack traces, and failure patterns in the debug log
|
|
28
|
+
3. Consider launching a subagent to understand the relevant Illusion Code features
|
|
29
|
+
4. Explain what you found in plain language
|
|
30
|
+
5. Suggest concrete fixes or next steps
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- Read the error message carefully before searching code
|
|
35
|
+
- Don't guess — verify your hypothesis before changing code
|
|
36
|
+
- Fix the root cause, not the symptom
|
|
37
|
+
- Don't retry the same approach if it failed — investigate why
|
|
38
|
+
- If stuck after 3 attempts, explain what you've tried and ask for help
|