Skip to content

.NET: MessageInjectingChatClient - support multi-instance hosts (pluggable message source, surface injected messages) #9531

Description

Problem

MessageInjectingChatClient can only deliver a message to a run that lives in the same process as the caller. Its pending queue is stored in AgentSession.StateBag and guarded by a per-process ConditionalWeakTable<AgentSession, SemaphoreSlim>, and the class is sealed, so a host cannot back the queue with a shared store.

This affects any host that runs more than one replica without session affinity. A run streams from one replica, but a user's mid-run message can arrive at another. EnqueueMessagesAsync on the receiving replica only reaches a deserialized copy of the session, so the live run never sees the message.

Separately, drained messages are added to the request but never yielded as response updates. A streaming client that owns the transcript therefore cannot tell that a message was injected, or where.

Use Case and Evidence

A chat UI talks to an agent through a streaming HTTP endpoint (AG-UI) behind a load balancer. The browser owns the transcript and sends it with each run. While a long tool call is running, the user types a correction. We want the agent to see it before its next model call, as MessageInjectingChatClient is designed to do, rather than after the run finishes.

With several replicas:

  • The correction may land on a replica that is not running the stream, so it cannot reach the live session.
  • Even when it does reach the run, the client is never told the message was consumed or where it was inserted. On the next run the client cannot replay the transcript in the order the model saw it.

The same process-local limitation is discussed for background agents in #8760.

Desired Outcome

  • A host can deliver a message to a run that is in progress on another instance, so it is injected before that run's next model call.
  • A streaming consumer can learn when an injected message was consumed and its position relative to the surrounding response updates, so a client that owns the transcript can record it in place.

Alternatives Considered

  • A custom DelegatingChatClient below function invocation that reads a shared queue (Redis/Valkey) before each call and adds the messages. This works, but duplicates MessageInjectingChatClient and diverges from the framework's injection semantics, such as re-calling the model when messages are pending after a final response.
  • A relay to the owning replica (pub/sub keyed by run) that calls EnqueueMessagesAsync on the live session there. Every host must then build its own registry of live session objects per run, and the injected message is still not surfaced to the stream.
  • Sticky sessions. Fragile across deploys and scale-in, and not available in every hosting environment.
  • Waiting until the run ends and sending the message as a new run. Works today, but the agent cannot change course mid-run.

Proposed Direction

  1. An optional pending-message source, set through options, that MessageInjectingChatClient drains before each inner call alongside its in-memory queue. A shared-store implementation could then feed a run from any instance.
  2. An option to yield drained messages as response updates (for example a user-role update with the message id) before the inner client's updates for that call.

I'm happy to contribute the .NET implementation of both once the shape is agreed.

Scope and Non-Goals

No response

Language

.NET

Contribution Intent

I am willing to implement this

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 19, 2026
  2. changed the title [-][Feature]: .NET: MessageInjectingChatClient - support multi-instance hosts (pluggable message source, surface injected messages)[/-] [+].NET: [Feature]: .NET: MessageInjectingChatClient - support multi-instance hosts (pluggable message source, surface injected messages)[/+] on Oct 19, 2026
  3. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 19, 2026
  4. changed the title [-].NET: [Feature]: .NET: MessageInjectingChatClient - support multi-instance hosts (pluggable message source, surface injected messages)[/-] [+].NET: MessageInjectingChatClient - support multi-instance hosts (pluggable message source, surface injected messages)[/+] on Oct 19, 2026
  5. JeffinSiby commented on Oct 19, 2026

    @JeffinSiby
    Author

    Roger Barreto (@rogerbarreto) If you think this issue is worth pursing, this was what I had in mind:

    public abstract class InjectedMessageSource
    {
        public abstract ValueTask<IEnumerable<ChatMessage>> DequeueMessagesAsync(
            InjectedMessageSourceContext context, CancellationToken cancellationToken = default);
    }
    
    public sealed class InjectedMessageSourceContext
    {
        public InjectedMessageSourceContext(AIAgent agent, AgentSession session, ChatOptions? options)
        {
            this.Agent = Throw.IfNull(agent);
            this.Session = Throw.IfNull(session);
            this.Options = options;
        }
    
        public AIAgent Agent { get; }
    
        public AgentSession Session { get; }
    
        public ChatOptions? Options { get; }
    }
    • The source is drained after the in-memory queue before every inner call, and also drives the "call again if messages are pending" check.
    • IncludeInjectedMessagesInResponse yields injected messages ahead of the inner updates (non-streaming: prepended to the response).
    • Note: with that option on, FunctionInvokingChatClient keeps injected messages in its history without per-service-call persistence, which overlaps with .NET: fix: keep injected messages in function loop history #6059. Happy to restrict or document that.
    • Configured via UseMessageInjection(options) or ChatClientAgentOptions.MessageInjectionOptions. HarnessAgent would need its own setting.

    Would love to hear your thoughts on it.

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

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions