Skip to content

Render a terminal result the streaming path did not recognise - #203

Merged
DavidNic11 merged 1 commit into
mainfrom
fix/temporal-engine-streaming-terminal
Aug 7, 2026
Merged

Render a terminal result the streaming path did not recognise#203
DavidNic11 merged 1 commit into
mainfrom
fix/temporal-engine-streaming-terminal

Conversation

@DavidNic11

Copy link
Copy Markdown
Collaborator

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 the
world cup?", and the backend did everything right —

POST /invoke                                    202
ConversationWorkflow  CheckNeedsCapability → RetrieveSkills → RetrieveAgents
                    → SelectDelegate → ResolveSkillTools → PlanAction
                    → LaunchToolRun
ToolRun run-ad12da07…  Succeeded    Job Complete 1/1 in 5s

…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

AgentGraphLike is the seam ADR 0036 is built on, and TemporalEngine is a
second implementation of it. But every terminal branch in the streaming loop
keys off a LangGraph node namecomposeResponse, 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 update instead
of the node name: update.error and update.pendingToolCalls. So engine
failures 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 result was captured, render it.

Unreachable for the graph itself, which always hits a branch above first, so
AGENT_ENGINE=langgraph behaviour is unchanged.

Why not just name the engine's update after a graph node

Yielding { composeResponse: … } from TemporalEngine.stream() is a smaller
diff 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-orchestrator suite: 59 passed.
  • The new engine-shaped test fails without the src change (confirmed by
    reverting it: 1 failed | 58 passed), so it genuinely covers the bug.
  • Second test pins the existing behaviour: a stream ending with neither a
    terminal node nor a result still reports the error.

Deploying it

This needs an agent-orchestrator image rebuild, not just a chart bump — the
change is in app code. No chart or values change, so no version bump here.

🤖 Generated with Claude Code

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>

@imaustink imaustink left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

@DavidNic11
DavidNic11 merged commit c6ce83a into main Aug 7, 2026
6 checks passed
@DavidNic11
DavidNic11 deleted the fix/temporal-engine-streaming-terminal branch August 7, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants