Skip to content

fix(core): only end a stream that was actually started - #1016

Open
sharpca wants to merge 1 commit into
apache:mainfrom
sharpca:fix/999-astream-result-post-end-stream
Open

sharpca wants to merge 1 commit into
apache:mainfrom
sharpca:fix/999-astream-result-post-end-stream

Conversation

@sharpca

@sharpca sharpca commented Nov 1, 2026

Copy link
Copy Markdown

Why are these changes needed?

Application.astream_result() ends a stream in two places: the success path and the exception handler. The exception handler called post_end_stream unconditionally, but pre_start_stream only runs for streaming actions. A non-streaming action that raises therefore fans out post_end_stream for a stream that was never started.

Adapters that keep per-stream state blow up there. LocalTrackingClient.post_end_stream looks the stream up in stream_state -- which only pre_start_stream populates -- so it raised KeyError from inside the handler and replaced the action's own exception with an unrelated one. Because the handler runs while the original exception is in flight, users saw a bare KeyError with no trace of the real failure, even when their application code raised something perfectly readable.

This tracks whether pre_start_stream completed and only fans out post_end_stream when it did. Streams that did start are cleaned up exactly as before, so stream_state bookkeeping is unchanged for the streaming path.

Fixes #999

Related issue number

#999

Checks

  • I've successfully run make lint && make test (see notes below on the local equivalent)
  • I've added tests where applicable.
  • For core features, I have updated the documentation.
  • For user-impacting features, I have updated the changelog.

Verification details:

  • tests/core/test_application.py -- 143 passed
  • tests/tracking/test_local_tracking_client.py -- 17 passed, 1 skipped
  • tests/core -- 426 passed; the 9 failures are all in test_graphviz_display.py and come from the optional graphviz package being absent in this environment, unrelated to this change
  • black --line-length 100 --check passes

Two regression tests were added:

  • test_astream_result_non_streaming_exception_skips_post_end_stream (core): asserts the lifecycle hook is not invoked when the action is not streaming
  • test_astream_result_preserves_non_streaming_action_exception (tracking): asserts the action's ValueError reaches the caller with the built-in local tracker attached

A non-streaming action halted through ``astream_result`` never starts a
stream, so ``pre_start_stream`` is not called for it. The exception handler
nevertheless called ``post_end_stream`` unconditionally. Adapters that keep
per-stream state -- such as the local tracking client, whose
``post_end_stream`` looks the entry up in ``stream_state`` -- raised a
``KeyError`` from inside the handler, replacing the action's exception with
an unrelated one.

Track whether ``pre_start_stream`` completed and fan out ``post_end_stream``
only for streams that actually started. Cleanup for streams that did start
is unchanged.

Fixes apache#999
@YongGoose

Copy link
Copy Markdown
Member

This makes the same source change as #1000, which was opened on Oct 27 for the same issue (#999): a stream_started flag set after pre_start_stream and checked before post_end_stream.

I applied both on current main (36e3ff3) in a Linux container with Python 3.12. Both fix the repro from #999, and tests/core and tests/tracking pass with either. #1000 also tests that a stream that did start still gets its post_end_stream. It may be worth agreeing on one of the two so review time isn't split.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Application, State, Graph, Actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

astream_result() masks action exceptions with a tracker KeyError when the action is non-streaming

2 participants