.NET: Adding AgentRunContext to allow accessing agent run info in external downstream components (#3476)

* Add an AsyncLocal AgentRunContext

* Update AgentRunContext session naming

* Make AgentRunContext readonly and add ADR

* Make session nullable and add unit tests

* Add unit tests for setting the context in AIAgent

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix sample in ADR

* Fix broken unit test

* Add unit test for checking if middleware can access AgentRunContext

* Fix build error after merge.

* Fix AgentRunContextTests after merge from main

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
westey
2026-02-05 09:45:24 +00:00
committed by GitHub
co-authored by Copilot
parent 9e51e2f0bc
commit de80543302
6 changed files with 630 additions and 7 deletions
@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -22,6 +24,8 @@ namespace Microsoft.Agents.AI;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract class AIAgent
{
private static readonly AsyncLocal<AgentRunContext?> s_currentContext = new();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay =>
this.Name is { } name ? $"Id = {this.Id}, Name = {name}" : $"Id = {this.Id}";
@@ -76,6 +80,18 @@ public abstract class AIAgent
/// </remarks>
public virtual string? Description { get; }
/// <summary>
/// Gets or sets the <see cref="AgentRunContext"/> for the current agent run.
/// </summary>
/// <remarks>
/// This value flows across async calls.
/// </remarks>
public static AgentRunContext? CurrentRunContext
{
get => s_currentContext.Value;
protected set => s_currentContext.Value = value;
}
/// <summary>Asks the <see cref="AIAgent"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
@@ -252,8 +268,11 @@ public abstract class AIAgent
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreAsync(messages, session, options, cancellationToken);
CancellationToken cancellationToken = default)
{
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
return this.RunCoreAsync(messages, session, options, cancellationToken);
}
/// <summary>
/// Core implementation of the agent invocation logic with a collection of chat messages.
@@ -370,12 +389,22 @@ public abstract class AIAgent
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
/// </para>
/// </remarks>
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
public async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreStreamingAsync(messages, session, options, cancellationToken);
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
AgentRunContext context = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
CurrentRunContext = context;
await foreach (var update in this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
// Restore context again when resuming after the caller code executes.
CurrentRunContext = context;
}
}
/// <summary>
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>Provides context for an in-flight agent run.</summary>
public sealed class AgentRunContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentRunContext"/> class.
/// </summary>
/// <param name="agent">The <see cref="AIAgent"/> that is executing the current run.</param>
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run if any.</param>
/// <param name="requestMessages">The request messages passed into the current run.</param>
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run.</param>
public AgentRunContext(
AIAgent agent,
AgentSession? session,
IReadOnlyCollection<ChatMessage> requestMessages,
AgentRunOptions? agentRunOptions)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.RunOptions = agentRunOptions;
}
/// <summary>Gets the <see cref="AIAgent"/> that is executing the current run.</summary>
public AIAgent Agent { get; }
/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
public AgentSession? Session { get; }
/// <summary>Gets the request messages passed into the current run.</summary>
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
public AgentRunOptions? RunOptions { get; }
}