fix: normalize multi-round gateway streaming#132
Conversation
Signed-off-by: harivilasp <harivilasp@gmail.com>
ashwing
left a comment
There was a problem hiding this comment.
The per-round lifecycle leak and colliding output_index are the sharp edges from #119, and one normalization boundary is the right shape. Traced it against the three acceptance criteria — single lifecycle ✅, contiguous output_index ✅, monotonic sequence_number holds on the success path. One gap and two minor notes inline.
| @@ -242,8 +262,11 @@ fn run_stream(ctx: RequestContext, exec_ctx: Arc<ExecutionContext>, auth: Option | |||
| yield error_sse_chunk(&e.to_string()); | |||
There was a problem hiding this comment.
#119's criterion 2 was "monotonic sequence_number on every emitted frame." These error_sse_chunk frames (this arm and the Ok(Err) one) bypass the accumulator, so if a round already emitted frames (seq 0..k) and a later round fails, the client sees an error frame with no sequence_number after a numbered sequence. The accumulator is moved into the spawned task and only handed back on Ok(Ok(...)), so these arms don't have a handle to it today — worth restructuring so error frames get stamped too, or calling it out as a known follow-up before this closes #119.
| let Some(sender) = stream_events else { | ||
| return Ok(()); | ||
| }; | ||
| let Some(stream_accumulator) = stream_accumulator else { |
There was a problem hiding this comment.
stream_events and stream_accumulator are separate Options but always travel together (both Some on the stream path, both None on blocking). This guard — and the matching one in emit_gateway_completed_events — silently drops every synthetic gateway event if a caller ever passes stream_events: Some with stream_accumulator: None. Bundling them into one type (the way StreamEmitContext already does on the upstream path) would make "both or neither" unrepresentable rather than convention.
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn multi_round_stream_has_single_lifecycle_and_monotonic_public_sequence() { |
There was a problem hiding this comment.
Two multi-round edges worth covering since this PR owns the invariant now: (a) a round that emits multiple public output items before the final round — the current test only ever has one item per round, so it never checks output_offset advancing by item count vs. +1; (b) the response.incomplete/max-rounds terminal path — confirms the terminal still gets a monotonic sequence_number after N rounds. Not blocking.
Signed-off-by: harivilasp <harivilasp@gmail.com>
67f443b to
228ddec
Compare
|
@harivilasp Thanks for the fix. Four structural requests, each building on the previous: 1. Move 2. Parse each SSE line once. Today every line is parsed up to three times: Renumbering becomes 3. Mirror 4. Same frame currency for synthetic and deferred events. The gateway-built events ( |
|
will try to address by tomrrow |
Summary
Revives and updates the GatewayAccumulator work for #119 on top of current
main.The gateway streaming path currently forwards each upstream round as if it were an independent client-visible response. In a multi-round gateway/tool loop that leaks upstream lifecycle and indexing details to clients: duplicate response lifecycle events, sequence numbers that reset between rounds, and output indexes that collide across rounds.
This PR introduces a single stream accumulation layer for gateway responses so the public SSE stream is owned and normalized at one boundary before it reaches the client.
Closes #119.
What Changed
Why
Issue #119 documents three symptoms with the same root cause: no stage owns the final client-visible SSE sequence for multi-round gateway execution.
Without this, clients that initialize on
response.createdcan reset more than once, clients that order or dedupe bysequence_numbersee the sequence reset mid-stream, and clients keyed byoutput_indexcan overwrite prior round output when later rounds reuse the same upstream indexes.Validation
Ran the following locally from
agent/issue-119-gateway-accumulator-verified:cargo fmt --checkcargo test -p agentic-server-core --test web_search_tool_test multi_round_stream_has_single_lifecycle_and_monotonic_public_sequencecargo test -p agentic-server-core --test web_search_tool_testcargo clippy -p agentic-server-core --all-targets -- -D warningscargo test -p agentic-server-corecargo clippy --all-targets -- -D warningscargo testuvx pre-commit run --all-filesResults:
web_search_tool_test: 18 passedagentic-server-core: 337 passed, 1 ignoredNotes
This was previously explored in draft PR #129 and cancelled because it was considered too narrow relative to #121. This PR keeps the #119 behavior fix focused, but includes the regression coverage needed to prove the multi-round public stream invariants. It also updates the revived test for the current
RequestPayload.cache_saltfield added onmain.