We take the first two at every call site where a model is invoked inside the graph, and the third in the one place every stream passes through. Any one of them fixes the bug in isolation; all three mean a future call site that forgets one is still covered.
Bug 3: "are drawn.Found it"
The messages mode yields text chunks, and nearly every consumer joins them verbatim. But an agent's reply is often several model calls separated by tool calls: some text, a tool call, more text. Joined verbatim, the last sentence before a tool call and the first sentence after it run together with no space. Insert a paragraph break whenever text resumes after a tool boundary. If your interface shows tool activity, record where each text segment falls relative to the tool calls, so the reply can be drawn in the order it happened rather than as one block above a list of tools.
Bug 4: other message types in a "messages" stream
Despite the name, stream_mode="messages" yields every message the graph produces, not only the assistant's tokens: tool calls, tool results and the human input can all appear. Check the chunk's type before treating its content as reply text, and treat a tool result as data. Better still, translate the raw stream once into typed events - text, tool call, tool result - and have every surface consume those, so the filtering lives in one place.
Bug 5: retrying a stream that has already started
Model calls fail transiently, and retrying is right. Retrying a stream is right only until its first chunk has been delivered. After that, a retry replays the start of the answer to a user who has already seen it, and they receive half an answer followed by the whole answer. The rule is simple and absolute: track whether anything has been yielded, and once it has, surface the error instead of retrying.
Be precise about what to retry, too. The official OpenAI Python SDK, which many LangChain integrations sit on, retries timeouts, conflicts, rate limits and server errors (408, 409, 429 and 5xx) and connection failures - but only for the request it makes. A connection that drops while the response body is being read is outside that retry, and a streamed turn spends nearly all its time reading the body. The resulting exception may have an empty message: str(httpx.ReadError()) is an empty string, so an error reporter that prints only the message shows the user nothing at all. Classify transport errors by their type, not their text.
Retry at the client, around the single model call, not by re-running the graph: re-running a turn re-executes its tools and rewrites its checkpoints. Olano's managed model tier retries a small allowlist of transient failures this way, a few times with jittered back-off, and never once a stream has produced output.
Bug 6: the turn dies when the browser does
The simplest server design runs the agent inside the HTTP response that streams it. Then the turn's lifetime is the socket's: a phone locking its screen or switching from wifi to mobile data closes the connection, the framework cancels the response, and the cancellation propagates into the graph mid-tool-call. Run the turn as its own task and make each HTTP stream a viewer of it, with a sequence number on every event so a returning client can resume from where it left off. Then make sure an explicit Stop button actually stops the task, since closing the socket no longer does.