chcode 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.
- chcode/__init__.py +0 -0
- chcode/__main__.py +5 -0
- chcode/agent_setup.py +395 -0
- chcode/agents/__init__.py +0 -0
- chcode/agents/definitions.py +158 -0
- chcode/agents/loader.py +104 -0
- chcode/agents/runner.py +159 -0
- chcode/chat.py +1630 -0
- chcode/cli.py +142 -0
- chcode/config.py +571 -0
- chcode/display.py +325 -0
- chcode/prompts.py +640 -0
- chcode/session.py +149 -0
- chcode/skill_manager.py +165 -0
- chcode/utils/__init__.py +3 -0
- chcode/utils/enhanced_chat_openai.py +368 -0
- chcode/utils/git_checker.py +38 -0
- chcode/utils/git_manager.py +261 -0
- chcode/utils/modelscope_ratelimit.py +65 -0
- chcode/utils/multimodal.py +268 -0
- chcode/utils/shell/__init__.py +17 -0
- chcode/utils/shell/output.py +63 -0
- chcode/utils/shell/provider.py +128 -0
- chcode/utils/shell/result.py +14 -0
- chcode/utils/shell/semantics.py +55 -0
- chcode/utils/shell/session.py +159 -0
- chcode/utils/skill_loader.py +565 -0
- chcode/utils/text_utils.py +14 -0
- chcode/utils/tool_result_pipeline.py +244 -0
- chcode/utils/tools.py +1724 -0
- chcode/vision_config.py +371 -0
- chcode-0.1.0.dist-info/METADATA +275 -0
- chcode-0.1.0.dist-info/RECORD +36 -0
- chcode-0.1.0.dist-info/WHEEL +4 -0
- chcode-0.1.0.dist-info/entry_points.txt +2 -0
- chcode-0.1.0.dist-info/licenses/LICENSE +21 -0
chcode/__init__.py
ADDED
|
File without changes
|
chcode/__main__.py
ADDED
chcode/agent_setup.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent 构建 — 中间件注册、checkpointer 初始化
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from langchain.agents import create_agent
|
|
13
|
+
from langchain.agents.middleware import (
|
|
14
|
+
dynamic_prompt,
|
|
15
|
+
wrap_tool_call,
|
|
16
|
+
wrap_model_call,
|
|
17
|
+
ModelRequest,
|
|
18
|
+
ModelResponse,
|
|
19
|
+
HumanInTheLoopMiddleware,
|
|
20
|
+
)
|
|
21
|
+
from langchain.agents.middleware.context_editing import (
|
|
22
|
+
ContextEditingMiddleware,
|
|
23
|
+
ClearToolUsesEdit,
|
|
24
|
+
)
|
|
25
|
+
from langchain.agents.middleware.summarization import SummarizationMiddleware
|
|
26
|
+
from langchain_core.messages import ToolMessage
|
|
27
|
+
from langchain.tools.tool_node import ToolCallRequest
|
|
28
|
+
from langgraph.types import Command
|
|
29
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
30
|
+
|
|
31
|
+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
|
|
32
|
+
from chcode.utils.modelscope_ratelimit import is_modelscope_model, get_modelscope_clients
|
|
33
|
+
from chcode.utils.multimodal import is_multimodal_model
|
|
34
|
+
from chcode.utils.skill_loader import SkillAgentContext
|
|
35
|
+
from chcode.display import console
|
|
36
|
+
from chcode.utils.tool_result_pipeline import (
|
|
37
|
+
clean_tool_output,
|
|
38
|
+
truncate_large_result,
|
|
39
|
+
enforce_per_turn_budget,
|
|
40
|
+
reset_budget_state, # noqa: F401 # 重新导出供其他模块使用
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
import aiosqlite
|
|
44
|
+
|
|
45
|
+
# ─── 内置默认模型配置 ──────────────────────────────────
|
|
46
|
+
|
|
47
|
+
import os
|
|
48
|
+
|
|
49
|
+
INNER_MODEL_CONFIG = {
|
|
50
|
+
"model": "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
51
|
+
"base_url": "https://api-inference.modelscope.cn/v1",
|
|
52
|
+
"api_key": os.getenv("ModelScopeToken"),
|
|
53
|
+
"temperature": 1,
|
|
54
|
+
"top_p": 1,
|
|
55
|
+
"stream_usage": True,
|
|
56
|
+
"extra_body": {"stream": True},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ─── 重试配置 ──────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
RETRY_DELAYS = [3, 10, 30, 60]
|
|
63
|
+
_fallback_models: list[dict] = []
|
|
64
|
+
_fallback_index: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def set_fallback_models(models: list[dict]) -> None:
|
|
68
|
+
global _fallback_models, _fallback_index
|
|
69
|
+
_fallback_models = models
|
|
70
|
+
_fallback_index = 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_fallback_model() -> dict | None:
|
|
74
|
+
if _fallback_index < len(_fallback_models):
|
|
75
|
+
return _fallback_models[_fallback_index]
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def advance_fallback() -> None:
|
|
80
|
+
global _fallback_index
|
|
81
|
+
_fallback_index += 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _load_fallback_config() -> dict | None:
|
|
85
|
+
"""获取当前备用模型"""
|
|
86
|
+
global _fallback_models
|
|
87
|
+
if not _fallback_models:
|
|
88
|
+
from chcode.config import load_model_json
|
|
89
|
+
|
|
90
|
+
data = load_model_json()
|
|
91
|
+
fallback = data.get("fallback", {})
|
|
92
|
+
if not fallback:
|
|
93
|
+
return None
|
|
94
|
+
_fallback_models = list(fallback.values())
|
|
95
|
+
|
|
96
|
+
return get_fallback_model()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ─── 中间件 ──────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@wrap_tool_call
|
|
103
|
+
async def handle_tool_errors(
|
|
104
|
+
request: ToolCallRequest, handler: Callable[[ToolCallRequest], Command]
|
|
105
|
+
) -> Command | ToolMessage:
|
|
106
|
+
try:
|
|
107
|
+
return await handler(request)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
return ToolMessage(
|
|
110
|
+
f"Tool error: Please check your input and try again ({e})",
|
|
111
|
+
tool_call_id=request.tool_call["id"],
|
|
112
|
+
status="error",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ModelSwitchError(Exception):
|
|
117
|
+
"""标记需要切换模型的异常"""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@wrap_tool_call
|
|
122
|
+
async def filter_vision_tool(
|
|
123
|
+
request: ToolCallRequest,
|
|
124
|
+
handler: Callable[[ToolCallRequest], Command],
|
|
125
|
+
) -> Command | ToolMessage:
|
|
126
|
+
"""多模态模型时屏蔽 vision 工具 — 模型自带视觉能力"""
|
|
127
|
+
tool_name = request.tool_call.get("name", "")
|
|
128
|
+
if tool_name == "vision":
|
|
129
|
+
model_config = request.runtime.context.model_config
|
|
130
|
+
model_name = model_config.get("model", "")
|
|
131
|
+
|
|
132
|
+
if is_multimodal_model(model_name):
|
|
133
|
+
return ToolMessage(
|
|
134
|
+
content="当前模型支持原生视觉,图片/视频已直接嵌入消息,无需调用 vision 工具。请直接分析消息中的图片/视频内容。",
|
|
135
|
+
tool_call_id=request.tool_call["id"],
|
|
136
|
+
status="error",
|
|
137
|
+
)
|
|
138
|
+
return await handler(request)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@wrap_model_call
|
|
142
|
+
async def model_retry_with_backoff(
|
|
143
|
+
request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
144
|
+
) -> ModelResponse:
|
|
145
|
+
"""指数级退避重试中间件 — 每次调用独立计数"""
|
|
146
|
+
max_retries = 4
|
|
147
|
+
|
|
148
|
+
retry_count = 0
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
return await handler(request)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
retry_count += 1
|
|
155
|
+
|
|
156
|
+
if retry_count >= max_retries:
|
|
157
|
+
fallback = _load_fallback_config()
|
|
158
|
+
if fallback:
|
|
159
|
+
console.print(f"[yellow]主模型重试{retry_count}次失败,切换到备用模型...[/yellow]")
|
|
160
|
+
raise ModelSwitchError("切换到备用模型")
|
|
161
|
+
console.print(f"[red]请求失败,无备用模型可用,放弃请求\n {e}[/red]")
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
delay_idx = min(retry_count - 1, len(RETRY_DELAYS) - 1)
|
|
165
|
+
delay = RETRY_DELAYS[delay_idx]
|
|
166
|
+
|
|
167
|
+
console.print(f"[yellow]请求失败 ({retry_count}/{max_retries}), {delay}秒后重试...\n {e}[/yellow]")
|
|
168
|
+
|
|
169
|
+
await asyncio.sleep(delay)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dynamic_prompt
|
|
173
|
+
async def load_skills(request: ModelRequest) -> str:
|
|
174
|
+
"""构建 system prompt — Level 1: 注入所有 Skills 元数据"""
|
|
175
|
+
skill_loader = request.runtime.context.skill_loader
|
|
176
|
+
os_name = sys.platform
|
|
177
|
+
model_config = request.runtime.context.model_config
|
|
178
|
+
model_name = model_config.get("model", "")
|
|
179
|
+
|
|
180
|
+
native_vision = is_multimodal_model(model_name)
|
|
181
|
+
|
|
182
|
+
if native_vision:
|
|
183
|
+
base_prompt = f"""You are a coding assistant. OS: {os_name}. CWD: {request.runtime.context.working_directory}.
|
|
184
|
+
|
|
185
|
+
Tools:
|
|
186
|
+
- bash: execute shell commands and scripts. Stop immediately if the user refuses.
|
|
187
|
+
- read_file: view file content; write_file: create or save files; edit: modify existing files. Always read before write, prefer edit over write_file.
|
|
188
|
+
- glob: find files by name pattern; grep: search file contents with regex; list_dir: browse directory structure.
|
|
189
|
+
- web_search: search the Internet; web_fetch: fetch and read a URL's content.
|
|
190
|
+
- ask_user: present choices to the user and collect their input or confirmation.
|
|
191
|
+
- todo_write: create and manage a task list for complex multi-step work.
|
|
192
|
+
- load_skill: when a request matches a skill's description, load it first to get detailed instructions.
|
|
193
|
+
|
|
194
|
+
Guidelines:
|
|
195
|
+
- Never create .md/README files unless explicitly asked.
|
|
196
|
+
- You have native vision capability. When the user sends an image or video file path, the image/video is already embedded in the message — analyze it directly. Do NOT call the vision tool."""
|
|
197
|
+
else:
|
|
198
|
+
base_prompt = f"""You are a coding assistant. OS: {os_name}. CWD: {request.runtime.context.working_directory}.
|
|
199
|
+
|
|
200
|
+
Tools:
|
|
201
|
+
- bash: execute shell commands and scripts. Stop immediately if the user refuses.
|
|
202
|
+
- read_file: view file content; write_file: create or save files; edit: modify existing files. Always read before write, prefer edit over write_file.
|
|
203
|
+
- glob: find files by name pattern; grep: search file contents with regex; list_dir: browse directory structure.
|
|
204
|
+
- web_search: search the Internet; web_fetch: fetch and read a URL's content.
|
|
205
|
+
- ask_user: present choices to the user and collect their input or confirmation.
|
|
206
|
+
- todo_write: create and manage a task list for complex multi-step work.
|
|
207
|
+
- load_skill: when a request matches a skill's description, load it first to get detailed instructions.
|
|
208
|
+
- vision: analyze an image or video file using a vision model. Use when the user provides an image/video path or asks about visual content. Supports PNG, JPG, GIF, BMP, WebP, TIFF, MP4, MOV, AVI, MKV, WebM. The user can paste file paths directly in chat.
|
|
209
|
+
|
|
210
|
+
Guidelines:
|
|
211
|
+
- Never create .md/README files unless explicitly asked.
|
|
212
|
+
- When the user sends an image or video file path, use vision to understand it before responding."""
|
|
213
|
+
|
|
214
|
+
return await asyncio.to_thread(skill_loader.build_system_prompt, base_prompt)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@wrap_model_call
|
|
218
|
+
async def load_model(
|
|
219
|
+
request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
220
|
+
) -> ModelResponse:
|
|
221
|
+
"""动态加载模型"""
|
|
222
|
+
model_config = request.runtime.context.model_config
|
|
223
|
+
kwargs = dict(model_config)
|
|
224
|
+
if is_modelscope_model(model_config):
|
|
225
|
+
sync_client, async_client = get_modelscope_clients()
|
|
226
|
+
kwargs["http_client"] = sync_client
|
|
227
|
+
kwargs["http_async_client"] = async_client
|
|
228
|
+
return await handler(request.override(model=EnhancedChatOpenAI(**kwargs)))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@wrap_model_call
|
|
232
|
+
async def fix_messages(
|
|
233
|
+
request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
234
|
+
) -> ModelResponse:
|
|
235
|
+
"""过滤隐藏消息"""
|
|
236
|
+
messages = request.messages
|
|
237
|
+
real_messages = [m for m in messages if not m.additional_kwargs.get("composed", "")]
|
|
238
|
+
if len(real_messages) == len(messages):
|
|
239
|
+
return await handler(request)
|
|
240
|
+
return await handler(request.override(messages=real_messages))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@wrap_model_call
|
|
244
|
+
async def tool_result_budget(
|
|
245
|
+
request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
246
|
+
) -> ModelResponse:
|
|
247
|
+
"""工具结果截断和 token 预算控制"""
|
|
248
|
+
workplace = request.runtime.context.working_directory
|
|
249
|
+
messages = list(request.messages)
|
|
250
|
+
changed = False
|
|
251
|
+
for i, msg in enumerate(messages):
|
|
252
|
+
if isinstance(msg, ToolMessage) and msg.content:
|
|
253
|
+
if msg.additional_kwargs.get("_budget_ok"):
|
|
254
|
+
continue
|
|
255
|
+
cleaned = clean_tool_output(msg.content)
|
|
256
|
+
truncated = truncate_large_result(
|
|
257
|
+
cleaned,
|
|
258
|
+
msg.name or "",
|
|
259
|
+
msg.tool_call_id,
|
|
260
|
+
workplace=workplace,
|
|
261
|
+
)
|
|
262
|
+
new_kwargs = {**msg.additional_kwargs, "_budget_ok": True}
|
|
263
|
+
messages[i] = msg.model_copy(update={"content": truncated, "additional_kwargs": new_kwargs})
|
|
264
|
+
changed = True
|
|
265
|
+
if changed:
|
|
266
|
+
messages = enforce_per_turn_budget(messages, budget=200_000, workplace=workplace)
|
|
267
|
+
return await handler(request.override(messages=messages))
|
|
268
|
+
return await handler(request)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ─── Agent 构建 ──────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class AsyncHITL(HumanInTheLoopMiddleware):
|
|
275
|
+
"""异步 HITL 中间件 — 审批在 chat loop 中处理"""
|
|
276
|
+
|
|
277
|
+
async def awrap_model_call(self, request, handler):
|
|
278
|
+
return await handler(request)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
_hitl_middleware: AsyncHITL | None = None
|
|
282
|
+
_summarization_model: EnhancedChatOpenAI | None = None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_interrupt_on(yolo: bool) -> dict:
|
|
286
|
+
return (
|
|
287
|
+
{}
|
|
288
|
+
if yolo
|
|
289
|
+
else {
|
|
290
|
+
"bash": {"allowed_decisions": ["approve", "reject"]},
|
|
291
|
+
"edit": {"allowed_decisions": ["approve", "reject"]},
|
|
292
|
+
"write_file": {"allowed_decisions": ["approve", "reject"]},
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _dummy_model():
|
|
298
|
+
from langchain_openai import ChatOpenAI
|
|
299
|
+
|
|
300
|
+
return ChatOpenAI(model="placeholder", api_key="sk-placeholder", max_retries=0)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def build_agent(
|
|
304
|
+
model_config: dict | None = None,
|
|
305
|
+
checkpointer: AsyncSqliteSaver | None = None,
|
|
306
|
+
mcp_tools: list | None = None,
|
|
307
|
+
yolo: bool = False,
|
|
308
|
+
) -> object:
|
|
309
|
+
"""构建 agent 实例"""
|
|
310
|
+
global _hitl_middleware, _summarization_model
|
|
311
|
+
|
|
312
|
+
cfg = model_config or INNER_MODEL_CONFIG
|
|
313
|
+
model = _dummy_model()
|
|
314
|
+
|
|
315
|
+
_hitl_middleware = AsyncHITL(interrupt_on=_build_interrupt_on(yolo))
|
|
316
|
+
_summarization_model = EnhancedChatOpenAI(**cfg)
|
|
317
|
+
|
|
318
|
+
# 加载 fallback 模型配置
|
|
319
|
+
from chcode.config import load_model_json, get_context_window_size
|
|
320
|
+
|
|
321
|
+
data = load_model_json()
|
|
322
|
+
fallback = data.get("fallback", {})
|
|
323
|
+
if fallback:
|
|
324
|
+
current_model = cfg.get("model", "")
|
|
325
|
+
filtered = [v for k, v in fallback.items() if v.get("model") != current_model]
|
|
326
|
+
set_fallback_models(filtered)
|
|
327
|
+
|
|
328
|
+
# 摘要触发阈值 = 上下文窗口的 90%
|
|
329
|
+
model_name = cfg.get("model", "")
|
|
330
|
+
ctx_window = get_context_window_size(model_name)
|
|
331
|
+
summary_trigger = int(ctx_window * 0.9)
|
|
332
|
+
|
|
333
|
+
agent = create_agent(
|
|
334
|
+
model,
|
|
335
|
+
_get_all_tools() + (mcp_tools or []),
|
|
336
|
+
middleware=[
|
|
337
|
+
handle_tool_errors,
|
|
338
|
+
filter_vision_tool,
|
|
339
|
+
tool_result_budget,
|
|
340
|
+
load_skills,
|
|
341
|
+
load_model,
|
|
342
|
+
model_retry_with_backoff,
|
|
343
|
+
fix_messages,
|
|
344
|
+
ContextEditingMiddleware(
|
|
345
|
+
edits=[
|
|
346
|
+
ClearToolUsesEdit(
|
|
347
|
+
trigger=100_000,
|
|
348
|
+
keep=3,
|
|
349
|
+
exclude_tools=["read_file"],
|
|
350
|
+
placeholder="[Old tool result content cleared]",
|
|
351
|
+
)
|
|
352
|
+
]
|
|
353
|
+
),
|
|
354
|
+
SummarizationMiddleware(
|
|
355
|
+
model=_summarization_model,
|
|
356
|
+
trigger=("tokens", summary_trigger),
|
|
357
|
+
keep=("messages", 20),
|
|
358
|
+
),
|
|
359
|
+
_hitl_middleware,
|
|
360
|
+
],
|
|
361
|
+
context_schema=SkillAgentContext,
|
|
362
|
+
checkpointer=checkpointer,
|
|
363
|
+
)
|
|
364
|
+
return agent
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def update_hitl_config(yolo: bool) -> None:
|
|
368
|
+
"""运行时更新 HITL interrupt_on 配置,无需重建 agent"""
|
|
369
|
+
if _hitl_middleware is not None:
|
|
370
|
+
_hitl_middleware.interrupt_on = _build_interrupt_on(yolo)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def update_summarization_model(model_config: dict) -> None:
|
|
374
|
+
"""运行时更新 SummarizationMiddleware 的模型"""
|
|
375
|
+
if _summarization_model is not None:
|
|
376
|
+
new_model = EnhancedChatOpenAI(**model_config)
|
|
377
|
+
for key in new_model.model_fields_set:
|
|
378
|
+
try:
|
|
379
|
+
if key in new_model.__dict__:
|
|
380
|
+
setattr(_summarization_model, key, new_model.__dict__[key])
|
|
381
|
+
except (AttributeError, TypeError):
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async def create_checkpointer(db_path: Path) -> AsyncSqliteSaver:
|
|
386
|
+
"""创建异步 SQLite checkpointer"""
|
|
387
|
+
conn = await aiosqlite.connect(str(db_path))
|
|
388
|
+
return AsyncSqliteSaver(conn)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _get_all_tools() -> list:
|
|
392
|
+
"""获取所有工具(延迟导入避免循环依赖)"""
|
|
393
|
+
from chcode.utils.tools import ALL_TOOLS
|
|
394
|
+
|
|
395
|
+
return ALL_TOOLS
|
|
File without changes
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class AgentDefinition:
|
|
8
|
+
agent_type: str
|
|
9
|
+
when_to_use: str
|
|
10
|
+
system_prompt: str
|
|
11
|
+
tools: list[str] | None = None
|
|
12
|
+
disallowed_tools: list[str] = field(default_factory=list)
|
|
13
|
+
model: str | None = None
|
|
14
|
+
read_only: bool = False
|
|
15
|
+
source: str = "built-in"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_GENERAL_PURPOSE_SYSTEM_PROMPT = """You are a sub-agent for ChCode, a terminal-based AI coding assistant. Given the task description, use the tools available to complete it fully.
|
|
19
|
+
|
|
20
|
+
Your strengths:
|
|
21
|
+
- Searching for code, configurations, and patterns across large codebases
|
|
22
|
+
- Analyzing multiple files to understand system architecture
|
|
23
|
+
- Investigating complex questions that require exploring many files
|
|
24
|
+
- Performing multi-step research tasks
|
|
25
|
+
|
|
26
|
+
Guidelines:
|
|
27
|
+
- For file searches: search broadly when you don't know where something lives. Use read_file when you know the specific file path.
|
|
28
|
+
- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
|
|
29
|
+
- Be thorough: Check multiple locations, consider different naming conventions, look for related files.
|
|
30
|
+
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one.
|
|
31
|
+
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested.
|
|
32
|
+
|
|
33
|
+
When you complete the task, respond with a concise report covering what was done and any key findings."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_EXPLORE_SYSTEM_PROMPT = """You are a file search specialist for ChCode. You excel at thoroughly navigating and exploring codebases.
|
|
37
|
+
|
|
38
|
+
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
|
39
|
+
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
|
|
40
|
+
- Creating new files (no write_file, touch, or file creation of any kind)
|
|
41
|
+
- Modifying existing files (no edit operations)
|
|
42
|
+
- Deleting files (no rm or deletion)
|
|
43
|
+
- Moving or copying files (no mv or cp)
|
|
44
|
+
- Running ANY commands that change system state
|
|
45
|
+
|
|
46
|
+
Your role is EXCLUSIVELY to search and analyze existing code.
|
|
47
|
+
|
|
48
|
+
Your strengths:
|
|
49
|
+
- Rapidly finding files using glob patterns
|
|
50
|
+
- Searching code and text with powerful regex patterns via grep
|
|
51
|
+
- Reading and analyzing file contents
|
|
52
|
+
|
|
53
|
+
Guidelines:
|
|
54
|
+
- Use glob for broad file pattern matching
|
|
55
|
+
- Use grep for searching file contents with regex
|
|
56
|
+
- Use read_file when you know the specific file path you need to read
|
|
57
|
+
- Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
|
58
|
+
- NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
|
|
59
|
+
- Adapt your search approach based on the thoroughness level specified by the caller
|
|
60
|
+
- Communicate your final report directly as a regular message - do NOT attempt to create files
|
|
61
|
+
|
|
62
|
+
NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:
|
|
63
|
+
- Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations
|
|
64
|
+
- Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files
|
|
65
|
+
|
|
66
|
+
Complete the user's search request efficiently and report your findings clearly."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_PLAN_SYSTEM_PROMPT = """You are a software architect and planning specialist for ChCode. Your role is to explore the codebase and design implementation plans.
|
|
70
|
+
|
|
71
|
+
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
|
72
|
+
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
|
|
73
|
+
- Creating new files (no write_file, touch, or file creation of any kind)
|
|
74
|
+
- Modifying existing files (no edit operations)
|
|
75
|
+
- Deleting files (no rm or deletion)
|
|
76
|
+
- Running ANY commands that change system state
|
|
77
|
+
|
|
78
|
+
Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
|
|
79
|
+
|
|
80
|
+
You will be provided with a set of requirements and optionally a perspective on how to approach the design process.
|
|
81
|
+
|
|
82
|
+
## Your Process
|
|
83
|
+
|
|
84
|
+
1. **Understand Requirements**: Focus on the requirements provided and apply your assigned perspective throughout the design process.
|
|
85
|
+
|
|
86
|
+
2. **Explore Thoroughly**:
|
|
87
|
+
- Read any files provided to you in the initial prompt
|
|
88
|
+
- Find existing patterns and conventions using glob, grep, and read_file
|
|
89
|
+
- Understand the current architecture
|
|
90
|
+
- Identify similar features as reference
|
|
91
|
+
- Trace through relevant code paths
|
|
92
|
+
- Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
|
93
|
+
- NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
|
|
94
|
+
|
|
95
|
+
3. **Design Solution**:
|
|
96
|
+
- Create implementation approach based on your assigned perspective
|
|
97
|
+
- Consider trade-offs and architectural decisions
|
|
98
|
+
- Follow existing patterns where appropriate
|
|
99
|
+
|
|
100
|
+
4. **Detail the Plan**:
|
|
101
|
+
- Provide step-by-step implementation strategy
|
|
102
|
+
- Identify dependencies and sequencing
|
|
103
|
+
- Anticipate potential challenges
|
|
104
|
+
|
|
105
|
+
## Required Output
|
|
106
|
+
|
|
107
|
+
End your response with:
|
|
108
|
+
|
|
109
|
+
### Critical Files for Implementation
|
|
110
|
+
List 3-5 files most critical for implementing this plan:
|
|
111
|
+
- path/to/file1
|
|
112
|
+
- path/to/file2
|
|
113
|
+
- path/to/file3
|
|
114
|
+
|
|
115
|
+
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
BUILT_IN_AGENTS: dict[str, AgentDefinition] = {
|
|
119
|
+
"general-purpose": AgentDefinition(
|
|
120
|
+
agent_type="general-purpose",
|
|
121
|
+
when_to_use=(
|
|
122
|
+
"General-purpose agent for researching complex questions, searching for code, "
|
|
123
|
+
"and executing multi-step tasks. When you are searching for a keyword or file "
|
|
124
|
+
"and are not confident that you will find the right match in the first few tries "
|
|
125
|
+
"use this agent to perform the search for you."
|
|
126
|
+
),
|
|
127
|
+
system_prompt=_GENERAL_PURPOSE_SYSTEM_PROMPT,
|
|
128
|
+
tools=None,
|
|
129
|
+
disallowed_tools=[],
|
|
130
|
+
read_only=False,
|
|
131
|
+
),
|
|
132
|
+
"Explore": AgentDefinition(
|
|
133
|
+
agent_type="Explore",
|
|
134
|
+
when_to_use=(
|
|
135
|
+
"Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns "
|
|
136
|
+
'(eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions '
|
|
137
|
+
'about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired '
|
|
138
|
+
'thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or '
|
|
139
|
+
'"very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
|
140
|
+
),
|
|
141
|
+
system_prompt=_EXPLORE_SYSTEM_PROMPT,
|
|
142
|
+
tools=None,
|
|
143
|
+
disallowed_tools=["write_file", "edit"],
|
|
144
|
+
read_only=True,
|
|
145
|
+
),
|
|
146
|
+
"Plan": AgentDefinition(
|
|
147
|
+
agent_type="Plan",
|
|
148
|
+
when_to_use=(
|
|
149
|
+
"Software architect agent for designing implementation plans. Use this when you need to "
|
|
150
|
+
"plan the implementation strategy for a task. Returns step-by-step plans, identifies "
|
|
151
|
+
"critical files, and considers architectural trade-offs."
|
|
152
|
+
),
|
|
153
|
+
system_prompt=_PLAN_SYSTEM_PROMPT,
|
|
154
|
+
tools=None,
|
|
155
|
+
disallowed_tools=["write_file", "edit"],
|
|
156
|
+
read_only=True,
|
|
157
|
+
),
|
|
158
|
+
}
|
chcode/agents/loader.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from chcode.agents.definitions import AgentDefinition
|
|
9
|
+
|
|
10
|
+
DEFAULT_AGENT_PATHS = [
|
|
11
|
+
Path.cwd() / ".chat" / "agents",
|
|
12
|
+
Path.home() / ".chat" / "agents",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_agent_md(md_path: Path) -> AgentDefinition | None:
|
|
17
|
+
try:
|
|
18
|
+
content = md_path.read_text(encoding="utf-8")
|
|
19
|
+
except Exception:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
|
|
23
|
+
if not fm_match:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
frontmatter = yaml.safe_load(fm_match.group(1))
|
|
28
|
+
except yaml.YAMLError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
if not isinstance(frontmatter, dict):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
agent_type = frontmatter.get("name", "")
|
|
35
|
+
description = frontmatter.get("description", "")
|
|
36
|
+
|
|
37
|
+
if not agent_type or not description:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
body = content[fm_match.end() :]
|
|
41
|
+
system_prompt = body.strip()
|
|
42
|
+
if not system_prompt:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
tools_raw = frontmatter.get("tools")
|
|
46
|
+
tools = (
|
|
47
|
+
[t.strip() for t in tools_raw.split(",") if t.strip()]
|
|
48
|
+
if isinstance(tools_raw, str)
|
|
49
|
+
else None
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
disallowed_raw = frontmatter.get("disallowed_tools")
|
|
53
|
+
disallowed_tools = (
|
|
54
|
+
[t.strip() for t in disallowed_raw.split(",") if t.strip()]
|
|
55
|
+
if isinstance(disallowed_raw, str)
|
|
56
|
+
else []
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
model = frontmatter.get("model") or None
|
|
60
|
+
read_only = bool(frontmatter.get("read_only", False))
|
|
61
|
+
|
|
62
|
+
return AgentDefinition(
|
|
63
|
+
agent_type=agent_type,
|
|
64
|
+
when_to_use=description.replace("\\n", "\n"),
|
|
65
|
+
system_prompt=system_prompt,
|
|
66
|
+
tools=tools,
|
|
67
|
+
disallowed_tools=disallowed_tools,
|
|
68
|
+
model=model,
|
|
69
|
+
read_only=read_only,
|
|
70
|
+
source="custom",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_agents_cache: dict[str, AgentDefinition] | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_agents(extra_paths: list[Path] | None = None) -> dict[str, AgentDefinition]:
|
|
78
|
+
global _agents_cache
|
|
79
|
+
|
|
80
|
+
if _agents_cache is not None and not extra_paths:
|
|
81
|
+
return dict(_agents_cache)
|
|
82
|
+
|
|
83
|
+
from chcode.agents.definitions import BUILT_IN_AGENTS
|
|
84
|
+
|
|
85
|
+
result: dict[str, AgentDefinition] = dict(BUILT_IN_AGENTS)
|
|
86
|
+
|
|
87
|
+
paths = list(DEFAULT_AGENT_PATHS)
|
|
88
|
+
if extra_paths:
|
|
89
|
+
paths = extra_paths + paths
|
|
90
|
+
|
|
91
|
+
for base_path in paths:
|
|
92
|
+
if not base_path.exists():
|
|
93
|
+
continue
|
|
94
|
+
for item in base_path.iterdir():
|
|
95
|
+
if not item.is_file() or not item.suffix == ".md":
|
|
96
|
+
continue
|
|
97
|
+
agent = _parse_agent_md(item)
|
|
98
|
+
if agent and agent.agent_type not in result:
|
|
99
|
+
result[agent.agent_type] = agent
|
|
100
|
+
|
|
101
|
+
if not extra_paths:
|
|
102
|
+
_agents_cache = result
|
|
103
|
+
|
|
104
|
+
return result
|