From e11ec9d9411860f67fb1c9bf619c68997230f934 Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Mon, 29 Sep 2025 09:40:15 -0700
Subject: [PATCH] .NET Workflows - Declarative Workflow Integration Tests
(#956)
* Checkpoint / 100% Pass
* Checkpoint: ActionExecutorResult
* Update dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Namespace
* Sync updates
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../DeclarativeWorkflowBuilder.cs | 7 +-
.../Extensions/AgentProviderExtensions.cs | 62 +++++++
.../Extensions/ChatMessageExtensions.cs | 28 ++-
.../Extensions/FormulaValueExtensions.cs | 41 ++++-
.../Extensions/IWorkflowContextExtensions.cs | 22 +--
.../Interpreter/ActionExecutorResult.cs | 35 ++++
.../Interpreter/DeclarativeActionExecutor.cs | 7 +-
.../Interpreter/DeclarativeWorkflowContext.cs | 35 +++-
.../DeclarativeWorkflowExecutor.cs | 12 +-
.../Interpreter/DelegateActionExecutor.cs | 6 +-
.../Interpreter/ExecutorResultMessage.cs | 16 --
.../Interpreter/WorkflowActionVisitor.cs | 151 +++++++++-------
.../AddConversationMessageExecutor.cs | 2 -
.../ObjectModel/ConditionGroupExecutor.cs | 6 +-
.../CopyConversationMessagesExecutor.cs | 10 +-
.../ObjectModel/GotoExecutor.cs | 19 --
.../ObjectModel/InvokeAzureAgentExecutor.cs | 52 +-----
.../ObjectModel/QuestionExecutor.cs | 8 +-
.../RetrieveConversationMessageExecutor.cs | 1 -
.../RetrieveConversationMessagesExecutor.cs | 1 -
.../PowerFx/Functions/UserMessage.cs | 2 +
.../PowerFx/SystemScope.cs | 10 +-
.../PowerFx/TypeSchema.cs | 17 ++
.../{BasicAgent.yaml => TestAgent.yaml} | 2 +-
.../AzureAgentProviderTest.cs | 2 +-
.../DeclarativeWorkflowTest.cs | 95 ++--------
.../Framework/AgentFactory.cs | 83 ++++++---
.../Framework/AgentFixture.cs | 20 ---
.../Framework/IntegrationTest.cs | 54 ++++++
.../Framework/Testcase.cs | 31 +++-
.../Framework/WorkflowEvents.cs | 4 +
.../Framework/WorkflowTest.cs | 169 +++++++++++++++---
.../Testcases/ConversationMessages.json | 26 +++
.../Testcases/DeepResearch.json | 40 +++++
.../Testcases/HumanInLoop.json | 20 +++
.../Testcases/InvokeAgent.json | 10 +-
.../Testcases/Marketing.json | 23 +++
.../Testcases/MathChat.json | 29 +++
.../Testcases/SendActivity.json | 14 +-
...essages.yaml => ConversationMessages.yaml} | 0
.../Workflows/InvokeAgent.yaml | 2 +-
.../DeclarativeWorkflowTest.cs | 38 ++--
.../ObjectModel/WorkflowActionExecutorTest.cs | 2 +-
.../WorkflowTest.cs | 10 ++
.../Workflows/LoopBreak.yaml | 14 +-
.../Workflows/LoopContinue.yaml | 14 +-
.../Workflows/LoopEach.yaml | 11 +-
workflow-samples/DeepResearch.yaml | 26 ++-
workflow-samples/MathChat.yaml | 10 +-
49 files changed, 870 insertions(+), 429 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs
delete mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs
delete mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs
rename dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/{BasicAgent.yaml => TestAgent.yaml} (73%)
delete mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/ConversationMessages.json
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/DeepResearch.json
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/HumanInLoop.json
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json
rename dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/{AddMessages.yaml => ConversationMessages.yaml} (100%)
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
index 2e2a6460af..10ac05f003 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
@@ -17,7 +17,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
public static class DeclarativeWorkflowBuilder
{
///
- /// Builds a workflow from the provided YAML definition.
+ /// Builder for converting a Foundry workflow object-model YAML definition into a process.
///
/// The type of the input message
/// The path to the workflow.
@@ -55,18 +55,19 @@ public static class DeclarativeWorkflowBuilder
throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(Workflow)}.");
}
- string rootId = WorkflowActionVisitor.Steps.Root(workflowElement.BeginDialog?.Id.Value);
+ string rootId = WorkflowActionVisitor.Steps.Root(workflowElement);
WorkflowFormulaState state = new(options.CreateRecalcEngine());
state.Initialize(workflowElement.WrapWithBot(), options.Configuration);
DeclarativeWorkflowExecutor rootExecutor =
new(rootId,
+ options.AgentProvider,
state,
message => inputTransform?.Invoke(message) ?? DefaultTransform(message));
WorkflowActionVisitor visitor = new(rootExecutor, state, options);
WorkflowElementWalker walker = new(visitor);
- walker.Visit(rootElement);
+ walker.Visit(workflowElement);
return visitor.Complete();
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs
new file mode 100644
index 0000000000..e5b8068b1b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class AgentProviderExtensions
+{
+ public static async IAsyncEnumerable InvokeAgentAsync(
+ this WorkflowAgentProvider agentProvider,
+ string executorId,
+ IWorkflowContext context,
+ string agentName,
+ string? conversationId,
+ bool autoSend,
+ string? additionalInstructions = null,
+ IEnumerable? inputMessages = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false);
+
+ ChatClientAgentRunOptions options =
+ new(
+ new ChatOptions()
+ {
+ Instructions = additionalInstructions,
+ });
+
+ AgentThread agentThread = conversationId is not null && agent is ChatClientAgent chatClientAgent ? chatClientAgent.GetNewThread(conversationId) : agent.GetNewThread();
+ IAsyncEnumerable agentUpdates =
+ inputMessages is not null ?
+ agent.RunStreamingAsync([.. inputMessages], agentThread, options, cancellationToken) :
+ agent.RunStreamingAsync(agentThread, options, cancellationToken);
+
+ await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false))
+ {
+ await AssignConversationIdAsync(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false);
+
+ if (autoSend)
+ {
+ await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update)).ConfigureAwait(false);
+ }
+
+ yield return update;
+ }
+
+ async ValueTask AssignConversationIdAsync(string? assignValue)
+ {
+ if (assignValue is not null && conversationId is null)
+ {
+ conversationId = assignValue;
+
+ await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
index 66f7254d6f..9eec9796ef 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
@@ -16,10 +16,15 @@ internal static class ChatMessageExtensions
FormulaValue.NewRecordFromFields(message.GetMessageFields());
public static TableValue ToTable(this IEnumerable messages) =>
- FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord()));
+ FormulaValue.NewTable(TypeSchema.Message.MessageRecordType, messages.Select(message => message.ToRecord()));
- public static IEnumerable ToChatMessages(this DataValue messages)
+ public static IEnumerable? ToChatMessages(this DataValue? messages)
{
+ if (messages is null || messages is BlankDataValue)
+ {
+ return null;
+ }
+
if (messages is TableDataValue table)
{
return table.ToChatMessages();
@@ -35,7 +40,7 @@ internal static class ChatMessageExtensions
return [text.ToChatMessage()];
}
- return [];
+ return null;
}
public static IEnumerable ToChatMessages(this TableDataValue messages)
@@ -185,10 +190,11 @@ internal static class ChatMessageExtensions
private static IEnumerable GetMessageFields(this ChatMessage message)
{
+ yield return new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Id, message.MessageId.ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Role, message.Role.Value.ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Author, message.AuthorName.ToFormula());
- yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(s_contentRecordType, message.GetContentRecords()));
+ yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(TypeSchema.Message.ContentRecordType, message.GetContentRecords()));
yield return new NamedValue(TypeSchema.Message.Fields.Text, message.Text.ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Metadata, message.AdditionalProperties.ToRecord());
}
@@ -228,18 +234,4 @@ internal static class ChatMessageExtensions
}
}
}
-
- private static readonly RecordType s_contentRecordType =
- RecordType.Empty()
- .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String)
- .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String);
-
- private static readonly RecordType s_messageRecordType =
- RecordType.Empty()
- .Add(TypeSchema.Message.Fields.Id, FormulaType.String)
- .Add(TypeSchema.Message.Fields.Role, FormulaType.String)
- .Add(TypeSchema.Message.Fields.Author, FormulaType.String)
- .Add(TypeSchema.Message.Fields.Content, s_contentRecordType.ToTable())
- .Add(TypeSchema.Message.Fields.Text, FormulaType.String)
- .Add(TypeSchema.Message.Fields.Metadata, RecordType.Empty());
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
index cc4761bf07..034f4bcbb7 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
@@ -9,7 +9,9 @@ using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Agents.Workflows.Declarative.PowerFx;
+using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
using BlankType = Microsoft.PowerFx.Types.BlankType;
@@ -37,6 +39,7 @@ internal static class FormulaValueExtensions
DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue),
DateTime datetimeValue => FormulaValue.New(datetimeValue),
TimeSpan timeValue => FormulaValue.New(timeValue),
+ ChatMessage chatMessage => chatMessage.ToRecord(),
ExpandoObject expandoValue => expandoValue.ToRecord(),
object when value is IDictionary dictionaryValue => dictionaryValue.ToRecord(),
object when value is IEnumerable tableValue => tableValue.ToTable(),
@@ -209,10 +212,40 @@ internal static class FormulaValueExtensions
return TableType.Empty();
}
- private static TableValue ToTable(this IEnumerable value) =>
- FormulaValue.NewTable(
- value.ToTableType().ToRecord(),
- [.. value.OfType().Select(element => element.ToRecord())]);
+ private static TableValue ToTable(this IEnumerable value)
+ {
+ Type? elementType = value.GetType().GetElementType();
+ if (elementType is null || elementType == typeof(object))
+ {
+ IEnumerator enumerator = value.GetEnumerator();
+ if (enumerator.MoveNext())
+ {
+ elementType = enumerator.Current?.GetType();
+ }
+ }
+
+ return
+ elementType switch
+ {
+ null => FormulaValue.NewTable(RecordType.EmptySealed(), []),
+ _ when elementType == typeof(ExpandoObject) =>
+ FormulaValue.NewTable(
+ value.ToTableType().ToRecord(),
+ [.. value.OfType().Select(element => element.ToRecord())]),
+ _ when typeof(ChatMessage).IsAssignableFrom(elementType) =>
+ FormulaValue.NewTable(
+ TypeSchema.Message.MessageRecordType,
+ [.. value.OfType().Select(message => message.ToRecord())]),
+ _ when typeof(IDictionary).IsAssignableFrom(elementType) => value.ToTableOfRecords(),
+ _ => throw new DeclarativeModelException($"Unsupported element type: {elementType.Name}"),
+ };
+ }
+
+ private static TableValue ToTableOfRecords(this IEnumerable list)
+ {
+ RecordValue[] elements = [.. list.OfType().Select(table => table.ToRecord())];
+ return FormulaValue.NewTable(elements.First().Type, elements);
+ }
private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
index ee1b5697c2..d85bf373a7 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Interpreter;
@@ -20,7 +19,7 @@ internal static class IWorkflowContextExtensions
context.AddEventAsync(new DeclarativeActionCompletedEvent(action));
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
- context.SendMessageAsync(new ExecutorResultMessage(id, result));
+ context.SendMessageAsync(new ActionExecutorResult(id, result));
public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias));
@@ -47,25 +46,6 @@ internal static class IWorkflowContextExtensions
await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false);
}
- // Ensure "System.Conversation.Id" and "System.ConversationId" are properly initialized when referenced.
- public static async ValueTask EnsureWorkflowConversationAsync(this IWorkflowContext context, WorkflowAgentProvider agentProvider, StringExpression expression, CancellationToken cancellationToken)
- {
- if (expression.IsVariableReference &&
- expression.VariableReference.IsVariableReferenceWithScope(VariableNamespace.System, out string? variableName))
- {
- if (string.Equals(variableName, SystemScope.Names.Conversation, StringComparison.Ordinal) ||
- string.Equals(variableName, SystemScope.Names.ConversationId, StringComparison.Ordinal))
- {
- FormulaValue variableValue = context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System);
- if (variableValue is BlankValue)
- {
- string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
- await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
- }
- }
- }
- }
-
private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context)
{
if (context is not DeclarativeWorkflowContext declarativeContext)
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs
new file mode 100644
index 0000000000..c75f82195c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
+
+///
+/// Message sent to initiate a transition to another .
+///
+public sealed record class ActionExecutorResult
+{
+ ///
+ /// The identifier of the that produced this message.
+ ///
+ public string ExecutorId { get; }
+
+ ///
+ /// The result of the action, if any provided.
+ ///
+ public object? Result { get; }
+
+ internal ActionExecutorResult(string executorId, object? result = null)
+ {
+ this.ExecutorId = executorId;
+ this.Result = result;
+ }
+
+ internal static ActionExecutorResult ThrowIfNot(object? message)
+ {
+ if (message is not ActionExecutorResult executorMessage)
+ {
+ throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ActionExecutorResult)})");
+ }
+
+ return executorMessage;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
index c870b77138..f323f7cc57 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -21,7 +21,7 @@ internal abstract class DeclarativeActionExecutor(TAction model, Workfl
public new TAction Model => (TAction)base.Model;
}
-internal abstract class DeclarativeActionExecutor : Executor
+internal abstract class DeclarativeActionExecutor : Executor
{
private string? _parentId;
private readonly WorkflowFormulaState _state;
@@ -54,7 +54,7 @@ internal abstract class DeclarativeActionExecutor : Executor true;
///
- public override async ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context)
+ public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context)
{
if (this.Model.Disabled)
{
@@ -64,11 +64,10 @@ internal abstract class DeclarativeActionExecutor : Executor
- public ValueTask ReadStateAsync(string key, string? scopeName = null) => this.Source.ReadStateAsync(key, scopeName);
+ public async ValueTask ReadStateAsync(string key, string? scopeName = null)
+ {
+ bool isManagedScope =
+ scopeName is not null && // null scope cannot be managed
+ VariableScopeNames.IsValidName(scopeName);
+
+ return typeof(TValue) switch
+ {
+ // Not a managed scope, just pass through. This is valid when a declarative
+ // workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
+ _ when !isManagedScope => await this.Source.ReadStateAsync(key, scopeName).ConfigureAwait(false),
+ // Retrieve formula values directly from the managed state to avoid conversion.
+ _ when typeof(TValue) == typeof(FormulaValue) => (TValue?)(object?)this.State.Get(key, scopeName),
+ // Retrieve native types from the source context to avoid conversion.
+ _ => await this.Source.ReadStateAsync(key, scopeName).ConfigureAwait(false),
+ };
+ }
///
public ValueTask> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName);
@@ -86,9 +101,8 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
private ValueTask UpdateStateAsync(string key, T? value, string? scopeName, bool allowSystem = true)
{
bool isManagedScope =
- scopeName != null && // null scope cannot be managed
- (ManagedScopes.Contains(scopeName) ||
- (allowSystem && VariableScopeNames.System.Equals(scopeName, StringComparison.Ordinal)));
+ scopeName is not null && // null scope cannot be managed
+ VariableScopeNames.IsValidName(scopeName);
if (!isManagedScope)
{
@@ -97,6 +111,11 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
return this.Source.QueueStateUpdateAsync(key, value, scopeName);
}
+ if (!ManagedScopes.Contains(scopeName!) && !allowSystem)
+ {
+ throw new DeclarativeActionException($"Cannot manage variable definitions in scope: '{scopeName}'.");
+ }
+
return value switch
{
null => QueueEmptyStateAsync(),
@@ -127,19 +146,19 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
ValueTask QueueDataValueStateAsync(DataValue dataValue)
{
- FormulaValue formulaValue = dataValue.ToFormula();
if (isManagedScope)
{
+ FormulaValue formulaValue = dataValue.ToFormula();
this.State.Set(key, formulaValue, scopeName);
}
- return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ return this.Source.QueueStateUpdateAsync(key, dataValue.ToObject(), scopeName);
}
ValueTask QueueNativeStateAsync(object? rawValue)
{
- FormulaValue formulaValue = rawValue.ToFormula();
if (isManagedScope)
{
+ FormulaValue formulaValue = rawValue.ToFormula();
this.State.Set(key, formulaValue, scopeName);
}
return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
index a912258157..b634dc21e3 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
@@ -2,6 +2,7 @@
using System;
using System.Threading.Tasks;
+using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Extensions.AI;
@@ -12,6 +13,7 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
///
internal sealed class DeclarativeWorkflowExecutor(
string workflowId,
+ WorkflowAgentProvider agentProvider,
WorkflowFormulaState state,
Func inputTransform) :
Executor(workflowId)
@@ -22,9 +24,15 @@ internal sealed class DeclarativeWorkflowExecutor(
// No state to restore if we're starting from the beginning.
state.SetInitialized();
+ DeclarativeWorkflowContext declarativeContext = new(context, state);
ChatMessage input = inputTransform.Invoke(message);
- state.SetLastMessage(input);
- await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
+ string conversationId = await agentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
+ await declarativeContext.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
+
+ await agentProvider.CreateMessageAsync(conversationId, input, cancellationToken: default).ConfigureAwait(false);
+ await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
+
+ await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs
index efa4f04643..8f52f53c53 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs
@@ -10,10 +10,10 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
internal delegate ValueTask DelegateAction(IWorkflowContext context, TMessage message, CancellationToken cancellationToken) where TMessage : notnull;
-internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction? action = null, bool emitResult = true)
- : DelegateActionExecutor(actionId, state, action, emitResult)
+internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction? action = null, bool emitResult = true)
+ : DelegateActionExecutor(actionId, state, action, emitResult)
{
- public override ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context)
+ public override ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context)
{
Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}");
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs
deleted file mode 100644
index 99d3a71984..0000000000
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
-
-internal sealed record class ExecutorResultMessage(string ExecutorId, object? Result = null)
-{
- public static ExecutorResultMessage ThrowIfNot(object? message)
- {
- if (message is not ExecutorResultMessage executorMessage)
- {
- throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ExecutorResultMessage)})");
- }
-
- return executorMessage;
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
index c9ebdca9a6..c85d885fa1 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
@@ -17,9 +17,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
internal static class Steps
{
+ public static string Root(AdaptiveDialog action) => $"{action.BeginDialog?.Id.Value ?? DefaultWorkflowId}_{nameof(Root)}";
+
public static string Root(string? actionId = null) => $"{actionId ?? DefaultWorkflowId}_{nameof(Root)}";
public static string Post(string actionId) => $"{actionId}_{nameof(Post)}";
+
+ public static string Restart(string actionId) => $"{actionId}_{nameof(Restart)}";
}
private readonly WorkflowBuilder _workflowBuilder;
@@ -66,17 +70,21 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
// Complete the action scope.
void CompletionHandler()
{
+ // No completion for root scope
if (this._workflowModel.GetDepth(item.Id.Value) > 1)
{
- DelegateAction? action = null;
+ DelegateAction? action = null;
ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId);
if (conditionGroup is not null)
{
action = conditionGroup.DoneAsync;
}
- string completionId = this.ContinuationFor(item.Id.Value, action); // End scope
- this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); // Connect with final action
- this._workflowModel.AddLink(completionId, Steps.Post(parentId)); // Merge with parent scope
+
+ // Define post action for this scope
+ string completionId = this.ContinuationFor(item.Id.Value, action);
+ this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId);
+ // Transition to post action of parent scope
+ this._workflowModel.AddLink(completionId, Steps.Post(parentId));
}
}
}
@@ -85,11 +93,11 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
this.Trace(item);
- ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(item.GetParentId());
+ string parentId = GetParentId(item);
+ ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId);
if (conditionGroup is not null)
{
string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item);
- string parentId = GetParentId(item);
this._workflowModel.AddNode(new DelegateActionExecutor(stepId, this._workflowState), parentId, CompletionHandler);
base.VisitConditionItem(item);
@@ -145,9 +153,12 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
this.Trace(item);
- GotoExecutor action = new(item, this._workflowState);
+ // Represent action with default executor
+ DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);
+ // Transition to target action
this._workflowModel.AddLink(action.Id, item.ActionId.Value);
+ // Define a clean-start to ensure "goto" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
@@ -155,21 +166,28 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
this.Trace(item);
+ // Entry point for loop
ForeachExecutor action = new(item, this._workflowState);
string loopId = ForeachExecutor.Steps.Next(action.Id);
- this.ContinueWith(action, condition: null, CompletionHandler); // Foreach
- this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, action.TakeNextAsync), action.Id); // Loop Increment
- string continuationId = this.ContinuationFor(action.Id, action.ParentId); // Action continuation
+ this.ContinueWith(action, condition: null, CompletionHandler);
+ // Transition to select the next item
+ this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, action.TakeNextAsync), action.Id);
+
+ // Transition to post action if no more items
+ string continuationId = this.ContinuationFor(action.Id, action.ParentId);
this._workflowModel.AddLink(loopId, continuationId, (_) => !action.HasValue);
+ // Transition to start of inner actions if there is a current item
string startId = ForeachExecutor.Steps.Start(action.Id);
this._workflowModel.AddNode(new DelegateActionExecutor(startId, this._workflowState), action.Id);
this._workflowModel.AddLink(loopId, startId, (_) => action.HasValue);
void CompletionHandler()
{
- string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation
+ // Transition to end of inner actions
+ string endActionsId = ForeachExecutor.Steps.End(action.Id);
this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.ResetAsync), action.Id);
+ // Transition to select the next item
this._workflowModel.AddLink(endActionsId, loopId);
}
}
@@ -178,13 +196,18 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
this.Trace(item);
- ForeachExecutor? loopExecutor = this._workflowModel.LocateParent(item.GetParentId());
- if (loopExecutor is not null)
+ // Locate the nearest "Foreach" loop that contains this action
+ ForeachExecutor? loopAction = this._workflowModel.LocateParent(item.GetParentId());
+ // Skip action if its not contained a loop
+ if (loopAction is not null)
{
- DefaultActionExecutor breakLoopExecutor = new(item, this._workflowState);
- this.ContinueWith(breakLoopExecutor);
- this._workflowModel.AddLink(breakLoopExecutor.Id, Steps.Post(loopExecutor.Id));
- this.RestartAfter(breakLoopExecutor.Id, breakLoopExecutor.ParentId);
+ // Represent action with default executor
+ DefaultActionExecutor action = new(item, this._workflowState);
+ this.ContinueWith(action);
+ // Transition to post action
+ this._workflowModel.AddLink(action.Id, Steps.Post(loopAction.Id));
+ // Define a clean-start to ensure "break" is not a source for any edge
+ this.RestartAfter(action.Id, action.ParentId);
}
}
@@ -192,34 +215,21 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
this.Trace(item);
- ForeachExecutor? loopExecutor = this._workflowModel.LocateParent(item.GetParentId());
- if (loopExecutor is not null)
+ // Locate the nearest "Foreach" loop that contains this action
+ ForeachExecutor? loopAction = this._workflowModel.LocateParent(item.GetParentId());
+ // Skip action if its not contained a loop
+ if (loopAction is not null)
{
- DefaultActionExecutor continueLoopExecutor = new(item, this._workflowState);
- this.ContinueWith(continueLoopExecutor);
- this._workflowModel.AddLink(continueLoopExecutor.Id, ForeachExecutor.Steps.Next(loopExecutor.Id));
- this.RestartAfter(continueLoopExecutor.Id, continueLoopExecutor.ParentId);
+ // Represent action with default executor
+ DefaultActionExecutor action = new(item, this._workflowState);
+ this.ContinueWith(action);
+ // Transition to select the next item
+ this._workflowModel.AddLink(action.Id, ForeachExecutor.Steps.Next(loopAction.Id));
+ // Define a clean-start to ensure "continue" is not a source for any edge
+ this.RestartAfter(action.Id, action.ParentId);
}
}
- protected override void Visit(EndConversation item)
- {
- this.Trace(item);
-
- DefaultActionExecutor endExecutor = new(item, this._workflowState);
- this.ContinueWith(endExecutor);
- this.RestartAfter(item.Id.Value, endExecutor.ParentId);
- }
-
- protected override void Visit(EndDialog item)
- {
- this.Trace(item);
-
- DefaultActionExecutor endExecutor = new(item, this._workflowState);
- this.ContinueWith(endExecutor);
- this.RestartAfter(item.Id.Value, endExecutor.ParentId);
- }
-
protected override void Visit(Question item)
{
this.Trace(item);
@@ -228,25 +238,55 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
string actionId = item.GetId();
string postId = Steps.Post(actionId);
- QuestionExecutor questionExecutor = new(item, this._workflowState);
- this.ContinueWith(questionExecutor);
+ // Entry point for question
+ QuestionExecutor action = new(item, this._workflowState);
+ this.ContinueWith(action);
+ // Transition to post action if complete
this._workflowModel.AddLink(actionId, postId, QuestionExecutor.IsComplete);
+ // Perpare for input request if not complete
string prepareId = QuestionExecutor.Steps.Prepare(actionId);
- this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, questionExecutor.PrepareResponseAsync, emitResult: false), parentId, message => !QuestionExecutor.IsComplete(message));
+ this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, action.PrepareResponseAsync, emitResult: false), parentId, message => !QuestionExecutor.IsComplete(message));
+ // Define input action
string inputId = QuestionExecutor.Steps.Input(actionId);
+ //ModeledPort inputPort = new(InputPort.Create(inputId)); // %%% MODELING
InputPort inputPort = InputPort.Create(inputId);
this._workflowModel.AddPort(inputPort, parentId);
this._workflowModel.AddLinkFromPeer(parentId, inputId);
+ // Capture input response
string captureId = QuestionExecutor.Steps.Capture(actionId);
- this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, questionExecutor.CaptureResponseAsync, emitResult: false), parentId);
+ this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), parentId);
- this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, questionExecutor.CompleteAsync), parentId, QuestionExecutor.IsComplete);
+ // Transition to post action if complete
+ this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), parentId, QuestionExecutor.IsComplete);
+ // Transition to prepare action if not complete
this._workflowModel.AddLink(captureId, prepareId, message => !QuestionExecutor.IsComplete(message));
}
+ protected override void Visit(EndDialog item)
+ {
+ this.Trace(item);
+
+ // Represent action with default executor
+ DefaultActionExecutor action = new(item, this._workflowState);
+ this.ContinueWith(action);
+ // Define a clean-start to ensure "end" is not a source for any edge
+ this.RestartAfter(item.Id.Value, action.ParentId);
+ }
+
+ protected override void Visit(EndConversation item)
+ {
+ this.Trace(item);
+
+ // Represent action with default executor
+ DefaultActionExecutor action = new(item, this._workflowState);
+ this.ContinueWith(action);
+ // Define a clean-start to ensure "end" is not a source for any edge
+ this.RestartAfter(action.Id, action.ParentId);
+ }
+
protected override void Visit(CreateConversation item)
{
this.Trace(item);
@@ -354,15 +394,9 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
#region Not supported
- protected override void Visit(AnswerQuestionWithAI item)
- {
- this.NotSupported(item);
- }
+ protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item);
- protected override void Visit(DeleteActivity item)
- {
- this.NotSupported(item);
- }
+ protected override void Visit(DeleteActivity item) => this.NotSupported(item);
protected override void Visit(GetActivityMembers item) => this.NotSupported(item);
@@ -386,10 +420,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
protected override void Visit(AdaptiveCardPrompt item) => this.NotSupported(item);
- protected override void Visit(CSATQuestion item)
- {
- this.NotSupported(item);
- }
+ protected override void Visit(CSATQuestion item) => this.NotSupported(item);
protected override void Visit(OAuthInput item) => this.NotSupported(item);
@@ -452,9 +483,9 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
this._workflowModel.AddLinkFromPeer(parentId, executor.Id, condition);
}
- private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction);
+ private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction);
- private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null)
+ private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null)
{
actionId = Steps.Post(actionId);
this._workflowModel.AddNode(new DelegateActionExecutor(actionId, this._workflowState, stepAction), parentId);
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
index 4944d15552..1e0694a572 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
@@ -18,8 +18,6 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode
protected override async ValueTask