Metadata-Version: 2.4
Name: xiaoyu-agent
Version: 0.6.0
Summary: 小羽 — a harness coding agent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: openai<3,>=2.0

# 小羽 · Xiaoyu

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

一个自建的 harness coding agent。

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

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

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

- OpenAI 兼容协议接自建 LiteLLM 网关，流式输出
- **六个工具**：
  - `read_file`（支持 `offset` / `limit` 只读一段）
  - `grep`（正则搜索，自动跳过 `.git` / `node_modules` / `__pycache__` 等噪声）
  - `list_files`（glob 列文件）
  - `str_replace`（精确替换，主要编辑手段）
  - `write_file`（整文件覆盖，只用于新建或全量重写）
  - `bash`
- **`explore` 子 agent**：便宜模型 + 只读工具做检索，返回带 `路径:行号` + 原文行的结论。
  详见下方「explore 与实测数据」
- **编辑护栏**：改已有文件必须先**完整** `read_file`（`bash cat` 不算，只读一段也不算）；
  读完之后文件被外部改动会拒绝写入并要求重读；`old_str` 不唯一或匹配不上会带行号提示打回；
  行中间开始 + 多行替换会被拦（必然破坏缩进）
- **上下文压缩**：本地估算 token + 用真实 usage 校准；超阈值时把早期历史交给便宜模型摘要。
  原始任务永久保留，摘要不层层累加，压缩后反而更大则放弃
- 写文件和执行命令**默认逐个人工确认**，`str_replace` 显示 `-/+` 差异预览
- 交互 REPL（`/help` `/tools` `/model` `/usage` `/context` `/compact` `/clear`）+ 一次性执行模式
- **按模型分开记账**的 token 统计；eval 可横向扫 12 个候选模型并算成本

还没做：接内部工具（飞书 / EDW / Amazon 运营）、TUI、真沙箱隔离。

## 测试

```bash
# 工具层单元测试（不打网络）
.venv/bin/python -m unittest discover -s tests -t .   # 130 个测试

# 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 <本仓库> && 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
.venv/bin/xiaoyu                          # 交互模式
.venv/bin/xiaoyu "把 utils.py 里的类型注解补全"   # 一次性执行
.venv/bin/xiaoyu --model bedrock-claude-opus-5
```

REPL 里：`/help` `/tools` `/model` `/usage` `/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` 工具会在你机器上执行模型给出的任意命令。默认每条都要你确认，这是唯一的防线。
`--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）
├── tests/                  本地测试，130 个，全部不打网络
│   ├── test_tools.py             工具层、编辑护栏、连续读拦截
│   ├── test_context.py           token 估算、压缩、消息序列合法性
│   ├── test_readonly_tools.py    grep / list_files、explore 只读边界
│   ├── test_agent_paths.py       主循环、中断恢复、摘要回退、REPL 命令（假 client）
│   ├── test_models.py            候选模型、成本计算、横向对比排序
│   └── test_eval_assertions.py   eval 断言的双向自证
└── xiaoyu/                 包
    ├── config.py           运行配置 + .env 解析 + key 读取（永不回显）
    ├── tools.py            工具注册表、六个工具、护栏
    ├── agent.py            主循环：流式、tool_calls 累加、审批、压缩触发
    ├── explore.py          explore 子 agent（便宜模型 + 只读工具）
    ├── compaction.py       上下文压缩：切点、摘要、回退保护
    ├── tokens.py           本地 token 估算 + 用真实 usage 校准
    ├── cli.py              REPL、斜杠命令、确认交互
    ├── ui.py               ANSI 输出
    └── evals/              eval 集（放包内，避免占用 `evals` 这个通用顶层名）
        ├── harness.py      Case/Context + 断言原语
        ├── cases.py        具体任务
        ├── models.py       12 个候选模型 + 成本计算
        ├── prices.json     单价（从 litellm_config.yaml 同步，改价需手工更新）
        ├── 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 的真实价值**，下一个大动作。
7. 真沙箱隔离（容器 / `sandbox-exec`）——目前 `bash` 在 `--yolo` 下没有边界。
8. CI（现在测试靠手动跑）、`prices.json` 自动同步、TUI 精致化。
