DEV Community

anusha
anusha

Posted on

I Wanted a Streaming LangChain Agent. The Agent SDK Already Had the Hard Parts.

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 } });
},
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)