Lang on Rock
Architecture

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 digest

Tool 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

  1. The manifest is already in the prefix, cached.
  2. The agent picks ids from rows it holds.
  3. One get with every id — a section when only the schema is needed, a find when only a quoted passage is, an offset: pos from 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.

On this page