diff --git a/.github/actions/azure-functions-integration-setup/action.yml b/.github/actions/azure-functions-integration-setup/action.yml
index 6be5afb814..357168d92e 100644
--- a/.github/actions/azure-functions-integration-setup/action.yml
+++ b/.github/actions/azure-functions-integration-setup/action.yml
@@ -28,6 +28,18 @@ runs:
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
echo "Azurite (Azure Storage emulator) is ready"
+ - name: Start Redis
+ shell: bash
+ run: |
+ if [ "$(docker ps -aq -f name=redis)" ]; then
+ echo "Stopping and removing existing Redis"
+ docker rm -f redis
+ fi
+ echo "Starting Redis"
+ docker run -d --name redis -p 6379:6379 redis:latest
+ echo "Waiting for Redis to be ready"
+ timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
+ echo "Redis is ready"
- name: Install Azure Functions Core Tools
shell: bash
run: |
diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml
index 66b9122726..eb4f123f42 100644
--- a/.github/workflows/python-merge-tests.yml
+++ b/.github/workflows/python-merge-tests.yml
@@ -154,7 +154,7 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 10
- run: uv run poe azure-ai-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
+ run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
diff --git a/agent-samples/README.md b/agent-samples/README.md
index 0ee940f3a0..ea5c8b0aeb 100644
--- a/agent-samples/README.md
+++ b/agent-samples/README.md
@@ -1,3 +1,3 @@
# Declarative Agents
-This folder contains sample agent definitions than be ran using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
+This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 4825a42921..9202b36f2a 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -11,13 +11,13 @@
-
+
-
+
@@ -100,7 +100,7 @@
-
+
@@ -112,19 +112,21 @@
-
-
-
-
+
+
+
+
-
-
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 5e08a766f9..002efdbab1 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -33,6 +33,7 @@
+
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index 6ae60933c6..3fc7c3974b 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -2,9 +2,9 @@
1.0.0
- $(VersionPrefix)-$(VersionSuffix).251204.1
- $(VersionPrefix)-preview.251204.1
- 1.0.0-preview.251204.1
+ $(VersionPrefix)-$(VersionSuffix).251219.1
+ $(VersionPrefix)-preview.251219.1
+ 1.0.0-preview.251219.1
Debug;Release;Publish
true
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs
index 05a7d86f15..d79787d260 100644
--- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs
@@ -19,12 +19,12 @@ public AgenticUIAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOp
this._jsonSerializerOptions = jsonSerializerOptions;
}
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs
index 8ac9928fbe..ab9ca2fca3 100644
--- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs
@@ -20,12 +20,12 @@ public PredictiveStateUpdatesAgent(AIAgent innerAgent, JsonSerializerOptions jso
this._jsonSerializerOptions = jsonSerializerOptions;
}
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs
index c10450fcfb..1a1e58860a 100644
--- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs
@@ -19,12 +19,12 @@ public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializer
this._jsonSerializerOptions = jsonSerializerOptions;
}
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj b/dotnet/samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj
new file mode 100644
index 0000000000..df0b60a4a1
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj
@@ -0,0 +1,47 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ ReliableStreaming
+ ReliableStreaming
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs b/dotnet/samples/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs
new file mode 100644
index 0000000000..e642b64337
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs
@@ -0,0 +1,320 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.Logging;
+
+namespace ReliableStreaming;
+
+///
+/// HTTP trigger functions for reliable streaming of durable agent responses.
+///
+///
+/// This class exposes two endpoints:
+///
+/// -
+/// Create
+/// Starts an agent run and streams responses. The response format depends on the
+/// Accept header: text/plain returns raw text (ideal for terminals), while
+/// text/event-stream or any other value returns Server-Sent Events (SSE).
+///
+/// -
+/// Stream
+/// Resumes a stream from a cursor position, enabling reliable message delivery
+///
+///
+///
+public sealed class FunctionTriggers
+{
+ private readonly RedisStreamResponseHandler _streamHandler;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Redis stream handler for reading/writing agent responses.
+ /// The logger instance.
+ public FunctionTriggers(RedisStreamResponseHandler streamHandler, ILogger logger)
+ {
+ this._streamHandler = streamHandler;
+ this._logger = logger;
+ }
+
+ ///
+ /// Creates a new agent session, starts an agent run with the provided prompt,
+ /// and streams the response back to the client.
+ ///
+ ///
+ ///
+ /// The response format depends on the Accept header:
+ ///
+ /// - text/plain: Returns raw text output, ideal for terminal display with curl
+ /// - text/event-stream or other: Returns Server-Sent Events (SSE) with cursor support
+ ///
+ ///
+ ///
+ /// The response includes an x-conversation-id header containing the conversation ID.
+ /// For SSE responses, clients can use this conversation ID to resume the stream if disconnected
+ /// by calling the endpoint with the conversation ID and the last received cursor.
+ ///
+ ///
+ /// Each SSE event contains the following fields:
+ ///
+ /// - id: The Redis stream entry ID (use as cursor for resumption)
+ /// - event: Either "message" for content or "done" for stream completion
+ /// - data: The text content of the response chunk
+ ///
+ ///
+ ///
+ /// The HTTP request containing the prompt in the body.
+ /// The Durable Task client for signaling agents.
+ /// The function invocation context.
+ /// Cancellation token.
+ /// A streaming response in the format specified by the Accept header.
+ [Function(nameof(CreateAsync))]
+ public async Task CreateAsync(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/create")] HttpRequest request,
+ [DurableClient] DurableTaskClient durableClient,
+ FunctionContext context,
+ CancellationToken cancellationToken)
+ {
+ // Read the prompt from the request body
+ string prompt = await new StreamReader(request.Body).ReadToEndAsync(cancellationToken);
+ if (string.IsNullOrWhiteSpace(prompt))
+ {
+ return new BadRequestObjectResult("Request body must contain a prompt.");
+ }
+
+ AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
+
+ // Create a new agent thread
+ AgentThread thread = agentProxy.GetNewThread();
+ AgentThreadMetadata metadata = thread.GetService()
+ ?? throw new InvalidOperationException("Failed to get AgentThreadMetadata from new thread.");
+
+ this._logger.LogInformation("Creating new agent session: {ConversationId}", metadata.ConversationId);
+
+ // Run the agent in the background (fire-and-forget)
+ DurableAgentRunOptions options = new() { IsFireAndForget = true };
+ await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
+
+ this._logger.LogInformation("Agent run started for session: {ConversationId}", metadata.ConversationId);
+
+ // Check Accept header to determine response format
+ // text/plain = raw text output (ideal for terminals)
+ // text/event-stream or other = SSE format (supports resumption)
+ string? acceptHeader = request.Headers.Accept.FirstOrDefault();
+ bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
+
+ return await this.StreamToClientAsync(
+ conversationId: metadata.ConversationId!, cursor: null, useSseFormat, request.HttpContext, cancellationToken);
+ }
+
+ ///
+ /// Resumes streaming from a specific cursor position for an existing session.
+ ///
+ ///
+ ///
+ /// Use this endpoint to resume a stream after disconnection. Pass the conversation ID
+ /// (from the x-conversation-id response header) and the last received cursor
+ /// (Redis stream entry ID) to continue from where you left off.
+ ///
+ ///
+ /// If no cursor is provided, streaming starts from the beginning of the stream.
+ /// This allows clients to replay the entire response if needed.
+ ///
+ ///
+ /// The response format depends on the Accept header:
+ ///
+ /// - text/plain: Returns raw text output, ideal for terminal display with curl
+ /// - text/event-stream or other: Returns Server-Sent Events (SSE) with cursor support
+ ///
+ ///
+ ///
+ /// The HTTP request. Use the cursor query parameter to specify the cursor position.
+ /// The conversation ID to stream from.
+ /// Cancellation token.
+ /// A streaming response in the format specified by the Accept header.
+ [Function(nameof(StreamAsync))]
+ public async Task StreamAsync(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "agent/stream/{conversationId}")] HttpRequest request,
+ string conversationId,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(conversationId))
+ {
+ return new BadRequestObjectResult("Conversation ID is required.");
+ }
+
+ // Get the cursor from query string (optional)
+ string? cursor = request.Query["cursor"].FirstOrDefault();
+
+ this._logger.LogInformation(
+ "Resuming stream for conversation {ConversationId} from cursor: {Cursor}",
+ conversationId,
+ cursor ?? "(beginning)");
+
+ // Check Accept header to determine response format
+ // text/plain = raw text output (ideal for terminals)
+ // text/event-stream or other = SSE format (supports cursor-based resumption)
+ string? acceptHeader = request.Headers.Accept.FirstOrDefault();
+ bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
+
+ return await this.StreamToClientAsync(conversationId, cursor, useSseFormat, request.HttpContext, cancellationToken);
+ }
+
+ ///
+ /// Streams chunks from the Redis stream to the HTTP response.
+ ///
+ /// The conversation ID to stream from.
+ /// Optional cursor to resume from. If null, streams from the beginning.
+ /// True to use SSE format, false for plain text.
+ /// The HTTP context for writing the response.
+ /// Cancellation token.
+ /// An empty result after streaming completes.
+ private async Task StreamToClientAsync(
+ string conversationId,
+ string? cursor,
+ bool useSseFormat,
+ HttpContext httpContext,
+ CancellationToken cancellationToken)
+ {
+ // Set response headers based on format
+ httpContext.Response.Headers.ContentType = useSseFormat
+ ? "text/event-stream"
+ : "text/plain; charset=utf-8";
+ httpContext.Response.Headers.CacheControl = "no-cache";
+ httpContext.Response.Headers.Connection = "keep-alive";
+ httpContext.Response.Headers["x-conversation-id"] = conversationId;
+
+ // Disable response buffering if supported
+ httpContext.Features.Get()?.DisableBuffering();
+
+ try
+ {
+ await foreach (StreamChunk chunk in this._streamHandler.ReadStreamAsync(
+ conversationId,
+ cursor,
+ cancellationToken))
+ {
+ if (chunk.Error != null)
+ {
+ this._logger.LogWarning("Stream error for conversation {ConversationId}: {Error}", conversationId, chunk.Error);
+ await WriteErrorAsync(httpContext.Response, chunk.Error, useSseFormat, cancellationToken);
+ break;
+ }
+
+ if (chunk.IsDone)
+ {
+ await WriteEndOfStreamAsync(httpContext.Response, chunk.EntryId, useSseFormat, cancellationToken);
+ break;
+ }
+
+ if (chunk.Text != null)
+ {
+ await WriteChunkAsync(httpContext.Response, chunk, useSseFormat, cancellationToken);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ this._logger.LogInformation("Client disconnected from stream {ConversationId}", conversationId);
+ }
+
+ return new EmptyResult();
+ }
+
+ ///
+ /// Writes a text chunk to the response.
+ ///
+ private static async Task WriteChunkAsync(
+ HttpResponse response,
+ StreamChunk chunk,
+ bool useSseFormat,
+ CancellationToken cancellationToken)
+ {
+ if (useSseFormat)
+ {
+ await WriteSSEEventAsync(response, "message", chunk.Text!, chunk.EntryId);
+ }
+ else
+ {
+ await response.WriteAsync(chunk.Text!, cancellationToken);
+ }
+
+ await response.Body.FlushAsync(cancellationToken);
+ }
+
+ ///
+ /// Writes an end-of-stream marker to the response.
+ ///
+ private static async Task WriteEndOfStreamAsync(
+ HttpResponse response,
+ string entryId,
+ bool useSseFormat,
+ CancellationToken cancellationToken)
+ {
+ if (useSseFormat)
+ {
+ await WriteSSEEventAsync(response, "done", "[DONE]", entryId);
+ }
+ else
+ {
+ await response.WriteAsync("\n", cancellationToken);
+ }
+
+ await response.Body.FlushAsync(cancellationToken);
+ }
+
+ ///
+ /// Writes an error message to the response.
+ ///
+ private static async Task WriteErrorAsync(
+ HttpResponse response,
+ string error,
+ bool useSseFormat,
+ CancellationToken cancellationToken)
+ {
+ if (useSseFormat)
+ {
+ await WriteSSEEventAsync(response, "error", error, null);
+ }
+ else
+ {
+ await response.WriteAsync($"\n[Error: {error}]\n", cancellationToken);
+ }
+
+ await response.Body.FlushAsync(cancellationToken);
+ }
+
+ ///
+ /// Writes a Server-Sent Event to the response stream.
+ ///
+ private static async Task WriteSSEEventAsync(
+ HttpResponse response,
+ string eventType,
+ string data,
+ string? id)
+ {
+ StringBuilder sb = new();
+
+ // Include the ID if provided (used as cursor for resumption)
+ if (!string.IsNullOrEmpty(id))
+ {
+ sb.AppendLine($"id: {id}");
+ }
+
+ sb.AppendLine($"event: {eventType}");
+ sb.AppendLine($"data: {data}");
+ sb.AppendLine(); // Empty line marks end of event
+
+ await response.WriteAsync(sb.ToString());
+ }
+}
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/Program.cs b/dotnet/samples/AzureFunctions/08_ReliableStreaming/Program.cs
new file mode 100644
index 0000000000..6c48ed4177
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/Program.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams.
+// It exposes two HTTP endpoints:
+// 1. Create - Starts an agent run and streams responses back via Server-Sent Events (SSE)
+// 2. Stream - Resumes a stream from a specific cursor position, enabling reliable message delivery
+//
+// This pattern is inspired by OpenAI's background mode for the Responses API, which allows clients
+// to disconnect and reconnect to ongoing agent responses without losing messages.
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OpenAI.Chat;
+using ReliableStreaming;
+using StackExchange.Redis;
+
+// Get the Azure OpenAI endpoint and deployment name from environment variables.
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+
+// Get Redis connection string from environment variable.
+string redisConnectionString = Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING")
+ ?? "localhost:6379";
+
+// Get the Redis stream TTL from environment variable (default: 10 minutes).
+int redisStreamTtlMinutes = int.TryParse(
+ Environment.GetEnvironmentVariable("REDIS_STREAM_TTL_MINUTES"),
+ out int ttlMinutes) ? ttlMinutes : 10;
+
+// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
+string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+
+// Travel Planner agent instructions - designed to produce longer responses for demonstrating streaming.
+const string TravelPlannerName = "TravelPlanner";
+const string TravelPlannerInstructions =
+ """
+ You are an expert travel planner who creates detailed, personalized travel itineraries.
+ When asked to plan a trip, you should:
+ 1. Create a comprehensive day-by-day itinerary
+ 2. Include specific recommendations for activities, restaurants, and attractions
+ 3. Provide practical tips for each destination
+ 4. Consider weather and local events when making recommendations
+ 5. Include estimated times and logistics between activities
+
+ Always use the available tools to get current weather forecasts and local events
+ for the destination to make your recommendations more relevant and timely.
+
+ Format your response with clear headings for each day and include emoji icons
+ to make the itinerary easy to scan and visually appealing.
+ """;
+
+// Configure the function app to host the AI agent.
+FunctionsApplicationBuilder builder = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableAgents(options =>
+ {
+ // Define the Travel Planner agent with tools for weather and events
+ options.AddAIAgentFactory(TravelPlannerName, sp =>
+ {
+ return client.GetChatClient(deploymentName).CreateAIAgent(
+ instructions: TravelPlannerInstructions,
+ name: TravelPlannerName,
+ services: sp,
+ tools: [
+ AIFunctionFactory.Create(TravelTools.GetWeatherForecast),
+ AIFunctionFactory.Create(TravelTools.GetLocalEvents),
+ ]);
+ });
+ });
+
+// Register Redis connection as a singleton
+builder.Services.AddSingleton(_ =>
+ ConnectionMultiplexer.Connect(redisConnectionString));
+
+// Register the Redis stream response handler - this captures agent responses
+// and publishes them to Redis Streams for reliable delivery.
+// Registered as both the concrete type (for FunctionTriggers) and the interface (for the agent framework).
+builder.Services.AddSingleton(sp =>
+ new RedisStreamResponseHandler(
+ sp.GetRequiredService(),
+ TimeSpan.FromMinutes(redisStreamTtlMinutes)));
+builder.Services.AddSingleton(sp =>
+ sp.GetRequiredService());
+
+using IHost app = builder.Build();
+
+app.Run();
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/README.md b/dotnet/samples/AzureFunctions/08_ReliableStreaming/README.md
new file mode 100644
index 0000000000..f1c68c2339
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/README.md
@@ -0,0 +1,264 @@
+# Reliable Streaming with Redis
+
+This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams as a message broker. It enables clients to disconnect and reconnect to ongoing agent responses without losing messages, inspired by [OpenAI's background mode](https://platform.openai.com/docs/guides/background) for the Responses API.
+
+## Key Concepts Demonstrated
+
+- **Reliable message delivery**: Agent responses are persisted to Redis Streams, allowing clients to resume from any point
+- **Content negotiation**: Use `Accept: text/plain` for raw terminal output, or `Accept: text/event-stream` for SSE format
+- **Server-Sent Events (SSE)**: Standard streaming format that works with `curl`, browsers, and most HTTP clients
+- **Cursor-based resumption**: Each SSE event includes an `id` field that can be used to resume the stream
+- **Fire-and-forget agent invocation**: The agent runs in the background while the client streams from Redis via an HTTP trigger function
+
+## Environment Setup
+
+See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
+
+### Additional Requirements: Redis
+
+This sample requires a Redis instance. Start a local Redis instance using Docker:
+
+```bash
+docker run -d --name redis -p 6379:6379 redis:latest
+```
+
+To verify Redis is running:
+
+```bash
+docker ps | grep redis
+```
+
+## Running the Sample
+
+Start the Azure Functions host:
+
+```bash
+func start
+```
+
+### 1. Test Streaming with curl
+
+Open a new terminal and start a travel planning request. Use the `-i` flag to see response headers (including the conversation ID) and `Accept: text/plain` for raw text output:
+
+**Bash (Linux/macOS/WSL):**
+
+```bash
+curl -i -N -X POST http://localhost:7071/api/agent/create \
+ -H "Content-Type: text/plain" \
+ -H "Accept: text/plain" \
+ -d "Plan a 7-day trip to Tokyo, Japan for next month. Include daily activities, restaurant recommendations, and tips for getting around."
+```
+
+**PowerShell:**
+
+```powershell
+curl -i -N -X POST http://localhost:7071/api/agent/create `
+ -H "Content-Type: text/plain" `
+ -H "Accept: text/plain" `
+ -d "Plan a 7-day trip to Tokyo, Japan for next month. Include daily activities, restaurant recommendations, and tips for getting around."
+```
+
+You'll first see the response headers, including:
+
+```text
+HTTP/1.1 200 OK
+Content-Type: text/plain; charset=utf-8
+x-conversation-id: @dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890
+...
+```
+
+Then the agent's response will stream to your terminal in chunks, similar to a ChatGPT-style experience (though not character-by-character).
+
+> **Note:** The `-N` flag in curl disables output buffering, which is essential for seeing the stream in real-time. The `-i` flag includes the HTTP headers in the output.
+
+### 2. Demonstrate Stream Interruption and Resumption
+
+This is the key feature of reliable streaming! Follow these steps to see it in action:
+
+#### Step 1: Start a stream and note the conversation ID
+
+Run the curl command from step 1. Watch for the `x-conversation-id` header in the response - **copy this value**, you'll need it to resume.
+
+```text
+x-conversation-id: @dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890
+```
+
+#### Step 2: Interrupt the stream
+
+While the agent is still generating text, press **`Ctrl+C`** to interrupt the stream. The agent continues running in the background - your messages are being saved to Redis!
+
+#### Step 3: Resume the stream
+
+Use the conversation ID you copied to resume streaming from where you left off. Include the `Accept: text/plain` header to get raw text output:
+
+**Bash (Linux/macOS/WSL):**
+
+```bash
+# Replace with your actual conversation ID from the x-conversation-id header
+CONVERSATION_ID="@dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890"
+
+curl -N -H "Accept: text/plain" "http://localhost:7071/api/agent/stream/${CONVERSATION_ID}"
+```
+
+**PowerShell:**
+
+```powershell
+# Replace with your actual conversation ID from the x-conversation-id header
+$conversationId = "@dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890"
+
+curl -N -H "Accept: text/plain" "http://localhost:7071/api/agent/stream/$conversationId"
+```
+
+You'll see the **entire response replayed from the beginning**, including the parts you already received before interrupting.
+
+#### Step 4 (Advanced): Resume from a specific cursor
+
+If you're using SSE format, each event includes an `id` field that you can use as a cursor to resume from a specific point:
+
+```bash
+# Resume from a specific cursor position
+curl -N "http://localhost:7071/api/agent/stream/${CONVERSATION_ID}?cursor=1734567890123-0"
+```
+
+### 3. Alternative: SSE Format for Programmatic Clients
+
+If you need the full Server-Sent Events format with cursors for resumable streaming, use `Accept: text/event-stream` (or omit the Accept header):
+
+```bash
+curl -i -N -X POST http://localhost:7071/api/agent/create \
+ -H "Content-Type: text/plain" \
+ -H "Accept: text/event-stream" \
+ -d "Plan a 7-day trip to Tokyo, Japan."
+```
+
+This returns SSE-formatted events with `id`, `event`, and `data` fields:
+
+```text
+id: 1734567890123-0
+event: message
+data: # 7-Day Tokyo Adventure
+
+id: 1734567890124-0
+event: message
+data: ## Day 1: Arrival and Exploration
+
+id: 1734567890999-0
+event: done
+data: [DONE]
+```
+
+The `id` field is the Redis stream entry ID - use it as the `cursor` parameter to resume from that exact point.
+
+### Understanding the Response Headers
+
+| Header | Description |
+|--------|-------------|
+| `x-conversation-id` | The conversation ID (session key). Use this to resume the stream. |
+| `Content-Type` | Either `text/plain` or `text/event-stream` depending on your `Accept` header. |
+| `Cache-Control` | Set to `no-cache` to prevent caching of the stream. |
+
+## Architecture Overview
+
+```text
+┌─────────────┐ POST /agent/create ┌─────────────────────┐
+│ Client │ (Accept: text/plain or SSE)│ Azure Functions │
+│ (curl) │ ──────────────────────────► │ (FunctionTriggers) │
+└─────────────┘ └──────────┬──────────┘
+ ▲ │
+ │ Text or SSE stream Signal Entity
+ │ │
+ │ ▼
+ │ ┌─────────────────────┐
+ │ │ AgentEntity │
+ │ │ (Durable Entity) │
+ │ └──────────┬──────────┘
+ │ │
+ │ IAgentResponseHandler
+ │ │
+ │ ▼
+ │ ┌─────────────────────┐
+ │ │ RedisStreamResponse │
+ │ │ Handler │
+ │ └──────────┬──────────┘
+ │ │
+ │ XADD (write)
+ │ │
+ │ ▼
+ │ ┌─────────────────────┐
+ └─────────── XREAD (poll) ────────── │ Redis Streams │
+ │ (Durable Log) │
+ └─────────────────────┘
+```
+
+### Data Flow
+
+1. **Client sends prompt**: The `Create` endpoint receives the prompt and generates a new agent thread.
+
+2. **Agent invoked**: The durable entity (`AgentEntity`) is signaled to run the travel planner agent. This is fire-and-forget from the HTTP request's perspective.
+
+3. **Responses captured**: As the agent generates responses, `RedisStreamResponseHandler` (implementing `IAgentResponseHandler`) extracts the text from each `AgentRunResponseUpdate` and publishes it to a Redis Stream keyed by session ID.
+
+4. **Client polls Redis**: The HTTP response streams events by polling the Redis Stream. For SSE format, each event includes the Redis entry ID as the `id` field.
+
+5. **Resumption**: If the client disconnects, it can call the `Stream` endpoint with the conversation ID (from the `x-conversation-id` header) and optionally the last received cursor to resume from that point.
+
+## Message Delivery Guarantees
+
+This sample provides **at-least-once delivery** with the following characteristics:
+
+- **Durability**: Messages are persisted to Redis Streams with configurable TTL (default: 10 minutes).
+- **Ordering**: Messages are delivered in order within a session.
+- **Resumption**: Clients can resume from any point using cursor-based pagination.
+- **Replay**: Clients can replay the entire stream by omitting the cursor.
+
+### Important Considerations
+
+- **No exactly-once delivery**: If a client disconnects exactly when receiving a message, it may receive that message again upon resumption. Clients should handle duplicate messages idempotently.
+- **TTL expiration**: Streams expire after the configured TTL. Clients cannot resume streams that have expired.
+- **Redis guarantees**: Redis streams are backed by Redis persistence mechanisms (RDB/AOF). Ensure your Redis instance is configured for durability as needed.
+
+## When to Use These Patterns
+
+The patterns demonstrated in this sample are ideal for:
+
+- **Long-running agent tasks**: When agent responses take minutes to complete (e.g., deep research, complex planning)
+- **Unreliable network connections**: Mobile apps, unstable WiFi, or connections that may drop
+- **Resumable experiences**: Users should be able to close and reopen an app without losing context
+- **Background processing**: When you want to fire off a task and check on it later
+
+These patterns may be overkill for:
+
+- **Simple, fast responses**: If responses complete in a few seconds, standard streaming is simpler
+- **Stateless interactions**: If there's no need to resume or replay conversations
+- **Very high throughput**: Redis adds latency; for maximum throughput, direct streaming may be better
+
+## Configuration
+
+| Environment Variable | Description | Default |
+|---------------------|-------------|---------|
+| `REDIS_CONNECTION_STRING` | Redis connection string | `localhost:6379` |
+| `REDIS_STREAM_TTL_MINUTES` | How long streams are retained after last write | `10` |
+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL | (required) |
+| `AZURE_OPENAI_DEPLOYMENT` | Azure OpenAI deployment name | (required) |
+| `AZURE_OPENAI_KEY` | API key (optional, uses Azure CLI auth if not set) | (optional) |
+
+## Cleanup
+
+To stop and remove the Redis Docker containers:
+
+```bash
+docker stop redis
+docker rm redis
+```
+
+## Disclaimer
+
+> ⚠️ **This sample is for illustration purposes only and is not intended to be production-ready.**
+>
+> A production implementation should consider:
+>
+> - Redis cluster configuration for high availability
+> - Authentication and authorization for the streaming endpoints
+> - Rate limiting and abuse prevention
+> - Monitoring and alerting for stream health
+> - Graceful handling of Redis failures
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/RedisStreamResponseHandler.cs b/dotnet/samples/AzureFunctions/08_ReliableStreaming/RedisStreamResponseHandler.cs
new file mode 100644
index 0000000000..b0a95f49f6
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/RedisStreamResponseHandler.cs
@@ -0,0 +1,213 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Runtime.CompilerServices;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.DurableTask;
+using StackExchange.Redis;
+
+namespace ReliableStreaming;
+
+///
+/// Represents a chunk of data read from a Redis stream.
+///
+/// The Redis stream entry ID (can be used as a cursor for resumption).
+/// The text content of the chunk, or null if this is a completion/error marker.
+/// True if this chunk marks the end of the stream.
+/// An error message if something went wrong, or null otherwise.
+public readonly record struct StreamChunk(string EntryId, string? Text, bool IsDone, string? Error);
+
+///
+/// An implementation of that publishes agent response updates
+/// to Redis Streams for reliable delivery. This enables clients to disconnect and reconnect
+/// to ongoing agent responses without losing messages.
+///
+///
+///
+/// Redis Streams provide a durable, append-only log that supports consumer groups and message
+/// acknowledgment. This implementation uses auto-generated IDs (which are timestamp-based)
+/// as sequence numbers, allowing clients to resume from any point in the stream.
+///
+///
+/// Each agent session gets its own Redis Stream, keyed by session ID. The stream entries
+/// contain text chunks extracted from objects.
+///
+///
+public sealed class RedisStreamResponseHandler : IAgentResponseHandler
+{
+ private const int MaxEmptyReads = 300; // 5 minutes at 1 second intervals
+ private const int PollIntervalMs = 1000;
+
+ private readonly IConnectionMultiplexer _redis;
+ private readonly TimeSpan _streamTtl;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Redis connection multiplexer.
+ /// The time-to-live for stream entries. Streams will expire after this duration of inactivity.
+ public RedisStreamResponseHandler(IConnectionMultiplexer redis, TimeSpan streamTtl)
+ {
+ this._redis = redis;
+ this._streamTtl = streamTtl;
+ }
+
+ ///
+ public async ValueTask OnStreamingResponseUpdateAsync(
+ IAsyncEnumerable messageStream,
+ CancellationToken cancellationToken)
+ {
+ // Get the current session ID from the DurableAgentContext
+ // This is set by the AgentEntity before invoking the response handler
+ DurableAgentContext? context = DurableAgentContext.Current;
+ if (context is null)
+ {
+ throw new InvalidOperationException(
+ "DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
+ }
+
+ // Get conversation ID from the current thread context, which is only available in the context of
+ // a durable agent execution.
+ string conversationId = context.CurrentThread.GetService()?.ConversationId
+ ?? throw new InvalidOperationException("Unable to determine conversation ID from the current thread.");
+ string streamKey = GetStreamKey(conversationId);
+
+ IDatabase db = this._redis.GetDatabase();
+ int sequenceNumber = 0;
+
+ await foreach (AgentRunResponseUpdate update in messageStream.WithCancellation(cancellationToken))
+ {
+ // Extract just the text content - this avoids serialization round-trip issues
+ string text = update.Text;
+
+ // Only publish non-empty text chunks
+ if (!string.IsNullOrEmpty(text))
+ {
+ // Create the stream entry with the text and metadata
+ NameValueEntry[] entries =
+ [
+ new NameValueEntry("text", text),
+ new NameValueEntry("sequence", sequenceNumber++),
+ new NameValueEntry("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
+ ];
+
+ // Add to the Redis Stream with auto-generated ID (timestamp-based)
+ await db.StreamAddAsync(streamKey, entries);
+
+ // Refresh the TTL on each write to keep the stream alive during active streaming
+ await db.KeyExpireAsync(streamKey, this._streamTtl);
+ }
+ }
+
+ // Add a sentinel entry to mark the end of the stream
+ NameValueEntry[] endEntries =
+ [
+ new NameValueEntry("text", ""),
+ new NameValueEntry("sequence", sequenceNumber),
+ new NameValueEntry("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
+ new NameValueEntry("done", "true"),
+ ];
+ await db.StreamAddAsync(streamKey, endEntries);
+
+ // Set final TTL - the stream will be cleaned up after this duration
+ await db.KeyExpireAsync(streamKey, this._streamTtl);
+ }
+
+ ///
+ public ValueTask OnAgentResponseAsync(AgentRunResponse message, CancellationToken cancellationToken)
+ {
+ // This handler is optimized for streaming responses.
+ // For non-streaming responses, we don't need to store in Redis since
+ // the response is returned directly to the caller.
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ /// Reads chunks from a Redis stream for the given session, yielding them as they become available.
+ ///
+ /// The conversation ID to read from.
+ /// Optional cursor to resume from. If null, reads from the beginning.
+ /// Cancellation token.
+ /// An async enumerable of stream chunks.
+ public async IAsyncEnumerable ReadStreamAsync(
+ string conversationId,
+ string? cursor,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ string streamKey = GetStreamKey(conversationId);
+
+ IDatabase db = this._redis.GetDatabase();
+ string startId = string.IsNullOrEmpty(cursor) ? "0-0" : cursor;
+
+ int emptyReadCount = 0;
+ bool hasSeenData = false;
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ StreamEntry[]? entries = null;
+ string? errorMessage = null;
+
+ try
+ {
+ entries = await db.StreamReadAsync(streamKey, startId, count: 100);
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+ }
+
+ if (errorMessage != null)
+ {
+ yield return new StreamChunk(startId, null, false, errorMessage);
+ yield break;
+ }
+
+ // entries is guaranteed to be non-null if errorMessage is null
+ if (entries!.Length == 0)
+ {
+ if (!hasSeenData)
+ {
+ emptyReadCount++;
+ if (emptyReadCount >= MaxEmptyReads)
+ {
+ yield return new StreamChunk(
+ startId,
+ null,
+ false,
+ $"Stream not found or timed out after {MaxEmptyReads * PollIntervalMs / 1000} seconds");
+ yield break;
+ }
+ }
+
+ await Task.Delay(PollIntervalMs, cancellationToken);
+ continue;
+ }
+
+ hasSeenData = true;
+
+ foreach (StreamEntry entry in entries)
+ {
+ startId = entry.Id.ToString();
+ string? text = entry["text"];
+ string? done = entry["done"];
+
+ if (done == "true")
+ {
+ yield return new StreamChunk(startId, null, true, null);
+ yield break;
+ }
+
+ if (!string.IsNullOrEmpty(text))
+ {
+ yield return new StreamChunk(startId, text, false, null);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets the Redis Stream key for a given conversation ID.
+ ///
+ /// The conversation ID.
+ /// The Redis Stream key.
+ internal static string GetStreamKey(string conversationId) => $"agent-stream:{conversationId}";
+}
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/Tools.cs b/dotnet/samples/AzureFunctions/08_ReliableStreaming/Tools.cs
new file mode 100644
index 0000000000..fce73bc378
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/Tools.cs
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+
+namespace ReliableStreaming;
+
+///
+/// Mock travel tools that return hardcoded data for demonstration purposes.
+/// In a real application, these would call actual weather and events APIs.
+///
+internal static class TravelTools
+{
+ ///
+ /// Gets a weather forecast for a destination on a specific date.
+ /// Returns mock weather data for demonstration purposes.
+ ///
+ /// The destination city or location.
+ /// The date for the forecast (e.g., "2025-01-15" or "next Monday").
+ /// A weather forecast summary.
+ [Description("Gets the weather forecast for a destination on a specific date. Use this to provide weather-aware recommendations in the itinerary.")]
+ public static string GetWeatherForecast(string destination, string date)
+ {
+ // Mock weather data based on destination for realistic responses
+ Dictionary weatherByRegion = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Tokyo"] = ("Partly cloudy with a chance of light rain", 58, 45),
+ ["Paris"] = ("Overcast with occasional drizzle", 52, 41),
+ ["New York"] = ("Clear and cold", 42, 28),
+ ["London"] = ("Foggy morning, clearing in afternoon", 48, 38),
+ ["Sydney"] = ("Sunny and warm", 82, 68),
+ ["Rome"] = ("Sunny with light breeze", 62, 48),
+ ["Barcelona"] = ("Partly sunny", 59, 47),
+ ["Amsterdam"] = ("Cloudy with light rain", 46, 38),
+ ["Dubai"] = ("Sunny and hot", 85, 72),
+ ["Singapore"] = ("Tropical thunderstorms in afternoon", 88, 77),
+ ["Bangkok"] = ("Hot and humid, afternoon showers", 91, 78),
+ ["Los Angeles"] = ("Sunny and pleasant", 72, 55),
+ ["San Francisco"] = ("Morning fog, afternoon sun", 62, 52),
+ ["Seattle"] = ("Rainy with breaks", 48, 40),
+ ["Miami"] = ("Warm and sunny", 78, 65),
+ ["Honolulu"] = ("Tropical paradise weather", 82, 72),
+ };
+
+ // Find a matching destination or use a default
+ (string condition, int highF, int lowF) forecast = ("Partly cloudy", 65, 50);
+ foreach (KeyValuePair entry in weatherByRegion)
+ {
+ if (destination.Contains(entry.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ forecast = entry.Value;
+ break;
+ }
+ }
+
+ return $"""
+ Weather forecast for {destination} on {date}:
+ Conditions: {forecast.condition}
+ High: {forecast.highF}°F ({(forecast.highF - 32) * 5 / 9}°C)
+ Low: {forecast.lowF}°F ({(forecast.lowF - 32) * 5 / 9}°C)
+
+ Recommendation: {GetWeatherRecommendation(forecast.condition)}
+ """;
+ }
+
+ ///
+ /// Gets local events happening at a destination around a specific date.
+ /// Returns mock event data for demonstration purposes.
+ ///
+ /// The destination city or location.
+ /// The date to search for events (e.g., "2025-01-15" or "next week").
+ /// A list of local events and activities.
+ [Description("Gets local events and activities happening at a destination around a specific date. Use this to suggest timely activities and experiences.")]
+ public static string GetLocalEvents(string destination, string date)
+ {
+ // Mock events data based on destination
+ Dictionary eventsByCity = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Tokyo"] = [
+ "🎭 Kabuki Theater Performance at Kabukiza Theatre - Traditional Japanese drama",
+ "🌸 Winter Illuminations at Yoyogi Park - Spectacular light displays",
+ "🍜 Ramen Festival at Tokyo Station - Sample ramen from across Japan",
+ "🎮 Gaming Expo at Tokyo Big Sight - Latest video games and technology",
+ ],
+ ["Paris"] = [
+ "🎨 Impressionist Exhibition at Musée d'Orsay - Extended evening hours",
+ "🍷 Wine Tasting Tour in Le Marais - Local sommelier guided",
+ "🎵 Jazz Night at Le Caveau de la Huchette - Historic jazz club",
+ "🥐 French Pastry Workshop - Learn from master pâtissiers",
+ ],
+ ["New York"] = [
+ "🎭 Broadway Show: Hamilton - Limited engagement performances",
+ "🏀 Knicks vs Lakers at Madison Square Garden",
+ "🎨 Modern Art Exhibit at MoMA - New installations",
+ "🍕 Pizza Walking Tour of Brooklyn - Artisan pizzerias",
+ ],
+ ["London"] = [
+ "👑 Royal Collection Exhibition at Buckingham Palace",
+ "🎭 West End Musical: The Phantom of the Opera",
+ "🍺 Craft Beer Festival at Brick Lane",
+ "🎪 Winter Wonderland at Hyde Park - Rides and markets",
+ ],
+ ["Sydney"] = [
+ "🏄 Pro Surfing Competition at Bondi Beach",
+ "🎵 Opera at Sydney Opera House - La Bohème",
+ "🦘 Wildlife Night Safari at Taronga Zoo",
+ "🍽️ Harbor Dinner Cruise with fireworks",
+ ],
+ ["Rome"] = [
+ "🏛️ After-Hours Vatican Tour - Skip the crowds",
+ "🍝 Pasta Making Class in Trastevere",
+ "🎵 Classical Concert at Borghese Gallery",
+ "🍷 Wine Tasting in Roman Cellars",
+ ],
+ };
+
+ // Find events for the destination or use generic events
+ string[] events = [
+ "🎭 Local theater performance",
+ "🍽️ Food and wine festival",
+ "🎨 Art gallery opening",
+ "🎵 Live music at local venues",
+ ];
+
+ foreach (KeyValuePair entry in eventsByCity)
+ {
+ if (destination.Contains(entry.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ events = entry.Value;
+ break;
+ }
+ }
+
+ string eventList = string.Join("\n• ", events);
+ return $"""
+ Local events in {destination} around {date}:
+
+ • {eventList}
+
+ 💡 Tip: Book popular events in advance as they may sell out quickly!
+ """;
+ }
+
+ private static string GetWeatherRecommendation(string condition)
+ {
+ // Use case-insensitive comparison instead of ToLowerInvariant() to satisfy CA1308
+ return condition switch
+ {
+ string c when c.Contains("rain", StringComparison.OrdinalIgnoreCase) || c.Contains("drizzle", StringComparison.OrdinalIgnoreCase) =>
+ "Bring an umbrella and waterproof jacket. Consider indoor activities for backup.",
+ string c when c.Contains("fog", StringComparison.OrdinalIgnoreCase) =>
+ "Morning visibility may be limited. Plan outdoor sightseeing for afternoon.",
+ string c when c.Contains("cold", StringComparison.OrdinalIgnoreCase) =>
+ "Layer up with warm clothing. Hot drinks and cozy cafés recommended.",
+ string c when c.Contains("hot", StringComparison.OrdinalIgnoreCase) || c.Contains("warm", StringComparison.OrdinalIgnoreCase) =>
+ "Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours.",
+ string c when c.Contains("thunder", StringComparison.OrdinalIgnoreCase) || c.Contains("storm", StringComparison.OrdinalIgnoreCase) =>
+ "Keep an eye on weather updates. Have indoor alternatives ready.",
+ _ => "Pleasant conditions expected. Great day for outdoor exploration!"
+ };
+ }
+}
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/host.json b/dotnet/samples/AzureFunctions/08_ReliableStreaming/host.json
new file mode 100644
index 0000000000..4247b37c97
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/host.json
@@ -0,0 +1,21 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information",
+ "ReliableStreaming": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/AzureFunctions/08_ReliableStreaming/local.settings.json b/dotnet/samples/AzureFunctions/08_ReliableStreaming/local.settings.json
new file mode 100644
index 0000000000..5dfdb17999
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/08_ReliableStreaming/local.settings.json
@@ -0,0 +1,12 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
+ "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
+ "AZURE_OPENAI_ENDPOINT": "",
+ "AZURE_OPENAI_DEPLOYMENT": "",
+ "REDIS_CONNECTION_STRING": "localhost:6379",
+ "REDIS_STREAM_TTL_MINUTES": "10"
+ }
+}
diff --git a/dotnet/samples/AzureFunctions/README.md b/dotnet/samples/AzureFunctions/README.md
index e60b0f662e..2545712ea4 100644
--- a/dotnet/samples/AzureFunctions/README.md
+++ b/dotnet/samples/AzureFunctions/README.md
@@ -9,6 +9,7 @@ This directory contains samples for Azure Functions.
- **[05_AgentOrchestration_HITL](05_AgentOrchestration_HITL)**: A sample that demonstrates how to implement a human-in-the-loop workflow using durable orchestration, including external event handling for human approval.
- **[06_LongRunningTools](06_LongRunningTools)**: A sample that demonstrates how agents can start and interact with durable orchestrations from tool calls to enable long-running tool scenarios.
- **[07_AgentAsMcpTool](07_AgentAsMcpTool)**: A sample that demonstrates how to configure durable AI agents to be accessible as Model Context Protocol (MCP) tools.
+- **[08_ReliableStreaming](08_ReliableStreaming)**: A sample that demonstrates how to implement reliable streaming for durable agents using Redis Streams, enabling clients to disconnect and reconnect without losing messages.
## Running the Samples
diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs
index 41538085db..9f7812cc50 100644
--- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs
+++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs
@@ -22,17 +22,17 @@ public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptio
this._jsonSerializerOptions = jsonSerializerOptions;
}
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken)
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs
index f515e97531..69e3db58c7 100644
--- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs
+++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs
@@ -22,17 +22,17 @@ public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jso
this._jsonSerializerOptions = jsonSerializerOptions;
}
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken)
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs
index 8321efaa73..d5fd9f187b 100644
--- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs
+++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs
@@ -35,18 +35,18 @@ public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOpt
}
///
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken)
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
///
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs
index 4588c7bd60..603698b579 100644
--- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs
+++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs
@@ -17,17 +17,17 @@ public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializer
this._jsonSerializerOptions = jsonSerializerOptions;
}
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken)
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs
index 8f1039251d..a4e588f347 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs
@@ -34,7 +34,7 @@ public override AgentThread GetNewThread()
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
- public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override async Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
@@ -45,7 +45,7 @@ public override async Task RunAsync(IEnumerable m
}
// Clone the input messages and turn them into response messages with upper case text.
- List responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
+ List responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the thread of the input and output messages.
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
@@ -58,7 +58,7 @@ public override async Task RunAsync(IEnumerable m
};
}
- public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
@@ -69,7 +69,7 @@ public override async IAsyncEnumerable RunStreamingAsync
}
// Clone the input messages and turn them into response messages with upper case text.
- List responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
+ List responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the thread of the input and output messages.
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
@@ -79,7 +79,7 @@ public override async IAsyncEnumerable RunStreamingAsync
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
- AuthorName = this.DisplayName,
+ AuthorName = message.AuthorName,
Role = ChatRole.Assistant,
Contents = message.Contents,
ResponseId = Guid.NewGuid().ToString("N"),
@@ -88,7 +88,7 @@ public override async IAsyncEnumerable RunStreamingAsync
}
}
- private static IEnumerable CloneAndToUpperCase(IEnumerable messages, string agentName) => messages.Select(x =>
+ private static IEnumerable CloneAndToUpperCase(IEnumerable messages, string? agentName) => messages.Select(x =>
{
// Clone the message and update its author to be the agent.
var messageClone = x.Clone();
diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/OpenAIChatClientAgent.cs b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/OpenAIChatClientAgent.cs
index b295bfecea..a0b59d1053 100644
--- a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/OpenAIChatClientAgent.cs
+++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/OpenAIChatClientAgent.cs
@@ -87,10 +87,10 @@ public virtual IAsyncEnumerable RunStreamingAsync
}
///
- public sealed override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
- base.RunAsync(messages, thread, options, cancellationToken);
+ protected sealed override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
+ base.RunCoreAsync(messages, thread, options, cancellationToken);
///
- public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
- base.RunStreamingAsync(messages, thread, options, cancellationToken);
+ protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
+ base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
}
diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/OpenAIResponseClientAgent.cs b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/OpenAIResponseClientAgent.cs
index 622223307c..f894a5434c 100644
--- a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/OpenAIResponseClientAgent.cs
+++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/OpenAIResponseClientAgent.cs
@@ -105,10 +105,10 @@ public virtual async IAsyncEnumerable RunStreamingAsync
}
///
- public sealed override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
- base.RunAsync(messages, thread, options, cancellationToken);
+ protected sealed override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
+ base.RunCoreAsync(messages, thread, options, cancellationToken);
///
- public sealed override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
- base.RunStreamingAsync(messages, thread, options, cancellationToken);
+ protected sealed override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
+ base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
}
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
index e67846f54c..54791c1992 100644
--- a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
@@ -35,7 +35,7 @@
-
+
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
index a865f43be5..ad23b11b17 100644
--- a/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
@@ -35,7 +35,7 @@
-
+
diff --git a/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs b/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
index 740b959a7a..ff7af20ba9 100644
--- a/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
+++ b/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
@@ -48,9 +48,9 @@ public WeatherForecastAgent(IChatClient chatClient)
{
}
- public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override async Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- var response = await base.RunAsync(messages, thread, options, cancellationToken);
+ var response = await base.RunCoreAsync(messages, thread, options, cancellationToken);
// If the agent returned a valid structured output response
// we might be able to enhance the response with an adaptive card.
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
index e804fbb389..cf88a89177 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
@@ -30,7 +30,6 @@ internal sealed class A2AAgent : AIAgent
private readonly string? _id;
private readonly string? _name;
private readonly string? _description;
- private readonly string? _displayName;
private readonly ILogger _logger;
///
@@ -40,9 +39,8 @@ internal sealed class A2AAgent : AIAgent
/// The unique identifier for the agent.
/// The the name of the agent.
/// The description of the agent.
- /// The display name of the agent.
/// Optional logger factory to use for logging.
- public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null)
+ public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null)
{
_ = Throw.IfNull(a2aClient);
@@ -50,7 +48,6 @@ public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, str
this._id = id;
this._name = name;
this._description = description;
- this._displayName = displayName;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger();
}
@@ -71,7 +68,7 @@ public override AgentThread DeserializeThread(JsonElement serializedThread, Json
=> new A2AAgentThread(serializedThread, jsonSerializerOptions);
///
- public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override async Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
@@ -134,7 +131,7 @@ public override async Task RunAsync(IEnumerable m
}
///
- public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
@@ -203,9 +200,6 @@ public override async IAsyncEnumerable RunStreamingAsync
///
public override string? Name => this._name ?? base.Name;
- ///
- public override string DisplayName => this._displayName ?? base.DisplayName;
-
///
public override string? Description => this._description ?? base.Description;
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs
index 095481c0d4..d57ed4cb42 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs
@@ -33,9 +33,8 @@ public static class A2AClientExtensions
/// The unique identifier for the agent.
/// The the name of the agent.
/// The description of the agent.
- /// The display name of the agent.
/// Optional logger factory for enabling logging within the agent.
/// An instance backed by the A2A agent.
- public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null) =>
- new A2AAgent(client, id, name, description, displayName, loggerFactory);
+ public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) =>
+ new A2AAgent(client, id, name, description, loggerFactory);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
index 4cff385dcc..afed5d1518 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
@@ -60,18 +60,6 @@ public abstract class AIAgent
///
public virtual string? Name { get; }
- ///
- /// Gets a display-friendly name for the agent.
- ///
- ///
- /// The agent's if available, otherwise the .
- ///
- ///
- /// This property provides a guaranteed non-null string suitable for display in user interfaces,
- /// logs, or other contexts where a readable identifier is needed.
- ///
- public virtual string DisplayName => this.Name ?? this.Id;
-
///
/// Gets a description of the agent's purpose, capabilities, or behavior.
///
@@ -230,6 +218,35 @@ public Task RunAsync(
/// A task that represents the asynchronous operation. The task result contains an with the agent's output.
///
///
+ /// This method delegates to to perform the actual agent invocation. It handles collections of messages,
+ /// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
+ /// context-rich conversations.
+ ///
+ ///
+ /// The messages are processed in the order provided and become part of the conversation history.
+ /// The agent's response will also be added to if one is provided.
+ ///
+ ///
+ public Task RunAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default) =>
+ this.RunCoreAsync(messages, thread, options, cancellationToken);
+
+ ///
+ /// Core implementation of the agent invocation logic with a collection of chat messages.
+ ///
+ /// The collection of messages to send to the agent for processing.
+ ///
+ /// The conversation thread to use for this invocation. If , a new thread will be created.
+ /// The thread will be updated with the input messages and any response messages generated during invocation.
+ ///
+ /// Optional configuration parameters for controlling the agent's invocation behavior.
+ /// The to monitor for cancellation requests. The default is .
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
+ ///
+ ///
/// This is the primary invocation method that implementations must override. It handles collections of messages,
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
/// context-rich conversations.
@@ -239,7 +256,7 @@ public Task RunAsync(
/// The agent's response will also be added to if one is provided.
///
///
- public abstract Task RunAsync(
+ protected abstract Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -324,6 +341,34 @@ public IAsyncEnumerable RunStreamingAsync(
/// An asynchronous enumerable of instances representing the streaming response.
///
///
+ /// This method delegates to to perform the actual streaming invocation. It provides real-time
+ /// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
+ ///
+ ///
+ /// Each represents a portion of the complete response, allowing consumers
+ /// to display partial results, implement progressive loading, or provide immediate feedback to users.
+ ///
+ ///
+ public IAsyncEnumerable RunStreamingAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default) =>
+ this.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
+
+ ///
+ /// Core implementation of the agent streaming invocation logic with a collection of chat messages.
+ ///
+ /// The collection of messages to send to the agent for processing.
+ ///
+ /// The conversation thread to use for this invocation. If , a new thread will be created.
+ /// The thread will be updated with the input messages and any response updates generated during invocation.
+ ///
+ /// Optional configuration parameters for controlling the agent's invocation behavior.
+ /// The to monitor for cancellation requests. The default is .
+ /// An asynchronous enumerable of instances representing the streaming response.
+ ///
+ ///
/// This is the primary streaming invocation method that implementations must override. It provides real-time
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
///
@@ -332,7 +377,7 @@ public IAsyncEnumerable RunStreamingAsync(
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
///
///
- public abstract IAsyncEnumerable RunStreamingAsync(
+ protected abstract IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs
index 4c0ff1a36d..e7bf58f39f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs
@@ -81,7 +81,7 @@ public override AgentThread DeserializeThread(JsonElement serializedThread, Json
=> this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
///
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -89,7 +89,7 @@ public override Task RunAsync(
=> this.InnerAgent.RunAsync(messages, thread, options, cancellationToken);
///
- public override IAsyncEnumerable RunStreamingAsync(
+ protected override IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs b/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs
index 6ca2f38d3d..203bab21ed 100644
--- a/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs
@@ -58,7 +58,7 @@ public override AgentThread DeserializeThread(JsonElement serializedThread, Json
=> new CopilotStudioAgentThread(serializedThread, jsonSerializerOptions);
///
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -96,7 +96,7 @@ public override async Task RunAsync(
}
///
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs
index 3271b40853..8dcc46b53c 100644
--- a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs
@@ -231,7 +231,7 @@ private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
return new EntityInfo(
Id: entityId,
Type: "agent",
- Name: agent.DisplayName,
+ Name: agent.Name ?? agent.Id,
Description: agent.Description,
Framework: "agent_framework",
Tools: tools,
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
index 2035b792fd..d841a80ddd 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
@@ -63,7 +63,7 @@ public override AgentThread DeserializeThread(
/// Thrown when the agent has not been registered.
/// Thrown when the provided thread is not valid for a durable agent.
/// Thrown when cancellation is requested (cancellation is not supported for durable agents).
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -128,7 +128,7 @@ public override async Task RunAsync(
/// Optional run options.
/// The cancellation token.
/// A streaming response enumerable.
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
index 58f9598a7e..ecff2d5c90 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
@@ -23,7 +23,7 @@ public override AgentThread GetNewThread()
return new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!));
}
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -70,7 +70,7 @@ public override async Task RunAsync(
return await agentRunHandle.ReadAgentResponseAsync(cancellationToken);
}
- public override IAsyncEnumerable RunStreamingAsync(
+ protected override IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs
index 8822ebcc39..4a6074fcb6 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs
@@ -21,13 +21,13 @@ internal sealed class EntityAgentWrapper(
// The ID of the agent is always the entity ID.
protected override string? IdCore => this._entityContext.Id.ToString();
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
- AgentRunResponse response = await base.RunAsync(
+ AgentRunResponse response = await base.RunCoreAsync(
messages,
thread,
this.GetAgentEntityRunOptions(options),
@@ -37,13 +37,13 @@ public override async Task RunAsync(
return response;
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- await foreach (AgentRunResponseUpdate update in base.RunStreamingAsync(
+ await foreach (AgentRunResponseUpdate update in base.RunCoreStreamingAsync(
messages,
thread,
this.GetAgentEntityRunOptions(options),
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
index 10b1bc54ff..fa0b9ef287 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
@@ -32,7 +32,7 @@ public async ValueTask ExecuteAsync(FunctionContext context)
}
HttpRequestData? httpRequestData = null;
- TaskEntityDispatcher? dispatcher = null;
+ string? encodedEntityRequest = null;
DurableTaskClient? durableTaskClient = null;
ToolInvocationContext? mcpToolInvocationContext = null;
@@ -43,8 +43,8 @@ public async ValueTask ExecuteAsync(FunctionContext context)
case HttpRequestData request:
httpRequestData = request;
break;
- case TaskEntityDispatcher entityDispatcher:
- dispatcher = entityDispatcher;
+ case string entityRequest:
+ encodedEntityRequest = entityRequest;
break;
case DurableTaskClient client:
durableTaskClient = client;
@@ -78,14 +78,14 @@ public async ValueTask ExecuteAsync(FunctionContext context)
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunAgentEntityFunctionEntryPoint)
{
- if (dispatcher is null)
+ if (encodedEntityRequest is null)
{
throw new InvalidOperationException($"Task entity dispatcher binding is missing for the invocation {context.InvocationId}.");
}
- await BuiltInFunctions.InvokeAgentAsync(
- dispatcher,
+ context.GetInvocationResult().Value = await BuiltInFunctions.InvokeAgentAsync(
durableTaskClient,
+ encodedEntityRequest,
context);
return;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
index ebd378ac3b..3d824994e9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
@@ -7,6 +7,7 @@
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Worker.Grpc;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -22,14 +23,14 @@ internal static class BuiltInFunctions
internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}";
// Exposed as an entity trigger via AgentFunctionsProvider
- public static async Task InvokeAgentAsync(
- [EntityTrigger] TaskEntityDispatcher dispatcher,
+ public static Task InvokeAgentAsync(
[DurableClient] DurableTaskClient client,
+ string encodedEntityRequest,
FunctionContext functionContext)
{
// This should never be null except if the function trigger is misconfigured.
- ArgumentNullException.ThrowIfNull(dispatcher);
ArgumentNullException.ThrowIfNull(client);
+ ArgumentNullException.ThrowIfNull(encodedEntityRequest);
ArgumentNullException.ThrowIfNull(functionContext);
// Create a combined service provider that includes both the existing services
@@ -38,7 +39,8 @@ public static async Task InvokeAgentAsync(
// This method is the entry point for the agent entity.
// It will be invoked by the Azure Functions runtime when the entity is called.
- await dispatcher.DispatchAsync(new AgentEntity(combinedServiceProvider, functionContext.CancellationToken));
+ AgentEntity entity = new(combinedServiceProvider, functionContext.CancellationToken);
+ return GrpcEntityRunner.LoadAndRunAsync(encodedEntityRequest, entity, combinedServiceProvider);
}
public static async Task RunAgentHttpAsync(
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
index d32f4bb0e2..a606629dc2 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
@@ -1,5 +1,9 @@
# Release History
+##
+
+- Addressed incompatibility issue with `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` >= 1.11.0 ([#2759](https://github.com/microsoft/agent-framework/pull/2759))
+
## v1.0.0-preview.251125.1
- Added support for .NET 10 ([#2128](https://github.com/microsoft/agent-framework/pull/2128))
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
index cce8fbd1b0..f626db2a90 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
@@ -73,7 +73,7 @@ private static DefaultFunctionMetadata CreateAgentTrigger(string name)
Language = "dotnet-isolated",
RawBindings =
[
- """{"name":"dispatcher","type":"entityTrigger","direction":"In"}""",
+ """{"name":"encodedEntityRequest","type":"entityTrigger","direction":"In"}""",
"""{"name":"client","type":"durableClient","direction":"In"}"""
],
EntryPoint = BuiltInFunctions.RunAgentEntityFunctionEntryPoint,
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs
index 3fcc9cad27..92c817b124 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs
@@ -61,7 +61,7 @@ public static IEndpointConventionBuilder MapOpenAIChatCompletions(
path ??= $"/{agent.Name}/v1/chat/completions";
var group = endpoints.MapGroup(path);
- var endpointAgentName = agent.DisplayName;
+ var endpointAgentName = agent.Name ?? agent.Id;
group.MapPost("/", async ([FromBody] CreateChatCompletion request, CancellationToken cancellationToken)
=> await AIAgentChatCompletionsProcessor.CreateChatCompletionAsync(agent, request, cancellationToken).ConfigureAwait(false))
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs
index 9a395b9b12..ae96636f16 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs
@@ -76,7 +76,7 @@ public static IEndpointConventionBuilder MapOpenAIResponses(
var handlers = new ResponsesHttpHandler(responsesService);
var group = endpoints.MapGroup(responsesPath);
- var endpointAgentName = agent.DisplayName;
+ var endpointAgentName = agent.Name ?? agent.Id;
// Create response endpoint
group.MapPost("/", handlers.CreateResponseAsync)
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs b/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs
index fd2a1950e9..6907fe8889 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs
@@ -42,13 +42,13 @@ public override AgentThread GetNewThread()
}
///
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this._purviewWrapper.ProcessAgentContentAsync(messages, thread, options, this._innerAgent, cancellationToken);
}
///
- public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var response = await this._purviewWrapper.ProcessAgentContentAsync(messages, thread, options, this._innerAgent, cancellationToken).ConfigureAwait(false);
foreach (var update in response.ToAgentRunResponseUpdates())
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffsWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffsWorkflowBuilder.cs
index 9e5b61ac42..9a3abfe960 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffsWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffsWorkflowBuilder.cs
@@ -125,14 +125,14 @@ public HandoffsWorkflowBuilder WithHandoff(AIAgent from, AIAgent to, string? han
{
Throw.ArgumentException(
nameof(to),
- $"The provided target agent '{to.DisplayName}' has no description, name, or instructions, and no handoff description has been provided. " +
+ $"The provided target agent '{to.Name ?? to.Id}' has no description, name, or instructions, and no handoff description has been provided. " +
"At least one of these is required to register a handoff so that the appropriate target agent can be chosen.");
}
}
if (!handoffs.Add(new(to, handoffReason)))
{
- Throw.InvalidOperationException($"A handoff from agent '{from.DisplayName}' to agent '{to.DisplayName}' has already been registered.");
+ Throw.InvalidOperationException($"A handoff from agent '{from.Name ?? from.Id}' to agent '{to.Name ?? to.Id}' has already been registered.");
}
return this;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AgentRunStreamingExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AgentRunStreamingExecutor.cs
index ea80f646f0..ae3a932feb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AgentRunStreamingExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AgentRunStreamingExecutor.cs
@@ -20,7 +20,7 @@ internal sealed class AgentRunStreamingExecutor(AIAgent agent, bool includeInput
protected override async ValueTask TakeTurnAsync(List messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
{
- List? roleChanged = messages.ChangeAssistantToUserForOtherParticipants(agent.DisplayName);
+ List? roleChanged = messages.ChangeAssistantToUserForOtherParticipants(agent.Name ?? agent.Id);
List updates = [];
await foreach (var update in agent.RunStreamingAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false))
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
index 59dc49f143..24e0eea3cb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
@@ -67,7 +67,7 @@ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
List updates = [];
List allMessages = handoffState.Messages;
- List? roleChanges = allMessages.ChangeAssistantToUserForOtherParticipants(this._agent.DisplayName);
+ List? roleChanges = allMessages.ChangeAssistantToUserForOtherParticipants(this._agent.Name ?? this._agent.Id);
await foreach (var update in this._agent.RunStreamingAsync(allMessages,
options: this._agentOptions,
@@ -85,7 +85,7 @@ await AddUpdateAsync(
new AgentRunResponseUpdate
{
AgentId = this._agent.Id,
- AuthorName = this._agent.DisplayName,
+ AuthorName = this._agent.Name ?? this._agent.Id,
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
CreatedAt = DateTimeOffset.UtcNow,
MessageId = Guid.NewGuid().ToString("N"),
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
index 70fcee15df..7c0479b85e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
@@ -79,8 +79,8 @@ private async ValueTask UpdateThreadAsync(IEnumerable RunAsync(
+ protected override async
+ Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -101,8 +101,8 @@ Task RunAsync(
return merger.ComputeMerged(workflowThread.LastResponseId!, this.Id, this.Name);
}
- public override async
- IAsyncEnumerable RunStreamingAsync(
+ protected override async
+ IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs b/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs
index 21fbfda639..542bafdbf4 100644
--- a/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs
@@ -17,18 +17,18 @@ namespace Microsoft.Agents.AI;
///
internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
{
- /// The delegate to use as the implementation of .
+ /// The delegate to use as the implementation of .
private readonly Func, AgentThread?, AgentRunOptions?, AIAgent, CancellationToken, Task>? _runFunc;
- /// The delegate to use as the implementation of .
+ /// The delegate to use as the implementation of .
///
- /// When non-, this delegate is used as the implementation of and
+ /// When non-, this delegate is used as the implementation of and
/// will be invoked with the same arguments as the method itself.
- /// When , will delegate directly to the inner agent.
+ /// When , will delegate directly to the inner agent.
///
private readonly Func, AgentThread?, AgentRunOptions?, AIAgent, CancellationToken, IAsyncEnumerable>? _runStreamingFunc;
- /// The delegate to use as the implementation of both and .
+ /// The delegate to use as the implementation of both and .
private readonly Func, AgentThread?, AgentRunOptions?, Func, AgentThread?, AgentRunOptions?, CancellationToken, Task>, CancellationToken, Task>? _sharedFunc;
///
@@ -36,7 +36,7 @@ internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
///
/// The inner agent.
///
- /// A delegate that provides the implementation for both and .
+ /// A delegate that provides the implementation for both and .
/// In addition to the arguments for the operation, it's provided with a delegate to the inner agent that should be
/// used to perform the operation on the inner agent. It will handle both the non-streaming and streaming cases.
///
@@ -61,13 +61,13 @@ public AnonymousDelegatingAIAgent(
///
/// The inner agent.
///
- /// A delegate that provides the implementation for . When ,
- /// must be non-null, and the implementation of
+ /// A delegate that provides the implementation for . When ,
+ /// must be non-null, and the implementation of
/// will use for the implementation.
///
///
- /// A delegate that provides the implementation for . When ,
- /// must be non-null, and the implementation of
+ /// A delegate that provides the implementation for . When ,
+ /// must be non-null, and the implementation of
/// will use for the implementation.
///
/// is .
@@ -85,7 +85,7 @@ public AnonymousDelegatingAIAgent(
}
///
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -132,7 +132,7 @@ await this._sharedFunc(
}
///
- public override IAsyncEnumerable RunStreamingAsync(
+ protected override IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
index a5a34d24a9..f4a7fcd9c2 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
@@ -149,7 +149,7 @@ public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options,
internal ChatOptions? ChatOptions => this._agentOptions?.ChatOptions;
///
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -193,7 +193,7 @@ private static IChatClient ApplyRunOptionsTransformations(AgentRunOptions? optio
}
///
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
index 7eefcebc55..2463b266c7 100644
--- a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
@@ -21,10 +21,10 @@ internal FunctionInvocationDelegatingAgent(AIAgent innerAgent, Func RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.RunAsync(messages, thread, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken);
- public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.RunStreamingAsync(messages, thread, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken);
// Decorate options to add the middleware function
diff --git a/dotnet/src/Microsoft.Agents.AI/LoggingAgent.cs b/dotnet/src/Microsoft.Agents.AI/LoggingAgent.cs
index b986e58bae..03b85d1ef5 100644
--- a/dotnet/src/Microsoft.Agents.AI/LoggingAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/LoggingAgent.cs
@@ -55,7 +55,7 @@ public JsonSerializerOptions JsonSerializerOptions
}
///
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
if (this._logger.IsEnabled(LogLevel.Debug))
@@ -72,7 +72,7 @@ public override async Task RunAsync(
try
{
- AgentRunResponse response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
+ AgentRunResponse response = await base.RunCoreAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
if (this._logger.IsEnabled(LogLevel.Debug))
{
@@ -101,7 +101,7 @@ public override async Task RunAsync(
}
///
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (this._logger.IsEnabled(LogLevel.Debug))
@@ -119,7 +119,7 @@ public override async IAsyncEnumerable RunStreamingAsync
IAsyncEnumerator e;
try
{
- e = base.RunStreamingAsync(messages, thread, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
+ e = base.RunCoreStreamingAsync(messages, thread, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (OperationCanceledException)
{
diff --git a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
index 7cd3c27b70..35d31371c3 100644
--- a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
@@ -78,7 +78,7 @@ public bool EnableSensitiveData
}
///
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
@@ -89,7 +89,7 @@ public override async Task RunAsync(
}
///
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
@@ -114,7 +114,9 @@ private void UpdateCurrentActivity(Activity? previousActivity)
// Override information set by OpenTelemetryChatClient to make it specific to invoke_agent.
- activity.DisplayName = $"{OpenTelemetryConsts.GenAI.InvokeAgent} {this.DisplayName}";
+ activity.DisplayName = string.IsNullOrWhiteSpace(this.Name)
+ ? $"{OpenTelemetryConsts.GenAI.InvokeAgent} {this.Id}"
+ : $"{OpenTelemetryConsts.GenAI.InvokeAgent} {this.Name}({this.Id})";
activity.SetTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.InvokeAgent);
if (!string.IsNullOrWhiteSpace(this._providerName))
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
index 9869d47f6b..0b491fb303 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
@@ -42,16 +42,14 @@ public void Constructor_WithAllParameters_InitializesPropertiesCorrectly()
const string TestId = "test-id";
const string TestName = "test-name";
const string TestDescription = "test-description";
- const string TestDisplayName = "test-display-name";
// Act
- var agent = new A2AAgent(this._a2aClient, TestId, TestName, TestDescription, TestDisplayName);
+ var agent = new A2AAgent(this._a2aClient, TestId, TestName, TestDescription);
// Assert
Assert.Equal(TestId, agent.Id);
Assert.Equal(TestName, agent.Name);
Assert.Equal(TestDescription, agent.Description);
- Assert.Equal(TestDisplayName, agent.DisplayName);
}
[Fact]
@@ -70,7 +68,6 @@ public void Constructor_WithDefaultParameters_UsesBaseProperties()
Assert.NotEmpty(agent.Id);
Assert.Null(agent.Name);
Assert.Null(agent.Description);
- Assert.Equal(agent.Id, agent.DisplayName);
}
[Fact]
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs
index e21035003e..5b84324e8b 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs
@@ -19,10 +19,9 @@ public void GetAIAgent_WithAllParameters_ReturnsA2AAgentWithSpecifiedProperties(
const string TestId = "test-agent-id";
const string TestName = "Test Agent";
const string TestDescription = "This is a test agent description";
- const string TestDisplayName = "Test Display Name";
// Act
- var agent = a2aClient.GetAIAgent(TestId, TestName, TestDescription, TestDisplayName);
+ var agent = a2aClient.GetAIAgent(TestId, TestName, TestDescription);
// Assert
Assert.NotNull(agent);
@@ -30,6 +29,5 @@ public void GetAIAgent_WithAllParameters_ReturnsA2AAgentWithSpecifiedProperties(
Assert.Equal(TestId, agent.Id);
Assert.Equal(TestName, agent.Name);
Assert.Equal(TestDescription, agent.Description);
- Assert.Equal(TestDisplayName, agent.DisplayName);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs
index e3bda2081a..a1c8cb32bf 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
+using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -33,18 +34,20 @@ public AIAgentTests()
this._agentMock = new Mock { CallBase = true };
this._agentMock
- .Setup(x => x.RunAsync(
- It.IsAny>(),
- this._agentThreadMock.Object,
- It.IsAny(),
- It.IsAny()))
+ .Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.ReturnsAsync(this._invokeResponse);
this._agentMock
- .Setup(x => x.RunStreamingAsync(
- It.IsAny>(),
- this._agentThreadMock.Object,
- It.IsAny(),
- It.IsAny()))
+ .Protected()
+ .Setup>("RunCoreStreamingAsync",
+ ItExpr.IsAny>(),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.Returns(ToAsyncEnumerableAsync(this._invokeStreamingResponses));
}
@@ -64,13 +67,14 @@ public async Task InvokeWithoutMessageCallsMockedInvokeWithEmptyArrayAsync()
Assert.Equal(this._invokeResponse, response);
// Verify that the mocked method was called with the expected parameters
- this._agentMock.Verify(
- x => x.RunAsync(
- It.Is>(messages => messages.Count == 0),
- this._agentThreadMock.Object,
- options,
- cancellationToken),
- Times.Once);
+ this._agentMock
+ .Protected()
+ .Verify>("RunCoreAsync",
+ Times.Once(),
+ ItExpr.Is>(messages => !messages.Any()),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.Is(o => o == options),
+ ItExpr.Is(ct => ct == cancellationToken));
}
///
@@ -90,13 +94,14 @@ public async Task InvokeWithStringMessageCallsMockedInvokeWithMessageInCollectio
Assert.Equal(this._invokeResponse, response);
// Verify that the mocked method was called with the expected parameters
- this._agentMock.Verify(
- x => x.RunAsync(
- It.Is>(messages => messages.Count == 1 && messages.First().Text == Message),
- this._agentThreadMock.Object,
- options,
- cancellationToken),
- Times.Once);
+ this._agentMock
+ .Protected()
+ .Verify>("RunCoreAsync",
+ Times.Once(),
+ ItExpr.Is>(messages => messages.Count() == 1 && messages.First().Text == Message),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.Is(o => o == options),
+ ItExpr.Is(ct => ct == cancellationToken));
}
///
@@ -116,13 +121,14 @@ public async Task InvokeWithSingleMessageCallsMockedInvokeWithMessageInCollectio
Assert.Equal(this._invokeResponse, response);
// Verify that the mocked method was called with the expected parameters
- this._agentMock.Verify(
- x => x.RunAsync(
- It.Is>(messages => messages.Count == 1 && messages.First() == message),
- this._agentThreadMock.Object,
- options,
- cancellationToken),
- Times.Once);
+ this._agentMock
+ .Protected()
+ .Verify>("RunCoreAsync",
+ Times.Once(),
+ ItExpr.Is>(messages => messages.Count() == 1 && messages.First() == message),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.Is(o => o == options),
+ ItExpr.Is(ct => ct == cancellationToken));
}
///
@@ -144,13 +150,14 @@ public async Task InvokeStreamingWithoutMessageCallsMockedInvokeWithEmptyArrayAs
}
// Verify that the mocked method was called with the expected parameters
- this._agentMock.Verify(
- x => x.RunStreamingAsync(
- It.Is>(messages => messages.Count == 0),
- this._agentThreadMock.Object,
- options,
- cancellationToken),
- Times.Once);
+ this._agentMock
+ .Protected()
+ .Verify>("RunCoreStreamingAsync",
+ Times.Once(),
+ ItExpr.Is>(messages => !messages.Any()),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.Is(o => o == options),
+ ItExpr.Is(ct => ct == cancellationToken));
}
///
@@ -173,13 +180,14 @@ public async Task InvokeStreamingWithStringMessageCallsMockedInvokeWithMessageIn
}
// Verify that the mocked method was called with the expected parameters
- this._agentMock.Verify(
- x => x.RunStreamingAsync(
- It.Is>(messages => messages.Count == 1 && messages.First().Text == Message),
- this._agentThreadMock.Object,
- options,
- cancellationToken),
- Times.Once);
+ this._agentMock
+ .Protected()
+ .Verify>("RunCoreStreamingAsync",
+ Times.Once(),
+ ItExpr.Is>(messages => messages.Count() == 1 && messages.First().Text == Message),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.Is(o => o == options),
+ ItExpr.Is(ct => ct == cancellationToken));
}
///
@@ -202,13 +210,14 @@ public async Task InvokeStreamingWithSingleMessageCallsMockedInvokeWithMessageIn
}
// Verify that the mocked method was called with the expected parameters
- this._agentMock.Verify(
- x => x.RunStreamingAsync(
- It.Is>(messages => messages.Count == 1 && messages.First() == message),
- this._agentThreadMock.Object,
- options,
- cancellationToken),
- Times.Once);
+ this._agentMock
+ .Protected()
+ .Verify>("RunCoreStreamingAsync",
+ Times.Once(),
+ ItExpr.Is>(messages => messages.Count() == 1 && messages.First() == message),
+ ItExpr.Is(t => t == this._agentThreadMock.Object),
+ ItExpr.Is(o => o == options),
+ ItExpr.Is(ct => ct == cancellationToken));
}
[Fact]
@@ -375,14 +384,14 @@ public override AgentThread GetNewThread()
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
- public override IAsyncEnumerable RunStreamingAsync(
+ protected override IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs
index 50271b7eee..2a6cc7bb81 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs
@@ -38,19 +38,21 @@ public DelegatingAIAgentTests()
this._innerAgentMock.Setup(x => x.GetNewThread()).Returns(this._testThread);
this._innerAgentMock
- .Setup(x => x.RunAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
+ .Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.ReturnsAsync(this._testResponse);
this._innerAgentMock
- .Setup(x => x.RunStreamingAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
+ .Protected()
+ .Setup>("RunCoreStreamingAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.Returns(ToAsyncEnumerableAsync(this._testStreamingResponses));
this._delegatingAgent = new TestDelegatingAIAgent(this._innerAgentMock.Object);
@@ -159,7 +161,12 @@ public async Task RunAsyncDefaultsToInnerAgentAsync()
var innerAgentMock = new Mock();
innerAgentMock
- .Setup(x => x.RunAsync(expectedMessages, expectedThread, expectedOptions, expectedCancellationToken))
+ .Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.Is>(m => m == expectedMessages),
+ ItExpr.Is(t => t == expectedThread),
+ ItExpr.Is(o => o == expectedOptions),
+ ItExpr.Is(ct => ct == expectedCancellationToken))
.Returns(expectedResult.Task);
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
@@ -193,7 +200,12 @@ public async Task RunStreamingAsyncDefaultsToInnerAgentAsync()
var innerAgentMock = new Mock();
innerAgentMock
- .Setup(x => x.RunStreamingAsync(expectedMessages, expectedThread, expectedOptions, expectedCancellationToken))
+ .Protected()
+ .Setup>("RunCoreStreamingAsync",
+ ItExpr.Is>(m => m == expectedMessages),
+ ItExpr.Is(t => t == expectedThread),
+ ItExpr.Is(o => o == expectedOptions),
+ ItExpr.Is(ct => ct == expectedCancellationToken))
.Returns(ToAsyncEnumerableAsync(expectedResults));
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs
index d20bd9be00..09ee72504a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs
@@ -76,12 +76,12 @@ public override AgentThread GetNewThread()
throw new NotImplementedException();
}
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
- public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs
index 69560421cf..dfabaca64e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs
@@ -290,7 +290,7 @@ public override AgentThread DeserializeThread(JsonElement serializedThread, Json
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -305,7 +305,7 @@ public override async Task RunAsync(
return updates.ToAgentRunResponse();
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -358,7 +358,7 @@ public override AgentThread DeserializeThread(JsonElement serializedThread, Json
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
- public override async Task RunAsync(
+ protected override async Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -373,7 +373,7 @@ public override async Task RunAsync(
return updates.ToAgentRunResponse();
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs
index df8caea214..1777ff456a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs
@@ -303,12 +303,12 @@ public FakeForwardedPropsAgent()
public JsonElement ReceivedForwardedProperties { get; private set; }
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs
index c96f2d92d0..df51d1cbc4 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs
@@ -342,12 +342,12 @@ internal sealed class FakeStateAgent : AIAgent
{
public override string? Description => "Agent for state testing";
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
index 3e80a58369..402451b061 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
@@ -430,12 +430,12 @@ private sealed class MultiResponseAgent : AIAgent
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions);
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -519,12 +519,12 @@ private sealed class TestAgent : AIAgent
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions);
- public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
- public override async IAsyncEnumerable RunStreamingAsync(
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs
index 0ba879f024..c80cd73941 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs
@@ -19,6 +19,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi
private const string AzureFunctionsPort = "7071";
private const string AzuritePort = "10000";
private const string DtsPort = "8080";
+ private const string RedisPort = "6379";
private static readonly string s_dotnetTargetFramework = GetTargetFramework();
private static readonly HttpClient s_sharedHttpClient = new();
@@ -392,6 +393,136 @@ await this.WaitForConditionAsync(
});
}
+ [Fact]
+ public async Task ReliableStreamingSampleValidationAsync()
+ {
+ string samplePath = Path.Combine(s_samplesPath, "08_ReliableStreaming");
+ await this.RunSampleTestAsync(samplePath, async (logs) =>
+ {
+ Uri createUri = new($"http://localhost:{AzureFunctionsPort}/api/agent/create");
+ this._outputHelper.WriteLine($"Starting reliable streaming agent via POST request to {createUri}...");
+
+ // Test the agent endpoint with a simple prompt
+ const string RequestBody = "Plan a 3-day trip to Seattle. Include daily activities.";
+ using HttpContent content = new StringContent(RequestBody, Encoding.UTF8, "text/plain");
+ using HttpRequestMessage request = new(HttpMethod.Post, createUri)
+ {
+ Content = content
+ };
+ request.Headers.Add("Accept", "text/plain");
+
+ using HttpResponseMessage response = await s_sharedHttpClient.SendAsync(
+ request,
+ HttpCompletionOption.ResponseHeadersRead);
+
+ // The response should be successful
+ Assert.True(response.IsSuccessStatusCode, $"Agent request failed with status: {response.StatusCode}");
+ Assert.Equal("text/plain", response.Content.Headers.ContentType?.MediaType);
+
+ // The response headers should include the conversation ID
+ string? conversationId = response.Headers.GetValues("x-conversation-id")?.FirstOrDefault();
+ Assert.NotNull(conversationId);
+ Assert.NotEmpty(conversationId);
+ this._outputHelper.WriteLine($"Agent conversation ID: {conversationId}");
+
+ // Read the streamed response
+ using Stream responseStream = await response.Content.ReadAsStreamAsync();
+ using StreamReader reader = new(responseStream);
+ StringBuilder responseText = new();
+ char[] buffer = new char[1024];
+ int bytesRead;
+
+ // Read for a reasonable amount of time to get some content
+ using CancellationTokenSource readTimeout = new(TimeSpan.FromSeconds(30));
+ try
+ {
+ while (!readTimeout.Token.IsCancellationRequested)
+ {
+ bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length);
+ if (bytesRead == 0)
+ {
+ // Check if we've received enough content
+ if (responseText.Length > 50)
+ {
+ break;
+ }
+ await Task.Delay(100, readTimeout.Token);
+ continue;
+ }
+
+ responseText.Append(buffer, 0, bytesRead);
+ if (responseText.Length > 200)
+ {
+ // We've received enough content to validate
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Timeout is acceptable if we got some content
+ }
+
+ string responseContent = responseText.ToString();
+ Assert.True(responseContent.Length > 0, "Expected to receive some streamed content");
+ this._outputHelper.WriteLine($"Received {responseContent.Length} characters of streamed content");
+
+ // Test resumption by calling the stream endpoint
+ Uri streamUri = new($"http://localhost:{AzureFunctionsPort}/api/agent/stream/{conversationId}");
+ this._outputHelper.WriteLine($"Testing stream resumption via GET request to {streamUri}...");
+
+ using HttpRequestMessage streamRequest = new(HttpMethod.Get, streamUri);
+ streamRequest.Headers.Add("Accept", "text/plain");
+
+ using HttpResponseMessage streamResponse = await s_sharedHttpClient.SendAsync(
+ streamRequest,
+ HttpCompletionOption.ResponseHeadersRead);
+ Assert.True(streamResponse.IsSuccessStatusCode, $"Stream request failed with status: {streamResponse.StatusCode}");
+ Assert.Equal("text/plain", streamResponse.Content.Headers.ContentType?.MediaType);
+
+ // Verify the conversation ID header is present
+ string? resumedConversationId = streamResponse.Headers.GetValues("x-conversation-id")?.FirstOrDefault();
+ Assert.Equal(conversationId, resumedConversationId);
+
+ // Read some content from the resumed stream
+ using Stream resumedStream = await streamResponse.Content.ReadAsStreamAsync();
+ using StreamReader resumedReader = new(resumedStream);
+ StringBuilder resumedText = new();
+
+ using CancellationTokenSource resumedReadTimeout = new(TimeSpan.FromSeconds(10));
+ try
+ {
+ while (!resumedReadTimeout.Token.IsCancellationRequested)
+ {
+ bytesRead = await resumedReader.ReadAsync(buffer, 0, buffer.Length);
+ if (bytesRead == 0)
+ {
+ if (resumedText.Length > 50)
+ {
+ break;
+ }
+ await Task.Delay(100, resumedReadTimeout.Token);
+ continue;
+ }
+
+ resumedText.Append(buffer, 0, bytesRead);
+ if (resumedText.Length > 100)
+ {
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Timeout is acceptable if we got some content
+ }
+
+ string resumedContent = resumedText.ToString();
+ Assert.True(resumedContent.Length > 0, "Expected to receive some content from resumed stream");
+ this._outputHelper.WriteLine($"Received {resumedContent.Length} characters from resumed stream");
+ });
+ }
+
private async Task InvokeMcpToolAsync(McpClient mcpClient, string toolName, string query)
{
this._outputHelper.WriteLine($"Invoking MCP tool '{toolName}'...");
@@ -482,6 +613,21 @@ await this.WaitForConditionAsync(
message: "DTS emulator is running",
timeout: TimeSpan.FromSeconds(30));
}
+
+ // Start Redis if it's not already running
+ if (!await this.IsRedisRunningAsync())
+ {
+ await this.StartDockerContainerAsync(
+ containerName: "redis",
+ image: "redis:latest",
+ ports: ["-p", "6379:6379"]);
+
+ // Wait for Redis
+ await this.WaitForConditionAsync(
+ condition: this.IsRedisRunningAsync,
+ message: "Redis is running",
+ timeout: TimeSpan.FromSeconds(30));
+ }
}
private async Task IsAzuriteRunningAsync()
@@ -562,6 +708,49 @@ private async Task IsDtsEmulatorRunningAsync()
}
}
+ private async Task IsRedisRunningAsync()
+ {
+ this._outputHelper.WriteLine($"Checking if Redis is running at localhost:{RedisPort}...");
+
+ try
+ {
+ using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(30));
+ ProcessStartInfo startInfo = new()
+ {
+ FileName = "docker",
+ Arguments = "exec redis redis-cli ping",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using Process process = new() { StartInfo = startInfo };
+ if (!process.Start())
+ {
+ this._outputHelper.WriteLine("Failed to start docker exec command");
+ return false;
+ }
+
+ string output = await process.StandardOutput.ReadToEndAsync(timeoutCts.Token);
+ await process.WaitForExitAsync(timeoutCts.Token);
+
+ if (process.ExitCode == 0 && output.Contains("PONG", StringComparison.OrdinalIgnoreCase))
+ {
+ this._outputHelper.WriteLine("Redis is running");
+ return true;
+ }
+
+ this._outputHelper.WriteLine($"Redis is not running. Exit code: {process.ExitCode}, Output: {output}");
+ return false;
+ }
+ catch (Exception ex)
+ {
+ this._outputHelper.WriteLine($"Redis is not running: {ex.Message}");
+ return false;
+ }
+ }
+
private async Task StartDockerContainerAsync(string containerName, string image, string[] ports)
{
// Stop existing container if it exists
@@ -646,6 +835,7 @@ private Process StartFunctionApp(string samplePath, List logs)
startInfo.EnvironmentVariables["DURABLE_TASK_SCHEDULER_CONNECTION_STRING"] =
$"Endpoint=http://localhost:{DtsPort};TaskHub=default;Authentication=None";
startInfo.EnvironmentVariables["AzureWebJobsStorage"] = "UseDevelopmentStorage=true";
+ startInfo.EnvironmentVariables["REDIS_CONNECTION_STRING"] = $"localhost:{RedisPort}";
Process process = new() { StartInfo = startInfo };
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs
index b0ad7ec0fe..e6824a2dd4 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs
@@ -17,13 +17,13 @@ public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null) => new DummyAgentThread();
- public override Task RunAsync(
+ protected override Task RunCoreAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) => Task.FromResult(new AgentRunResponse([.. messages]));
- public override IAsyncEnumerable RunStreamingAsync(
+ protected override IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/AIAgentWithOpenAIExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/AIAgentWithOpenAIExtensionsTests.cs
index de8c459be0..60c37c9b82 100644
--- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/AIAgentWithOpenAIExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/AIAgentWithOpenAIExtensionsTests.cs
@@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Moq;
+using Moq.Protected;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
using ChatRole = Microsoft.Extensions.AI.ChatRole;
using OpenAIChatMessage = OpenAI.Chat.ChatMessage;
@@ -76,22 +77,28 @@ public async Task RunAsync_CallsUnderlyingAgentAsync()
var responseMessage = new ChatMessage(ChatRole.Assistant, [new TextContent(ResponseText)]);
mockAgent
- .Setup(a => a.RunAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.ReturnsAsync(new AgentRunResponse([responseMessage]));
// Act
var result = await mockAgent.Object.RunAsync(openAiMessages, mockThread.Object, options, cancellationToken);
// Assert
- mockAgent.Verify(
- a => a.RunAsync(
- It.Is>(msgs =>
+ mockAgent.Protected()
+ .Verify("RunCoreAsync",
+ Times.Once(),
+ ItExpr.Is>(msgs =>
msgs.ToList().Count == 1 &&
msgs.ToList()[0].Text == TestMessageText),
mockThread.Object,
options,
- cancellationToken),
- Times.Once);
+ cancellationToken
+ );
Assert.NotNull(result);
Assert.NotEmpty(result.Content);
@@ -160,7 +167,12 @@ public async Task RunStreamingAsync_CallsUnderlyingAgentAsync()
};
mockAgent
- .Setup(a => a.RunStreamingAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Protected()
+ .Setup>("RunCoreStreamingAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.Returns(ToAsyncEnumerableAsync(responseUpdates));
// Act
@@ -172,15 +184,16 @@ public async Task RunStreamingAsync_CallsUnderlyingAgentAsync()
}
// Assert
- mockAgent.Verify(
- a => a.RunStreamingAsync(
- It.Is>(msgs =>
+ mockAgent.Protected()
+ .Verify("RunCoreStreamingAsync",
+ Times.Once(),
+ ItExpr.Is>(msgs =>
msgs.ToList().Count == 1 &&
msgs.ToList()[0].Text == TestMessageText),
mockThread.Object,
options,
- cancellationToken),
- Times.Once);
+ cancellationToken
+ );
Assert.True(updateCount > 0, "Expected at least one streaming update");
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Purview.UnitTests/PurviewWrapperTests.cs b/dotnet/tests/Microsoft.Agents.AI.Purview.UnitTests/PurviewWrapperTests.cs
index 22b729dda4..eafc67f7fc 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Purview.UnitTests/PurviewWrapperTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Purview.UnitTests/PurviewWrapperTests.cs
@@ -8,6 +8,7 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
+using Moq.Protected;
namespace Microsoft.Agents.AI.Purview.UnitTests;
@@ -277,11 +278,13 @@ public async Task ProcessAgentContentAsync_WithBlockedPrompt_ReturnsBlockedMessa
Assert.Single(result.Messages);
Assert.Equal(ChatRole.System, result.Messages[0].Role);
Assert.Equal("Prompt blocked by policy", result.Messages[0].Text);
- mockAgent.Verify(x => x.RunAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()), Times.Never);
+
+ mockAgent.Protected().Verify("RunCoreAsync",
+ Times.Never(),
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny());
}
[Fact]
@@ -295,11 +298,12 @@ public async Task ProcessAgentContentAsync_WithAllowedPromptAndBlockedResponse_R
var mockAgent = new Mock();
var innerResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Sensitive response"));
- mockAgent.Setup(x => x.RunAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
+ mockAgent.Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.ReturnsAsync(innerResponse);
this._mockProcessor.SetupSequence(x => x.ProcessMessagesAsync(
@@ -333,11 +337,12 @@ public async Task ProcessAgentContentAsync_WithAllowedPromptAndResponse_ReturnsI
var mockAgent = new Mock();
var innerResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Safe response"));
- mockAgent.Setup(x => x.RunAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
+ mockAgent.Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
.ReturnsAsync(innerResponse);
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
@@ -375,11 +380,12 @@ public async Task ProcessAgentContentAsync_WithIgnoreExceptions_ContinuesOnError
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response from inner agent"));
var mockAgent = new Mock();
- mockAgent.Setup(x => x.RunAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
+ mockAgent.Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny