Skip to content

Support resetting response streams#495

Merged
heyitsaamir merged 12 commits into
microsoft:mainfrom
heyitsaamir:heyitsaamir-prototype-response-streams
Jul 7, 2026
Merged

Support resetting response streams#495
heyitsaamir merged 12 commits into
microsoft:mainfrom
heyitsaamir:heyitsaamir-prototype-response-streams

Conversation

@heyitsaamir

@heyitsaamir heyitsaamir commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • make HttpStream reusable after close: a later emit() or update() starts a new streamed message cycle on the same stream instance
  • keep repeated close() calls idempotent until another emit/update occurs
  • update the stream example with a multi-stream flow that emits an Adaptive Card in the first stream final message, closes, then reuses ctx.stream for a second streamed segment
  • add coverage for close idempotency followed by emit-driven stream reopening
  • refresh uv.lock to include the missing formatted-messaging workspace member and dependency metadata

Context

This addresses the scenario in #489 where a handler needs to:

  1. stream part of a response,
  2. include a normal message or card in the flow,
  3. continue with another streamed response.

Before this change, the only way to do that was to close ctx.stream and then reach into the private ctx._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 ActivityContext track 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. Repeated close() 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-stream path:

Screen.Recording.2026-07-01.at.6.08.51.PM.mov
ctx.stream.emit("[stream 1] Starting the first streamed response.")
ctx.stream.emit(MessageActivityInput(text="...").add_card(card))
await ctx.stream.close()

ctx.stream.emit("[stream 2] Reusing ctx.stream after close().")

This demonstrates the intended lifecycle: one streamed message is finalized, and the same ctx.stream instance is reused for the next streamed message when the handler emits again.

Addresses #489

heyitsaamir and others added 2 commits July 1, 2026 18:04
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 <[email protected]>
Include the formatted-messaging workspace member and refreshed dependency metadata in uv.lock.

Co-authored-by: Copilot <[email protected]>
@heyitsaamir
heyitsaamir marked this pull request as ready for review July 2, 2026 01:05
Copilot AI review requested due to automatic review settings July 2, 2026 01:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.close with reset: bool = False to allow reusing a stream instance after finalizing a streamed message.
  • Switch HttpStream.on_close to 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.

Comment thread packages/apps/src/microsoft_teams/apps/plugins/streamer.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/plugins/streamer.py
Comment thread packages/apps/tests/test_http_stream.py
heyitsaamir and others added 4 commits July 1, 2026 18:10
Add a standalone simple-card path to the stream example for validating Adaptive Card delivery independently of streaming reset behavior.

Co-authored-by: Copilot <[email protected]>
Update the multi-stream stream example to send the simple Adaptive Card between streamed response segments.

Co-authored-by: Copilot <[email protected]>
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 <[email protected]>
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 <[email protected]>
Comment thread examples/stream/src/main.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/http_stream.py Outdated
heyitsaamir and others added 4 commits July 2, 2026 16:06
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 <[email protected]>
Prefer the HttpStream.closed property where the code is checking whether the current stream cycle has been finalized.

Co-authored-by: Copilot <[email protected]>
Align protocol documentation and example text with the emit-after-close stream cycle behavior.

Co-authored-by: Copilot <[email protected]>
Replace AdaptiveCard.model_validate in the stream example with the builder-style AdaptiveCard and TextBlock classes.

Co-authored-by: Copilot <[email protected]>
@heyitsaamir
heyitsaamir requested a review from lilyydu July 6, 2026 17:09
Comment thread packages/apps/tests/test_http_stream.py Outdated

@rajan-chari rajan-chari left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. My comment is a clarification question.

…-response-streams

# Conflicts:
#	packages/apps/src/microsoft_teams/apps/http_stream.py
Wrap test close-event waits with explicit timeouts so the test fails clearly instead of hanging if the close event is not emitted.

Co-authored-by: Copilot <[email protected]>
pull Bot pushed a commit to Mattlk13/teams.ts that referenced this pull request Jul 9, 2026
…ndling (microsoft#635)

## Summary

Ports two merged **teams.py** PRs to TypeScript:

- microsoft/teams.py#453 — streaming error handling for the 2-minute
timeout & "content stream not allowed"
- microsoft/teams.py#495 — support resetting response streams (reuse
after `close()`)

## Streaming error handling (port of microsoft#453)

- Add `TerminalStreamError`, `StreamTimedOutError`, and
`StreamNotAllowedError` (in `types/streamer.ts`).
- `HttpStream.send()` now inspects the `403` response body and maps it
to the right error instead of always treating a `403` as canceled:
  - `exceeded streaming time` → `StreamTimedOutError` (marks `timedOut`)
  - `cancel` → `StreamCancelledError`
  - `not allowed` → `StreamNotAllowedError`
  - anything else / empty body → `TerminalStreamError`
- On a 2-minute streaming timeout, the stream finalizes by **updating
the original message in place** — it drops the `streaminfo` entity and
stream `channelData` so the send routes through *update* (reusing the
id) rather than posting a duplicate.
- Chunk sends swallow a timeout mid-stream; `close()` then sends the
buffered content as a plain final message.
- `retry` gains a `nonRetryable` option so terminal stream errors are
not retried.

## Reusable streams (port of microsoft#495)

- Emitting or updating after `close()` reopens the stream on the **same
instance**, starting a new streamed message.
- `close()` is idempotent until the next emit/update.
- `app.process.ts` close listener switched from `once` → `on` so each
reused-stream close fires.

## Example

Adds `examples/stream`, mirroring the teams.py example's final state:

- default message → single-stream demo with suggested actions
- `simple-card` → sends a minimal Adaptive Card outside the streaming
flow
- `multi-stream` → emits an Adaptive Card as stream 1's final message,
`close()`s, then reuses `ctx.stream` for a second streamed response

## Tests

- `packages/apps` http-stream spec extended: 403 message→error mapping
(parametrized), empty-body 403 → terminal error, final-send timeout
updates in place, mid-stream timeout updates in place, and
emit-after-close stream reuse.
- Full `packages/apps` suite passes (294 tests); `tsc` and eslint clean
across the changed library files and the new example.

## Notes

- The Python-only `uv.lock` change was not ported.
- `examples/stream/.env` is gitignored (no credentials committed).

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[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.

4 participants