The agent API
Six verbs, what each had to earn, and why tool count is itself a token cost.
Few verbs, all deterministic.
manifest() -> TSV, usually already in the cached prefix
get(ids[], {section, offset, limit, find}?) -> slices, one round trip for N concepts
snapshot() -> digest, to check whether the manifest is stale
search(query, k) -> ranked rows, each direct hit carrying pos
write(bundle, path, content, replaces?) -> new digest, plus that file's warnings
delete(bundle, path, replaces?) -> new digestTool count is a token cost
The four read definitions measure 1,122 tokens in the client's system prompt, paid on every single turn whether or not any of them is called, and the bill scales with the count: fifteen tools would run several thousand.
That is the whole reason this list is short. Each verb had to earn a place against that standing
cost. manifest and search narrow, get fetches, snapshot invalidates. Nothing else did until
an agent needed somewhere to put what it learned.
write costs 413 and delete 206, taking the standing bill to 1,741 — 55 percent on top of the
read-only figure. write alone ties get as the most expensive definition in the set, and 70 of
its tokens are the single paragraph teaching a model to recover from a refused precondition. That
paragraph is the cheap option: the alternative, a separate tool that reads a concept's hash, would
have cost a whole definition and left write explaining replaces anyway. delete is half the
size because it inherits the lesson by reference instead of restating it.
A deployment that only ever reads pays all 619 for nothing. There is no flag that omits them.
Batching get is what kills latency
The agent reads the manifest, picks three ids, fetches all three in one call. Two round trips total, against four or five for the navigator pattern.
This works only because the manifest carries the link graph. Without the links column the agent
cannot know the third id until it has read the second concept, and the batch collapses back into a
walk.
The bundle column does the same job for filtering. The agent narrows without loading anything
extra.
A read can no longer flood the context
Every get returns slices, not unbounded strings, and the MCP layer caps a read at 15,000
characters per concept by default. A partial slice frames itself — @@ id [start..end of total] —
so the model knows how to continue with offset, and find takes a literal phrase and answers
with a window around the first occurrence plus the offset of every one. The worst case a naive
read can put into a context fell from the size of the document, whatever it is, to a constant
around 3,800 tokens; a located passage runs about 500. The library and the HTTP API stay unbounded
by default, because their callers are programs rather than prompts.
Why the write verb writes Markdown, not a snapshot
An earlier draft listed put(bundle, changes) as the fourth verb, "only if agents write". The
watcher settled that question by making that particular shape incoherent.
The source folder is the truth, and the watcher recompiles from it. An agent that wrote a snapshot
through put would have that snapshot silently replaced by the next filesystem event or the next
rescan, at most 30 seconds later. The write would look like it succeeded and then quietly vanish,
which is worse than not offering it.
So the verb that exists writes the source Markdown, exactly the way a human does through the source API, and then recompiles before answering. One writer, one source of truth, and the watcher has nothing to disagree with. What the draft got wrong was the target, not the capability.
A precondition a model can actually satisfy
Every other client names the version it replaces with an If-Match hash, which is what stops two
editors losing each other's work. A model has no hash and no cheap way to get one: fetching it first
costs a round trip on every write, and a tool that returns it costs a definition in every session.
The refusal carries it instead. A write without replaces against an existing concept is refused
with that concept's current hash appended, so the retry succeeds. Creating stays one call, because
omitting replaces is the assertion that the concept is new; replacing costs two the first time.
The hash lookup runs on any failure rather than on a recognised conflict, because the embedded path
throws its own message and the remote path throws a 412, and matching either string would break the
moment one of them was reworded.
Creating the tenant is part of writing
A tenant with no source directory is created by the first write to it, registered, and watched from then on. The daemon gained a mutable tenant map for this, so the registration takes effect without a restart.
The one case that is refused is a tenant that already has snapshots and no registered directory,
which is what sync on the command line leaves behind. Bootstrapping that would compile an empty
folder over a real manifest.
The MCP server
langonrock mcp <dsn> speaks MCP over stdio on top of Connection, so it works against an
embedded path, a local daemon or a remote server without knowing which.
Descriptions state when to call, not just what the tool does
"Call this before anything else." "Pass every id you need in one call." "Never guess an id."
A description that only describes gets under-triggered. Spending schema tokens on when-to-call buys more than spending them on another tool.
The manifest is exposed twice on purpose
As a tool for every client, and as an MCP resource at okf://manifest for clients that preload
resources into context.
The resource path is the one that actually delivers the design's central claim. A preloaded manifest lands in the cacheable prompt prefix. One arriving mid-conversation as a tool result does not, and pays full price every turn.
What search returns, and what it does not
Manifest rows. Never bodies.
That keeps the two-hop path intact: narrow with search, then fetch only the chosen ids with
get. A search that returned bodies would be a single hop that costs more than the manifest it was
meant to avoid reading. What search does add is a pointer: each direct hit ends in a pos cell,
the offset where the query's words cluster densest in the body, six to sixteen tokens per result.
The second hop becomes get with {offset: pos, limit: 2000} — or find with a literal phrase
when there is one — and the passage arrives instead of the document.
The shape of a good session
- The manifest is already in the prefix, cached.
- The agent picks ids from rows it holds.
- One
getwith every id — asectionwhen only the schema is needed, afindwhen only a quoted passage is, anoffset: posfrom a search hit when the passage was described.
On a large tenant, step 1 becomes search or manifest(bundle) — and the manifest tool's own
description says which, computed from this tenant's measured size. The count of round trips does
not change.