diff --git a/dotnet/samples/03-workflows/Declarative/AotCheckpointing/Program.cs b/dotnet/samples/03-workflows/Declarative/AotCheckpointing/Program.cs index 2bfd6d51d72..8f515b7d866 100644 --- a/dotnet/samples/03-workflows/Declarative/AotCheckpointing/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/AotCheckpointing/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using System.Text.Json.Serialization; using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; @@ -15,12 +17,12 @@ namespace Demo.Workflows.Declarative.AotCheckpointing; /// /// Demonstrates JSON checkpointing of a declarative workflow under reflection-disabled -/// (the AOT / trim-aggressive constraint set +/// (the AOT / trim-aggressive constraint set /// via JsonSerializerIsReflectionEnabledByDefault=false in the csproj). /// /// -/// The key call is -/// with . Drop the options argument to observe the AOT failure. See README. +/// The key call is with +/// . Drop the options argument to observe the AOT failure. See README. /// internal sealed class Program { @@ -31,14 +33,14 @@ public static async Task Main(string[] args) await CreateGreeterAgentAsync(foundryEndpoint, configuration); - string workflowInput = Application.GetInput(args); + WorkflowInput workflowInput = new(Application.GetInput(args)); Workflow CreateWorkflow() { AzureAgentProvider agentProvider = new(foundryEndpoint, new AzureCliCredential()); DeclarativeWorkflowOptions options = new(agentProvider) { Configuration = configuration }; string workflowPath = Path.Combine(AppContext.BaseDirectory, "AotCheckpointing.yaml"); - return DeclarativeWorkflowBuilder.Build(workflowPath, options); + return DeclarativeWorkflowBuilder.Build(workflowPath, options, TransformInput); } DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-HHmmss-ff}")); @@ -74,7 +76,10 @@ Workflow CreateWorkflow() } } - private static async Task> RunAndStreamAsync(Workflow workflow, string input, CheckpointManager checkpointManager) + private static ChatMessage TransformInput(WorkflowInput input) => + new(ChatRole.User, JsonSerializer.Serialize(input, AotCheckpointingJsonContext.Default.WorkflowInput)); + + private static async Task> RunAndStreamAsync(Workflow workflow, WorkflowInput input, CheckpointManager checkpointManager) { StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input, checkpointManager).ConfigureAwait(false); return await DrainAsync(run).ConfigureAwait(false); @@ -172,3 +177,8 @@ private static void TryDelete(DirectoryInfo directory) } } } + +internal sealed record WorkflowInput(string Message); + +[JsonSerializable(typeof(WorkflowInput))] +internal sealed partial class AotCheckpointingJsonContext : JsonSerializerContext; diff --git a/dotnet/samples/03-workflows/Declarative/AotCheckpointing/README.md b/dotnet/samples/03-workflows/Declarative/AotCheckpointing/README.md index 9856274bfc8..b601f40299d 100644 --- a/dotnet/samples/03-workflows/Declarative/AotCheckpointing/README.md +++ b/dotnet/samples/03-workflows/Declarative/AotCheckpointing/README.md @@ -25,15 +25,54 @@ reflection-disabled `System.Text.Json` -- the same constraint imposed by return is the proof JSON **reads** round-trip too. The resumed run is disposed immediately; without a pending external request it would park in `WaitForInputAsync` indefinitely. +- The initial workflow input is a `WorkflowInput` record. `TransformInput` uses the + source-generated `AotCheckpointingJsonContext` to serialize it into a + `ChatMessage` before the declarative workflow runs. `DeclarativeWorkflowJsonOptions` is marked `[Experimental("MAAI001")]`. Suppress that diagnostic in your csproj to use it. -### Registering user-defined types +### Initial input and checkpoint serialization are separate -For workflows whose inputs or custom `ActionExecutorResult.Result` -payloads are user-defined, clone `Default` and append your own resolver: +`DeclarativeWorkflowBuilder.Build` accepts an optional `inputTransform` delegate. +For a non-`ChatMessage` input, the default behavior is to call `ToString()`; the +checkpoint serializer is not involved in this conversion. Use a source-generated +context (or your own `JsonSerializerOptions`) in the delegate when the workflow +should receive a JSON representation of a typed input: + +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Agents.AI.Workflows.Declarative; +using Microsoft.Extensions.AI; + +internal sealed record WorkflowInput(string Message); + +[JsonSerializable(typeof(WorkflowInput))] +internal sealed partial class AppJsonContext : JsonSerializerContext; + +Workflow workflow = DeclarativeWorkflowBuilder.Build( + workflowPath, + options, + input => new ChatMessage( + ChatRole.User, + JsonSerializer.Serialize(input, AppJsonContext.Default.WorkflowInput))); +``` + +This sample uses `AotCheckpointingJsonContext` for that initial-input transform. +The separate `DeclarativeWorkflowJsonOptions.Default` passed to +`CheckpointManager.CreateJson` supplies type information for declarative workflow +checkpoint state. If checkpoint state also contains application-defined payloads, +clone those options and append the application's resolver as shown below; adding +the resolver to checkpoint options does not automatically change the initial input. + +### Registering user-defined checkpoint types + +For custom `ActionExecutorResult.Result` payloads or other user-defined values +that are persisted in workflow checkpoint state, clone `Default` and append your +own resolver: ```csharp JsonSerializerOptions options = new(DeclarativeWorkflowJsonOptions.Default);