.NET: [BREAKING] Support Checkpoint Serialization (#735)

* feat: Support Checkpoint Serialization

* Implements serialization roundtripping for checkpoints.
* Adds support for JSON serialization
* Adds FileSystem-based checkpoint persistence

* fix: Executor State does not deserialize correctly

The StateManager was not properly handling delay-deserialized values.

* Fix PortableValue handling in StateManager (this makes it delegate to PortableValue the uwnrapping)
* Fix UnitTest to actually test checkpoint serialization
* Additional review comment fixes

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
Jacob Alber
2025-09-17 19:57:19 +00:00
committed by GitHub
co-authored by Chris
parent 2a04c4197e
commit 2015f0dc09
90 changed files with 3011 additions and 341 deletions
@@ -2,7 +2,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Checkpointing;
using Microsoft.Shared.Diagnostics;
using MessageHandlerF =
@@ -17,24 +19,29 @@ namespace Microsoft.Agents.Workflows.Execution;
internal class MessageRouter
{
private readonly Dictionary<Type, MessageHandlerF> _typedHandlers;
private readonly Dictionary<TypeId, Type> _runtimeTypeMap;
private readonly bool _hasCatchall;
internal MessageRouter(Dictionary<Type, MessageHandlerF> handlers)
{
this._typedHandlers = Throw.IfNull(handlers);
this._hasCatchall = this._typedHandlers.ContainsKey(typeof(object));
Throw.IfNull(handlers);
this.IncomingTypes = [.. this._typedHandlers.Keys];
this._typedHandlers = handlers;
this._runtimeTypeMap = handlers.Keys.ToDictionary(t => new TypeId(t), t => t);
this._hasCatchall = handlers.ContainsKey(typeof(object));
this.IncomingTypes = [.. handlers.Keys];
}
public HashSet<Type> IncomingTypes { get; }
public bool CanHandle(object message) => this.CanHandle(Throw.IfNull(message).GetType());
public bool CanHandle(object message) => this.CanHandle(new TypeId(Throw.IfNull(message).GetType()));
public bool CanHandle(Type candidateType) => this.CanHandle(new TypeId(Throw.IfNull(candidateType)));
public bool CanHandle(Type candidateType)
public bool CanHandle(TypeId candidateType)
{
// For now we only support routing to handlers registered on the exact type (no base type delegation).
return this._hasCatchall || this._typedHandlers.ContainsKey(candidateType);
return this._hasCatchall || this._runtimeTypeMap.ContainsKey(candidateType);
}
public async ValueTask<CallResult?> RouteMessageAsync(object message, IWorkflowContext context, bool requireRoute = false)
@@ -43,6 +50,13 @@ internal class MessageRouter
CallResult? result = null;
if (message is PortableValue portableValue &&
this._runtimeTypeMap.TryGetValue(portableValue.TypeId, out Type? runtimeType))
{
// If we found a runtime type, we can use it
message = portableValue.AsType(runtimeType) ?? message;
}
try
{
if (this._typedHandlers.TryGetValue(message.GetType(), out MessageHandlerF? handler))