Skip to content

Add "addAttachment" support to StreamingResponses to support appending documents and adaptive cards. #632

Description

@MattB-msft

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:

Attachments = [];

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

  1. AddAttachment_IncludedInFinalMessage — Verifies a single attachment is present in the final activity.
  2. 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

  1. 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)
}
  1. Update reset() method to clear attachments:
// Add to the existing reset() method:
this._attachments = undefined
  1. Update createFinalMessage() — No change needed; the existing line activity.attachments = this._attachments already handles the collection correctly.

  2. 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

  1. 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)
  1. Add a reset() method (if not already present) or update existing reset logic to clear attachments:
self._attachments = None
  1. 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

  1. Incremental vs. bulk — The AddAttachment method is additive. It does not replace existing attachments. This matches the pattern used by AddCitation.
  2. 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.
  3. Merge with FinalMessage — If FinalMessage is set and already contains attachments, the new attachments are appended rather than replacing them.
  4. Reset semanticsResetAsync() clears the attachment collection to prevent state leaking across multiple streaming sequences within a single turn.

References

Metadata

Metadata

Labels

TriageNew issue, yet to be triaged
No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions