Skip to content

.NET SDK #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
@@ -80,6 +80,29 @@
"pages": ["sdk/python/encoder/overview"]
}
]
},
{
"group": ".NET",
"pages": [
"sdk/dotnet/overview",
"sdk/dotnet/types",
"sdk/dotnet/events",
"sdk/dotnet/json",
{
"group": "Agents",
"pages": [
"sdk/dotnet/agents",
{
"group": "IChatClient",
"pages": [
"/sdk/dotnet/agent-types/chatclientagent",
"/sdk/dotnet/agent-types/statefulchatclientagent"
]
}
]
},
"sdk/dotnet/aspnet"
]
}
]
}
@@ -95,6 +118,11 @@
"anchor": "Python SDK",
"href": "https://docs.ag-ui.com/sdk/python/core/overview",
"icon": "python"
},
{
"anchor": ".NET SDK",
"href": "https://docs.ag-ui.com/sdk/dotnet/core/overview",
"icon": "microsoft"
}
]
}
522 changes: 522 additions & 0 deletions docs/sdk/dotnet/agent-types/chatclientagent.mdx

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions docs/sdk/dotnet/agent-types/statefulchatclientagent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: "StatefulChatClientAgent"
description:
"Agent User Interaction Protocol .NET SDK StatefulChatClientAgent reference"
---

# The StatefulChatClientAgent

The `StatefulChatClientAgent` is a specialised agent implementation that derives
from `ChatClientAgent` in order to provide frontend-agent state collaboration.

It relies on `ChatClientAgent` to provide all the same core functionality, but
tweaks the system message to direct the LLM to actively read and write to the
shared state of the agent, while registering new backend tools that allow the
agent to do so.

It emits the necessary Agent User Interaction Protocol events to handle state
synchronisation between the frontend and the agent, allowing for a rapid
bootstrapping of this common scenario.

<Warning>
This agent is experimental and may not provide the best results in all scenarios.

It relies heavily on a hardcoded system message that wraps the configured one,
and so may sometimes fail to always produce the expected behaviour or interact
with the shared state in all cases.

It is recommended to use this agent to bootstrap your proof of concept, and then
use it as a reference to implement your own agent from `ChatClientAgent` or
`IAGUIAgent` that is tailored to your specific use case.

</Warning>

## Usage

```csharp
record Recipe(
string Name,
string Description,
string Ingredients,
string Instructions
);

var myProviderIChatClient = GetProviderIChatClient();
var chatClient = new ChatClientBuilder(myProviderIChatClient)
.UseFunctionInvocation()
.Build();

var agent = new StatefulChatClientAgent<Recipe>(
chatClient,
// Define initial state for the agent to begin with if not provided by the run input
new Recipe(),
new StatefulChatClientAgentOptions<Recipe>
{
SystemMessage = "You can override the system message passed in the RunAgentInput",
ChatOptions = new ChatOptions
{
Tools = [
AiFunctionFactory.Create(
MyDotnetDelegate
)
]
}
}
)
```

## Options

The agent accepts an options type `StateChatClientAgentOptions<TState>` that
derives from `ChatClientAgentOptions` and allows you to provide all the same
options as you would to a `ChatClientAgent`.

```csharp
public record StatefulChatClientAgentOptions<TState> : ChatClientAgentOptions where TState : notnull
{
public string StateRetrievalFunctionName { get; init; } = "retrieve_state";
public string StateRetrievalFunctionDescription { get; init; } = "Retrieves the current shared state of the agent.";
public string StateUpdateFunctionName { get; init; } = "update_state";
public string StateUpdateFunctionDescription { get; init; } = "Updates the current shared state of the agent.";
public bool EmitStateFunctionsToFrontend { get; init; } = true;
}
```

| Option | Default Value | Description |
| ----------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
| `StateRetrievalFunctionName` | `"retrieve_state"` | Name of the function to retrieve the current shared state. |
| `StateRetrievalFunctionDescription` | `"Retrieves the current shared state of the agent."` | Description for the state retrieval function. |
| `StateUpdateFunctionName` | `"update_state"` | Name of the function to update the current shared state. |
| `StateUpdateFunctionDescription` | `"Updates the current shared state of the agent."` | Description for the state update function. |
| `EmitStateFunctionsToFrontend` | `true` | Whether to notify the frontend when state functions are called. |

## Extending the Agent

The agent can also be extended by deriving from it, but it does not currently
offer any additional functionality beyond what `ChatClientAgent` provides, but
it does allow you to override the same methods as you would be able to when
deriving from `ChatClientAgent`.

<Warning>
Beware deriving from this agent, as if you interfere with the methods it
overrides, you may as well just derive from `ChatClientAgent` instead.
</Warning>
288 changes: 288 additions & 0 deletions docs/sdk/dotnet/agents.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
---
title: "Overview"
description: "Agent User Interaction Protocol .NET SDK Agent functionality"
---

# Agents

The Agent User Interaction Protocol SDK provides out-of-the-box agent
implementations that you can customise and build upon, as well as a low-level
interface for implementing your own agents from scratch.

## Low-Level

You can implement your own agents using the low-level interface, which gives you
the most flexibility and control over the agent's behaviour.

Reasons you might want to use the interface directly include:

- You need to implement a custom agent that uses a different agent framework.
- You need to implement a custom agent that doesn't fit the standard LLM-backed
conversational flow.

### The `IAGUIAgent` Interface

```csharp
public interface IAGUIAgent
{
Task RunAsync(
RunAgentInput input,
ChannelWriter<BaseEvent> events,
CancellationToken cancellationToken = default
);
}
```

All of the agents in the SDK implement this interface, which provides a simple
and extensible contract for running Agent User Interaction Protocol compatible
agents with the `RunAsync` method that takes the following parameters:

| Property | Type | Description |
| ------------------- | -------------------------- | -------------------------------------- |
| `input` | `RunAgentInput` | Input parameters for running the agent |
| `events` | `ChannelWriter<BaseEvent>` | Channel to write events to |
| `cancellationToken` | `CancellationToken` | Token to cancel the operation |

#### Invocation Helpers

You can construct your own channel to pass to the `RunAsync` method as follows:

```csharp
var channel = Channel.CreateUnbounded<BaseEvent>(
new UnboundedChannelOptions {
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = true,
}
);

_ = Task.Run(async () => {
await agent.RunAsync(
input,
channel.Writer,
cancellationToken
);
});

await foreach (var emittedEvent in
channel.Reader.ReadAllAsync(cancellationToken))
{
// Handle the emitted event
}
```

The SDK provides an extension method that takes care of this for you:

```csharp
await foreach(var emittedEvent in
agent.RunToCompletionAsync(input, cancellationToken))
{
// Handle the emitted event
}
```

### `EchoAgent` Example

The SDK includes a simple `EchoAgent` that implements the interface:

````csharp
public sealed class EchoAgent : IAGUIAgent
{
public async Task RunAsync(
RunAgentInput input,
ChannelWriter<BaseEvent> events,
CancellationToken ct = default
)
{
await events.WriteAsync(
new RunStartedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},
ct
);

var lastMessage = input.Messages.LastOrDefault();

await Task.Delay(500, ct);

switch (lastMessage)
{
case SystemMessage system:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing system message:\n\n```\n{system.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

case UserMessage user:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing user message:\n\n```\n{user.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

case AssistantMessage assistant:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing assistant message:\n\n```\n{assistant.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

case ToolMessage tool:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing tool message for tool call '{tool.ToolCallId}':\n\n```\n{tool.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

default:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Unknown message type: {lastMessage?.GetType().Name ?? "null"}"
))
{
await events.WriteAsync(ev, ct);
}
break;
}

await events.WriteAsync(
new RunFinishedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},
ct
);

events.Complete();
}
}
````

This simple agent echoes the last message it receives, but demonstrates the core
lifecycle events and how to emit Agent User Interaction Protocol events using
the provided channel writer.

## `Microsoft.Extensions.AI` Integration

<Note>
The SDK uses the `Microsoft.Extensions.AI` package, not the
`Microsoft.Extensions.AI.Abstractions` package, as it needs some of the
concrete implementations to handle function calling and other features.
</Note>

The SDK takes a dependency on the
[Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
package which means you can use the `IChatClient` abstractions to power your
agents - and bring all the LLM providers you already use.

### The `ChatClientAgent` Class

This class can be used standalone or as a base class for your own agents. It
provides for the core scenario of running an agent that involves an LLM
conversational flow, and handles the boilerplate of emitting lifecycle events
and handling backend and frontend tool calls.

```csharp
var myProviderIChatClient = GetProviderIChatClient();

var chatClient = new ChatClientBuilder(myProviderIChatClient)
.UseFunctionInvocation()
.Build();

var agent = new ChatClientAgent(
chatClient,
new ChatClientAgentOptions
{
SystemMessage = "You can override the system message passed in the RunAgentInput",
ChatOptions = new ChatOptions
{
Tools = [
AiFunctionFactory.Create(
MyDotnetDelegate
)
]
}
}
);
```

<Card
title="ChatClientAgent Reference"
icon="cube"
href="/sdk/dotnet/agent-types/chatclientagent"
color="#3B82F6"
iconType="solid"
>
Complete documentation of the `ChatClientAgent` class
</Card>

### The `StatefulChatClientAgent` Class

This agent extends the `ChatClientAgent` class and is tailored for scenarios
where the agent collaborates with the frontend to maintain a shared state.

An example of this could be where you have some state in the frontend that
represents the task the user is working on, and you want the agent to be able to
read and write to that state.

```csharp
record Recipe(
string Name,
string Description,
string Ingredients,
string Instructions
);

var myProviderIChatClient = GetProviderIChatClient();
var chatClient = new ChatClientBuilder(myProviderIChatClient)
.UseFunctionInvocation()
.Build();

var agent = new StatefulChatClientAgent<Recipe>(
chatClient,
// Define initial state for the agent to begin with if not provided by the run input
new Recipe(),
new StatefulChatClientAgentOptions<Recipe>
{
SystemMessage = "You can override the system message passed in the RunAgentInput",
ChatOptions = new ChatOptions
{
Tools = [
AiFunctionFactory.Create(
MyDotnetDelegate
)
]
}
}
)
```

This agent will use your provided system message, and tweak it to direct the LLM
to read and write to the shared state of type `Recipe` that you provide.

It registers new backend tools that allow the agent to do so, and emits the
necessary Agent User Interaction Protocol events to keep the frontend in sync
with the agent's state.

<Card
title="StatefulChatClientAgent Reference"
icon="cube"
href="/sdk/dotnet/agent-types/statefulchatclientagent"
color="#3B82F6"
iconType="solid"
>
Complete documentation of the `StatefulChatClientAgent` class
</Card>
51 changes: 51 additions & 0 deletions docs/sdk/dotnet/aspnet.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: "ASP.NET"
description: "Agent User Interaction Protocol .NET SDK ASP.NET integration"
---

The most common way to integrate Agent User Interaction Protocol agents
currently is over HTTP which frontends like
[CopilotKit](https://www.copilotkit.ai) can use via the AG-UI
[HttpAgent](/concepts/agents#httpagent) agent implementation.

For that reason the .NET SDK provides a simple extension method to register a
[Minimal API](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview)
POST endpoint that can be used to expose agents:

```csharp
builder.Services.ConfigureHttpJsonOptions(opts =>
{
// Necessary as the type discriminator is not the first property in the JSON objects used by the AG-UI protocol.
opts.SerializerOptions.AllowOutOfOrderMetadataProperties = true;
opts.SerializerOptions.WriteIndented = false;
opts.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

// Necessary as consumers of the AG-UI protocol (e.g. Zod-powered schemas) will not accept null values for optional properties.
// So we need to ensure that null values are not serialized.
opts.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});

builder.Services.AddChatClient(
new ChatClientBuilder(myProviderIChatClient)
.UseFunctionInvocation()
);

var app = builder.Build();

var agentsGroup = app.MapGroup("/agents");

agentsGroup.MapAgentEndpoint(
"my-agent-name",
sp => {
var chatClient = sp.GetRequiredService<IChatClient>();
return new ChatClientAgent(chatClient);
}
);
```

This will create a POST endpoint at `/agents/my-agent-name` that accepts the
`RunAgentInput` body and returns a server-sent-events stream of the Agent User
Interaction Protocol events by invoking the provided agent.

You can configure the endpoint like any other one, including adding all the same
authentication / authorization / policy behaviour you would expect from ASP.NET.
340 changes: 340 additions & 0 deletions docs/sdk/dotnet/events.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
---
title: "Events"
description:
"Documentation for the events used in the Agent User Interaction Protocol .NET
SDK"
---

# Events

The Agent User Interaction Protocol .NET SDK uses a streaming event-based
architecture. Events are the fundamental units of communication between agents
and the frontend. This section documents the event types and their properties.

## EventTypes Enum

The `EventTypes` class defines all possible event types in the system:

```csharp
public static class EventTypes
{
public const string TEXT_MESSAGE_START = "TEXT_MESSAGE_START";
public const string TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT";
public const string TEXT_MESSAGE_END = "TEXT_MESSAGE_END";
public const string TOOL_CALL_START = "TOOL_CALL_START";
public const string TOOL_CALL_ARGS = "TOOL_CALL_ARGS";
public const string TOOL_CALL_END = "TOOL_CALL_END";
public const string STATE_SNAPSHOT = "STATE_SNAPSHOT";
public const string STATE_DELTA = "STATE_DELTA";
public const string MESSAGES_SNAPSHOT = "MESSAGES_SNAPSHOT";
public const string RAW = "RAW";
public const string CUSTOM = "CUSTOM";
public const string RUN_STARTED = "RUN_STARTED";
public const string RUN_FINISHED = "RUN_FINISHED";
public const string RUN_ERROR = "RUN_ERROR";
public const string STEP_STARTED = "STEP_STARTED";
public const string STEP_FINISHED = "STEP_FINISHED";
}
```

## BaseEvent

All events inherit from the `BaseEvent` type, which provides common properties
shared across all event types.

<Note>
The .NET SDK uses polymorphic serialization for events, with the{" "}
<code>type</code> discriminator field handled via attributes. Each event type
is a derived record of <code>BaseEvent</code>.
</Note>

```csharp
public abstract record BaseEvent
{
public long? Timestamp { get; init; }
public object? RawEvent { get; init; }
}
```

| Property | Type | Description |
| ----------- | --------- | ------------------------------------------------- |
| `Timestamp` | `long?` | Timestamp when the event was created |
| `RawEvent` | `object?` | Original event data if this event was transformed |

## Lifecycle Events

These events represent the lifecycle of an agent run.

### RunStartedEvent

Signals the start of an agent run.

```csharp
public sealed record RunStartedEvent : BaseEvent
{
public required string ThreadId { get; init; }
public required string RunId { get; init; }
}
```

| Property | Type | Description |
| ---------- | -------- | ----------------------------- |
| `ThreadId` | `string` | ID of the conversation thread |
| `RunId` | `string` | ID of the agent run |

### RunFinishedEvent

Signals the successful completion of an agent run.

```csharp
public sealed record RunFinishedEvent : BaseEvent
{
public required string ThreadId { get; init; }
public required string RunId { get; init; }
}
```

| Property | Type | Description |
| ---------- | -------- | ----------------------------- |
| `ThreadId` | `string` | ID of the conversation thread |
| `RunId` | `string` | ID of the agent run |

### RunErrorEvent

Signals an error during an agent run.

```csharp
public sealed record RunErrorEvent : BaseEvent
{
public required string Message { get; init; }
public string? Code { get; init; }
}
```

| Property | Type | Description |
| --------- | --------- | ------------- |
| `Message` | `string` | Error message |
| `Code` | `string?` | Error code |

### StepStartedEvent

Signals the start of a step within an agent run.

```csharp
public sealed record StepStartedEvent : BaseEvent
{
public required string StepName { get; init; }
}
```

| Property | Type | Description |
| ---------- | -------- | ---------------- |
| `StepName` | `string` | Name of the step |

### StepFinishedEvent

Signals the completion of a step within an agent run.

```csharp
public sealed record StepFinishedEvent : BaseEvent
{
public required string StepName { get; init; }
}
```

| Property | Type | Description |
| ---------- | -------- | ---------------- |
| `StepName` | `string` | Name of the step |

## Text Message Events

These events represent the lifecycle of text messages in a conversation.

### TextMessageStartEvent

Signals the start of a text message.

```csharp
public sealed record TextMessageStartEvent : BaseEvent
{
public required string MessageId { get; init; }
public string Role => MessageRoles.Assistant;
}
```

| Property | Type | Description |
| ----------- | -------- | --------------------------------- |
| `MessageId` | `string` | Unique identifier for the message |
| `Role` | `string` | Role is always "assistant" |

### TextMessageContentEvent

Represents a chunk of content in a streaming text message.

```csharp
public sealed record TextMessageContentEvent : BaseEvent
{
public required string MessageId { get; init; }
public required string Delta { get; init; }
}
```

| Property | Type | Description |
| ----------- | -------- | ----------------------------------------- |
| `MessageId` | `string` | Matches the ID from TextMessageStartEvent |
| `Delta` | `string` | Text content chunk (non-empty) |

### TextMessageEndEvent

Signals the end of a text message.

```csharp
public sealed record TextMessageEndEvent : BaseEvent
{
public required string MessageId { get; init; }
}
```

| Property | Type | Description |
| ----------- | -------- | ----------------------------------------- |
| `MessageId` | `string` | Matches the ID from TextMessageStartEvent |

## Tool Call Events

These events represent the lifecycle of tool calls made by agents.

### ToolCallStartEvent

Signals the start of a tool call.

```csharp
public sealed record ToolCallStartEvent : BaseEvent
{
public required string ToolCallId { get; init; }
public required string ToolCallName { get; init; }
public string? ParentMessageId { get; init; }
}
```

| Property | Type | Description |
| ----------------- | --------- | ----------------------------------- |
| `ToolCallId` | `string` | Unique identifier for the tool call |
| `ToolCallName` | `string` | Name of the tool being called |
| `ParentMessageId` | `string?` | ID of the parent message |

### ToolCallArgsEvent

Represents a chunk of argument data for a tool call.

```csharp
public sealed record ToolCallArgsEvent : BaseEvent
{
public required string ToolCallId { get; init; }
public required string Delta { get; init; }
}
```

| Property | Type | Description |
| ------------ | -------- | -------------------------------------- |
| `ToolCallId` | `string` | Matches the ID from ToolCallStartEvent |
| `Delta` | `string` | Argument data chunk |

### ToolCallEndEvent

Signals the end of a tool call.

```csharp
public sealed record ToolCallEndEvent : BaseEvent
{
public required string ToolCallId { get; init; }
}
```

| Property | Type | Description |
| ------------ | -------- | -------------------------------------- |
| `ToolCallId` | `string` | Matches the ID from ToolCallStartEvent |

## State Management Events

These events are used to manage agent state.

### StateSnapshotEvent

Provides a complete snapshot of an agent's state.

```csharp
public sealed record StateSnapshotEvent : BaseEvent
{
public required object Snapshot { get; init; }
}
```

| Property | Type | Description |
| ---------- | -------- | ----------------------- |
| `Snapshot` | `object` | Complete state snapshot |

### StateDeltaEvent

Provides a partial update to an agent's state using JSON Patch.

```csharp
public sealed record StateDeltaEvent : BaseEvent
{
public required ImmutableList<object> Delta { get; init; } = ImmutableList<object>.Empty;
}
```

| Property | Type | Description |
| -------- | ----------------------- | ----------------------------------- |
| `Delta` | `ImmutableList<object>` | Collection of JSON Patch operations |

### MessagesSnapshotEvent

Provides a snapshot of all messages in a conversation.

```csharp
public sealed record MessagesSnapshotEvent : BaseEvent
{
public required ImmutableList<BaseMessage> Messages { get; init; } = ImmutableList<BaseMessage>.Empty;
}
```

| Property | Type | Description |
| ---------- | ---------------------------- | ----------------------------- |
| `Messages` | `ImmutableList<BaseMessage>` | Collection of message objects |

## Special Events

### RawEvent

Used to pass through events from external systems.

```csharp
public sealed record RawEvent : BaseEvent
{
public required object Event { get; init; }
public string? Source { get; init; }
}
```

| Property | Type | Description |
| -------- | --------- | ------------------- |
| `Event` | `object` | Original event data |
| `Source` | `string?` | Source of the event |

### CustomEvent

Used for application-specific custom events.

```csharp
public sealed record CustomEvent : BaseEvent
{
public required string Name { get; init; }
public required object Value { get; init; }
}
```

| Property | Type | Description |
| -------- | -------- | ------------------------------- |
| `Name` | `string` | Name of the custom event |
| `Value` | `object` | Value associated with the event |
35 changes: 35 additions & 0 deletions docs/sdk/dotnet/json.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: "JSON"
description: "Working with JSON in the .NET SDK"
---

# JSON Handling

The Agent User Interaction Protocol .NET SDK uses the `System.Text.Json` types
for handling JSON data.

In order to provide the best handling when talking to the frontend, the
following `JsonSerializerOptions` are recommended:

```csharp
new JsonSerializerOptions(JsonSerializerDefaults.Web) {
AllowOutOfOrderMetadataProperties = true,
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,

DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
}
```

`System.Text.Json` by default when handling polymorphic types expects the type
discriminator (a metadata property) to be one of the first properties in the
JSON object - which is not guaranteed by either the frontends you interact with
or the JSON specification itself.

Setting `AllowOutOfOrderMetadataProperties` to `true` allows the serializer to
handle this case correctly.

Some frontends (like Zod-powered schemas) may not accept null values for
optional properties, so setting `DefaultIgnoreCondition` to
`JsonIgnoreCondition.WhenWritingNull` ensures that null values are not
serialized.
98 changes: 98 additions & 0 deletions docs/sdk/dotnet/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: "Overview"
description: "Core concepts in the Agent User Interaction Protocol .NET SDK"
---

<Card
title="Nuget Availability TBD"
icon="exclamation"
color="#F97316"
iconType="solid"
>
For now, it is possible to clone the code and add it as a project reference.

</Card>

```bash
dotnet add package AGUIDotnet
```

# AGUIDotnet

The Agent User Interaction Protocol SDK for .NET provides the core types and
baseline agent implementations for building AG-UI compatible agents using the
[Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
`IChatClient` abstractions.

## .NET Version Support

The AGUIDotnet package currently targets the .NET 9 SDK, and includes a
framework reference for `Microsoft.AspNetCore.App` in order to provide helpful
integrations for hosting agents via endpoints.

## Types

Core data structures that represent building blocks of the system:

- [RunAgentInput](/sdk/dotnet/types#runagentinput) - Input parameters for
running agents
- [BaseMessage](/sdk/dotnet/types#message-types) - Message types that AG-UI
provides

<Card
title="Types Reference"
icon="cube"
href="/sdk/dotnet/types"
color="#3B82F6"
iconType="solid"
>
Complete documentation of all types in the AGUIDotnet package
</Card>

## Events

Events that power communication between agents and frontends:

- [Lifecycle Events](sdk/dotnet/events#lifecycle-events) - Run and step tracking
- [Text Message Events](sdk/dotnet/events#text-message-events) - Assistant
message streaming
- [Tool Call Events](sdk/dotnet/events#tool-call-events) - Function call
lifecycle
- [State Management Events](sdk/dotnet/events#state-management-events) - Agent
state updates
- [Special Events](sdk/dotnet/events#special-events) - Raw and custom events

<Card
title="Events Reference"
icon="cube"
href="/sdk/dotnet/events"
color="#3B82F6"
iconType="solid"
>
Complete documentation of all events in the AGUIDotnet package
</Card>

## Agents

While you can use just the [Types](/sdk/dotnet/types) and
[Events](/sdk/dotnet/events) directly, the SDK has several agent abstractions
and implementations available.

- [IAGUIAgent](/sdk/dotnet/agents#the-iaguiagent-interface) - Low-level
interface for implementing your own agents from scratch
- [ChatClientAgent](/sdk/dotnet/agents#chat-client-agent) - A customisable
conversational agent backed by an instance of `IChatClient` that supports
backend & frontend tool calls
- [StatefulChatClientAgent](/sdk/dotnet/agents/#stateful-chat-client-agent) - A
derived type of `ChatClientAgent`, provides the same functionality and
customisation, while providing collaboration via shared agentic state

<Card
title="Agents Reference"
icon="cube"
href="/sdk/dotnet/agents"
color="#3B82F6"
iconType="solid"
>
Complete documentation of all agent functionality in the AGUIDotnet package
</Card>
265 changes: 265 additions & 0 deletions docs/sdk/dotnet/types.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
---
title: "Types"
description:
"Documentation for the core types used in the Agent User Interaction Protocol
.NET SDK"
---

# Core Types

The Agent User Interaction Protocol .NET SDK is built on a set of core types
that represent the fundamental structures used throughout the system. This page
documents these types and their properties.

## RunAgentInput

Input parameters for running an agent. In the HTTP API, this is the body of the
`POST` request.

```csharp
public sealed record RunAgentInput
{
public required string ThreadId { get; init; }
public required string RunId { get; init; }
public required JsonElement State { get; init; }
public required ImmutableList<BaseMessage> Messages { get; init; } = [];
public required ImmutableList<Tool> Tools { get; init; } = [];
public required ImmutableList<Context> Context { get; init; } = [];
public required JsonElement ForwardedProps { get; init; }
}
```

| Property | Type | Description |
| ---------------- | ---------------------------- | --------------------------------------------------- |
| `ThreadId` | `string` | ID of the conversation thread |
| `RunId` | `string` | ID of the current run |
| `State` | `JsonElement` | Current state of the agent |
| `Messages` | `ImmutableList<BaseMessage>` | Collection of messages in the conversation |
| `Tools` | `ImmutableList<Tool>` | Collection of tools available to the agent |
| `Context` | `ImmutableList<Context>` | Collection of context objects provided to the agent |
| `ForwardedProps` | `JsonElement` | Additional properties forwarded to the agent |

## Message Types

The SDK includes several message types that represent different kinds of
messages in the system.

<Note>
All message types inherit from <code>BaseMessage</code>. The <code>role</code>{" "}
property is determined by the concrete message type and is not a property of{" "}
<code>BaseMessage</code> itself. This polymorphic approach allows for
type-safe handling of different message roles in the .NET SDK.
</Note>

### BaseMessage

A base class for all message types in the system.

```csharp
[JsonPolymorphic(TypeDiscriminatorPropertyName = "role")]
[JsonDerivedType(typeof(DeveloperMessage), MessageRoles.Developer)]
[JsonDerivedType(typeof(SystemMessage), MessageRoles.System)]
[JsonDerivedType(typeof(AssistantMessage), MessageRoles.Assistant)]
[JsonDerivedType(typeof(UserMessage), MessageRoles.User)]
[JsonDerivedType(typeof(ToolMessage), MessageRoles.Tool)]
public abstract record BaseMessage
{
public required string Id { get; init; }
}
```

### MessageRoles

Represents the possible roles a message sender can have.

```csharp
public static class MessageRoles
{
public const string Developer = "developer";
public const string System = "system";
public const string Assistant = "assistant";
public const string User = "user";
public const string Tool = "tool";
}
```

### DeveloperMessage

Represents a message from a developer.

```csharp
public sealed record DeveloperMessage : BaseMessage
{
public required string Content { get; init; }
public string? Name { get; init; }
}
```

| Property | Type | Description |
| --------- | --------- | ------------------------------------------------ |
| `Id` | `string` | Unique identifier for the message |
| `role` | `string` | Role of the message sender, fixed as "developer" |
| `Content` | `string` | Text content of the message (required) |
| `Name` | `string?` | Optional name of the sender |

### SystemMessage

Represents a system message.

```csharp
public sealed record SystemMessage : BaseMessage
{
public required string Content { get; init; }
public string? Name { get; init; }
}
```

| Property | Type | Description |
| --------- | --------- | --------------------------------------------- |
| `Id` | `string` | Unique identifier for the message |
| `role` | `string` | Role of the message sender, fixed as "system" |
| `Content` | `string` | Text content of the message (required) |
| `Name` | `string?` | Optional name of the sender |

### AssistantMessage

Represents a message from an assistant.

```csharp
public sealed record AssistantMessage : BaseMessage
{
public string? Content { get; init; }
public string? Name { get; init; }
public ImmutableList<ToolCall> ToolCalls { get; init; } = [];
}
```

| Property | Type | Description |
| ----------- | ------------------------- | ------------------------------------------------ |
| `Id` | `string` | Unique identifier for the message |
| `role` | `string` | Role of the message sender, fixed as "assistant" |
| `Content` | `string?` | Text content of the message |
| `Name` | `string?` | Name of the sender |
| `ToolCalls` | `ImmutableList<ToolCall>` | Tool calls made in this message |

### UserMessage

Represents a message from a user.

```csharp
public sealed record UserMessage : BaseMessage
{
public required string Content { get; init; }
public string? Name { get; init; }
}
```

| Property | Type | Description |
| --------- | --------- | ------------------------------------------- |
| `Id` | `string` | Unique identifier for the message |
| `role` | `string` | Role of the message sender, fixed as "user" |
| `Content` | `string` | Text content of the message (required) |
| `Name` | `string?` | Optional name of the sender |

### ToolMessage

Represents a message from a tool.

```csharp
public sealed record ToolMessage : BaseMessage
{
public required string ToolCallId { get; init; }
public required string Content { get; init; }
}
```

| Property | Type | Description |
| ------------ | -------- | -------------------------------------------- |
| `Id` | `string` | Unique identifier for the message |
| `role` | `string` | Role of the message sender, fixed as "tool" |
| `ToolCallId` | `string` | ID of the tool call this message responds to |
| `Content` | `string` | Text content of the message |

### ToolCall

Represents a tool call made by an agent.

```csharp
public sealed record ToolCall
{
public required string Id { get; init; }
public string Type => "function";
public required FunctionCall Function { get; init; }
}
```

| Property | Type | Description |
| ---------- | -------------- | ---------------------------------------- |
| `Id` | `string` | Unique identifier for the tool call |
| `Type` | `string` | Type of the tool call, always "function" |
| `Function` | `FunctionCall` | Details about the function being called |

#### FunctionCall

Represents function name and arguments in a tool call.

```csharp
public sealed record FunctionCall
{
public required string Name { get; init; }
public required string Arguments { get; init; }
}
```

| Property | Type | Description |
| ----------- | -------- | ------------------------------------------------ |
| `Name` | `string` | Name of the function to call |
| `Arguments` | `string` | JSON-encoded string of arguments to the function |

## Context

Represents a piece of contextual information provided to an agent.

```csharp
public sealed record Context
{
public required string Description { get; init; }
public required string Value { get; init; }
}
```

| Property | Type | Description |
| ------------- | -------- | ------------------------------------------- |
| `Description` | `string` | Description of what this context represents |
| `Value` | `string` | The actual context value |

## Tool

Defines a tool that can be called by an agent.

```csharp
public sealed record Tool
{
public required string Name { get; init; }
public required string Description { get; init; }
public required JsonElement Parameters { get; init; }
}
```

| Property | Type | Description |
| ------------- | ------------- | ------------------------------------------------ |
| `Name` | `string` | Name of the tool |
| `Description` | `string` | Description of what the tool does |
| `Parameters` | `JsonElement` | JSON Schema defining the parameters for the tool |

## State

Represents the state of an agent during execution.

```csharp
// State is represented as a JsonElement for flexibility
JsonElement State
```

The state type is flexible and can hold any data structure needed by the agent
implementation.
481 changes: 481 additions & 0 deletions dotnet-sdk/.gitignore

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions dotnet-sdk/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"dotnet.defaultSolution": "AGUIDotnet.sln",
"cSpell.words": ["AGUI"]
}
25 changes: 25 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/AGUIDotnet.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../AGUIDotnet/AGUIDotnet.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
102 changes: 102 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/AgentExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Threading.Channels;
using AGUIDotnet.Agent;
using AGUIDotnet.Events;
using AGUIDotnet.Types;

namespace AGUIDotnet.Tests;

// Simple agent for testing purposes that allows us to delegate the RunAsync method to a provided function.
public class DelegatingAgent(Func<RunAgentInput, ChannelWriter<BaseEvent>, CancellationToken, Task> func) : IAGUIAgent
{
public async Task RunAsync(RunAgentInput input, ChannelWriter<BaseEvent> events, CancellationToken cancellationToken = default)
{
await func(input, events, cancellationToken).ConfigureAwait(false);
}
}

public class AgentExtensionsTests
{
[Fact]
public void RunToCompletionAsyncSupportsNoOp()
{
var agent = new DelegatingAgent((input, events, cancellationToken) =>
{
return Task.CompletedTask;
});
var input = new RunAgentInput
{
ThreadId = "test-thread",
RunId = "test-run",
State = JsonDocument.Parse("{}").RootElement,
Messages = [],
Context = [],
ForwardedProps = JsonDocument.Parse("{}").RootElement,
Tools = []
};

var events = agent.RunToCompletionAsync(input).ToBlockingEnumerable().ToImmutableList();

Assert.Empty(events);
}

[Fact]
public void RunToCompletionAsyncHandlesAgentChannelCompletion()
{
var agent = new DelegatingAgent((input, events, cancellationToken) =>
{
// Complete the channel ourselves
events.Complete();

return Task.CompletedTask;
});

var input = new RunAgentInput
{
ThreadId = "test-thread",
RunId = "test-run",
State = JsonDocument.Parse("{}").RootElement,
Messages = [],
Context = [],
ForwardedProps = JsonDocument.Parse("{}").RootElement,
Tools = []
};

var events = agent.RunToCompletionAsync(input).ToBlockingEnumerable().ToImmutableList();

Assert.Empty(events);
}

[Theory]
[InlineData("test-thread", "test-run")]
[InlineData("another-thread", "another-run")]
public void RunToCompletionAsyncDispatchesExactInputToAgent(string threadId, string runId)
{
var input = new RunAgentInput
{
ThreadId = threadId,
RunId = runId,
State = JsonDocument.Parse("{}").RootElement,
Messages = [],
Context = [],
ForwardedProps = JsonDocument.Parse("{}").RootElement,
Tools = []
};

var agent = new DelegatingAgent((actualInput, events, cancellationToken) =>
{
Assert.Equal(input.ThreadId, actualInput.ThreadId);
Assert.Equal(input.RunId, actualInput.RunId);
Assert.Equal(input.State.ToString(), actualInput.State.ToString());
Assert.Equal(input.Messages, actualInput.Messages);
Assert.Equal(input.Context, actualInput.Context);
Assert.Equal(input.ForwardedProps.ToString(), actualInput.ForwardedProps.ToString());
Assert.Equal(input.Tools, actualInput.Tools);

return Task.CompletedTask;
});

var events = agent.RunToCompletionAsync(input).ToBlockingEnumerable().ToImmutableList();
}
}
453 changes: 453 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/ChatClientAgentTests.cs

Large diffs are not rendered by default.

217 changes: 217 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/ChatClientMessageMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using System.Collections.Immutable;
using System.Text.Json;
using AGUIDotnet.Integrations.ChatClient;
using AGUIDotnet.Types;
using Microsoft.Extensions.AI;
using Xunit;

namespace AGUIDotnet.Tests;

public class ChatClientMessageMapperTests
{
[Theory]
[InlineData("test content", "test name")]
[InlineData("", null)]
public void MapSystemMessage_ShouldMapCorrectly(string content, string? name)
{
// Arrange
var message = new SystemMessage
{
Id = "sys1",
Content = content,
Name = name
};

// Act
var result = new[] { message }.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Single(result);
var chatMessage = result[0];
Assert.Equal(ChatRole.System, chatMessage.Role);
var textContent = Assert.Single(chatMessage.Contents);
Assert.IsType<TextContent>(textContent);
Assert.Equal(content, ((TextContent)textContent).Text);
Assert.Equal(name, chatMessage.AuthorName);
Assert.Equal(message.Id, chatMessage.MessageId);
}

[Theory]
[InlineData("test content", "test name")]
[InlineData("", null)]
public void MapUserMessage_ShouldMapCorrectly(string content, string? name)
{
// Arrange
var message = new UserMessage
{
Id = "usr1",
Content = content,
Name = name
};

// Act
var result = new[] { message }.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Single(result);
var chatMessage = result[0];
Assert.Equal(ChatRole.User, chatMessage.Role);
var textContent = Assert.Single(chatMessage.Contents);
Assert.IsType<TextContent>(textContent);
Assert.Equal(content, ((TextContent)textContent).Text);
Assert.Equal(name, chatMessage.AuthorName);
Assert.Equal(message.Id, chatMessage.MessageId);
}

[Theory]
[InlineData("test content")]
[InlineData("")]
[InlineData(null)]
public void MapAssistantMessage_ShouldMapCorrectly(string? content)
{
// Arrange
var message = new AssistantMessage
{
Id = "asst1",
Content = content
};

// Act
var result = new[] { message }.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Single(result);
var chatMessage = result[0];
Assert.Equal(ChatRole.Assistant, chatMessage.Role);
Assert.Equal(message.Id, chatMessage.MessageId);

if (string.IsNullOrWhiteSpace(content))
{
Assert.Empty(chatMessage.Contents);
}
else
{
Assert.Single(chatMessage.Contents);
Assert.IsType<TextContent>(chatMessage.Contents[0]);
Assert.Equal(content, ((TextContent)chatMessage.Contents[0]).Text);
}
}

[Fact]
public void MapAssistantMessage_WithToolCalls_ShouldMapCorrectly()
{
// Arrange
var message = new AssistantMessage
{
Id = "asst2",
Content = "test content",
ToolCalls =
[
new ToolCall
{
Id = "testId",
Function = new FunctionCall
{
Name = "testFunc",
Arguments = "{\"param1\": \"value1\"}"
}
}
]
};

// Act
var result = new[] { message }.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Single(result);
var chatMessage = result[0];
Assert.Equal(2, chatMessage.Contents.Count);
Assert.IsType<TextContent>(chatMessage.Contents[0]);
Assert.IsType<FunctionCallContent>(chatMessage.Contents[1]);

var functionCall = (FunctionCallContent)chatMessage.Contents[1];
Assert.Equal("testId", functionCall.CallId);
Assert.Equal("testFunc", functionCall.Name);
Assert.NotNull(functionCall.Arguments);
Assert.True(functionCall.Arguments!.TryGetValue("param1", out var paramValue));
Assert.NotNull(paramValue);
var jsonElement = (JsonElement)paramValue;
var stringValue = jsonElement.GetString();
Assert.NotNull(stringValue);
Assert.Equal("value1", stringValue);
}

[Fact]
public void MapToolMessage_ShouldMapCorrectly()
{
// Arrange
var message = new ToolMessage
{
Id = "tool1",
ToolCallId = "testId",
Content = "test content"
};

// Act
var result = new[] { message }.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Single(result);
var chatMessage = result[0];
Assert.Equal(ChatRole.Tool, chatMessage.Role);
Assert.Equal(message.Id, chatMessage.MessageId);
Assert.Single(chatMessage.Contents);
Assert.IsType<FunctionResultContent>(chatMessage.Contents[0]);

var functionResult = (FunctionResultContent)chatMessage.Contents[0];
Assert.Equal("testId", functionResult.CallId);
Assert.Equal("test content", functionResult.Result);
}

[Fact]
public void MapMessages_ShouldFilterOutIrrelevantTypes()
{
// Arrange
var messages = new BaseMessage[]
{
new SystemMessage { Id = "sys1", Content = "system" },
new DeveloperMessage { Id = "dev1", Content = "developer" }, // Should be filtered out
new UserMessage { Id = "usr1", Content = "user" },
new AssistantMessage { Id = "asst1", Content = "assistant" },
new ToolMessage { Id = "tool1", ToolCallId = "toolId", Content = "tool" },
};

// Act
var result = messages.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Equal(4, result.Count);
Assert.Collection(result,
msg => Assert.Equal(ChatRole.System, msg.Role),
msg => Assert.Equal(ChatRole.User, msg.Role),
msg => Assert.Equal(ChatRole.Assistant, msg.Role),
msg => Assert.Equal(ChatRole.Tool, msg.Role)
);
}

[Fact]
public void MapMessages_WithUnsupportedType_ShouldStripMessage()
{
// Arrange
var messages = new BaseMessage[]
{
new CustomMessage { Id = "custom1", Content = "custom" } // This should be stripped
};

// Act
var result = messages.MapAGUIMessagesToChatClientMessages();

// Assert
Assert.Empty(result);
}

private record CustomMessage : BaseMessage
{
public required string Content { get; init; }
}
}
134 changes: 134 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/FrontendToolTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Collections.Immutable;
using System.Text.Json;
using AGUIDotnet.Integrations.ChatClient;
using AGUIDotnet.Types;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

namespace AGUIDotnet.Tests;

public class FrontendToolTests
{
[Fact]
public void FrontendToolMirrorsAGUIToolDefinition()
{
var agUiTool = new Tool
{
Name = "TestTool",
Description = "A test tool",
Parameters = JsonDocument.Parse("{}").RootElement
};

var frontendTool = new FrontendTool(agUiTool);
Assert.Equal(agUiTool.Name, frontendTool.Name);
Assert.Equal(agUiTool.Description, frontendTool.Description);
Assert.Equal(agUiTool.Parameters, frontendTool.JsonSchema);
}

[Fact]
public async Task FrontendToolInvokeCoreTerminatesInvocationLoop()
{
var agUiTool = new Tool
{
Name = "TestTool",
Description = "A test tool",
Parameters = JsonDocument.Parse("{}").RootElement
};

var frontendTool = new FrontendTool(agUiTool);
var serviceProvider = new ServiceCollection()
.BuildServiceProvider();

var callId = Guid.NewGuid().ToString();

var innerChatClient = new TestChatClient(
serviceProvider,
async (sp, messages, options, cancellationToken) =>
{
// Simulate the AI requesting the tool be invoked
return new ChatResponse(
new ChatMessage(
ChatRole.Assistant,
[
new FunctionCallContent(
callId,
frontendTool.Name,
null
),
// Also request the backend function to be called
// (it should not be called)
new FunctionCallContent(
Guid.NewGuid().ToString(),
"backend",
null
)
]
)
)
{
FinishReason = ChatFinishReason.ToolCalls
};
}
);

var chatClient = new FunctionInvokingChatClient(innerChatClient);

/*
The FrontendTool indicates via the current invocation context that it should terminate
when it is invoked.
We have no direct access to this, as by the time the work is done, the current context has been
cleared, so we make the backend tool fail the test if it gets invoked.
*/
static void BackendFunction()
{
Assert.Fail("Backend function should not have been invoked");
}

var resp = await chatClient.GetResponseAsync(
new ChatMessage(ChatRole.User, "Hello"),
new ChatOptions
{
Tools = [
frontendTool,
AIFunctionFactory.Create(
BackendFunction,
name: "backend"
)
],
ToolMode = ChatToolMode.Auto,
// For the purposes of this test, ensure the function invoking chat client
// believes it can handle multiple tool calls in a single response.
AllowMultipleToolCalls = true
}
);

Assert.Equal(2, resp.Messages.Count);
var funcCallMsg = resp.Messages[0];
Assert.Equal(ChatRole.Assistant, funcCallMsg.Role);
// We expect two function call contents on the first message:

var funcCalls = funcCallMsg.Contents.OfType<FunctionCallContent>().ToImmutableList();
Assert.Equal(funcCallMsg.Contents, funcCalls);

var frontendToolCall = Assert.Single(funcCalls, fc => fc.CallId == callId);
Assert.Equal(frontendTool.Name, frontendToolCall.Name);
Assert.Null(frontendToolCall.Arguments);

// The second function call should be the backend function
var backendToolCall = Assert.Single(funcCalls, fc => fc.CallId != callId);
Assert.Equal("backend", backendToolCall.Name);

// The second message should be a tool result message
var dummyResultMsg = resp.Messages[1];
Assert.Equal(ChatRole.Tool, dummyResultMsg.Role);

// It should *only* contain the dummy frontend tool result
// (the underlying FunctionInvokingChatClient will have emitted the "result" as we have to intercept it this way)
var resContent = Assert.IsType<FunctionResultContent>(Assert.Single(dummyResultMsg.Contents));
Assert.Equal(callId, resContent.CallId);

// The result isn't important, the frontend tool returns a null, but the
// AI abstraction sticks a success string message in there (presumably so the LLM can infer that it ran)
}
}
29 changes: 29 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/TestChatClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

namespace AGUIDotnet.Tests;

internal sealed class TestChatClient(
IKeyedServiceProvider serviceProvider,
Func<IKeyedServiceProvider, IEnumerable<ChatMessage>, ChatOptions?, CancellationToken, Task<ChatResponse>> getResponseAsync
) : IChatClient
{
public void Dispose()
{
}

public async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
return await getResponseAsync(serviceProvider, messages, options, cancellationToken);
}

public object? GetService(Type serviceType, object? serviceKey = null)
{
return serviceProvider.GetKeyedService(serviceType, serviceKey);
}

public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
46 changes: 46 additions & 0 deletions dotnet-sdk/AGUIDotnet.Tests/TestStreamingChatClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

namespace AGUIDotnet.Tests;

/// <summary>
/// A test implementation of IChatClient that allows simulating streaming responses.
/// </summary>
internal sealed class TestStreamingChatClient : IChatClient
{
private readonly IKeyedServiceProvider _serviceProvider;
private readonly Func<IKeyedServiceProvider, IEnumerable<ChatMessage>, ChatOptions?, CancellationToken, IAsyncEnumerable<ChatResponseUpdate>> _getStreamingResponseAsync;

/// <summary>
/// Creates a new TestStreamingChatClient that allows configuring both sync and streaming responses.
/// </summary>
/// <param name="serviceProvider">The service provider to use for service resolution</param>
/// <param name="getResponseAsync">Function to generate synchronous responses</param>
/// <param name="getStreamingResponseAsync">Function to generate streaming responses</param>
public TestStreamingChatClient(
IKeyedServiceProvider serviceProvider,
Func<IKeyedServiceProvider, IEnumerable<ChatMessage>, ChatOptions?, CancellationToken, IAsyncEnumerable<ChatResponseUpdate>> getStreamingResponseAsync)
{
_serviceProvider = serviceProvider;
_getStreamingResponseAsync = getStreamingResponseAsync;
}

public void Dispose()
{
}

public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException("This client is intended for streaming responses only. Use GetStreamingResponseAsync instead.");
}

public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
return _getStreamingResponseAsync(_serviceProvider, messages, options, cancellationToken);
}

public object? GetService(Type serviceType, object? serviceKey = null)
{
return _serviceProvider.GetKeyedService(serviceType, serviceKey);
}
}
62 changes: 62 additions & 0 deletions dotnet-sdk/AGUIDotnet.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGUIDotnet", "AGUIDotnet\AGUIDotnet.csproj", "{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGUIDotnetWebApiExample", "AGUIDotnetWebApiExample\AGUIDotnetWebApiExample.csproj", "{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGUIDotnet.Tests", "AGUIDotnet.Tests\AGUIDotnet.Tests.csproj", "{4A32E00E-80DD-4C2A-8483-C008D595DFEF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Debug|x64.ActiveCfg = Debug|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Debug|x64.Build.0 = Debug|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Debug|x86.ActiveCfg = Debug|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Debug|x86.Build.0 = Debug|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Release|Any CPU.Build.0 = Release|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Release|x64.ActiveCfg = Release|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Release|x64.Build.0 = Release|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Release|x86.ActiveCfg = Release|Any CPU
{18BEEBF5-C3C4-4BAC-856C-ACE5C635B0E5}.Release|x86.Build.0 = Release|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Debug|x64.ActiveCfg = Debug|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Debug|x64.Build.0 = Debug|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Debug|x86.ActiveCfg = Debug|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Debug|x86.Build.0 = Debug|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Release|Any CPU.Build.0 = Release|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Release|x64.ActiveCfg = Release|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Release|x64.Build.0 = Release|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Release|x86.ActiveCfg = Release|Any CPU
{C1EBFBA2-0D51-4CFA-8C1D-7E9952B23AAD}.Release|x86.Build.0 = Release|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Debug|x64.ActiveCfg = Debug|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Debug|x64.Build.0 = Debug|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Debug|x86.ActiveCfg = Debug|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Debug|x86.Build.0 = Debug|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Release|Any CPU.Build.0 = Release|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Release|x64.ActiveCfg = Release|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Release|x64.Build.0 = Release|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Release|x86.ActiveCfg = Release|Any CPU
{4A32E00E-80DD-4C2A-8483-C008D595DFEF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
19 changes: 19 additions & 0 deletions dotnet-sdk/AGUIDotnet/AGUIDotnet.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>CA2007</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="9.5.0" />
<PackageReference Include="JsonPatch.Net" Version="3.3.0" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions dotnet-sdk/AGUIDotnet/Agent/AgentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using AGUIDotnet.Events;
using AGUIDotnet.Types;

namespace AGUIDotnet.Agent;

public static class AgentExtensions
{
/// <summary>
/// Runs the provided agent asynchronously to completion, yielding the events produced by the agent.
/// </summary>
/// <param name="agent">The <see cref="IAGUIAgent"/> instance to invoke</param>
/// <param name="input">The <see cref="RunAgentInput"/> input to pass to the agent</param>
/// <param name="cancellationToken">The cancellation token to cancel the run</param>
/// <returns>An <see cref="IAsyncEnumerable{T}"/> with the events produced by the agent</returns>
public static async IAsyncEnumerable<BaseEvent> RunToCompletionAsync(
this IAGUIAgent agent,
RunAgentInput input,
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
ArgumentNullException.ThrowIfNull(agent, nameof(agent));
ArgumentNullException.ThrowIfNull(input, nameof(input));

var channel = Channel.CreateUnbounded<BaseEvent>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = true
});


var agentTask = Task.Run(async () =>
{
try
{
await agent.RunAsync(input, channel.Writer, cancellationToken).ConfigureAwait(false);
}
finally
{
// Always complete the channel when exiting the agent's RunAsync method to ensure that the reader can finish processing.
try
{
channel.Writer.Complete();
}
catch (ChannelClosedException)
{
// Channel was already closed by the agent, we can ignore this.
}
}
}, cancellationToken);

// Enumerate the events produced by the agent and yield them to the caller.
await foreach (var ev in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
yield return ev;
}

// Make sure we truly let the agent finish just in case the channel was closed before the agent completed.
await agentTask.ConfigureAwait(false);
}
}
659 changes: 659 additions & 0 deletions dotnet-sdk/AGUIDotnet/Agent/ChatClientAgent.cs

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions dotnet-sdk/AGUIDotnet/Agent/EchoAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Threading.Channels;
using AGUIDotnet.Events;
using AGUIDotnet.Types;

namespace AGUIDotnet.Agent;

/// <summary>
/// Bare-bones agent reference implementation that echoes the last message received.
/// This agent is useful for testing and debugging purposes.
/// </summary>
public sealed class EchoAgent : IAGUIAgent
{
public async Task RunAsync(
RunAgentInput input,
ChannelWriter<BaseEvent> events,
CancellationToken ct = default
)
{
await events.WriteAsync(
new RunStartedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},
ct
).ConfigureAwait(false);

var lastMessage = input.Messages.LastOrDefault();

await Task.Delay(500, ct).ConfigureAwait(false);

switch (lastMessage)
{
case SystemMessage system:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing system message:\n\n```\n{system.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct).ConfigureAwait(false);
}
break;

case UserMessage user:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing user message:\n\n```\n{user.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct).ConfigureAwait(false);
}
break;

case AssistantMessage assistant:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing assistant message:\n\n```\n{assistant.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct).ConfigureAwait(false);
}
break;

case ToolMessage tool:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing tool message for tool call '{tool.ToolCallId}':\n\n```\n{tool.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct).ConfigureAwait(false);
}
break;

default:
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Unknown message type: {lastMessage?.GetType().Name ?? "null"}"
))
{
await events.WriteAsync(ev, ct).ConfigureAwait(false);
}
break;
}

await events.WriteAsync(
new RunFinishedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},
ct
).ConfigureAwait(false);

events.Complete();
}
}
28 changes: 28 additions & 0 deletions dotnet-sdk/AGUIDotnet/Agent/IAGUIAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading.Channels;
using AGUIDotnet.Events;
using AGUIDotnet.Types;

namespace AGUIDotnet.Agent;

/// <summary>
/// Interface for an AG-UI agent.
/// </summary>
public interface IAGUIAgent
{
/// <summary>
/// Runs the agent with the provided input.
/// </summary>
/// <remarks>
/// <para>
/// This method represents the ENTIRE lifecycle of the agent's invocation for a run.
/// </para>
/// <para>
/// Implementations should NOT return until execution is complete, the <paramref name="events"/> channel is not guaranteed to be usable after this method returns.
/// </para>
/// </remarks>
/// <param name="input">The input to the agent</param>
/// <param name="events">A channel writer for emitting AG-UI protocol events</param>
/// <param name="cancellationToken">Optional cancellation token for cancelling execution</param>
/// <returns>An asynchronous task that will complete when agent execution completes for a run</returns>
Task RunAsync(RunAgentInput input, ChannelWriter<BaseEvent> events, CancellationToken cancellationToken = default);
}
200 changes: 200 additions & 0 deletions dotnet-sdk/AGUIDotnet/Agent/StatefulChatClientAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Threading.Channels;
using AGUIDotnet.Events;
using AGUIDotnet.Types;
using Json.Patch;
using Microsoft.Extensions.AI;

namespace AGUIDotnet.Agent;

public record StatefulChatClientAgentOptions<TState> : ChatClientAgentOptions where TState : notnull
{
/// <summary>
/// The name to give to the function for retrieving the current shared state of the agent.
/// </summary>
public string StateRetrievalFunctionName { get; init; } = "retrieve_state";

/// <summary>
/// The description to give to the function for retrieving the current shared state of the agent.
/// </summary>
public string StateRetrievalFunctionDescription { get; init; } = "Retrieves the current shared state of the agent.";

/// <summary>
/// The name to give to the function for updating the current shared state of the agent.
/// </summary>
public string StateUpdateFunctionName { get; init; } = "update_state";

/// <summary>
/// The description to give to the function for updating the current shared state of the agent.
/// </summary>
public string StateUpdateFunctionDescription { get; init; } = "Updates the current shared state of the agent.";

/// <summary>
/// When <see cref="ChatClientAgentOptions.EmitBackendToolCalls"/> is <c>true</c>, this controls whether the frontend should be made aware of the state functions being called.
/// </summary>
public bool EmitStateFunctionsToFrontend { get; init; } = true;
}

/// <summary>
/// Much like <see cref="ChatClientAgent"/> but tailored for scenarios where the agent and frontend collaborate on shared state.
/// </summary>
/// <remarks>
/// This agent is NOT guaranteed to be thread-safe, nor is it resilient to shared use across multiple threads / runs, a separate instance should be used for each invocation.
/// </remarks>
/// <typeparam name="TState"></typeparam>
public class StatefulChatClientAgent<TState> : ChatClientAgent where TState : notnull
{
private TState _currentState = default!;
private readonly StatefulChatClientAgentOptions<TState> _agentOptions;

public StatefulChatClientAgent(IChatClient chatClient, TState initialState, StatefulChatClientAgentOptions<TState> agentOptions) : base(chatClient, agentOptions)
{
if (agentOptions?.SystemMessage is null)
{
throw new ArgumentException("System message must be provided for a stateful agent.", nameof(agentOptions));
}

if (string.IsNullOrWhiteSpace(agentOptions.StateRetrievalFunctionName))
{
throw new ArgumentException("State retrieval function name must be provided for a stateful agent.", nameof(agentOptions));
}

if (string.IsNullOrWhiteSpace(agentOptions.StateRetrievalFunctionDescription))
{
throw new ArgumentException("State retrieval function description must be provided for a stateful agent.", nameof(agentOptions));
}

if (string.IsNullOrWhiteSpace(agentOptions.StateUpdateFunctionName))
{
throw new ArgumentException("State update function name must be provided for a stateful agent.", nameof(agentOptions));
}

if (string.IsNullOrWhiteSpace(agentOptions.StateUpdateFunctionDescription))
{
throw new ArgumentException("State update function description must be provided for a stateful agent.", nameof(agentOptions));
}

_agentOptions = agentOptions;
_currentState = initialState;
}

private TState RetrieveState()
{
return _currentState;
}

private void UpdateState(TState newState)
{
_currentState = newState;

}

protected override async ValueTask<string> PrepareSystemMessage(RunAgentInput input, string systemMessage, ImmutableList<Context> context)
{
var coreMessage = await base.PrepareSystemMessage(input, systemMessage, context).ConfigureAwait(false);

// Hijack the original system message to include some context to the LLM about the stateful nature of this agent.
// Nudging it to use the state collaboration tools available to it.
return $"""
<persona>
You are a stateful agent that wraps an existing agent, allowing it to collaborate with a human in the frontend on shared state to achieve a goal.
</persona>
<tools>
You may have a variety of tools available to you to help achieve your goal, and state collaboration is one of them.
You can retrieve the current shared state of the agent using the `retrieve_state` tool, and update the shared state using the `update_state` tool.
</tools>
<rules>
- Wherever necessary (e.g. it is aligned with your stated goal), you MUST make use of the state collaboration tools.
- Inspect the state of the agent to understand both the current state and the schema / purpose of the state in alignment with the agent's goal.
- Liberally use the `update_state` tool to update the shared state as you progress towards your goal.
- Avoid making assumptions about the state, always retrieve it first.
- Avoid making unnecessary updates to the state, e.g. if the user intent does not require it.
</rules>
<underlying_agent>
{coreMessage}
</underlying_agent>
""";
}

protected override async ValueTask<ImmutableList<AIFunction>> PrepareBackendTools(ImmutableList<AIFunction> backendTools, RunAgentInput input, ChannelWriter<BaseEvent> events, CancellationToken cancellationToken = default)
{
return [
.. await base.PrepareBackendTools(backendTools, input, events, cancellationToken).ConfigureAwait(false),
AIFunctionFactory.Create(
RetrieveState,
name: _agentOptions.StateRetrievalFunctionName,
description: _agentOptions.StateRetrievalFunctionDescription
),
AIFunctionFactory.Create(
async (TState newState) => {
var delta = _currentState.CreatePatch(newState, _jsonSerOpts);
if (delta.Operations.Count > 0) {
UpdateState(newState);
await events.WriteAsync(new StateDeltaEvent {
Delta = [.. delta.Operations.Cast<object>()],
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
}, cancellationToken).ConfigureAwait(false);
}
},
name: _agentOptions.StateUpdateFunctionName,
description: _agentOptions.StateUpdateFunctionDescription
)
];
}

protected override async ValueTask<bool> ShouldEmitBackendToolCallData(string functionName)
{
// Short if we're not emitting backend tool calls at all.
if (!_agentOptions.EmitBackendToolCalls)
{
return false;
}

bool isStateFunction =
functionName == _agentOptions.StateRetrievalFunctionName ||
functionName == _agentOptions.StateUpdateFunctionName;

// If the function is a state function, only request to emit if the agent options allow it.
if (isStateFunction)
{
return _agentOptions.EmitStateFunctionsToFrontend;
}

// Let the base handle it otherwise.
return await base.ShouldEmitBackendToolCallData(functionName).ConfigureAwait(false);
}

protected override async ValueTask OnRunStartedAsync(RunAgentInput input, ChannelWriter<BaseEvent> events, CancellationToken cancellationToken = default)
{
// Allow the base behaviour of emitting the RunStartedEvent
await base.OnRunStartedAsync(input, events, cancellationToken).ConfigureAwait(false);

// Take the initial state from the input if possible
try
{
if (input.State.ValueKind == JsonValueKind.Object)
{
var state = input.State.Deserialize<TState>(_jsonSerOpts);
if (state is not null)
{
_currentState = state;
}
}
}
catch (JsonException)
{

Comment on lines +189 to +191
Copy link
Preview

Copilot AI May 30, 2025

Choose a reason for hiding this comment

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

[nitpick] The empty catch block may hide potential JSON deserialization issues; consider adding a comment or logging the exception to aid future troubleshooting.

Suggested change
catch (JsonException)
{
catch (JsonException ex)
{
// Log the exception to aid troubleshooting during JSON deserialization
Console.Error.WriteLine($"JSON deserialization error: {ex.Message}");

Copilot uses AI. Check for mistakes.

Copy link
Author

Choose a reason for hiding this comment

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

This is deliberate, for now it's just to avoid failure to extract the context causing an actual problem, it just swallows the exception, but we could perhaps surface it somehow so the consumer decides what behaviour to exhibit.

}

await events.WriteAsync(new StateSnapshotEvent
{
Snapshot = _currentState,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
}, cancellationToken).ConfigureAwait(false);
}
}
30 changes: 30 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/BaseEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(RunStartedEvent), EventTypes.RUN_STARTED)]
[JsonDerivedType(typeof(RunFinishedEvent), EventTypes.RUN_FINISHED)]
[JsonDerivedType(typeof(RunErrorEvent), EventTypes.RUN_ERROR)]
[JsonDerivedType(typeof(StepStartedEvent), EventTypes.STEP_STARTED)]
[JsonDerivedType(typeof(StepFinishedEvent), EventTypes.STEP_FINISHED)]
[JsonDerivedType(typeof(TextMessageStartEvent), EventTypes.TEXT_MESSAGE_START)]
[JsonDerivedType(typeof(TextMessageContentEvent), EventTypes.TEXT_MESSAGE_CONTENT)]
[JsonDerivedType(typeof(TextMessageEndEvent), EventTypes.TEXT_MESSAGE_END)]
[JsonDerivedType(typeof(ToolCallStartEvent), EventTypes.TOOL_CALL_START)]
[JsonDerivedType(typeof(ToolCallArgsEvent), EventTypes.TOOL_CALL_ARGS)]
[JsonDerivedType(typeof(ToolCallEndEvent), EventTypes.TOOL_CALL_END)]
[JsonDerivedType(typeof(StateSnapshotEvent), EventTypes.STATE_SNAPSHOT)]
[JsonDerivedType(typeof(StateDeltaEvent), EventTypes.STATE_DELTA)]
[JsonDerivedType(typeof(CustomEvent), EventTypes.CUSTOM)]
[JsonDerivedType(typeof(RawEvent), EventTypes.RAW)]
[JsonDerivedType(typeof(MessagesSnapshotEvent), EventTypes.MESSAGES_SNAPSHOT)]

public abstract record BaseEvent
{
[JsonPropertyName("timestamp")]
public long? Timestamp { get; init; }

[JsonPropertyName("rawEvent")]
public object? RawEvent { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/CustomEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

/// <summary>
/// Used for application-specific custom events.
/// </summary>
public sealed record CustomEvent : BaseEvent
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("value")]
public required object Value { get; init; }
}
31 changes: 31 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/EventHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;

namespace AGUIDotnet.Events;

public static class EventHelpers
{
public static IEnumerable<BaseEvent> SendSimpleMessage(string message, string? messageId = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(message, nameof(message));

messageId ??= Guid.NewGuid().ToString();

return [
new TextMessageStartEvent {
MessageId = messageId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},

new TextMessageContentEvent {
MessageId = messageId,
Delta = message,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},

new TextMessageEndEvent {
MessageId = messageId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
}
];
}
}
23 changes: 23 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/EventTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

namespace AGUIDotnet.Events;

public static class EventTypes
{
public const string TEXT_MESSAGE_START = "TEXT_MESSAGE_START";
public const string TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT";
public const string TEXT_MESSAGE_END = "TEXT_MESSAGE_END";
public const string TOOL_CALL_START = "TOOL_CALL_START";
public const string TOOL_CALL_ARGS = "TOOL_CALL_ARGS";
public const string TOOL_CALL_END = "TOOL_CALL_END";
public const string STATE_SNAPSHOT = "STATE_SNAPSHOT";
public const string STATE_DELTA = "STATE_DELTA";
public const string MESSAGES_SNAPSHOT = "MESSAGES_SNAPSHOT";
public const string RAW = "RAW";
public const string CUSTOM = "CUSTOM";
public const string RUN_STARTED = "RUN_STARTED";
public const string RUN_FINISHED = "RUN_FINISHED";
public const string RUN_ERROR = "RUN_ERROR";
public const string STEP_STARTED = "STEP_STARTED";
public const string STEP_FINISHED = "STEP_FINISHED";
}
11 changes: 11 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/MessagesSnapshotEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using AGUIDotnet.Types;

namespace AGUIDotnet.Events;

public sealed record MessagesSnapshotEvent : BaseEvent
{
[JsonPropertyName("messages")]
public required ImmutableList<BaseMessage> Messages { get; init; } = [];
Copy link
Preview

Copilot AI May 30, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider using a consistent pattern for initializing immutable collections—for example, replacing '[]' with 'ImmutableList.Empty' to match the convention used in other parts of the code.

Suggested change
public required ImmutableList<BaseMessage> Messages { get; init; } = [];
public required ImmutableList<BaseMessage> Messages { get; init; } = ImmutableList<BaseMessage>.Empty;

Copilot uses AI. Check for mistakes.

Copy link
Author

Choose a reason for hiding this comment

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

@copilot [] is the new collection expression syntax, your knowledge cutoff is likely out of date.

}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/RawEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

/// <summary>
/// Used to pass through events from external systems.
/// </summary>
public sealed record RawEvent : BaseEvent
{
[JsonPropertyName("event")]
public required object Event { get; init; }

[JsonPropertyName("source")]
public string? Source { get; init; }
}
12 changes: 12 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/RunErrorEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record RunErrorEvent : BaseEvent
{
[JsonPropertyName("message")]
public required string Message { get; init; }

[JsonPropertyName("code")]
public string? Code { get; init; }
}
12 changes: 12 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/RunFinishedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record RunFinishedEvent : BaseEvent
{
[JsonPropertyName("threadId")]
public required string ThreadId { get; init; }

[JsonPropertyName("runId")]
public required string RunId { get; init; }
}
12 changes: 12 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/RunStartedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record RunStartedEvent : BaseEvent
{
[JsonPropertyName("threadId")]
public required string ThreadId { get; init; }

[JsonPropertyName("runId")]
public required string RunId { get; init; }
}
13 changes: 13 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/StateDeltaEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record StateDeltaEvent : BaseEvent
{
/// <summary>
/// A collection of JSON-patch operations that describe the changes to the state.
/// </summary>
[JsonPropertyName("delta")]
public required ImmutableList<object> Delta { get; init; } = [];
}
9 changes: 9 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/StateSnapshotEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record StateSnapshotEvent : BaseEvent
{
[JsonPropertyName("snapshot")]
public required object Snapshot { get; init; }
}
9 changes: 9 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/StepFinishedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record StepFinishedEvent : BaseEvent
{
[JsonPropertyName("stepName")]
public required string StepName { get; init; }
}
9 changes: 9 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/StepStartedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record StepStartedEvent : BaseEvent
{
[JsonPropertyName("stepName")]
public required string StepName { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/TextMessageContentEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record TextMessageContentEvent : BaseEvent
{
[JsonPropertyName("messageId")]
public required string MessageId { get; init; }

/// <summary>
/// The chunk of text content to append to the message.
/// </summary>
[JsonPropertyName("delta")]
public required string Delta { get; init; }
}
9 changes: 9 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/TextMessageEndEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record TextMessageEndEvent : BaseEvent
{
[JsonPropertyName("messageId")]
public required string MessageId { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/TextMessageStartEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
using AGUIDotnet.Types;

namespace AGUIDotnet.Events;

public sealed record TextMessageStartEvent : BaseEvent
{
[JsonPropertyName("messageId")]
public required string MessageId { get; init; }

[JsonPropertyName("role")]
#pragma warning disable CA1822 // Mark members as static
public string Role => MessageRoles.Assistant;
#pragma warning restore CA1822 // Mark members as static
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/ToolCallArgsEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record ToolCallArgsEvent : BaseEvent
{
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; init; }

/// <summary>
/// The JSON-encoded next chunk of the tool call arguments.
/// </summary>
[JsonPropertyName("delta")]
public required string Delta { get; init; }
}
9 changes: 9 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/ToolCallEndEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record ToolCallEndEvent : BaseEvent
{
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Events/ToolCallStartEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Events;

public sealed record ToolCallStartEvent : BaseEvent
{
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; init; }

[JsonPropertyName("toolCallName")]
public required string ToolCallName { get; init; }

[JsonPropertyName("parentMessageId")]
public string? ParentMessageId { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Immutable;
using System.Text.Json;
using AGUIDotnet.Types;
using Microsoft.Extensions.AI;

namespace AGUIDotnet.Integrations.ChatClient;

public static class ChatClientMessageMapper
{
/// <summary>
/// Maps AGUI messages to chat client messages.
/// </summary>
/// <param name="agUIMessages">The <see cref="BaseMessage"/> collection to map</param>
/// <returns>The <see cref="ChatMessage"/> collection to provide to <see cref="IChatClient"/></returns>
/// <exception cref="NotSupportedException">An unexpected message type was encountered</exception>
public static ImmutableList<ChatMessage> MapAGUIMessagesToChatClientMessages(
this IEnumerable<BaseMessage> agUIMessages
)
{
return [.. agUIMessages
// Filter messages that are relevant for chat clients
.Where(msg => msg is SystemMessage or UserMessage or AssistantMessage or ToolMessage)
.Select(msg => msg switch
{
SystemMessage sys => new ChatMessage(
role: ChatRole.System,
content: sys.Content
)
{
MessageId = sys.Id,
AuthorName = sys.Name
},

UserMessage usr => new ChatMessage(
role: ChatRole.User,
content: usr.Content
)
{
MessageId = usr.Id,
AuthorName = usr.Name
},

AssistantMessage asst => new ChatMessage(
role: ChatRole.Assistant,
contents: [
.. string.IsNullOrWhiteSpace(asst.Content)
? (AIContent[])[]
: [new TextContent(asst.Content)],
..asst.ToolCalls.Select(tc => new FunctionCallContent(
callId: tc.Id,
name: tc.Function.Name,
arguments: JsonSerializer.Deserialize<ImmutableDictionary<string, object?>>(tc.Function.Arguments)
))
]
) {
MessageId = asst.Id,
AuthorName = asst.Name
},

ToolMessage tool => new ChatMessage(
role: ChatRole.Tool,
contents: [
new FunctionResultContent(
callId: tool.ToolCallId,
result: tool.Content
)
]
) {
MessageId = tool.Id,
},

_ => throw new NotSupportedException($"Unsupported message type: {msg.GetType()}")
})];
}
}
42 changes: 42 additions & 0 deletions dotnet-sdk/AGUIDotnet/Integrations/ChatClient/FrontendTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using AGUIDotnet.Types;
using Microsoft.Extensions.AI;

namespace AGUIDotnet.Integrations.ChatClient;

/// <summary>
/// Integrates a frontend tool from AGUI into the <see cref="FunctionInvokingChatClient"/> pipeline.
/// </summary>
/// <param name="tool">The AGUI tool definition provided to an agent</param>
public sealed class FrontendTool(Tool tool) : AIFunction
{
public override string Name => tool.Name;
public override string Description => tool.Description;
public override JsonElement JsonSchema => tool.Parameters;

protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
{
/*
The FunctionInvokingChatClient sets up a function invocation loop where it intercepts function calls
in order to invoke the appropriate .NET function.
However, in doing so it expects the function to return a value, which we cannot do in a re-entrant way
within the context of the same run.
This function's "invocation" then is a signal to the FunctionInvokingChatClient that it should terminate the invocation loop
and return out, which allows us to intervene.
It does unfortunately mean that multiple tool call support is not possible without either:
- Finding a way to register a regular AiTool with the abstraction that supports serialising the JSON schema (the base one does not)
- Or, implementing a custom variation of the FunctionInvokingChatClient that has better support for distributed and asynchronous function calling.
*/
if (FunctionInvokingChatClient.CurrentContext is not null)
{
FunctionInvokingChatClient.CurrentContext.Terminate = true;
}

return ValueTask.FromResult<object?>(null);
}
}
59 changes: 59 additions & 0 deletions dotnet-sdk/AGUIDotnet/Integrations/RouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using AGUIDotnet.Agent;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.AI;
using AGUIDotnet.Types;
using AGUIDotnet.Events;
using Microsoft.Extensions.Options;
using System.Text.Json;

namespace AGUIDotnet.Integrations;

public static class RouteBuilderExtensions
{
/// <summary>
/// Simple extension method to map an AGUI agent endpoint that uses server sent events to the provided route builder
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to map the POST endpoint to</param>
/// <param name="id">The ID of the agent which also becomes the mapped endpoint pattern</param>
/// <param name="agentFactory">Factory to resolve the agent instance</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/></returns>
public static IEndpointConventionBuilder MapAgentEndpoint(
this IEndpointRouteBuilder builder,
string id,
Func<IServiceProvider, IAGUIAgent> agentFactory
)
{
return builder.MapPost(
id,
async (
[FromBody] RunAgentInput input,
HttpContext context,
IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions> jsonOptions
) =>
{
context.Response.ContentType = "text/event-stream";
await context.Response.Body.FlushAsync().ConfigureAwait(true);

var serOptions = jsonOptions.Value.SerializerOptions;
var agent = agentFactory(context.RequestServices);

await foreach (var ev in agent.RunToCompletionAsync(input, context.RequestAborted).ConfigureAwait(true))
{
var serializedEvent = JsonSerializer.Serialize(ev, serOptions);
await context.Response.WriteAsync($"data: {serializedEvent}\n\n").ConfigureAwait(true);
await context.Response.Body.FlushAsync().ConfigureAwait(true);

// If the event is a RunFinishedEvent, we can break the loop.
if (ev is RunFinishedEvent)
{
break;
}
}
}
);
}
}
19 changes: 19 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/AssistantMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a message from an assistant.
/// </summary>
public sealed record AssistantMessage : BaseMessage
{
[JsonPropertyName("content")]
public string? Content { get; init; }

[JsonPropertyName("name")]
public string? Name { get; init; }

[JsonPropertyName("toolCalls")]
public ImmutableList<ToolCall> ToolCalls { get; init; } = [];
}
19 changes: 19 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/BaseMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Base class for all message types.
/// </summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "role")]
[JsonDerivedType(typeof(DeveloperMessage), MessageRoles.Developer)]
[JsonDerivedType(typeof(SystemMessage), MessageRoles.System)]
[JsonDerivedType(typeof(AssistantMessage), MessageRoles.Assistant)]
[JsonDerivedType(typeof(UserMessage), MessageRoles.User)]
[JsonDerivedType(typeof(ToolMessage), MessageRoles.Tool)]
public abstract record BaseMessage
{
[JsonPropertyName("id")]
[JsonPropertyOrder(-1)]
public required string Id { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/Context.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a context object to provide additional information to the agent.
/// </summary>
public sealed record Context
{
[JsonPropertyName("description")]
public required string Description { get; init; }

[JsonPropertyName("value")]
public required string Value { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/DeveloperMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a message from a developer.
/// </summary>
public sealed record DeveloperMessage : BaseMessage
{
[JsonPropertyName("content")]
public required string Content { get; init; }

[JsonPropertyName("name")]
public string? Name { get; init; }
}
19 changes: 19 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/FunctionCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a function call made by the agent.
/// </summary>
public sealed record FunctionCall
{
[JsonPropertyName("name")]
public required string Name { get; init; }

/// <summary>
/// The JSON-encoded arguments (as a string) for the function call.
/// </summary>
[JsonPropertyName("arguments")]
public required string Arguments { get; init; }
}
12 changes: 12 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/MessageRoles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace AGUIDotnet.Types;

public static class MessageRoles
{
public const string System = "system";
public const string User = "user";
public const string Assistant = "assistant";
public const string Tool = "tool";
public const string Developer = "developer";
}
50 changes: 50 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/RunAgentInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

public sealed record RunAgentInput
{
/// <summary>
/// ID of the conversation thread.
/// </summary>
[JsonPropertyName("threadId")]
public required string ThreadId { get; init; }

/// <summary>
/// ID of the current run.
/// </summary>
[JsonPropertyName("runId")]
public required string RunId { get; init; }

/// <summary>
/// The current state of the agent, at the time of the agent being called.
/// </summary>
[JsonPropertyName("state")]
public required JsonElement State { get; init; }

/// <summary>
/// The messages that are part of the conversation thread.
/// </summary>
[JsonPropertyName("messages")]
public required ImmutableList<BaseMessage> Messages { get; init; } = [];

/// <summary>
/// Tools available to the agent from the caller.
/// </summary>
[JsonPropertyName("tools")]
public required ImmutableList<Tool> Tools { get; init; } = [];

/// <summary>
/// Context items that provide additional information to the agent.
/// </summary>
[JsonPropertyName("context")]
public required ImmutableList<Context> Context { get; init; } = [];

/// <summary>
/// Additional forwarded properties that are passed to the agent.
/// </summary>
[JsonPropertyName("forwardedProps")]
public required JsonElement ForwardedProps { get; init; }
}
15 changes: 15 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/SystemMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a system message.
/// </summary>
public sealed record SystemMessage : BaseMessage
{
[JsonPropertyName("content")]
public required string Content { get; init; }

[JsonPropertyName("name")]
public string? Name { get; init; }
}
22 changes: 22 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/Tool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Defines a tool that can be called by an agent.
/// </summary>
public sealed record Tool
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("description")]
public required string Description { get; init; }

/// <summary>
/// The JSON schema for the parameters that this tool accepts
/// </summary>
[JsonPropertyName("parameters")]
public required JsonElement Parameters { get; init; }
}
20 changes: 20 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/ToolCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a tool call made by an agent.
/// </summary>
public sealed record ToolCall
{
[JsonPropertyName("id")]
public required string Id { get; init; }

[JsonPropertyName("type")]
#pragma warning disable CA1822 // Mark members as static
public string Type => "function";
#pragma warning restore CA1822 // Mark members as static

[JsonPropertyName("function")]
public required FunctionCall Function { get; init; }
}
23 changes: 23 additions & 0 deletions dotnet-sdk/AGUIDotnet/Types/ToolMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;

namespace AGUIDotnet.Types;

/// <summary>
/// Represents a message that is a response from a tool.
/// </summary>
/// <remarks>
/// <para>
/// This is used for scenarios where the agent has requested the frontend to call a tool, and the frontend is dispatching the result of the call to the agent.
/// </para>
/// <para>
/// NOTE: This message is received in a subsequent run after the one the agent requested the tool call in.
/// </para>
/// </remarks>
public sealed record ToolMessage : BaseMessage
{
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; init; }

[JsonPropertyName("content")]
public required string Content { get; init; }
}
Loading