Skip to content

Commit f273085

Browse files
committed
chore(agent_loop): document accumulated_text streaming-already-emitted semantics
A reviewer raised the concern that pushing intermediate tool_use text into `accumulated_text` could cause a double display when the streaming path falls back to it for an empty final EndTurn — first via the live stream, then again as `final_response`. Verification (grep on bridge.rs): * Streaming success arm (~bridge.rs:3032 `Ok(())`) calls only `record_delivery` + `send_lifecycle_reaction`. It does NOT invoke `send_response(buffered_text)`. The buffered fallback only fires on the `Err(stream_error)` adapter-failure arm — that is recovery, not a duplicate emit. * `signal_response_complete` is a phase-change ping, not a re-emit of response text. * `final_response` (which may carry the accumulated_text fallback) is consumed for session persistence, memory extraction, and hooks — never pushed back onto `stream_tx`. Conclusion: the concern is a false positive against the current wiring. This commit pins the contract in code so a future refactor of the bridge cannot quietly introduce a double display: the comment explains the post-stream-fallback semantics and flags the exact invariant that must hold (stream success arm must NOT re-emit `final_response` unless the empty-text fallback is suppressed).
1 parent 633bac9 commit f273085

1 file changed

Lines changed: 26 additions & 0 deletions

File tree

crates/librefang-runtime/src/agent_loop.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4749,6 +4749,32 @@ pub async fn run_agent_loop_streaming(
47494749
// streaming sink already forwards the deltas to the channel,
47504750
// but the in-memory accumulator is what feeds the empty-text
47514751
// fallback in finalize_end_turn_text. Mirrors the sync path.
4752+
//
4753+
// IMPORTANT (streaming-already-emitted semantics): every byte
4754+
// pushed into `accumulated_text` here has *already been
4755+
// delivered to the client* via the streaming sink. The
4756+
// accumulator is a **post-stream** fallback, not a re-emit:
4757+
// * On final EndTurn with non-empty text the live deltas
4758+
// drove the UI, and `final_response` is only used for
4759+
// session persistence + memory extraction.
4760+
// * On final EndTurn with empty text, finalize_end_turn_text
4761+
// returns `accumulated_text` as `final_response`, but the
4762+
// stream has already drained — no re-push to `stream_tx`
4763+
// happens (see signal_response_complete is fire-only).
4764+
// * The bridge.rs streaming success path
4765+
// (channel_bridge.rs ~3032 `Ok(())` arm) calls only
4766+
// `record_delivery` + lifecycle reaction; it never invokes
4767+
// `send_response` with the buffered text. Fallback to
4768+
// `send_response(buffered_text)` only fires on the
4769+
// `Err(stream_error)` adapter-failure arm — that is the
4770+
// intended recovery path, not a duplicate display.
4771+
//
4772+
// So the surface-level concern of "double display" does not
4773+
// manifest with the current bridge wiring. Any future
4774+
// refactor that has the streaming success arm also
4775+
// re-emit `final_response` MUST either drop the
4776+
// accumulated_text fallback in finalize_end_turn_text or
4777+
// gate it on a `streaming_already_emitted: bool` flag.
47524778
let intermediate_text = response.text();
47534779
if !intermediate_text.trim().is_empty() {
47544780
if !accumulated_text.is_empty() {

0 commit comments

Comments
 (0)