Subhead: Tokens, tool calls, reconnects, and crash recovery — one durable actor shape instead of four subsystems.
Every streaming agent demo ends at the same cliff: the model streams a pretty answer, and then real life shows up. A user asks a follow-up. Then another one before the first answer finishes. A tool needs to run mid-conversation. The tab refreshes. The isolate restarts. Each of those is fine in the demo and a subsystem in production.
I built a sample that takes the other path: a LangChain tool-calling agent that runs inside a durable actor on Telnyx Edge Compute, where streaming, history, reconnects, and crash recovery are properties of the storage the agent already uses.
The code is here:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/langchain-streaming-agent
The shape
One StreamingAgent extends Agent per conversation. The Agent SDK gives it three durable primitives I stopped having to build:
- A message log — the conversation, persisted and ordered.
- An event log — a cursor-replayable stream of progress events.
- Named tasks — work that survives crashes and restarts.
The LangChain side plugs in as a custom chat model. TelnyxStreamingChatModel extends BaseChatModel calls the Telnyx Inference binding — pre-authenticated, zero credentials in the deployed function — with stream: true, parses the SSE body, and yields real AIMessageChunks, including streamed tool-call deltas. From there, LangChain's createToolCallingAgent and AgentExecutor work unchanged.
Tokens that commit before they stream
The pattern that makes everything else work: every token delta the model produces is committed to the agent's event log before it's pushed to clients.
onToken: async (text) => {
roundText += text;
await this.emit({ type: "token", payload: { turn, text } });
},
Commit-before-push sounds like a small ordering detail. It's the whole feature. The browser attaches with resume: true and a cursor; refresh mid-answer and it replays exactly the events it missed — no gaps, no duplicates. The durable log is also why the demo can show tool calls as first-class events (tool_start, tool_result) instead of burying them in the text.
The part that ate my afternoon
Two things surprised me, and both are worth knowing before you build the same thing.
First: AgentExecutor invokes the model per round — it does not stream it. There are no on_chat_model_stream events to subscribe to. The fix is to capture tokens at the model layer: the chat model fires an onToken hook per SSE delta, and the agent commits each one. One hook, ordered, durable.
Second: streaming tool calls break naive model wrappers. The model emits a tool call as deltas — the function name in one chunk, the JSON arguments in pieces. The next round needs them back whole, with tool_call_id intact, or the API rejects the round and you get a silent retry loop. The sample's wire mapping handles all three shapes LangChain uses (parsed tool_calls, streaming tool_call_chunks, and the raw additional_kwargs.tool_calls the executor rebuilds scratchpad turns from).
Rapid-fire questions, crash recovery
Because the run loop tracks an answeredThrough high-water mark — the message seq of the last answered user turn — sending three questions in two seconds just works: each queued run drains the backlog oldest first, and every question gets its own streamed answer with the history that came before it.
And because that marker only advances after the answer commits, a crash mid-turn reprocesses exactly the unanswered turns. The retry logic is the log.
Try it
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/langchain-streaming-agent
npm install && cp .env.example .env && npm run local:dev
Two browser windows on the same session show you the durable part: refresh one mid-answer and watch it resume from the cursor. The full walkthrough — including adding your own tool — is in the repo's GUIDE.md, and the deployed function needs no API key at all: inference runs through the platform's pre-authenticated Telnyx binding.
Top comments (0)