.NET: [BREAKING]Delay AIContext Materialization until the end of the pipeline is reached. (#3883)

* Delay AIContext Materialization until the end of the pipeline is reached.

* Address PR comments.

* Address PR comments
This commit is contained in:
westey
2026-02-12 15:44:19 +00:00
committed by GitHub
Unverified
parent d71e076d15
commit 72a863f4bf
12 changed files with 101 additions and 90 deletions
@@ -119,14 +119,13 @@ namespace SampleApp
{
AIFunctionFactory.Create((string item) => AddTodoItem(context.Session, item), "AddTodoItem", "Adds an item to the todo list."),
AIFunctionFactory.Create((int index) => RemoveTodoItem(context.Session, index), "RemoveTodoItem", "Removes an item from the todo list. Index is zero based.")
}).ToList(),
}),
Messages =
(inputContext.Messages ?? [])
.Concat(
[
new MEAI.ChatMessage(ChatRole.User, outputMessageBuilder.ToString()).WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!)
])
.ToList()
});
}
@@ -56,41 +56,44 @@ public sealed class AIContext
public string? Instructions { get; set; }
/// <summary>
/// Gets or sets a collection of messages to add to the conversation history.
/// Gets or sets the sequence of messages to use for the current invocation.
/// </summary>
/// <value>
/// A list of <see cref="ChatMessage"/> instances to be permanently added to the conversation history,
/// or <see langword="null"/> if no messages should be added.
/// A sequence of <see cref="ChatMessage"/> instances to be used for the current invocation,
/// or <see langword="null"/> if no messages should be used.
/// </value>
/// <remarks>
/// <para>
/// Unlike <see cref="Instructions"/> and <see cref="Tools"/>, messages added through this property become
/// permanent additions to the conversation history. They will persist beyond the current invocation and
/// will be available in future interactions within the same conversation thread.
/// Unlike <see cref="Instructions"/> and <see cref="Tools"/>, messages added through this property may become
/// permanent additions to the conversation history.
/// If chat history is managed by the underlying AI service, these messages will become part of chat history.
/// If chat history is managed using a <see cref="ChatHistoryProvider"/>, these messages will be passed to the
/// <see cref="ChatHistoryProvider.InvokedCoreAsync(ChatHistoryProvider.InvokedContext, System.Threading.CancellationToken)"/> method,
/// and the provider can choose which of these messages to permanently add to the conversation history.
/// </para>
/// <para>
/// This property is useful for:
/// <list type="bullet">
/// <item><description>Injecting relevant historical context or background information</description></item>
/// <item><description>Injecting relevant historical context e.g. memories</description></item>
/// <item><description>Injecting relevant background information e.g. via Retrieval Augmented Generation</description></item>
/// <item><description>Adding system messages that provide ongoing context</description></item>
/// <item><description>Including retrieved information that should be part of the conversation record</description></item>
/// <item><description>Inserting contextual exchanges that inform the current conversation</description></item>
/// </list>
/// </para>
/// </remarks>
public IList<ChatMessage>? Messages { get; set; }
public IEnumerable<ChatMessage>? Messages { get; set; }
/// <summary>
/// Gets or sets a collection of tools or functions to make available to the AI model for the current invocation.
/// Gets or sets a sequence of tools or functions to make available to the AI model for the current invocation.
/// </summary>
/// <value>
/// A list of <see cref="AITool"/> instances that will be available to the AI model during the current invocation,
/// A sequence of <see cref="AITool"/> instances that will be available to the AI model during the current invocation,
/// or <see langword="null"/> if no additional tools should be provided.
/// </value>
/// <remarks>
/// <para>
/// These tools are transient and apply only to the current AI model invocation. They are combined with any
/// tools already configured for the agent to provide an expanded set of capabilities for the specific interaction.
/// These tools are transient and apply only to the current AI model invocation. Any existing tools
/// are provided as input to the <see cref="AIContextProvider"/> instances, so context providers can choose to modify or replace the existing tools
/// as needed based on the current context. The resulting set of tools is then passed to the underlying AI model, which may choose to utilize them when generating responses.
/// </para>
/// <para>
/// Context-specific tools enable:
@@ -102,5 +105,5 @@ public sealed class AIContext
/// </list>
/// </para>
/// </remarks>
public IList<AITool>? Tools { get; set; }
public IEnumerable<AITool>? Tools { get; set; }
}
@@ -170,8 +170,7 @@ public sealed class Mem0Provider : AIContextProvider
Instructions = inputContext.Instructions,
Messages =
(inputContext.Messages ?? [])
.Concat(outputMessage is not null ? [outputMessage] : [])
.ToList(),
.Concat(outputMessage is not null ? [outputMessage] : []),
Tools = inputContext.Tools
};
}
@@ -677,7 +677,7 @@ public sealed partial class ChatClientAgent : AIAgent
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
}
List<ChatMessage> inputMessagesForChatClient = [];
IEnumerable<ChatMessage> inputMessagesForChatClient = inputMessages;
// Populate the session messages only if we are not continuing an existing response as it's not allowed
if (chatOptions?.ContinuationToken is null)
@@ -688,13 +688,8 @@ public sealed partial class ChatClientAgent : AIAgent
// The ChatHistoryProvider returns the merged result (history + input messages).
if (chatHistoryProvider is not null)
{
var invokingContext = new ChatHistoryProvider.InvokingContext(this, typedSession, inputMessages);
var providerMessages = await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
inputMessagesForChatClient.AddRange(providerMessages);
}
else
{
inputMessagesForChatClient.AddRange(inputMessages);
var invokingContext = new ChatHistoryProvider.InvokingContext(this, typedSession, inputMessagesForChatClient);
inputMessagesForChatClient = await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
// If we have an AIContextProvider, we should get context from it, and update our
@@ -705,8 +700,8 @@ public sealed partial class ChatClientAgent : AIAgent
var aiContext = new AIContext
{
Instructions = chatOptions?.Instructions,
Messages = inputMessagesForChatClient.ToList(),
Tools = chatOptions?.Tools as List<AITool> ?? chatOptions?.Tools?.ToList()
Messages = inputMessagesForChatClient,
Tools = chatOptions?.Tools
};
foreach (var aiContextProvider in aiContextProviders)
@@ -715,13 +710,14 @@ public sealed partial class ChatClientAgent : AIAgent
aiContext = await aiContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
// Use the returned messages, tools and instructions directly since the provider accumulated them.
inputMessagesForChatClient = aiContext.Messages as List<ChatMessage> ?? aiContext.Messages?.ToList() ?? [];
// Materialize the accumulated messages and tools once at the end of the provider pipeline.
inputMessagesForChatClient = aiContext.Messages ?? [];
if (chatOptions?.Tools is { Count: > 0 } || aiContext.Tools is { Count: > 0 })
var tools = aiContext.Tools as IList<AITool> ?? aiContext.Tools?.ToList();
if (chatOptions?.Tools is { Count: > 0 } || tools is { Count: > 0 })
{
chatOptions ??= new();
chatOptions.Tools = aiContext.Tools;
chatOptions.Tools = tools;
}
if (chatOptions?.Instructions is not null || aiContext.Instructions is not null)
@@ -750,7 +746,10 @@ public sealed partial class ChatClientAgent : AIAgent
chatOptions.ConversationId = typedSession.ConversationId;
}
return (typedSession, chatOptions, inputMessagesForChatClient, continuationToken);
// Materialize the accumulated messages once at the end of the provider pipeline, reusing the existing list if possible.
List<ChatMessage> messagesList = inputMessagesForChatClient as List<ChatMessage> ?? inputMessagesForChatClient.ToList();
return (typedSession, chatOptions, messagesList, continuationToken);
}
private void UpdateSessionConversationId(ChatClientAgentSession session, string? responseConversationId, CancellationToken cancellationToken)
@@ -171,7 +171,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
{
Instructions = inputContext.Instructions,
Messages = inputContext.Messages,
Tools = (inputContext.Tools ?? []).Concat(tools).ToList()
Tools = (inputContext.Tools ?? []).Concat(tools)
};
}
@@ -204,8 +204,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
.Concat(
[
new ChatMessage(ChatRole.User, contextText).WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!)
])
.ToList(),
]),
Tools = inputContext.Tools
};
}
@@ -105,7 +105,7 @@ public sealed class TextSearchProvider : AIContextProvider
{
Instructions = inputContext.Instructions,
Messages = inputContext.Messages,
Tools = (inputContext.Tools ?? []).Concat(this._tools).ToList()
Tools = (inputContext.Tools ?? []).Concat(this._tools)
};
}
@@ -161,8 +161,7 @@ public sealed class TextSearchProvider : AIContextProvider
.Concat(
[
new ChatMessage(ChatRole.User, formatted).WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!)
])
.ToList(),
]),
Tools = inputContext.Tools
};
}
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -33,9 +34,10 @@ public class AIContextTests
};
Assert.NotNull(context.Messages);
Assert.Equal(2, context.Messages.Count);
Assert.Equal("Hello", context.Messages[0].Text);
Assert.Equal("Hi there!", context.Messages[1].Text);
var messages = context.Messages.ToList();
Assert.Equal(2, messages.Count);
Assert.Equal("Hello", messages[0].Text);
Assert.Equal("Hi there!", messages[1].Text);
}
[Fact]
@@ -51,8 +53,9 @@ public class AIContextTests
};
Assert.NotNull(context.Tools);
Assert.Equal(2, context.Tools.Count);
Assert.Equal("Function1", context.Tools[0].Name);
Assert.Equal("Function2", context.Tools[1].Name);
var tools = context.Tools.ToList();
Assert.Equal(2, tools.Count);
Assert.Equal("Function1", tools[0].Name);
Assert.Equal("Function2", tools[1].Name);
}
}
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
@@ -54,7 +55,7 @@ public sealed class Mem0ProviderTests : IDisposable
await sut.ClearStoredMemoriesAsync(mockSession);
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, mockSession, new AIContext { Messages = new List<ChatMessage> { question } }));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?.LastOrDefault()?.Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, mockSession, [input]));
@@ -63,8 +64,8 @@ public sealed class Mem0ProviderTests : IDisposable
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, mockSession, new AIContext { Messages = new List<ChatMessage> { question } }));
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?.LastOrDefault()?.Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?.LastOrDefault()?.Text ?? string.Empty);
}
[Fact(Skip = SkipReason)]
@@ -79,7 +80,7 @@ public sealed class Mem0ProviderTests : IDisposable
await sut.ClearStoredMemoriesAsync(mockSession);
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, mockSession, new AIContext { Messages = new List<ChatMessage> { question } }));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?.LastOrDefault()?.Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, mockSession, [assistantIntro]));
@@ -88,8 +89,8 @@ public sealed class Mem0ProviderTests : IDisposable
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, mockSession, new AIContext { Messages = new List<ChatMessage> { question } }));
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?.LastOrDefault()?.Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?.LastOrDefault()?.Text ?? string.Empty);
}
[Fact(Skip = SkipReason)]
@@ -110,8 +111,8 @@ public sealed class Mem0ProviderTests : IDisposable
var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, mockSession1, new AIContext { Messages = new List<ChatMessage> { question } }));
var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, mockSession2, new AIContext { Messages = new List<ChatMessage> { question } }));
Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?.LastOrDefault()?.Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?.LastOrDefault()?.Text ?? string.Empty);
// Act
await sut1.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, mockSession1, [assistantIntro]));
@@ -119,8 +120,8 @@ public sealed class Mem0ProviderTests : IDisposable
var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, mockSession2, question);
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding1.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxAfterAdding2.Messages?[0].Text ?? string.Empty);
Assert.Contains("Caoimhe", ctxAfterAdding1.Messages?.LastOrDefault()?.Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxAfterAdding2.Messages?.LastOrDefault()?.Text ?? string.Empty);
// Cleanup
await sut1.ClearStoredMemoriesAsync(mockSession1);
@@ -133,7 +134,7 @@ public sealed class Mem0ProviderTests : IDisposable
for (int i = 0; i < attempts; i++)
{
ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, session, new AIContext { Messages = new List<ChatMessage> { question } }), CancellationToken.None);
var text = ctx.Messages?[0].Text;
var text = ctx.Messages?.LastOrDefault()?.Text;
if (!string.IsNullOrEmpty(text) && text.IndexOf("Caoimhe", StringComparison.OrdinalIgnoreCase) >= 0)
{
break;
@@ -118,9 +118,10 @@ public sealed class Mem0ProviderTests : IDisposable
Assert.Equal("What is my name?", doc.RootElement.GetProperty("query").GetString());
Assert.NotNull(aiContext.Messages);
Assert.Equal(2, aiContext.Messages.Count);
Assert.Equal(AgentRequestMessageSourceType.External, aiContext.Messages[0].GetAgentRequestMessageSourceType());
var contextMessage = aiContext.Messages[1];
var messages = aiContext.Messages.ToList();
Assert.Equal(2, messages.Count);
Assert.Equal(AgentRequestMessageSourceType.External, messages[0].GetAgentRequestMessageSourceType());
var contextMessage = messages[1];
Assert.Equal(ChatRole.User, contextMessage.Role);
Assert.Contains("Name is Caoimhe", contextMessage.Text);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, contextMessage.GetAgentRequestMessageSourceType());
@@ -496,9 +496,9 @@ public partial class ChatClientAgentTests
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext
{
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages).ToList(),
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages),
Instructions = ctx.AIContext.Instructions + "\ncontext provider instructions",
Tools = (ctx.AIContext.Tools ?? []).Concat(new[] { AIFunctionFactory.Create(() => { }, "context provider function") }).ToList()
Tools = (ctx.AIContext.Tools ?? []).Concat(new[] { AIFunctionFactory.Create(() => { }, "context provider function") })
}));
mockProvider
.Protected()
@@ -567,7 +567,7 @@ public partial class ChatClientAgentTests
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext
{
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages).ToList(),
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages),
}));
mockProvider
.Protected()
@@ -626,7 +626,7 @@ public partial class ChatClientAgentTests
new ValueTask<AIContext>(new AIContext
{
Instructions = ctx.AIContext.Instructions,
Messages = ctx.AIContext.Messages?.ToList(),
Messages = ctx.AIContext.Messages,
Tools = ctx.AIContext.Tools
}));
@@ -1875,9 +1875,9 @@ public partial class ChatClientAgentTests
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext
{
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages).ToList(),
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages),
Instructions = ctx.AIContext.Instructions + "\ncontext provider instructions",
Tools = (ctx.AIContext.Tools ?? []).Concat(new[] { AIFunctionFactory.Create(() => { }, "context provider function") }).ToList()
Tools = (ctx.AIContext.Tools ?? []).Concat(new[] { AIFunctionFactory.Create(() => { }, "context provider function") })
}));
mockProvider
.Protected()
@@ -1954,7 +1954,7 @@ public partial class ChatClientAgentTests
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext
{
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages).ToList(),
Messages = (ctx.AIContext.Messages ?? []).Concat(aiContextProviderMessages),
}));
mockProvider
.Protected()
@@ -106,12 +106,13 @@ public sealed class TextSearchProviderTests
Assert.Equal("Sample user question?\nAdditional part", capturedInput);
Assert.Null(aiContext.Instructions); // TextSearchProvider uses a user message for context injection.
Assert.NotNull(aiContext.Messages);
Assert.Equal(3, aiContext.Messages!.Count); // 2 input messages + 1 search result message
Assert.Equal("Sample user question?", aiContext.Messages![0].Text);
Assert.Equal("Additional part", aiContext.Messages![1].Text);
Assert.Equal(AgentRequestMessageSourceType.External, aiContext.Messages![0].GetAgentRequestMessageSourceType());
Assert.Equal(AgentRequestMessageSourceType.External, aiContext.Messages![1].GetAgentRequestMessageSourceType());
var message = aiContext.Messages!.Last();
var messages = aiContext.Messages!.ToList();
Assert.Equal(3, messages.Count); // 2 input messages + 1 search result message
Assert.Equal("Sample user question?", messages[0].Text);
Assert.Equal("Additional part", messages[1].Text);
Assert.Equal(AgentRequestMessageSourceType.External, messages[0].GetAgentRequestMessageSourceType());
Assert.Equal(AgentRequestMessageSourceType.External, messages[1].GetAgentRequestMessageSourceType());
var message = messages.Last();
Assert.Equal(ChatRole.User, message.Role);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, message.GetAgentRequestMessageSourceType());
string text = message.Text!;
@@ -181,11 +182,13 @@ public sealed class TextSearchProviderTests
// Assert
Assert.NotNull(aiContext.Messages); // Input messages are preserved.
Assert.Single(aiContext.Messages!);
Assert.Equal("Q?", aiContext.Messages![0].Text);
var messages = aiContext.Messages!.ToList();
Assert.Single(messages);
Assert.Equal("Q?", messages[0].Text);
Assert.NotNull(aiContext.Tools);
Assert.Single(aiContext.Tools);
var tool = aiContext.Tools.Single();
var tools = aiContext.Tools!.ToList();
Assert.Single(tools);
var tool = tools[0];
Assert.Equal(expectedName, tool.Name);
Assert.Equal(expectedDescription, tool.Description);
}
@@ -202,8 +205,9 @@ public sealed class TextSearchProviderTests
// Assert
Assert.NotNull(aiContext.Messages); // Input messages are preserved on error.
Assert.Single(aiContext.Messages!);
Assert.Equal("Q?", aiContext.Messages![0].Text);
var messages = aiContext.Messages!.ToList();
Assert.Single(messages);
Assert.Equal("Q?", messages[0].Text);
Assert.Null(aiContext.Tools);
this._loggerMock.Verify(
l => l.Log(
@@ -297,9 +301,10 @@ public sealed class TextSearchProviderTests
// Assert
Assert.NotNull(aiContext.Messages);
Assert.Equal(2, aiContext.Messages!.Count); // 1 input message + 1 formatted result message
Assert.Equal("Q?", aiContext.Messages![0].Text);
Assert.Equal("Custom formatted context with 2 results.", aiContext.Messages![1].Text);
var messages = aiContext.Messages!.ToList();
Assert.Equal(2, messages.Count); // 1 input message + 1 formatted result message
Assert.Equal("Q?", messages[0].Text);
Assert.Equal("Custom formatted context with 2 results.", messages[1].Text);
}
[Fact]
@@ -332,9 +337,10 @@ public sealed class TextSearchProviderTests
// Assert
Assert.NotNull(aiContext.Messages);
Assert.Equal(2, aiContext.Messages!.Count); // 1 input message + 1 formatted result message
Assert.Equal("Q?", aiContext.Messages![0].Text);
Assert.Equal("R1,R2", aiContext.Messages![1].Text);
var messages = aiContext.Messages!.ToList();
Assert.Equal(2, messages.Count); // 1 input message + 1 formatted result message
Assert.Equal("Q?", messages[0].Text);
Assert.Equal("R1,R2", messages[1].Text);
}
[Fact]
@@ -350,8 +356,9 @@ public sealed class TextSearchProviderTests
// Assert
Assert.NotNull(aiContext.Messages); // Input messages are preserved when no results found.
Assert.Single(aiContext.Messages!);
Assert.Equal("Q?", aiContext.Messages![0].Text);
var messages = aiContext.Messages!.ToList();
Assert.Single(messages);
Assert.Equal("Q?", messages[0].Text);
Assert.Null(aiContext.Instructions);
Assert.Null(aiContext.Tools);
}
@@ -396,9 +396,10 @@ public class ChatHistoryMemoryProviderTests
Times.Once);
Assert.NotNull(aiContext.Messages);
Assert.Equal(2, aiContext.Messages.Count);
Assert.Equal(AgentRequestMessageSourceType.External, aiContext.Messages[0].GetAgentRequestMessageSourceType());
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, aiContext.Messages[1].GetAgentRequestMessageSourceType());
var messages = aiContext.Messages.ToList();
Assert.Equal(2, messages.Count);
Assert.Equal(AgentRequestMessageSourceType.External, messages[0].GetAgentRequestMessageSourceType());
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, messages[1].GetAgentRequestMessageSourceType());
}
[Fact]