Skip to content

Conversation

@javiercn
Copy link
Contributor

@javiercn javiercn commented Nov 4, 2025

This PR adds comprehensive support for tool calling in the AG-UI implementation for .NET, including client-side and server-side function execution, conversation management, and proper serialization across boundaries.

Key Changes

1. AGUIAgent → AGUIChatClient: Architectural Shift

Previous Architecture:

  • The original design used AGUIAgent as a wrapper around an IChatClient
  • This prevented using FunctionInvokingChatClient and required creating custom threads and so on.
    • IChatClient now handles the conversation on a given turn(s) and ChatClientAgent handles the run, memory, etc.

New Architecture:

  • AGUIChatClient is now an IChatClient implementation that communicates with AG-UI compliant servers
  • It properly extends DelegatingChatClient and wraps a FunctionInvokingChatClient
  • The internal architecture now has clear layers:
    1. AGUIChatClient (outer) - manages conversation ID and ServerFunctionCallContent wrapping/unwrapping
    2. FunctionInvokingChatClient (middle) - handles client-side tool execution
    3. AGUIChatClientHandler (inner) - handles HTTP communication with AG-UI server

Why this matters:

  • Composability: Leverages the existing FunctionInvokingChatClient for client-side tool execution
  • Separation of concerns: HTTP communication, tool execution, and conversation management are cleanly separated
  • Standards compliance: Implements IChatClient interface consistently with the rest of the ecosystem

2. ConversationId Handling in AG-UI

AG-UI has a fundamental constraint: it requires the full message history on every turn and uses ThreadId for conversation tracking. However, the Microsoft.Extensions.AI IChatClient interface uses ConversationId to indicate stateful conversation management where the client maintains history.

The Challenge:

  • When ChatOptions.ConversationId is set, FunctionInvokingChatClient assumes the underlying client maintains history and only sends new messages
  • AG-UI always needs the full history, even across multiple turns
  • We need to preserve the conversation identity for the caller while preventing FunctionInvokingChatClient from treating the conversation as stateful

The Solution:
The PR implements a sophisticated "conversation ID juggling" pattern:

  1. On Request (Client → Server):

    // If user provides a conversation ID, extract it and clear it from options
    if (options?.ConversationId != null)
    {
        conversationId = options.ConversationId;
        innerOptions = options.Clone();
        innerOptions.AdditionalProperties["agui_thread_id"] = conversationId;
        innerOptions.ConversationId = null;  // Hide from FunctionInvokingChatClient
    }
  2. During Tool Execution:

    • For client tool calls: Store the thread ID in FunctionCallContent.AdditionalProperties["agui_thread_id"]
    • When the tool result comes back with the history, extract the thread ID from the penultimate message
    • This ensures continuity across the function invocation round-trip
  3. On Response (Server → Client):

    // Extract thread ID from first update if not already set
    if (firstUpdate != null && conversationId == null)
    {
        conversationId = firstUpdate.AdditionalProperties["agui_thread_id"];
    }
    
    // Restore conversation ID for all updates before yielding to caller
    finalUpdate.ConversationId = conversationId;

Why this complex approach?

  • AG-UI protocol requirement: Must send full history every time
  • Microsoft.Extensions.AI design: ConversationId implies incremental history
  • FunctionInvokingChatClient behavior: Only sends new messages when ConversationId is set
  • Solution: Hide ConversationId from the middleware layer, use AdditionalProperties as temporary storage, restore it for the caller

This ensures:

  • ✅ Callers see consistent ConversationId across all updates
  • ✅ Full message history is always sent to AG-UI server
  • ✅ Multi-turn conversations work correctly
  • ✅ Tool calls preserve conversation context

3. ServerFunctionCallContent: Hiding Server-Executed Tools

The Problem:
When a server executes a tool, it returns a FunctionCallContent in the response stream. The wrapping FunctionInvokingChatClient sees this and tries to execute it again on the client side, causing:

  • Duplicate execution attempts
  • Errors when the client doesn't have the server's tool registered
  • Confusion about where tools should execute

The Solution - ServerFunctionCallContent:

private class ServerFunctionCallContent(FunctionCallContent functionCall) : AIContent
{
    public FunctionCallContent FunctionCallContent { get; } = functionCall;
}

How it works:

  1. Detection (in AGUIChatClientHandler):

    if (update.Contents[0] is FunctionCallContent fcc)
    {
        if (clientToolSet.Contains(fcc.Name))
        {
            // Client tool - let FunctionInvokingChatClient handle it
        }
        else
        {
            // Server tool - wrap it to hide from FunctionInvokingChatClient
            update.Contents[0] = new ServerFunctionCallContent(fcc);
        }
    }
  2. Unwrapping (in AGUIChatClient):

    for (var i = 0; i < update.Contents.Count; i++)
    {
        if (content is ServerFunctionCallContent serverFunctionCallContent)
        {
            update.Contents[i] = serverFunctionCallContent.FunctionCallContent;
        }
    }

Why a wrapper class?

  • FunctionInvokingChatClient only recognizes FunctionCallContent as something it should execute
  • By wrapping server tool calls in ServerFunctionCallContent, they become "invisible" to the middleware
  • The outer AGUIChatClient then unwraps them before yielding to the caller
  • Result: Callers see normal FunctionCallContent, but middleware doesn't try to execute server tools

Benefits:

  • ✅ Server tools execute only on the server
  • ✅ Client tools execute only on the client
  • ✅ Mixed server/client tool scenarios work correctly
  • ✅ No duplicate execution attempts
  • ✅ Clean separation of execution boundaries

Additional Changes in Broad Strokes

Message Type Hierarchy

  • Introduced proper message type hierarchy: AGUIMessage (base) with concrete types:
    • AGUIUserMessage
    • AGUIAssistantMessage
    • AGUISystemMessage
    • AGUIDeveloperMessage
    • AGUIToolMessage
  • Replaced generic AGUIMessage with role strings to strongly-typed message classes
  • Improved type safety and serialization handling

Tool Call Events

  • Added comprehensive tool call events:
    • ToolCallStartEvent
    • ToolCallArgsEvent
    • ToolCallEndEvent
    • ToolCallResultEvent
  • Converted between ChatResponseUpdate (with FunctionCallContent) and AG-UI events
  • Proper streaming of tool call arguments

JSON Serialization

  • Added JsonSerializerOptions parameter to AGUIChatClient constructor
  • Allows custom JSON contexts for client-specific types
  • Server and client can use different serialization contexts
  • Merged AGUI context with custom contexts automatically
  • Proper handling of cross-boundary serialization (server types → JsonElement on client)

@markwallace-microsoft markwallace-microsoft added documentation Improvements or additions to documentation .NET labels Nov 4, 2025
@javiercn javiercn force-pushed the javiercn/ag-ui-support-for-net branch from 874ab82 to 470a4e9 Compare November 4, 2025 17:42
@javiercn javiercn force-pushed the javiercn/ag-ui-tool-calls branch from 3e04ada to 3d76689 Compare November 4, 2025 20:03
@javiercn javiercn changed the title .NET: AG-UI support for .NET: Support for tool calling [WIP] .NET: AG-UI support for .NET: Support for tool calling Nov 4, 2025
@javiercn javiercn force-pushed the javiercn/ag-ui-support-for-net branch from 3ee8c5c to 6819297 Compare November 5, 2025 12:56
@javiercn javiercn force-pushed the javiercn/ag-ui-tool-calls branch from aab3bca to d4c1b00 Compare November 5, 2025 13:23
@javiercn javiercn force-pushed the javiercn/ag-ui-support-for-net branch from dbe5787 to f18e885 Compare November 5, 2025 13:25
@javiercn javiercn force-pushed the javiercn/ag-ui-tool-calls branch from d4c1b00 to 0071988 Compare November 5, 2025 13:29
@javiercn javiercn force-pushed the javiercn/ag-ui-support-for-net branch from ba5e4a2 to ea7c7af Compare November 5, 2025 15:07
@javiercn javiercn changed the base branch from javiercn/ag-ui-support-for-net to main November 5, 2025 16:11
@javiercn javiercn force-pushed the javiercn/ag-ui-tool-calls branch from 1004759 to ae684aa Compare November 5, 2025 16:11
@javiercn javiercn force-pushed the javiercn/ag-ui-tool-calls branch from 90bcd28 to 447eef1 Compare November 5, 2025 20:01
@javiercn javiercn marked this pull request as ready for review November 5, 2025 20:01
Copilot AI review requested due to automatic review settings November 5, 2025 20:01
@javiercn javiercn changed the title [WIP] .NET: AG-UI support for .NET: Support for tool calling .NET: AG-UI support for .NET: Support for tool calling Nov 5, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request adds comprehensive tool calling support to the AG-UI framework, enabling both client-side and server-side function execution. The changes include:

  • Refactored the AG-UI architecture to use AGUIChatClient (implementing IChatClient) instead of AGUIAgent
  • Added polymorphic message types (AGUIUserMessage, AGUIAssistantMessage, AGUIToolMessage, etc.) replacing the single AGUIMessage class
  • Implemented tool call event types (ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallResultEvent) for streaming tool execution
  • Added extensive unit and integration tests covering tool calling scenarios
  • Updated samples to demonstrate client/server tool calling capabilities

Reviewed Changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
dotnet/src/Microsoft.Agents.AI.AGUI/AGUIChatClient.cs New IChatClient implementation with tool calling support
dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs Core conversion logic between ChatResponseUpdate and AG-UI events including tool calls
dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIMessage*.cs Polymorphic message type hierarchy replacing single AGUIMessage class
dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ToolCall*.cs New event types for tool call streaming
dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs Comprehensive unit tests for tool calling conversion
dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs End-to-end integration tests with Azure OpenAI
dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs Server-side endpoint supporting tool execution
dotnet/samples/AGUIClientServer/AGUIClient/Program.cs Updated client sample with tool calling
dotnet/samples/AGUIClientServer/AGUIServer/Program.cs Updated server sample with tool registration

@javiercn javiercn added this pull request to the merge queue Nov 7, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 7, 2025
@markwallace-microsoft
Copy link
Member

Integration failure unrelated

  Passed OpenAIAssistant.IntegrationTests.OpenAIAssistantIRunTests.RunWithChatMessageReturnsExpectedResultAsync [6 s]
[xUnit.net 00:03:09.00]     OpenAIAssistant.IntegrationTests.OpenAIAssistantClientExtensionsTests.CreateAIAgentAsync_WithHostedCodeInterpreter_RunsCodeAsync(createMechanism: "CreateWithChatClientAgentOptionsSync") [FAIL]

@markwallace-microsoft markwallace-microsoft added this pull request to the merge queue Nov 7, 2025
Merged via the queue into main with commit e859edc Nov 7, 2025
15 checks passed
@crickman crickman deleted the javiercn/ag-ui-tool-calls branch November 11, 2025 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants