@@ -147,7 +147,7 @@ from i in Enumerable.Range(1, numAgents)
147147 for ( int iter = 0 ; iter < 3 ; iter ++ )
148148 {
149149 const string UserInput = "abc" ;
150- ( string updateText , List < ChatMessage > ? result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , UserInput ) ] ) ;
150+ ( string updateText , List < ChatMessage > ? result , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , UserInput ) ] ) ;
151151
152152 Assert . NotNull ( result ) ;
153153 Assert . Equal ( numAgents + 1 , result . Count ) ;
@@ -225,7 +225,7 @@ public async Task BuildConcurrent_AgentsRunInParallelAsync()
225225 barrier . Value = new TaskCompletionSource < bool > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
226226 remaining . Value = 2 ;
227227
228- ( string updateText , List < ChatMessage > ? result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
228+ ( string updateText , List < ChatMessage > ? result , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
229229 Assert . NotEmpty ( updateText ) ;
230230 Assert . NotNull ( result ) ;
231231
@@ -258,7 +258,7 @@ public async Task Handoffs_NoTransfers_ResponseServedByOriginalAgentAsync()
258258 } ) , description : "nop" ) )
259259 . Build ( ) ;
260260
261- ( string updateText , List < ChatMessage > ? result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
261+ ( string updateText , List < ChatMessage > ? result , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
262262
263263 Assert . Equal ( "Hello from agent1" , updateText ) ;
264264 Assert . NotNull ( result ) ;
@@ -296,7 +296,7 @@ public async Task Handoffs_OneTransfer_ResponseServedBySecondAgentAsync()
296296 . WithHandoff ( initialAgent , nextAgent )
297297 . Build ( ) ;
298298
299- ( string updateText , List < ChatMessage > ? result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
299+ ( string updateText , List < ChatMessage > ? result , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
300300
301301 Assert . Equal ( "Hello from agent2" , updateText ) ;
302302 Assert . NotNull ( result ) ;
@@ -406,7 +406,7 @@ public async Task Handoffs_TwoTransfers_HandoffTargetsDoNotReceiveHandoffFunctio
406406 . WithHandoff ( secondAgent , thirdAgent )
407407 . Build ( ) ;
408408
409- ( string updateText , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
409+ ( string updateText , _ , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
410410
411411 Assert . Contains ( "Hello from agent3" , updateText ) ;
412412
@@ -604,7 +604,7 @@ public async Task Handoffs_TwoTransfers_ResponseServedByThirdAgentAsync()
604604 . WithHandoff ( secondAgent , thirdAgent )
605605 . Build ( ) ;
606606
607- ( string updateText , List < ChatMessage > ? result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
607+ ( string updateText , List < ChatMessage > ? result , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "abc" ) ] ) ;
608608
609609 Assert . Equal ( "Hello from agent3" , updateText ) ;
610610 Assert . NotNull ( result ) ;
@@ -651,7 +651,7 @@ public async Task BuildGroupChat_AgentsRunInOrderAsync(int maxIterations)
651651 for ( int iter = 0 ; iter < 3 ; iter ++ )
652652 {
653653 const string UserInput = "abc" ;
654- ( string updateText , List < ChatMessage > ? result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , UserInput ) ] ) ;
654+ ( string updateText , List < ChatMessage > ? result , _ ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , UserInput ) ] ) ;
655655
656656 Assert . NotNull ( result ) ;
657657 Assert . Equal ( maxIterations + 1 , result . Count ) ;
@@ -705,17 +705,15 @@ public async Task Handoffs_ReturnToPrevious_DisabledByDefault_SecondTurnRoutesVi
705705 . WithHandoff ( coordinator , specialist )
706706 . Build ( ) ;
707707
708- var environment = InProcessExecution . Lockstep ;
709- string sessionId = Guid . NewGuid ( ) . ToString ( "N" ) ;
708+ CheckpointManager checkpointManager = CheckpointManager . CreateInMemory ( ) ;
709+ const ExecutionEnvironment Environment = ExecutionEnvironment . InProcess_Lockstep ;
710710
711711 // Turn 1: coordinator hands off to specialist
712- ( _ , List < ChatMessage > ? turn1Result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "book an appointment" ) ] , environment , sessionId ) ;
712+ WorkflowRunResult result = await RunWorkflowCheckpointedAsync ( workflow , [ new ChatMessage ( ChatRole . User , "book an appointment" ) ] , Environment , checkpointManager ) ;
713713 Assert . Equal ( 1 , coordinatorCallCount ) ;
714714
715715 // Turn 2: without ReturnToPrevious, coordinator should be invoked again
716- Assert . NotNull ( turn1Result ) ;
717- turn1Result . Add ( new ChatMessage ( ChatRole . User , "my id is 12345" ) ) ;
718- _ = await RunWorkflowAsync ( workflow , turn1Result , environment , sessionId ) ;
716+ _ = await RunWorkflowCheckpointedAsync ( workflow , [ new ChatMessage ( ChatRole . User , "my id is 12345" ) ] , Environment , checkpointManager , result . LastCheckpoint ) ;
719717 Assert . Equal ( 2 , coordinatorCallCount ) ;
720718 }
721719
@@ -744,18 +742,16 @@ public async Task Handoffs_ReturnToPrevious_Enabled_SecondTurnRoutesDirectlyToSp
744742 . EnableReturnToPrevious ( )
745743 . Build ( ) ;
746744
747- var environment = InProcessExecution . Lockstep ;
748- string sessionId = Guid . NewGuid ( ) . ToString ( "N" ) ;
745+ CheckpointManager checkpointManager = CheckpointManager . CreateInMemory ( ) ;
746+ const ExecutionEnvironment Environment = ExecutionEnvironment . InProcess_Lockstep ;
749747
750748 // Turn 1: coordinator hands off to specialist
751- ( _ , List < ChatMessage > ? turn1Result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "book an appointment" ) ] , environment , sessionId ) ;
749+ WorkflowRunResult result = await RunWorkflowCheckpointedAsync ( workflow , [ new ChatMessage ( ChatRole . User , "book an appointment" ) ] , Environment , checkpointManager ) ;
752750 Assert . Equal ( 1 , coordinatorCallCount ) ;
753751 Assert . Equal ( 1 , specialistCallCount ) ;
754752
755753 // Turn 2: with ReturnToPrevious, specialist should be invoked directly, coordinator should NOT be called again
756- Assert . NotNull ( turn1Result ) ;
757- turn1Result . Add ( new ChatMessage ( ChatRole . User , "my id is 12345" ) ) ;
758- _ = await RunWorkflowAsync ( workflow , turn1Result , environment , sessionId ) ;
754+ _ = await RunWorkflowCheckpointedAsync ( workflow , [ new ChatMessage ( ChatRole . User , "my id is 12345" ) ] , Environment , checkpointManager , result . LastCheckpoint ) ;
759755 Assert . Equal ( 1 , coordinatorCallCount ) ; // coordinator NOT called again
760756 Assert . Equal ( 2 , specialistCallCount ) ; // specialist called again
761757 }
@@ -782,11 +778,8 @@ public async Task Handoffs_ReturnToPrevious_Enabled_BeforeAnyHandoff_RoutesViaIn
782778 . EnableReturnToPrevious ( )
783779 . Build ( ) ;
784780
785- var environment = InProcessExecution . Lockstep ;
786- string sessionId = Guid . NewGuid ( ) . ToString ( "N" ) ;
787-
788781 // First turn with no prior handoff: should route to initial (coordinator) agent
789- _ = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "hello" ) ] , environment , sessionId ) ;
782+ _ = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "hello" ) ] ) ;
790783 Assert . Equal ( 1 , coordinatorCallCount ) ;
791784 }
792785
@@ -825,81 +818,73 @@ public async Task Handoffs_ReturnToPrevious_Enabled_AfterHandoffBackToCoordinato
825818 . EnableReturnToPrevious ( )
826819 . Build ( ) ;
827820
828- var environment = InProcessExecution . Lockstep ;
829- string sessionId = Guid . NewGuid ( ) . ToString ( "N" ) ;
821+ CheckpointManager checkpointManager = CheckpointManager . CreateInMemory ( ) ;
822+ const ExecutionEnvironment Environment = ExecutionEnvironment . InProcess_Lockstep ;
830823
831824 // Turn 1: coordinator → specialist → coordinator (specialist hands back)
832- ( _ , List < ChatMessage > ? turn1Result ) = await RunWorkflowAsync ( workflow , [ new ChatMessage ( ChatRole . User , "book an appointment" ) ] , environment , sessionId ) ;
825+ WorkflowRunResult result = await RunWorkflowCheckpointedAsync ( workflow , [ new ChatMessage ( ChatRole . User , "book an appointment" ) ] , Environment , checkpointManager ) ;
833826 Assert . Equal ( 2 , coordinatorCallCount ) ; // called twice: initial handoff + receiving handback
834827 Assert . Equal ( 1 , specialistCallCount ) ; // specialist called once, then handed back
835828
836829 // Turn 2: after handoff back to coordinator, should route to coordinator (not specialist)
837- Assert . NotNull ( turn1Result ) ;
838- turn1Result . Add ( new ChatMessage ( ChatRole . User , "never mind" ) ) ;
839- _ = await RunWorkflowAsync ( workflow , turn1Result , environment , sessionId ) ;
830+ _ = await RunWorkflowCheckpointedAsync ( workflow , [ new ChatMessage ( ChatRole . User , "never mind" ) ] , Environment , checkpointManager , result . LastCheckpoint ) ;
840831 Assert . Equal ( 3 , coordinatorCallCount ) ; // coordinator called again on turn 2
841832 Assert . Equal ( 1 , specialistCallCount ) ; // specialist NOT called
842833 }
843834
844- private static async Task < ( string UpdateText , List < ChatMessage > ? Result ) > RunWorkflowAsync (
845- Workflow workflow , List < ChatMessage > input , InProcessExecutionEnvironment environment , string ? sessionId = null )
846- {
847- StringBuilder sb = new ( ) ;
848-
849- await using StreamingRun run = await environment . RunStreamingAsync ( workflow , input , sessionId ) ;
850- await run . TrySendMessageAsync ( new TurnToken ( emitEvents : true ) ) ;
835+ private sealed record WorkflowRunResult ( string UpdateText , List < ChatMessage > ? Result , CheckpointInfo ? LastCheckpoint ) ;
851836
852- WorkflowOutputEvent ? output = null ;
853- await foreach ( WorkflowEvent evt in run . WatchStreamAsync ( ) . ConfigureAwait ( false ) )
854- {
855- if ( evt is AgentResponseUpdateEvent executorComplete )
856- {
857- sb . Append ( executorComplete . Data ) ;
858- }
859- else if ( evt is WorkflowOutputEvent e )
860- {
861- output = e ;
862- break ;
863- }
864- else if ( evt is WorkflowErrorEvent errorEvent )
865- {
866- Assert . Fail ( $ "Workflow execution failed with error: { errorEvent . Exception } ") ;
867- }
868- }
837+ private static Task < WorkflowRunResult > RunWorkflowCheckpointedAsync (
838+ Workflow workflow , List < ChatMessage > input , ExecutionEnvironment executionEnvironment , CheckpointManager checkpointManager , CheckpointInfo ? fromCheckpoint = null )
839+ {
840+ InProcessExecutionEnvironment environment = executionEnvironment . ToWorkflowExecutionEnvironment ( )
841+ . WithCheckpointing ( checkpointManager ) ;
869842
870- return ( sb . ToString ( ) , output ? . As < List < ChatMessage > > ( ) ) ;
843+ return RunWorkflowCheckpointedAsync ( workflow , input , environment , fromCheckpoint ) ;
871844 }
872845
873- private static async Task < ( string UpdateText , List < ChatMessage > ? Result ) > RunWorkflowAsync (
874- Workflow workflow , List < ChatMessage > input , ExecutionEnvironment executionEnvironment = ExecutionEnvironment . InProcess_Lockstep )
846+ private static async Task < WorkflowRunResult > RunWorkflowCheckpointedAsync (
847+ Workflow workflow , List < ChatMessage > input , InProcessExecutionEnvironment environment , CheckpointInfo ? fromCheckpoint = null )
875848 {
876- StringBuilder sb = new ( ) ;
849+ await using StreamingRun run =
850+ fromCheckpoint != null ? await environment . ResumeStreamingAsync ( workflow , fromCheckpoint )
851+ : await environment . OpenStreamingAsync ( workflow ) ;
877852
878- InProcessExecutionEnvironment environment = executionEnvironment . ToWorkflowExecutionEnvironment ( ) ;
879- await using StreamingRun run = await environment . RunStreamingAsync ( workflow , input ) ;
853+ await run . TrySendMessageAsync ( input ) ;
880854 await run . TrySendMessageAsync ( new TurnToken ( emitEvents : true ) ) ;
881855
856+ StringBuilder sb = new ( ) ;
882857 WorkflowOutputEvent ? output = null ;
858+ CheckpointInfo ? lastCheckpoint = null ;
883859 await foreach ( WorkflowEvent evt in run . WatchStreamAsync ( ) . ConfigureAwait ( false ) )
884860 {
885- if ( evt is AgentResponseUpdateEvent executorComplete )
886- {
887- sb . Append ( executorComplete . Data ) ;
888- }
889- else if ( evt is WorkflowOutputEvent e )
861+ switch ( evt )
890862 {
891- output = e ;
892- break ;
893- }
894- else if ( evt is WorkflowErrorEvent errorEvent )
895- {
896- Assert . Fail ( $ "Workflow execution failed with error: { errorEvent . Exception } ") ;
863+ case AgentResponseUpdateEvent executorComplete :
864+ sb . Append ( executorComplete . Data ) ;
865+ break ;
866+
867+ case WorkflowOutputEvent e :
868+ output = e ;
869+ break ;
870+
871+ case WorkflowErrorEvent errorEvent :
872+ Assert . Fail ( $ "Workflow execution failed with error: { errorEvent . Exception } ") ;
873+ break ;
874+
875+ case SuperStepCompletedEvent stepCompleted :
876+ lastCheckpoint = stepCompleted . CompletionInfo ? . Checkpoint ;
877+ break ;
897878 }
898879 }
899880
900- return ( sb . ToString ( ) , output ? . As < List < ChatMessage > > ( ) ) ;
881+ return new ( sb . ToString ( ) , output ? . As < List < ChatMessage > > ( ) , lastCheckpoint ) ;
901882 }
902883
884+ private static Task < WorkflowRunResult > RunWorkflowAsync (
885+ Workflow workflow , List < ChatMessage > input , ExecutionEnvironment executionEnvironment = ExecutionEnvironment . InProcess_Lockstep )
886+ => RunWorkflowCheckpointedAsync ( workflow , input , executionEnvironment . ToWorkflowExecutionEnvironment ( ) ) ;
887+
903888 private sealed class DoubleEchoAgentWithBarrier ( string name , StrongBox < TaskCompletionSource < bool > > barrier , StrongBox < int > remaining ) : DoubleEchoAgent ( name )
904889 {
905890 protected override async IAsyncEnumerable < AgentResponseUpdate > RunCoreStreamingAsync (
0 commit comments