Support resetting response streams#495
Open
heyitsaamir wants to merge 10 commits into
Open
Conversation
Add a reset option to stream close so handlers can finalize one streamed message and reuse the same stream instance for a later streamed message. Update the stream example with a multi-stream flow and cover the lifecycle in HttpStream tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include the formatted-messaging workspace member and refreshed dependency metadata in uv.lock. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a reset option to the streaming close lifecycle so a handler can finalize one streamed response, send a non-streamed message (or card), and then reuse the same stream instance for a later streamed segment—addressing the “multiple streams in one handler” scenario without reaching into private context internals.
Changes:
- Extend
StreamerProtocol.close/HttpStream.closewithreset: bool = Falseto allow reusing a stream instance after finalizing a streamed message. - Switch
HttpStream.on_closeto persistent subscriptions so close handlers can fire for each finalized streamed message. - Update the streaming example to demonstrate a “multi-stream” flow and add a regression test for reset + reuse.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Adds missing workspace member metadata and refreshes dependency metadata. |
packages/apps/tests/test_http_stream.py |
Adds coverage for closing with reset=True and reusing the same stream instance. |
packages/apps/src/microsoft_teams/apps/plugins/streamer.py |
Updates the public StreamerProtocol API/docs to include close(reset=...). |
packages/apps/src/microsoft_teams/apps/http_stream.py |
Implements close(reset=...) semantics and allows multiple close events via .on("close", ...). |
examples/stream/src/main.py |
Demonstrates interleaving two stream segments with a normal message in between. |
examples/stream/README.md |
Documents how to run the multi-stream demo. |
Add a standalone simple-card path to the stream example for validating Adaptive Card delivery independently of streaming reset behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the multi-stream stream example to send the simple Adaptive Card between streamed response segments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the multi-stream example to emit the Adaptive Card through ctx.stream before close(reset=True), so the card is part of the first stream's final message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make close finalize the current streamed message while a later emit or update starts a new stream cycle on the same instance. Keep repeated close calls idempotent until another emit occurs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lilyydu
reviewed
Jul 2, 2026
| def create_simple_card() -> AdaptiveCard: | ||
| return AdaptiveCard.model_validate( | ||
| { | ||
| "type": "AdaptiveCard", |
Collaborator
There was a problem hiding this comment.
nit: can we use the builder methods instead?
Rename the per-message reset helper and add a separate helper for preparing the stream instance for the next stream cycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prefer the HttpStream.closed property where the code is checking whether the current stream cycle has been finalized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align protocol documentation and example text with the emit-after-close stream cycle behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace AdaptiveCard.model_validate in the stream example with the builder-style AdaptiveCard and TextBlock classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HttpStreamreusable after close: a lateremit()orupdate()starts a new streamed message cycle on the same stream instanceclose()calls idempotent until another emit/update occursmulti-streamflow that emits an Adaptive Card in the first stream final message, closes, then reusesctx.streamfor a second streamed segmentuv.lockto include the missingformatted-messagingworkspace member and dependency metadataContext
This addresses the scenario in #489 where a handler needs to:
Before this change, the only way to do that was to close
ctx.streamand then reach into the privatectx._activity_sender.create_stream(...)API to create another stream.Motivation
This is useful for conversational AI and tool-using bot flows where a response is not always one uninterrupted text stream.
For example, a bot may need to stream an explanation while it reasons or searches, include a structured checkpoint message, approval prompt, or card, and then continue streaming the final answer after that intermediate interaction.
Without a public lifecycle API, developers either have to avoid streaming for these richer flows or access private SDK internals to create a fresh stream. Reopening the stream on the next emit gives them a supported way to split one handler's response into multiple sequential streamed messages while keeping the SDK's one-stream-object model.
Alternatives considered
ctx.new_stream()We first considered adding a public
ctx.new_stream()method that would create and return another stream object.That solves the private API issue, but it makes the model feel like a handler can manage multiple stream objects at once. In practice, Teams appears to behave more reliably when there is only one active stream at a time. Multiple stream objects also complicate ownership, finalization, event handler registration, and retry behavior.
Context-managed multiple streams
We also considered having
ActivityContexttrack all streams created during a handler and close them all at the end.That keeps cleanup centralized, but it still encourages multiple open streams during one handler. It also makes ordering less explicit: a stream could remain active while a normal message or a second stream is sent, which is exactly the behavior that seemed risky.
close(reset=True)We also tried an explicit
close(reset=True)option. It worked, but it made the API more awkward and introduced a flag for what is conceptually the next stream cycle.The approach in this PR keeps
close()simple: it finalizes the current streamed message. Repeatedclose()calls remain idempotent. If the caller later emits or updates, the stream starts a new cycle automatically.Example
The stream example now includes a
multi-streampath:Screen.Recording.2026-07-01.at.6.08.51.PM.mov
This demonstrates the intended lifecycle: one streamed message is finalized, and the same
ctx.streaminstance is reused for the next streamed message when the handler emits again.Addresses #489