// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; namespace AGUIDojoServer.SharedState; [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")] 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, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken); } protected override async IAsyncEnumerable RunCoreStreamingAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions || !properties.TryGetValue("ag_ui_state", out JsonElement state)) { await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false)) { yield return update; } yield break; } 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: "RecipeResponse", schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions"); ChatMessage stateUpdateMessage = new( ChatRole.System, [ new TextContent("Here is the current state in JSON format:"), new TextContent(state.GetRawText()), new TextContent("The new state is:") ]); var firstRunMessages = messages.Append(stateUpdateMessage); var allUpdates = new List(); await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, 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(); if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot)) { byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( stateSnapshot, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement))); yield return new AgentResponseUpdate { Contents = [new DataContent(stateBytes, "application/json")] }; } else { yield break; } 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, thread, options, cancellationToken).ConfigureAwait(false)) { yield return update; } } }