Server -> Client:`connected`、`token`、`thinking`、`step`、`final_answer`、`skill_match`、`confirmation_request`、`confirmation_result`、`ask_human`、`error`、`pong`
Server -> Client:`connected`、`token`、`thinking`、`step`、`final_answer`、`skill_match`、`confirmation_request`、`confirmation_result`、`ask_human`、`error`、`pong`
@ -42,6 +42,11 @@ The agent-level fallback sequence when the primary agent fails: main agent → R
### ReAct Streaming Contract
### ReAct Streaming Contract
The protocol `ReActEngine.execute_stream()` yields to consumers: first zero or more `token` events whose `data.content` are incremental content fragments, then exactly one `final_answer` event whose `data.output` is the concatenation of all token fragments (the complete text). The two events carry the same content — token is the增量 view, final_answer is the聚合 view. Consumers must pick one accumulation strategy (append tokens, or wait for final_answer) and cannot mix both without producing doubled output. When `execute_stream()` is wrapped from a sync `execute()` via `_wrap_sync_as_stream`, no token events are emitted and final_answer's output is the sole content carrier.
The protocol `ReActEngine.execute_stream()` yields to consumers: first zero or more `token` events whose `data.content` are incremental content fragments, then exactly one `final_answer` event whose `data.output` is the concatenation of all token fragments (the complete text). The two events carry the same content — token is the增量 view, final_answer is the聚合 view. Consumers must pick one accumulation strategy (append tokens, or wait for final_answer) and cannot mix both without producing doubled output. When `execute_stream()` is wrapped from a sync `execute()` via `_wrap_sync_as_stream`, no token events are emitted and final_answer's output is the sole content carrier.
### Streaming Milestone
A chat message that progresses through `streaming → completed | error` states as WebSocket events arrive, used to surface long-running expert team operations (expert results, team synthesis) to the user as live progress indicators rather than blocking until completion.
A streaming milestone is opened by a chunk event (`expert_result_chunk` / `team_synthesis_chunk`) and must be finalized by a terminal event of the same type family (`expert_result` / `team_synthesis`) carrying a `status` field. If the terminal event never arrives — e.g., the stream is interrupted by cancellation or exception without a cleanup broadcast — the milestone becomes an orphan, spinning forever. Stable identifiers (`synthesis_id`) injected into both chunk and terminal events let the frontend precisely match a terminal event to its open milestone across retries and concurrent teams; positional matching (last streaming milestone) is unreliable in those scenarios and serves only as a backward-compatibility fallback.
- "expert_step WebSocket events reached frontend with missing fields — frontend WsServerMessage contract silently degraded (no expert_id/expert_name/expert_color/content/step)"
- "cancel_task() could not cooperatively cancel a streaming task — execute_stream continued emitting tokens after user cancellation"
- "If synthesis streaming raised CancelledError or any Exception, no terminal team_synthesis event was emitted; frontend streaming milestone spun forever"
- "Frontend could not precisely match a team_synthesis terminal event to its open streaming milestone across retries / concurrent teams (no stable synthesis_id)"