test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled

This commit is contained in:
2026-01-24 03:05:12 +11:00
parent f78f2388b3
commit 539852f81c
2584 changed files with 287471 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
using System.Text.Json.Serialization;
namespace AGUIClient;
[JsonSerializable(typeof(SensorRequest))]
[JsonSerializable(typeof(SensorResponse))]
internal sealed partial class AGUIClientSerializerContext : JsonSerializerContext;

View File

@@ -0,0 +1,213 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
using System.CommandLine;
using System.ComponentModel;
using System.Reflection;
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AGUIClient;
public static class Program
{
public static async Task<int> Main(string[] args)
{
// Create root command with options
RootCommand rootCommand = new("AGUIClient");
rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
// Run the command
return await rootCommand.Parse(args).InvokeAsync();
}
private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
{
// Set up the logging
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
ILogger logger = loggerFactory.CreateLogger("AGUIClient");
// Retrieve configuration settings
IConfigurationRoot configRoot = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.Build();
string serverUrl = configRoot["AGUI_SERVER_URL"] ?? "http://localhost:5100";
logger.LogInformation("Connecting to AG-UI server at: {ServerUrl}", serverUrl);
// Create the AG-UI client agent
using HttpClient httpClient = new()
{
Timeout = TimeSpan.FromSeconds(60)
};
var changeBackground = AIFunctionFactory.Create(
() =>
{
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.WriteLine("Changing color to blue");
},
name: "change_background_color",
description: "Change the console background color to dark blue."
);
var readClientClimateSensors = AIFunctionFactory.Create(
([Description("The sensors measurements to include in the response")] SensorRequest request) =>
{
return new SensorResponse()
{
Temperature = 22.5,
Humidity = 45.0,
AirQualityIndex = 75
};
},
name: "read_client_climate_sensors",
description: "Reads the climate sensor data from the client device.",
serializerOptions: AGUIClientSerializerContext.Default.Options
);
var chatClient = new AGUIChatClient(
httpClient,
serverUrl,
jsonSerializerOptions: AGUIClientSerializerContext.Default.Options);
AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
description: "AG-UI Client Agent",
tools: [changeBackground, readClientClimateSensors]);
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
try
{
while (true)
{
// Get user message
Console.Write("\nUser (:q or quit to exit): ");
string? message = Console.ReadLine();
if (string.IsNullOrWhiteSpace(message))
{
Console.WriteLine("Request cannot be empty.");
continue;
}
if (message is ":q" or "quit")
{
break;
}
messages.Add(new(ChatRole.User, message));
// Call RunStreamingAsync to get streaming updates
bool isFirstUpdate = true;
string? threadId = null;
var updates = new List<ChatResponseUpdate>();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
{
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
updates.Add(chatUpdate);
if (chatUpdate.ConversationId != null)
{
threadId = chatUpdate.ConversationId;
}
// Display run started information from the first update
if (isFirstUpdate && threadId != null && update.ResponseId != null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
// Display different content types with appropriate formatting
foreach (AIContent content in update.Contents)
{
switch (content)
{
case TextContent textContent:
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write(textContent.Text);
Console.ResetColor();
break;
case FunctionCallContent functionCallContent:
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}, Arguments: {PrintArguments(functionCallContent.Arguments)}]");
Console.ResetColor();
break;
case FunctionResultContent functionResultContent:
Console.ForegroundColor = ConsoleColor.Magenta;
if (functionResultContent.Exception != null)
{
Console.WriteLine($"\n[Function Result - Exception: {functionResultContent.Exception}]");
}
else
{
Console.WriteLine($"\n[Function Result - Result: {functionResultContent.Result}]");
}
Console.ResetColor();
break;
case ErrorContent errorContent:
Console.ForegroundColor = ConsoleColor.Red;
string code = errorContent.AdditionalProperties?["Code"] as string ?? "Unknown";
Console.WriteLine($"\n[Error - Code: {code}, Message: {errorContent.Message}]");
Console.ResetColor();
break;
}
}
}
if (updates.Count > 0 && !updates[^1].Contents.Any(c => c is TextContent))
{
var lastUpdate = updates[^1];
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine();
Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
Console.ResetColor();
}
messages.Clear();
Console.WriteLine();
}
}
catch (OperationCanceledException)
{
logger.LogInformation("AGUIClient operation was canceled.");
}
catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException and not ThreadAbortException and not AccessViolationException)
{
logger.LogError(ex, "An error occurred while running the AGUIClient");
return;
}
}
private static string PrintArguments(IDictionary<string, object?>? arguments)
{
if (arguments == null)
{
return "";
}
var builder = new StringBuilder().AppendLine();
foreach (var kvp in arguments)
{
builder
.AppendLine($" Name: {kvp.Key}")
.AppendLine($" Value: {kvp.Value}");
}
return builder.ToString();
}
}

View File

@@ -0,0 +1,34 @@
# AG-UI Client
This is a console application that demonstrates how to connect to an AG-UI server and interact with remote agents using the AG-UI protocol.
## Features
- Connects to an AG-UI server endpoint
- Displays streaming updates with color-coded output:
- **Yellow**: Run started notifications
- **Cyan**: Agent text responses (streamed)
- **Green**: Run finished notifications
- **Red**: Error messages (if any)
- Interactive prompt loop for sending messages
## Configuration
Set the following environment variable to specify the AG-UI server URL:
```powershell
$env:AGUI_SERVER_URL="http://localhost:5100"
```
If not set, the default is `http://localhost:5100`.
## Running the Client
1. Make sure the AG-UI server is running
2. Run the client:
```bash
cd AGUIClient
dotnet run
```
3. Enter your messages and observe the streaming updates
4. Type `:q` or `quit` to exit

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
namespace AGUIClient;
internal sealed class SensorRequest
{
public bool IncludeTemperature { get; set; } = true;
public bool IncludeHumidity { get; set; } = true;
public bool IncludeAirQualityIndex { get; set; } = true;
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
namespace AGUIClient;
internal sealed class SensorResponse
{
public double Temperature { get; set; }
public double Humidity { get; set; }
public int AirQualityIndex { get; set; }
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using AGUIDojoServer.AgenticUI;
using AGUIDojoServer.BackendToolRendering;
using AGUIDojoServer.PredictiveStateUpdates;
using AGUIDojoServer.SharedState;
namespace AGUIDojoServer;
[JsonSerializable(typeof(WeatherInfo))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(Plan))]
[JsonSerializable(typeof(Step))]
[JsonSerializable(typeof(StepStatus))]
[JsonSerializable(typeof(StepStatus?))]
[JsonSerializable(typeof(JsonPatchOperation))]
[JsonSerializable(typeof(List<JsonPatchOperation>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(DocumentState))]
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
namespace AGUIDojoServer.AgenticUI;
internal static class AgenticPlanningTools
{
[Description("Create a plan with multiple steps.")]
public static Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
{
return new Plan
{
Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
};
}
[Description("Update a step in the plan with new description or status.")]
public static async Task<List<JsonPatchOperation>> UpdatePlanStepAsync(
[Description("The index of the step to update.")] int index,
[Description("The new description for the step (optional).")] string? description = null,
[Description("The new status for the step (optional).")] StepStatus? status = null)
{
var changes = new List<JsonPatchOperation>();
if (description is not null)
{
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/description",
Value = description
});
}
if (status.HasValue)
{
// Status must be lowercase to match AG-UI frontend expectations: "pending" or "completed"
string statusValue = status.Value == StepStatus.Pending ? "pending" : "completed";
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/status",
Value = statusValue
});
}
await Task.Delay(1000);
return changes;
}
}

View File

@@ -0,0 +1,88 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer.AgenticUI;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateAgenticUI")]
internal sealed class AgenticUIAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public AgenticUIAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track function calls that should trigger state events
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Process contents: track function calls and emit state events for results
List<AIContent> stateEventsToEmit = new();
foreach (var content in update.Contents)
{
if (content is FunctionCallContent callContent)
{
if (callContent.Name == "create_plan" || callContent.Name == "update_plan_step")
{
trackedFunctionCalls[callContent.CallId] = callContent;
break;
}
}
else if (content is FunctionResultContent resultContent)
{
// Check if this result matches a tracked function call
if (trackedFunctionCalls.TryGetValue(resultContent.CallId, out var matchedCall))
{
var bytes = JsonSerializer.SerializeToUtf8Bytes((JsonElement)resultContent.Result!, this._jsonSerializerOptions);
// Determine event type based on the function name
if (matchedCall.Name == "create_plan")
{
stateEventsToEmit.Add(new DataContent(bytes, "application/json"));
}
else if (matchedCall.Name == "update_plan_step")
{
stateEventsToEmit.Add(new DataContent(bytes, "application/json-patch+json"));
}
}
}
}
yield return update;
yield return new AgentResponseUpdate(
new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
{
MessageId = "delta_" + Guid.NewGuid().ToString("N"),
CreatedAt = update.CreatedAt,
ResponseId = update.ResponseId,
AuthorName = update.AuthorName,
Role = update.Role,
ContinuationToken = update.ContinuationToken,
AdditionalProperties = update.AdditionalProperties,
})
{
AgentId = update.AgentId
};
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
internal sealed class JsonPatchOperation
{
[JsonPropertyName("op")]
public required string Op { get; set; }
[JsonPropertyName("path")]
public required string Path { get; set; }
[JsonPropertyName("value")]
public object? Value { get; set; }
[JsonPropertyName("from")]
public string? From { get; set; }
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
internal sealed class Plan
{
[JsonPropertyName("steps")]
public List<Step> Steps { get; set; } = [];
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
internal sealed class Step
{
[JsonPropertyName("description")]
public required string Description { get; set; }
[JsonPropertyName("status")]
public StepStatus Status { get; set; } = StepStatus.Pending;
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
[JsonConverter(typeof(JsonStringEnumConverter<StepStatus>))]
internal enum StepStatus
{
Pending,
Completed
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.BackendToolRendering;
internal sealed class WeatherInfo
{
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("conditions")]
public string Conditions { get; init; } = string.Empty;
[JsonPropertyName("humidity")]
public int Humidity { get; init; }
[JsonPropertyName("wind_speed")]
public int WindSpeed { get; init; }
[JsonPropertyName("feelsLike")]
public int FeelsLike { get; init; }
}

View File

@@ -0,0 +1,180 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json;
using AGUIDojoServer.AgenticUI;
using AGUIDojoServer.BackendToolRendering;
using AGUIDojoServer.PredictiveStateUpdates;
using AGUIDojoServer.SharedState;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ChatClient = OpenAI.Chat.ChatClient;
namespace AGUIDojoServer;
internal static class ChatClientAgentFactory
{
private static AzureOpenAIClient? s_azureOpenAIClient;
private static string? s_deploymentName;
public static void Initialize(IConfiguration configuration)
{
string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
s_azureOpenAIClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential());
}
public static ChatClientAgent CreateAgenticChat()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().AsAIAgent(
name: "AgenticChat",
description: "A simple chat agent using Azure OpenAI");
}
public static ChatClientAgent CreateBackendToolRendering()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().AsAIAgent(
name: "BackendToolRenderer",
description: "An agent that can render backend tools using Azure OpenAI",
tools: [AIFunctionFactory.Create(
GetWeather,
name: "get_weather",
description: "Get the weather for a given location.",
AGUIDojoServerSerializerContext.Default.Options)]);
}
public static ChatClientAgent CreateHumanInTheLoop()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().AsAIAgent(
name: "HumanInTheLoopAgent",
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
}
public static ChatClientAgent CreateToolBasedGenerativeUI()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().AsAIAgent(
name: "ToolBasedGenerativeUIAgent",
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
}
public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
{
Name = "AgenticUIAgent",
Description = "An agent that generates agentic user interfaces using Azure OpenAI",
ChatOptions = new ChatOptions
{
Instructions = """
When planning use tools only, without any other messages.
IMPORTANT:
- Use the `create_plan` tool to set the initial state of the steps
- Use the `update_plan_step` tool to update the status of each step
- Do NOT repeat the plan or summarise it in a message
- Do NOT confirm the creation or updates in a message
- Do NOT ask the user for additional information or next steps
- Do NOT leave a plan hanging, always complete the plan via `update_plan_step` if one is ongoing.
- Continue calling update_plan_step until all steps are marked as completed.
Only one plan can be active at a time, so do not call the `create_plan` tool
again until all the steps in current plan are completed.
""",
Tools = [
AIFunctionFactory.Create(
AgenticPlanningTools.CreatePlan,
name: "create_plan",
description: "Create a plan with multiple steps.",
AGUIDojoServerSerializerContext.Default.Options),
AIFunctionFactory.Create(
AgenticPlanningTools.UpdatePlanStepAsync,
name: "update_plan_step",
description: "Update a step in the plan with new description or status.",
AGUIDojoServerSerializerContext.Default.Options)
],
AllowMultipleToolCalls = false
}
});
return new AgenticUIAgent(baseAgent, options);
}
public static AIAgent CreateSharedState(JsonSerializerOptions options)
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
var baseAgent = chatClient.AsIChatClient().AsAIAgent(
name: "SharedStateAgent",
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
return new SharedStateAgent(baseAgent, options);
}
public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options)
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
{
Name = "PredictiveStateUpdatesAgent",
Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
ChatOptions = new ChatOptions
{
Instructions = """
You are a document editor assistant. When asked to write or edit content:
IMPORTANT:
- Use the `write_document` tool with the full document text in Markdown format
- Format the document extensively so it's easy to read
- You can use all kinds of markdown (headings, lists, bold, etc.)
- However, do NOT use italic or strike-through formatting
- You MUST write the full document, even when changing only a few words
- When making edits to the document, try to make them minimal - do not change every word
- Keep stories SHORT!
- After you are done writing the document you MUST call a confirm_changes tool after you call write_document
After the user confirms the changes, provide a brief summary of what you wrote.
""",
Tools = [
AIFunctionFactory.Create(
WriteDocument,
name: "write_document",
description: "Write a document. Use markdown formatting to format the document.",
AGUIDojoServerSerializerContext.Default.Options)
]
}
});
return new PredictiveStateUpdatesAgent(baseAgent, options);
}
[Description("Get the weather for a given location.")]
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
{
Temperature = 20,
Conditions = "sunny",
Humidity = 50,
WindSpeed = 10,
FeelsLike = 25
};
[Description("Write a document in markdown format.")]
private static string WriteDocument([Description("The document content to write.")] string document)
{
// Simply return success - the document is tracked via state updates
return "Document written successfully";
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.PredictiveStateUpdates;
internal sealed class DocumentState
{
[JsonPropertyName("document")]
public string Document { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer.PredictiveStateUpdates;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreatePredictiveStateUpdates")]
internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
private const int ChunkSize = 10; // Characters per chunk for streaming effect
public PredictiveStateUpdatesAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track the last emitted document state to avoid duplicates
string? lastEmittedDocument = null;
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Check if we're seeing a write_document tool call and emit predictive state
bool hasToolCall = false;
string? documentContent = null;
foreach (var content in update.Contents)
{
if (content is FunctionCallContent callContent && callContent.Name == "write_document")
{
hasToolCall = true;
// Try to extract the document argument directly from the dictionary
if (callContent.Arguments?.TryGetValue("document", out var documentValue) == true)
{
documentContent = documentValue?.ToString();
}
}
}
// Always yield the original update first
yield return update;
// If we got a complete tool call with document content, "fake" stream it in chunks
if (hasToolCall && documentContent != null && documentContent != lastEmittedDocument)
{
// Chunk the document content and emit progressive state updates
int startIndex = 0;
if (lastEmittedDocument != null && documentContent.StartsWith(lastEmittedDocument, StringComparison.Ordinal))
{
// Only stream the new portion that was added
startIndex = lastEmittedDocument.Length;
}
// Stream the document in chunks
for (int i = startIndex; i < documentContent.Length; i += ChunkSize)
{
int length = Math.Min(ChunkSize, documentContent.Length - i);
string chunk = documentContent.Substring(0, i + length);
// Prepare predictive state update as DataContent
var stateUpdate = new DocumentState { Document = chunk };
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateUpdate,
this._jsonSerializerOptions.GetTypeInfo(typeof(DocumentState)));
yield return new AgentResponseUpdate(
new ChatResponseUpdate(role: ChatRole.Assistant, [new DataContent(stateBytes, "application/json")])
{
MessageId = "snapshot" + Guid.NewGuid().ToString("N"),
CreatedAt = update.CreatedAt,
ResponseId = update.ResponseId,
AdditionalProperties = update.AdditionalProperties,
AuthorName = update.AuthorName,
ContinuationToken = update.ContinuationToken,
})
{
AgentId = update.AgentId
};
// Small delay to simulate streaming
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
lastEmittedDocument = documentContent;
}
}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUIDojoServer;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.Extensions.Options;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
logging.RequestBodyLogLimit = int.MaxValue;
logging.ResponseBodyLogLimit = int.MaxValue;
});
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIDojoServerSerializerContext.Default));
builder.Services.AddAGUI();
WebApplication app = builder.Build();
app.UseHttpLogging();
// Initialize the factory
ChatClientAgentFactory.Initialize(app.Configuration);
// Map the AG-UI agent endpoints for different scenarios
app.MapAGUI("/agentic_chat", ChatClientAgentFactory.CreateAgenticChat());
app.MapAGUI("/backend_tool_rendering", ChatClientAgentFactory.CreateBackendToolRendering());
app.MapAGUI("/human_in_the_loop", ChatClientAgentFactory.CreateHumanInTheLoop());
app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI());
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI(jsonOptions.Value.SerializerOptions));
app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));
app.MapAGUI("/predictive_state_updates", ChatClientAgentFactory.CreatePredictiveStateUpdates(jsonOptions.Value.SerializerOptions));
await app.RunAsync();
public partial class Program;

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"AGUIDojoServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5018"
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.SharedState;
internal sealed class Ingredient
{
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("amount")]
public string Amount { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.SharedState;
internal sealed class Recipe
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;
[JsonPropertyName("cooking_time")]
public string CookingTime { get; set; } = string.Empty;
[JsonPropertyName("special_preferences")]
public List<string> SpecialPreferences { get; set; } = [];
[JsonPropertyName("ingredients")]
public List<Ingredient> Ingredients { get; set; } = [];
[JsonPropertyName("instructions")]
public List<string> Instructions { get; set; } = [];
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.SharedState;
#pragma warning disable CA1812 // Used for the JsonSchema response format
internal sealed class RecipeResponse
#pragma warning restore CA1812
{
[JsonPropertyName("recipe")]
public Recipe Recipe { get; set; } = new();
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer.SharedState;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")]
internal sealed class SharedStateAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
{
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
yield break;
}
var firstRunOptions = new ChatClientAgentRunOptions
{
ChatOptions = chatRunOptions.ChatOptions.Clone(),
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
ContinuationToken = chatRunOptions.ContinuationToken,
ChatClientFactory = chatRunOptions.ChatClientFactory,
};
// Configure JSON schema response format for structured state output
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<RecipeResponse>(
schemaName: "RecipeResponse",
schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions");
ChatMessage stateUpdateMessage = new(
ChatRole.System,
[
new TextContent("Here is the current state in JSON format:"),
new TextContent(state.GetRawText()),
new TextContent("The new state is:")
]);
var firstRunMessages = messages.Append(stateUpdateMessage);
var allUpdates = new List<AgentResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
// Yield all non-text updates (tool calls, etc.)
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
if (hasNonTextContent)
{
yield return update;
}
}
var response = allUpdates.ToAgentResponse();
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
{
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
}
else
{
yield break;
}
var secondRunMessages = messages.Concat(response.Messages).Append(
new ChatMessage(
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
@host = http://localhost:5100
### Send a message to the AG-UI agent
POST {{host}}/
Content-Type: application/json
{
"threadId": "thread_123",
"runId": "run_456",
"messages": [
{
"role": "user",
"content": "What is the capital of France?"
}
],
"context": {}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIServer;
[JsonSerializable(typeof(ServerWeatherForecastRequest))]
[JsonSerializable(typeof(ServerWeatherForecastResponse))]
internal sealed partial class AGUIServerSerializerContext : JsonSerializerContext;

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using AGUIServer;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIServerSerializerContext.Default));
builder.Services.AddAGUI();
WebApplication app = builder.Build();
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Create the AI agent with tools
var agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(
name: "AGUIAssistant",
tools: [
AIFunctionFactory.Create(
() => DateTimeOffset.UtcNow,
name: "get_current_time",
description: "Get the current UTC time."
),
AIFunctionFactory.Create(
([Description("The weather forecast request")]ServerWeatherForecastRequest request) => {
return new ServerWeatherForecastResponse()
{
Summary = "Sunny",
TemperatureC = 25,
Date = request.Date
};
},
name: "get_server_weather_forecast",
description: "Gets the forecast for a specific location and date",
AGUIServerSerializerContext.Default.Options)
]);
// Map the AG-UI agent endpoint
app.MapAGUI("/", agent);
await app.RunAsync();

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"AGUIServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5100;https://localhost:5101"
}
}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
namespace AGUIServer;
internal sealed class ServerWeatherForecastRequest
{
public DateTime Date { get; set; }
public string Location { get; set; } = "Seattle";
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
namespace AGUIServer;
internal sealed class ServerWeatherForecastResponse
{
public string Summary { get; set; } = "";
public int TemperatureC { get; set; }
public DateTime Date { get; set; }
}

View File

@@ -0,0 +1,208 @@
# AG-UI Client and Server Sample
This sample demonstrates how to use the AG-UI (Agent UI) protocol to enable communication between a client application and a remote agent server. The AG-UI protocol provides a standardized way for clients to interact with AI agents.
## Overview
The demonstration has two components:
1. **AGUIServer** - An ASP.NET Core web server that hosts an AI agent and exposes it via the AG-UI protocol
2. **AGUIClient** - A console application that connects to the AG-UI server and displays streaming updates
> **Warning**
> The AG-UI protocol is still under development and changing.
> We will try to keep these samples updated as the protocol evolves.
## Configuring Environment Variables
Configure the required Azure OpenAI environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="<<your-model-endpoint>>"
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4.1-mini"
```
> **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables).
## Running the Sample
### Step 1: Start the AG-UI Server
```bash
cd AGUIServer
dotnet build
dotnet run --urls "http://localhost:5100"
```
The server will start and listen on `http://localhost:5100`.
### Step 2: Testing with the REST Client (Optional)
Before running the client, you can test the server using the included `.http` file:
1. Open [./AGUIServer/AGUIServer.http](./AGUIServer/AGUIServer.http) in Visual Studio or VS Code with the REST Client extension
2. Send a test request to verify the server is working
3. Observe the server-sent events stream in the response
Sample request:
```http
POST http://localhost:5100/
Content-Type: application/json
{
"threadId": "thread_123",
"runId": "run_456",
"messages": [
{
"role": "user",
"content": "What is the capital of France?"
}
],
"context": {}
}
```
### Step 3: Run the AG-UI Client
In a new terminal window:
```bash
cd AGUIClient
dotnet run
```
Optionally, configure a different server URL:
```powershell
$env:AGUI_SERVER_URL="http://localhost:5100"
```
### Step 4: Interact with the Agent
1. The client will connect to the AG-UI server
2. Enter your message at the prompt
3. Observe the streaming updates with color-coded output:
- **Yellow**: Run started notification showing thread and run IDs
- **Cyan**: Agent's text response (streamed character by character)
- **Green**: Run finished notification
- **Red**: Error messages (if any occur)
4. Type `:q` or `quit` to exit
## Sample Output
```
AGUIClient> dotnet run
info: AGUIClient[0]
Connecting to AG-UI server at: http://localhost:5100
User (:q or quit to exit): What is the capital of France?
[Run Started - Thread: thread_abc123, Run: run_xyz789]
The capital of France is Paris. It is known for its rich history, culture, and iconic landmarks such as the Eiffel Tower and the Louvre Museum.
[Run Finished - Thread: thread_abc123, Run: run_xyz789]
User (:q or quit to exit): Tell me a fun fact about space
[Run Started - Thread: thread_abc123, Run: run_def456]
Here's a fun fact: A day on Venus is longer than its year! Venus takes about 243 Earth days to rotate once on its axis, but only about 225 Earth days to orbit the Sun.
[Run Finished - Thread: thread_abc123, Run: run_def456]
User (:q or quit to exit): :q
```
## How It Works
### Server Side
The `AGUIServer` uses the `MapAGUI` extension method to expose an agent through the AG-UI protocol:
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.AsAIAgent(
instructions: "You are a helpful assistant.",
name: "AGUIAssistant");
app.MapAGUI("/", agent);
```
This automatically handles:
- HTTP POST requests with message payloads
- Converting agent responses to AG-UI event streams
- Server-sent events (SSE) formatting
- Thread and run management
### Client Side
The `AGUIClient` uses the `AGUIChatClient` to connect to the remote server:
```csharp
using HttpClient httpClient = new();
var chatClient = new AGUIChatClient(
httpClient,
endpoint: serverUrl,
modelId: "agui-client",
jsonSerializerOptions: null);
AIAgent agent = chatClient.AsAIAgent(
instructions: null,
name: "agui-client",
description: "AG-UI Client Agent",
tools: []);
bool isFirstUpdate = true;
AgentResponseUpdate? currentUpdate = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
// First update indicates run started
if (isFirstUpdate)
{
Console.WriteLine($"[Run Started - Thread: {update.ConversationId}, Run: {update.ResponseId}]");
isFirstUpdate = false;
}
currentUpdate = update;
foreach (AIContent content in update.Contents)
{
switch (content)
{
case TextContent textContent:
// Display streaming text
Console.Write(textContent.Text);
break;
case ErrorContent errorContent:
// Display error notification
Console.WriteLine($"[Error: {errorContent.Message}]");
break;
}
}
}
// Last update indicates run finished
if (currentUpdate != null)
{
Console.WriteLine($"\n[Run Finished - Thread: {currentUpdate.ConversationId}, Run: {currentUpdate.ResponseId}]");
}
```
The `RunStreamingAsync` method:
1. Sends messages to the server via HTTP POST
2. Receives server-sent events (SSE) stream
3. Parses events into `AgentResponseUpdate` objects
4. Yields updates as they arrive for real-time display
## Key Concepts
- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
- **AgentResponseUpdate**: Contains the response data with:
- `ResponseId`: The unique run identifier
- `ConversationId`: The thread/conversation identifier
- `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
- **Run Lifecycle**:
- The **first** `AgentResponseUpdate` in a run indicates the run has started
- Subsequent updates contain streaming content as the agent processes
- The **last** `AgentResponseUpdate` in a run indicates the run has finished
- If an error occurs, the update will contain `ErrorContent`