Skip to content

Commit f25fb35

Browse files
Add Umbraco.Ai.Agui project with AG-UI protocol types
Implements AG-UI protocol models and events for agent-UI communication: Models: - AguiRunRequest with resume support for interrupt continuation - AguiMessage, AguiToolCall, AguiFunctionCall for message types - AguiContextItem, AguiTool for context and tool definitions - AguiResumeInfo, AguiInterruptInfo for interrupt handling - AguiMessageRole, AguiRunOutcome enums Events (21 total): - Lifecycle: RunStarted, RunFinished, RunError, StepStarted, StepFinished - Messages: TextMessageStart, TextMessageContent, TextMessageEnd, TextMessageChunk - Tools: ToolCallStart, ToolCallArgs, ToolCallEnd, ToolCallResult, ToolCallChunk - State: StateSnapshot, StateDelta, MessagesSnapshot - Activity: ActivitySnapshot, ActivityDelta - Special: Raw, Custom All JSON property names use camelCase per AG-UI spec. Includes interrupt draft spec support (outcome, interrupt, resume). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ae9ee16 commit f25fb35

41 files changed

Lines changed: 1259 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Umbraco.Ai.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Ai.Persistence.SqlS
2424
EndProject
2525
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Ai.Persistence.Sqlite", "src\Umbraco.Ai.Persistence.Sqlite\Umbraco.Ai.Persistence.Sqlite.csproj", "{5D759E09-0005-4A89-B473-FB049D08279E}"
2626
EndProject
27+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Ai.Agui", "src\Umbraco.Ai.Agui\Umbraco.Ai.Agui.csproj", "{8048B8F9-BB8D-478C-AD20-CBA199172143}"
28+
EndProject
2729
Global
2830
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2931
Debug|Any CPU = Debug|Any CPU
@@ -166,6 +168,18 @@ Global
166168
{5D759E09-0005-4A89-B473-FB049D08279E}.Release|x64.Build.0 = Release|Any CPU
167169
{5D759E09-0005-4A89-B473-FB049D08279E}.Release|x86.ActiveCfg = Release|Any CPU
168170
{5D759E09-0005-4A89-B473-FB049D08279E}.Release|x86.Build.0 = Release|Any CPU
171+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
172+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Debug|Any CPU.Build.0 = Debug|Any CPU
173+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Debug|x64.ActiveCfg = Debug|Any CPU
174+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Debug|x64.Build.0 = Debug|Any CPU
175+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Debug|x86.ActiveCfg = Debug|Any CPU
176+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Debug|x86.Build.0 = Debug|Any CPU
177+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Release|Any CPU.ActiveCfg = Release|Any CPU
178+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Release|Any CPU.Build.0 = Release|Any CPU
179+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Release|x64.ActiveCfg = Release|Any CPU
180+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Release|x64.Build.0 = Release|Any CPU
181+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Release|x86.ActiveCfg = Release|Any CPU
182+
{8048B8F9-BB8D-478C-AD20-CBA199172143}.Release|x86.Build.0 = Release|Any CPU
169183
EndGlobalSection
170184
GlobalSection(SolutionProperties) = preSolution
171185
HideSolutionNode = FALSE
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace Umbraco.Ai.Agui;
2+
3+
/// <summary>
4+
/// Constants for AG-UI protocol.
5+
/// </summary>
6+
public static class AguiConstants
7+
{
8+
/// <summary>
9+
/// AG-UI event type strings as defined in the specification.
10+
/// Uses UPPER_SNAKE_CASE to match the official AG-UI protocol.
11+
/// </summary>
12+
public static class EventTypes
13+
{
14+
// Lifecycle events
15+
public const string RunStarted = "RUN_STARTED";
16+
public const string RunFinished = "RUN_FINISHED";
17+
public const string RunError = "RUN_ERROR";
18+
public const string StepStarted = "STEP_STARTED";
19+
public const string StepFinished = "STEP_FINISHED";
20+
21+
// Message events
22+
public const string TextMessageStart = "TEXT_MESSAGE_START";
23+
public const string TextMessageContent = "TEXT_MESSAGE_CONTENT";
24+
public const string TextMessageEnd = "TEXT_MESSAGE_END";
25+
public const string TextMessageChunk = "TEXT_MESSAGE_CHUNK";
26+
27+
// Tool events
28+
public const string ToolCallStart = "TOOL_CALL_START";
29+
public const string ToolCallArgs = "TOOL_CALL_ARGS";
30+
public const string ToolCallEnd = "TOOL_CALL_END";
31+
public const string ToolCallResult = "TOOL_CALL_RESULT";
32+
public const string ToolCallChunk = "TOOL_CALL_CHUNK";
33+
34+
// State events
35+
public const string StateSnapshot = "STATE_SNAPSHOT";
36+
public const string StateDelta = "STATE_DELTA";
37+
public const string MessagesSnapshot = "MESSAGES_SNAPSHOT";
38+
39+
// Activity events
40+
public const string ActivitySnapshot = "ACTIVITY_SNAPSHOT";
41+
public const string ActivityDelta = "ACTIVITY_DELTA";
42+
43+
// Special events
44+
public const string Raw = "RAW";
45+
public const string Custom = "CUSTOM";
46+
}
47+
48+
/// <summary>
49+
/// AG-UI message role strings as defined in the specification.
50+
/// </summary>
51+
public static class MessageRoles
52+
{
53+
public const string User = "user";
54+
public const string Assistant = "assistant";
55+
public const string System = "system";
56+
public const string Tool = "tool";
57+
public const string Developer = "developer";
58+
}
59+
60+
/// <summary>
61+
/// AG-UI run outcome strings.
62+
/// </summary>
63+
public static class RunOutcomes
64+
{
65+
public const string Success = "success";
66+
public const string Interrupt = "interrupt";
67+
}
68+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Umbraco.Ai.Agui.Streaming;
3+
4+
namespace Umbraco.Ai.Agui.Configuration;
5+
6+
/// <summary>
7+
/// Extension methods for configuring AG-UI services.
8+
/// </summary>
9+
public static class ServiceCollectionExtensions
10+
{
11+
/// <summary>
12+
/// Adds AG-UI services to the service collection.
13+
/// </summary>
14+
/// <param name="services">The service collection.</param>
15+
/// <returns>The service collection for chaining.</returns>
16+
public static IServiceCollection AddAgui(this IServiceCollection services)
17+
{
18+
return services.AddAgui(_ => { });
19+
}
20+
21+
/// <summary>
22+
/// Adds AG-UI services to the service collection with options configuration.
23+
/// </summary>
24+
/// <param name="services">The service collection.</param>
25+
/// <param name="configureOptions">The options configuration action.</param>
26+
/// <returns>The service collection for chaining.</returns>
27+
public static IServiceCollection AddAgui(
28+
this IServiceCollection services,
29+
Action<AguiStreamOptions> configureOptions)
30+
{
31+
var options = new AguiStreamOptions();
32+
configureOptions(options);
33+
34+
services.AddSingleton(options);
35+
services.AddSingleton<AguiEventSerializer>();
36+
37+
return services;
38+
}
39+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Umbraco.Ai.Agui.Events.Activity;
5+
6+
/// <summary>
7+
/// Event that provides incremental updates to an activity snapshot using JSON Patch (RFC 6902).
8+
/// </summary>
9+
public sealed record ActivityDeltaEvent : BaseAguiEvent
10+
{
11+
/// <summary>
12+
/// Gets or sets the message identifier.
13+
/// </summary>
14+
[JsonPropertyName("messageId")]
15+
public required string MessageId { get; init; }
16+
17+
/// <summary>
18+
/// Gets or sets the activity type.
19+
/// </summary>
20+
[JsonPropertyName("activityType")]
21+
public required string ActivityType { get; init; }
22+
23+
/// <summary>
24+
/// Gets or sets the JSON Patch operations to apply.
25+
/// </summary>
26+
[JsonPropertyName("patch")]
27+
public required JsonElement Patch { get; init; }
28+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Umbraco.Ai.Agui.Events.Activity;
4+
5+
/// <summary>
6+
/// Event that delivers a complete snapshot of an activity message.
7+
/// Activity messages are frontend-only UI updates that don't affect the conversation history.
8+
/// </summary>
9+
public sealed record ActivitySnapshotEvent : BaseAguiEvent
10+
{
11+
/// <summary>
12+
/// Gets or sets the message identifier.
13+
/// </summary>
14+
[JsonPropertyName("messageId")]
15+
public required string MessageId { get; init; }
16+
17+
/// <summary>
18+
/// Gets or sets the activity type (e.g., "thinking", "searching", "processing").
19+
/// </summary>
20+
[JsonPropertyName("activityType")]
21+
public required string ActivityType { get; init; }
22+
23+
/// <summary>
24+
/// Gets or sets the activity content.
25+
/// </summary>
26+
[JsonPropertyName("content")]
27+
public required string Content { get; init; }
28+
29+
/// <summary>
30+
/// Gets or sets whether this snapshot should replace any existing activity with the same messageId.
31+
/// </summary>
32+
[JsonPropertyName("replace")]
33+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
34+
public bool? Replace { get; init; }
35+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Text.Json.Serialization;
2+
using Umbraco.Ai.Agui.Events.Activity;
3+
using Umbraco.Ai.Agui.Events.Lifecycle;
4+
using Umbraco.Ai.Agui.Events.Messages;
5+
using Umbraco.Ai.Agui.Events.Special;
6+
using Umbraco.Ai.Agui.Events.State;
7+
using Umbraco.Ai.Agui.Events.Tools;
8+
9+
namespace Umbraco.Ai.Agui.Events;
10+
11+
/// <summary>
12+
/// Abstract base record for AG-UI events.
13+
/// Uses JsonPolymorphic for automatic type discrimination in JSON serialization.
14+
/// </summary>
15+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
16+
[JsonDerivedType(typeof(RunStartedEvent), AguiConstants.EventTypes.RunStarted)]
17+
[JsonDerivedType(typeof(RunFinishedEvent), AguiConstants.EventTypes.RunFinished)]
18+
[JsonDerivedType(typeof(RunErrorEvent), AguiConstants.EventTypes.RunError)]
19+
[JsonDerivedType(typeof(StepStartedEvent), AguiConstants.EventTypes.StepStarted)]
20+
[JsonDerivedType(typeof(StepFinishedEvent), AguiConstants.EventTypes.StepFinished)]
21+
[JsonDerivedType(typeof(TextMessageStartEvent), AguiConstants.EventTypes.TextMessageStart)]
22+
[JsonDerivedType(typeof(TextMessageContentEvent), AguiConstants.EventTypes.TextMessageContent)]
23+
[JsonDerivedType(typeof(TextMessageEndEvent), AguiConstants.EventTypes.TextMessageEnd)]
24+
[JsonDerivedType(typeof(TextMessageChunkEvent), AguiConstants.EventTypes.TextMessageChunk)]
25+
[JsonDerivedType(typeof(ToolCallStartEvent), AguiConstants.EventTypes.ToolCallStart)]
26+
[JsonDerivedType(typeof(ToolCallArgsEvent), AguiConstants.EventTypes.ToolCallArgs)]
27+
[JsonDerivedType(typeof(ToolCallEndEvent), AguiConstants.EventTypes.ToolCallEnd)]
28+
[JsonDerivedType(typeof(ToolCallResultEvent), AguiConstants.EventTypes.ToolCallResult)]
29+
[JsonDerivedType(typeof(ToolCallChunkEvent), AguiConstants.EventTypes.ToolCallChunk)]
30+
[JsonDerivedType(typeof(StateSnapshotEvent), AguiConstants.EventTypes.StateSnapshot)]
31+
[JsonDerivedType(typeof(StateDeltaEvent), AguiConstants.EventTypes.StateDelta)]
32+
[JsonDerivedType(typeof(MessagesSnapshotEvent), AguiConstants.EventTypes.MessagesSnapshot)]
33+
[JsonDerivedType(typeof(ActivitySnapshotEvent), AguiConstants.EventTypes.ActivitySnapshot)]
34+
[JsonDerivedType(typeof(ActivityDeltaEvent), AguiConstants.EventTypes.ActivityDelta)]
35+
[JsonDerivedType(typeof(CustomEvent), AguiConstants.EventTypes.Custom)]
36+
[JsonDerivedType(typeof(RawEvent), AguiConstants.EventTypes.Raw)]
37+
public abstract record BaseAguiEvent : IAguiEvent
38+
{
39+
/// <summary>
40+
/// Gets or sets the timestamp of the event in Unix milliseconds.
41+
/// </summary>
42+
[JsonPropertyName("timestamp")]
43+
public long? Timestamp { get; init; }
44+
45+
/// <summary>
46+
/// Gets or sets optional raw event data for passthrough scenarios.
47+
/// </summary>
48+
[JsonPropertyName("rawEvent")]
49+
public object? RawEvent { get; init; }
50+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Umbraco.Ai.Agui.Events;
2+
3+
/// <summary>
4+
/// Marker interface for all AG-UI events.
5+
/// Type discrimination is handled via JsonPolymorphic attributes on BaseAguiEvent.
6+
/// </summary>
7+
public interface IAguiEvent
8+
{
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Umbraco.Ai.Agui.Events.Lifecycle;
4+
5+
/// <summary>
6+
/// Event emitted when an agent run encounters an error.
7+
/// </summary>
8+
public sealed record RunErrorEvent : BaseAguiEvent
9+
{
10+
/// <summary>
11+
/// Gets or sets the error message.
12+
/// </summary>
13+
[JsonPropertyName("message")]
14+
public required string Message { get; init; }
15+
16+
/// <summary>
17+
/// Gets or sets the optional error code.
18+
/// </summary>
19+
[JsonPropertyName("code")]
20+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
21+
public string? Code { get; init; }
22+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Text.Json.Serialization;
2+
using Umbraco.Ai.Agui.Models;
3+
4+
namespace Umbraco.Ai.Agui.Events.Lifecycle;
5+
6+
/// <summary>
7+
/// Event emitted when an agent run finishes.
8+
/// </summary>
9+
public sealed record RunFinishedEvent : BaseAguiEvent
10+
{
11+
/// <summary>
12+
/// Gets or sets the thread identifier.
13+
/// </summary>
14+
[JsonPropertyName("threadId")]
15+
public required string ThreadId { get; init; }
16+
17+
/// <summary>
18+
/// Gets or sets the run identifier.
19+
/// </summary>
20+
[JsonPropertyName("runId")]
21+
public required string RunId { get; init; }
22+
23+
/// <summary>
24+
/// Gets or sets the run outcome.
25+
/// </summary>
26+
[JsonPropertyName("outcome")]
27+
public AguiRunOutcome Outcome { get; init; } = AguiRunOutcome.Success;
28+
29+
/// <summary>
30+
/// Gets or sets the interrupt information when outcome is Interrupt.
31+
/// </summary>
32+
[JsonPropertyName("interrupt")]
33+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
34+
public AguiInterruptInfo? Interrupt { get; init; }
35+
36+
/// <summary>
37+
/// Gets or sets the optional result data.
38+
/// </summary>
39+
[JsonPropertyName("result")]
40+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
41+
public object? Result { get; init; }
42+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Umbraco.Ai.Agui.Events.Lifecycle;
5+
6+
/// <summary>
7+
/// Event emitted when an agent run starts.
8+
/// </summary>
9+
public sealed record RunStartedEvent : BaseAguiEvent
10+
{
11+
/// <summary>
12+
/// Gets or sets the thread identifier.
13+
/// </summary>
14+
[JsonPropertyName("threadId")]
15+
public required string ThreadId { get; init; }
16+
17+
/// <summary>
18+
/// Gets or sets the run identifier.
19+
/// </summary>
20+
[JsonPropertyName("runId")]
21+
public required string RunId { get; init; }
22+
23+
/// <summary>
24+
/// Gets or sets the optional parent run identifier for nested/child runs.
25+
/// </summary>
26+
[JsonPropertyName("parentRunId")]
27+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
28+
public string? ParentRunId { get; init; }
29+
30+
/// <summary>
31+
/// Gets or sets the optional input data for the run.
32+
/// </summary>
33+
[JsonPropertyName("input")]
34+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
35+
public JsonElement? Input { get; init; }
36+
}

0 commit comments

Comments
 (0)