17 min read

How to Build a Local Agent Bridge: Context and Failures

The HTTP in a local agent bridge is twenty lines. Compiling context and designing failures is where the real work is. Here is how both are built.

The HTTP in my local agent bridge took an afternoon, and the part carrying any real decision was about twenty lines. A loopback listener, a token check on the header, a route or two, nothing that needed much thought once the shape of the problem was clear. The other two problems took the rest of that day: compiling context an agent does not already have into something worth handing it, and deciding what should happen when any part of that fails. The first post in this series covered why the transport is not a choice you get to make: a browser extension cannot spawn a child process, so whatever calls the bridge has to cross a real network boundary, and that boundary picks HTTP before a single line of bridge code exists. This post assumes that argument and starts where it stops.

Context compilation is the practice of reducing a large specification, a voice guide, a set of project notes, into a compact card that is generated once, cached on disk, and reused across requests, recompiled only when a source file actually changes. It differs from sending the full specification on every call because the expensive step, reading and reasoning about the source material, happens exactly once, not on every single request the bridge answers.

A left-to-right flow of five stages in a single request. Client sends token. Bridge authenticates. Bridge loads or compiles the context card. Agent runs headless with the card as its system prompt. Response normalised and returned. The third stage is coloured amber and drawn visibly larger than the other four, annotated 60 to 120 seconds the first time, then milliseconds, arguing that four of the five stages are trivial and one is not.
A left-to-right flow of five stages in a single request. Client sends token. Bridge authenticates. Bridge loads or compiles the context card. Agent runs headless with the card as its system prompt. Response normalised and returned. The third stage is coloured amber and drawn visibly larger than the other four, annotated 60 to 120 seconds the first time, then milliseconds, arguing that four of the five stages are trivial and one is not.

Table of Contents#

What does a local agent bridge actually have to do?#

Strip away the trust boundary argument, already made in the first post in this series, and a local agent bridge reduces to three jobs performed in sequence for every request it answers. Authenticate the caller. Assemble whatever context the agent needs but does not already have loaded. Invoke the agent headless, wait on it, and return something a caller can act on. The diagram above is that sequence drawn out as five stages rather than three, because the middle job splits into loading a context card and, sometimes, building one from scratch.

Look at where the amber card sits. It is the third of five stages, and it is the only one drawn larger than its neighbours, because it is the only one whose cost is unpredictable. Authentication is a comparison against a stored token, the same operation every time, in the same handful of milliseconds every time. Invocation is a subprocess call with a timeout attached to it. Normalising the response is reshaping a JSON object. None of those three vary by more than noise from one request to the next. Loading or compiling the context card is the one stage that might take milliseconds or might take two minutes, depending entirely on whether anything changed since the last time it ran, and that difference is the entire subject of the next three sections.

Why is the HTTP the easy part?#

A loopback listener that only answers requests from the machine it runs on is a few lines in any framework built in the last decade. Checking a header against a token stored on disk is a string comparison. Routing a handful of endpoints, one to authenticate, one to invoke the agent, one for a health check, is boilerplate any web framework ships pre-built. None of it required a design decision beyond the ones the first post in this series already walked through: bind to loopback, validate the origin, generate a token on first run and store it owner-readable only. Those three decisions, once made, do not need revisiting per request. They execute the same way every time.

The two remaining problems do not have that property. Context compilation has to decide, on every single call, whether the cached artifact still matches the source material it was built from, and that decision has real consequences if it gets it wrong in either direction. Serving a stale card means the agent reasons from conventions that changed last week. Recompiling when nothing changed wastes the sixty to one hundred twenty seconds a compile actually costs, on a request that did not need to pay it. Failure design has an even less forgiving property: get the taxonomy wrong and every failure mode collapses into one undifferentiated response, and a caller who cannot tell a bad token from an unreadable specification file cannot do anything with the answer except retry blindly. HTTP does not have decisions like these baked into it anywhere. That is what makes it the easy part, and it is also why so much writing about local agent bridges stops at the transport and calls the interesting work finished.

How do you hand an agent context it does not already have?#

Context compilation buys back the latency of every call after the first one. Sending a full specification on every request means the agent re-reads and re-reasons about the same conventions, the same voice notes, the same project history, on every invocation, and pays that token cost each time regardless of whether anything changed since the last call. A compiled context card collapses that repeated work into a single artifact, generated once and served from disk on every subsequent request until a source file actually changes. The first call still pays the full cost, sixty to one hundred twenty seconds is the honest range for a specification of any real size, because the specification does have to be read, reasoned about, and reduced. Every call after that reads a cache in milliseconds instead. The tradeoff is a cache invalidation problem in place of a repeated-work problem, and cache invalidation is a solved category of engineering, not an open research question.

That framing matters because the naive alternative looks simpler on paper and is not simpler in practice. Reading every source file fresh on every call avoids the question of when a cache goes stale, but it replaces that question with a worse one: an agent that reasons slower on every request for the entire life of the bridge, not just the first one. A card compiled once and reused rather than rebuilt per call is the same trade a lot of production systems make once they stop treating every request as independent of the one before it. The specification the card was built from does not change every day. It should not be re-read every hour.

When should a compiled context card be thrown away?#

The rule that decides this is small enough to state in one sentence: recompile when a source file's modification time no longer matches the mtime recorded when the card was last built, and serve the cache otherwise. Every source file that fed the compile step gets its mtime written into the card at build time, alongside the compiled content itself. On the next request, the bridge compares the current mtime of every source file against what the card recorded. If they match, nothing has changed since the compile, and the cache is correct to serve. If even one source file's mtime has moved, something changed, and the card is rebuilt from scratch before that request is answered.

Two source files feed a compile step that writes a cached context card recording each source file's modification time. Then a fork: mtimes match, so the cache is served, or a source file changed, so the card is recompiled. A purple note states that hand edits made directly to the card survive, because editing the card does not change the recorded mtimes of the source files it was built from.
Two source files feed a compile step that writes a cached context card recording each source file's modification time. Then a fork: mtimes match, so the cache is served, or a source file changed, so the card is recompiled. A purple note states that hand edits made directly to the card survive, because editing the card does not change the recorded mtimes of the source files it was built from.

The property worth calling out on its own, because it is easy to miss and it is the reason the design works as well as it does, is what happens when someone edits the card directly. Opening the compiled card in an editor and fixing a line by hand does not touch the mtimes of the source files it was built from, only the mtime of the card itself, which the invalidation check never inspects. That means a hand correction survives the next several requests untouched, right up until a real source file changes and forces a genuine recompile that overwrites it. This is not an accident of the implementation. It is what makes the cache trustworthy enough to edit directly when something in it is wrong, instead of forcing every correction to happen upstream in the source material before it can take effect.

What should happen when the agent fails?#

The single rule the rest of this section argues for is: never degrade silently. If a specification file cannot be read, the bridge returns 503 with the exact path it tried to read, and it does not fall back to a generic prompt assembled from whatever context happens to be available. That second option looks harmless from the outside, because the response it produces still reads like a normal draft. It has sentences, it has structure, it answers the question that was asked. What it does not have is any connection to the specification the caller believed was informing it, and a fluent response with no such connection is worse than an error, because an error gets noticed and a fluent wrong answer gets used. Output that reads fine but was not written from your specification is the output that ends up in an email or a pull request or a message to a client, precisely because nothing about reading it signals that anything went wrong.

A failed memory write still returns 200, because losing a memory entry and corrupting a draft are different failures with different consequences for the caller, and the status code should reflect the one that actually matters. Memory in an agent bridge is a convenience layer, a record kept so the next call carries more context than this one did. If that record fails to write, the caller's request was still answered correctly, because the write happens after the draft is already generated, not before it. Nothing about the response depended on the write succeeding, so failing the whole request over it would train callers to distrust responses that were, in fact, entirely correct. A specification read failure has no equivalent slack. A draft generated without the specification is not a slightly worse draft, it is a draft generated from nothing pretending to be a draft generated from something. Losing a convenience and corrupting a result are not the same failure, and only one of them deserves silence.

That asymmetry is the entire argument for treating failure modes as a design surface rather than an afterthought bolted onto whatever the happy path already does, and it is why the taxonomy below has six distinct rows instead of one.

Why does one status code per cause matter more than it sounds?#

A caller that receives a generic server error learns exactly one thing: something went wrong. It does not learn whether retrying is worth attempting, whether the request itself needs to change, or whether a human needs to go fix a file on disk before anything will work again. The HTTP specification already draws the distinction that matters here. RFC 9110 defines the 4xx class as the one where "the client seems to have erred", and the 5xx class as the one where "the server is aware that it has erred or is incapable of performing the requested method". Read plainly, that is the difference between a caller who can fix the problem by changing the request and a caller who cannot fix anything until someone goes and touches the server. Collapsing every bridge failure into one 500 throws that distinction away and replaces it with a single bit of information: broken. A caller deserves better than a bit.

CauseStatusWhat the caller seesWhat to do
Missing or wrong token401An authentication failure, not a generic errorCheck the token file and resend it in the header exactly as stored
Request body too large413A clear size rejection before any agent work startsTrim the payload or split it into smaller requests
Model produced unusable output422A response explaining the draft could not be validatedRetry, or inspect whatever produced the malformed output
Agent CLI could not run502An upstream failure distinct from the bridge's own codeConfirm the agent binary is installed and reachable on PATH
Specification file unreadable503The exact path the bridge tried to readRestore or fix that file, then resend the request
Agent exceeded its budget504A timeout, not a hangRaise the budget or investigate why the agent is running slow

The six above are the causes a caller has to tell apart in order to do something useful with the answer. They are not every status the bridge can emit, and they are not meant to be. A malformed request body returns 400 and an unknown route returns 404, both fixed by a caller without knowing anything about the agent or the context card underneath it, so neither earns a row of its own. The list worth designing carefully is the shorter one, the failures where the same underlying problem could plausibly be reported three different ways, and where a caller genuinely cannot fix the request without knowing which one actually happened.

A six row list mapping cause to status code: bad token to 401, body too large to 413, unusable model output to 422, agent CLI could not run to 502, specification unreadable to 503, and agent exceeded its budget to 504. The number 500 is deliberately absent from the list, because the diagram argues that a generic server error tells a caller nothing that any of these six codes does not tell them better.
A six row list mapping cause to status code: bad token to 401, body too large to 413, unusable model output to 422, agent CLI could not run to 502, specification unreadable to 503, and agent exceeded its budget to 504. The number 500 is deliberately absent from the list, because the diagram argues that a generic server error tells a caller nothing that any of these six codes does not tell them better.

Notice what is missing from that table and from the diagram above it. There is no row for 500. That absence is deliberate, but it is not a claim that the code has been engineered away, and it would be easy to read it as one. The bridge still has a 500. It sits at the end of the error mapping as the last branch, the one reached by anything that did not match a named cause above it, and there is a second one wrapping the request handler for errors that escape the mapping entirely. Both are readable in the reference repo linked below, and I would rather point at them than let a table imply they are gone.

The claim worth making is narrower than "no 500 anywhere". It is that 500 should be the branch nothing arrives at, and every time something does arrive there, that is a failure mode nobody has named yet. Naming them is most of the design work that matters here, because a system that cannot tell you why it failed cannot tell you what to fix, and a caller stuck guessing at that distinction learns to distrust every response, not just the broken ones.

The context compiler, the mtime cache, and the six-code failure taxonomy described above all exist in a minimal runnable form in a public reference repo, for anyone who wants to see the design wired together rather than described.

What none of it had survived, at the point this post describes, is contact with a real caller. Three bugs turned up in the first afternoon I pointed one at the bridge, and the 422 row in the table above is one of the repairs rather than something designed in from the start.

FAQ#

Can the context cache go stale?#

Only if a source file changes without its modification time changing, which does not happen through normal edits. Every ordinary write, whether from an editor, a script, or a git checkout, updates the file's mtime, and the bridge compares that mtime against what the card recorded at compile time on every request. The one way to fool the check is to modify a file's contents while deliberately preserving its original mtime, which requires tooling most editors do not expose and most workflows never touch.

What happens on a cold start, before any card exists?#

The first request after a cold start pays the full compile cost, sixty to one hundred twenty seconds for a specification of real size, because there is no cache to compare mtimes against yet. That request is slower than every request after it, and it should be. The alternative, serving something before the specification has actually been read, is exactly the silent degradation the failure design in this post argues against.

Does this work with an agent other than Claude Code?#

The pattern does not depend on which agent sits behind the bridge. Context compilation only needs an agent that can accept a compiled card as part of its system prompt, and failure design only needs an agent whose CLI produces a distinguishable success or failure per invocation. Both properties are common to headless coding agents generally, not specific to any one vendor.

Why not just send the whole specification on every call?#

Because the specification does not change every hour, and re-reading it as though it might is a cost paid on every request instead of once. A card compiled once and served from cache turns that repeated cost into a single upfront one, and the mtime check keeps the cache honest without asking a human to remember to invalidate it manually.

What does the token actually protect against?#

It stops any process that can reach the loopback address, which on most machines is any other local process, from invoking the agent as though it were the authorised caller. It does not protect against a compromised machine, because a token stored on disk is only as safe as the disk it sits on. It is the same authentication a network boundary always needs once a process boundary is no longer doing that job for free.

Why does a specification failure return 503 instead of 500?#

Because 503 tells a caller the service is unavailable for a specific, checkable reason, the file at a named path could not be read, rather than an unspecified internal error. The distinction matters because a caller reading 503 with a path knows exactly what to go check. A caller reading 500 knows nothing beyond the fact that something, somewhere, did not work.

Is compiling context once instead of every call actually safe?#

It is safe in proportion to how well the invalidation check is built, and an mtime comparison against every source file is a conservative check, not an optimistic one. It errs toward recompiling more than strictly necessary rather than less, because any file touched at all, even by a change that turns out to be cosmetic, forces a fresh compile on the next request. The failure mode this design avoids is the more dangerous one, an agent reasoning from a card that quietly no longer matches its source.

Share:

Stay in the loop

New posts on AI engineering, Claude Code, and building with agents.