Designing the Agent

A bounded tool loop, a deliberately small tool set, and mandatory citations — the agent as a use case, the model as an adapter.

The agent is the part of this project I was most likely to over-build. A chatbot can do anything; an engineer needs it to do one thing, repeatably, with a bound on the damage. So the design started from the constraints rather than the capabilities.

A Bounded Loop

The agent is a straightforward tool-calling loop with a hard iteration ceiling (max_iterations) and a hard wall-clock ceiling per run (ingest.run_timeout, ten minutes). If the model stalls, the run ends failed and is recorded as such—it does not hang the UI or leave a source in limbo forever. Loops are also where the model is most likely to wander, so the ceiling is a correctness feature, not a resource guard.

The core drives the loop and calls a port; the OpenAI SDK lives in an adapter. In tests, a scripted fake model drives the same loop. That is the payoff of “the agent is a use case, the model is an adapter”: the interesting logic—which tool, what order, what to do with the output—is tested without a network.

A Small Tool Set

The model reaches the world only through named tools, and the list is short on purpose:

  • Read: read_index, list_topics, list_branches, search_atoms, search_notes, list_sources, get_tree, read_source_node, read_atom, read_note, walk_links, list_tags.
  • Write: write_atom, update_atom, write_note, update_note, link_atoms.

A tight set has two benefits: the model spends its budget reasoning rather than exploring, and every action it can take is one I have named and can audit. There is no generic “run SQL” tool, no filesystem access, no HTTP fetch during a query.

Three Operations, One Loop

The same loop runs three operations with different tool subsets:

  • Ingest — atomize and compile. Here the agent may also read_source to pull extracted text; the tool is not offered during queries, so answers come from compiled knowledge instead of re-reading raw documents.
  • Query — answer a question over indexes, search, pages, and atoms.
  • Lint — the semantic checks (contradictions, supersession, gaps), sharing the query tool set.

Citations Are Mandatory

A query answer must cite the pages it used, as wikilinks. Atoms and source nodes are implementation detail and are not cited in prose—they are the machinery behind the page. An answer that cannot be traced is a failed run, by definition.

Filing works the same way in reverse: a good answer can become a kind: query page, explicitly and idempotently (a run is filed once). The page inherits the atoms of the pages it cites—as provenance and as tags—so a synthesis is as findable as the knowledge behind it. And because a synthesis may legitimately stand alone, query pages are exempt from the “cites no source” and “untagged” findings.

Debugging Lives in the Terminal

The whole loop is observable with --log-level debug: every LLM turn (phase, iteration, tool calls, tokens), every tool call with its duration and result digest, and each ingest’s start and finish with atoms, pages, and tokens. When a run produces something surprising, the answer is in the log, not in a debugger.


The agent is designed from its limits: a bounded tool loop, a deliberately small tool set, and mandatory citations. The chapter argues that each constraint improved the output instead of narrowing it—bounded iterations prevent runaway work, a small tool set keeps behaviour explainable, and citations make an answer checkable. An unconstrained agent may be more capable, but only a bounded one can be audited after it fails.

AI Insights: Bound the Agent
Source Code: MPL-2.0 · Content: CC BY-SA 4.0 · Credits & AI Disclosure