Skip to content

.NET: Support for intermediate state from Tools for AG-UI #2081

Closed

Description

Hi,
I am trying to build Deep Research Agent using AGUI and CopilotKit. I would like to do below steps -

  1. Break the objective into 3-4 short steps for the UI to display
Image

This is working using useCoAgentStateRender with DelegatingAIAgent as above.

  1. For Each Steps It will call perform_research tool. <-- This steps also works fine. Here is my tool definition -

[Description("Perform a research task and track progress.")]
static async IAsyncEnumerable<AgentRunResponseUpdate> PerformResearch([Description("The research query")] string query)
{
    var agentState = new AgentStateSnapshot { Searches = [] };
    agentState.Searches.Add(new SearchInfo { Query = query, Status= "InProgress"});

    byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
                agentState);

    // Emit state snapshot as DataContent
    yield return new AgentRunResponseUpdate
    {
        Contents = [new DataContent(stateBytes, "application/json")]
    };
    // Simulate initial research phase
    var research_content = $" Research Overview: Actual content......";
  

    agentState.Searches.Add(new SearchInfo { Query = query, Status= "Done" });
    stateBytes = JsonSerializer.SerializeToUtf8Bytes(
              agentState);

    yield return new AgentRunResponseUpdate
    {
        Contents = [new DataContent(stateBytes, "application/json")]
    };

   yield return new AgentRunResponseUpdate
    {
        Contents = [new TextContent(research_content)]
    };
}

Above snippet has few problem

  • Unable to access existing state
  • all three outputs are returning together at once it seems
  • all three outputs are returning as TEXT_MESSAGE_CONTENT so not invoking useCoAgentStateRender and it is getting treated as Tool's actual output.

My question -

  • How do I get existing Agent State in Tool?
  • How do I return intermediate state seperatly.
  • How do I stream these intermediate state as STATE_SNAPSHOT

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 11, 2025
  2. changed the title [-][.NET] Support for intermediate state from Tools for AG-UI[/-] [+].NET: [.NET] Support for intermediate state from Tools for AG-UI[/+] on Nov 11, 2025
  3. changed the title [-].NET: [.NET] Support for intermediate state from Tools for AG-UI[/-] [+].NET: Support for intermediate state from Tools for AG-UI[/+] on Nov 11, 2025
  4. anktsrkr commented on Nov 11, 2025

    @anktsrkr
    Author

    Instead of above, I can think of CustomFunctionCallingMiddleware but seems we cant yield the AgentRunResponseUpdate from it.

    Any suggestion on this Javier Calvarro Nelson (@javiercn) ?

  5. javiercn commented on Nov 12, 2025

    @javiercn
    Contributor

    Ankit Sarkar (@anktsrkr) thanks for contacting us.

    In general, you need to decorate the agent to do these things. We will have more demos coming soon, but I'll try to give you some pointers.

    • How do I get existing Agent State in Tool?

    Since the state is sent on every turn, there are a couple of ways I can think of doing this. You can get the state on RunStreamingAsync from the chat options, create new chat options and add a tool instance to the options that contains the state.

    • How do I return intermediate state separately.
    • How do I stream these intermediate state as STATE_SNAPSHOT

    Typically, you can't do this from tool calls. the pattern in AG-UI is that you define additional tool definitions that map tool invocations to state updates, and then you convert those tool calls into state snapshots/deltas before sending them to the client. We will have a sample for this pattern in the following days. I'll update this issue when that's available.

    https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedStateAgent.cs shows how to access the state from AG-UI and how to emit it.

    Let me know if this helps or you need more assistance.

  6. markwallace-microsoft commented on Mar 20, 2026

    @markwallace-microsoft
    Contributor

    Ankit Sarkar (@anktsrkr), friendly reminder — this issue is waiting on your response. Please share any updates when you get a chance. (This is an automated message.)

  7. added
    questionUsage: [Issues, PRs], Target: all issues or PRs where additional context is needed from the author
    and removed on Jun 22, 2026
  8. javiercn commented on Aug 18, 2026

    @javiercn
    Contributor

    I took another look at this. An AIFunction still produces one final tool result, but a delegating agent can surface intermediate updates while the function is executing by using a per-run channel.

    The delegating agent creates the function for that run. Its lambda captures both the current AG-UI state and the channel writer, so no ambient context or AdditionalProperties plumbing is needed:

    var updates = Channel.CreateUnbounded<AgentResponseUpdate>();
    
    var runOptions =
        options as ChatClientAgentRunOptions
        ?? new ChatClientAgentRunOptions();
    
    runOptions.ChatOptions =
        runOptions.ChatOptions?.Clone()
        ?? new ChatOptions();
    
    runOptions.ChatOptions.TryGetRunAgentInput(out RunAgentInput? input);
    
    AgentState state =
        input?.State.Deserialize<AgentState>()
        ?? new AgentState();
    
    runOptions.ChatOptions.Tools ??= [];
    runOptions.ChatOptions.Tools.Add(
        AIFunctionFactory.Create(
            async (string query, CancellationToken cancellationToken) =>
            {
                state.Searches.Add(new SearchInfo
                {
                    Query = query,
                    Status = "InProgress"
                });
    
                await updates.Writer.WriteAsync(
                    CreateStateSnapshot(state),
                    cancellationToken);
    
                string result = await PerformResearchAsync(
                    query,
                    cancellationToken);
    
                state.Searches.Single(s => s.Query == query).Status = "Done";
    
                await updates.Writer.WriteAsync(
                    CreateStateSnapshot(state),
                    cancellationToken);
    
                return result;
            },
            name: "perform_research",
            description: "Performs a research task and reports progress."));

    The delegating agent then pumps the inner-agent stream into the same channel and yields the combined stream:

    Task pump = PumpInnerAgentAsync();
    
    await foreach (AgentResponseUpdate update in
        updates.Reader.ReadAllAsync(cancellationToken))
    {
        yield return update;
    }
    
    await pump;
    
    async Task PumpInnerAgentAsync()
    {
        try
        {
            await foreach (AgentResponseUpdate update in
                InnerAgent.RunStreamingAsync(
                    messages,
                    session,
                    runOptions,
                    cancellationToken))
            {
                await updates.Writer.WriteAsync(
                    update,
                    cancellationToken);
            }
    
            updates.Writer.TryComplete();
        }
        catch (Exception error)
        {
            updates.Writer.TryComplete(error);
        }
    }

    CreateStateSnapshot wraps the state as an AG-UI event:

    static AgentResponseUpdate CreateStateSnapshot(AgentState state) =>
        new()
        {
            Role = ChatRole.Assistant,
            RawRepresentation = new ChatResponseUpdate
            {
                Role = ChatRole.Assistant,
                RawRepresentation = new StateSnapshotEvent
                {
                    Snapshot = JsonSerializer.SerializeToElement(state)
                }
            }
        };

    This allows the channel updates to pass through while the inner agent is blocked waiting for the tool’s final result.

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: .Netag-uiUsage: [Issues, PRs], Target: AG-UI protocol integrationquestionUsage: [Issues, PRs], Target: all issues or PRs where additional context is needed from the author

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions