Revision · Project 4 — askr
Project 4 built askr, an AI CLI that calls the Claude Messages API, streams the answer back word by word, and can retrieve from your own notes first. It’s the capstone of Phase 1, where ownership, errors, traits, async, and HTTP all get used at once. Here’s what to carry forward.
What this part covered
Section titled “What this part covered”- Trait-first design — every network call hides behind a trait (
LlmClient,Embedder), not a concrete type, so tests run with no key and no network, the binary falls back to a mock, and the real client is just one moreimpl. - The Messages API shape — calling Claude means getting the request/response JSON exactly right with
reqwest+serde, and pulling secrets from the environment, never the source. - A hermetic mock — a
MockLlmimplementor makes the whole suite offline and fast, which is the payoff of depending on the trait rather than the HTTP client. - Streaming without corruption — Server-Sent Events arrive in byte-sized pieces that can split a character in half, so you parse the stream deliberately and drive output through a
sinkcallback for low perceived latency. - RAG in principle — before asking, the program chunks your files, embeds them, retrieves the most relevant passages, and stuffs them into the prompt, so the model answers from your text.
- Vector search by hand — turning text into geometry, a hand-written cosine-similarity hot loop, and an in-memory vector store with
top-k, tested with mock embeddings so no real embedder is needed. - Production hardening — read config at the edges and fail loudly, collapse errors to one type with a clean exit, split a library from the binary, add GitHub Actions CI, and profile before optimizing.
The takeaway
Section titled “The takeaway”An AI CLI is mostly untrusted, asynchronous bytes: a stream that can stall, truncate, or split a character. Rust won’t let you pretend those bytes are a clean String, so every await is a point the borrow checker has already proven your data safe to hold and every fallible step returns a Result you must handle. This closes Phase 1 and bridges into Phase 2, where the question turns inward: building systems where nobody has to be trusted.