# hey
A minimal terminal AI coding agent in Rust. A Swiss-army knife for the terminal: the core is just an agent loop plus three traits (`Provider` / `Tool` / `Output`). Everything else — skills, MCP tools, sessions, context pruning — is plain files and protocols, no language-level plugins.
```bash
cargo build --release # zero system deps (rustls); single binary
cargo test # all-green is the acceptance bar for each milestone
cargo clippy # zero warnings
```
## Features
- **Protocol-agnostic core**: a unified IR (`src/llm/ir.rs`) plus adapters — OpenAI-compatible SSE (default), Anthropic (M2), Google Gemini (M5); add a protocol as a config-declared adapter without touching the kernel.
- **Multi-provider**: `[provider.*]` sections, CLI/env/config/default resolution, `provider/model` prefix on `--model`, no-auth requests when `api_key` is omitted (Ollama/vLLM-friendly).
- **Debugging**: `--effort <off|minimal|low|medium|high|xhigh|max>` — unified reasoning depth (pi / Claude Code compatible): OpenAI sends `reasoning_effort` (`off` → `none`), Anthropic uses adaptive thinking + `output_config.effort` (legacy endpoints: `budget_tokens`), Google maps `thinkingLevel` (gemini-3) or `thinkingBudget` (2.5). `/effort` + `/thinking` REPL commands switch it at runtime. HTTP/HTTPS/SOCKS5 proxy with `NO_PROXY` support. Verified with local `gost` SOCKS5 gateway.
- **Thinking round-trip**: reasoning (`reasoning_content` on DeepSeek-style endpoints, Claude thinking blocks, Gemini thought parts) is accumulated and **passed back verbatim on the next turn** — required by DeepSeek thinking mode (400 otherwise).
- **5 built-in tools** (`bash` / `read_file` / `write_file` / `edit` / `grep`): bash output is heuristically filtered to save tokens (`--raw` escapes), but `git status/diff/log/show` output is never filtered — git context always arrives complete. Plus a `compress` tool for context summarization.
- **MCP clients** (`[mcp.*]` stdio / HTTP servers): tools are imported into the registry as `<server>_<tool>` (dots sanitized to `_` per OpenAI tool-name rules), startup failures only warn. Real-world verified: [Context7](https://context7.com) remote MCP (`resolve-library-id` → `query-docs`), and `@modelcontextprotocol/server-filesystem` (read/write/list/edit).
- **Skills** (`.hey/skills/` + `~/.config/hey/skills/`): frontmatter-validated markdown instructions, progressive disclosure in the system prompt, and a built-in guide so the agent can teach itself new skills.
- **Sessions**: append-only JSONL under `~/.local/state/hey/sessions/`, resume with `-c` / `--resume <id>`; oversized resumes are rebuilt as a lossless summary plus a recent window, and the summary is **persisted to the session file** so repeated resumes reuse it without re-running the LLM. All requests share connect timeouts; reasoning models stay safe by default.
- **Token frugality**: layered retry (request + turn), DCP-style context pruning (dedup / error cleanup / compress / nudge), dynamic system prompt with AGENTS.md injection (warns at >50K chars, no truncation).
- **Trust**: `.hey/` resources are only loaded after an interactive confirmation (persisted to `~/.config/hey/trust.json`); `--approve` opts in explicitly.
## Install
```bash
cargo install --path . # from this repo
# or: cargo install hey --git https://github.com/<you>/hey
```
Zero system dependencies (rustls); works on Linux/macOS (bash tool needs a POSIX shell). On Windows: the bash tool auto-detects `sh` in PATH (Git Bash / WSL) and falls back to PowerShell; you can pin a shell with `[tools] shell`.
## Configuration
A complete annotated example covering every section (providers / MCP / proxy / agent / retry / compaction / permission / tools) lives in [docs/config.example.toml](docs/config.example.toml).
`--config` > `.hey/config.toml` > `~/.config/hey/config.toml`; env overrides: `HEY_PROVIDER` / `HEY_MODEL` (selection only), `HTTP(S)_PROXY` / `ALL_PROXY` / `NO_PROXY` for the proxy, and `RUST_LOG` for log verbosity (default `warn`; e.g. `RUST_LOG=info` or `RUST_LOG=hey::mcp=debug` to debug MCP). `${VAR}` references in config values are expanded at load. Model selection is standard: `-m provider/model` (like codex/opencode), or zero-config via `--base-url`. Example:
```toml
[default]
provider = "openai"
model = "gpt-4o"
[provider.openai]
base_url = "https://api.openai.com/v1"
api_key = "${OPENAI_API_KEY}" # omit for no-auth (Ollama/vLLM/gateway)
effort = "high" # default reasoning depth for this provider
[provider.anthropic]
protocol = "anthropic"
base_url = "https://api.anthropic.com/v1"
api_key = "${ANTHROPIC_API_KEY}"
models = ["claude-sonnet-4-5"]
effort = "high" # adaptive thinking + output_config.effort (new API)
# max_tokens = 16000 # optional; default = auto-sized for the effort level
# legacy_thinking = true # old API/compat gateways: maps effort to budget_tokens
[provider.gemini]
protocol = "google"
base_url = "https://generativelanguage.googleapis.com/v1beta"
api_key = "${GEMINI_API_KEY}"
models = ["gemini-2.5-flash"]
effort = "high" # maps to thinkingBudget (Google has no effort concept)
[provider.local] # no auth
base_url = "http://localhost:11434/v1"
models = ["qwen3:14b"]
[proxy]
url = "http://127.0.0.1:7890" # default: env HTTP(S)_PROXY / ALL_PROXY
connect_timeout_secs = 30 # connect timeout (safe default; no normal connection exceeds this)
read_timeout_secs = 0 # stream read timeout; 0 = disabled (default — reasoning models can be silent)
[agent]
max_turns = 20
budget_tokens = 40000
parallel_tools = true # run same-turn tool calls in parallel (default true; false = serial)
[retry]
enabled = true
max_retries = 5
base_delay_ms = 2000
max_delay_ms = 60000
jitter = true
respect_retry_after = true
[compaction]
dedup = true
purge_errors_after = 4
nudge_threshold = 0.7
compress_tool = true
[permission]
allow = ["bash: cargo *"] # default: allow all (pi-style); set rules to restrict; unmatched → blocked
[tools]
# 可选:外部过滤命令(bash 工具输出过 stdin 给命令,取 stdout 为过滤结果;
# 失败/为空回退内置启发式;raw=true 时绕过)。缺省 = 内置过滤。
# bash_filter = "grep -E 'error|warning|FAILED'"
# shell = "powershell" # Windows 下默认找 sh(Git Bash/WSL)否则回退 PowerShell;此配置可显式钉住
# timeout_secs = 120 # bash 默认超时秒数;0 = 禁用(无限等待);模型参数 timeout_secs 可覆盖
# Remote MCP (HTTP/SSE): no local subprocess, direct HTTP connection
# Real-world verified with Context7 (resolve-library-id → query-docs):
# [mcp.context7]
# url = "https://mcp.context7.com/mcp"
# respawn = false
# MCP filesystem (stdio): 12 tools injected (read_text_file / write_file / list_directory / search_files / etc.)
[mcp.fs]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allow"]
# respawn = true # crash the subprocess restarts automatically (default false:
# # transport failure is reported as-is; recover with REPL /reload)
```
## Usage
```text
hey # REPL (cross-turn session memory)
hey "explain this repo" # one-shot
hey -p "summarize README.md" < notes.txt # print mode + stdin pipe
hey --output json "run the tests" # JSONL event stream for scripts/CI
hey -m zen/deepseek-v4-flash "..." # standard provider/model selection
hey -m kilo/tencent/hy3:free "async Rust" # kilo gateway (free hy3 reasoning model, OpenRouter-compatible)
hey --base-url http://localhost:11434/v1 -m qwen3:14b "..." # zero-config ad-hoc provider
hey --base-url https://api.x.com/v1 -m gpt-4o --api-key "$KEY" "..." # ad-hoc + protocol openai
hey -t high "..." # reasoning depth (--effort: off|minimal|low|medium|high|xhigh|max)
hey @src/main.rs "review this file" # @file expansion
hey -c # continue latest session
hey --resume <id> "..." # resume a specific session
hey --sessions-dir /tmp/sess -p "..." # session store directory (default ~/.local/state/hey/sessions)
hey --approve # trust this project's .hey/ resources (headless)
hey --no-mcp / --no-skills # disable extensions
```
`--output json` emits one JSON object per line (events from the agent loop):
| `type` | fields | emitted |
| --- | --- | --- |
| `text_delta` | `delta` | streamed assistant text |
| `thinking_delta` | `delta` | streamed reasoning (if any) |
| `tool_call` | `name`, `args` | tool invocation (args is a JSON string) |
| `tool_result` | `name`, `ok`, `output` | tool result |
| `usage` | `input`, `output` | token counts (per turn) |
| `finished` | `reason`, `turns` | turn completion |
| `error` | `message` | fatal error (exit code 1) |
Every event is flushed immediately (safe for pipes; survives Ctrl-C).
REPL commands: `/help`, `/model [provider]/name` (switch live), `/effort <level>` (switch reasoning depth live), `/thinking` (toggle thinking display), `/usage`, `/compact`, `/reload`, `/resume <id>|-` (switch session), `/sessions`, `/sessions rm <id>`, `/sessions prune <keep>`, `/skill:<name>`. Ctrl-C cancels the current turn; Ctrl-C again exits.
### Startup output
On REPL launch, hey prints a one-line summary of what it loaded (provider, skills, MCP servers):
```text
hey 0.1.0
provider: kilo/tencent/hy3:free
skills: 2 (review-pr, fix-issue)
mcp: 2/2 connected: context7, filesystem (16 tools)
```
This is **on by default** (pi-style: you see what's loaded). To suppress it, set
`quiet_startup = true` in your config, or pass `--quiet` on the command line.
`--verbose` forces the summary even when `quiet_startup = true`. Errors (e.g.
`[hey] mcp 'context7' skipped: ...`) are always shown regardless of this setting.
### Example config for local (no-auth) use
```toml
[provider.local]
base_url = "http://localhost:11434/v1"
models = ["qwen3:14b"]
```
```text
hey -m local/qwen3:14b "hello" # or: --base-url http://localhost:11434/v1 -m qwen3:14b
```
### Example: kilo gateway (hy3 reasoning model)
[kilo](https://kilo.ai) provides free reasoning models via an OpenAI-compatible endpoint.
The API key is managed in `~/.bashrc` (`export KILO_API_KEY='...'`), not hardcoded in the config:
```toml
[provider.kilo]
protocol = "openai"
base_url = "https://api.kilo.ai/api/openrouter"
api_key = "${KILO_API_KEY}"
models = ["tencent/hy3:free"]
effort = "low"
context_window = 262144
```
```text
hey -m kilo/tencent/hy3:free "explain async Rust"
```
### Example: SOCKS5 proxy + filesystem MCP (real-world combo)
Proxies the LLM traffic through a local SOCKS5 tunnel while keeping a local stdio MCP server (filesystem) running normally — MCP traffic stays on the loopback, only LLM traffic goes through the proxy. Verified with the script in `scripts/smoke-real.sh`.
```toml
[proxy]
url = "socks5://127.0.0.1:1080" # any SOCKS5/HTTP proxy; also picked up from ALL_PROXY env
[mcp.fs]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
respawn = true
```
```text
hey -m kilo/tencent/hy3:free "@src/main.rs review the Ctrl-C cleanup"
# LLM traffic → SOCKS5 → upstream API
# MCP filesystem tools → direct loopback (unaffected by proxy)
```
## Extending hey
- **Skills**: drop a `SKILL.md` with `name` + `description` frontmatter into `.hey/skills/<name>/` (or `~/.config/hey/skills/<name>/`). The agent itself can create new skills with `write_file`, then `/reload` to activate them in the next turn — file-level self-evolution.
- **MCP**: any stdio or HTTP MCP server becomes tools automatically. Name conflicts: later registration wins (MCP overrides built-ins).
## Architecture
```text
src/
├── llm/ IR + Provider trait + adapters (openai / anthropic / google / retry)
├── config.rs TOML config + env expansion + provider selection + [mcp.*]
├── proxy.rs proxy resolution (CLI > config > env, socks5, NO_PROXY)
├── agent.rs agent loop: request → stream → tools → backfill (re-try / nudge /
│ session persist / DCP dedup), plus:
│ ├── agent/prompt.rs system prompt (AGENTS.md + skills injection)
│ ├── agent/context.rs token estimate / pair compaction / thinking prune
│ ├── agent/execution.rs ToolRunner: precheck → parallel/serial run → ordered backfill
│ └── agent/compress.rs compress tool (lossless summary; resume rebuild reuses it)
├── tools.rs Tool trait + bash output filter + 5 built-in tools
├── skills.rs frontmatter validation + project/global scan + progressive disclosure
├── sessions.rs JSONL sessions (~/.local/state/hey/sessions/)
├── mcp.rs MCP stdio client (JSON-RPC 2.0)
├── trust.rs project trust gate (~/.config/hey/trust.json)
├── ui.rs terminal / JSON / test output
├── prompt.rs @file expansion + CLI prompt assembly (stdin pipe + args)
├── completer.rs REPL tab completion (/help /quit /model /etc)
├── ctrl_c.rs Ctrl-C handler + termios save/restore
├── lib.rs library entry (kernel API for embedders)
└── main.rs CLI + REPL (/usage /compact /model /reload /skill:<name>)
```
Design: `docs/DESIGN.md` (Chinese). Comparison with pi/other agents: `docs/COMPARISON.md`, `docs/VS_PI.md`.
## Testing
Unit tests (pure functions: config/retry/filter/compaction/skills/sessions/trust) + integration tests (self-written MockServer, FakeProvider, FlakyProvider, fake MCP server) — no real network involved.
## Development
Three quality gates are enforced locally and in CI (`cargo test` / `cargo clippy --all-targets -D warnings` / `cargo fmt -- --check`); a commit must pass all three plus a release build. MSRV: **1.88** (edition 2024 + let-chains). CI (GitHub Actions) additionally runs a RustSec dependency audit. Formatting follows the default `cargo fmt` style — no custom rustfmt.toml. Optional: `scripts/smoke-real.sh` runs all three protocol adapters against a real multi-protocol gateway (key read from `~/.pi/agent/auth.json`, not committed; requires a `cargo build --release` first).
## Benchmark
Real-gateway coding benchmark (systechn-pro / Qwen3.6-35B, on this repo, judged by `cargo test` + `cargo clippy` in an isolated git worktree):
| task | result | evidence |
| --- | --- | --- |
| REPL history persistence | solved | 161 tests green, incl. new roundtrip + 0600-permission test |
| compact_messages boundary tests | solved | 162 tests green, 2 new tests (also corrected the spec's keep_tail assumption) |
| session-append error escalation | solved (3rd try) | 161 tests green; first error!/then warn! via `Mutex<Option<HashSet>>`, new test, no new deps — unlocked by the tooling fixes below |
| .hey/task.md checkpoint guidance | solved | `finished: complete`, new system-prompt test |
| parse_args debug logging | solved (2nd try) | 163 tests green; added debug! logs (tool+args_len, no secrets) + 3 tests, fixed the null-handling test to match existing behavior |
**solved 5/5.** The two initially-failed tasks were unlocked by real-world tooling fixes (see commit log): `edit` failure now reports the old_text head + a re-read instruction, and the system prompt gained edit discipline (no blind retries, small steps, `git diff` self-check). The final T3 run implemented + self-verified (release tests green) in one pass instead of an edit/fix loop. Run with `scripts/bench-real.sh` (needs the local systechn gateway + `cargo build --release`); per-task artifacts land in `target/bench/` (git-ignored).