// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; /// /// Base class for multi-agent agent orchestration patterns. /// public abstract partial class OrchestratingAgent : AIAgent { /// Key used to persist state with the runtime. private const string StateKey = "State"; /// /// Initializes a new instance of the class. /// /// Specifies the agents participating in this orchestration. /// An optional name for this agent. protected OrchestratingAgent(IReadOnlyList agents, string? name = null) { _ = Throw.IfNullOrEmpty(agents); this.Agents = agents; this.Name = name; } /// public override string? Name { get; } /// /// Gets the list of member targets involved in the orchestration. /// protected IReadOnlyList Agents { get; } /// Gets the serializer options to use by the orchestration. public JsonSerializerOptions? SerializerOptions { get; set; } /// /// Gets the associated logger. /// public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance; /// /// Optional callback that is invoked for every agent response. /// public Func, ValueTask>? ResponseCallback { get; set; } /// /// Optional callback that is invoked for every agent update. /// public Func? StreamingResponseCallback { get; set; } /// public sealed override async Task RunAsync( IReadOnlyCollection messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNull(messages); if (thread is not null) { if (thread.MessageStore is null) { throw new InvalidOperationException("An agent service managed thread is not supported by this agent."); } List messagesList = []; await foreach (var threadMessage in thread.GetMessagesAsync(cancellationToken).ConfigureAwait(false)) { messagesList.Add(threadMessage); } messagesList.AddRange(messages); messages = messagesList; } var orchestrationResult = await this.RunAsync(messages, options, runtime: null, cancellationToken).ConfigureAwait(false); return await orchestrationResult.Task.ConfigureAwait(false); } /// public sealed override async IAsyncEnumerable RunStreamingAsync( IReadOnlyCollection messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // TODO: There should be a RunAsync overload that returns an OrchestratingAgentStreamingResponse, which this then delegates to. var response = await this.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false); foreach (var update in response.ToAgentRunResponseUpdates()) { yield return update; } } /// /// Initiates processing of the orchestration. /// /// The input message. /// Optional parameters for agent invocation. /// The runtime associated with the orchestration. /// The to monitor for cancellation requests. The default is . public async ValueTask RunAsync( IReadOnlyCollection messages, AgentRunOptions? options = null, IActorRuntimeContext? runtime = null, CancellationToken cancellationToken = default) { Throw.IfNull(messages, nameof(messages)); cancellationToken.ThrowIfCancellationRequested(); ILogger logger = this.LoggerFactory.CreateLogger(this.GetType().Name); OrchestratingAgentContext context = new() { OrchestratingAgent = this, Runtime = runtime, Options = options, Logger = logger, }; LogOrchestrationInvoked(logger, this.DisplayName, context.Id); CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cancellationToken = cts.Token; JsonElement? checkpoint = await this.ReadCheckpointAsync(context, cancellationToken).ConfigureAwait(false); Task completion = checkpoint is null ? this.RunCoreAsync(messages, context, cancellationToken) : this.ResumeCoreAsync(checkpoint.Value, context, cancellationToken); if (logger.IsEnabled(LogLevel.Trace)) { _ = LogCompletionAsync(logger, context, completion); } return new OrchestratingAgentResponse(context, completion, cts, logger); } /// /// Initiates processing of the orchestration. /// /// The input message. /// The context for this operation. /// A cancellation token that can be used to cancel the operation. protected abstract Task RunCoreAsync(IReadOnlyCollection messages, OrchestratingAgentContext context, CancellationToken cancellationToken); /// /// Resumes processing of the orchestration. /// /// The last checkpoint state available from which to resume the operation. /// The context for this operation. /// A cancellation token that can be used to cancel the operation. protected abstract Task ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken); /// /// Runs the agent with input messages and respond with both streamed and regular messages. /// /// The agent being run /// The associated orchestration context for this run. /// The list of chat messages to send. /// Options to use when invoking the agent. /// A cancellation token that can be used to cancel the operation. /// A task that returns the response . protected static async ValueTask RunAsync(AIAgent agent, OrchestratingAgentContext context, IReadOnlyCollection input, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { // Utilize streaming iff a streaming callback is provided; otherwise, use the non-streaming API. AgentRunResponse response; if (context.OrchestratingAgent?.StreamingResponseCallback is { } streamingCallback) { // For streaming, enumerate all the updates, invoking the callback for each, and storing them all. // Then convert them all into a single response instance. List updates = []; await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, options: options ?? context.Options, cancellationToken: cancellationToken).ConfigureAwait(false)) { updates.Add(update); await streamingCallback(update).ConfigureAwait(false); } response = updates.ToAgentRunResponse(); } else { // For non-streaming, just invoke the non-streaming method and get back the response. response = await agent.RunAsync(input, options: options ?? context.Options, cancellationToken: cancellationToken).ConfigureAwait(false); } // Regardless of whether we invoked streaming callbacks for individual updates, invoke the non-streaming callback with the final response instance. // This can be used as an indication of completeness if someone otherwise only cares about the streaming updates. if (context.OrchestratingAgent?.ResponseCallback is { } responseCallback) { await responseCallback.Invoke(response.Messages).ConfigureAwait(false); } return response; } /// Writes the specified checkpoint state to the runtime. /// The state to persist. /// The context for the orchestrating operation. /// A cancellation token that can be used to cancel the operation. /// A Task that completes when the asynchronous operation quiesces. protected async Task WriteCheckpointAsync(JsonElement state, OrchestratingAgentContext context, CancellationToken cancellationToken) { _ = Throw.IfNull(context); if (context.Runtime is not null) { while (true) { var response = await context.Runtime.WriteAsync( new ActorWriteOperationBatch(context.ETag ?? "", [new SetValueOperation(StateKey, state)]), cancellationToken).ConfigureAwait(false); if (response.Success) { break; } // If the write failed, there was a concurrency conflict where someone else updated the state. // But we don't actually care about consistency between the previous checkpoint and the current one, // so we just retry the write with the new etag. context.ETag = response.ETag; } } } /// Read checkpoint information, if it exists, for the specified context. /// The context for the orchestrating operation. /// A cancellation token that can be used to cancel the operation. /// The loaded state, or null if it doesn't exist. protected async ValueTask ReadCheckpointAsync(OrchestratingAgentContext context, CancellationToken cancellationToken) { _ = Throw.IfNull(context); if (context.Runtime is not null) { ReadResponse response = await context.Runtime.ReadAsync( new ActorReadOperationBatch([new GetValueOperation(StateKey)]), cancellationToken).ConfigureAwait(false); context.ETag = response.ETag; if (response.Results is { } results && results[results.Count - 1] is GetValueResult { Value: not null } getValueResult) { return getValueResult.Value.Value; } } return default; } [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} started ('{Id}')")] private static partial void LogOrchestrationInvoked(ILogger logger, string orchestration, string id); [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed ('{Id}'). Result: '{Result}'")] private static partial void LogOrchestrationResult(ILogger logger, string orchestration, string id, string result); [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} cancellation requested ('{Id}')")] internal static partial void LogOrchestrationCancellationRequested(ILogger logger, string orchestration, string id); [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} failed ('{Id}')")] private static partial void LogOrchestrationFailure(ILogger logger, string orchestration, string id, Exception error); [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} invoking agent '{Agent}' ('{Id}')")] private static partial void LogOrchestrationSubagentRunning(ILogger logger, string orchestration, string id, string agent); [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed agent '{Agent}' ('{Id}')")] private static partial void LogOrchestrationSubagentCompleted(ILogger logger, string orchestration, string id, string agent); private protected void LogOrchestrationSubagentRunning(OrchestratingAgentContext context, AIAgent agent) => LogOrchestrationSubagentRunning(context.Logger, context.ToString(), context.Id, agent.DisplayName); private protected void LogOrchestrationSubagentCompleted(OrchestratingAgentContext context, AIAgent agent) => LogOrchestrationSubagentCompleted(context.Logger, context.ToString(), context.Id, agent.DisplayName); private static async Task LogCompletionAsync(ILogger logger, OrchestratingAgentContext context, Task completion) { try { AgentRunResponse result = await completion.ConfigureAwait(false); if (logger.IsEnabled(LogLevel.Trace)) { JsonSerializerOptions jso = context.OrchestratingAgent?.SerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions; LogOrchestrationResult(logger, context.ToString(), context.Id, JsonSerializer.Serialize(result, jso.GetTypeInfo(typeof(AgentRunResponse)))); } } catch (Exception ex) { LogOrchestrationFailure(logger, context.ToString(), context.Id, ex); } } }