Skip to main content

Extensibility

AGUIStreamOptions customizes how ChatResponseUpdate streams are converted to AG-UI events. It is fluent and method-only: create an instance, register Map* hooks, and pass it to ToChatRequestContext.
var streamOptions = new AGUIStreamOptions()
    .MapResultAsStateSnapshot("create_plan");

var ctx = input.ToChatRequestContext(jsonSerializerOptions, streamOptions);
Use stream options when your server needs to surface framework-specific content types, custom interrupts, state updates, or streaming tool-argument previews.

Built-in conversion

You do not need stream options for the common path. The hosting layer already maps:
  • TextContent to TEXT_MESSAGE_*
  • TextReasoningContent to REASONING_*
  • FunctionCallContent to TOOL_CALL_*
  • FunctionResultContent to TOOL_CALL_RESULT
  • ToolApprovalRequestContent and InterruptRequestContent to interrupt outcomes
  • ChatResponseUpdate.RawRepresentation containing a BaseEvent directly to that event
If a ChatResponseUpdate.RawRepresentation is a BaseEvent, the hosting layer emits it verbatim. This is often the simplest way for a custom IChatClient wrapper to inject StateSnapshotEvent, CustomEvent, or RawEvent values.

MapContent

MapContent(Func<AIContent, IEnumerable<BaseEvent>?> mapper) receives an otherwise-unmapped AIContent and returns AG-UI events to emit. Return null to let the next mapper try the content.
using AGUI.Abstractions;
using AGUI.Hosting.AspNetCore;
using Microsoft.Extensions.AI;

public sealed class WorkflowStepContent : AIContent
{
    public string StepName { get; set; } = string.Empty;
}

var streamOptions = new AGUIStreamOptions()
    .MapContent(content =>
        content is WorkflowStepContent step
            ? [new StepStartedEvent { StepName = step.StepName }]
            : null);
Use this for framework-specific content types such as workflow markers, progress notifications, or domain events.
TextReasoningContent is already handled natively. The Step 07 sample emits TextReasoningContent, and the hosting layer converts it to reasoning events without custom MapContent configuration.

MapInterrupt

MapInterrupt(Func<AIContent, AGUIInterrupt?> mapper) receives an otherwise-unmapped AIContent and returns an AGUIInterrupt when the run needs user input. The hosting layer emits RunFinishedEvent with an interrupt outcome and stops the run. Return null to continue to the next mapper or to MapContent.
using AGUI.Abstractions;
using AGUI.Hosting.AspNetCore;
using Microsoft.Extensions.AI;

public sealed class ConfirmationRequiredContent : AIContent
{
    public string RequestId { get; set; } = string.Empty;

    public string Message { get; set; } = string.Empty;
}

var streamOptions = new AGUIStreamOptions()
    .MapInterrupt(content =>
        content is ConfirmationRequiredContent confirmation
            ? new AGUIInterrupt
            {
                Id = confirmation.RequestId,
                Reason = InterruptReasons.Confirmation,
                Message = confirmation.Message,
            }
            : null);
Use this for custom human-in-the-loop content that is not represented by the built-in InterruptRequestContent or tool approval content.

MapCall

MapCall(string toolName, Func<FunctionCallContent, IEnumerable<BaseEvent>> mapper) receives a matching FunctionCallContent and emits extra events after the normal tool call start, args, and end events.
using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Hosting.AspNetCore;

string? lastDocument = null;

var streamOptions = new AGUIStreamOptions()
    .MapCall("write_document", call =>
    {
        var document = call.Arguments?.TryGetValue("document", out var value) == true
            ? value?.ToString()
            : null;

        if (string.IsNullOrEmpty(document) || document == lastDocument)
        {
            return [];
        }

        lastDocument = document;

        var snapshot = JsonSerializer.SerializeToElement(new
        {
            document
        });

        return [new StateSnapshotEvent { Snapshot = snapshot }];
    });
Use this for streaming tool-argument previews. For example, while a model is building arguments for write_document, the UI can update a live document preview before the server-side tool finishes.

MapResult

MapResult(string toolName, Func<FunctionResultContent, IEnumerable<BaseEvent>> mapper) receives a matching tool result and emits extra events after the normal ToolCallResultEvent.
using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Hosting.AspNetCore;

var streamOptions = new AGUIStreamOptions()
    .MapResult("save_recipe", result =>
    {
        var snapshot = JsonSerializer.SerializeToElement(new
        {
            saved = true,
            recipe = result.Result,
        });

        return
        [
            new StateSnapshotEvent { Snapshot = snapshot },
            new CustomEvent
            {
                Name = "recipe.saved",
                Value = snapshot,
            },
        ];
    });
Use this when a server-side tool result should also update state, activity, or application-specific UI.

State mapping helpers

Two convenience methods cover the common state-management cases:
var streamOptions = new AGUIStreamOptions()
    .MapResultAsStateSnapshot("create_plan")
    .MapResultAsStateDelta("update_plan_step");
  • MapResultAsStateSnapshot(toolName) emits a StateSnapshotEvent
  • MapResultAsStateDelta(toolName) emits a StateDeltaEvent
Both helpers expect FunctionResultContent.Result to be a JsonElement. If your tool returns another type, use MapResult and serialize the value yourself.

RawRepresentation pass-through

For custom IChatClient wrappers, setting RawRepresentation to an AG-UI event is often simpler than defining a new AIContent type.
using System.Text.Json;
using AGUI.Abstractions;
using Microsoft.Extensions.AI;

var state = JsonSerializer.SerializeToElement(new
{
    status = "ready"
});

yield return new ChatResponseUpdate
{
    RawRepresentation = new StateSnapshotEvent
    {
        Snapshot = state,
    },
};
The converter emits the StateSnapshotEvent directly. If the raw event appears before a run has started, the hosting layer emits RUN_STARTED first.

Chaining

You can register multiple MapContent or MapInterrupt callbacks. They are tried in registration order, and the first non-null result wins.
var streamOptions = new AGUIStreamOptions()
    .MapInterrupt(TryMapConfirmation)
    .MapInterrupt(TryMapInputRequest)
    .MapContent(TryMapWorkflowStep)
    .MapContent(TryMapProgress);
Keep mappings small and focused. Use the built-in conversion for standard text, reasoning, tools, and interrupts, then add stream options only for the events that are specific to your server.