mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Fix declarative resume edge predicates to recognize both direct and PortableValue-wrapped forms after checkpoint restore (#5323)
* Fix declarative workflows edge predicates after checkpoint restore * Update test names to make them clearer and more discoverable. * Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
b03cb324d5
commit
c85d24da44
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
|
||||
/// <summary>
|
||||
@@ -25,6 +27,11 @@ public sealed record class ActionExecutorResult
|
||||
|
||||
internal static ActionExecutorResult ThrowIfNot(object? message)
|
||||
{
|
||||
if (message is PortableValue portableValue && portableValue.IsType(out ActionExecutorResult? unwrapped))
|
||||
{
|
||||
return unwrapped;
|
||||
}
|
||||
|
||||
if (message is not ActionExecutorResult executorMessage)
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ActionExecutorResult)})");
|
||||
|
||||
+4
-2
@@ -27,9 +27,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseA
|
||||
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
|
||||
}
|
||||
|
||||
public static bool RequiresInput(object? message) => message is ExternalInputRequest;
|
||||
public static bool RequiresInput(object? message) =>
|
||||
message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
|
||||
|
||||
public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
|
||||
public static bool RequiresNothing(object? message) =>
|
||||
message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
|
||||
|
||||
private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}");
|
||||
private AzureAgentInput? AgentInput => this.Model.Input;
|
||||
|
||||
+4
-2
@@ -46,12 +46,14 @@ internal sealed class InvokeMcpToolExecutor(
|
||||
/// <summary>
|
||||
/// Determines if the message indicates external input is required.
|
||||
/// </summary>
|
||||
public static bool RequiresInput(object? message) => message is ExternalInputRequest;
|
||||
public static bool RequiresInput(object? message) =>
|
||||
message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the message indicates no external input is required.
|
||||
/// </summary>
|
||||
public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
|
||||
public static bool RequiresNothing(object? message) =>
|
||||
message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool EmitResultEvent => false;
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that edge predicates correctly handle PortableValue-wrapped messages,
|
||||
/// which occur after checkpoint restore (JSON round-trip).
|
||||
/// </summary>
|
||||
public sealed class PortableValuePredicateTests
|
||||
{
|
||||
#region ActionExecutorResult.ThrowIfNot
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithDirectActionExecutorResult_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test-executor");
|
||||
|
||||
// Act
|
||||
ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(result);
|
||||
|
||||
// Assert
|
||||
actual.Should().BeSameAs(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedActionExecutorResult_Unwraps()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test-executor");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act
|
||||
ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(wrapped);
|
||||
|
||||
// Assert
|
||||
actual.ExecutorId.Should().Be("test-executor");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithNonActionExecutorResult_Throws()
|
||||
{
|
||||
// Arrange
|
||||
object message = "not an ActionExecutorResult";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithNull_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedNonResult_Throws()
|
||||
{
|
||||
// Arrange
|
||||
PortableValue wrapped = new("not an ActionExecutorResult");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(wrapped));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InvokeAzureAgentExecutor Predicates
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithDirectExternalInputRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(request).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
PortableValue wrapped = new(request);
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithActionExecutorResult_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(result).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithDirectActionExecutorResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(result).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithExternalInputRequest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(request).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InvokeMcpToolExecutor Predicates
|
||||
|
||||
[Fact]
|
||||
public void InvokeMcpToolExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
PortableValue wrapped = new(request);
|
||||
|
||||
// Act & Assert
|
||||
InvokeMcpToolExecutor.RequiresInput(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeMcpToolExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
InvokeMcpToolExecutor.RequiresNothing(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region QuestionExecutor.IsComplete
|
||||
|
||||
[Fact]
|
||||
public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NullResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange - result with null Result property means "complete"
|
||||
ActionExecutorResult result = new("test", result: null);
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
QuestionExecutor.IsComplete(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NonNullResult_ReturnsFalse()
|
||||
{
|
||||
// Arrange - result with non-null Result property means "not complete"
|
||||
ActionExecutorResult result = new("test", result: true);
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
QuestionExecutor.IsComplete(wrapped).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user