Tech9/18/2026·4 min read

Two Implementations Are a Tax, Not Redundancy

We had an agent loop in Rust and another in Swift. Deleting the Swift one: 22,207 lines removed, five added. The duplication was not free — it billed us as one-off fixes.

The theory that seemed reasonable

We built an agent runtime in Rust, because the same runtime had to serve an iOS app, an Android app, and a keyboard extension, and because the interesting parts — streaming, tool dispatch, context management — are the parts where a language with real ownership primitives earns its keep.

That part was right. Then we built a second one in Swift.

The reasoning at the time was the reasoning people usually give: the Rust core is the engine, and the Swift framework is the native layer the app talks to. Thin adapters are cheap, the platform idiom is worth having, and having a native implementation means we are not blocked by the FFI boundary. It is a reasonable-sounding position, and it is a description of how things start.

What it became was something else. Two loops meant two retry policies — one of them would treat a 429 as fatal and the other would not. It meant two context-compression heuristics, which meant the same conversation could be summarized differently depending on which process happened to handle the turn. It meant two places that turned a tool result into a message, so a bug fixed in one was still live in the other, and we had no way to tell which one a user had actually hit. None of these is dramatic. All of them are permanent, and they compound quietly.

The duplication was never free. It was a tax we had chosen not to name.

The tell was what it forced us to build

Duplication shows itself not at the moment you create it, but at the moment you try to work on it.

We needed to verify one behaviour of the Rust runtime from a Swift test. Because the Swift side could not reach it through the production path, we hand-wrote an HTTP stream bridge in Swift — a URLSession-backed shim — and then hand-wrote a stub URLProtocol to intercept it, so the test could drive the stream deterministically. That is a real piece of infrastructure: request construction, chunked decoding, cancellation, error mapping. It existed for one purpose, which was to make the second copy testable, and it reimplemented something the Rust side had been doing correctly the entire time.

That is the shape of the tax. It never arrives as a bill for duplication. It arrives as a series of small patches that each look like the right thing to do in isolation, and that collectively amount to maintaining a second implementation of a system you already finished.

Deleting is the easy direction

The consolidation itself was the least interesting part of the work, and the numbers are the honest summary of why:

214 files changed, 5 insertions(+), 22,207 deletions(-)

Twenty-two thousand lines removed, five added. Nothing had to be written, because that is what "the first implementation already works" means in practice. The second copy had been costing maintenance, not delivering capability — which is exactly what a tax looks like on a balance sheet.

The files that went tell the story of what had been duplicated without meaning to be. A 560-line agent loop. A 377-line lifecycle controller, managing state transitions that the Rust runtime already modelled. A 234-line model invocation service. And then a context stack: a compression service with its own token estimator and middleware, a memory manager with three interchangeable stores, two session services. Every one of those is a decision someone had to make twice, and two decisions that differ is a bug that has not happened yet.

The seam has to be narrow enough to enforce

Consolidation is not done when the second copy is gone. It is done when there is nothing left for the host to re-implement.

We got that wrong the first time. With the Swift framework deleted, the host still owned three things that it should not have owned: it made its own HTTP requests, it decoded the agent's event stream itself, and it rendered tool calls itself. Each of those is a small, defensible piece of platform work, and together they are a second implementation wearing a much better disguise — because this one had no loop, so it did not look like one.

The next day that came back too. More than sixteen thousand lines moved out of the host, and what it kept was a projection: the host renders state, and the state has exactly one definition, produced by the runtime that also produces the events it renders.

That is the rule worth keeping:

One wire, one loop, and no per-host replay logic.

If a host can reconstruct the same session differently from the same event stream, the boundary has leaked. The tell is always the same: a host that has learned something about the protocol. Ours had, which is why the second half of the cleanup was necessary.

What it cost and what it bought

Two things are worth saying plainly.

The seam is real, and it is not free. Everything that used to be native Swift — the loop, the state machine, the memory store — now lives behind a foreign function interface. That is a genuine cost: the boundary is where types stop being checked by the compiler across languages, where an error has to be reconstructed, where a debugger stops helping. Consolidation is not automatically right. It is right when the thing being consolidated is a policy, because two policies means two answers to the same question, and nobody can enumerate which question will come up first.

Rust became the cross-platform core in earnest. Before this, Rust was one implementation among several. After it, the agent runtime, the input engine, and the rendering pipeline for both platforms all live in the same place, with the native layers reduced to what they should always have been: the parts that genuinely need to be native. A codebase's center of gravity moves a little at a time, and this was the move.

Takeaways

Duplicate implementations are a tax, and the tax is never itemized as such. It shows up as small one-off patches that each look reasonable — a shim, an adapter, a second policy that has to be kept roughly in step — and it is paid forever in drift you cannot enumerate.

Watch for the moment duplication forces you to build something on the weaker side. A test harness, a bridge, a compatibility shim, written purely so the second copy is reachable: that is the tax being itemized, and it is the cheapest possible signal that the second copy is not earning its keep.

Deleting is easier than adding, and the diff will tell you whether the first implementation was really finished. Twenty-two thousand deletions and five insertions is not a refactor. It is a receipt.

And check the seam after you delete. The second copy rarely disappears all at once; it reappears as host-side protocol knowledge — a place the app still makes its own requests, decodes its own events, renders its own tool calls. Consolidation is complete when the host knows nothing about the protocol except how to display what the runtime already decided.

Products in this post

Keep reading