Skip to content

Commit f0909a7

Browse files
authored
[dotnet] Fix SendAndWaitAsync to throw OperationCanceledException on external cancellation (#543)
1 parent 5a9b475 commit f0909a7

File tree

3 files changed

+55
-1
lines changed

3 files changed

+55
-1
lines changed

dotnet/src/Session.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ public async Task<string> SendAsync(MessageOptions options, CancellationToken ca
147147
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
148148
/// <returns>A task that resolves with the final assistant message event, or null if none was received.</returns>
149149
/// <exception cref="TimeoutException">Thrown if the timeout is reached before the session becomes idle.</exception>
150+
/// <exception cref="OperationCanceledException">Thrown if the <paramref name="cancellationToken"/> is cancelled.</exception>
150151
/// <exception cref="InvalidOperationException">Thrown if the session has been disposed.</exception>
151152
/// <remarks>
152153
/// <para>
@@ -201,7 +202,12 @@ void Handler(SessionEvent evt)
201202
cts.CancelAfter(effectiveTimeout);
202203

203204
using var registration = cts.Token.Register(() =>
204-
tcs.TrySetException(new TimeoutException($"SendAndWaitAsync timed out after {effectiveTimeout}")));
205+
{
206+
if (cancellationToken.IsCancellationRequested)
207+
tcs.TrySetCanceled(cancellationToken);
208+
else
209+
tcs.TrySetException(new TimeoutException($"SendAndWaitAsync timed out after {effectiveTimeout}"));
210+
});
205211
return await tcs.Task;
206212
}
207213

dotnet/test/SessionTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,30 @@ public async Task SendAndWait_Throws_On_Timeout()
403403
Assert.Contains("timed out", ex.Message);
404404
}
405405

406+
[Fact]
407+
public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cancelled()
408+
{
409+
var session = await Client.CreateSessionAsync();
410+
411+
// Set up wait for tool execution to start BEFORE sending
412+
var toolStartTask = TestHelper.GetNextEventOfTypeAsync<ToolExecutionStartEvent>(session);
413+
414+
using var cts = new CancellationTokenSource();
415+
416+
// Start SendAndWaitAsync - don't await it yet
417+
var sendTask = session.SendAndWaitAsync(
418+
new MessageOptions { Prompt = "run the shell command 'sleep 10' (note this works on both bash and PowerShell)" },
419+
cancellationToken: cts.Token);
420+
421+
// Wait for the tool to begin executing before cancelling
422+
await toolStartTask;
423+
424+
// Cancel the token
425+
cts.Cancel();
426+
427+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sendTask);
428+
}
429+
406430
[Fact]
407431
public async Task Should_Create_Session_With_Custom_Config_Dir()
408432
{
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
models:
2+
- claude-sonnet-4.5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: run the shell command 'sleep 10' (note this works on both bash and PowerShell)
9+
- role: assistant
10+
content: I'll run the sleep command for you.
11+
- role: assistant
12+
tool_calls:
13+
- id: toolcall_0
14+
type: function
15+
function:
16+
name: report_intent
17+
arguments: '{"intent":"Running sleep command"}'
18+
- role: assistant
19+
tool_calls:
20+
- id: toolcall_1
21+
type: function
22+
function:
23+
name: ${shell}
24+
arguments: '{"command":"sleep 10","description":"Execute sleep 10 command","initial_wait":15,"mode":"sync"}'

0 commit comments

Comments
 (0)