Skip to content

Commit 572302d

Browse files
authored
.NET Fix - Surface agent failure for orchestration (#13369)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Agent failure results in timeout for `OrchestrationResult`. Fixes: #13149 Fixes: #13040 Fixes: #12987 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> `AgentActor` exception being eaten by the agent-runtime and not exposed via the `TaskCompletionSource<>` used by the `OrchestrationResult`/ ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent de20575 commit 572302d

File tree

5 files changed

+46
-19
lines changed

5 files changed

+46
-19
lines changed

dotnet/src/Agents/Orchestration/AgentActor.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,31 @@ protected ValueTask<ChatMessageContent> InvokeAsync(ChatMessageContent input, Ca
9292
/// <returns>A task that returns the response <see cref="ChatMessageContent"/>.</returns>
9393
protected async ValueTask<ChatMessageContent> InvokeAsync(IList<ChatMessageContent> input, CancellationToken cancellationToken)
9494
{
95-
this.Context.Cancellation.ThrowIfCancellationRequested();
95+
try
96+
{
97+
this.Context.Cancellation.ThrowIfCancellationRequested();
9698

97-
this._lastResponse = null;
99+
this._lastResponse = null;
98100

99-
AgentInvokeOptions options = this.GetInvokeOptions(HandleMessageAsync);
100-
if (this.Context.StreamingResponseCallback == null)
101-
{
102-
// No need to utilize streaming if no callback is provided
103-
await this.InvokeAsync(input, options, cancellationToken).ConfigureAwait(false);
101+
AgentInvokeOptions options = this.GetInvokeOptions(HandleMessageAsync);
102+
if (this.Context.StreamingResponseCallback == null)
103+
{
104+
// No need to utilize streaming if no callback is provided
105+
await this.InvokeAsync(input, options, cancellationToken).ConfigureAwait(false);
106+
}
107+
else
108+
{
109+
await this.InvokeStreamingAsync(input, options, cancellationToken).ConfigureAwait(false);
110+
}
111+
112+
return this._lastResponse ?? new ChatMessageContent(AuthorRole.Assistant, string.Empty);
104113
}
105-
else
114+
catch (Exception exception)
106115
{
107-
await this.InvokeStreamingAsync(input, options, cancellationToken).ConfigureAwait(false);
116+
this.Context.FailureCallback.Invoke(exception);
117+
throw;
108118
}
109119

110-
return this._lastResponse ?? new ChatMessageContent(AuthorRole.Assistant, string.Empty);
111-
112120
async Task HandleMessageAsync(ChatMessageContent message)
113121
{
114122
this._lastResponse = message; // Keep track of most recent response for both invocation modes

dotnet/src/Agents/Orchestration/AgentOrchestration.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,19 @@ public async ValueTask<OrchestrationResult<TOutput>> InvokeAsync(
114114

115115
CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
116116

117+
TaskCompletionSource<TOutput> completion = new();
118+
117119
OrchestrationContext context =
118120
new(this.OrchestrationLabel,
119121
topic,
120122
this.ResponseCallback,
121123
this.StreamingResponseCallback,
124+
exception => completion.SetException(exception),
122125
this.LoggerFactory,
123126
cancellationToken);
124127

125128
ILogger logger = this.LoggerFactory.CreateLogger(this.GetType());
126129

127-
TaskCompletionSource<TOutput> completion = new();
128-
129130
AgentType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false);
130131

131132
cancellationToken.ThrowIfCancellationRequested();

dotnet/src/Agents/Orchestration/OrchestrationContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System;
34
using System.Threading;
45
using Microsoft.Extensions.Logging;
56
using Microsoft.SemanticKernel.Agents.Runtime;
@@ -16,11 +17,13 @@ internal OrchestrationContext(
1617
TopicId topic,
1718
OrchestrationResponseCallback? responseCallback,
1819
OrchestrationStreamingCallback? streamingCallback,
20+
Action<Exception> failureCallback,
1921
ILoggerFactory loggerFactory,
2022
CancellationToken cancellation)
2123
{
2224
this.Orchestration = orchestration;
2325
this.Topic = topic;
26+
this.FailureCallback = failureCallback;
2427
this.ResponseCallback = responseCallback;
2528
this.StreamingResponseCallback = streamingCallback;
2629
this.LoggerFactory = loggerFactory;
@@ -59,4 +62,9 @@ internal OrchestrationContext(
5962
/// Optional callback that is invoked for every agent response.
6063
/// </summary>
6164
public OrchestrationStreamingCallback? StreamingResponseCallback { get; }
65+
66+
/// <summary>
67+
/// Gets the callback that is invoked when an operation fails due to an exception.
68+
/// </summary>
69+
public Action<Exception> FailureCallback { get; }
6270
}

dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ public class OrchestrationResultTests
1616
public void Constructor_InitializesPropertiesCorrectly()
1717
{
1818
// Arrange
19-
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
19+
Exception? captureException = null;
20+
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, exception => captureException = exception, NullLoggerFactory.Instance, CancellationToken.None);
2021
TaskCompletionSource<string> tcs = new();
2122

2223
// Act
2324
using CancellationTokenSource cancelSource = new();
2425
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
2526

2627
// Assert
28+
Assert.Null(captureException);
2729
Assert.Equal("TestOrchestration", result.Orchestration);
2830
Assert.Equal(new TopicId("testTopic"), result.Topic);
2931
}
@@ -32,7 +34,8 @@ public void Constructor_InitializesPropertiesCorrectly()
3234
public async Task GetValueAsync_ReturnsCompletedValue_WhenTaskIsCompletedAsync()
3335
{
3436
// Arrange
35-
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
37+
Exception? captureException = null;
38+
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, exception => captureException = exception, NullLoggerFactory.Instance, CancellationToken.None);
3639
TaskCompletionSource<string> tcs = new();
3740
using CancellationTokenSource cancelSource = new();
3841
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
@@ -43,14 +46,16 @@ public async Task GetValueAsync_ReturnsCompletedValue_WhenTaskIsCompletedAsync()
4346
string actualValue = await result.GetValueAsync();
4447

4548
// Assert
49+
Assert.Null(captureException);
4650
Assert.Equal(expectedValue, actualValue);
4751
}
4852

4953
[Fact]
5054
public async Task GetValueAsync_WithTimeout_ReturnsCompletedValue_WhenTaskCompletesWithinTimeoutAsync()
5155
{
5256
// Arrange
53-
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
57+
Exception? captureException = null;
58+
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, exception => captureException = exception, NullLoggerFactory.Instance, CancellationToken.None);
5459
TaskCompletionSource<string> tcs = new();
5560
using CancellationTokenSource cancelSource = new();
5661
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
@@ -62,28 +67,32 @@ public async Task GetValueAsync_WithTimeout_ReturnsCompletedValue_WhenTaskComple
6267
string actualValue = await result.GetValueAsync(timeout);
6368

6469
// Assert
70+
Assert.Null(captureException);
6571
Assert.Equal(expectedValue, actualValue);
6672
}
6773

6874
[Fact]
6975
public async Task GetValueAsync_WithTimeout_ThrowsTimeoutException_WhenTaskDoesNotCompleteWithinTimeoutAsync()
7076
{
7177
// Arrange
72-
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
78+
Exception? captureException = null;
79+
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, exception => captureException = exception, NullLoggerFactory.Instance, CancellationToken.None);
7380
TaskCompletionSource<string> tcs = new();
7481
using CancellationTokenSource cancelSource = new();
7582
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
7683
TimeSpan timeout = TimeSpan.FromMilliseconds(50);
7784

7885
// Act & Assert
7986
TimeoutException exception = await Assert.ThrowsAsync<TimeoutException>(() => result.GetValueAsync(timeout).AsTask());
87+
Assert.Null(captureException);
8088
}
8189

8290
[Fact]
8391
public async Task GetValueAsync_ReturnsCompletedValue_WhenCompletionIsDelayedAsync()
8492
{
8593
// Arrange
86-
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
94+
Exception? captureException = null;
95+
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, exception => captureException = exception, NullLoggerFactory.Instance, CancellationToken.None);
8796
TaskCompletionSource<int> tcs = new();
8897
using CancellationTokenSource cancelSource = new();
8998
using OrchestrationResult<int> result = new(context, tcs, cancelSource, NullLogger.Instance);
@@ -100,6 +109,7 @@ public async Task GetValueAsync_ReturnsCompletedValue_WhenCompletionIsDelayedAsy
100109
int actualValue = await result.GetValueAsync();
101110

102111
// Assert
112+
Assert.Null(captureException);
103113
Assert.Equal(expectedValue, actualValue);
104114
}
105115
}

dotnet/src/VectorData/SqliteVec/SqliteMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public TRecord MapFromStorageToDataModel(DbDataReader reader, bool includeVector
5757

5858
var floats = new float[length / 4];
5959
var bytes = MemoryMarshal.Cast<float, byte>(floats);
60-
stream.ReadExactly(bytes);
60+
stream.ReadExactly([.. bytes]);
6161
#else
6262
var floats = MemoryMarshal.Cast<byte, float>((byte[])reader[ordinal]).ToArray();
6363
#endif

0 commit comments

Comments
 (0)