Agent Engineering · Frontend Architecture

    Streaming in Agent UIs: Use the SDK or Roll Your Own?

    The case for the SDK ends with three tests, and says zero failures means the decision is already made. We fail one of them, and it is the one that decides everything downstream. A reply from the other side of the build-or-buy line.

    August 26, 2026 | 9 min read

    Key takeaways
    • We fail the vocabulary test. The event stream is our product surface, so the hook's fixed model is the constraint rather than the help.
    • A chat hook is an ORM over the conversation. Excellent while your access pattern is ordinary. Expensive the moment it is not.
    • Two of our seven event types map cleanly onto the hook's model. The other five land in the generic data channel, outside the state machine we would be adopting it for.
    • “SDK or our own” is the wrong unit. It is five decisions, and the honest answer differs per layer.
    • One row of the standard failure list is not ours: our stop endpoint aborts generation server-side, so cancellation is real rather than cosmetic.

    Part 1 · The decision

    The test we fail

    Zero failures and the SDK wins without a bake-off. We do not have zero.

    The three tests are transport, durability and vocabulary. Ours is the third. Our stream is not a decorated transcript, it is the product surface. It carries approval gates that pause a run mid-flight, provenance attached to individual claims, and components the agent chooses at runtime. When the event vocabulary is the thing you sell, a library that fixes that vocabulary is not saving you work. It is setting your ceiling.

    What follows is not a rebuttal of the failure list. That list is accurate, and most of it is ours either way. It is an argument about which layer we were being asked to give up, and what it would have cost us.

    What is the vocabulary test?

    The vocabulary test asks one thing: is your event stream a decorated transcript, or is it the product? If you expect to keep inventing events, a library that fixes the set of them does not remove work. It moves the work into an adapter you maintain forever, and it caps what your interface can say.
    Vocabulary
    7
    event types our backend emits today, with two more on next quarter's board
    Clean map
    2
    of those seven become a first-class part the library would manage
    Escape hatch
    5
    fall through to the generic channel, where nothing types or reconciles them
    The three tests scored against our stackThree rows scoring transport, durability and vocabulary. Transport passes because the wire protocol is ordinary server-sent events. Durability passes because run recovery is already solved on the backend. Vocabulary fails because the event stream is the product surface and the set of events keeps growing.THE THREE TESTS, SCOREDzero failures and the SDK wins. we do not have zero.TRANSPORTPASS · NO REASON TO BUILDOrdinary server-sent events over a single POST.Nothing here a library would struggle with.DURABILITYPASS · NO REASON TO BUILDThe run is already durable server-side.Resume is a backend contract, not a hook feature.VOCABULARYFAIL · THIS ONE DECIDES ITApprovals, provenance, runtime components.The stream is the product, and it keeps growing.ONE FAILURE IS NOT A TIE-BREAK. IT IS THE DECISION.eerly.ai
    Figure 1. Two tests give us no reason to build anything. The third one we could not argue our way out of, because it describes what the product is.

    A chat hook is an ORM over the conversation. While your access pattern is ordinary CRUD it is a gift. The moment it is not, you spend your days coaxing the ORM into emitting the SQL you already knew how to write.

    The analogy we kept coming back to

    Nobody regrets an ORM on a CRUD app. People regret one on a reporting workload, and the regret always arrives the same way: not as a wall, but as a slow accumulation of escape hatches until the abstraction is carrying none of the weight and all of the constraints. That is the shape we were looking at.

    Part 2 · The mismatch

    Where the shapes stop lining up

    Our backend emits a run. The hook models a message.

    A run is made of steps, approvals and attached sources. A message is made of parts. Two of our event types have a natural home in that model: token becomes a text part, tool.call becomes a tool part. The remaining five go through the generic data channel, untyped, unmanaged, and outside the state machine that is the hook's main offer.

    Seven event types mapped onto the hook's message modelOur seven event types mapped onto the hook's model. Token and tool.call map cleanly onto a text part and a tool part. Run started, step started, approval requested, source attached and run finished all funnel into a single generic data channel marked untyped and unmanaged.WHERE THE SHAPES STOP LINING UPour vocabulary, mapped onto the model the hook ownsOUR EVENT STREAMTHE HOOK'S MODELtokentool.callrun.startedstep.startedapproval.requestedsource.attachedrun.finishedtext parttool partgeneric data channeluntyped · unmanagedoutside the state machineTWO OF SEVEN MAP CLEANLY.the five that make the product ours land in the escape hatch.eerly.ai
    Figure 2. The whole argument in one picture. We would take on every constraint of the managed model, and the events we care about would sit outside it.

    The adapter that translates our events into parts and back out again has no value to a customer; it exists to satisfy the library. Writing it is not a shortcut we declined. It is a layer we would have been adding.

    Split those seven by what they do and the gap stops looking like a rounding error. Everything the library manages for us is output. Everything that makes the interface ours is lifecycle or interaction, and all of it lands in the same untyped bucket.

    Where the seven events land, by categoryGrouped bar chart of the same seven event types split into three categories. Output events, token and tool.call, give two events typed by the library and none in the generic channel. Lifecycle events, run started, step started and run finished, give zero typed and three in the generic channel. Interaction events, approval requested and source attached, give zero typed and two in the generic channel.WHERE THE SEVEN EVENTS LANDsame seven events as Figure 2, grouped by what they dotyped by the librarygeneric data channel3210EVENTS200302OUTPUTtoken, tool.callLIFECYCLErun / step / finishedINTERACTIONapproval, sourceThe library manages our output. It does not manage our product.eerly.ai
    Figure 3. The two bars the library covers are the two nobody would pay for. Approvals and attached sources are the reason a user picks our interface, and they are the ones with no home in the model.

    Part 3 · The unbundling

    It is five decisions, not one

    “SDK or our own” is the wrong unit, and we agree with the original piece about that.

    Draw the stack and the argument gets more precise. Rendering was never the hook's job. Transport and resumption can be replaced on their own. What the hook really sells sits in two layers: provider normalization, and the client state machine.

    Five layers of the streaming stack, with ownership marked per layerFive layers of the streaming stack with ownership marked. Rendering the UI is ours either way. The message and run state machine, and provider event translation, are what the hook actually sells. Transport, resume and cancel is replaceable on its own. Model token output belongs to the vendor.IT IS FIVE DECISIONS, NOT ONEthe honest answer differs per layerRendering the UIOURS EITHER WAYMessage & run state machineWHAT THE HOOK SELLSTransport, resume, cancelREPLACEABLE ON ITS OWNProvider event translationWHAT THE HOOK SELLSModel token outputTHE VENDOR'SReplacing only the transport layer is a real option. It deserved ruling out, not skipping.eerly.ai
    Figure 4. We diverge in the state machine, which is the one layer you cannot swap on its own. That is why the middle path did not rescue us.

    Part 4 · The case

    Three reasons we keep ours

    Three, not a list of seven. The rest were preferences dressed as arguments.

    • We would be translating our model into theirs, permanently. The hook owns the message list, so its shape becomes our shape. Every new event type we invent starts life as a question about how to smuggle it through the data channel.
    • No upgrade tax, and the backend keeps the contract. These SDKs have already changed their wire format once across a major version. With our own handler the backend defines the vocabulary and the frontend consumes it, so our roadmap is not coupled to someone else's release schedule.
    • Failures land in code we wrote. A misbehaving stream gives us a stack trace in our own reducer rather than an unexpected intermediate state in one we did not write. Time-to-diagnose beats lines-of-code saved.

    One correction to the standard list while we are here. The failure where a stop button halts the interface while the server keeps generating is a real bug, and it is not ours: our stop path hits a real endpoint that aborts the run, so cancellation stops the tokens rather than just the paint. Refresh and stop are separate routes, on purpose.

    Operational takeaway
    Cost the adapter, not the handler
    The comparison is rarely “write a streaming handler” against “install a library”. It is “write a handler you own” against “write an adapter you own forever, plus a library you do not”. Price the second one properly and the gap narrows fast.

    Part 5 · The other side

    When the hook is the right call

    Most teams reading this should install the library. We are the exception, and exceptions should have to earn it.

    If the conversation is your product, rather than a window onto some longer job the backend is running, the hook already models nearly everything you emit. Text, tool calls, errors, retries: typed and reconciled before you touch them. Support assistants and documentation copilots live here. The work you skip is real, dull, and worth weeks.

    Adopt
    Your events are stable and the message list is already the shape of your data. Nothing in the stream is something a customer would notice you inventing.
    Adopt, then re-evaluate
    One or two custom events, no obvious plan for more. Ship on the library, then pick a trigger for revisiting it: a third custom event, or the first special case in the adapter. Write the trigger down, or you will rationalise past it.
    Own it
    Your stream carries interaction rather than output. Approvals that block, sources pinned to claims, components the agent picks while it runs. Hand that vocabulary to a library and you have set the ceiling for the product.

    None of the standard failure list stops applying to us, by the way. Partial token flushing, reconnection, scroll anchoring, error recovery: ours to fix, one at a time, instead of installing something that fixes them. That is a real cost. We took it on purpose.

    Part 6 · The method

    Run the test on your own stack

    An afternoon at a whiteboard beats a week of opinions.

    1. Write down every event your backend emits, including the ones already sitting on next quarter's roadmap. Start from that list rather than a feature table.
    2. Map each event onto the library's model. First-class part, or generic channel. Count both columns before anyone starts arguing.
    3. Score transport, durability and vocabulary. Zero failures, install the library and go to lunch. One failure, work out which layer it lives in.
    4. Split the stack. Rendering, state machine, provider translation, transport, model output. Ownership differs per layer, and teams that skip this step end up issuing one verdict for five questions.
    5. Price the adapter. If it is permanent, invisible to customers, and grows every time you add an event, subtract it from whatever the library promised to save you.

    Do that and the argument tends to die on its own. Most teams find zero failures and a clean map, and the library wins before lunch. A few find their own five-of-seven, and by then nobody is arguing about taste.

    The library is good. That was never the question. The question was whether the model it manages is the model we are building, and ours is not it.

    Eerly AI Studio
    Eerly AI Studio

    The stream should carry the decision, not just the transcript.

    Eerly AI Studio streams approval gates and per-claim provenance across the systems your teams already run, inside the permissions those systems already enforce.

    Book a demo →

    FAQ

    Frequently asked questions

    The questions engineers ask us most often about streaming architecture, and the short answers.

    Should you build your own streaming handler or use a chat SDK?

    Use the SDK unless your event vocabulary is part of what you sell. Three tests settle it: transport, durability, vocabulary. Ordinary wire protocol, no need to recover a run after a refresh, stable event set, and you should install the library and stop debating. Fail one of them, vocabulary above all, and the library's fixed message model becomes a constraint you carry for as long as the product lives.

    What is the vocabulary test?

    It asks whether your stream is a decorated transcript or the product itself. A stream that pauses a run for approval, pins sources to individual claims and hands back components the agent picked while it ran is not a transcript. Once the vocabulary is the thing you sell, a library that fixes it sets your ceiling.

    When is a streaming SDK clearly the right call?

    When the conversation is the product rather than a window onto a longer job the backend is running. Support assistants and documentation copilots emit text, tool calls and errors, which the library already models and reconciles. Building your own handler there costs weeks of edge cases around partial tokens, aborted requests, scroll behaviour and error recovery, and buys nothing a user would notice.

    Why is a chat hook like an ORM over the conversation?

    Both swap direct control for a managed model. While your access pattern is ordinary the trade is a gift. Once it is not, you spend your days coaxing the abstraction into emitting what you already knew how to write. The regret rarely arrives as a wall. It shows up as escape hatches piling up until the abstraction carries none of the weight and all of the constraints.

    What breaks when custom events go through a generic data channel?

    They sit outside the state machine you adopted the library for. Nothing types them, nothing reconciles them across a reconnect, they do not move the library's status transitions, and every consumer re-derives meaning from a raw payload. You keep the constraints of the managed model and lose the management for the events that differentiate you.

    Can you keep an SDK for transport and still own the client state machine?

    Sometimes. Transport with resume and cancel swaps out on its own, and rendering is yours either way. The layer you cannot swap independently is the state machine, because it defines the shape of the message list. If that is where you diverge, the middle path does not save you.

    Does writing your own streaming handler mean inheriting the usual streaming bugs?

    Yes, most of them, and any honest build-or-buy argument says so up front. Partial token flushing, aborted requests, reconnection, scroll anchoring, error recovery: all yours. Cancellation is the one worth checking case by case, since it is a backend question rather than a library one.

    How do you make a stop button actually stop generation?

    Aborting the client request only stops the paint. To stop the tokens you need a server route that ends the run: the client calls stop with the run ID, the server aborts its own request to the model provider, and the run gets marked cancelled in storage so a later reconnect does not resume something the user already killed. Keep resume and stop as separate routes.

    Notes & further reading

    • This is a response post. The original argument for adopting the SDK, including the three tests and the failure list referenced throughout, is published alongside it: Streaming in Agent UIs: Use the SDK or Roll Your Own?
    • The event counts, mappings and layer ownership here describe the Eerly AI Studio streaming stack in August 2026. It is an account of one codebase, not a benchmark.
    • MDN Web Docs. Using server-sent events. Background on the transport layer discussed in Part 1. developer.mozilla.org
    • MDN Web Docs. AbortController. The client half of cancellation, and the reason the server half is the part that matters. developer.mozilla.org
    Kinal Kukda
    Written by
    Kinal Kukda
    Frontend Architect, Eerly.ai

    Kinal is a frontend architect specializing in large-scale web architecture, design systems and web performance. She writes about bridging complex frontend ecosystems with scalable engineering practices, from micro-frontends and state management patterns through to CI/CD and Core Web Vitals optimization.