The agent API
Six verbs, three opt-in database tools, what each had to earn, and why tool count is itself a token cost.
Few verbs, all deterministic.
manifest(bundle?) -> 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?, bundle?) -> ranked rows, each direct hit carrying pos
write(bundle, path, content, replaces?) -> one committed document, the new snapshot, its warnings
delete(bundle, path, replaces) -> one committed removal, the new snapshotWith --database-tools, three more:
transact(changes[], expectedRevision?) -> one revision for up to 1,000 changes
history(before?, limit?) -> committed revisions, newest first
restore(revision, expectedRevision) -> a new revision holding an older stateTool 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 412 and delete 207, 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, 412 tokens
against 413, 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.
The database earned three tools, off by default
The engine underneath gained atomic batches, history and restore. Over MCP they are opt-in:
langonrock mcp <dsn> --database-tools adds transact, history and restore, and so does
createMcpServer(connection, advice, { databaseTools: true }) from the library. Their schemas add
510 tokens to every session, measured without the tenant's advice sentence: 1,718 for the six
default tools, 2,228 for all nine.
Most models never need them. write and delete already commit one document atomically each. The
opt-in is for a model that has to change several documents together, or take a change back, and
the six default tools stay six for everyone else.
| Tool | Input | Output |
|---|---|---|
transact | changes (1 to 1,000), expectedRevision? | The revision, as JSON |
history | before?, limit? (1 to 100) | A page of revisions, JSON |
restore | revision, expectedRevision, both required | The new revision, as JSON |
A change is {operation: "write", bundle, path, content, replaces?} or
{operation: "delete", bundle, path, replaces}, and every hash is the full 64-character digest. "No changes commit if any
precondition fails" is in the tool's own description, because that is the property a model has to
plan around. See transactions and history and
restore for the semantics behind each one.
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 most a naive read can
put into a context fell from the size of the document, whatever it is, to a constant of about
3,760 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 a document
An earlier draft listed put(bundle, changes) as the fourth verb, writing a compiled snapshot,
"only if agents write". What shipped writes the unit people write: one Markdown document,
frontmatter included. The database stores it exactly, the same compiler derives its id, sections
and links, and one commit publishes the document and the recompiled read model together. There is
no second, compiled write path for the two to disagree about.
On a native tenant write is a one-change transaction. It answers once the commit is durable, with
the new snapshot and the compiler's warnings for that one file, and the next manifest, search
or get already sees it. Nothing has to sync afterwards. A tenant that predates the engine and has
not been migrated still takes the old path, editing its source folder and recompiling, and the
answer reads the same either way.
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.
write {bundle: "inbox", path: "idea.md", content: "..."}
→ concept already exists: re-read the concept and retry with its new hash
retry with replaces: "968f33390cab..."
write {bundle: "inbox", path: "idea.md", content: "...", replaces: "968f33390cab..."}
→ wrote inbox/idea.md (hash 4c1e...), snapshot 19e8c4026a96, 1 conceptsCreating 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.
One failure never carries a retry line: an indeterminate commit, where something failed after the
engine began replacing its HEAD, so it cannot say whether the commit landed. The model is told
exactly that and given nothing to retry blindly, because a second write on top of a commit that
did land is the lost update the precondition exists to prevent.
delete needs the hash every time, since deleting has no create case to spend the omission on.
Omitting it gets the hash to retry with, and omitting it against a concept that is not there gets
concept does not exist.
Creating the tenant is part of writing
A tenant with no knowledge yet is created by its first write, and a bundle by the first document written into it. A native tenant needs no source folder and no registration, so persisting a first note takes no setup.
The case that is refused is a tenant that predates the engine and has no registered source folder: its writes still go through its original Markdown, so it needs that mapping or a migration first.
The MCP server
langonrock mcp <dsn> speaks MCP over stdio on top of the same connection the library uses, so it
works against an embedded path, a local daemon or a remote server without knowing which. The MCP
SDK loads only when the server starts, so nothing else in the package pays for it.
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.