@@ -36,8 +36,8 @@ namespace GitHub.Copilot.SDK;
3636/// }
3737/// });
3838///
39- /// // Send a message
40- /// await session.SendAsync (new MessageOptions { Prompt = "Hello, world!" });
39+ /// // Send a message and wait for completion
40+ /// await session.SendAndWaitAsync (new MessageOptions { Prompt = "Hello, world!" });
4141/// </code>
4242/// </example>
4343public class CopilotSession : IAsyncDisposable
@@ -76,8 +76,13 @@ internal CopilotSession(string sessionId, JsonRpc rpc)
7676 /// <returns>A task that resolves with the ID of the response message, which can be used to correlate events.</returns>
7777 /// <exception cref="InvalidOperationException">Thrown if the session has been disposed.</exception>
7878 /// <remarks>
79- /// The message is processed asynchronously. Subscribe to events via <see cref="On"/> to receive
80- /// streaming responses and other session events.
79+ /// <para>
80+ /// This method returns immediately after the message is queued. Use <see cref="SendAndWaitAsync"/>
81+ /// if you need to wait for the assistant to finish processing.
82+ /// </para>
83+ /// <para>
84+ /// Subscribe to events via <see cref="On"/> to receive streaming responses and other session events.
85+ /// </para>
8186 /// </remarks>
8287 /// <example>
8388 /// <code>
@@ -107,6 +112,70 @@ public async Task<string> SendAsync(MessageOptions options, CancellationToken ca
107112 return response . MessageId ;
108113 }
109114
115+ /// <summary>
116+ /// Sends a message to the Copilot session and waits until the session becomes idle.
117+ /// </summary>
118+ /// <param name="options">Options for the message to be sent, including the prompt and optional attachments.</param>
119+ /// <param name="timeout">Timeout duration (default: 60 seconds). Controls how long to wait; does not abort in-flight agent work.</param>
120+ /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
121+ /// <returns>A task that resolves with the final assistant message event, or null if none was received.</returns>
122+ /// <exception cref="TimeoutException">Thrown if the timeout is reached before the session becomes idle.</exception>
123+ /// <exception cref="InvalidOperationException">Thrown if the session has been disposed.</exception>
124+ /// <remarks>
125+ /// <para>
126+ /// This is a convenience method that combines <see cref="SendAsync"/> with waiting for
127+ /// the <c>session.idle</c> event. Use this when you want to block until the assistant
128+ /// has finished processing the message.
129+ /// </para>
130+ /// <para>
131+ /// Events are still delivered to handlers registered via <see cref="On"/> while waiting.
132+ /// </para>
133+ /// </remarks>
134+ /// <example>
135+ /// <code>
136+ /// // Send and wait for completion with default 60s timeout
137+ /// var response = await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" });
138+ /// Console.WriteLine(response?.Data?.Content); // "4"
139+ /// </code>
140+ /// </example>
141+ public async Task < AssistantMessageEvent ? > SendAndWaitAsync (
142+ MessageOptions options ,
143+ TimeSpan ? timeout = null ,
144+ CancellationToken cancellationToken = default )
145+ {
146+ var effectiveTimeout = timeout ?? TimeSpan . FromSeconds ( 60 ) ;
147+ var tcs = new TaskCompletionSource < AssistantMessageEvent ? > ( ) ;
148+ AssistantMessageEvent ? lastAssistantMessage = null ;
149+
150+ void Handler ( SessionEvent evt )
151+ {
152+ if ( evt is AssistantMessageEvent assistantMessage )
153+ {
154+ lastAssistantMessage = assistantMessage ;
155+ }
156+ else if ( evt . Type == "session.idle" )
157+ {
158+ tcs . TrySetResult ( lastAssistantMessage ) ;
159+ }
160+ else if ( evt is SessionErrorEvent errorEvent )
161+ {
162+ var message = errorEvent . Data ? . Message ?? "session error" ;
163+ tcs . TrySetException ( new InvalidOperationException ( $ "Session error: { message } ") ) ;
164+ }
165+ }
166+
167+ using var subscription = On ( Handler ) ;
168+
169+ await SendAsync ( options , cancellationToken ) ;
170+
171+ using var cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
172+ cts . CancelAfter ( effectiveTimeout ) ;
173+
174+ using var registration = cts . Token . Register ( ( ) =>
175+ tcs . TrySetException ( new TimeoutException ( $ "SendAndWaitAsync timed out after { effectiveTimeout } ") ) ) ;
176+ return await tcs . Task ;
177+ }
178+
110179 /// <summary>
111180 /// Registers a callback for session events.
112181 /// </summary>
@@ -271,7 +340,10 @@ public async Task<IReadOnlyList<SessionEvent>> GetMessagesAsync(CancellationToke
271340 var response = await _rpc . InvokeWithCancellationAsync < GetMessagesResponse > (
272341 "session.getMessages" , [ new { sessionId = SessionId } ] , cancellationToken ) ;
273342
274- return response . Events . Select ( e => SessionEvent . FromJson ( e . ToJsonString ( ) ) ) . ToList ( ) ;
343+ return response . Events
344+ . Select ( e => SessionEvent . FromJson ( e . ToJsonString ( ) ) )
345+ . OfType < SessionEvent > ( )
346+ . ToList ( ) ;
275347 }
276348
277349 /// <summary>
0 commit comments