内容
# AshCode
[](https://github.com/AuthRan/AshCode/actions/workflows/ci.yml)
[](https://pypi.org/project/ashcode/)
[](https://www.python.org/)
[](LICENSE)
一款基于Python的终端编程代理,具备流式代理循环、12种内置工具、MCP服务器、子代理、上下文压缩和可配置的shell及文件操作审批层。
AshCode可以读取和写入文件、运行shell命令、搜索网络,并将任务委托给范围内的子代理 — 所有这些都由任何兼容OpenAI的模型端点驱动。从头开始构建,以了解编码代理的实际需求:不仅是工具调用循环,还有有限窗口下的上下文管理、破坏性操作的 安全门控以及模型卡住时的恢复。
<p align="center">
<img src="resources/AshCode_singleShot.gif"
alt="AshCode从终端解决单个提示:模型调用list_dir,工具结果流回,代理回答"
width="850">
</p>
<p align="center">
<sub><b>单次模式。</b> 一个提示,一个答案 — 代理选择自己的工具并退出。<br>
<code>python main.py "列出此目录中的文件"</code></sub>
</p>
<p align="center">
<sub>
视频演示:
<a href="https://youtu.be/G9MtxyqXOQ0">▶ 浏览代码库</a>
·
<a href="https://youtu.be/DsuoKmBzODU">▶ 创建和删除文件</a>
</sub>
</p>
---
## 目录
- [为什么存在](#为什么存在)
- [功能](#功能)
- [架构](#架构)
- [安装](#安装)
- [用法](#用法)
- [配置](#配置)
- [扩展](#扩展)
- [工程笔记](#工程笔记)
- [测试](#测试)
- [项目布局](#项目布局)
---
## 为什么存在
将聊天完成包装在一个`while`循环中可以得到一个演示。演示和可用代理之间的差距是由一些不那么华丽的问题组成的,这个项目试图正确地解决它们,而不是跳过它们:
- **上下文窗口已满。** 真实的会话在20分钟内就会超过限制。AshCode在窗口容量达到80%时,将历史记录压缩成结构化摘要,并单独清除过时的工具输出,这样长时间的会话就会平稳地降级,而不是出错。
- **模型卡住。** 它们将调用相同的失败工具永远。基于签名的循环检测器捕获确切的重复和短循环,然后注入纠正提示。
- **代理运行破坏性命令。** 六个审批策略控制修改操作,具有交互式确认路径和文件写入前的差异预览。
- **广泛探索会破坏上下文。** 子代理运行具有自己的回合预算和受限的工具集,返回摘要而不是生成摘要所需的20次文件读取。
- **流式处理使得解析状态机成为必要。** 工具调用跨SSE块分散,需要正确地重新组装 — 请参阅[工程笔记](#工程笔记)。
## 功能
**代理循环**
- 全流式 — 文本在工具调用被收集的同时逐个字符渲染
- 单次调用中并行工具调用
- 类型事件流(`AgentEvent`)将循环与任何渲染器解耦
- 可配置的回合预算,具备平稳耗尽
**12种内置工具**
| | |
|---|---|
| `read_file` `write_file` `edit_file` `apply_patch` | 文件操作,带有统一差异预览 |
| `list_dir` `glob` `grep` | 导航和搜索 |
| `shell` | 命令执行,带有超时和环境清理 |
| `web_search` `web_fetch` | 网络访问 |
| `memory` `todo` | 跨回合状态 |
**上下文管理**
- 在窗口容量达到80%时自动压缩为结构化摘要
- 工具输出在受保护的最近窗口外被修剪
- 每回合和累计令牌会计
**安全**
- 六种审批策略:`on-request`、`on-failure`、`auto`、`auto-edit`、`never`、`yolo`
- shell命令的正则表达式分类为危险和已知安全集
- 工作目录外的写入始终需要确认
- 机密通过模式(`*KEY*`、`*TOKEN*`、`*SECRET*`)从子进程环境中清除
**可扩展性**
- 通过stdio和HTTP/SSE的MCP服务器,通过与内置工具相同的注册表呈现
- 自定义工具从`.ai-agent/tools/*.py`自动发现
- 代理运行和工具调用的生命周期钩子
- 从工作目录中选取`AGENT.MD`作为项目说明
**会话**
- 保存、列出和恢复会话;创建和恢复检查点
- 在重启期间保留完整的消息历史和令牌总数
## 架构
```mermaid
flowchart TB
CLI["CLI / TUI<br/><i>渲染事件</i>"]
Agent["Agent<br/><i>异步生成器,产生类型事件</i>"]
Client["LLMClient<br/><i>SSE → 类型流事件</i>"]
Context["ContextManager<br/><i>压缩 + 修剪</i>"]
Registry["ToolRegistry<br/><i>验证 → 钩子 → 批准 → 执行</i>"]
Approval["ApprovalManager"]
Builtin["12种内置工具"]
MCP["MCP服务器"]
Sub["子代理<br/><i>嵌套代理,范围工具</i>"]
CLI -->|"用户消息"| Agent
Agent -->|"AgentEvent流"| CLI
Agent <--> Client
Agent <--> Context
Agent --> Registry
Registry --> Approval
Approval -.->|"确认?"| CLI
Registry --> Builtin
Registry --> MCP
Registry --> Sub
Sub -.->|"递归"| Agent
```
承载负载的决策是 **`Agent.run()` 是一个异步生成器,它产生类型事件,对终端一无所知。** TUI是一个消费者;子代理是另一个消费者,以编程方式消耗相同的事件并丢弃渲染。添加Web前端或非交互式CI运行意味着编写新的消费者,而不是触摸循环。
第二个是 **工具是自我描述的。** 工具声明了一个Pydantic模式,该模式自动转换为OpenAI函数模式。添加工具不需要更改代理循环、注册表或提示 — 这使得用户提供的工具的自动发现成为一个~40行的功能,而不是重新设计。
## 安装
需要Python 3.11+。
```bash
pip install ashcode
```
<details>
<summary>从源代码编译,以便进行修改</summary>
```bash
git clone https://github.com/AuthRan/AshCode.git
cd AshCode
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```
在下面用`python main.py`替换`ashcode`命令即可运行。
</details>
配置凭证:
```bash
cp .env.example .env
```
```ini
API_KEY=您的openrouter-api-key
BASE_URL=https://openrouter.ai/api/v1
```
任何兼容OpenAI的端点都有效 — OpenRouter、OpenAI、Together或本地llama.cpp或vLLM服务器。将`BASE_URL`指向它并设置`model.name`。
> **注意:** 模型必须支持工具调用。在OpenRouter上,`scripts/find_free_tool_models.py`列出了免费支持的模型。
## 用法
**交互式:**
```bash
ashcode
```
**单个提示:**
```bash
ashcode "解释client/llm_client.py中的重试逻辑"
```
**针对其他目录:**
```bash
ashcode --cwd ../other-project
```
### 斜杠命令
| 命令 | |
|---|---|
| `/help` | 显示命令 |
| `/config` | 当前模型、策略、工作目录、回合预算 |
| `/model [name]` | 显示或切换模型,会话中 |
| `/approval [policy]` | 显示或切换审批策略,会话中 |
| `/tools` | 列出所有已注册的工具 |
| `/mcp` | MCP服务器连接状态 |
| `/stats` | 回合次数和令牌使用情况 |
| `/clear` | 重置对话和循环检测器 |
| `/save` · `/sessions` · `/resume <id>` | 会话持久化 |
| `/checkpoint` · `/restore <id>` | 会话内的检查点 |
| `/exit` | 退出 |
## 配置
配置使用TOML,合并自两个层次 — 用户级文件,然后是项目级`.ai-agent/config.toml`,后者覆盖前者。机密存储在`.env`中,永不进入配置文件。
```toml
hooks_enabled = true
max_turns = 100
approval = "on-request"
[model]
name = "anthropic/claude-sonnet-4.5"
temperature = 0
context_window = 200000
# 从shell工具的环境中清除匹配的变量
[shell_environment]
exclude_patterns = ["*KEY*", "*TOKEN*", "*SECRET*"]
# 在代理运行和工具调用周围运行脚本
[[hooks]]
name = "lint_after_edit"
trigger = "after_tool"
command = "python ./scripts/lint.py"
# 将MCP服务器的工具暴露给代理
[mcp_servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
```
### 审批策略
| 策略 | 行为 |
|---|---|
| `on-request` | 默认,对非识别为安全的任何操作提示 *(默认)* |
| `on-failure` | 仅在命令失败后提示 |
| `auto` | 批准命令,对工作目录外的写入提示 |
| `auto-edit` | 批准文件编辑,对其他命令提示 |
| `never` | 仅运行已知安全的命令,拒绝其他命令,永不提示 |
| `yolo` | 批准所有操作 |
危险命令(`rm -rf /`、`mkfs`、`dd if=`、fork炸弹、`curl | sh`)在除`yolo`外的所有策略下直接拒绝。
> 命令分类是模式匹配,而不是沙盒 — 它增加了事故的成本,但并未遏制确定的事故。在容器中运行不受信任的工作负载。
### 项目说明
在工作目录中放置一个`AGENT.MD`,其内容将被加载到系统中 — 惯例、架构说明、代理在触摸代码之前应了解的内容。
## 扩展
### 自定义工具
`.ai-agent/tools/*.py`中的任何`Tool`子类都会被发现并在启动时注册。无需注册调用,无需配置条目:
```python
from pydantic import BaseModel, Field
from tools.base import Tool, ToolInvocation, ToolResult, ToolKind
class RunTestsParams(BaseModel):
path: str = Field(".", description="要测试的目录或文件")
class RunTestsTool(Tool):
name = "run_tests"
description = "运行pytest套件并返回摘要。"
kind = ToolKind.SHELL
schema = RunTestsParams
async def execute(self, invocation: ToolInvocation) -> ToolResult:
params = RunTestsParams(**invocation.params)
...
return ToolResult.success_result(output)
```
`kind`决定工具是否被视为可变,因此是否通过审批门控。
### 子代理
以声明方式定义,拥有自己的工具允许列表和回合预算。附带`codebase_investigator`(只读探索)和`code_reviewer):
```python
SubagentDefinition(
name="codebase_investigator",
description="调查代码库以回答有关结构和实现的问题",
goal_prompt="您是代码库调查专家。不要修改任何文件。",
allowed_tools=["read_file", "grep", "glob", "list_dir"],
max_turns=20,
)
```
## 工程笔记
构建过程中遇到的三个问题比预期的更有趣。
**流式工具调用是一个重新组装问题。** 提供者通过多个SSE块发送一个工具调用 — 第一个块携带ID和函数名,随后的每个块携带参数JSON片段。累积这些片段时,分支过深意味着只有第一个片段存活下来,工具被调用时带有`{}`。修复非常简单;注意到“工具拒绝我的参数”实际上是“参数从未到达”值得记住。现在通过重放片段的回归测试覆盖。
**安全代码必须关闭。** 审批门控是一系列检查,最终返回`APPROVED`,因此任何路径如果没有明确捕获某些内容,则会一路通过到宽容 — 而shell命令声明没有影响文件路径,因此采用了这种路径。所有非阻止命令在没有提示的情况下运行。默认情况下,如果默认值为`NEEDS_CONFIRMATION`,则相同的结构错误将产生一个讨厌的代理,而不是不安全的代理。现在测试断言 *负面情况:* 特定命令提示。
**依赖于进度的超时不是超时。** 子代理超时通过在产生的事件之间比较截止日期来强制执行,这仅在子代理产生事件时有效。存在超时情况的案例 — 阻塞在网络读取上 — 产生无。`asyncio.wait_for`从外部取消,而不是要求协程自检。
## 测试
```bash
pip install -r requirements-dev.txt
pytest -q
```
30个测试覆盖流式解析器、审批门控和请求有效载荷 — 这三个地方的静默错误最为昂贵。每个修复都通过在应用修复前运行其新测试并确认它们失败来验证,因为如果修复前测试通过,则测试无效。
CI在每次推送和拉取请求时,在Python 3.11、3.12和3.13上运行测试套件,以及一个导入检查,以捕获导入时的副作用。
## 项目布局
```
agent/ 代理循环,会话状态,类型事件,持久化
client/ 流式LLM客户端,SSE → 类型流事件
tools/ 工具ABC,注册表,12种内置工具,MCP集成,子代理
context/ 上下文窗口管理,压缩,循环检测
safety/ 审批策略和命令分类
config/ Pydantic配置模式和分层TOML加载器
hooks/ 生命周期钩子执行
prompts/ 系统提示组装
ui/ Rich终端UI
tests/ 回归测试
```
## 许可证
MIT