Product9/18/2026·6 min read

One Agent Runtime, Two Platforms, and a Keyboard

QuipKey's agent became cross-platform through one Rust runtime for admission, connectors, memory and event projection, with native host lifecycles.

The third client was already in the room

QuipKey began as an iOS keyboard with an agent panel. Then the product grew a main app, an Android build and a set of capabilities that only make sense when the agent can continue working after the keyboard disappears: connectors, memory, files, background jobs and generated cards.

The easy way to add a platform is to copy the host implementation. Give Android a new service, translate the iOS callbacks, and let the screens converge later. That approach creates a convincing first demo and a second definition of every important state. A connector can be enabled on one platform and rejected on the other. A tool result can become a message twice. Memory can be written by the conversation that started it and by the app that happens to be open. The two clients look similar while the agent behaves differently.

The harder decision was to identify what the hosts must not own:

The runtime owns the agent's meaning; the host owns its surface and lifecycle.

Turns, model routing, tool dispatch, connector admission, memory, workspace files and the event stream belong to one Rust runtime. iOS, Android and the keyboard are consumers. They render the facts they receive, send user intent back through the same contract, and keep platform-specific concerns at the edge.

This is not a claim that the screens are identical. The keyboard has a narrow window and a different focus model. Android has its own input method lifecycle. iOS has a keyboard extension process and a main app process. Parity means a tool call has one identity and one outcome across those surfaces, not that every control has the same frame.

Capabilities are an intersection

The first cross-platform bug looked like a missing feature. A skill was present in the package and appeared enabled in the UI, but the device could not execute one of its operations. The catalog had answered the question "does this package declare the skill?" The UI had treated that answer as "can this host run it?"

Those are different questions. A capability is available only when both sides are true:

available = deployed_package ∩ device_execution

The runtime now owns that intersection. The package manifest describes the skill and its declared operations. The host supplies the device facts: permissions, platform support, credentials and the relay that will carry a request. A skill that needs a connector is not enabled merely because its files are installed. A connector whose authorization has expired is not ready merely because its row exists.

This matters for a keyboard because the keyboard cannot quietly borrow the main app's assumptions. It may be running in a separate process, with a different permission state and a shorter lifetime. The same resolver answers for both hosts, and the host receives a named state: unavailable, available but not connected, or ready. A boolean would force the UI to guess whether it should hide the action or explain how to connect it.

The package boundary also made installation honest. Built-in skills arrive in the application bundle, while user-created skills are packages with a manifest and a revision. The runtime reads one registry. There is no second list in the Android UI that can drift from the iOS list, and there is no host-side rule that silently revives a package the runtime rejected.

One event model, many projections

The agent does not produce a single response string. A turn has a sequence: the user intent, thinking, tool calls, tool results, citations, cards, files and a final answer. Early host implementations each projected that sequence differently. One rendered tool calls as a result card, another appended them to the transcript, and a third waited until the turn ended before showing anything.

The runtime now emits one event model with stable identifiers. A tool call and its result share an id. A card can be related to the artifact it produced. A citation can be attached to the answer that used it. A host can collapse the thinking rows or put them behind a disclosure control, but it cannot invent a second event or lose the relation between a result and the operation that made it.

That distinction fixed several visible problems at once. QuipKey's conversation can show thinking and tool calls as a readable process instead of a wall of text. Android can render the same process with Compose. The keyboard can show a compact projection while the main app shows the full transcript. A file handoff goes to the workspace and remains an artifact the user can preview, rather than becoming a string in one host's chat history.

The host still owns layout. It decides how a markdown table expands, where a long answer scrolls, and what a long-press means in its surface. It does not decide whether an event happened. That is the boundary that lets a UI fix remain a UI fix instead of changing agent semantics on one platform.

Memory needs one writer

Memory was the next place where copying the host would have been especially damaging. A conversation can produce a fact worth keeping. The app can also ask for a cleanup pass. If both write the same memory files, the result depends on which process happens to finish last.

The runtime assigns the cause of a write. A conversation may curate its own turn. A scheduled cleanup pass may reorganize existing entries. The user can edit a memory directly. Each operation has a named source and a revision, so an automatic writer can replace only the state it read and no longer silently overwrites a human edit.

The storage shape follows the product's useful constraint: a memory is a file the user can inspect and change, not a hidden row that only one screen knows how to display. The iOS and Android pages are projections of those files. The keyboard can show a read-only view without becoming a second memory database.

The same owner rule applies to the workspace. An image or an HTML file created by a tool is stored as an artifact, and the transcript keeps a reference. Preview, pinning and sharing operate on the artifact. A host may choose a grid or a list, but it cannot make a second copy whose lifecycle diverges from the workspace.

The keyboard is where the boundary is tested

The keyboard exposes every hidden assumption because it can disappear at any moment. The agent runtime can be loading a model while the input method receives a key. A tool can return after the user has switched apps. A connector can ask for a permission that only the main app can present.

The host adapter therefore owns a small set of explicit states: whether the keyboard window is ready, whether the input surface has focus, whether the runtime is ready to accept a turn, and how a request is handed back to the main app when the keyboard cannot present something. The runtime owns the turn itself. A cold start may delay a response, but it does not change what a tool call means.

That separation is why the Android keyboard could follow the iOS feature work without reimplementing the agent. The platform work was real — input method APIs, window ownership, navigation and Compose layout all needed native solutions — but the agent's package, capability, memory and event contracts did not fork with it.

What we gained by refusing the copy

There is a visible cost. A shared runtime needs a strict contract, and a host adapter cannot patch around a missing state by inventing a local boolean. Some changes take longer because the owner has to be moved to the layer that can actually see both sides of the problem.

The benefit is a product that can grow without multiplying its semantics. A new connector is admitted once. A new skill is described once. A new event type is projected on each host, but its identity and lifecycle remain common. When a memory or workspace bug is fixed, the keyboard, iOS and Android consume the same fix.

The two platforms are still different products to build and test. They share something more valuable than pixels: a runtime that makes the same promises wherever QuipKey is open.

Takeaways

Start a cross-platform design by assigning state owners. The host should own its window, focus and lifecycle; the runtime should own the agent's meaning and durable facts.

Compute capability availability from package and device state together. A declared skill is not an executable skill until the current host can carry every operation it exposes.

Use one event model and let each host project it. Layout and gesture differences are healthy; separate interpretations of tool calls, citations and artifacts are drift.

Treat the keyboard as a first-class client. Its short lifecycle exposes hidden assumptions early, and a clean host adapter lets the same agent contracts serve a keyboard, a phone app and another platform.

Products in this post

Keep reading