microsoft / microsoft/agent-framework
.NET: [Bug]: Workflow handler cancellation is reported as executor failure
@peibekwe is already working on this.
Since Sep 7, 2026.
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
## Description
When a cancellation-aware .NET workflow delegate handler observes cancellation through the runtime-provided `CancellationToken`, a direct `Executor.ExecuteCoreAsync` call converts the cancellation into an executor failure.
This is reproducible on current `main` (`2c49f50cf`) with .NET SDK `10.0.303` on macOS arm64:
```text
Expected: System.OperationCanceledException
Actual: System.Reflection.TargetInvocationException
Inner: System.Threading.Tasks.TaskCanceledException
```
At the `ExecuteCoreAsync` layer, the same execution also submits one `ExecutorFailedEvent` whose data is the `TaskCanceledException`. Whether that event is subsequently observable through a complete lockstep or off-thread public event stream is mode- and timing-dependent because those streams stop yielding after cancellation.
There are two inconsistent cancellation paths:
1. Delegate handlers registered through `RouteBuilder` let `OperationCanceledException` reach `MessageRouter.RouteMessageAsync`, whose catch-all converts it to `CallResult.RaisedException(...)`.
2. The legacy reflection handler path already converts `OperationCanceledException` to `CallResult.Cancelled(...)`, but `Executor.ExecuteCoreAsync` treats every non-success result, including `IsCancelled`, as an `ExecutorFailedEvent` followed by `TargetInvocationException`.
PR #1280 introduced propagation of the runtime token together with the explicit `CallResult.IsCancelled` / `CallResult.Cancelled` state. The reflection adapter produces that state for `OperationCanceledException`, but the common executor path does not branch on `IsCancelled`; it routes every non-success result through the existing failure path. Both lockstep and off-thread run-event implementations have normal `OperationCanceledException` handling, which the wrapper cannot reach. This strongly suggests incomplete cancellation-state integration, although #1280 does not document the intended outer exception/event contract.
Proposed behavior, subject to maintainer confirmation: cooperative cancellation associated with the runtime-provided token should remain cancellation rather than being wrapped in `TargetInvocationException` or classified as an executor failure. Existing `TargetInvocationException` behavior for non-cancellation handler exceptions should remain unchanged.
The behavior for an `OperationCanceledException` raised while the runtime token is not cancelled needs maintainer confirmation. The two routing implementations currently disagree, so this report does not assume that boundary.
## Code Sample
```csharp
[Fact]
public async Task HandlerCancellationPropagatesWithoutFailureEventAsync()
{
using CancellationTokenSource cancellationSource = new();
FunctionExecutor executor = new(
"cancel",
async (message, context, cancellationToken) =>
{
cancellationSource.Cancel();
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
});
TestWorkflowContext context = new(executor.Id);
Task ActAsync() => executor.ExecuteCoreAsync(
"message",
new(typeof(string)),
context,
cancellationSource.Token).AsTask();
await Assert.ThrowsAnyAsync(ActAsync);
Assert.DoesNotContain(
context.EmittedEvents,
evt => evt is ExecutorFailedEvent);
}
```
The test fails on current `main` before reaching the event assertion:
```text
Assert.ThrowsAny() Failure: Exception type was not compatible
Expected: typeof(System.OperationCanceledException)
Actual: typeof(System.Reflection.TargetInvocationException)
---- System.Reflection.TargetInvocationException
-------- System.Threading.Tasks.TaskCanceledException
```
A separate current-behavior test confirmed that `ExecuteCoreAsync` submits an `ExecutorFailedEvent` containing the same `TaskCanceledException`. Public event-stream observability still needs independent lockstep and off-thread coverage.
## Error Messages / Stack Traces
```text
System.Reflection.TargetInvocationException:
Error invoking handler for
Inner exception:
System.Threading.Tasks.TaskCanceledException:
A task was canceled.
Relevant frames:
MessageRouter.RouteMessageAsync(...)
Executor.ExecuteCoreAsync(...)
```
## Package Versions
`Microsoft.Agents.AI.Workflows` from `main` at `2c49f50cf08ebb6c1687146336f039051f159333`.
## .NET Version
`.NET SDK 10.0.303`, macOS arm64.
## Additional Context
A bounded implementation should:
- preserve cooperative cancellation through the common executor path;
- avoid `ExecutorFailedEvent` and `TargetInvocationException` for that path;
- align delegate and reflection-based handler behavior;
- add regression coverage for direct execution plus lockstep and off-thread environments;
- explicitly pin the maintainer-approved behavior for unrelated/foreign cancellation exceptions.
If maintainers confirm the cancellation contract, I am willing to implement the focused fix and regression coverage within three days.
AI assistance was used for call-path analysis and drafting. The reported failing and current-behavior tests were executed in an isolated checkout; no passing result is claimed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.