Metadata-Version: 2.4
Name: xiaoyu-agent
Version: 0.10.2
Summary: 小羽 — a harness coding agent
Project-URL: Repository, https://github.com/pholex/xiaoyu
Project-URL: Issues, https://github.com/pholex/xiaoyu/issues
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: openai==2.52.0

# 小羽 · Xiaoyu

[![ci](https://github.com/pholex/xiaoyu/actions/workflows/ci.yml/badge.svg)](https://github.com/pholex/xiaoyu/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/xiaoyu-agent)](https://pypi.org/project/xiaoyu-agent/)

> Weaving code, connecting dots, and showing you the best harness architecture.

一个自建的 harness coding agent。零第三方运行依赖（只有 openai SDK），
Windows / macOS / Linux 全平台，`pip install xiaoyu-agent` 即用。

名字取自董永传说里的七仙女**天羽**——织女织布，小羽织代码。羽毛在传统语义里是飞升与轻盈，
对应这个 agent 想要的手感：行云流水、轻量、无负担。

顺带一个巧合：**harness 本身就是织机的部件**（提综装置，控制经线升降的那套框架）。
所以"织女 + harness"不是比附，是同一个词。

## 现在能做什么（v0.10）

- 任意 OpenAI 兼容端点（LiteLLM / vLLM / 各家官方 API），流式输出
- **六个基础工具**（`explore` 与 `skill` 另见下方）：
  - `read_file`（支持 `offset` / `limit` 只读一段）
  - `grep`（正则搜索，自动跳过 `.git` / `node_modules` / `__pycache__` 等噪声）
  - `list_files`（glob 列文件，输出统一正斜杠）
  - `str_replace`（精确替换，主要编辑手段）
  - `write_file`（整文件覆盖，只用于新建或全量重写）
  - `bash`（Windows 上自动换 PowerShell 执行，工具描述与 system prompt 按平台生成）
- **`explore` 子 agent**：便宜模型 + 只读工具做检索，返回带 `路径:行号` + 原文行的结论。
  详见下方「explore 与实测数据」
- **编辑护栏**：改已有文件必须先**完整** `read_file`（`bash cat` 不算，只读一段也不算）；
  读完之后文件被外部改动会拒绝写入并要求重读；`old_str` 不唯一或匹配不上会带行号提示打回；
  行中间开始 + 多行替换会被拦（必然破坏缩进）
- **分层上下文回收**：超阈值先 **microcompact**（把较早的大块 `read_file`/`grep`/`bash`
  输出替换成占位符——不花模型调用、不磨损结论，够用就不做摘要）；
  不够再全量摘要——本地估算 token + 用真实 usage 校准，早期历史交给便宜模型总结。
  原始任务永久保留，摘要不层层累加，压缩后反而更大则放弃；
  **断路器**：连续两次压缩省不到 10% 就暂停自动压缩（手动 `/compact` 不受限）
- **权限规则**：`allow bash(git *)` / `deny bash(curl *)` / `allow write_file(src/*)` 这类
  规则免逐次确认或直接拦截；**deny 在任何模式下都生效（包括 `--yolo`）**；
  复合命令每一段都要被 allow 覆盖，含 `$( )` / 反引号 / `>` 的命令不吃 allow 前缀规则；
  规则放用户级 `permissions.txt` 或仓库 `.xiaoyu/permissions.txt`，REPL 里
  `/allow` `/deny` 直接写入、`/perm` 查看；确认框答 `a` = 本会话该工具不再问
- **危险命令硬拦截**：`rm -rf /`、fork bomb、`mkfs`、`dd` 直写块设备、Windows `format`
  等不可撤销操作在任何模式下都不执行，**包括 `--yolo`**——审批是"用户想不想"，这层是"绝不"
- **项目级指令文件**：读仓库根目录的 `AGENTS.md`（或 `XIAOYU.md` / `CLAUDE.md`，首个命中）
  进 system prompt——项目自带的规范（怎么跑测试、代码约定）跟着仓库走，不用每次口头交代
- **插件工具**：第三方包在 entry point 组 `xiaoyu.tools` 里声明工厂函数，
  `pip install` 后自动挂载（这是接内部工具的代码层通道）；坏插件只警告不拦启动、
  不许覆盖内置工具、未声明的能力按需要确认处理（fail-closed）
- **会话落盘**：交互与一次性执行的每条消息 append 到用户目录 `sessions/*.jsonl`
  （首行 meta，压缩/清屏记事件），供事后诊断，也是将来 `/resume` 的地基
- **SKILL.md 技能**：扫描 `~/.agents/skills/`（跨客户端规范库）与用户配置目录 `skills/`，
  与 Anthropic / agentskills.io 同形态；渐进披露——索引进 system prompt，
  正文由模型用 `skill` 工具按需加载，`/skills` 查看；
  索引有预算（单条描述 ≤250 字符、总量 ≤上下文窗口 1%），技能装再多也不吃常驻上下文
- **错误分类与自动恢复**：限流/瞬时错误按分类指数退避重试（±25% jitter 错峰，
  服务端给了 `Retry-After` 就听它的），重试只在这一层（SDK 层已关，不会 3×3 叠加）；
  上下文超限先强制压缩再重试，鉴权错误直接报清楚不空转；
  中断（Ctrl-C）后全量扫描补齐悬空的 tool 结果、半截流式回答也入历史，随时能继续对话
- **循环护栏**：撞到单轮工具调用上限时让模型收尾交代（做了什么/剩什么/建议），
  不静默截断；连续相同 (工具, 参数) 调用第 3 次附加提示、第 5 次拒绝执行——
  便宜模型容易原地打转，得在 harness 层刹住
- **工具可用性探测**：工具可挂 `check_fn`，探测不过就不进 schemas、拒绝执行
  （`/tools` 里标记 `[不可用]`）
- 写文件和执行命令**默认逐个人工确认**，`str_replace` 显示 `-/+` 差异预览
- 交互 REPL（`/help` `/tools` `/skills` `/model` `/usage` `/context` `/compact`
  `/perm` `/allow` `/deny` `/clear`）
  + 一次性执行模式 + 启动横幅；`xiaoyu config` 配置向导（全平台固定路径，免找 `.env`）
- **按模型分开记账**的 token 统计；eval 可横向扫 12 个候选模型并算成本
- **跨平台**：Windows（PowerShell 分派、输出统一 UTF-8）/ macOS / Linux，
  CI 三平台 × 两 Python 版本矩阵验证

还没做：接内部工具（飞书 / EDW / Amazon 运营；SKILL.md + 插件 entry point 两条载体已就绪）、
`/resume` 恢复会话（落盘已就绪）、TUI、真沙箱隔离。

## 测试

```bash
# 单元测试（255 个，全部不打网络；CI 在三平台 × py3.11/3.14 跑同一套）
.venv/bin/python -m unittest discover -s tests -t .

# eval：真实调模型跑端到端任务
.venv/bin/xiaoyu-eval --list
.venv/bin/xiaoyu-eval                            # 全部 case
.venv/bin/xiaoyu-eval --case targeted_edit -v    # 单个 case + 完整输出
.venv/bin/xiaoyu-eval --model bedrock-claude-opus-5 --repeat 3
```

### eval 集在测什么

| case | 卡的是什么 |
|---|---|
| `fix_and_test` | 修 bug + 加注解 + **自己写测试并真的跑通** |
| `targeted_edit` | 130 行文件里定点改 —— 用 diff 行数上限抓"整文件重写" |
| `readonly_answer` | 只读任务一个字都不许改 —— 抓"手痒乱动文件" |
| `multi_file_rename` | 跨 3 文件重命名，改完测试还得过 —— 抓"改一半" |

判据全部机械可判（文件内容、diff 规模、测试退出码、用了哪个工具），没有主观评分。
结果存到当前目录 `xiaoyu-eval-results/*.json`，含 token、耗时、工具调用序列，用来比较改 prompt / 换模型前后的差异。
失败的 case 会额外保存现场（transcript + 最终文件内容）——临时工作区跑完就删，不留现场就没法诊断。

**写新 case 的铁律：断言必须双向自证**。先喂"已知正确答案"确认全 PASS，
再喂"看似完成但实际错"确认能 FAIL。这条已经固化成 `tests/test_eval_assertions.py`，
不打网络就能跑，加 case 时顺手补上正反两个 fixture。

踩过的三个坑（都会让你误判成"agent 不行"）：

- 初始文件因为 `textwrap.dedent` 找不到公共前缀而带着缩进写进去，语法直接错
- 用 `file_contains("2 ")` 判断指数退避，占位函数里的 `return value * 2` 也命中，等于永远通过
- 用 `file_contains("ZeroDivisionError")` 判断"处理了除零"——模型抛 `ValueError` 是同样合理的设计，
  指令里没规定异常类型，这个断言等于偷偷加了一条没提的要求。**判行为，别判字面**

判行为用 `python_snippet_ok`：探针脚本写到工作区之外的临时文件、以工作区为 cwd 和 `PYTHONPATH` 运行，
既避开 shell 引号地狱，也不会污染 `nothing_written` / `unchanged_except` 的快照。

## 安装

```bash
pip install xiaoyu-agent      # 或 pipx install xiaoyu-agent
xiaoyu --version              # 验证装上了（多 Python 并存时也能确认升级生效在哪个环境）
```

升级：

```bash
pip install --upgrade xiaoyu-agent    # pipx 装的用：pipx upgrade xiaoyu-agent
```

开发模式：

```bash
git clone https://github.com/pholex/xiaoyu.git && cd xiaoyu
python3 -m venv .venv
.venv/bin/pip install -e .
```

## 配置

首次使用直接跑配置向导（全平台，写到固定的用户级路径，从此不用找 `.env` 放哪）：

```bash
xiaoyu config            # 交互向导：端点、模型、key
xiaoyu config --show     # 查看生效配置与各项来源（key 永不回显）
xiaoyu config --path     # 打印用户级配置文件路径
xiaoyu config --set XIAOYU_MODEL=deepseek-v4-pro   # 非交互写入，可重复
```

用户级配置文件的位置：macOS / Linux 在 `~/.config/xiaoyu/.env`（跟随 `$XDG_CONFIG_HOME`），
**Windows 在 `%APPDATA%\xiaoyu\.env`**。

也可以手动在任意工作目录放 `.env`（零依赖自解析，仓库里的已被 `.gitignore` 排除）：

```ini
XIAOYU_BASE_URL=https://<你的网关>/v1
XIAOYU_MODEL=bedrock-claude-sonnet-5
XIAOYU_API_KEY=<你的-key>
```

优先级：**真实环境变量 > 当前目录 `.env` > 项目根 `.env` > 用户级 `.env`**，所以临时覆盖很方便：

```bash
XIAOYU_MODEL=bedrock-claude-opus-5 xiaoyu
```

macOS 上 key 也可以不落盘，改用 Keychain（`.env` 里留空即可，会自动回退去读；Windows 上请用 `.env` 或环境变量）：

```bash
security add-generic-password -a "$USER" -s "XIAOYU_API_KEY" -U -w
```

| 变量 | 默认值 | 说明 |
|---|---|---|
| `XIAOYU_BASE_URL` | —（必填） | 任意 OpenAI 兼容 `/v1` 端点：LiteLLM、vLLM、各家官方 API… |
| `XIAOYU_MODEL` | `deepseek-v4-pro` | 见下方"选模型" |
| `XIAOYU_API_KEY` | — | 端点的 API key |
| `XIAOYU_ENV_FILE` | — | 指定 `.env` 路径，等价于 `--env-file` |

## 用

```bash
xiaoyu                                  # 交互模式（xy 是等价缩写）
xy "把 utils.py 里的类型注解补全"          # 一次性执行
xiaoyu --model bedrock-claude-opus-5    # 指定模型
```

REPL 里：`/help` `/tools` `/skills` `/model` `/usage` `/context` `/compact` `/clear` `/exit`

## 选模型

默认 **主模型 `deepseek-v4-pro` + 摘要 `deepseek-v4-flash`**，国产便宜模型优先。

依据是 12 个候选模型 × 4 个 case 的实测（`xiaoyu/evals/results/*sweep.json`）：

| 模型 | 相对输入单价 | 4 case | 单个 case 成本 |
|---|---|---|---|
| deepseek-v4-flash | 1× | 4/4 | $0.0026 |
| qwen3.7-plus | 2× | 4/4 | $0.0052 |
| **deepseek-v4-pro** | **3×** | **4/4** | **$0.0061** |
| glm-5.2 | 8× | 4/4 | $0.011 |
| gpt-5.6-luna | 7× | 4/4 | $0.016 |
| qwen3.7-max | 12× | 4/4 | $0.030 |
| kimi-k3 | 20× | 4/4 | $0.034 |
| gpt-5.6-terra | 19× | 3/4 | $0.037 |
| bedrock-claude-sonnet-5 | 20× | 4/4 | $0.062 |
| gpt-5.6-sol | 37× | 4/4 | $0.099 |
| bedrock-claude-opus-5 | 37× | 4/4 | $0.124 |
| bedrock-claude-fable-5 | 68× | 4/4 | $0.147 |

同一个任务，**最贵的比最便宜的高 57 倍**，而通过率没差别 —— 所以默认取便宜的。

⚠️ **但这张表不能用来证明"便宜模型够用"**：12 个模型几乎全部满分，说明
**当前 eval 集没有区分度**，4 个 case 都是单文件小改或机械重命名，任何能正常调工具的模型都做得到。
用"全都满分"的 eval 选模型等于抛硬币。真要有依据，得补能让模型露馅的 case：
需要迭代调试的、大文件多处精确编辑的、指令自相矛盾需要顶回来的、长上下文触发压缩的、
以及"不该动的别动"。这件事按当前模型能力性价比不高，暂时搁置。

**硬活手动升级**，别指望默认模型包打天下：

```bash
xiaoyu --model bedrock-claude-opus-5     # 启动时指定
# 或 REPL 里随时切：/model bedrock-claude-opus-5
```

关于 token usage：**12 个候选全都回传 usage**（含 `gpt-5.6-*`），所以压缩的 token 校准
在所有模型上都有效。注意这跟"Mantle / Responses API 不回 usage"的经验相反 ——
经 `/v1/chat/completions` + `stream_options.include_usage` 这条路是回的。

## explore 与实测数据

`explore` 把检索委托给便宜模型的只读子 agent（默认 `deepseek-v4-flash`），
它只有 `read_file` / `grep` / `list_files`——**绝不给 bash**，否则「只读」是空话
（有测试实测跑完这三个工具后整个工作区字节不变）。

在一个「4 层间接跳转 + 每层都有诱饵常量」的多跳追踪任务上量了五组：

| 组 | 配置 | 主模型 in tok | 总成本 | 是否用了 explore |
|---|---|---|---|---|
| A | 关闭 explore | 24165 | $0.01168 | — |
| B | 开启，弱引导 | 25398 (+5%) | $0.01238 (+6%) | 没用 |
| C | 开启，**强制**使用 | **11925 (-51%)** | **$0.00957 (-18%)** | 用了 |
| D | 强引导，自主 | 29097 (+20%) | $0.01640 (+40%) | 用了，但又重读了 8 个文件 |
| E | 修好证据行 + offset | 25804 (+7%) | $0.01237 (+6%) | 没用 |

五组数据给出的结论，每一条都反直觉：

1. **用了确实有效**（C）：主模型上下文砍一半、总成本降 18%。主 agent 从 9 次工具调用降到 3 次。
   主模型越贵收益越大——flash 是 1×、`deepseek-v4-pro` 是 3×，换成 opus-5（37×）差距会拉到十几倍。
2. **靠 prompt 引导的采用率只有 1/3**（B、D、E 三次里只有 D 主动用了）。措辞劝不动模型。
3. **挂上不用也要付钱**（B）：工具 schema 每轮随请求发送，光是存在就 +5%。工具不能无限加。
4. **D 组暴露的是真 bug**：模型给 `read_file` 传了 `offset` 参数（主流 harness 的标准签名），
   我们没实现 → 8 次读有 4 次报废。**只有走「explore 之后再重读」这条路径才会触发**，前三组碰不到。
5. **模型重读是合理的**：D 组它自己说「链已经清晰了，但让我验证 FORWARD_TO 确实被使用而非 FALLBACK」。
   当时 explore 只返回路径行号、没有原文，而任务里警告了有诱饵——**不信是对的**。
   所以现在要求子 agent 必须给出**原文证据行**，并说明排除了哪些干扰项。

因为第 2 条，采用率改成 **harness 层面强制**而不是继续改措辞：
连续 3 次 `read_file` 追加提示，连续 5 次直接拦截并要求改用 `explore`；
**用任何其它工具即重置计数**——这样「读那几个马上要改的文件」不会被误伤。
没挂 `explore` 时该机制完全不触发（劝它用一个不存在的工具是荒谬的）。

> 方法论上最值钱的一条：**「没被调用」不等于「没有用」**。A、B 两组里 explore 一次没被调用，
> 当时差点直接删掉；是 C 组「强制用一次」才量出 -51%。**功能没被采用**和**功能没有价值**
> 是两个独立问题，必须分开验证。

## ⚠️ 安全

`bash` 工具会在你机器上执行模型给出的任意命令。默认每条都要你确认，这是主要防线；
`deny` 权限规则和危险命令硬拦截在 `--yolo` 下仍然生效，但覆盖面有限。
`--yolo` 会关掉逐条确认——只在一次性、可丢弃的目录里用。

**已经踩过一次**：eval 是无人值守 + `--yolo` 跑的，某个模型跑 pytest 失败后执行了
`pip install pytest`，装进了系统 Python 的 site-packages（那个目录 admin 组可写、免 sudo）。
现在 eval 会注入 `PIP_REQUIRE_VIRTUALENV=true` 挡住这条路，但要清楚：
**这只堵了一个具体出口，不是沙箱**。`read_file` / `str_replace` / `write_file` 有工作区边界检查，
`bash` 没有——它仍能写你有权限的任何地方。真隔离要靠容器或 `sandbox-exec`。

## 结构

```
xiaoyu/                     仓库根
├── pyproject.toml          注册 xiaoyu / xy / xiaoyu-eval 三个命令；依赖精确锁版本
├── .env                    运行配置（含 key，已 gitignore）
├── .github/workflows/      CI（三平台矩阵 + 打包验证）与 release（tag → PyPI 自动发布）
├── .githooks/pre-push      本地兜底：push 前跑全部测试
├── experiments/            可复现实验脚本（README 里的数字出处）
├── tests/                  单元测试 255 个，全部不打网络
│   ├── test_tools.py             工具层、编辑护栏、连续读拦截、硬拦截、平台分派
│   ├── test_context.py           token 估算、压缩、断路器、消息序列合法性
│   ├── test_microcompact.py      microcompact 分层回收、压缩摘要 prompt 结构
│   ├── test_permissions.py       权限规则解析、判定管线、会话授权、Agent 集成
│   ├── test_plugins.py           entry_points 插件加载、fail-closed 默认、工具顺序稳定
│   ├── test_readonly_tools.py    grep / list_files、explore 只读边界
│   ├── test_agent_paths.py       主循环、中断恢复、配对补齐、项目指令、打转检测（假 client）
│   ├── test_errors.py            错误分类器、Retry-After/jitter、重试/压缩恢复路径
│   ├── test_skills.py            SKILL.md 解析、扫描、渐进披露、索引预算、check_fn
│   ├── test_user_config.py       xiaoyu config、用户级 .env、平台路径
│   ├── test_session_log.py       会话落盘
│   ├── test_banner.py            启动横幅
│   ├── test_models.py            候选模型、成本计算、横向对比排序
│   └── test_eval_assertions.py   eval 断言的双向自证
└── xiaoyu/                 包
    ├── config.py           运行配置 + .env 解析链 + key 读取（永不回显）
    ├── tools.py            工具注册表、基础工具、护栏、硬拦截、插件加载、平台分派
    ├── permissions.py      权限规则：allow/deny、bash 前缀、路径 glob、会话授权
    ├── agent.py            主循环：流式、tool_calls 累加、审批、分层回收、恢复、循环护栏
    ├── errors.py           API 错误分类器（限流/瞬时/超限/鉴权）+ Retry-After 解析
    ├── explore.py          explore 子 agent（便宜模型 + 只读工具）
    ├── skills.py           SKILL.md 技能：扫描、frontmatter、渐进披露
    ├── compaction.py       上下文压缩：切点、摘要、回退保护、断路器
    ├── session_log.py      会话落盘（JSONL）
    ├── tokens.py           本地 token 估算 + 用真实 usage 校准
    ├── cli.py              REPL、斜杠命令、config 子命令、确认交互
    ├── banner.py           启动横幅（窄终端降级）
    ├── ui.py               ANSI 输出、Windows VT/UTF-8 适配
    └── evals/              eval 集（放包内，避免占用 `evals` 这个通用顶层名）
        ├── harness.py      Case/Context + 断言原语
        ├── cases.py        具体任务
        ├── models.py       12 个候选模型 + 成本计算
        ├── prices.json     单价（随包分发；改价需手工更新）
        ├── runner.py       执行器（支持 --sweep 横向扫模型）
        └── results/        历史跑分归档（仅在仓库；新结果写到当前目录 xiaoyu-eval-results/）
```

外层 `xiaoyu/` 是仓库、内层是包，这是 Python 的标准形态（同 `requests/requests`），不是冗余。

## 路线（按价值排，不按容易排）

1. ~~**`str_replace` 编辑工具**~~ — v0.2（严格匹配 + 唯一性校验 + 先读再改 + 失败带提示回错）
2. ~~**eval 集**~~ — v0.2 建成，但**当前没有区分度**（12 个模型几乎全满分），
   要有依据得补「需要迭代调试 / 大文件多处精确编辑 / 指令自相矛盾 / 长上下文 / 该克制不动」这类硬 case。
   按当前模型能力性价比不高，**已搁置**。
3. ~~**上下文压缩**~~ — v0.3（本地估算 + usage 校准 + 安全切点 + 摘要不累加 + 变大则回退）
4. ~~**模型路由**~~ — v0.3/v0.4（摘要与 explore 走便宜模型；主模型默认换成国产便宜模型）
5. ~~**`explore` 子 agent**~~ — v0.4（便宜模型 + 只读工具 + harness 层面强制采用）
6. **接内部工具** — 飞书、EDW、Amazon 运营那套。**这才是自建 harness 相对 Codex 的真实价值**，
   下一个大动作。载体已就绪：v0.8 起支持 SKILL.md（与 `~/.agents/skills/` 规范库直接互通）；
   v0.10 起有代码层通道——entry point 组 `xiaoyu.tools`，内部工具包 `pip install` 即挂载。
7. 真沙箱隔离（容器 / `sandbox-exec`）——v0.7 先落了危险命令硬拦截兜底，
   但 `--yolo` 下仍无真正边界，沙箱才是完整答案。
8. ~~CI / 发布流水线~~ — v0.9.1 全部就位：GitHub Actions 三平台 × 两 Python 版本测试 +
   打包验证；推 `vX.Y.Z` tag 即经 PyPI Trusted Publishing（OIDC，无长期 token）自动发布，
   含 tag 与 `__version__` 一致性检查；本地 `pre-push` hook 兜底。
   发版流程 = 改 `__init__.py` 版本号 → commit → 推 tag，其余全自动。
9. `/resume` 恢复会话（会话落盘已就绪）、`prices.json` 自动同步、TUI 精致化。
