.NET: Pass AdditionalProperties from parent to child when exposing an agent as a FunctionTool (#3219)

* Pass AdditionalProperties from parent to child when exposing an agent as a FunctionTool

* Rename variable to improve readability.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
westey
2026-01-20 18:20:08 +00:00
committed by GitHub
co-authored by Copilot
parent 742937194a
commit 73761aa4a3
2 changed files with 66 additions and 1 deletions
@@ -73,7 +73,12 @@ public static partial class AIAgentExtensions
[Description("Input query to invoke the agent.")] string query,
CancellationToken cancellationToken)
{
var response = await agent.RunAsync(query, thread: thread, cancellationToken: cancellationToken).ConfigureAwait(false);
// Propagate any additional properties from the parent agent's run to the child agent if the parent is using a FunctionInvokingChatClient.
AgentRunOptions? agentRunOptions = FunctionInvokingChatClient.CurrentContext?.Options?.AdditionalProperties is AdditionalPropertiesDictionary dict
? new AgentRunOptions { AdditionalProperties = dict }
: null;
var response = await agent.RunAsync(query, thread: thread, options: agentRunOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return response.Text;
}
@@ -277,6 +277,48 @@ public class AgentExtensionsTests
Assert.Equal("Complex response", result.ToString());
}
[Fact]
public async Task CreateFromAgent_InvokeWithAdditionalProperties_PropagatesAdditionalPropertiesToChildAgentAsync()
{
// Arrange
var expectedResponse = new AgentResponse
{
AgentId = "agent-123",
ResponseId = "response-456",
CreatedAt = DateTimeOffset.UtcNow,
Messages = { new ChatMessage(ChatRole.Assistant, "Complex response") }
};
var testAgent = new TestAgent("TestAgent", "Test description", expectedResponse);
var aiFunction = testAgent.AsAIFunction();
// Use reflection to set the protected CurrentContext property
var context = new FunctionInvocationContext()
{
Options = new()
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "customProperty1", "value1" },
{ "customProperty2", 42 }
}
}
};
SetFunctionInvokingChatClientCurrentContext(context);
// Act
var arguments = new AIFunctionArguments() { ["query"] = "Test query" };
var result = await aiFunction.InvokeAsync(arguments);
// Assert
Assert.NotNull(result);
Assert.Equal("Complex response", result.ToString());
Assert.NotNull(testAgent.ReceivedAgentRunOptions);
Assert.NotNull(testAgent.ReceivedAgentRunOptions!.AdditionalProperties);
Assert.Equal("value1", testAgent.ReceivedAgentRunOptions!.AdditionalProperties["customProperty1"]);
Assert.Equal(42, testAgent.ReceivedAgentRunOptions!.AdditionalProperties["customProperty2"]);
}
[Theory]
[InlineData("MyAgent", "MyAgent")]
[InlineData("Agent123", "Agent123")]
@@ -302,6 +344,22 @@ public class AgentExtensionsTests
Assert.Equal(expectedFunctionName, result.Name);
}
/// <summary>
/// Uses reflection to set the protected static CurrentContext property on FunctionInvokingChatClient.
/// </summary>
private static void SetFunctionInvokingChatClientCurrentContext(FunctionInvocationContext? context)
{
// Access the private static field _currentContext which is an AsyncLocal<FunctionInvocationContext?>
var currentContextField = typeof(FunctionInvokingChatClient).GetField(
"_currentContext",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (currentContextField?.GetValue(null) is AsyncLocal<FunctionInvocationContext?> asyncLocal)
{
asyncLocal.Value = context;
}
}
/// <summary>
/// Test implementation of AIAgent for testing purposes.
/// </summary>
@@ -334,6 +392,7 @@ public class AgentExtensionsTests
public override string? Description { get; }
public List<ChatMessage> ReceivedMessages { get; } = [];
public AgentRunOptions? ReceivedAgentRunOptions { get; private set; }
public CancellationToken LastCancellationToken { get; private set; }
public int RunAsyncCallCount { get; private set; }
@@ -346,6 +405,7 @@ public class AgentExtensionsTests
this.RunAsyncCallCount++;
this.LastCancellationToken = cancellationToken;
this.ReceivedMessages.AddRange(messages);
this.ReceivedAgentRunOptions = options;
if (this._exceptionToThrow is not null)
{