hey 0.1.0

Minimal terminal AI coding agent: kernel loop + MCP/Skills self-evolution
Documentation

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.

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 (offnone), 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 remote MCP (resolve-library-idquery-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

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.

--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:

[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

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):

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

[provider.local]
base_url = "http://localhost:11434/v1"
models = ["qwen3:14b"]
hey -m local/qwen3:14b "hello"                 # or: --base-url http://localhost:11434/v1 -m qwen3:14b

Example: kilo gateway (hy3 reasoning model)

kilo 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:

[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
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.

[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
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

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).