Skip to content

.NET: fix: Ensure DeclarativeActionCompletedEvent is raised for every action#6780

Open
PratikWayase wants to merge 1 commit into
microsoft:mainfrom
PratikWayase:fix/3743-declarative-action-completed-event
Open

.NET: fix: Ensure DeclarativeActionCompletedEvent is raised for every action#6780
PratikWayase wants to merge 1 commit into
microsoft:mainfrom
PratikWayase:fix/3743-declarative-action-completed-event

Conversation

@PratikWayase

Copy link
Copy Markdown

PR Description


Motivation & Context

  1. Why is this change required? The DeclarativeActionCompletedEvent was not being raised for parent actions when terminal actions (Goto, EndDialog, etc.) bypassed the normal workflow flow. This broke the contract that every started action must emit a completion event.

  2. What problem does it solve? When a GotoAction or other terminal action is nested inside a non-discrete action (like ConditionGroup or Foreach), the parent's completion event was skipped because the terminal action created a direct link to its target, bypassing the parent's Post step where the completion event is raised.

  3. What scenario does it contribute to? This is critical for workflow designers and debuggers that rely on completion events to track action execution state. Without this fix, the workflow designer would show incomplete/incorrect action state for workflows using goto/end actions inside conditions or loops.

  4. If it fixes an open issue, please link to the issue below. Fixes .NET Workflows - Ensure DeclarativeActionCompletedEvent is raised for every action #3743


Description & Review Guide

  • What are the major changes?

    • Added LocateNonDiscreteAncestors<T>() method to WorkflowModel to find parent actions that need completion events
    • Added InjectParentCompletionEvents() helper method to WorkflowActionVisitor that inserts intermediate completion steps in the workflow graph
    • Updated Visit() methods for GotoAction, BreakLoop, ContinueLoop, EndDialog, EndConversation, CancelAllDialogs, and CancelDialog to use the new helper
    • Added regression test GotoInsideCondition_RaisesParentCompletionEventAsync
  • What is the impact of these changes?

    • Before: GotoAction inside a ConditionGroup → skips parent completion event
    • After: GotoAction inside a ConditionGroup → injects completion step → parent completion event is raised
    • Existing workflow behavior is unchanged; only the event emission is fixed
    • One existing test (CaseInsensitive.yaml) was updated to expect the now-correct completion count
  • What do you want reviewers to focus on?

    • The LocateNonDiscreteAncestors method uses type name matching to identify non-discrete actions. Consider if there's a more robust approach.
    • The InjectParentCompletionEvents method handles the stopBeforeParentId parameter for loop control flow (BreakLoop/ContinueLoop) - verify this logic is correct
    • Verify the regression test adequately covers the bug scenario

Related Issue

Fixes #3743


Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI review requested due to automatic review settings June 27, 2026 10:22
@moonbox3 moonbox3 added .NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows labels Jun 27, 2026
@github-actions github-actions Bot changed the title fix: Ensure DeclarativeActionCompletedEvent is raised for every action .NET: fix: Ensure DeclarativeActionCompletedEvent is raised for every action Jun 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the .NET declarative workflow graph construction so terminal actions (e.g., GotoAction, loop control, and end/cancel actions) still trigger DeclarativeActionCompletedEvent for non-discrete parent actions whose completion is normally raised in their Post step.

Changes:

  • Adds WorkflowModel.LocateNonDiscreteAncestors<T>() to find non-discrete ancestor executors that require explicit completion emission when control flow bypasses Post.
  • Introduces completion-step injection for terminal transitions (and updates several terminal-action visitors to use the injected completion chain).
  • Adds a regression workflow (GotoInsideCondition.yaml) and a unit test asserting the parent ConditionGroup completion event is emitted; updates CaseInsensitive.yaml expected completion count.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs Injects ancestor completion steps for terminal transitions and adds helper logic to patch missing completion events.
dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowModel.cs Adds ancestor-walk helper to locate non-discrete parents that rely on Post-step completion emission.
dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs Adds regression test for #3743 and updates one expected completion count.
dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/GotoInsideCondition.yaml New workflow fixture reproducing GotoAction inside a ConditionGroup.

Comment on lines +642 to +663
string currentId = terminalActionId;

foreach (var ancestor in nonDiscreteAncestors)
{
if (stopBeforeParentId is not null &&
string.Equals(ancestor.Id, stopBeforeParentId, StringComparison.Ordinal))
{
break;
}

string completionId = $"{terminalActionId}_Complete_{ancestor.Id}";
var completionStep = new DelegateActionExecutor(
completionId,
this._workflowState,
async (context, _, ct) => await context.RaiseCompletionEventAsync(ancestor.Model, ct).ConfigureAwait(false),
emitResult: false);

this._workflowModel.AddNode(completionStep, ancestor.ParentId);
this._workflowModel.AddLink(currentId, completionId);

currentId = completionId;
}
Comment on lines +322 to +323
this._workflowModel.AddNode(completionStep, ancestor.ParentId);
this._workflowModel.AddLink(currentId, completionId);
Comment on lines +353 to +354
this._workflowModel.AddNode(completionStep, ancestor.ParentId);
this._workflowModel.AddLink(currentId, completionId);
Comment on lines +384 to +385
this._workflowModel.AddNode(completionStep, ancestor.ParentId);
this._workflowModel.AddLink(currentId, completionId);
Comment on lines +415 to +416
this._workflowModel.AddNode(completionStep, ancestor.ParentId);
this._workflowModel.AddLink(currentId, completionId);
Comment on lines +174 to +183
Type actionType = nonDiscreteAction.GetType();
Type? baseType = actionType.BaseType;

// ConditionGroupExecutor and ForeachExecutor are non-discrete
// They have IsDiscreteAction = false and raise completion in their Post step
bool isNonDiscrete = actionType.Name.Contains("ConditionGroupExecutor") ||
actionType.Name.Contains("ForeachExecutor") ||
(baseType is not null && (baseType.Name.Contains("ConditionGroupExecutor") ||
baseType.Name.Contains("ForeachExecutor")));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET Workflows - Ensure DeclarativeActionCompletedEvent is raised for every action

3 participants