Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,12 +17,12 @@ namespace Demo.Workflows.Declarative.AotCheckpointing;

/// <summary>
/// Demonstrates JSON checkpointing of a declarative workflow under reflection-disabled
/// <see cref="System.Text.Json.JsonSerializer"/> (the AOT / trim-aggressive constraint set
/// <see cref="JsonSerializer"/> (the AOT / trim-aggressive constraint set
/// via <c>JsonSerializerIsReflectionEnabledByDefault=false</c> in the csproj).
/// </summary>
/// <remarks>
/// The key call is <see cref="CheckpointManager.CreateJson(ICheckpointStore{System.Text.Json.JsonElement}, System.Text.Json.JsonSerializerOptions?)"/>
/// with <see cref="DeclarativeWorkflowJsonOptions.Default"/>. Drop the options argument to observe the AOT failure. See README.
/// The key call is <see cref="CheckpointManager.CreateJson"/> with
/// <see cref="DeclarativeWorkflowJsonOptions.Default"/>. Drop the options argument to observe the AOT failure. See README.
/// </remarks>
internal sealed class Program
{
Expand All @@ -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<string>(workflowPath, options);
return DeclarativeWorkflowBuilder.Build<WorkflowInput>(workflowPath, options, TransformInput);
}

DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-HHmmss-ff}"));
Expand Down Expand Up @@ -74,7 +76,10 @@ Workflow CreateWorkflow()
}
}

private static async Task<List<CheckpointInfo>> 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<List<CheckpointInfo>> RunAndStreamAsync(Workflow workflow, WorkflowInput input, CheckpointManager checkpointManager)
{
StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input, checkpointManager).ConfigureAwait(false);
return await DrainAsync(run).ConfigureAwait(false);
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkflowInput>(
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);
Expand Down
Loading