Skip to content

Implement stream cancelation#337

Merged
heyitsaamir merged 6 commits into
microsoft:mainfrom
jerbob92:feature/implement-stream-cancelation-332
Apr 8, 2026
Merged

Implement stream cancelation#337
heyitsaamir merged 6 commits into
microsoft:mainfrom
jerbob92:feature/implement-stream-cancelation-332

Conversation

@jerbob92

Copy link
Copy Markdown
Contributor

Fixes #332

@jerbob92

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree company="Klippa App B.V."

@lilyydu lilyydu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for filing this!

Comment thread packages/apps/src/microsoft_teams/apps/http_stream.py Outdated
@lilyydu

lilyydu commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

looks like the build is failing, could you also run ruff check and ruff format in your next commit?

@jerbob92

jerbob92 commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

looks like the build is failing, could you also run ruff check and ruff format in your next commit?

Yeah I wanted to setup pre-commit, but I couldn't get it to install, had some issues with the dotnet tool nbgv.

@lilyydu lilyydu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! @heyitsaamir do you mind taking a look?

@heyitsaamir

Copy link
Copy Markdown
Collaborator

I like the approach here. Soemthings I think we should do:

  1. Emit should be a no-op if the stream is cancelled.
  2. We potentially need a way to short-circuit the AI generation if emit is short-circuiting. One approach is to throw on emit if stream is cancelled. But then we'd break all existing handlers. Another approach is for the handlers to have access to this signal, and choose to short-circuit themselves.

I'm leaning toward throwing on emit.

Thoughts?

@jerbob92

jerbob92 commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author
  • Emit should be a no-op if the stream is cancelled.

What do you mean? It already is.

  • We potentially need a way to short-circuit the AI generation if emit is short-circuiting. One approach is to throw on emit if stream is cancelled. But then we'd break all existing handlers. Another approach is for the handlers to have access to this signal, and choose to short-circuit themselves.

I'd like the second approach more. Perhaps something using yield? (I'm not really a Python dev so not sure)

Edit:
Nvm, I see there is some EventEmitter system in place already, better to use that.

@heyitsaamir

Copy link
Copy Markdown
Collaborator
  • Emit should be a no-op if the stream is cancelled.

What do you mean? It already is.

  • We potentially need a way to short-circuit the AI generation if emit is short-circuiting. One approach is to throw on emit if stream is cancelled. But then we'd break all existing handlers. Another approach is for the handlers to have access to this signal, and choose to short-circuit themselves.

I'd like the second approach more. Perhaps something using yield? (I'm not really a Python dev so not sure)

Edit: Nvm, I see there is some EventEmitter system in place already, better to use that.

Oops you're right. Ignore 1st.

@jerbob92 why do you prefer second? I think your prompt.send throwing if the stream gets cancelled is ok.

@jerbob92

jerbob92 commented Apr 3, 2026

Copy link
Copy Markdown
Contributor Author
  • Emit should be a no-op if the stream is cancelled.

What do you mean? It already is.

  • We potentially need a way to short-circuit the AI generation if emit is short-circuiting. One approach is to throw on emit if stream is cancelled. But then we'd break all existing handlers. Another approach is for the handlers to have access to this signal, and choose to short-circuit themselves.

I'd like the second approach more. Perhaps something using yield? (I'm not really a Python dev so not sure)
Edit: Nvm, I see there is some EventEmitter system in place already, better to use that.

Oops you're right. Ignore 1st.

@jerbob92 why do you prefer second? I think your prompt.send throwing if the stream gets cancelled is ok.

Personally I think an event based approach is cleaner, but maybe that would only really make sense if the cancelation may happen without the user doing emits, for that to work the Streamer would either need an endpoint to check for cancelation every x duration or it needs to be an actual HTTP stream. (or maybe I just didn't really get your comment and you're talking about something that's not directly related to the MR)

@lilyydu

lilyydu commented Apr 3, 2026

Copy link
Copy Markdown
Collaborator

I like the approach here. Soemthings I think we should do:

  1. Emit should be a no-op if the stream is cancelled.
  2. We potentially need a way to short-circuit the AI generation if emit is short-circuiting. One approach is to throw on emit if stream is cancelled. But then we'd break all existing handlers. Another approach is for the handlers to have access to this signal, and choose to short-circuit themselves.

I'm leaning toward throwing on emit.

Thoughts?

+1 with Aamir — throwing on emit seems like the right approach. I think event-based only adds value if cancellation happens between emits

@heyitsaamir

Copy link
Copy Markdown
Collaborator

Yeah event emitter is less useful here because it would catch the errors asynchronously (out of band), so coordinating it back with where you're emitting in application code will be difficult.

heyitsaamir and others added 2 commits April 7, 2026 14:51
- Add StreamCancelledError (extends asyncio.CancelledError) for
  stream-specific cancellation signaling
- emit() raises StreamCancelledError when cancelled, breaking the
  caller's AI generation loop instead of silently dropping chunks
- close() returns immediately when stream is cancelled instead of
  waiting 30s for queue to drain
- _flush() signals _state_changed in finally block so close() wakes
  up on errors
- app_process catches StreamCancelledError and returns 200 so Teams
  does not retry the request
- Remove unnecessary bare except/re-raise in _send()
- Add LOG_LEVEL env support to ai-test example

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@heyitsaamir

Copy link
Copy Markdown
Collaborator

Updated the PR with some changes:

  • Added StreamCancelledError (extends asyncio.CancelledError) — a dedicated exception for stream cancellation. emit() now raises this instead of silently returning, which breaks the caller's AI generation loop (e.g. stops the OpenAI stream mid-generation).
  • When the handler raises StreamCancelledError, the activity processor catches it and returns a 200 to Teams, preventing retries.
  • Fixed close() hanging for up to 30s after cancellation — it now returns immediately when the stream is cancelled.

Tested by:

  1. Starting a stream
  2. Stopping it
  3. Killing the service — no delay (before, there was a ~30s delay since emitting was still silently happening and close() was waiting for the queue to drain)

Then:

  1. Starting a stream
  2. Stopping it
  3. Sending another message — sent normally, no issues running the handler (before, the stream request would get retried because the exception bubbled up as a 500)

@heyitsaamir
heyitsaamir merged commit 73c4e3f into microsoft:main Apr 8, 2026
4 checks passed
heyitsaamir added a commit to microsoft/teams.ts that referenced this pull request Apr 13, 2026
## Summary
- Detect when Teams stops a stream (user presses Stop or 2-min timeout)
via 403 response and cancel immediately instead of silently retrying
until timeout
- Add `StreamCancelledError` class and `canceled` property to
`IStreamer` interface
- `HttpStream.send()` catches 403, sets `canceled` flag, and throws
`StreamCancelledError`
- `emit()` and `close()` bail out immediately when canceled
- `retry()` skips retries for `StreamCancelledError` (no point retrying
a permanent cancellation)
- `flush()` suppresses error log for expected cancellation
- `app.process` catches `StreamCancelledError` and returns 200 so Teams
doesn't retry

Ports [teams.py#337](microsoft/teams.py#337) to
TypeScript.

To test, I ran a streaming bot, press Stop mid-stream — verify no held
connection, verify future messages weren't impacted, verified that
stream didn't restart b/c of a 500.

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: HttpStream doesn't handle canceling of stream

3 participants