rab Coding Agent
1. Why
I wanted to understand coding agents from the inside out - the agentic loop, tool invocation dispatch, and how an LLM-driven system makes decisions and acts on them. The main inspiration was pi, a coding agent I interact with daily. I wanted to build something similar to grasp the internals.
2. Goal
A TUI-based coding agent in the spirit of pi: capable of connecting to different LLM providers, exposing a set of primitive tools (read, write, edit, bash), and running in the terminal. No IDE dependency, no browser - just a terminal chat interface for agent-assisted development.
3. Name
rab is an archaic Slavic word for slave or servant, commonly found in the phrase Раб Божији (Rab Božiji) - Servant of God. It shares the same origin with a robot, carrying the same notion of a servant who performs work on behalf of another - a fitting name for an agent broker that orchestrates tireless AI agents. Some call coding agents clankers - a term that evokes clumsy, rattling machinery. rab is the opposite: a quiet, devoted servant, faithful rather than noisy.
4. How
I chose Rust for the implementation. The reasons are engineering-pragmatic:
- Safe by default - no segfaults, no use-after-free, the compiler catches entire classes of bugs before they reach runtime.
- Performance and memory footprint - a Rust binary starts fast and idles at single-digit MBs of RSS. This matters for a TUI tool that should feel instant.
- LLM-generated code target - Rust's strict type system and clear error messages produce correct code on fewer attempts than dynamic languages. The compiler acts as a second reviewer.
- Library ecosystem - crates.io covers everything needed: async runtimes, HTTP clients, serialization, terminal manipulation.
Clojure was the obvious alternative, but it didn't make the cut. TUI libraries in Clojure are virtually non-existent. JVM startup times kill the instant-feedback loop a TUI tool needs. Memory footprint is an order of magnitude higher. And most critically: even the best LLMs generate poor Clojure code - hallucinations around macros, wrong interop patterns, nonsensical destructuring. Libraries like clojure-mcp and my own pi-clojure help at the edges but don't fix the core problem. Rust, by contrast, produces correct code on the first or second attempt with remarkable consistency.
For LLM provider abstraction I used the genai crate. It normalises the provider API surface (OpenAI-compatible, Anthropic, Gemini, etc.) behind a single interface, so I could focus on the agentic loop and tool dispatch without writing provider glue code.
For the terminal UI, Ratatui seemed like the obvious choice. It is the de-facto TUI framework in Rust. I started building an app-style interface with it.
5. What went well
The core tool set came together quickly:
- bash - shell command execution with streaming stdout/stderr and exit code capture.
- read - file reading with line limiting and offset support.
- write - atomic file creation/overwrite.
- edit - exact-text replacement with uniqueness validation against the target file.
- telegramattach - file attachment for Telegram bridge.
genai connected to DeepSeek (and later other providers) with minimal configuration. The basic agentic loop - prompt → LLM → parse tool calls → dispatch → observe → loop - was functional within days.
Dogfooding started immediately. rab was writing parts of itself from the second week. The feedback loop of "find a missing feature, implement it via rab, use it the same session" was as productive as expected. Rust's fast compile times (sub-second for incremental builds) kept the iteration rate high.
6. What I didn't like - Ratatui
Ratatui is a retained-mode immediate-mode hybrid that forces an application-centric architecture. You own an event loop, manage a terminal-wide widget tree, and redraw on every tick. What I wanted was a chat-like session interface - streaming text, command prompts, structured output blocks - without buying into a full application framework.
After fighting Ratatui's layout and rendering model for a while, I made a pragmatic decision: rip it out and port pi-tui - a minimal TUI framework that gives you a readline-style input buffer and a scrollback pane. Nothing more. No widgets, no layout, no app loop.
It worked, to some extent. DeepSeek wrote the core in a single session. I started polishing the agent loop and adding features on top of a stable TUI foundation.
7. Cracks in the foundation
Having too many moving parts evolve in parallel - the TUI layer, the agentic loop, provider-specific quirks, tool dispatch ordering, cost tracking - made the whole experience less pleasant than it should have been. Every new feature required touching three subsystems, and regressions were common.
I started looking for a stable base library I could build on instead of maintaining everything myself. dirge by yogthos is another Rust coding agent with some unique ideas, but it's opinionated and I needed something simpler to build upon. The search led to yoagent - a Rust port of pi's agent loop and provider system. It gave me:
- A battle-tested agentic loop (same architecture as pi).
- A pluggable provider abstraction (already handling OpenAI, Anthropic, Gemini, DeepSeek, and OpenAI-compatible endpoints).
- Tool registration and dispatch.
- Conversation state management.
- Streaming and token accounting.
After adopting yoagent, removing genai and the hand-written loop, and adapting my existing tools to yoagent's Tool trait, the whole system tightened up considerably. Less code, fewer bugs, clearer separation of concerns. Development became enjoyable again.
8. Polish, or the lack thereof
rab is functional. You can clone it, run it, connect it to DeepSeek or OpenAI, and it will edit files and run commands. The issue is polish.
- Bugs surface every few sessions. They're easy to fix - a missing error branch here, a race condition on early Ctrl+C there - but they keep coming.
- I never tested most LLM providers beyond DeepSeek and OpenAI. Gemini, Anthropic, local models - all untested.
- rab has no users. No issue tracker. No CI running on real workflows.
So we arrive at the classic 90% problem: 90% of a useful coding agent is done, but the remaining 10% - the edge cases, the provider quirks, the unexpected terminal sizes, the SIGPIPE handling - is invisible until someone else runs it. Without users, there are no bug reports. Without bug reports, there is no polish. Without polish, there is no motivation to continue.
9. Where I ended up
I stopped working on rab and went back to using pi. But I didn't go empty-handed: I ported several ideas from rab into pi extensions. The telegramattach tool, the agentic loop visualisation, the cost-per-turn tracking - these live as pi extensions now, benefiting from pi's stable base and active user community.
10. What I learned
The journey was worth it. In the span of a few months I went from "how does pi even work?" to a deep understanding of:
- Agentic loop architecture - the request/parse/dispatch/observe cycle, and why streaming changes everything.
- Tool calling internals - how LLMs represent tool schemas, how providers differ in function-calling semantics, how to validate calls before dispatch.
- Provider abstraction - the surprising variance between supposedly "OpenAI-compatible" APIs, and why a test suite for provider backends is mandatory.
- The social layer of open-source - a tool with one user (yourself) is a prototype, not a product. The gap between prototype and product is approximately equal to the number of users times their tolerance for rough edges.
rab lives at https://github.com/markokocic/rab. It compiles, it runs, it works. I don't use it daily anymore, but the codebase is a reference for anyone building a similar system.
And who knows - maybe next time I'll start from scratch with all the lessons learned and skip the Ratatui detour entirely.