mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Peibekwe/workflows cancellation token fix (#1740)
* Propagate cancellation token down the stack * Added unit tests to cover workflow cancellation scenarios * Updated tests based on feedback to simplify assert. * Create custom AsyncEnumrable to gracefully handle cancellation for Channel reader. Tailor cancellation tests to declarative scenarios. * Update comment and naming for readability. * Fixing minor stylistic recommendation. --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
e87eed573b
commit
c83011b30d
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// A custom IAsyncEnumerable implementation that reads from a ChannelReader,
|
||||
/// and suppresses OperationCanceledException when the cancellation token is triggered.
|
||||
/// </summary>
|
||||
internal sealed class NonThrowingChannelReaderAsyncEnumerable<T>(ChannelReader<T> reader) : IAsyncEnumerable<T>
|
||||
{
|
||||
private class Enumerator(ChannelReader<T> reader, CancellationToken cancellationToken) : IAsyncEnumerator<T>
|
||||
{
|
||||
private T? _current;
|
||||
public T Current => this._current ?? throw new InvalidOperationException("Enumeration not started.");
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
// no-op - the reader should not be disposed.
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves to the next item in the channel.
|
||||
/// </summary>
|
||||
/// <returns>If successful, returns <c>true</c>, otherwise <c>false</c>.</returns>
|
||||
public async ValueTask<bool> MoveNextAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
bool hasData = await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (hasData)
|
||||
{
|
||||
this._current = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Swallow cancellation exceptions to prevent throwing from the enumerator
|
||||
// Enables clean cancellation and aligns with the expected behavior of IAsyncEnumerable.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an async enumerator that reads items from the channel.
|
||||
/// If cancellation is requested, the enumeration exits silently without throwing.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">An optional cancellation token from the caller.</param>
|
||||
/// <returns>An async enumerator over the channel items.</returns>
|
||||
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
=> new Enumerator(reader, cancellationToken);
|
||||
}
|
||||
@@ -139,11 +139,9 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
// Get the current epoch - we'll only respond to completion signals from this epoch or later
|
||||
int myEpoch = Volatile.Read(ref this._completionEpoch) + 1;
|
||||
|
||||
// Simply read from channel - all coordination is handled by Channel infrastructure
|
||||
// Note: When cancellation is requested, ReadAllAsync may throw OperationCanceledException
|
||||
// or may complete the enumeration. We check IsCancellationRequested explicitly at superstep
|
||||
// boundaries to ensure clean cancellation.
|
||||
await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
// Use custom async enumerable to avoid exceptions on cancellation.
|
||||
NonThrowingChannelReaderAsyncEnumerable<WorkflowEvent> eventStream = new(this._eventChannel.Reader);
|
||||
await foreach (WorkflowEvent evt in eventStream.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Filter out internal signals used for run loop coordination
|
||||
if (evt is InternalHaltSignal completionSignal)
|
||||
|
||||
Reference in New Issue
Block a user