Uh oh!
There was an error while loading. Please reload this page.
.NET: Fix QuestionExecutor looping after GotoAction re-entry in declarative workflows - #5635
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a re-entry bug in the declarative Question action that could cause infinite looping when a workflow uses GotoAction to jump back to a Question after a mismatch, and it re-enables multi-turn integration coverage to prevent regressions.
Changes:
- Fix
QuestionExecutorre-entry behavior by persisting “has executed” state in the correct executor scope and adjustingSkipOnFirstExecutionIfVariableHasValuelogic. - Extend the
ConfirmInputintegration testcase to first mismatch (forcing the loopback path) and then match (completing the workflow), including updated expected action sequences/counts. - Re-enable the previously skipped multi-turn integration test theory.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs | Corrects Question re-entry prompting by fixing _hasExecuted scoping and skip-mode evaluation. |
| dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs | Re-enables multi-turn integration test execution. |
| dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/ConfirmInput.json | Updates the testcase to exercise mismatch + GotoAction loopback + successful retry, with updated validation expectations. |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 91%
✓ Correctness
This PR fixes two cross-scope state issues in QuestionExecutor. First, it corrects the SkipOnFirstExecutionIfVariableHasValue logic from
isValueUndefined && !hasExecutedtoisValueUndefined || hasExecutedPreviously, which properly handles GotoAction re-entry. Second, it resolves the _promptCount cross-scope problem (noted in the prior review) by keeping all _promptCount reads/writes in the CaptureResponseAsync scope and passing the count explicitly to PromptAsync. The _hasExecuted flag is similarly moved so reads and writes happen in the same ExecuteAsync scope. DurableProperty defaults to 0 and DurableProperty defaults to false, so removing the explicit _promptCount initialization in ExecuteAsync is safe. The test changes re-enable previously-skipped multi-turn tests and add a mismatch response to exercise the GotoAction re-prompt path. The changes are logically sound and correctly address the identified bugs.
✓ Security Reliability
This PR fixes cross-scope state bugs in QuestionExecutor by moving
_hasExecutedreads and writes into the same executor scope (ExecuteAsync) and_promptCountreads and writes into the Capture executor scope, passing the count explicitly to PromptAsync. The SkipOnFirstExecutionIfVariableHasValue logic is also corrected fromisValueUndefined && !hasExecuted(which was broken for subsequent executions—always skipping) toisValueUndefined || hasExecutedPreviously(correctly proceding on subsequent executions). The resolved prior review comment about cross-scope _promptCount is addressed by this refactoring. Tests are enhanced with a mismatch-then-match retry path and re-enabled. No security or reliability issues found.
✓ Test Coverage
This PR fixes cross-scope state issues in QuestionExecutor by moving
_hasExecutedwrites toExecuteAsync(same scope as reads) and keeping_promptCountcoherent withinCaptureResponseAsync. The integration tests are improved (un-skipped, ConfirmInput now exercises the GotoAction re-prompt path). However, the core logic fix — changingSkipOnFirstExecutionIfVariableHasValuefromisValueUndefined && !hasExecutedtoisValueUndefined || hasExecutedPreviously— is not covered by any unit test that would catch a regression. The existing unit tests (QuestionExecuteWithSkipModeAsyncWithResultDefinedAsyncand…UndefinedAsync) only test first-execution scenarios where_hasExecuteddefaults tofalse, producing identical results under both old and new logic.
✓ Design Approach
I did not find a design-level issue in this diff. The change addresses the underlying executor-scope mismatch rather than papering over the symptom:
_hasExecutedis now read and written in the question action’s own scope, and prompt-count decisions are made with an explicit count passed across the prepare/capture boundary instead of depending on cross-executor durable state. The updated multi-turn tests also exercise theGotoActionre-prompt path that previously would have missed this class of bug.
Suggestions
- Add a unit test for
SkipOnFirstExecutionIfVariableHasValuewhen_hasExecutedis alreadytrue(re-execution via GotoAction). This is the scenario the logic fix targets, but no existing unit test differentiates old from new behavior. For example, a test where the variable is defined AND the executor has previously run should expectexpectPrompt: true—this would fail under the old logic (false && false = false) but pass under the new logic (false || true = true). The existing tests atQuestionExecutorTest.cs:84,100provide no regression guard becauseExecuteTestAsync(lines 293-319) creates a fresh executor each time, so_hasExecutedalways starts asfalse.
Automated review by peibekwe's agents
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Motivation and Context
The
ConfirmInputdeclarative workflow sample (and any workflow combining a Question action with aGotoActionloopback) entered an infinite loop on re-entry: the question would never re-prompt, so the mismatch branch would fire repeatedly without giving the user a chance to retry. Fixed the bug and re-enabled (and extended) the previously disabled integration test to capture this regression.Contribution Checklist