Skip to content

Commit 51baa33

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/scripts/docs-validation/npm_and_yarn-da0aa30ced
2 parents f6dca79 + 2e4cdd9 commit 51baa33

35 files changed

Lines changed: 1086 additions & 293 deletions

dotnet/GitHub.Copilot.SDK.slnx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,4 @@
1010
<Folder Name="/test/">
1111
<Project Path="test/GitHub.Copilot.SDK.Test.csproj" />
1212
</Folder>
13-
<Folder Name="/samples/">
14-
<Project Path="samples/Chat.csproj" />
15-
</Folder>
1613
</Solution>

dotnet/README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ SDK for programmatic control of GitHub Copilot CLI.
1010
dotnet add package GitHub.Copilot.SDK
1111
```
1212

13-
## Run the Sample
13+
## Run the Samples
1414

1515
Try the interactive chat sample (from the repo root):
1616

1717
```bash
18-
cd dotnet/samples
19-
dotnet run
18+
dotnet run --file dotnet/samples/Chat.cs
19+
```
20+
21+
The manual permission/tool-result resume sample can be run the same way:
22+
23+
```bash
24+
dotnet run --file dotnet/samples/ManualToolResume.cs
2025
```
2126

2227
## Quick Start
@@ -28,7 +33,7 @@ using GitHub.Copilot.SDK;
2833
await using var client = new CopilotClient();
2934
await client.StartAsync();
3035

31-
// Create a session (OnPermissionRequest is required)
36+
// Create a session (OnPermissionRequest is optional; ApproveAll allows every tool)
3237
await using var session = await client.CreateSessionAsync(new SessionConfig
3338
{
3439
Model = "gpt-5",
@@ -105,14 +110,14 @@ Create a new conversation session.
105110
- `SessionId` - Custom session ID
106111
- `Model` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.)
107112
- `ReasoningEffort` - Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `ListModelsAsync()` to check which models support this option.
108-
- `Tools` - Custom tools exposed to the CLI
113+
- `Tools` - Custom tool declarations exposed to the CLI. Declarations without an invocable `AIFunction` are left pending for manual resolution.
109114
- `SystemMessage` - System message customization
110115
- `AvailableTools` - List of tool names to allow
111116
- `ExcludedTools` - List of tool names to disable
112117
- `Provider` - Custom API provider configuration (BYOK)
113118
- `Streaming` - Enable streaming of response chunks (default: false)
114119
- `InfiniteSessions` - Configure automatic context compaction (see below)
115-
- `OnPermissionRequest` - **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
120+
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
116121
- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
117122
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
118123

@@ -122,7 +127,7 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i
122127

123128
**ResumeSessionConfig:**
124129

125-
- `OnPermissionRequest` - **Required.** Handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
130+
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
126131

127132
##### `PingAsync(string? message = null): Task<PingResponse>`
128133

@@ -726,7 +731,7 @@ No extra dependencies — uses built-in `System.Diagnostics.Activity`.
726731

727732
## Permission Handling
728733

729-
An `OnPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision.
734+
An `OnPermissionRequest` handler is optional when you create or resume a session. When provided, it is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and returns a decision. When omitted, permission requests are emitted as events and left pending for the consumer to resolve with the pending permission RPC.
730735

731736
### Approve All (simplest)
732737

@@ -789,7 +794,7 @@ var session = await client.CreateSessionAsync(new SessionConfig
789794

790795
### Resuming Sessions
791796

792-
Pass `OnPermissionRequest` when resuming a session too — it is required:
797+
You may pass `OnPermissionRequest` when resuming a session too:
793798

794799
```csharp
795800
var session = await client.ResumeSessionAsync("session-id", new ResumeSessionConfig

dotnet/samples/Chat.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#:project ../src/GitHub.Copilot.SDK.csproj
2+
13
using GitHub.Copilot.SDK;
24

35
await using var client = new CopilotClient();

dotnet/samples/Chat.csproj

Lines changed: 0 additions & 9 deletions
This file was deleted.

dotnet/samples/ManualToolResume.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#:project ../src/GitHub.Copilot.SDK.csproj
2+
3+
using System.ComponentModel;
4+
using GitHub.Copilot.SDK;
5+
using GitHub.Copilot.SDK.Rpc;
6+
using Microsoft.Extensions.AI;
7+
8+
var tool = ManualToolDeclaration();
9+
10+
// 1. Create a session with a declaration-only tool, then stop after the permission prompt.
11+
await using CopilotClient client1 = new();
12+
await using var session1 = await client1.CreateSessionAsync(new() { Tools = [tool] });
13+
14+
// Subscribe before sending so the permission event cannot be missed.
15+
var permissionRequested = WaitForEventAsync<PermissionRequestedEvent>(session1);
16+
await session1.SendAsync(new MessageOptions
17+
{
18+
Prompt = "Use the manual_resume_status tool with id 'alpha', then tell me the status.",
19+
});
20+
21+
var permissionEvent = await permissionRequested;
22+
await client1.ForceStopAsync();
23+
24+
await PauseAsync();
25+
26+
// 2. Resume pending work and grant permission to invoke the tool.
27+
await using CopilotClient client2 = new();
28+
await using var session2 = await client2.ResumeSessionAsync(session1.SessionId, new()
29+
{
30+
Tools = [tool],
31+
ContinuePendingWork = true,
32+
});
33+
34+
// Subscribe before approving so the external tool request cannot be missed.
35+
var toolRequested = WaitForEventAsync<ExternalToolRequestedEvent>(
36+
session2,
37+
evt => evt.Data.ToolName == "manual_resume_status");
38+
39+
await session2.Rpc.Permissions.HandlePendingPermissionRequestAsync(
40+
permissionEvent.Data.RequestId,
41+
new PermissionDecisionApproveOnce());
42+
43+
var toolEvent = await toolRequested;
44+
await client2.ForceStopAsync();
45+
46+
await PauseAsync();
47+
48+
// 3. Resume again and manually provide the pending tool result.
49+
await using var client3 = new CopilotClient();
50+
await using var session3 = await client3.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig
51+
{
52+
Tools = [tool],
53+
ContinuePendingWork = true,
54+
});
55+
56+
var assistantMessage = WaitForEventAsync<AssistantMessageEvent>(session3);
57+
await session3.Rpc.Tools.HandlePendingToolCallAsync(
58+
toolEvent.Data.RequestId,
59+
result: "MANUAL_STATUS_READY");
60+
61+
var answer = await assistantMessage;
62+
Console.WriteLine(answer.Data.Content);
63+
64+
static Task PauseAsync()
65+
{
66+
Console.WriteLine("Simulating time passing...\n");
67+
return Task.Delay(TimeSpan.FromSeconds(1));
68+
}
69+
70+
static AIFunctionDeclaration ManualToolDeclaration() =>
71+
AIFunctionFactory.Create(
72+
([Description("Identifier to look up")] string id) => $"not used: {id}",
73+
"manual_resume_status",
74+
"Looks up a status value. The SDK consumer supplies the result manually.")
75+
// Remove the invocable callback so the SDK leaves tool execution pending.
76+
.AsDeclarationOnly();
77+
78+
static async Task<T> WaitForEventAsync<T>(CopilotSession session, Func<T, bool>? predicate = null)
79+
where T : SessionEvent
80+
{
81+
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
82+
IDisposable? subscription = null;
83+
subscription = session.On(evt =>
84+
{
85+
if (evt is T typed && (predicate?.Invoke(typed) ?? true))
86+
{
87+
subscription?.Dispose();
88+
tcs.TrySetResult(typed);
89+
}
90+
});
91+
return await tcs.Task.WaitAsync(TimeSpan.FromMinutes(2));
92+
}

dotnet/src/Client.cs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ private static (SystemMessageConfig? wireConfig, Dictionary<string, Func<string,
508508
/// <summary>
509509
/// Creates a new Copilot session with the specified configuration.
510510
/// </summary>
511-
/// <param name="config">Configuration for the session, including the required <see cref="SessionConfig.OnPermissionRequest"/> handler.</param>
511+
/// <param name="config">Configuration for the session.</param>
512512
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
513513
/// <returns>A task that resolves to provide the <see cref="CopilotSession"/>.</returns>
514514
/// <remarks>
@@ -534,13 +534,6 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
534534
{
535535
ArgumentNullException.ThrowIfNull(config);
536536

537-
if (config.OnPermissionRequest == null)
538-
{
539-
throw new ArgumentException(
540-
"An OnPermissionRequest handler is required when creating a session. " +
541-
"For example, to allow all permissions, use CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });");
542-
}
543-
544537
var connection = await EnsureConnectedAsync(cancellationToken);
545538
var totalTimestamp = Stopwatch.GetTimestamp();
546539

@@ -670,10 +663,9 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
670663
/// Resumes an existing Copilot session with the specified configuration.
671664
/// </summary>
672665
/// <param name="sessionId">The ID of the session to resume.</param>
673-
/// <param name="config">Configuration for the resumed session, including the required <see cref="ResumeSessionConfig.OnPermissionRequest"/> handler.</param>
666+
/// <param name="config">Configuration for the resumed session.</param>
674667
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
675668
/// <returns>A task that resolves to provide the <see cref="CopilotSession"/>.</returns>
676-
/// <exception cref="ArgumentException">Thrown when <see cref="ResumeSessionConfig.OnPermissionRequest"/> is not set.</exception>
677669
/// <exception cref="InvalidOperationException">Thrown when the session does not exist or the client is not connected.</exception>
678670
/// <remarks>
679671
/// This allows you to continue a previous conversation, maintaining all conversation history.
@@ -697,13 +689,6 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
697689
ArgumentNullException.ThrowIfNull(sessionId);
698690
ArgumentNullException.ThrowIfNull(config);
699691

700-
if (config.OnPermissionRequest == null)
701-
{
702-
throw new ArgumentException(
703-
"An OnPermissionRequest handler is required when resuming a session. " +
704-
"For example, to allow all permissions, use new() { OnPermissionRequest = PermissionHandler.ApproveAll }.");
705-
}
706-
707692
var connection = await EnsureConnectedAsync(cancellationToken);
708693
var totalTimestamp = Stopwatch.GetTimestamp();
709694

@@ -1853,6 +1838,8 @@ public async ValueTask<ToolCallResponseV2> OnToolCallV2(string sessionId,
18531838
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
18541839
if (session.GetTool(toolName) is not { } tool)
18551840
{
1841+
// Support for not providing the tool handler is only available in the v3+ model.
1842+
// For v2, it must have been provided.
18561843
return new ToolCallResponseV2(new ToolResultObject
18571844
{
18581845
TextResultForLlm = $"Tool '{toolName}' is not supported.",
@@ -2012,7 +1999,7 @@ internal record ToolDefinition(
20121999
bool? OverridesBuiltInTool = null,
20132000
bool? SkipPermission = null)
20142001
{
2015-
public static ToolDefinition FromAIFunction(AIFunction function)
2002+
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
20162003
{
20172004
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
20182005
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;

dotnet/src/Session.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -425,17 +425,20 @@ private async Task ProcessEventsAsync()
425425
/// <summary>
426426
/// Registers custom tool handlers for this session.
427427
/// </summary>
428-
/// <param name="tools">A collection of AI functions that can be invoked by the assistant.</param>
428+
/// <param name="tools">A collection of AI function declarations available to the assistant.</param>
429429
/// <remarks>
430-
/// Tools allow the assistant to execute custom functions. When the assistant invokes a tool,
431-
/// the corresponding handler is called with the tool arguments.
430+
/// Tools backed by an <see cref="AIFunction"/> are invoked automatically. Declaration-only tools are
431+
/// left pending for the client to resolve via the external tool request event.
432432
/// </remarks>
433-
internal void RegisterTools(ICollection<AIFunction> tools)
433+
internal void RegisterTools(ICollection<AIFunctionDeclaration> tools)
434434
{
435435
_toolHandlers.Clear();
436436
foreach (var tool in tools)
437437
{
438-
_toolHandlers.Add(tool.Name, tool);
438+
if (tool.GetService<AIFunction>() is { } function)
439+
{
440+
_toolHandlers.Add(tool.Name, function);
441+
}
439442
}
440443
}
441444

@@ -457,7 +460,7 @@ internal void RegisterTools(ICollection<AIFunction> tools)
457460
/// When the assistant needs permission to perform certain actions (e.g., file operations),
458461
/// this handler is called to approve or deny the request.
459462
/// </remarks>
460-
internal void RegisterPermissionHandler(PermissionRequestHandler handler)
463+
internal void RegisterPermissionHandler(PermissionRequestHandler? handler)
461464
{
462465
_permissionHandler = handler;
463466
}

dotnet/src/Types.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2158,9 +2158,11 @@ protected SessionConfig(SessionConfig? other)
21582158
public bool? EnableConfigDiscovery { get; set; }
21592159

21602160
/// <summary>
2161-
/// Custom tool functions available to the language model during the session.
2161+
/// Custom tool declarations available to the language model during the session.
2162+
/// Declarations backed by an <see cref="AIFunction"/> are invoked automatically; declarations without one
2163+
/// are left for the client to handle via external tool request events.
21622164
/// </summary>
2163-
public ICollection<AIFunction>? Tools { get; set; }
2165+
public ICollection<AIFunctionDeclaration>? Tools { get; set; }
21642166
/// <summary>
21652167
/// System message configuration for the session.
21662168
/// </summary>
@@ -2429,9 +2431,11 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
24292431
public string? Model { get; set; }
24302432

24312433
/// <summary>
2432-
/// Custom tool functions available to the language model during the resumed session.
2434+
/// Custom tool declarations available to the language model during the resumed session.
2435+
/// Declarations backed by an <see cref="AIFunction"/> are invoked automatically; declarations without one
2436+
/// are left for the client to handle via external tool request events.
24332437
/// </summary>
2434-
public ICollection<AIFunction>? Tools { get; set; }
2438+
public ICollection<AIFunctionDeclaration>? Tools { get; set; }
24352439

24362440
/// <summary>
24372441
/// System message configuration.

dotnet/test/E2E/ClientE2ETests.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,27 +181,24 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u
181181
[Theory]
182182
[InlineData(true)] // stdio transport
183183
[InlineData(false)] // TCP transport
184-
public async Task Should_Throw_When_CreateSession_Called_Without_PermissionHandler(bool useStdio)
184+
public async Task Should_Allow_CreateSession_Called_Without_PermissionHandler(bool useStdio)
185185
{
186-
using var client = new CopilotClient(new CopilotClientOptions { UseStdio = useStdio });
187-
188-
var ex = await Assert.ThrowsAsync<ArgumentException>(() => client.CreateSessionAsync(new SessionConfig()));
186+
await using var client = new CopilotClient(new CopilotClientOptions { UseStdio = useStdio });
187+
await using var session = await client.CreateSessionAsync(new SessionConfig());
189188

190-
Assert.Contains("OnPermissionRequest", ex.Message);
191-
Assert.Contains("is required", ex.Message);
189+
Assert.NotNull(session.SessionId);
192190
}
193191

194192
[Theory]
195193
[InlineData(true)] // stdio transport
196194
[InlineData(false)] // TCP transport
197-
public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandler(bool useStdio)
195+
public async Task Should_Allow_ResumeSession_Called_Without_PermissionHandler(bool useStdio)
198196
{
199-
using var client = new CopilotClient(new CopilotClientOptions { UseStdio = useStdio });
200-
201-
var ex = await Assert.ThrowsAsync<ArgumentException>(() => client.ResumeSessionAsync("some-session-id", new()));
197+
await using var client = new CopilotClient(new CopilotClientOptions { UseStdio = useStdio });
198+
await using var originalSession = await client.CreateSessionAsync(new SessionConfig());
199+
await using var resumedSession = await client.ResumeSessionAsync(originalSession.SessionId, new());
202200

203-
Assert.Contains("OnPermissionRequest", ex.Message);
204-
Assert.Contains("is required", ex.Message);
201+
Assert.Equal(originalSession.SessionId, resumedSession.SessionId);
205202
}
206203

207204
[Theory]

0 commit comments

Comments
 (0)