Extract stateless message handling into AgentMessageHandler (step 4a)#192
Extract stateless message handling into AgentMessageHandler (step 4a)#192
Conversation
Seven SdkMessage cases move from the port.on switch in runAgent.ts into AgentMessageHandler.handle(): query_summary, message_thinking, message_text, message_compaction_start, message_compaction, done, error. The switch in runAgent retains explicit cases for the three stateful ones: tool_approval_request (async approval flow + usageBeforeTools snapshot), tool_error, and message_usage (delta annotation + lastUsage accumulator). Everything else falls to the default which calls handler.handle(msg). Known temporary regression: the 'compacted at X/Y (Z%)' context-usage annotation on message_compaction is gone. It needs lastUsage, which is set by message_usage — a step 4b case. The annotation comes back when 4b lands. 16 tests cover each message type: block transition, streamed text, conditional streaming (done/end_turn), and the query_summary parts formatting.
bananabot9000
left a comment
There was a problem hiding this comment.
Stateless router, clean extraction 🍌
AgentMessageHandler is truly stateless -- two constructor-injected dependencies (#layout, #logger), no mutable fields, no accumulators. Pure message routing: event in, layout/logger calls out.
7 cases moved verbatim from runAgent.ts: query_summary, message_thinking, message_text, message_compaction_start, message_compaction, done, error. All replaced with default: handler.handle(msg) in runAgent. Net -19 lines from the switch.
Documented regression is well-scoped: lastUsage compaction annotation deliberately left behind (stateful, depends on message_usage accumulator). Tracked for 4b. The right call -- dragging it in would break the stateless contract.
16 tests across 7 describe blocks. All branches covered. Test helpers are minimal. expected/actual pattern consistent with codebase conventions.
Observations (not blocking):
donedoesn't calltransitionBlock-- correct (response block already active), but means stop annotation appends to whatever block was last active. Worth noting for 4b.- Handler has no
defaultcase in its switch -- unhandled types silently no-op. Fine now, consider exhaustive check after 4b when more cases move over.
Good seam for the next step. The remaining runAgent cases (tool_approval_request, tool_error, message_usage) are the stateful ones that belong together in 4b.
Ship it 🚢🍌
What
Seven
SdkMessagecases move out of theport.onswitch inrunAgent.tsintoAgentMessageHandler.handle().Moved cases
query_summary,message_thinking,message_text,message_compaction_start,message_compaction,done,errorStays in runAgent
Three stateful cases remain in the switch with explicit
caselabels:tool_approval_request— async approval flow +usageBeforeToolssnapshottool_error— moving with the tool cases in 4bmessage_usage— delta annotation +lastUsageaccumulatorEverything else falls to
default: handler.handle(msg).Known temporary regression
The
[compacted at X/Y (Z%)]context-usage annotation onmessage_compactionis temporarily absent. It needslastUsage, which is set bymessage_usage— a step 4b case. Compaction only fires at 150k input tokens so this is unlikely to surface in normal use. The annotation is restored in 4b.Testing
16 unit tests covering each message type: block transition target, streamed text content, conditional streaming (
done/end_turn), andquery_summaryparts formatting including the optional thinking-block count.