Designing the Agent
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_sourceto 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.
