Skip to content
Open
Show file tree
Hide file tree
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
14 changes: 14 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIEventTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,18 @@ internal static class AGUIEventTypes
public const string StateSnapshot = "STATE_SNAPSHOT";

public const string StateDelta = "STATE_DELTA";

public const string ReasoningStart = "REASONING_START";

public const string ReasoningMessageStart = "REASONING_MESSAGE_START";

public const string ReasoningMessageContent = "REASONING_MESSAGE_CONTENT";

public const string ReasoningMessageEnd = "REASONING_MESSAGE_END";

public const string ReasoningEnd = "REASONING_END";

public const string ReasoningMessageChunk = "REASONING_MESSAGE_CHUNK";

public const string ReasoningEncryptedValue = "REASONING_ENCRYPTED_VALUE";
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ namespace Microsoft.Agents.AI.AGUI;
[JsonSerializable(typeof(ToolCallResultEvent))]
[JsonSerializable(typeof(StateSnapshotEvent))]
[JsonSerializable(typeof(StateDeltaEvent))]
[JsonSerializable(typeof(ReasoningStartEvent))]
[JsonSerializable(typeof(ReasoningMessageStartEvent))]
[JsonSerializable(typeof(ReasoningMessageContentEvent))]
[JsonSerializable(typeof(ReasoningMessageEndEvent))]
[JsonSerializable(typeof(ReasoningEndEvent))]
[JsonSerializable(typeof(ReasoningMessageChunkEvent))]
[JsonSerializable(typeof(ReasoningEncryptedValueEvent))]
[JsonSerializable(typeof(IDictionary<string, object?>))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
[JsonSerializable(typeof(IDictionary<string, System.Text.Json.JsonElement?>))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ public override BaseEvent Read(
AGUIEventTypes.ToolCallEnd => jsonElement.Deserialize(options.GetTypeInfo(typeof(ToolCallEndEvent))) as ToolCallEndEvent,
AGUIEventTypes.ToolCallResult => jsonElement.Deserialize(options.GetTypeInfo(typeof(ToolCallResultEvent))) as ToolCallResultEvent,
AGUIEventTypes.StateSnapshot => jsonElement.Deserialize(options.GetTypeInfo(typeof(StateSnapshotEvent))) as StateSnapshotEvent,
AGUIEventTypes.ReasoningStart => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningStartEvent))) as ReasoningStartEvent,
AGUIEventTypes.ReasoningMessageStart => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningMessageStartEvent))) as ReasoningMessageStartEvent,
AGUIEventTypes.ReasoningMessageContent => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningMessageContentEvent))) as ReasoningMessageContentEvent,
AGUIEventTypes.ReasoningMessageEnd => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningMessageEndEvent))) as ReasoningMessageEndEvent,
AGUIEventTypes.ReasoningEnd => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningEndEvent))) as ReasoningEndEvent,
AGUIEventTypes.ReasoningMessageChunk => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningMessageChunkEvent))) as ReasoningMessageChunkEvent,
AGUIEventTypes.ReasoningEncryptedValue => jsonElement.Deserialize(options.GetTypeInfo(typeof(ReasoningEncryptedValueEvent))) as ReasoningEncryptedValueEvent,
_ => throw new JsonException($"Unknown BaseEvent type discriminator: '{discriminator}'")
};

Expand Down Expand Up @@ -102,6 +109,27 @@ public override void Write(
case StateDeltaEvent stateDelta:
JsonSerializer.Serialize(writer, stateDelta, options.GetTypeInfo(typeof(StateDeltaEvent)));
break;
case ReasoningStartEvent reasoningStart:
JsonSerializer.Serialize(writer, reasoningStart, options.GetTypeInfo(typeof(ReasoningStartEvent)));
break;
case ReasoningMessageStartEvent reasoningMessageStart:
JsonSerializer.Serialize(writer, reasoningMessageStart, options.GetTypeInfo(typeof(ReasoningMessageStartEvent)));
break;
case ReasoningMessageContentEvent reasoningMessageContent:
JsonSerializer.Serialize(writer, reasoningMessageContent, options.GetTypeInfo(typeof(ReasoningMessageContentEvent)));
break;
case ReasoningMessageEndEvent reasoningMessageEnd:
JsonSerializer.Serialize(writer, reasoningMessageEnd, options.GetTypeInfo(typeof(ReasoningMessageEndEvent)));
break;
case ReasoningEndEvent reasoningEnd:
JsonSerializer.Serialize(writer, reasoningEnd, options.GetTypeInfo(typeof(ReasoningEndEvent)));
break;
case ReasoningMessageChunkEvent reasoningMessageChunk:
JsonSerializer.Serialize(writer, reasoningMessageChunk, options.GetTypeInfo(typeof(ReasoningMessageChunkEvent)));
break;
case ReasoningEncryptedValueEvent reasoningEncryptedValue:
JsonSerializer.Serialize(writer, reasoningEncryptedValue, options.GetTypeInfo(typeof(ReasoningEncryptedValueEvent)));
break;
default:
throw new InvalidOperationException($"Unknown event type: {value.GetType().Name}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAs
string? responseId = null;
var textMessageBuilder = new TextMessageBuilder();
var toolCallAccumulator = new ToolCallBuilder();
var reasoningBuilder = new ReasoningMessageBuilder();
await foreach (var evt in events.WithCancellation(cancellationToken).ConfigureAwait(false))
{
switch (evt)
Expand All @@ -41,6 +42,7 @@ public static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAs
responseId = runStarted.RunId;
toolCallAccumulator.SetConversationAndResponseIds(conversationId, responseId);
textMessageBuilder.SetConversationAndResponseIds(conversationId, responseId);
reasoningBuilder.SetConversationAndResponseIds(conversationId, responseId);
yield return ValidateAndEmitRunStart(runStarted);
break;
case RunFinishedEvent runFinished:
Expand Down Expand Up @@ -88,6 +90,36 @@ public static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAs
yield return CreateStateDeltaUpdate(stateDelta, conversationId, responseId, jsonSerializerOptions);
}
break;

// Reasoning events (explicit lifecycle form)
case ReasoningMessageStartEvent reasoningStart:
reasoningBuilder.AddReasoningStart(reasoningStart);
break;
case ReasoningMessageContentEvent reasoningContent:
yield return reasoningBuilder.EmitReasoningContent(reasoningContent);
break;
case ReasoningMessageEndEvent reasoningEnd:
reasoningBuilder.EndCurrentMessage(reasoningEnd);
break;

// Reasoning events (chunk shorthand form)
case ReasoningMessageChunkEvent reasoningChunk:
var chunkUpdate = reasoningBuilder.EmitReasoningChunk(reasoningChunk);
if (chunkUpdate is not null)
{
yield return chunkUpdate;
}
break;

// Encrypted reasoning value (emitted by either form)
case ReasoningEncryptedValueEvent encryptedValue:
yield return reasoningBuilder.EmitEncryptedValue(encryptedValue);
break;

// ReasoningStartEvent and ReasoningEndEvent are bracket markers only — no content to emit
case ReasoningStartEvent:
case ReasoningEndEvent:
break;
}
}
}
Expand Down Expand Up @@ -305,6 +337,81 @@ internal void SetConversationAndResponseIds(string conversationId, string respon
}
}

private sealed class ReasoningMessageBuilder()
{
private string? _currentMessageId;
private string? _conversationId;
private string? _responseId;

public void SetConversationAndResponseIds(string? conversationId, string? responseId)
{
this._conversationId = conversationId;
this._responseId = responseId;
}

public void AddReasoningStart(ReasoningMessageStartEvent reasoningStart)
{
if (this._currentMessageId != null)
{
throw new InvalidOperationException(
"Received ReasoningMessageStartEvent while another message is being processed.");
}

this._currentMessageId = reasoningStart.MessageId;
}

public ChatResponseUpdate EmitReasoningContent(ReasoningMessageContentEvent contentEvent)
{
return new ChatResponseUpdate(ChatRole.Assistant, [new TextReasoningContent(contentEvent.Delta)])
{
ConversationId = this._conversationId,
ResponseId = this._responseId,
MessageId = contentEvent.MessageId,
CreatedAt = DateTimeOffset.UtcNow
};
}

public ChatResponseUpdate? EmitReasoningChunk(ReasoningMessageChunkEvent chunkEvent)
{
if (string.IsNullOrEmpty(chunkEvent.Delta))
{
// Empty delta is the implicit close signal for chunk-based streaming
this._currentMessageId = null;
return null;
}

this._currentMessageId ??= chunkEvent.MessageId;
return new ChatResponseUpdate(ChatRole.Assistant, [new TextReasoningContent(chunkEvent.Delta)])
{
ConversationId = this._conversationId,
ResponseId = this._responseId,
MessageId = chunkEvent.MessageId,
CreatedAt = DateTimeOffset.UtcNow
};
}

public ChatResponseUpdate EmitEncryptedValue(ReasoningEncryptedValueEvent encryptedEvent)
{
return new ChatResponseUpdate(ChatRole.Assistant, [new TextReasoningContent("") { ProtectedData = encryptedEvent.EncryptedValue }])
{
ConversationId = this._conversationId,
ResponseId = this._responseId,
MessageId = encryptedEvent.EntityId,
CreatedAt = DateTimeOffset.UtcNow
};
}

public void EndCurrentMessage(ReasoningMessageEndEvent reasoningEnd)
{
if (!string.Equals(this._currentMessageId, reasoningEnd.MessageId, StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Received ReasoningMessageEndEvent for a different message than the current one.");
}
this._currentMessageId = null;
}
}

private static IDictionary<string, object?>? DeserializeArgumentsIfAvailable(string argsJson, JsonSerializerOptions options)
{
if (!string.IsNullOrEmpty(argsJson))
Expand Down Expand Up @@ -341,6 +448,7 @@ public static async IAsyncEnumerable<BaseEvent> AsAGUIEventStreamAsync(
};

string? currentMessageId = null;
string? currentReasoningMessageId = null;
await foreach (var chatResponse in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
{
if (chatResponse is { Contents.Count: > 0 } &&
Expand Down Expand Up @@ -414,6 +522,52 @@ chatResponse.Contents[0] is TextContent &&
Role = AGUIRoles.Tool
};
}
else if (content is TextReasoningContent reasoningContent
&& (!string.IsNullOrEmpty(reasoningContent.Text) || !string.IsNullOrEmpty(reasoningContent.ProtectedData)))
{
if (!string.Equals(currentReasoningMessageId, chatResponse.MessageId, StringComparison.Ordinal))
{
if (currentReasoningMessageId is not null)
{
yield return new ReasoningMessageEndEvent
{
MessageId = currentReasoningMessageId
};
yield return new ReasoningEndEvent
{
MessageId = currentReasoningMessageId
};
}

yield return new ReasoningStartEvent
{
MessageId = chatResponse.MessageId!
};
yield return new ReasoningMessageStartEvent
{
MessageId = chatResponse.MessageId!
};
currentReasoningMessageId = chatResponse.MessageId;
}

if (!string.IsNullOrEmpty(reasoningContent.Text))
{
yield return new ReasoningMessageContentEvent
{
MessageId = currentReasoningMessageId!,
Delta = reasoningContent.Text
};
}

if (!string.IsNullOrEmpty(reasoningContent.ProtectedData))
{
yield return new ReasoningEncryptedValueEvent
{
EntityId = currentReasoningMessageId!,
EncryptedValue = reasoningContent.ProtectedData
};
}
}
else if (content is DataContent dataContent)
{
if (MediaTypeHeaderValue.TryParse(dataContent.MediaType, out var mediaType) && mediaType.Equals(s_json))
Expand Down Expand Up @@ -467,6 +621,19 @@ chatResponse.Contents[0] is TextContent &&
}
}

// End the last reasoning block if there was one
if (currentReasoningMessageId is not null)
{
yield return new ReasoningMessageEndEvent
{
MessageId = currentReasoningMessageId
};
yield return new ReasoningEndEvent
{
MessageId = currentReasoningMessageId
};
}

// End the last message if there was one
if (currentMessageId is not null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

#if ASPNETCORE
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
#else
namespace Microsoft.Agents.AI.AGUI.Shared;
#endif

internal sealed class ReasoningEncryptedValueEvent : BaseEvent
{
public ReasoningEncryptedValueEvent()
{
this.Type = AGUIEventTypes.ReasoningEncryptedValue;
}

[JsonPropertyName("subtype")]
public string Subtype { get; set; } = "message";

[JsonPropertyName("entityId")]
public string EntityId { get; set; } = string.Empty;

[JsonPropertyName("encryptedValue")]
public string EncryptedValue { get; set; } = string.Empty;
}
20 changes: 20 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ReasoningEndEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

#if ASPNETCORE
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
#else
namespace Microsoft.Agents.AI.AGUI.Shared;
#endif

internal sealed class ReasoningEndEvent : BaseEvent
{
public ReasoningEndEvent()
{
this.Type = AGUIEventTypes.ReasoningEnd;
}

[JsonPropertyName("messageId")]
public string MessageId { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

#if ASPNETCORE
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
#else
namespace Microsoft.Agents.AI.AGUI.Shared;
#endif

internal sealed class ReasoningMessageChunkEvent : BaseEvent
{
public ReasoningMessageChunkEvent()
{
this.Type = AGUIEventTypes.ReasoningMessageChunk;
}

[JsonPropertyName("messageId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MessageId { get; set; }

[JsonPropertyName("delta")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Delta { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

#if ASPNETCORE
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
#else
namespace Microsoft.Agents.AI.AGUI.Shared;
#endif

internal sealed class ReasoningMessageContentEvent : BaseEvent
{
public ReasoningMessageContentEvent()
{
this.Type = AGUIEventTypes.ReasoningMessageContent;
}

[JsonPropertyName("messageId")]
public string MessageId { get; set; } = string.Empty;

[JsonPropertyName("delta")]
public string Delta { get; set; } = string.Empty;
}
Loading