-
Notifications
You must be signed in to change notification settings - Fork 1.6k
.NET: Adding AgentRunContext to allow accessing agent run info in external downstream components #3476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
.NET: Adding AgentRunContext to allow accessing agent run info in external downstream components #3476
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
d3b9639
Add an AsyncLocal AgentRunContext
westey-m 9cedaa5
Merge branch 'main' into agentruncontext
westey-m bc55ee2
Update AgentRunContext session naming
westey-m 8f23f22
Make AgentRunContext readonly and add ADR
westey-m e56f911
Make session nullable and add unit tests
westey-m 46c24e5
Merge branch 'main' into agentruncontext
westey-m 7677d18
Add unit tests for setting the context in AIAgent
westey-m 068f624
Apply suggestions from code review
westey-m 326b96d
Fix sample in ADR
westey-m cf34953
Merge branch 'agentruncontext' of https://github.com/westey-m/agent-f…
westey-m caf754c
Fix broken unit test
westey-m 0d205b3
Merge branch 'main' into agentruncontext
westey-m a29eb03
Add unit test for checking if middleware can access AgentRunContext
westey-m 086239b
Merge branch 'main' into agentruncontext
westey-m 7136683
Merge branch 'main' into agentruncontext
westey-m 91f77b8
Fix build error after merge.
westey-m efd2f43
Merge branch 'main' into agentruncontext
westey-m cebf817
Fix AgentRunContextTests after merge from main
westey-m 2b40b8e
Merge branch 'main' into agentruncontext
westey-m File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| --- | ||
| status: proposed | ||
| contact: westey-m | ||
| date: 2026-01-27 | ||
| deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3 | ||
| consulted: | ||
| informed: | ||
| --- | ||
|
|
||
| # AgentRunContext for Agent Run | ||
|
|
||
| ## Context and Problem Statement | ||
|
|
||
| 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: | ||
|
|
||
| 1. The agent that is executing the run | ||
| 2. The session associated with the run | ||
| 3. The request messages passed to the agent | ||
| 4. The run options controlling the agent's behavior | ||
|
|
||
| Additionally, some components may need to modify this context during execution, for example: | ||
|
|
||
| - Replacing the session with a different one | ||
| - Modifying the request messages before they reach the agent core | ||
| - Updating or replacing the run options entirely | ||
|
|
||
| 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. | ||
|
|
||
| ## Sample Scenario | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ```csharp | ||
| public static AIFunction AsAIFunctionWithSessionPropagation(this ChatClientAgent agent, AIFunctionFactoryOptions? options = null) | ||
| { | ||
| Throw.IfNull(agent); | ||
|
|
||
| [Description("Invoke an agent to retrieve some information.")] | ||
| async Task<string> InvokeAgentAsync( | ||
| [Description("Input query to invoke the agent.")] string query, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| // Get the session from the parent agent and pass it to the child agent. | ||
| var session = AIAgent.CurrentRunContext?.Session; | ||
|
|
||
| // Alternatively, the developer may want to create a new session but copy over the chat history from the parent agent. | ||
| // var parentChatHistory = AIAgent.CurrentRunContext?.Session?.GetService<IList<ChatMessage>>(); | ||
| // if (parentChatHistory != null) | ||
| // { | ||
| // var chp = new InMemoryChatHistoryProvider() | ||
| // foreach (var message in parentChatHistory) | ||
| // { | ||
| // chp.Add(message); | ||
| // } | ||
| // session = agent.GetNewSession(chp); | ||
| // } | ||
|
|
||
| var response = await agent.RunAsync(query, session: session, options: agentRunOptions, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
westey-m marked this conversation as resolved.
Outdated
|
||
| return response.Text; | ||
| } | ||
|
|
||
| options ??= new(); | ||
| options.Name ??= SanitizeAgentName(agent.Name); | ||
| options.Description ??= agent.Description; | ||
|
|
||
| return AIFunctionFactory.Create(InvokeAgentAsync, options); | ||
| } | ||
| ``` | ||
|
|
||
| ## Decision Drivers | ||
|
|
||
| - Components executing during an agent run need access to run context without explicit parameter passing through every layer | ||
| - Context should flow naturally across async calls without manual propagation | ||
| - The design should allow modification of context properties by agent decorators (e.g., replacing options or session) | ||
| - Solution should be consistent with patterns used in similar frameworks (e.g., `FunctionInvokingChatClient.CurrentContext` `HttpContext.Current`, `Activity.Current`) | ||
|
|
||
| ## Considered Options | ||
|
|
||
| - **Option 1**: Pass context explicitly through all method signatures | ||
| - **Option 2**: Use `AsyncLocal<T>` to provide ambient context accessible anywhere during the run | ||
| - **Option 3**: Use a combination of explicit parameters for `RunCoreAsync` and `AsyncLocal<T>` for ambient access | ||
|
|
||
| ## Decision Outcome | ||
|
|
||
| Chosen option: **Option 3** - Combination of explicit parameters and AsyncLocal ambient access. | ||
|
|
||
| This approach provides the best of both worlds: | ||
|
|
||
| 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. | ||
|
|
||
| ```csharp | ||
| public async Task<AgentResponse> RunAsync( | ||
| IEnumerable<ChatMessage> messages, | ||
| AgentSession? session = null, | ||
| AgentRunOptions? options = null, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); | ||
|
westey-m marked this conversation as resolved.
Outdated
|
||
| CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options); | ||
| return await this.RunCoreAsync(messages, session, options, cancellationToken).ConfigureAwait(false); | ||
| } | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ```csharp | ||
| public static AgentRunContext? CurrentRunContext | ||
| { | ||
| get => s_currentContext.Value; | ||
| protected set => s_currentContext.Value = value; | ||
| } | ||
| ``` | ||
|
|
||
| ### AgentRunContext Design | ||
|
|
||
| The `AgentRunContext` class encapsulates all run-related state: | ||
|
|
||
| ```csharp | ||
| public class AgentRunContext | ||
| { | ||
| public AgentRunContext( | ||
| AIAgent agent, | ||
| AgentSession session, | ||
| IReadOnlyCollection<ChatMessage> requestMessages, | ||
| AgentRunOptions? agentRunOptions) | ||
|
|
||
| public AIAgent Agent { get; } | ||
| public AgentSession Session { get; } | ||
|
westey-m marked this conversation as resolved.
Outdated
|
||
| public IReadOnlyCollection<ChatMessage> RequestMessages { get; } | ||
| public AgentRunOptions? RunOptions { get; } | ||
| } | ||
| ``` | ||
|
|
||
| Key design decisions: | ||
|
|
||
| - **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. | ||
|
|
||
| ### Benefits | ||
|
|
||
| 1. **Ambient Access**: Any code executing during the run can access context via `AIAgent.CurrentRunContext` without needing explicit parameters | ||
| 2. **Async Flow**: `AsyncLocal<T>` automatically flows across async/await boundaries | ||
| 3. **Modifiability**: Components can modify or replace session, messages, or options as needed | ||
| 4. **Testability**: The explicit parameter to `RunCoreAsync` makes unit testing straightforward | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunContext.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 or sets the <see cref="AIAgent"/> that is executing the current run.</summary> | ||
| public AIAgent Agent { get; } | ||
|
|
||
| /// <summary>Gets or sets the <see cref="AgentSession"/> that is associated with the current run.</summary> | ||
| public AgentSession? Session { get; } | ||
|
|
||
| /// <summary>Gets or sets the request messages passed into the current run.</summary> | ||
| public IReadOnlyCollection<ChatMessage> RequestMessages { get; } | ||
|
|
||
| /// <summary>Gets or sets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary> | ||
|
westey-m marked this conversation as resolved.
Outdated
|
||
| public AgentRunOptions? RunOptions { get; } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.