A terminal coding agent you can read end to end — hand-written agent loop, real tools, real permissions.
Quick Start · Features · Architecture · Configuration · Development · Roadmap
Eph-Code is an open source AI coding agent that lives in your terminal. You describe a change in plain language; it reads files, greps the project, edits code, and runs shell commands — asking for permission before it touches your shell.
It is deliberately small and readable. There is no agent framework underneath: the conversation loop is a hand-written while loop over the Vercel AI SDK v6, streaming text and tool calls straight into an Ink TUI. Every layer — provider, session, tool registry, permission, compaction — is a separate module you can open and understand in a single sitting, and each sits behind a narrow enough interface to evolve on its own.
It is a learning-grade but working agent: 7 tools, 3 LLM providers, doom-loop detection, automatic context compaction, and a test suite covering every module. Sessions are in-memory today — see the Roadmap.
The agent loop in action — streaming tool calls, a permission prompt waiting on y/n, and live token usage in the status bar:
- Bun ≥ 1.0 (runtime, package manager, and test runner — no Node.js required)
- An API key for one of: DeepSeek, Anthropic, or Google
git clone https://github.com/ryrenz/eph-code.git
cd Eph-Code
bun installBun auto-loads .env — no dotenv needed.
echo "DEEPSEEK_API_KEY=sk-your-key" > .envbun run devYou should land on the welcome screen above, with Ready in the status bar. Type a request and press Enter:
› read src/index.ts and explain the startup sequence
| Tool | What it does |
|---|---|
read |
Read a file with line numbers, truncated to a safe window |
write |
Create or overwrite a file |
edit |
Replace a snippet in place, with fuzzy matching |
glob |
Find files by pattern |
grep |
Search file contents across the project |
bash |
Run a shell command (permission-gated) |
invalid |
Catch and explain malformed tool calls instead of crashing the loop |
Tools are registered at startup in src/index.ts and described to the model from the markdown files next to their implementation (src/tool/*.md), so prompt and behavior stay in one place.
LLMs rarely reproduce a snippet byte for byte. edit walks nine strategies in order — exact match, line-trimmed, anchor similarity, whitespace-normalized, indentation-stripped, escape-sequence-aware, multi-occurrence, boundary-trimmed, and context-aware block matching — before it gives up. That is the difference between "edit failed, try again" loops and an agent that lands the change on the first attempt.
Read-only and file-writing tools run unattended; bash always asks first.
▲Allow bash {"command":"git status"} [y] / [n]
Rules live in src/permission/permission.ts as an allow / ask / deny table, and the TUI registers the prompt handler at startup. Deny or interrupt, and the tool result comes back as a refusal the model has to handle — the loop never breaks.
One EPH_MODEL string selects both provider and model:
# DeepSeek (default)
EPH_MODEL=deepseek/deepseek-chat bun run dev
# Anthropic
ANTHROPIC_API_KEY=sk-xxx EPH_MODEL=anthropic/claude-sonnet-4-20250514 bun run dev
# Google
GOOGLE_GENERATIVE_AI_API_KEY=xxx EPH_MODEL=google/gemini-2.0-flash bun run devAt 80% of the 120K-token budget the session compacts itself: old tool outputs are pruned first (protecting the most recent 40K tokens), and if that is not enough, the conversation is summarized by the model and the loop continues on the summary. Long refactors don't end in a context-length error.
The processor tracks recent tool calls across messages. When the agent starts calling the same tool with the same arguments in circles, the loop is terminated instead of quietly burning your tokens.
Retryable failures (429, 5xx, ECONNRESET, ETIMEDOUT, fetch failed) are retried with exponential backoff — 2s, 4s, 8s, 16s, capped at 30s — honoring a retry-after header when the provider sends one. Tool output is truncated to 2000 lines / 50KB before it ever reaches the model.
┌──────────────────────────┐
your keystrokes ──▶ │ TUI (Ink + React) │ ◀── streamed events
│ banner · messages · │
│ input · status · perm │
└────────────┬─────────────┘
│ Bus (typed pub/sub, Zod payloads)
▼
┌──────────────────────────┐
│ Session Processor │ while(step < maxSteps)
│ stream → tool → result │ doom-loop guard
└───┬───────────┬──────────┘
│ │
┌────────────▼──┐ ┌────▼───────────┐ ┌───────────────┐
│ Provider │ │ Tool Registry │──▶│ Permission │
│ AI SDK v6 │ │ read/write/... │ │ allow/ask/deny│
└───────────────┘ └────────────────┘ └───────────────┘
│
┌────────────▼──────────────┐
│ Session + Compaction │ prune → summarize
└───────────────────────────┘
src/
├── agent/ agent definitions + system prompts (build, compaction)
├── bus/ typed event bus — the only channel between core and TUI
├── permission/ allow / ask / deny rules and the ask handler
├── provider/ EPH_MODEL parsing → AI SDK language model
├── session/ messages, streaming processor, compaction, retry, prompts
├── tool/ tool implementations + their markdown descriptions
├── tui/ Ink components: banner, messages, input, status, permission
└── util/ id (ULID), token estimation, file logger
The core never imports the TUI. It publishes typed events on the bus (processor.text.delta, processor.tool.start, processor.tool.end, processor.usage), and the TUI subscribes — which is why the same engine could be driven by a different frontend without touching the agent loop.
| Variable | Default | Purpose |
|---|---|---|
EPH_MODEL |
deepseek/deepseek-chat |
<provider>/<model> — deepseek, anthropic, or google |
DEEPSEEK_API_KEY |
— | Required for the default provider |
ANTHROPIC_API_KEY |
— | Required for anthropic/* models |
GOOGLE_GENERATIVE_AI_API_KEY |
— | Required for google/* models |
Runtime logs are written to ~/.eph-code/debug.log — the TUI owns the terminal, so nothing is printed to stdout.
| Key | Action |
|---|---|
Enter |
Send the message |
y / n |
Answer a permission prompt |
Ctrl+C |
Interrupt the running agent loop; press twice while idle to exit |
Ctrl+Q |
Exit immediately |
exit |
Type it to quit |
bun test # run the full test suite
bun test test/tool # run one directory
bun run src/index.ts --helpTests mirror the source tree under test/ — every module (bus, permission, provider, session, compaction, retry, tools, util) has a matching spec.
Two project constraints worth knowing before you send a patch:
- This project targets AI SDK v6, not v5. Tool-call parts use
input(notargs), tool results carrytoolName+isError, and reasoning parts usetext. - TypeScript strict mode is on. Array index access and
process.env.*areT | undefinedand must be handled.
- Session persistence (sessions are in-memory today)
- Additional providers — the
codexandglmbranches are stubbed, awaiting endpoints - More agents beyond
build— the agent registry already supports per-agent prompts, tool sets, and step limits - Configurable permission rules instead of the built-in default table
Contributions are welcome. Open an issue or send a pull request; for anything larger than a bug fix, please open an issue first so we can agree on the direction.
Code comments and commit messages are English-only.
MIT © Renren Zhang
