Skip to content

Taking the Reading Skill Further ​

Over fifteen chapters you took a real project from its Cargo.toml to the last byte of a streaming (SSE) response. This final chapter introduces no new syntax. It answers one question: where do you go from here? By the end you should be able to answer:

  • Pick one of the three development paradigms — what are your first and second stops for reading?
  • Lay pi-mono's layer list next to jackdaw: which layers match, and which two slots are empty by design?
  • When AI-generated code runs with your user permissions, what does each of your four guardrails protect against?

Three Reading Maps ​

Backend services. You've already visited the first stop: jackdaw is an axum backend. Next, go to the axum repository's examples directory, pick two or three handlers, and hunt for what this book never showed you — graceful shutdown, connection pools, more elaborate shared state. After that, work through the official tokio tutorial, where the reading habit from chapter 7 ("await is a yield point") lands in complete programs.

Desktop and CLI apps. Read a Tauri app side by side with jackdaw: the frontend sends requests, the Rust core answers — structurally the same conversation as agent and gateway. Mind where the analogy breaks: the two sides talk over in-process calls, not HTTP, and the frontend is usually TypeScript. On the CLI side, you already read clap in chapter 10.

Systems libraries. Read The Embedded Rust Book. No tokio there, no runtime, often no heap allocator at all (no_std); the first few chapters are enough for a first pass. The goal isn't firmware tomorrow — it's testing whether your ownership reading survives without the standard library: the rules for &/&mut haven't moved an inch; what disappears is the infrastructure you lean on.

TIP

New project, same method: read the dependency section of Cargo.toml first to guess responsibilities, then find the entry point. The order you practiced in chapter 8 works on axum's examples too.

pi-mono Side by Side: One Layering, Two Languages ​

pi calls itself the "Pi Agent Harness" — a self-extensible family of coding agent tools, in TypeScript. Its layers: pi-coding-agent (interactive CLI), pi-agent-core (the agent runtime: tool calls and state management), pi-ai (one unified multi-provider LLM API), pi-tui, pi-telemetry, pi-durable, and chord (an application composition runtime). Set that next to jackdaw:

pi's layerjackdaw's counterpart
pi-ai (unified multi-provider API)trait Provider in provider.rs + the implementation in openai.rs
pi-agent-core (agent runtime)None — jackdaw forwards messages; it doesn't run the agent loop
pi-coding-agent (interactive CLI), pi-tuicli.rs, ask.rs (command-line entry points)
None — pi is not a gatewaygateway.rs + router.rs (protocol entry, routing and failover)

Read two things out of this table. First: layering isn't measured in file count, it's measured in "who depends on whom, and where do failures get handled". The gateway's module comment states the boundary plainly:

rust
//! The HTTP gateway: an OpenAI-compatible face in front of the router.
//!
//! Responsibilities here are deliberately narrow — the gateway checks the
//! bearer token, validates the request envelope, then hands everything to
//! [`crate::router`] and translates the outcome into HTTP. All upstream
//! semantics (failover, model rewriting) live in the router, which is why
//! `ask` gets identical behavior for free.
// ...

How to read it: start at the second line. Three verbs, three steps — check the token, check the request envelope, hand everything else to crate::router. What enters this layer is raw bytes; what leaves is an HTTP response; failover and model rewriting are none of its business. And because the semantics live in the router, ask reuses the routing and gets identical behavior — that's exactly what the last sentence says.

Second: the two empty slots are themselves information. pi deliberately doesn't build a gateway; jackdaw deliberately doesn't build an agent. One fact stitches the two together: pi supports custom providers (baseUrl + API key), so you can point it at a gateway and treat that gateway as a provider — in other words, pi can treat your jackdaw as one of its upstreams. In chapter 14 you practiced pointing agents at jackdaw; this is the same road from the other end.

An AI-Native Safety Checklist ​

pi's philosophy is a minimal core, transparency and auditability, and self-extension. One item is worth copying out verbatim: no built-in permission system — everything runs with your user permissions, and if you want isolation, you bring your own container. AI-native development works the same way. AI-written code runs on your machine, with your keys and your filesystem; no sandbox catches it for you. Your guardrails are four habits:

  1. Spec first. Before any code: goals, constraints, acceptance criteria. If you can't write the acceptance criteria, you're not ready to let the AI start (chapter 3).
  2. Small steps. One verifiable change at a time; cargo check is green before you take the next step (chapter 8).
  3. Tests arbitrate. Watch a test fail first, then have the AI change the implementation; only a green cargo test counts (chapter 15).
  4. Read the diff. Ownership moves, concurrency, unsafe, swallowed errors — those four you must understand yourself (chapter 5).

Three Extension Exercises ​

Ordered from easiest to hardest, all starting from jackdaw's actual state. Each notes the new concepts you'll hit and the chapters worth rereading.

One: intent-based rule routing. Add rules to a routing group: if the messages contain an image, route to a multimodal member; otherwise take the default. magpie's rule routing (tokens ≥ N, contains images, reasoning, agent, built-in intent classifier) goes much further — start with the hard rules. New concepts: parsing the request's messages for the first time (so far the body passes through untouched except for model); expressing rules as an enum and covering every case with pattern matching. Reread chapter 6, chapter 11, chapter 13.

Two: translating the Anthropic protocol. Following the shape of the two existing routes in build_app, add POST /v1/messages: accept an Anthropic-shaped request, translate it into OpenAI shape for the router, translate the response back. magpie speaks OpenAI / Anthropic / Gemini and translates between them; this exercise is the minimal slice of that. New concepts: field mapping between two sets of typed structs (serialization you no longer get for free from derive), and rewriting SSE event shapes — do the non-streaming path first; the two streaming event formats have nothing in common. Reread chapters 6, 12, and 14.

Three: usage statistics. Record token consumption per model, aggregate per group, and add a stats subcommand to print it. Compare with magpie: its routing strategies include usage; build the accounting first — only then does choosing that strategy become possible. The hard part is sharing a counter across requests, and the answer is already in the code:

rust
/// Owns the config view, the provider instances and the rotate counter.
pub struct Router {
    config: Arc<Config>,
    providers: HashMap<String, Arc<dyn Provider>>,
    /// Monotonic counter behind `rotate`: `fetch_add` is atomic, so
    /// concurrent requests always advance to different starts.
    rotate_counter: AtomicUsize,
}

How to read it: start at the last field, rotate_counter. The whole Router sits inside an Arc in AppState (chapter 14), all concurrent requests share one instance, and complete takes only &self — no &mut in sight. So "accumulate across requests" has exactly two roads: atomic operations (rotate_counter's fetch_add is one) or a mutex. Usage statistics is the same pattern plus a per-model bucket; Send/Sync, which chapter 7 waved past in one sentence, will bite you for real here. Reread chapters 7, 10, and 13.

Summary ​

First stops on the three maps: axum examples, Tauri, the embedded book. In the pi-versus-jackdaw table, the two empty slots matter as much as the four matches; the four guardrails — spec, small steps, tests, reading diffs — rest on one fact: AI's code runs as you. The later the exercise, the more concurrency it demands of you. The book ends here; the reading skill is yours now.

Self-Check ​

  1. You want to add rate limiting ("at most N requests per minute") to jackdaw. Does it belong in the gateway or the router? Why? Which existing piece of code is the closest precedent?
  2. The AI declares the usage counter as a plain usize field on Router and does += 1 inside complete. Will the compiler stop it? At which stage?
  3. Why does "pi has no built-in permission system" make reading diffs more important, not less?
Answers
  1. The gateway. Rate limiting, like bearer auth, is a cross-cutting concern that must reject before any upstream is touched; at the edge, every routing path shares one gate. The precedent is the authorized check, which chat_completions runs first thing. The router's candidate loop is the wrong home for request-level rejection.
  2. It will. complete(&self, ...) hands you an immutable borrow; += 1 assigns to a field behind a & reference, and the borrow checker rejects that at compile time — E0594, "cannot assign to ..., which is behind a & reference". That is exactly why rotate_counter is an AtomicUsize.
  3. No built-in permission system means every piece of code that gets in runs as your user; isolation is something you bring yourself (a container, say). If the defense isn't in the tool, it has to live in the code that enters your repository — and AI-generated diffs are the main entrance for code that runs with your permissions. Reading diffs is the only safety boundary that exists by default.