Render a terminal result the streaming path did not recognise - #203
Merged
Conversation
Every streaming turn on the Temporal engine failed in the UI with "agent stream
ended unexpectedly", including turns that had fully succeeded — tool launched,
ToolRun Succeeded, answer produced.
## The interface guarantees a result; the stream loop asked for a node name
`AgentGraphLike` is the seam ADR 0036 built on, and TemporalEngine is a second
implementation of it. But every terminal branch in the streaming loop keys off a
LangGraph NODE NAME — composeResponse, planAction, selectDelegate, bareAnswer,
delegateToAgent, checkActiveAgentRun. The engine has no LangGraph nodes. It
yields one terminal update under its own key, matches nothing, falls out of the
loop, and hits the "shouldn't normally happen" error.
So the branches are unreachable for any implementation that is not the graph,
which is exactly the case the interface exists to allow.
The fallback now keys on what the interface actually guarantees: if the stream
ended and a result was captured, render it. Errors and caller-supplied tool
calls already worked, because those two checks read `update` rather than the
node name — the success path was the one left out.
Unreachable for the graph itself, which always hits a branch above first, so
AGENT_ENGINE=langgraph is byte-identical in behaviour.
## Why not name the engine's update after a graph node
Yielding `{ composeResponse: ... }` from the engine would have been a smaller
diff and would have worked. It also would have meant a second implementation
lying about which node produced an answer to satisfy a check that should not
have been node-specific, and it would leave the next implementation to
rediscover this. Fixing the consumer keeps the interface honest.
Two tests: the engine-shaped terminal update now renders its answer, and a
stream that ends with neither a terminal node nor a result still reports the
error. The first fails without this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Every streaming turn on the Temporal engine fails in the UI with
❌ agent stream ended unexpectedly— including turns that fully succeeded.Observed on a live cluster running
AGENT_ENGINE=temporal: asked "who won theworld cup?", and the backend did everything right —
…and the user saw the error. Open WebUI streams, so this is every chat message.
The interface guarantees a result; the stream loop asked for a node name
AgentGraphLikeis the seam ADR 0036 is built on, andTemporalEngineis asecond implementation of it. But every terminal branch in the streaming loop
keys off a LangGraph node name —
composeResponse,planAction,selectDelegate,bareAnswer,delegateToAgent,checkActiveAgentRun.The engine has no LangGraph nodes. It yields one terminal update under its own
key, matches nothing, falls out of the loop, and hits the "shouldn't normally
happen" error.
Those branches are therefore unreachable for any implementation that is not the
graph — precisely the case the interface exists to allow.
Two checks in the same loop already got this right by reading
updateinsteadof the node name:
update.errorandupdate.pendingToolCalls. So enginefailures and caller-supplied tool calls (ADR 0035) both worked. The success
path was the one left out.
The fix
Key the fallback on what the interface actually guarantees — if the stream ended
and a
resultwas captured, render it.Unreachable for the graph itself, which always hits a branch above first, so
AGENT_ENGINE=langgraphbehaviour is unchanged.Why not just name the engine's update after a graph node
Yielding
{ composeResponse: … }fromTemporalEngine.stream()is a smallerdiff and would work. It would also mean a second implementation lying about
which node produced an answer, to satisfy a check that should not have been
node-specific — and it leaves the next implementation to rediscover this. Fixing
the consumer keeps the interface honest.
Verification
apps/agent-orchestratorsuite: 59 passed.reverting it:
1 failed | 58 passed), so it genuinely covers the bug.terminal node nor a result still reports the error.
Deploying it
This needs an
agent-orchestratorimage rebuild, not just a chart bump — thechange is in app code. No chart or values change, so no version bump here.
🤖 Generated with Claude Code