fix(abstractions): session events serialise to JSON (Partial no longer required)

This commit is contained in:
VolcanicAgenticCore
2026-09-25 13:17:11 +00:00
parent f9403aed77
commit 692992bb35
2 changed files with 67 additions and 1 deletions

View File

@@ -23,8 +23,12 @@ namespace Volcanic.AgenticCore.Abstractions.Providers;
[JsonDerivedType(typeof(Error), "error")]
public abstract record AssistantMessageEvent
{
/// <summary>
/// The cumulative message so far. Always set by <see cref="AssistantMessageBuilder"/>; not <c>required</c> because
/// System.Text.Json rejects a member that is both required and ignored, and this type is serialised inside session events.
/// </summary>
[JsonIgnore]
public required AssistantMessage Partial { get; init; }
public AssistantMessage Partial { get; init; } = null!;
public sealed record Start : AssistantMessageEvent;
public sealed record TextStart(int ContentIndex) : AssistantMessageEvent;

View File

@@ -0,0 +1,62 @@
using Volcanic.AgenticCore.Abstractions.Events;
using Volcanic.AgenticCore.Abstractions.Json;
using Volcanic.AgenticCore.Abstractions.Messages;
using Volcanic.AgenticCore.Abstractions.Providers;
using Volcanic.AgenticCore.Abstractions.Tools;
namespace Volcanic.AgenticCore.Tests.Abstractions;
/// <summary>A UI (or the headless sample) serialises every session event as JSON; none may throw.</summary>
public class AgentSessionEventJsonTests
{
[Fact]
public void Every_session_event_serialises_with_a_type_discriminator()
{
var builder = new AssistantMessageBuilder("faux", "faux", "echo");
builder.Start();
var delta = builder.AppendText("Hel");
var assistant = builder.Done(StopReason.Stop).Partial;
var model = new Model { Id = "echo", Name = "Echo", Provider = "faux", Api = "faux" };
AgentSessionEvent[] events =
[
new AgentSessionEvent.AgentStart(),
new AgentSessionEvent.TurnStart(0),
new AgentSessionEvent.MessageStart(UserMessage.FromText("hi")),
new AgentSessionEvent.MessageUpdate(delta.Partial, delta),
new AgentSessionEvent.MessageEnd(assistant),
new AgentSessionEvent.ToolExecutionStart("c1", "bash", VolcanicJson.Parse("""{"command":"ls"}""")),
new AgentSessionEvent.ToolExecutionUpdate("c1", "bash", VolcanicJson.EmptyObject, ToolResult.Text("partial")),
new AgentSessionEvent.ToolExecutionEnd("c1", "bash", ToolResult.Text("done"), false),
new AgentSessionEvent.TurnEnd(0, assistant, []),
new AgentSessionEvent.QueueUpdate(["steer"], []),
new AgentSessionEvent.CompactionStart(CompactionReason.Threshold),
new AgentSessionEvent.CompactionEnd(CompactionReason.Manual, new CompactionResult("sum", null, 10, 5), false, false, null),
new AgentSessionEvent.AutoRetryStart(1, 3, TimeSpan.FromSeconds(2), "529"),
new AgentSessionEvent.AutoRetryEnd(true, 1, null),
new AgentSessionEvent.SessionInfoChanged("name"),
new AgentSessionEvent.ThinkingLevelChanged(ThinkingLevel.High),
new AgentSessionEvent.ModelChanged(model),
new AgentSessionEvent.ExtensionError("ext", "tool_call", "boom"),
new AgentSessionEvent.Notification("hello", Volcanic.AgenticCore.Abstractions.Ui.NotifyKind.Warning),
new AgentSessionEvent.AgentEnd([assistant], false),
new AgentSessionEvent.AgentSettled(),
];
foreach (var e in events)
{
var json = VolcanicJson.Serialize(e);
Assert.StartsWith("{\"type\":\"", json);
}
var update = VolcanicJson.Serialize<AgentSessionEvent>(new AgentSessionEvent.MessageUpdate(delta.Partial, delta));
Assert.Contains("\"type\":\"message_update\"", update);
Assert.Contains("\"assistantMessageEvent\":{\"type\":\"text_delta\"", update);
Assert.Contains("\"delta\":\"Hel\"", update);
Assert.DoesNotContain("\"partial\"", update);
var end = VolcanicJson.Deserialize<AgentSessionEvent>(VolcanicJson.Serialize<AgentSessionEvent>(new AgentSessionEvent.MessageEnd(assistant)));
var typed = Assert.IsType<AgentSessionEvent.MessageEnd>(end);
Assert.Equal("Hel", typed.Message.Text);
}
}