Skip to content

Commit de80543

Browse files
westey-mCopilot
andauthored
.NET: Adding AgentRunContext to allow accessing agent run info in external downstream components (microsoft#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>
1 parent 9e51e2f commit de80543

6 files changed

Lines changed: 630 additions & 7 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
status: proposed
3+
contact: westey-m
4+
date: 2026-01-27
5+
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3
6+
consulted:
7+
informed:
8+
---
9+
10+
# AgentRunContext for Agent Run
11+
12+
## Context and Problem Statement
13+
14+
During an agent run, various components involved in the execution (middleware, filters, tools, nested agents, etc.) may need access to contextual information about the current run, such as:
15+
16+
1. The agent that is executing the run
17+
2. The session associated with the run
18+
3. The request messages passed to the agent
19+
4. The run options controlling the agent's behavior
20+
21+
Additionally, some components may need to modify this context during execution, for example:
22+
23+
- Replacing the session with a different one
24+
- Modifying the request messages before they reach the agent core
25+
- Updating or replacing the run options entirely
26+
27+
Currently, there is no standardized way to access or modify this context from arbitrary code that executes during an agent run, especially from deeply nested call stacks where the context is not explicitly passed.
28+
29+
## Sample Scenario
30+
31+
When using an Agent as an AIFunction developers may want to pass context from the parent agent run to the child agent run. For example, the developer may want to copy chat history to the child agent, or share the same session across both agents.
32+
33+
To enable these scenarios, we need a way to access the parent agent run context, including e.g. the parent agent itself, the parent agent session, and the parent run options from function tool calls.
34+
35+
```csharp
36+
public static AIFunction AsAIFunctionWithSessionPropagation(this ChatClientAgent agent, AIFunctionFactoryOptions? options = null)
37+
{
38+
Throw.IfNull(agent);
39+
40+
[Description("Invoke an agent to retrieve some information.")]
41+
async Task<string> InvokeAgentAsync(
42+
[Description("Input query to invoke the agent.")] string query,
43+
CancellationToken cancellationToken)
44+
{
45+
// Get the session from the parent agent and pass it to the child agent.
46+
var session = AIAgent.CurrentRunContext?.Session;
47+
48+
// Alternatively, the developer may want to create a new session but copy over the chat history from the parent agent.
49+
// var parentChatHistory = AIAgent.CurrentRunContext?.Session?.GetService<IList<ChatMessage>>();
50+
// if (parentChatHistory != null)
51+
// {
52+
// var chp = new InMemoryChatHistoryProvider();
53+
// foreach (var message in parentChatHistory)
54+
// {
55+
// chp.Add(message);
56+
// }
57+
// session = agent.GetNewSession(chp);
58+
// }
59+
60+
var response = await agent.RunAsync(query, session: session, cancellationToken: cancellationToken).ConfigureAwait(false);
61+
return response.Text;
62+
}
63+
64+
options ??= new();
65+
options.Name ??= SanitizeAgentName(agent.Name);
66+
options.Description ??= agent.Description;
67+
68+
return AIFunctionFactory.Create(InvokeAgentAsync, options);
69+
}
70+
```
71+
72+
## Decision Drivers
73+
74+
- Components executing during an agent run need access to run context without explicit parameter passing through every layer
75+
- Context should flow naturally across async calls without manual propagation
76+
- The design should allow modification of context properties by agent decorators (e.g., replacing options or session)
77+
- Solution should be consistent with patterns used in similar frameworks (e.g., `FunctionInvokingChatClient.CurrentContext` `HttpContext.Current`, `Activity.Current`)
78+
79+
## Considered Options
80+
81+
- **Option 1**: Pass context explicitly through all method signatures
82+
- **Option 2**: Use `AsyncLocal<T>` to provide ambient context accessible anywhere during the run
83+
- **Option 3**: Use a combination of explicit parameters for `RunCoreAsync` and `AsyncLocal<T>` for ambient access
84+
85+
## Decision Outcome
86+
87+
Chosen option: **Option 3** - Combination of explicit parameters and AsyncLocal ambient access.
88+
89+
This approach provides the best of both worlds:
90+
91+
1. **Explicit parameters are passed to `RunCoreAsync`**: The core agent implementation receives the parameters explicitly, making it clear what data is available and enabling easy unit testing. Any modification of these in a decorator will require calling `RunAsync` on the inner agent with the updated parameters, which would result in the inner agent creating a new `AgentRunContext` instance.
92+
93+
```csharp
94+
public async Task<AgentResponse> RunAsync(
95+
IEnumerable<ChatMessage> messages,
96+
AgentSession? session = null,
97+
AgentRunOptions? options = null,
98+
CancellationToken cancellationToken = default)
99+
{
100+
101+
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
102+
return await this.RunCoreAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
103+
}
104+
```
105+
106+
2. **`AsyncLocal<AgentRunContext?>` for ambient access**: The context is stored in an `AsyncLocal<T>` field, making it accessible from any code executing during the agent run via a static property.
107+
108+
The main scenario for this is to allow deeply nested components (e.g., tools, chat client middleware) to access the context without needing to pass it through every method signature. These are external components that cannot easily be modified to accept additional parameters. For internal components, we prefer passing any parameters explicitly.
109+
110+
```csharp
111+
public static AgentRunContext? CurrentRunContext
112+
{
113+
get => s_currentContext.Value;
114+
protected set => s_currentContext.Value = value;
115+
}
116+
```
117+
118+
### AgentRunContext Design
119+
120+
The `AgentRunContext` class encapsulates all run-related state:
121+
122+
```csharp
123+
public class AgentRunContext
124+
{
125+
public AgentRunContext(
126+
AIAgent agent,
127+
AgentSession? session,
128+
IReadOnlyCollection<ChatMessage> requestMessages,
129+
AgentRunOptions? agentRunOptions)
130+
131+
public AIAgent Agent { get; }
132+
public AgentSession? Session { get; }
133+
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
134+
public AgentRunOptions? RunOptions { get; }
135+
}
136+
```
137+
138+
Key design decisions:
139+
140+
- **All properties are read-only**: While some of the sub-properties on the provided properties (like `AgentRunOptions.AllowBackgroundResponses`) may be mutable, the `AgentRunContext` itself is immutable and we want to discourage anyone modifying the values in the context. Modifying the context is unlikely to result in the desired behavior, as the values will typically already have been used by the time any custom code accesses them.
141+
142+
### Benefits
143+
144+
1. **Ambient Access**: Any code executing during the run can access context via `AIAgent.CurrentRunContext` without needing explicit parameters
145+
2. **Async Flow**: `AsyncLocal<T>` automatically flows across async/await boundaries
146+
3. **Modifiability**: Components can modify or replace session, messages, or options as needed
147+
4. **Testability**: The explicit parameter to `RunCoreAsync` makes unit testing straightforward

dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Diagnostics;
6+
using System.Linq;
7+
using System.Runtime.CompilerServices;
68
using System.Text.Json;
79
using System.Threading;
810
using System.Threading.Tasks;
@@ -22,6 +24,8 @@ namespace Microsoft.Agents.AI;
2224
[DebuggerDisplay("{DebuggerDisplay,nq}")]
2325
public abstract class AIAgent
2426
{
27+
private static readonly AsyncLocal<AgentRunContext?> s_currentContext = new();
28+
2529
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
2630
private string DebuggerDisplay =>
2731
this.Name is { } name ? $"Id = {this.Id}, Name = {name}" : $"Id = {this.Id}";
@@ -76,6 +80,18 @@ public abstract class AIAgent
7680
/// </remarks>
7781
public virtual string? Description { get; }
7882

83+
/// <summary>
84+
/// Gets or sets the <see cref="AgentRunContext"/> for the current agent run.
85+
/// </summary>
86+
/// <remarks>
87+
/// This value flows across async calls.
88+
/// </remarks>
89+
public static AgentRunContext? CurrentRunContext
90+
{
91+
get => s_currentContext.Value;
92+
protected set => s_currentContext.Value = value;
93+
}
94+
7995
/// <summary>Asks the <see cref="AIAgent"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
8096
/// <param name="serviceType">The type of object being requested.</param>
8197
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
@@ -252,8 +268,11 @@ public Task<AgentResponse> RunAsync(
252268
IEnumerable<ChatMessage> messages,
253269
AgentSession? session = null,
254270
AgentRunOptions? options = null,
255-
CancellationToken cancellationToken = default) =>
256-
this.RunCoreAsync(messages, session, options, cancellationToken);
271+
CancellationToken cancellationToken = default)
272+
{
273+
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
274+
return this.RunCoreAsync(messages, session, options, cancellationToken);
275+
}
257276

258277
/// <summary>
259278
/// Core implementation of the agent invocation logic with a collection of chat messages.
@@ -370,12 +389,22 @@ public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
370389
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
371390
/// </para>
372391
/// </remarks>
373-
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
392+
public async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
374393
IEnumerable<ChatMessage> messages,
375394
AgentSession? session = null,
376395
AgentRunOptions? options = null,
377-
CancellationToken cancellationToken = default) =>
378-
this.RunCoreStreamingAsync(messages, session, options, cancellationToken);
396+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
397+
{
398+
AgentRunContext context = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
399+
CurrentRunContext = context;
400+
await foreach (var update in this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
401+
{
402+
yield return update;
403+
404+
// Restore context again when resuming after the caller code executes.
405+
CurrentRunContext = context;
406+
}
407+
}
379408

380409
/// <summary>
381410
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Collections.Generic;
4+
using Microsoft.Extensions.AI;
5+
using Microsoft.Shared.Diagnostics;
6+
7+
namespace Microsoft.Agents.AI;
8+
9+
/// <summary>Provides context for an in-flight agent run.</summary>
10+
public sealed class AgentRunContext
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="AgentRunContext"/> class.
14+
/// </summary>
15+
/// <param name="agent">The <see cref="AIAgent"/> that is executing the current run.</param>
16+
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run if any.</param>
17+
/// <param name="requestMessages">The request messages passed into the current run.</param>
18+
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run.</param>
19+
public AgentRunContext(
20+
AIAgent agent,
21+
AgentSession? session,
22+
IReadOnlyCollection<ChatMessage> requestMessages,
23+
AgentRunOptions? agentRunOptions)
24+
{
25+
this.Agent = Throw.IfNull(agent);
26+
this.Session = session;
27+
this.RequestMessages = Throw.IfNull(requestMessages);
28+
this.RunOptions = agentRunOptions;
29+
}
30+
31+
/// <summary>Gets the <see cref="AIAgent"/> that is executing the current run.</summary>
32+
public AIAgent Agent { get; }
33+
34+
/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
35+
public AgentSession? Session { get; }
36+
37+
/// <summary>Gets the request messages passed into the current run.</summary>
38+
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
39+
40+
/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
41+
public AgentRunOptions? RunOptions { get; }
42+
}

0 commit comments

Comments
 (0)