From 4df3496ef63e642cb98bd08927f039675ecee8bc Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 23 Jul 2026 07:34:06 -0700 Subject: [PATCH 1/6] Simplify AG-UI Step04 human-in-the-loop sample to idiomatic pattern The Step04 sample previously wrapped both the server and client agents in custom ServerFunctionApproval*Agent middleware (~470 lines across two files) to marshal a bespoke approval protocol over AG-UI. This is no longer needed: MapAGUIServer natively emits the tool-approval interrupt when the model calls an ApprovalRequiredAIFunction, and AGUIChatClient natively transports the client's ToolApprovalResponseContent decision back to resume the run. Changes: - Server: map the ChatClientAgent directly with MapAGUIServer; remove the ServerFunctionApprovalAgent wrapper, the JsonOptions plumbing, and the ApprovalJsonContext registration. - Client: use the AGUIChatClient-backed agent directly; the existing loop already handles ToolApprovalRequestContent -> CreateResponse idiomatically. - Delete ServerFunctionApprovalServerAgent.cs and ServerFunctionApprovalClientAgent.cs. Verified end-to-end (approval request -> approve -> tool executes -> final response) against GitHub Models. Both projects build with 0 warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1 --- .../AGUI/Step04_HumanInLoop/Client/Program.cs | 20 +- .../ServerFunctionApprovalClientAgent.cs | 265 ------------------ .../AGUI/Step04_HumanInLoop/Server/Program.cs | 15 +- .../ServerFunctionApprovalServerAgent.cs | 249 ---------------- 4 files changed, 6 insertions(+), 543 deletions(-) delete mode 100644 dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs delete mode 100644 dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs index 788ad4fb3d3..4567c589bb3 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs @@ -15,17 +15,12 @@ AGUIChatClient chatClient = new(new(httpClient, serverUrl)); -// Create agent -ChatClientAgent baseAgent = chatClient.AsAIAgent( +// Create agent. No custom approval agent is required: the loop below handles the approval interrupt +// directly, and AGUIChatClient transports the decision back to the server via the AG-UI resume mechanism. +AIAgent agent = chatClient.AsAIAgent( name: "AGUIAssistant", instructions: "You are a helpful assistant."); -// Use default JSON serializer options -JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default; - -// Wrap the agent with ServerFunctionApprovalClientAgent -ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions); - List messages = []; AgentSession? session = null; @@ -68,15 +63,6 @@ ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved); - if (approvalRequest.AdditionalProperties != null) - { - approvalResponse.AdditionalProperties = []; - foreach (var kvp in approvalRequest.AdditionalProperties) - { - approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value; - } - } - approvalResponses.Add(approvalResponse); break; diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs deleted file mode 100644 index 135d87bffde..00000000000 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using ServerFunctionApproval; - -/// -/// A delegating agent that handles server function approval requests and responses. -/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent -/// and the server's request_approval tool call pattern. -/// -internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent -{ - private readonly JsonSerializerOptions _jsonSerializerOptions; - - public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) - : base(innerAgent) - { - this._jsonSerializerOptions = jsonSerializerOptions; - } - - protected override Task RunCoreAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - CancellationToken cancellationToken = default) - { - return this.RunCoreStreamingAsync(messages, session, options, cancellationToken) - .ToAgentResponseAsync(cancellationToken); - } - - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Process and transform approval messages, creating a new message list - var processedMessages = ProcessOutgoingServerFunctionApprovals(messages.ToList(), this._jsonSerializerOptions); - - // Run the inner agent and intercept any approval requests - await foreach (var update in this.InnerAgent.RunStreamingAsync( - processedMessages, session, options, cancellationToken).ConfigureAwait(false)) - { - yield return ProcessIncomingServerApprovalRequests(update, this._jsonSerializerOptions); - } - } - -#pragma warning disable MEAI001 // Type is for evaluation purposes only - private static FunctionResultContent ConvertApprovalResponseToToolResult(ToolApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions) - { - return new FunctionResultContent( - callId: approvalResponse.RequestId, - result: JsonSerializer.SerializeToElement( - new ApprovalResponse - { - ApprovalId = approvalResponse.RequestId, - Approved = approvalResponse.Approved - }, - jsonOptions)); - } - - private static List CopyMessagesUpToIndex(List messages, int index) - { - var result = new List(index); - for (int i = 0; i < index; i++) - { - result.Add(messages[i]); - } - return result; - } - - private static List CopyContentsUpToIndex(IList contents, int index) - { - var result = new List(index); - for (int i = 0; i < index; i++) - { - result.Add(contents[i]); - } - return result; - } - - private static List ProcessOutgoingServerFunctionApprovals( - List messages, - JsonSerializerOptions jsonSerializerOptions) - { - List? result = null; - - Dictionary approvalRequests = []; - for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++) - { - var message = messages[messageIndex]; - List? transformedContents = null; - - // Process each content item in the message - HashSet approvalCalls = []; - for (var contentIndex = 0; contentIndex < message.Contents.Count; contentIndex++) - { - var content = message.Contents[contentIndex]; - - // Handle pending approval requests (transform to tool call) - if (content is ToolApprovalRequestContent approvalRequest && - approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true && - originalFunction is FunctionCallContent original) - { - approvalRequests[approvalRequest.RequestId] = approvalRequest; - transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); - transformedContents.Add(original); - } - // Handle pending approval responses (transform to tool result) - else if (content is ToolApprovalResponseContent approvalResponse && - approvalRequests.TryGetValue(approvalResponse.RequestId, out var correspondingRequest)) - { - transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); - transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions)); - approvalRequests.Remove(approvalResponse.RequestId); - correspondingRequest.AdditionalProperties?.Remove("original_function"); - } - // Skip historical approval content - else if (content is FunctionCallContent { Name: "request_approval" } approvalCall) - { - transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); - approvalCalls.Add(approvalCall.CallId); - } - else if (content is FunctionResultContent functionResult && - approvalCalls.Contains(functionResult.CallId)) - { - transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); - approvalCalls.Remove(functionResult.CallId); - } - else - { - transformedContents?.Add(content); - } - } - - if (transformedContents?.Count == 0) - { - continue; - } - else if (transformedContents != null) - { - // We made changes to contents, so use transformedContents - var newMessage = new ChatMessage(message.Role, transformedContents) - { - AuthorName = message.AuthorName, - MessageId = message.MessageId, - CreatedAt = message.CreatedAt, - RawRepresentation = message.RawRepresentation, - AdditionalProperties = message.AdditionalProperties - }; - result ??= CopyMessagesUpToIndex(messages, messageIndex); - result.Add(newMessage); - } - else - { - // We're already copying messages, so copy this unchanged message too - result?.Add(message); - } - // If result is null, we haven't made any changes yet, so keep processing - } - - return result ?? messages; - } - - private static AgentResponseUpdate ProcessIncomingServerApprovalRequests( - AgentResponseUpdate update, - JsonSerializerOptions jsonSerializerOptions) - { - IList? updatedContents = null; - for (var i = 0; i < update.Contents.Count; i++) - { - var content = update.Contents[i]; - if (content is FunctionCallContent { Name: "request_approval" } request) - { - updatedContents ??= [.. update.Contents]; - - // Serialize the function arguments as JsonElement - ApprovalRequest? approvalRequest; - if (request.Arguments?.TryGetValue("request", out var reqObj) == true && - reqObj is JsonElement je) - { - approvalRequest = (ApprovalRequest?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))); - } - else - { - approvalRequest = null; - } - - if (approvalRequest == null) - { - throw new InvalidOperationException("Failed to deserialize approval request."); - } - - var functionCallArgs = (Dictionary?)approvalRequest.FunctionArguments? - .Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary))); - - var approvalRequestContent = new ToolApprovalRequestContent( - requestId: approvalRequest.ApprovalId, - new FunctionCallContent( - callId: approvalRequest.ApprovalId, - name: approvalRequest.FunctionName, - arguments: functionCallArgs)); - - approvalRequestContent.AdditionalProperties ??= []; - approvalRequestContent.AdditionalProperties["original_function"] = content; - - updatedContents[i] = approvalRequestContent; - } - } - - if (updatedContents is not null) - { - var chatUpdate = update.AsChatResponseUpdate(); - return new AgentResponseUpdate(new ChatResponseUpdate() - { - Role = chatUpdate.Role, - Contents = updatedContents, - MessageId = chatUpdate.MessageId, - AuthorName = chatUpdate.AuthorName, - CreatedAt = chatUpdate.CreatedAt, - RawRepresentation = chatUpdate.RawRepresentation, - ResponseId = chatUpdate.ResponseId, - AdditionalProperties = chatUpdate.AdditionalProperties - }) - { - AgentId = update.AgentId, - ContinuationToken = update.ContinuationToken, - }; - } - - return update; - } -} -#pragma warning restore MEAI001 - -namespace ServerFunctionApproval -{ - public sealed class ApprovalRequest - { - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } - - [JsonPropertyName("function_name")] - public required string FunctionName { get; init; } - - [JsonPropertyName("function_arguments")] - public JsonElement? FunctionArguments { get; init; } - - [JsonPropertyName("message")] - public string? Message { get; init; } - } - - public sealed class ApprovalResponse - { - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } - - [JsonPropertyName("approved")] - public required bool Approved { get; init; } - } -} diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs index 570c02b8bbb..c61d41f5175 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -5,12 +5,9 @@ using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; -using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.HttpLogging; using Microsoft.Extensions.AI; -using Microsoft.Extensions.Options; using OpenAI.Chat; -using ServerFunctionApproval; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -23,8 +20,6 @@ }); builder.Services.AddHttpClient().AddLogging(); -builder.Services.ConfigureHttpJsonOptions(options => - options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default)); builder.Services.AddAGUIServer(); // WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, @@ -48,9 +43,6 @@ static string ApproveExpenseReport(string expenseReportId) return $"Expense report {expenseReportId} approved"; } -// Get JsonSerializerOptions -var jsonOptions = app.Services.GetRequiredService>().Value; - // Create approval-required tool #pragma warning disable MEAI001 // Type is for evaluation purposes only AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))]; @@ -70,8 +62,7 @@ static string ApproveExpenseReport(string expenseReportId) instructions: "You are a helpful assistant in charge of approving expenses", tools: tools); -// Wrap with ServerFunctionApprovalAgent -var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions); - -app.MapAGUIServer("/", agent); +// No custom approval protocol is required: MapAGUIServer emits the approval interrupt natively when the +// model calls the approval-required tool, and resumes the run when the client sends the decision back. +app.MapAGUIServer("/", baseAgent); await app.RunAsync(); diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs deleted file mode 100644 index 8c1f27eea9e..00000000000 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using ServerFunctionApproval; - -/// -/// A delegating agent that handles function approval requests on the server side. -/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent -/// and the request_approval tool call pattern for client communication. -/// -internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent -{ - private readonly JsonSerializerOptions _jsonSerializerOptions; - - public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) - : base(innerAgent) - { - this._jsonSerializerOptions = jsonSerializerOptions; - } - - protected override Task RunCoreAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - CancellationToken cancellationToken = default) - { - return this.RunCoreStreamingAsync(messages, session, options, cancellationToken) - .ToAgentResponseAsync(cancellationToken); - } - - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Process and transform incoming approval responses from client, creating a new message list - var processedMessages = ProcessIncomingFunctionApprovals(messages.ToList(), this._jsonSerializerOptions); - - // Run the inner agent and intercept any approval requests - await foreach (var update in this.InnerAgent.RunStreamingAsync( - processedMessages, session, options, cancellationToken).ConfigureAwait(false)) - { - yield return ProcessOutgoingApprovalRequests(update, this._jsonSerializerOptions); - } - } - -#pragma warning disable MEAI001 // Type is for evaluation purposes only - private static ToolApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions) - { - if (toolCall.Name != "request_approval" || toolCall.Arguments == null) - { - throw new InvalidOperationException("Invalid request_approval tool call"); - } - - var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) && - reqObj is JsonElement argsElement && - argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest && - approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call"); - return new ToolApprovalRequestContent( - requestId: request.ApprovalId, - new FunctionCallContent( - callId: request.ApprovalId, - name: request.FunctionName, - arguments: request.FunctionArguments)); - } - - private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions) - { - var approvalResponse = (result.Result is JsonElement je ? - (ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) : - result.Result is string str ? - (ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) : - result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result"); - return approval.CreateResponse(approvalResponse.Approved); - } -#pragma warning restore MEAI001 - - private static List CopyMessagesUpToIndex(List messages, int index) - { - var result = new List(index); - for (int i = 0; i < index; i++) - { - result.Add(messages[i]); - } - return result; - } - - private static List CopyContentsUpToIndex(IList contents, int index) - { - var result = new List(index); - for (int i = 0; i < index; i++) - { - result.Add(contents[i]); - } - return result; - } - - private static List ProcessIncomingFunctionApprovals( - List messages, - JsonSerializerOptions jsonSerializerOptions) - { - List? result = null; - - // Track approval ID to original call ID mapping - _ = new Dictionary(); -#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - Dictionary trackedRequestApprovalToolCalls = []; // Remote approvals - for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++) - { - var message = messages[messageIndex]; - List? transformedContents = null; - for (int j = 0; j < message.Contents.Count; j++) - { - var content = message.Contents[j]; - if (content is FunctionCallContent { Name: "request_approval" } toolCall) - { - result ??= CopyMessagesUpToIndex(messages, messageIndex); - transformedContents ??= CopyContentsUpToIndex(message.Contents, j); - var approvalRequest = ConvertToolCallToApprovalRequest(toolCall, jsonSerializerOptions); - transformedContents.Add(approvalRequest); - trackedRequestApprovalToolCalls[toolCall.CallId] = approvalRequest; - result.Add(new ChatMessage(message.Role, transformedContents) - { - AuthorName = message.AuthorName, - MessageId = message.MessageId, - CreatedAt = message.CreatedAt, - RawRepresentation = message.RawRepresentation, - AdditionalProperties = message.AdditionalProperties - }); - } - else if (content is FunctionResultContent toolResult && - trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval)) - { - result ??= CopyMessagesUpToIndex(messages, messageIndex); - transformedContents ??= CopyContentsUpToIndex(message.Contents, j); - var approvalResponse = ConvertToolResultToApprovalResponse(toolResult, approval, jsonSerializerOptions); - transformedContents.Add(approvalResponse); - result.Add(new ChatMessage(message.Role, transformedContents) - { - AuthorName = message.AuthorName, - MessageId = message.MessageId, - CreatedAt = message.CreatedAt, - RawRepresentation = message.RawRepresentation, - AdditionalProperties = message.AdditionalProperties - }); - } - else - { - result?.Add(message); - } - } - } -#pragma warning restore MEAI001 - - return result ?? messages; - } - - private static AgentResponseUpdate ProcessOutgoingApprovalRequests( - AgentResponseUpdate update, - JsonSerializerOptions jsonSerializerOptions) - { - IList? updatedContents = null; - for (var i = 0; i < update.Contents.Count; i++) - { - var content = update.Contents[i]; -#pragma warning disable MEAI001 // Type is for evaluation purposes only - if (content is ToolApprovalRequestContent request && request.ToolCall is FunctionCallContent functionCall) - { - updatedContents ??= [.. update.Contents]; - var approvalId = request.RequestId; - - var approvalData = new ApprovalRequest - { - ApprovalId = approvalId, - FunctionName = functionCall.Name, - FunctionArguments = functionCall.Arguments, - Message = $"Approve execution of '{functionCall.Name}'?" - }; - - updatedContents[i] = new FunctionCallContent( - callId: approvalId, - name: "request_approval", - arguments: new Dictionary { ["request"] = approvalData }); - } -#pragma warning restore MEAI001 - } - - if (updatedContents is not null) - { - var chatUpdate = update.AsChatResponseUpdate(); - // Yield a tool call update that represents the approval request - return new AgentResponseUpdate(new ChatResponseUpdate() - { - Role = chatUpdate.Role, - Contents = updatedContents, - MessageId = chatUpdate.MessageId, - AuthorName = chatUpdate.AuthorName, - CreatedAt = chatUpdate.CreatedAt, - RawRepresentation = chatUpdate.RawRepresentation, - ResponseId = chatUpdate.ResponseId, - AdditionalProperties = chatUpdate.AdditionalProperties - }) - { - AgentId = update.AgentId, - ContinuationToken = update.ContinuationToken - }; - } - - return update; - } -} - -namespace ServerFunctionApproval -{ - // Define approval models - public sealed class ApprovalRequest - { - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } - - [JsonPropertyName("function_name")] - public required string FunctionName { get; init; } - - [JsonPropertyName("function_arguments")] - public IDictionary? FunctionArguments { get; init; } - - [JsonPropertyName("message")] - public string? Message { get; init; } - } - - public sealed class ApprovalResponse - { - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } - - [JsonPropertyName("approved")] - public required bool Approved { get; init; } - } - - [JsonSerializable(typeof(ApprovalRequest))] - [JsonSerializable(typeof(ApprovalResponse))] - [JsonSerializable(typeof(Dictionary))] - public sealed partial class ApprovalJsonContext : JsonSerializerContext; -} From 9c68eb73e93e5be358545bb703c2af179185f859 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:03:29 +0100 Subject: [PATCH 2/6] Update AG-UI Step04 README to describe native approval flow The Step04 human-in-the-loop sample no longer uses the custom ServerFunctionApprovalServerAgent / ServerFunctionApprovalClientAgent wrappers. Update the README so it describes the idiomatic native flow: the server maps a plain agent with MapAGUIServer and relies on ApprovalRequiredAIFunction to raise the approval interrupt, and the client handles ToolApprovalRequestContent and replies with ToolApprovalResponseContent. --- dotnet/samples/02-agents/AGUI/README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/dotnet/samples/02-agents/AGUI/README.md b/dotnet/samples/02-agents/AGUI/README.md index 004b9f3b4e4..558ac3932e9 100644 --- a/dotnet/samples/02-agents/AGUI/README.md +++ b/dotnet/samples/02-agents/AGUI/README.md @@ -133,10 +133,8 @@ Demonstrates human-in-the-loop approval workflows for sensitive operations. This An AG-UI server that implements approval workflows. Demonstrates: -- Wrapping tools with `ApprovalRequiredAIFunction` -- Converting `FunctionApprovalRequestContent` to approval requests -- Middleware pattern with `ServerFunctionApprovalServerAgent` -- Complete function call capture and restoration +- Wrapping a tool with `ApprovalRequiredAIFunction` so it requires approval before running +- Mapping a plain agent with `MapAGUIServer`, which natively emits an approval interrupt when the model calls the approval-required tool and resumes the run once the client sends the decision back **Run the server:** @@ -149,12 +147,10 @@ dotnet run --urls http://localhost:8888 An interactive client that handles approval requests from the server. Demonstrates: -- Using `ServerFunctionApprovalClientAgent` middleware -- Detecting `FunctionApprovalRequestContent` -- Displaying approval details to users -- Prompting for approval/rejection -- Sending approval responses with `FunctionApprovalResponseContent` -- Resuming conversation after approval +- Detecting `ToolApprovalRequestContent` in the streamed response +- Displaying approval details to the user and prompting for approval or rejection +- Sending the decision back as a `ToolApprovalResponseContent` created with `approvalRequest.CreateResponse(approved)` +- Resuming the run so the server continues after the decision is received **Run the client:** From f80a0cbeed869e21b4a6b48977e605b7e88c7f23 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:09:06 +0100 Subject: [PATCH 3/6] Fix AG-UI Step04 README server port to match client default The Step04 client defaults to http://localhost:5100 (and the server launchSettings also uses 5100), but the README told users to run the server on port 8888, so the client could not reach it. Align the Step04 server run command to 5100. Other steps intentionally keep 8888 because their clients default to that port. --- dotnet/samples/02-agents/AGUI/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/02-agents/AGUI/README.md b/dotnet/samples/02-agents/AGUI/README.md index 558ac3932e9..5fcb3444069 100644 --- a/dotnet/samples/02-agents/AGUI/README.md +++ b/dotnet/samples/02-agents/AGUI/README.md @@ -140,7 +140,7 @@ An AG-UI server that implements approval workflows. Demonstrates: ```bash cd Step04_HumanInLoop/Server -dotnet run --urls http://localhost:8888 +dotnet run --urls http://localhost:5100 ``` #### Client (`Step04_HumanInLoop/Client`) From 3aa47c3a1105c17111dd24556dfff6ca93797bc0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Sat, 25 Jul 2026 16:04:56 -0700 Subject: [PATCH 4/6] Update AG-UI .NET samples for latest MAF + AG-UI SDK and align with docs - Bump AGUI.* packages 0.0.3 to 0.0.4 (Directory.Packages.props) - Step01/02/03: drop AddHttpClient().AddLogging() server noise and simplify the client run-started output to match the getting-started doc (no thread plumbing) - Step04 (HITL): remove HTTP body logging and MEAI001 pragmas, give the approval tool an explicit name, and align the resume decision message with the doc - Step05 (state): replace the custom SharedStateAgent/StatefulAgent DataContent pattern (dropped by released AGUI.Server) with declarative AGUIStreamOptions.MapResultAsStateSnapshot plus a thin RecipeStateAgent that reads RunAgentInput.State, and align the Recipe models with the docs - Refresh README to the shipped API (MapAGUIServer, ApprovalRequiredAIFunction, declarative state) Verified: all 10 sample projects build; Step04 approval/resume and Step05 state snapshot round-trip run end-to-end against GitHub Models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1 --- dotnet/Directory.Packages.props | 10 +- dotnet/samples/02-agents/AGUI/README.md | 14 +- .../Step01_GettingStarted/Client/Program.cs | 9 +- .../Step01_GettingStarted/Server/Program.cs | 1 - .../Step02_BackendTools/Client/Program.cs | 9 +- .../Step02_BackendTools/Server/Program.cs | 1 - .../Step03_FrontendTools/Client/Program.cs | 9 +- .../Step03_FrontendTools/Server/Program.cs | 1 - .../AGUI/Step04_HumanInLoop/Client/Program.cs | 6 +- .../AGUI/Step04_HumanInLoop/Server/Program.cs | 23 +-- .../Step05_StateManagement/Client/Program.cs | 191 +++++++++--------- .../Client/StatefulAgent.cs | 87 -------- .../Step05_StateManagement/Server/Program.cs | 69 ++++--- .../Server/RecipeModels.cs | 52 +++-- .../Server/RecipeStateAgent.cs | 46 +++++ .../Server/SharedStateAgent.cs | 159 --------------- 16 files changed, 245 insertions(+), 442 deletions(-) delete mode 100644 dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/StatefulAgent.cs create mode 100644 dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs delete mode 100644 dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 0d4e07415d4..b5d8c38fc8d 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -54,11 +54,11 @@ - - - - - + + + + + diff --git a/dotnet/samples/02-agents/AGUI/README.md b/dotnet/samples/02-agents/AGUI/README.md index 5fcb3444069..3168545aabc 100644 --- a/dotnet/samples/02-agents/AGUI/README.md +++ b/dotnet/samples/02-agents/AGUI/README.md @@ -51,7 +51,7 @@ dotnet run --urls http://localhost:8888 An interactive console client that connects to an AG-UI server. Demonstrates: - Creating an AG-UI client with `AGUIChatClient` -- Managing conversation threads +- Managing multi-turn conversations with an `AgentSession` - Streaming responses with `RunStreamingAsync` - Displaying colored console output for different content types - Supporting both interactive and automated modes @@ -163,15 +163,15 @@ Try asking the agent to perform sensitive operations like "Approve expense repor ### Step05_StateManagement -An AG-UI server and client that demonstrate state management with predictive updates. +An AG-UI server and client that demonstrate shared state management. #### Server (`Step05_StateManagement/Server`) Demonstrates: -- Defining state schemas using C# records -- Using `SharedStateAgent` middleware for state management -- Streaming predictive state updates with `AgentState` content +- Exposing a `generate_recipe` tool that returns the complete recipe +- Mapping the tool result to a `STATE_SNAPSHOT` event with `AGUIStreamOptions.MapResultAsStateSnapshot` +- Reading the client's current recipe from `RunAgentInput.State` - Managing shared state between client and server - Using JSON serialization contexts for state types @@ -206,7 +206,7 @@ dotnet run ### Client-Side -1. `AGUIAgent` sends HTTP POST request to server +1. `AGUIChatClient` sends HTTP POST request to server 2. Server responds with SSE stream 3. Client parses events into `AgentResponseUpdate` objects 4. Updates are displayed based on content type @@ -224,7 +224,7 @@ dotnet run `ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace. -If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone. +If your ASP.NET Core host shares session storage across users, pair `MapAGUIServer` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone. ## Troubleshooting diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Client/Program.cs index bb18aa9a959..a788c36bdf9 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Client/Program.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using AGUI.Abstractions; using AGUI.Client; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; @@ -50,7 +49,6 @@ // Stream the response bool isFirstUpdate = true; - string? threadId = null; await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) { @@ -59,11 +57,8 @@ // First update indicates run started if (isFirstUpdate) { - // AGUIChatClient is stateless and never surfaces a ConversationId; the thread - // id is carried on the AG-UI RUN_STARTED event's raw representation. - threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]"); + Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]"); Console.ResetColor(); isFirstUpdate = false; } @@ -87,7 +82,7 @@ } Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.WriteLine("\n[Run Finished]"); Console.ResetColor(); } } diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs index 407bf1c4e82..4fcd27ef4b5 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs @@ -7,7 +7,6 @@ using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUIServer(); // WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Client/Program.cs index e6a8e834fbc..0433e8c0770 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Client/Program.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using AGUI.Abstractions; using AGUI.Client; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; @@ -50,7 +49,6 @@ // Stream the response bool isFirstUpdate = true; - string? threadId = null; await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) { @@ -59,11 +57,8 @@ // First update indicates run started if (isFirstUpdate) { - // AGUIChatClient is stateless and never surfaces a ConversationId; the thread - // id is carried on the AG-UI RUN_STARTED event's raw representation. - threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]"); + Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]"); Console.ResetColor(); isFirstUpdate = false; } @@ -119,7 +114,7 @@ } Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.WriteLine("\n[Run Finished]"); Console.ResetColor(); } } diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs index 83cf2500c9d..28e3e885521 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs @@ -11,7 +11,6 @@ using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient().AddLogging(); builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default)); builder.Services.AddAGUIServer(); diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Client/Program.cs index 5b01e70ded5..3a0cdfda588 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Client/Program.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; -using AGUI.Abstractions; using AGUI.Client; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; @@ -63,7 +62,6 @@ static string GetUserLocation() // Stream the response bool isFirstUpdate = true; - string? threadId = null; await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) { @@ -72,11 +70,8 @@ static string GetUserLocation() // First update indicates run started if (isFirstUpdate) { - // AGUIChatClient is stateless and never surfaces a ConversationId; the thread - // id is carried on the AG-UI RUN_STARTED event's raw representation. - threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]"); + Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]"); Console.ResetColor(); isFirstUpdate = false; } @@ -112,7 +107,7 @@ static string GetUserLocation() } Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.WriteLine("\n[Run Finished]"); Console.ResetColor(); } } diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs index 407bf1c4e82..4fcd27ef4b5 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs @@ -7,7 +7,6 @@ using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUIServer(); // WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs index 4567c589bb3..c4ce0d3f54f 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Client/Program.cs @@ -39,7 +39,6 @@ messages.Add(new ChatMessage(ChatRole.User, input)); Console.WriteLine(); -#pragma warning disable MEAI001 List approvalResponses = []; do @@ -101,11 +100,10 @@ messages.AddRange(response.Messages); foreach (AIContent approvalResponse in approvalResponses) { - messages.Add(new ChatMessage(ChatRole.Tool, [approvalResponse])); + messages.Add(new ChatMessage(ChatRole.User, [approvalResponse])); } } while (approvalResponses.Count > 0); -#pragma warning restore MEAI001 Console.WriteLine("\n"); Console.ForegroundColor = ConsoleColor.White; @@ -113,7 +111,6 @@ Console.ResetColor(); } -#pragma warning disable MEAI001 static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, FunctionCallContent fcc) { Console.ForegroundColor = ConsoleColor.Yellow; @@ -135,4 +132,3 @@ static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, F Console.WriteLine("============================================================"); Console.ResetColor(); } -#pragma warning restore MEAI001 diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs index c61d41f5175..a73465c1d3b 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -5,21 +5,10 @@ using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; -using Microsoft.AspNetCore.HttpLogging; using Microsoft.Extensions.AI; using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -builder.Services.AddHttpLogging(logging => -{ - logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody - | HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody; - logging.RequestBodyLogLimit = int.MaxValue; - logging.ResponseBodyLogLimit = int.MaxValue; -}); - -builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUIServer(); // WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, @@ -29,8 +18,6 @@ WebApplication app = builder.Build(); -app.UseHttpLogging(); - string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] @@ -43,10 +30,12 @@ static string ApproveExpenseReport(string expenseReportId) return $"Expense report {expenseReportId} approved"; } -// Create approval-required tool -#pragma warning disable MEAI001 // Type is for evaluation purposes only -AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))]; -#pragma warning restore MEAI001 +// Wrap the tool in ApprovalRequiredAIFunction so the run interrupts for approval before it executes. +AITool[] tools = +[ + new ApprovalRequiredAIFunction( + AIFunctionFactory.Create(ApproveExpenseReport, name: "approve_expense_report")) +]; // Create base agent // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs index fbda62cc92a..746d42cc9f7 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json; using System.Text.Json.Serialization; @@ -20,16 +20,15 @@ AGUIChatClient chatClient = new(new(httpClient, serverUrl)); -AIAgent baseAgent = chatClient.AsAIAgent( +AIAgent agent = chatClient.AsAIAgent( name: "recipe-client", description: "AG-UI Recipe Client Agent"); -// Wrap the base agent with state management -JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web) -{ - TypeInfoResolver = RecipeSerializerContext.Default -}; -StatefulAgent agent = new(baseAgent, jsonOptions, new AgentState()); +JsonSerializerOptions jsonOptions = RecipeSerializerContext.Default.Options; + +// The recipe lives on the client. It is sent to the server on every turn (so the agent edits the +// existing recipe) and refreshed from each STATE_SNAPSHOT the server streams back. +Recipe currentRecipe = new(); AgentSession session = await agent.CreateSessionAsync(); List messages = @@ -42,7 +41,7 @@ while (true) { // Get user input - Console.Write("\nUser (:q to quit, :state to show state): "); + Console.Write("\nUser (:q to quit, :state to show recipe): "); string? message = Console.ReadLine(); if (string.IsNullOrWhiteSpace(message)) @@ -58,36 +57,51 @@ if (message.Equals(":state", StringComparison.OrdinalIgnoreCase)) { - DisplayState(agent.State.Recipe); + DisplayRecipe(currentRecipe); continue; } messages.Add(new ChatMessage(ChatRole.User, message)); + // Send the client's current recipe on the AG-UI RunAgentInput.State so the agent builds on it. + JsonElement stateJson = JsonSerializer.SerializeToElement( + new RecipeResponse { Recipe = currentRecipe }, jsonOptions); + ChatClientAgentRunOptions runOptions = new() + { + ChatOptions = new ChatOptions + { + RawRepresentationFactory = _ => new RunAgentInput { State = stateJson } + } + }; + // Stream the response bool isFirstUpdate = true; - string? threadId = null; - bool stateReceived = false; - Console.WriteLine(); - await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) + await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, runOptions)) { ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); // First update indicates run started if (isFirstUpdate) { - // AGUIChatClient is stateless and never surfaces a ConversationId; the thread - // id is carried on the AG-UI RUN_STARTED event's raw representation. - threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]"); + Console.WriteLine($"[Run Started - Run: {chatUpdate.ResponseId}]"); Console.ResetColor(); isFirstUpdate = false; } - // Display streaming content + // A STATE_SNAPSHOT arrives as a StateSnapshotEvent on the update's raw representation. + if (chatUpdate.RawRepresentation is StateSnapshotEvent snapshot && + snapshot.Snapshot.Deserialize(jsonOptions) is { } response) + { + currentRecipe = response.Recipe; + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("\n[State Snapshot Received]"); + Console.ResetColor(); + } + + // Display streaming text content foreach (AIContent content in update.Contents) { switch (content) @@ -98,14 +112,6 @@ Console.ResetColor(); break; - case DataContent dataContent when dataContent.MediaType == "application/json": - // This is a state snapshot - the StatefulAgent has already updated the state - stateReceived = true; - Console.ForegroundColor = ConsoleColor.Blue; - Console.WriteLine("\n[State Snapshot Received]"); - Console.ResetColor(); - break; - case ErrorContent errorContent: Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"\n[Error: {errorContent.Message}]"); @@ -116,14 +122,10 @@ } Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.WriteLine("\n[Run Finished]"); Console.ResetColor(); - // Display final state if received - if (stateReceived) - { - DisplayState(agent.State.Recipe); - } + DisplayRecipe(currentRecipe); } } catch (Exception ex) @@ -131,61 +133,53 @@ Console.WriteLine($"\nAn error occurred: {ex.Message}"); } -static void DisplayState(RecipeState? state) +static void DisplayRecipe(Recipe recipe) { - if (state == null) - { - Console.ForegroundColor = ConsoleColor.Gray; - Console.WriteLine("\n[No state available]"); - Console.ResetColor(); - return; - } - Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine("\n" + new string('=', 60)); - Console.WriteLine("CURRENT STATE"); + Console.WriteLine("CURRENT RECIPE"); Console.WriteLine(new string('=', 60)); Console.ResetColor(); - if (!string.IsNullOrEmpty(state.Title)) + if (string.IsNullOrEmpty(recipe.Title)) { - Console.WriteLine("\nRecipe:"); - Console.WriteLine($" Title: {state.Title}"); - if (!string.IsNullOrEmpty(state.Cuisine)) - { - Console.WriteLine($" Cuisine: {state.Cuisine}"); - } - - if (!string.IsNullOrEmpty(state.SkillLevel)) + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine("\n[No recipe yet]"); + Console.ResetColor(); + } + else + { + Console.WriteLine($"\n Title: {recipe.Title}"); + if (!string.IsNullOrEmpty(recipe.SkillLevel)) { - Console.WriteLine($" Skill Level: {state.SkillLevel}"); + Console.WriteLine($" Skill Level: {recipe.SkillLevel}"); } - if (state.PrepTimeMinutes > 0) + if (!string.IsNullOrEmpty(recipe.CookingTime)) { - Console.WriteLine($" Prep Time: {state.PrepTimeMinutes} minutes"); + Console.WriteLine($" Cooking Time: {recipe.CookingTime}"); } - if (state.CookTimeMinutes > 0) + if (recipe.SpecialPreferences.Count > 0) { - Console.WriteLine($" Cook Time: {state.CookTimeMinutes} minutes"); + Console.WriteLine($" Preferences: {string.Join(", ", recipe.SpecialPreferences)}"); } - if (state.Ingredients.Count > 0) + if (recipe.Ingredients.Count > 0) { Console.WriteLine("\n Ingredients:"); - foreach (var ingredient in state.Ingredients) + foreach (Ingredient ingredient in recipe.Ingredients) { - Console.WriteLine($" - {ingredient}"); + Console.WriteLine($" {ingredient.Icon} {ingredient.Name} - {ingredient.Amount}"); } } - if (state.Steps.Count > 0) + if (recipe.Instructions.Count > 0) { - Console.WriteLine("\n Steps:"); - for (int i = 0; i < state.Steps.Count; i++) + Console.WriteLine("\n Instructions:"); + for (int i = 0; i < recipe.Instructions.Count; i++) { - Console.WriteLine($" {i + 1}. {state.Steps[i]}"); + Console.WriteLine($" {i + 1}. {recipe.Instructions[i]}"); } } } @@ -195,40 +189,53 @@ static void DisplayState(RecipeState? state) Console.ResetColor(); } -// State wrapper -internal sealed class AgentState +namespace RecipeClient { - [JsonPropertyName("recipe")] - public RecipeState Recipe { get; set; } = new(); -} + // State response wrapper. Its shape mirrors what the server returns and renders as state. + internal sealed class RecipeResponse + { + [JsonPropertyName("recipe")] + public Recipe Recipe { get; set; } = new(); + } -// Recipe state model -internal sealed class RecipeState -{ - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; + // Recipe state model. + internal sealed class Recipe + { + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; - [JsonPropertyName("cuisine")] - public string Cuisine { get; set; } = string.Empty; + [JsonPropertyName("skill_level")] + public string SkillLevel { get; set; } = string.Empty; - [JsonPropertyName("ingredients")] - public List Ingredients { get; set; } = []; + [JsonPropertyName("cooking_time")] + public string CookingTime { get; set; } = string.Empty; - [JsonPropertyName("steps")] - public List Steps { get; set; } = []; + [JsonPropertyName("special_preferences")] + public List SpecialPreferences { get; set; } = []; - [JsonPropertyName("prep_time_minutes")] - public int PrepTimeMinutes { get; set; } + [JsonPropertyName("ingredients")] + public List Ingredients { get; set; } = []; - [JsonPropertyName("cook_time_minutes")] - public int CookTimeMinutes { get; set; } + [JsonPropertyName("instructions")] + public List Instructions { get; set; } = []; + } - [JsonPropertyName("skill_level")] - public string SkillLevel { get; set; } = string.Empty; -} + // A single ingredient. + internal sealed class Ingredient + { + [JsonPropertyName("icon")] + public string Icon { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; -// JSON serialization context -[JsonSerializable(typeof(AgentState))] -[JsonSerializable(typeof(RecipeState))] -[JsonSerializable(typeof(JsonElement))] -internal sealed partial class RecipeSerializerContext : JsonSerializerContext; + [JsonPropertyName("amount")] + public string Amount { get; set; } = string.Empty; + } + + // JSON serialization context. + [JsonSerializable(typeof(RecipeResponse))] + [JsonSerializable(typeof(Recipe))] + [JsonSerializable(typeof(Ingredient))] + internal sealed partial class RecipeSerializerContext : JsonSerializerContext; +} diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/StatefulAgent.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/StatefulAgent.cs deleted file mode 100644 index 8a8062befe6..00000000000 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/StatefulAgent.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Runtime.CompilerServices; -using System.Text.Json; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -namespace RecipeClient; - -/// -/// A delegating agent that manages client-side state and automatically attaches it to requests. -/// -/// The state type. -internal sealed class StatefulAgent : DelegatingAIAgent - where TState : class, new() -{ - private readonly JsonSerializerOptions _jsonSerializerOptions; - - /// - /// Gets or sets the current state. - /// - public TState State { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The underlying agent to delegate to. - /// The JSON serializer options for state serialization. - /// The initial state. If null, a new instance will be created. - public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions, TState? initialState = null) - : base(innerAgent) - { - this._jsonSerializerOptions = jsonSerializerOptions; - this.State = initialState ?? new TState(); - } - - /// - protected override Task RunCoreAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - CancellationToken cancellationToken = default) - { - return this.RunCoreStreamingAsync(messages, session, options, cancellationToken) - .ToAgentResponseAsync(cancellationToken); - } - - /// - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Add state to messages - List messagesWithState = [.. messages]; - - // Serialize the state using AgentState wrapper - byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( - this.State, - this._jsonSerializerOptions.GetTypeInfo(typeof(TState))); - DataContent stateContent = new(stateBytes, "application/json"); - ChatMessage stateMessage = new(ChatRole.System, [stateContent]); - messagesWithState.Add(stateMessage); - - // Stream the response and update state when received - await foreach (AgentResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, session, options, cancellationToken)) - { - // Check if this update contains a state snapshot - foreach (AIContent content in update.Contents) - { - if (content is DataContent dataContent && dataContent.MediaType == "application/json") - { - // Deserialize the state - if (JsonSerializer.Deserialize( - dataContent.Data.Span, - this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState) - { - this.State = newState; - } - } - } - - yield return update; - } - } -} diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs index 2232311630c..0b270263b76 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs @@ -1,15 +1,16 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using AGUI.Server; using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.AI; using OpenAI.Chat; using RecipeAssistant; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient().AddLogging(); builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default)); builder.Services.AddAGUIServer(); @@ -29,10 +30,32 @@ string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); -// Get JsonSerializerOptions -var jsonOptions = app.Services.GetRequiredService>().Value; +// The tool returns the complete recipe. The hosting layer turns each result into a STATE_SNAPSHOT +// event via AGUIStreamOptions.MapResultAsStateSnapshot("generate_recipe") - no protocol content by hand. +[Description("Generate or update the shared recipe and display it to the user.")] +static RecipeResponse GenerateRecipe( + [Description("The complete recipe to display.")] Recipe recipe) => new() { Recipe = recipe }; -// Create base agent +AITool generateRecipe = AIFunctionFactory.Create( + GenerateRecipe, + name: "generate_recipe", + description: "Generate or update the shared recipe and display it to the user.", + RecipeSerializerContext.Default.Options); + +const string SharedStateSystemPrompt = + """ + You are a helpful recipe assistant that maintains a shared recipe state with the user. + + IMPORTANT: + - When the user asks you to create, change, or improve a recipe, call the `generate_recipe` + tool with a COMPLETE recipe: a title, skill_level, cooking_time, special_preferences, the + full list of ingredients (each with an icon, name and amount) and the step-by-step + instructions. + - Always include every ingredient the recipe needs, keeping any the user already added. + - When the user only asks a question about the recipe, answer in plain text and do NOT call the tool. + """; + +// Create the AI agent with the recipe tool. // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. @@ -41,26 +64,22 @@ new DefaultAzureCredential()) .GetChatClient(deploymentName); -AIAgent baseAgent = chatClient.AsAIAgent( - name: "RecipeAgent", - instructions: """ - You are a helpful recipe assistant. When users ask you to create or suggest a recipe, - respond with a complete AgentState JSON object that includes: - - recipe.title: The recipe name - - recipe.cuisine: Type of cuisine (e.g., Italian, Mexican, Japanese) - - recipe.ingredients: Array of ingredient strings with quantities - - recipe.steps: Array of cooking instruction strings - - recipe.prep_time_minutes: Preparation time in minutes - - recipe.cook_time_minutes: Cooking time in minutes - - recipe.skill_level: One of "beginner", "intermediate", or "advanced" - - Always include all fields in the response. Be creative and helpful. - """); +AIAgent baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions +{ + Name = "RecipeAgent", + Description = "An agent that maintains a shared recipe state with the user.", + ChatOptions = new ChatOptions + { + Instructions = SharedStateSystemPrompt, + Tools = [generateRecipe], + }, +}); -// Wrap with state management middleware -AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions); +// Wrap with a thin agent that injects the client's current recipe (input side of shared state). +AIAgent agent = new RecipeStateAgent(baseAgent); -// Map the AG-UI agent endpoint -app.MapAGUIServer("/", agent); +// Map the AG-UI endpoint. A generate_recipe result becomes a STATE_SNAPSHOT event (output side). +app.MapAGUIServer("/", agent) + .WithMetadata(new AGUIStreamOptions().MapResultAsStateSnapshot("generate_recipe")); await app.RunAsync(); diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs index fc1d8320d22..0c0ab4776d5 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs @@ -1,43 +1,53 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; namespace RecipeAssistant; -// State wrapper -internal sealed class AgentState +// State response wrapper returned by the tool. Its shape is what the client renders as state. +internal sealed class RecipeResponse { [JsonPropertyName("recipe")] - public RecipeState Recipe { get; set; } = new(); + public Recipe Recipe { get; set; } = new(); } -// Recipe state model -internal sealed class RecipeState +// Recipe state model. +internal sealed class Recipe { [JsonPropertyName("title")] public string Title { get; set; } = string.Empty; - [JsonPropertyName("cuisine")] - public string Cuisine { get; set; } = string.Empty; + [JsonPropertyName("skill_level")] + public string SkillLevel { get; set; } = string.Empty; + + [JsonPropertyName("cooking_time")] + public string CookingTime { get; set; } = string.Empty; + + [JsonPropertyName("special_preferences")] + public List SpecialPreferences { get; set; } = []; [JsonPropertyName("ingredients")] - public List Ingredients { get; set; } = []; + public List Ingredients { get; set; } = []; - [JsonPropertyName("steps")] - public List Steps { get; set; } = []; + [JsonPropertyName("instructions")] + public List Instructions { get; set; } = []; +} - [JsonPropertyName("prep_time_minutes")] - public int PrepTimeMinutes { get; set; } +// A single ingredient. +internal sealed class Ingredient +{ + [JsonPropertyName("icon")] + public string Icon { get; set; } = string.Empty; - [JsonPropertyName("cook_time_minutes")] - public int CookTimeMinutes { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - [JsonPropertyName("skill_level")] - public string SkillLevel { get; set; } = string.Empty; + [JsonPropertyName("amount")] + public string Amount { get; set; } = string.Empty; } -// JSON serialization context -[JsonSerializable(typeof(AgentState))] -[JsonSerializable(typeof(RecipeState))] -[JsonSerializable(typeof(System.Text.Json.JsonElement))] +// JSON serialization context for the tool payloads. +[JsonSerializable(typeof(RecipeResponse))] +[JsonSerializable(typeof(Recipe))] +[JsonSerializable(typeof(Ingredient))] internal sealed partial class RecipeSerializerContext : JsonSerializerContext; diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs new file mode 100644 index 00000000000..f067f4744fd --- /dev/null +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using AGUI.Abstractions; +using AGUI.Server; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace RecipeAssistant; + +/// +/// A thin agent that reads the client's current recipe from the AG-UI +/// and prepends it to the conversation as a system message, so the model edits the existing recipe +/// instead of starting over. This handles the input side of shared state only. The output side is +/// declarative: the inner agent's generate_recipe tool result becomes a STATE_SNAPSHOT +/// via AGUIStreamOptions.MapResultAsStateSnapshot. +/// +internal sealed class RecipeStateAgent(AIAgent innerAgent) : DelegatingAIAgent(innerAgent) +{ + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + this.RunCoreStreamingAsync(messages, session, options, cancellationToken) + .ToAgentResponseAsync(cancellationToken); + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + if (options is ChatClientAgentRunOptions { ChatOptions: { } chatOptions } && + chatOptions.TryGetRunAgentInput(out RunAgentInput? input) && + input.State is { ValueKind: JsonValueKind.Object } state) + { + ChatMessage stateMessage = new( + ChatRole.System, + $"The user's current recipe state is:\n{state.GetRawText()}"); + messages = [stateMessage, .. messages]; + } + + return this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken); + } +} diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs deleted file mode 100644 index 032ce5976ec..00000000000 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Runtime.CompilerServices; -using System.Text.Json; -using AGUI.Abstractions; -using AGUI.Server; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -namespace RecipeAssistant; - -internal sealed class SharedStateAgent : DelegatingAIAgent -{ - private readonly JsonSerializerOptions _jsonSerializerOptions; - - public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) - : base(innerAgent) - { - this._jsonSerializerOptions = jsonSerializerOptions; - } - - protected override Task RunCoreAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - CancellationToken cancellationToken = default) - { - return this.RunCoreStreamingAsync(messages, session, options, cancellationToken) - .ToAgentResponseAsync(cancellationToken); - } - - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, - AgentSession? session = null, - AgentRunOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Check if the client sent state in the request - if (options is not ChatClientAgentRunOptions { ChatOptions: { } chatOptions } chatRunOptions || - !chatOptions.TryGetRunAgentInput(out RunAgentInput? agentInput) || - agentInput.State is not { ValueKind: JsonValueKind.Object } state) - { - // No state management requested, pass through to inner agent - await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - yield break; - } - - // Check if state has properties (not empty {}) - bool hasProperties = false; - foreach (JsonProperty _ in state.EnumerateObject()) - { - hasProperties = true; - break; - } - - if (!hasProperties) - { - // Empty state - treat as no state - await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - yield break; - } - - // First run: Generate structured state update - var firstRunOptions = new ChatClientAgentRunOptions - { - ChatOptions = chatRunOptions.ChatOptions.Clone(), - AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses, - ContinuationToken = chatRunOptions.ContinuationToken, - ChatClientFactory = chatRunOptions.ChatClientFactory, - }; - - // Configure JSON schema response format for structured state output - firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema( - schemaName: "AgentState", - schemaDescription: "A response containing a recipe with title, skill level, cooking time, ingredients, and instructions"); - - // Add current state to the conversation - state is already a JsonElement - ChatMessage stateUpdateMessage = new( - ChatRole.System, - [ - new TextContent("Here is the current state in JSON format:"), - new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))), - new TextContent("The new state is:") - ]); - - var firstRunMessages = messages.Append(stateUpdateMessage); - - // Collect all updates from first run - var allUpdates = new List(); - await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false)) - { - allUpdates.Add(update); - - // Yield all non-text updates (tool calls, etc.) - bool hasNonTextContent = update.Contents.Any(c => c is not TextContent); - if (hasNonTextContent) - { - yield return update; - } - } - - var response = allUpdates.ToAgentResponse(); - - // Try to deserialize the structured state response - if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot)) - { - // Serialize and emit as STATE_SNAPSHOT via DataContent - byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( - stateSnapshot, - this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement))); - yield return new AgentResponseUpdate - { - Contents = [new DataContent(stateBytes, "application/json")] - }; - } - else - { - yield break; - } - - // Second run: Generate user-friendly summary - var secondRunMessages = messages.Concat(response.Messages).Append( - new ChatMessage( - ChatRole.System, - [new TextContent("Please provide a concise summary of the state changes in at most two sentences.")])); - - await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - } - - private static bool TryDeserialize(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput) - { - try - { - T? deserialized = JsonSerializer.Deserialize(json, jsonSerializerOptions); - if (deserialized is null) - { - structuredOutput = default!; - return false; - } - - structuredOutput = deserialized; - return true; - } - catch - { - structuredOutput = default!; - return false; - } - } -} From 279c3a9cf8249a1d865890fd14fbf825985dfb87 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Sat, 25 Jul 2026 19:00:44 -0700 Subject: [PATCH 5/6] Name the Step02 backend tool search_restaurants to match the docs Give the SearchRestaurants tool an explicit "search_restaurants" name so the client displays an accurate tool name (not a compiler-mangled local-function name) and stays aligned with the backend-tool-rendering doc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1 --- .../samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs index 28e3e885521..8a06328bd4c 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs @@ -74,6 +74,7 @@ static RestaurantSearchResponse SearchRestaurants( [ AIFunctionFactory.Create( SearchRestaurants, + name: "search_restaurants", serializerOptions: jsonOptions.SerializerOptions) ]; From 567a6e20c6d4b0b90afe79097c587354041d94bb Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 27 Jul 2026 07:59:04 -0700 Subject: [PATCH 6/6] Add UTF-8 BOM to Step05 sample files to satisfy check-format The check-format CI job enforces the repository's utf-8-bom charset rule via dotnet format. The Step05 files added in this PR were saved without a BOM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1 --- .../02-agents/AGUI/Step05_StateManagement/Client/Program.cs | 2 +- .../02-agents/AGUI/Step05_StateManagement/Server/Program.cs | 2 +- .../AGUI/Step05_StateManagement/Server/RecipeModels.cs | 2 +- .../AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs index 746d42cc9f7..ac3f4b939b0 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Client/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json; using System.Text.Json.Serialization; diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs index 0b270263b76..fb4b9620900 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; using AGUI.Server; diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs index 0c0ab4776d5..613f684d312 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeModels.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs index f067f4744fd..d18df8a599f 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/RecipeStateAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json; using AGUI.Abstractions;