// Copyright (c) Microsoft. All rights reserved. using System.Runtime.CompilerServices; using System.Text.Json; 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.AdditionalProperties: { } properties } chatRunOptions || !properties.TryGetValue("ag_ui_state", out object? stateObj) || stateObj is not JsonElement state || state.ValueKind != JsonValueKind.Object) { // 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; } } }