Skip to content

.NET: Manually add messages to thread? #2054

Closed

Description

Is it possible to manually add messages to an AgentThread?

Let's take a look to this sample of code:

var userQuestion = "Why is Mars called the Red Planet"?
var informationForRAG = "...";     // Get from a Vector Store

var actualQuestion = $"You're an assistant the knows only the following information: {informationForRAG}. You need to answer the following question: {userQuestion}";

var thread = agent.GetNewThread();
var response = await agent.RunAsync(actualQuestion, thread);

As a result, the Thread’s MessageStore will include all the content of actualQuestion followed by the assistant’s response. For my use case, though, I’d like to add only the userQuestion and the response.

Is that possible?

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 10, 2025
  2. changed the title [-]Manually add messages to thread?[/-] [+].NET: Manually add messages to thread?[/+] on Nov 10, 2025
  3. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 28, 2025
  4. westey-m commented on Nov 28, 2025

    @westey-m
    Collaborator

    Marco Minerva (@marcominerva), thanks for logging the issue. This is not currently possible exactly as you describe it, but the scenario definitely makes sense and there are some closely related ways to achieve this.

    Do note that for some agent types it would never be possible. Any agent type that relies on the underlying service for chat history storage, e.g. ChatClientAgents based on Responses, or A2AAgents, would not be able to support this. With these services, whatever we send to to the service is persisted as part of the chat history automatically by the service and these services do not expose any way to alter this.

    For cases where the service does not manage chat history (E.g. OpenAI ChatCompletion or Responses with store=false), the agent adds messages automatically to the ChatMessageStore attached to the AgentThread. If tno customized the default ChatMessageStore is the InMemoryChatMessageStore.

    In this case, I can see a few options:

    1. We introduce some callback mechanism that allows modifying the message sent to the underlying service, but we continue to persist the original message.
    2. You use an AIContextProvider to do RAG, which adds additional messages into the chat history, which are much easier to just filter out or remove. This will be especially easy to filter out, if we also address this issue: .NET: Consider changing ChatMessageStore abstraction to match AIContextProvider interface #2518, where we would separate messages by source when notifying the message store.
      1. Filtering out the additional messages from the AIContextProvider can be done by adding a decorator over InMemoryChatMessageStore, which filters out anything provided by the Agent before storage.
      2. You can also modify the thread manually after the run from outside the agent, by getting the list of messages from the thread: IList<ChatMessage>? chatHistory = thread.GetService<IList<ChatMessage>>();. Note that this will return null if you are using an AI service with service side chat history storage or if you have a custom chat message store that does not expose this.

    I think 2i is probably cleanest, and is well supported right now.
    It also means that at usage time, callers don't have to concern themselves with RAG or message filtering, because once you have created / setup the agent everything is pretty much self contained.

    See the following docs on how to use a TextSearchProvider to add RAG to your Agent: https://learn.microsoft.com/en-us/agent-framework/user-guide/agents/agent-rag
    This component will inject a message into the chat history that contains the search result and the instruction to the LLM around using the data to answer the question. (the instructions are configurable via TextSearchProviderOptions).

    The injected messages are stamped with an additional property, so are easy to find later:
    ["IsTextSearchProviderOutput"] = true
    See: https://github.com/microsoft/agent-framework/blob/main/dotnet/src/Microsoft.Agents.AI/Data/TextSearchProvider.cs#L151

    A filtering decorator over ChatMessageStore would look something like this:

        public class RAGResultFilteringMessageStore(ChatMessageStore chatMessageStore) : ChatMessageStore
        {
            public override Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
                => chatMessageStore.AddMessagesAsync(
                    messages.Where(x => !(x.AdditionalProperties?.TryGetValue("IsTextSearchProviderOutput", out bool value) is true && value is true)),
                    cancellationToken);
    
            public override Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
                => chatMessageStore.GetMessagesAsync(cancellationToken);
    
            public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
                => chatMessageStore.Serialize(jsonSerializerOptions);
        }

    And you would be able to use it like this:

    AIAgent agent = azureOpenAIClient
        // Choosing the ChatCompletion service here, since it doesn't support or require in-service chat history storage.
        .GetChatClient(deploymentName)
        .CreateAIAgent(new ChatClientAgentOptions
        {
            Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
            AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions),
            ChatMessageStoreFactory = ctx => new RAGResultFilteringMessageStore(new InMemoryChatMessageStore())
        });

    Please let me know if this works well, we could also look at providing more out of the box filtering options for client side ChatMessageStores.

  5. CharlieDigital commented on Dec 2, 2025

    @CharlieDigital

    Upvoting this since our backend currently has a lot of threads that we are self-managing that will need to be reconstructed. Would appreciate an easier on-ramp for this use case and for official docs on how to load existing ChatMessage threads into the agent.

  6. CharlieDigital commented on Dec 2, 2025

    @CharlieDigital

    Wait, maybe I'm mis-understanding something: what is the intended delineation between ChatMessage and AgentThread? I noticed that this signature accepts both:

    Image

    It's not clear to me why the signature would accept both and when each should be used.

  7. CharlieDigital commented on Dec 3, 2025

    @CharlieDigital

    including chat history

    Is it safe to say that for teams currently self-managing chat history, AgentThread is a superset. More specifically, it is still possible to self manage chat history via the RunAsync(IEnumerable<ChatMessage> messages, ...) signature?

  8. westey-m commented on Dec 4, 2025

    @westey-m
    Collaborator

    Charles Chen (@CharlieDigital), AgentThread is designed to abstract the differences in underlying chat history storage requirements of underlying services away from you, since they can vary significantly.

    Some services, e.g. Responses (with store = true, which is the default), Foundry Agents, and OpenAI Assistants, only support storing chat history in the service itself, so there really is no way to manage chat history yourself when using these services. In this case the AgentThread just stores the service provided id for the chat history.

    If you are using a service that doesn't support/require service side chat history, e.g. OpenAI Chat Completion or Responses (store = false), the chat history is automatically managed in an InMemoryChatMessageStore, which is just storing an IList.
    It is possible to get at these as mentioned above and modify. Or you can provide your own message store if you want more control.
    If using one of these services, you could always not call GetNewThread, and not pass a thread to each invocation, and instead pass the full chat history to each run. That way you have to build your chat history manually.

    Note that if you do this with services that require service chat history storage, it will create a new service side thread for each run, which could be inefficient.

  9. marcominerva commented on Dec 9, 2025

    @marcominerva
    ContributorAuthor
    1. You use an AIContextProvider to do RAG, which adds additional messages into the chat history, which are much easier to just filter out or remove. This will be especially easy to filter out, if we also address this issue: .NET: Consider changing ChatMessageStore abstraction to match AIContextProvider interface #2518, where we would separate messages by source when notifying the message store.

      1. Filtering out the additional messages from the AIContextProvider can be done by adding a decorator over InMemoryChatMessageStore, which filters out anything provided by the Agent before storage.

    Thanks westey (@westey-m), this approach sounds interesting, I’ll give it a try!

  10. marcominerva commented on Dec 16, 2025

    @marcominerva
    ContributorAuthor

    westey (@westey-m), I was reading your note again:

    For cases where the service does not manage chat history (E.g. OpenAI ChatCompletion or Responses with store=false), the agent adds messages automatically to the ChatMessageStore attached to the AgentThread. If tno customized the default ChatMessageStore is the InMemoryChatMessageStore.

    You mention Responses with store=false. Is this a configuration setting? Could you give me an example of how to set store=false when using the Responses API?

  11. westey-m commented on Dec 30, 2025

    @westey-m
    Collaborator

    Could you give me an example of how to set store=false when using the Responses API

    Marco Minerva (@marcominerva), sure, see the following. It's possible to provide a ChatOptions instance for the underlying IChatClient used by the agent and that in turn supports providing the raw options type for the underlying service SDK. In this case CreateResponseOptions.

    AIAgent agent = new OpenAIClient(
        apiKey)
         .GetResponsesClient(model)
         .CreateAIAgent(options: new() { ChatOptions = new() { RawRepresentationFactory = (_) => new CreateResponseOptions() { StoredOutputEnabled = false } } });
  12. westey-m commented on Jan 5, 2026

    @westey-m
    Collaborator

    Marco Minerva (@marcominerva), I've merged a PR that makes it easier to filter out AIContextProvider generated messages from storage when storing messages in a message store.

    See the following for a RAG sample that doesn't store the context messages added by the rag AIContextProvider: https://github.com/microsoft/agent-framework/pull/2604/changes#diff-bb6e6dfc75745c0890d32d026c5fff875dbc7d1bca3548be9d7dc46bc59c55d7

    This should be in the next release.

  13. moved this from In Progress to Done in Agent Frameworkon Jan 5, 2026
  14. marcominerva commented on Jan 8, 2026

    @marcominerva
    ContributorAuthor

    Hi westey (@westey-m)!

    Thank you for your effort. I have one last question: on these lines in the PR: https://github.com/microsoft/agent-framework/pull/2604/changes#diff-bb6e6dfc75745c0890d32d026c5fff875dbc7d1bca3548be9d7dc46bc59c55d7R66-R70, I read the following comment:

    //we can also add a message removal policy
    // that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
    // we don't bloat chat history with all the search result messages.
    

    This is done using the WithAIContextProviderMessageRemoval extension method. Could you point me to where the messages are actually removed? I would like to understand the removal logic (for example, the presence of a specific AdditionalProperty), so that I can customize this if needed.

  15. westey-m commented on Jan 8, 2026

    @westey-m
    Collaborator

    Marco Minerva (@marcominerva), sure, I've added two extension methods. You can see them here:
    https://github.com/microsoft/agent-framework/blob/main/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreExtensions.cs

    WithMessageFilters is generic and allows you to define your own filter, while the other is the more specific WithAIContextProviderMessageRemoval. And yes, that comment does refer to WithAIContextProviderMessageRemoval.

    The invokedMessagesFilter Func can be passed to WithMessageFilters. It receives a ChatMessageStore.InvokedContext and produces one. In this function you can modify the provided InvokedContext to remove or change (as required) any messages before they are passed to the ChatMessageStore. Whatever is returned by this Func will be used by the ChatMessageStore.

    The built-in ChatMessageStore implementations will save the messages from the InvokedContext in the order RequestMessages, AIContextProviderMessages, and lastly ResponseMessages. Removing or modifying any of those will affect what is saved.

  16. marcominerva commented on Jan 8, 2026

    @marcominerva
    ContributorAuthor

    Thank you westey (@westey-m), this perfectly fits my scenario.

  17. lindexi commented on Feb 24, 2026

    @lindexi

    Add messages to AgentSession in 1.0.0-rc1 version:

    var inMemoryChatHistoryProvider = new InMemoryChatHistoryProvider();
    AgentSession session = await aiAgent.CreateSessionAsync();
    
    var chatMessages = inMemoryChatHistoryProvider.GetMessages(session);
    chatMessages.Add(new ChatMessage(...));
    chatMessages.Add(new ChatMessage(...));
    
    await foreach (var agentRunResponseUpdate in aiAgent.RunReasoningStreamingAsync(message, session,new AgentRunOptions()
                   {
                       
                   }))
    {
        ...
    }
  18. westey-m commented on Feb 25, 2026

    @westey-m
    Collaborator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetagentsUsage: [Issues, PRs], Target: Single agent

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions