Three Questions to Answer Before You Ship an Agent UI A developer building agent UIs argues that teams should decide where the agent loop runs before designing the chat interface, sorting five libraries into three architectural shapes: a vendor runtime in the request path, vendor code inside an owned API route, or the agent framework's own loop. The developer, who maintains the Angular project Threadplane, recommends the third shape for most teams since the agent server already holds the model key, handles authentication, and executes tool calls. The post also distinguishes three mechanisms commonly lumped under "generative UI": tool-call rendering, declarative specs like A2UI and json-render, and sandboxed MCP Apps mounted in iframes. Most teams building an agent UI start with the chat box and work backwards. I think that is the wrong order. The chat box is the easy part. The decisions that actually shape the product are made earlier, usually by default, and usually without anyone noticing they were decisions. In this post, I want to give the three I keep coming back to: I build in Angular and I maintain Threadplane https://threadplane.ai , so my examples lean that way. The questions do not. They apply to React, Vue, Svelte, and whatever you are using, and I have tried to keep the answers framework-neutral and to say where my own project makes a trade. Somebody's code has to call the model, look at what came back, run or forward any tool call, and call the model again. Where that code lives decides almost everything else about your architecture: who holds the API key, where approvals pause, where threads persist, and whose release schedule you are on. I compared five libraries on this question in September 2026, and the answers sort into three shapes. A vendor server in the request path. The UI library ships a runtime. You deploy it, your browser talks to it, and it talks to your agent. You get key custody, auth, agent routing, and trusted middleware in one place, and the door to a hosted thread store and inspector. You trade a second server on the hot path and, in at least one popular library, a direct path that is labelled either dev-only or enterprise. Vendor code in your route. No box to deploy, but the loop is the library's, running inside an API route you own. Server tools, approvals, and persistence can be first-class because the library owns both ends of the wire. The trade is that the loop is separate from the one your agent framework already has, and it moves when the library moves. Your agent. The loop is wherever your agent framework runs it, and the UI library talks to that server directly from the browser. One loop, owned by the framework you already chose. The trade is that the UI can only show what the agent server actually sends. If a framework's bridge never emits a state delta, no client can invent one. For me, the third shape is right for most teams, and it is the one Threadplane is built on. The reasoning is simple: if you are running an agent at all, you already have a server. It holds the model key, because the agent is the thing calling the model. It already has to authenticate, because it is on a network. It already sees every tool call, because it executes them. A vendor's box in front of it is a second place to solve a problem you already had one place to solve. That does not mean no server at all. You should still put an endpoint you own in front of your agent for auth, credentials, and rate limits. The difference from a vendor runtime is not the hop. It is who owns the code running in it. I wrote up the full comparison, twenty-four rows across five libraries with every cell source-checked, in Why Do Agent UI Libraries Require a Runtime? https://threadplane.ai/blog/why-do-agent-ui-libraries-require-a-runtime The honest version includes what my own choice costs: no hosted inspector, no multi-agent routing, and no server-side stream middleware from us. "Generative UI" hides several different mechanisms behind one phrase, and the libraries that support it rarely say which one they mean. Separate them before you compare anything. Tool-call rendering. The model calls a tool, and you map the tool name to a component. Every library does this, and it is where most teams should start. Declarative specs. The model authors a UI tree in a standard format, and the client renders it from a catalog of components you registered. The two open specs I see gaining ground are A2UI https://a2ui.org and json-render https://github.com/vercel-labs/json-render . Sandboxed apps. An MCP server returns a ui:// resource, and the client mounts it in an iframe. This is MCP Apps, and it is the one my project does not render yet. The choice that matters is inside the second bucket, and it is about contract shape , not renderers. With a fixed spec like json-render, the contract is application-owned. You define the schema, you validate the whole spec before anything mounts, and your handlers decide what every click means. It is a document you can lint, snapshot in a test, and reject before it renders. With a live protocol like A2UI, the surface is agent-owned. The agent creates it, keeps editing it over the life of the conversation, and receives structured actions back when the user interacts. Structure arrives in one message and data in another, so a card can mount as a skeleton and fill in as values land. It is a conversation you subscribe to, not a document you validate. My heuristic is short. If you can validate the entire UI before it renders, start with the fixed spec. If the surface has to live past its first render, with data trickling in, actions coming back, or edits across turns, step up to the protocol. Two examples make the line concrete. An order summary card that the agent produces once and never touches again is a fixed spec. A three-day itinerary that the agent proposes, fills in with prices over the next few seconds, and then rewrites on day two when the user objects is a live surface. One thing both shapes share, and it is worth knowing: the component registry is doing allowlist duty. A component name the model emits that you did not register renders nothing. That posture is the same in both, so it is not a reason to pick either. The Angular-specific walkthrough, with the same order card in both formats, is in json-render vs A2UI: Choosing a Generative UI Contract https://threadplane.ai/blog/json-render-vs-a2ui-choosing . Agents are hard to test end to end for one boring reason: the model does not return the same thing twice. Write an assertion against that and you have written a coin flip. The usual fix is to stop calling the model. Record its responses once as fixtures, replay them on every run, and the tests go deterministic while CI stops spending tokens. We test our whole demo fleet that way, and I recommend it. Two things about that setup are worth more thought than they usually get. Where you put the mock decides what is under test. Mock the agent at the app boundary and you have proven that your component renders what you handed it. Nothing more. Push the seam out to the model provider instead, so the real agent server runs against a fake model endpoint, and your graph's routing, your streaming transport, and your tool round-trips are all under test, because none of them were replaced. The only thing that is not real is the model. Every deterministic harness buys its determinism by deleting a dimension. Ours deletes time . Replayed responses arrive in one or two chunks by default instead of token by token, so assertions on the final DOM stay stable. That is a deliberate choice with the reason written in the source, and a handful of fixtures opt back in with tiny chunk sizes and real latencies where the progressive render is the thing under test. Here is the class of bug that disappears when you delete time. A child agent streams tokens under a namespace. A client that merges them into the transcript shows an extra chat bubble mid-run. Then the run settles, the parent publishes its authoritative state, the transcript is rebuilt, and the extra bubble vanishes. Assert on the finished DOM and the test passes, and not by luck. The end state is correct. Any assertion that runs after an await sees a settled system, and a self-correcting bug is precisely one that settles. Catching that class means driving a live model and sampling the DOM on a tight interval for the length of a run. No final-state suite can report it, and no final-state suite could. So the interesting question about your harness is not whether it is green. It is which dimension you deleted, and which tests opted back in. The full accounting, including a fixture-ordering mistake that makes a replay run never terminate, is in What Fixture Replay Can't Catch https://threadplane.ai/blog/what-fixture-replay-cant-catch . Each question has a default answer that arrives with whatever library you install first. The library ships a runtime, so the loop runs there. The library has a tool-rendering hook, so that is your generative UI. The library has a mock, so that is your test seam. None of those defaults is wrong. All of them are decisions, and I think you should make them as decisions. For me, the answers are: run the loop in the agent and put an endpoint you own in front of it, start with a fixed UI spec and step up to a live one per surface, and mock at the model provider while knowing exactly which dimension you gave up. Those are the answers Threadplane https://threadplane.ai is built around, and the docs https://threadplane.ai/docs show what each one looks like in Angular with a LangGraph or AG-UI backend. The questions are the part I am confident about.