Spec: AddAttachment Method for IStreamingResponse
Abstract
This specification describes the addition of an AddAttachment method to the IStreamingResponse interface in the Microsoft 365 Agents SDK. The method enables developers to attach files, cards, and other rich content to the final message of a streaming response. Attachments are accumulated during the streaming session and included only in the terminal Message activity sent when EndStreamAsync() is called — they are never included in intermediate Typing activities.
This feature was implemented in C# as part of PR #878 on the users/mbarbour/add_AddAttachmentToStreamingContract branch. This document details the C# implementation and provides guidance for implementing equivalent functionality in the TypeScript and Python SDKs.
Motivation
Streaming responses allow agents to progressively deliver text to users, providing a responsive UX. However, there was no built-in mechanism to include attachments (Adaptive Cards, images, files, etc.) in the final streamed message. Developers had to work around this by sending a separate follow-up activity after the stream ended, which resulted in a disjointed user experience.
The AddAttachment method solves this by allowing attachments to be queued during streaming and automatically included in the final message activity.
C# Implementation (Reference)
Interface Change: IStreamingResponse
File: src/libraries/Builder/Microsoft.Agents.Builder/IStreamingResponse.cs
/// <summary>
/// Adds an attachment to the collection of attachments for the final message.
/// </summary>
/// <param name="attachment">The attachment to add. Must not be <see langword="null"/>.</param>
void AddAttachment(Attachment attachment);
Implementation: StreamingResponse
File: src/libraries/Builder/Microsoft.Agents.Builder/StreamingResponse.cs
New Property
/// <summary>
/// Attachments to be included in the final message. This is only used for the final message,
/// and not for intermediate messages.
/// </summary>
public List<Attachment>? Attachments { get; set; } = [];
New Method
/// <summary>
/// Adds an attachment to the collection of attachments for the final message.
/// </summary>
/// <param name="attachment">The attachment to add. Must not be <see langword="null"/>.</param>
public void AddAttachment(Attachment attachment)
{
AssertionHelpers.ThrowIfNull(attachment, nameof(attachment));
Attachments ??= [];
Attachments.Add(attachment);
}
Updated CreateFinalMessage()
After existing entity/citation logic, attachments are merged into the final activity:
// Add Attachments if there are any
if (Attachments != null && Attachments.Count > 0)
{
if (activity.Attachments == null)
{
activity.Attachments = Attachments;
}
else if (!ReferenceEquals(activity.Attachments, Attachments))
{
foreach (var attachment in Attachments)
{
activity.Attachments.Add(attachment);
}
}
}
Updated ResetAsync()
The Attachments collection is cleared on reset:
Behavior Summary
| Scenario |
Behavior |
AddAttachment() called before EndStreamAsync() |
Attachment included in final Message activity |
AddAttachment() during intermediate streaming |
No effect on intermediate Typing activities |
FinalMessage already has attachments |
New attachments are appended (not replaced) |
FinalMessage is null |
Attachments are set directly on the synthesized activity |
After ResetAsync() |
Attachments collection is cleared |
| Null attachment passed |
Throws ArgumentNullException |
Test Coverage
File: src/tests/Microsoft.Agents.Builder.Tests/StreamingResponseTests.cs
AddAttachment_IncludedInFinalMessage — Verifies a single attachment is present in the final activity.
- Reset test — Verifies attachments are cleared after
ResetAsync() and not present in subsequent final messages.
TypeScript Implementation Guidance
Target File: packages/agents-hosting/src/app/streaming/streamingResponse.ts
Reference Commit: c43479e
Current State
The TypeScript StreamingResponse class already has:
private _attachments?: Attachment[] field
public setAttachments(attachments: Attachment[]): void — a bulk setter that replaces the entire array
The createFinalMessage() method already applies activity.attachments = this._attachments.
Required Changes
- Add
addAttachment method (incremental add, complementing the existing bulk setAttachments):
/**
* Adds an attachment to the collection of attachments for the final message.
*
* @param attachment The attachment to add.
* @throws Error if attachment is null or undefined.
*/
public addAttachment(attachment: Attachment): void {
if (!attachment) {
throw new Error('attachment cannot be null or undefined.')
}
if (!this._attachments) {
this._attachments = []
}
this._attachments.push(attachment)
}
- Update
reset() method to clear attachments:
// Add to the existing reset() method:
this._attachments = undefined
-
Update createFinalMessage() — No change needed; the existing line activity.attachments = this._attachments already handles the collection correctly.
-
Export from interface (if an IStreamingResponse interface is added in future):
addAttachment(attachment: Attachment): void
Test Requirements
- Verify
addAttachment adds to the collection incrementally.
- Verify the attachment appears in the final activity from
endStream().
- Verify
reset() clears attachments.
- Verify calling
addAttachment with null/undefined throws.
Python Implementation Guidance
Target File: libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py
Reference Commit: 479eb38
Current State
The Python StreamingResponse class already has:
self._attachments: Optional[list[Attachment]] = None field
def set_attachments(self, attachments: list[Attachment]) -> None — a bulk setter
The _queue_next_chunk() method applies attachments=self._attachments or [] in the final activity.
Required Changes
- Add
add_attachment method:
def add_attachment(self, attachment: Attachment) -> None:
"""
Adds an attachment to the collection of attachments for the final message.
Args:
attachment: The attachment to add. Must not be None.
Raises:
ValueError: If attachment is None.
"""
if attachment is None:
raise ValueError("attachment cannot be None")
if self._attachments is None:
self._attachments = []
self._attachments.append(attachment)
- Add a
reset() method (if not already present) or update existing reset logic to clear attachments:
- No change to
_queue_next_chunk() — The existing code already includes self._attachments in the final activity.
Test Requirements
- Verify
add_attachment appends to the internal list.
- Verify the attachment appears in the final activity after
end_stream().
- Verify reset clears the attachments list.
- Verify calling
add_attachment(None) raises ValueError.
API Surface Comparison
| Feature |
C# |
TypeScript |
Python |
| Bulk set |
(via FinalMessage.Attachments) |
setAttachments(attachments) |
set_attachments(attachments) |
| Incremental add |
AddAttachment(attachment) |
addAttachment(attachment) (new) |
add_attachment(attachment) (new) |
| Null guard |
ArgumentNullException |
Error |
ValueError |
| Reset clears |
✅ |
✅ (new) |
✅ (new) |
| Applies to final only |
✅ |
✅ |
✅ |
Design Decisions
- Incremental vs. bulk — The
AddAttachment method is additive. It does not replace existing attachments. This matches the pattern used by AddCitation.
- Final message only — Attachments are excluded from intermediate
Typing activities because channels may not render them correctly in partial updates and it would increase payload size unnecessarily.
- Merge with FinalMessage — If
FinalMessage is set and already contains attachments, the new attachments are appended rather than replacing them.
- Reset semantics —
ResetAsync() clears the attachment collection to prevent state leaking across multiple streaming sequences within a single turn.
References
Spec: AddAttachment Method for IStreamingResponse
Abstract
This specification describes the addition of an
AddAttachmentmethod to theIStreamingResponseinterface in the Microsoft 365 Agents SDK. The method enables developers to attach files, cards, and other rich content to the final message of a streaming response. Attachments are accumulated during the streaming session and included only in the terminalMessageactivity sent whenEndStreamAsync()is called — they are never included in intermediateTypingactivities.This feature was implemented in C# as part of PR #878 on the
users/mbarbour/add_AddAttachmentToStreamingContractbranch. This document details the C# implementation and provides guidance for implementing equivalent functionality in the TypeScript and Python SDKs.Motivation
Streaming responses allow agents to progressively deliver text to users, providing a responsive UX. However, there was no built-in mechanism to include attachments (Adaptive Cards, images, files, etc.) in the final streamed message. Developers had to work around this by sending a separate follow-up activity after the stream ended, which resulted in a disjointed user experience.
The
AddAttachmentmethod solves this by allowing attachments to be queued during streaming and automatically included in the final message activity.C# Implementation (Reference)
Interface Change:
IStreamingResponseFile:
src/libraries/Builder/Microsoft.Agents.Builder/IStreamingResponse.csImplementation:
StreamingResponseFile:
src/libraries/Builder/Microsoft.Agents.Builder/StreamingResponse.csNew Property
New Method
Updated
CreateFinalMessage()After existing entity/citation logic, attachments are merged into the final activity:
Updated
ResetAsync()The
Attachmentscollection is cleared on reset:Behavior Summary
AddAttachment()called beforeEndStreamAsync()MessageactivityAddAttachment()during intermediate streamingTypingactivitiesFinalMessagealready has attachmentsFinalMessageis nullResetAsync()ArgumentNullExceptionTest Coverage
File:
src/tests/Microsoft.Agents.Builder.Tests/StreamingResponseTests.csAddAttachment_IncludedInFinalMessage— Verifies a single attachment is present in the final activity.ResetAsync()and not present in subsequent final messages.TypeScript Implementation Guidance
Target File:
packages/agents-hosting/src/app/streaming/streamingResponse.tsReference Commit:
c43479eCurrent State
The TypeScript
StreamingResponseclass already has:private _attachments?: Attachment[]fieldpublic setAttachments(attachments: Attachment[]): void— a bulk setter that replaces the entire arrayThe
createFinalMessage()method already appliesactivity.attachments = this._attachments.Required Changes
addAttachmentmethod (incremental add, complementing the existing bulksetAttachments):reset()method to clear attachments:Update
createFinalMessage()— No change needed; the existing lineactivity.attachments = this._attachmentsalready handles the collection correctly.Export from interface (if an
IStreamingResponseinterface is added in future):Test Requirements
addAttachmentadds to the collection incrementally.endStream().reset()clears attachments.addAttachmentwithnull/undefinedthrows.Python Implementation Guidance
Target File:
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.pyReference Commit:
479eb38Current State
The Python
StreamingResponseclass already has:self._attachments: Optional[list[Attachment]] = Nonefielddef set_attachments(self, attachments: list[Attachment]) -> None— a bulk setterThe
_queue_next_chunk()method appliesattachments=self._attachments or []in the final activity.Required Changes
add_attachmentmethod:reset()method (if not already present) or update existing reset logic to clear attachments:_queue_next_chunk()— The existing code already includesself._attachmentsin the final activity.Test Requirements
add_attachmentappends to the internal list.end_stream().add_attachment(None)raisesValueError.API Surface Comparison
FinalMessage.Attachments)setAttachments(attachments)set_attachments(attachments)AddAttachment(attachment)addAttachment(attachment)(new)add_attachment(attachment)(new)ArgumentNullExceptionErrorValueErrorDesign Decisions
AddAttachmentmethod is additive. It does not replace existing attachments. This matches the pattern used byAddCitation.Typingactivities because channels may not render them correctly in partial updates and it would increase payload size unnecessarily.FinalMessageis set and already contains attachments, the new attachments are appended rather than replacing them.ResetAsync()clears the attachment collection to prevent state leaking across multiple streaming sequences within a single turn.References