Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu

## [Unreleased]

### Feature: selectable `ask_user` session behavior

Session create and cold resume now accept a language-specific `askUserVariant` option with `legacy` and `elicitation` values. SDK sessions retain the legacy question-and-answer tool by default. Select `elicitation` and provide an elicitation handler to expose the structured form-based `ask_user` tool.

### Feature: rotating session-scoped GitHub credentials

All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback.
Expand Down
7 changes: 5 additions & 2 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ Create a new conversation session.
- `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled.
- `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`.
- `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. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section.
- `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`.
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.

##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task<CopilotSession>`
Expand All @@ -146,6 +147,7 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i

- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
- `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`.
- `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume.

```csharp
await using var session = await client.CreateSessionAsync(new SessionConfig
Expand Down Expand Up @@ -871,7 +873,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `SkipPe

## User Input Requests

Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler:
Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler:

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
Expand Down Expand Up @@ -1004,6 +1006,7 @@ var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = PermissionHandler.ApproveAll,
AskUserVariant = AskUserVariant.Elicitation,
OnElicitationRequest = async (context) =>
{
// context.SessionId - Session that triggered the request
Expand Down
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.EnableCitations,
config.EnableFileChangeTracking,
wireSystemMessage,
config.AskUserVariant,
toolFilter.AvailableTools,
toolFilter.ExcludedTools,
config.ExcludedBuiltInAgents,
Expand Down Expand Up @@ -1428,6 +1429,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.EnableCitations,
config.EnableFileChangeTracking,
wireSystemMessage,
config.AskUserVariant,
toolFilter.AvailableTools,
toolFilter.ExcludedTools,
config.ExcludedBuiltInAgents,
Expand Down Expand Up @@ -2868,6 +2870,7 @@ internal record CreateSessionRequest(
bool? EnableCitations,
bool? EnableFileChangeTracking,
SystemMessageConfig? SystemMessage,
AskUserVariant? AskUserVariant,
IList<string>? AvailableTools,
IList<string>? ExcludedTools,
[property: JsonPropertyName("excludedBuiltinAgents")] IList<string>? ExcludedBuiltInAgents,
Expand Down Expand Up @@ -2983,6 +2986,7 @@ internal record ResumeSessionRequest(
bool? EnableCitations,
bool? EnableFileChangeTracking,
SystemMessageConfig? SystemMessage,
AskUserVariant? AskUserVariant,
IList<string>? AvailableTools,
IList<string>? ExcludedTools,
[property: JsonPropertyName("excludedBuiltinAgents")] IList<string>? ExcludedBuiltInAgents,
Expand Down
31 changes: 30 additions & 1 deletion dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3116,6 +3116,21 @@ public sealed class ManagedSettings
public ManagedSettingsPermissions? Permissions { get; set; }
}

/// <summary>
/// Selects the model-facing shape of the built-in <c>ask_user</c> tool.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<AskUserVariant>))]
public enum AskUserVariant
{
/// <summary>Use the legacy user-input request flow.</summary>
[JsonStringEnumMemberName("legacy")]
Legacy,

/// <summary>Use the elicitation request flow.</summary>
[JsonStringEnumMemberName("elicitation")]
Elicitation
}

/// <summary>
/// Shared configuration properties for creating or resuming a Copilot session.
/// Use <see cref="SessionConfig"/> when creating a new session, or
Expand Down Expand Up @@ -3205,6 +3220,7 @@ protected SessionConfigBase(SessionConfigBase? other)
ReasoningEffort = other.ReasoningEffort;
ReasoningSummary = other.ReasoningSummary;
ContextTier = other.ContextTier;
AskUserVariant = other.AskUserVariant;
CreateSessionFsProvider = other.CreateSessionFsProvider;
GitHubToken = other.GitHubToken;
GitHubTokenProvider = other.GitHubTokenProvider;
Expand Down Expand Up @@ -3372,6 +3388,15 @@ protected SessionConfigBase(SessionConfigBase? other)
/// <summary>System message configuration for the session.</summary>
public SystemMessageConfig? SystemMessage { get; set; }

/// <summary>
/// Selects the model-facing shape of the built-in <c>ask_user</c> tool.
/// The default is <see cref="GitHub.Copilot.AskUserVariant.Legacy"/>. To use
/// <see cref="GitHub.Copilot.AskUserVariant.Elicitation"/>, also provide
/// <see cref="OnElicitationRequest"/> so the host can answer structured forms.
/// The runtime resolves this option when it creates or cold-resumes the session.
/// </summary>
public AskUserVariant? AskUserVariant { get; set; }

/// <summary>List of tool names to allow; only these tools will be available when specified.</summary>
public IList<string>? AvailableTools { get; set; }

Expand Down Expand Up @@ -3470,7 +3495,11 @@ protected SessionConfigBase(SessionConfigBase? other)
/// <summary>Handler for permission requests from the server.</summary>
public Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? OnPermissionRequest { get; set; }

/// <summary>Handler for user input requests from the agent.</summary>
/// <summary>
/// Handler for user input requests from the agent. When provided with the default
/// <see cref="GitHub.Copilot.AskUserVariant.Legacy"/> variant, enables the
/// question-and-answer form of the <c>ask_user</c> tool.
/// </summary>
public Func<UserInputRequest, UserInputInvocation, Task<UserInputResponse>>? OnUserInputRequest { get; set; }

/// <summary>Slash commands registered for this session.</summary>
Expand Down
48 changes: 48 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,54 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse
Assert.False(agent.TryGetProperty("reasoningEffort", out _));
}

[Fact]
public async Task CreateSessionAsync_Forwards_AskUserVariant()
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });

await using var session = await client.CreateSessionAsync(new SessionConfig
{
AskUserVariant = AskUserVariant.Elicitation,
OnPermissionRequest = PermissionHandler.ApproveAll
});

var request = Assert.Single(server.Requests, request => request.Method == "session.create");
Assert.Equal("elicitation", request.Params.GetProperty("askUserVariant").GetString());

server.ClearRequests();
await using var defaultSession = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll
});
var defaultRequest = Assert.Single(server.Requests, request => request.Method == "session.create");
Assert.False(defaultRequest.Params.TryGetProperty("askUserVariant", out _));
}

[Fact]
public async Task ResumeSessionAsync_Forwards_AskUserVariant_On_Cold_Resume()
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });

await using var session = await client.ResumeSessionAsync("ask-user-variant", new ResumeSessionConfig
{
AskUserVariant = AskUserVariant.Legacy,
OnPermissionRequest = PermissionHandler.ApproveAll
});

var request = Assert.Single(server.Requests, request => request.Method == "session.resume");
Assert.Equal("legacy", request.Params.GetProperty("askUserVariant").GetString());

server.ClearRequests();
await using var defaultSession = await client.ResumeSessionAsync("ask-user-variant-default", new ResumeSessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll
});
var defaultRequest = Assert.Single(server.Requests, request => request.Method == "session.resume");
Assert.False(defaultRequest.Params.TryGetProperty("askUserVariant", out _));
}

[Fact]
public async Task SessionRequests_Serialize_AdditionalDirectories()
{
Expand Down
2 changes: 2 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
ReasoningEffort = "high",
ReasoningSummary = ReasoningSummary.Detailed,
ContextTier = ContextTier.LongContext,
AskUserVariant = AskUserVariant.Elicitation,
ConfigDirectory = "/config",
AvailableTools = ["tool1", "tool2"],
ExcludedTools = ["tool3"],
Expand Down Expand Up @@ -121,6 +122,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort);
Assert.Equal(original.ReasoningSummary, clone.ReasoningSummary);
Assert.Equal(original.ContextTier, clone.ContextTier);
Assert.Equal(original.AskUserVariant, clone.AskUserVariant);
Assert.Equal(original.ConfigDirectory, clone.ConfigDirectory);
Assert.Equal(original.AvailableTools, clone.AvailableTools);
Assert.Equal(original.ExcludedTools, clone.ExcludedTools);
Expand Down
6 changes: 4 additions & 2 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `EnableSessionStore` (\*bool): Enables the cross-session store for search and retrieval across sessions. When unset in `ModeCopilotCli`, the runtime default applies (enabled). In `ModeEmpty`, defaults to disabled.
- `GitHubTokenProvider` (GitHubTokenProvider): Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenResult` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenCancelled`. Cannot be combined with `GitHubToken`.
- `OnPermissionRequest` (PermissionHandlerFunc): Optional handler called before each tool execution to approve or deny it. When nil, permission requests are emitted as events and left pending for manual resolution. `copilot.PermissionHandler.ApproveAll` approves requests when managed settings are disabled and returns an error when `EnableManagedSettings` is true. Custom handlers can inspect `RequiresManagedApproval()` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `OnUserInputRequest` (UserInputHandler): Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section.
- `AskUserVariant` (AskUserVariant): Selects the model-facing shape of the `ask_user` tool. The zero value preserves legacy behavior; use `AskUserVariantElicitation` with `OnElicitationRequest`.
- `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
- `Commands` ([]CommandDefinition): Slash-commands registered for this session. See [Commands](#commands) section.
- `OnElicitationRequest` (ElicitationHandler): Handler for elicitation requests from the server. See [Elicitation Requests](#elicitation-requests-serverclient) section.
Expand All @@ -238,6 +239,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `Streaming` (*bool): Enable streaming delta events (nil = runtime default)
- `Commands` ([]CommandDefinition): Slash-commands. See [Commands](#commands) section.
- `OnElicitationRequest` (ElicitationHandler): Elicitation handler. See [Elicitation Requests](#elicitation-requests-serverclient) section.
- `AskUserVariant` (AskUserVariant): Selects the model-facing shape of the `ask_user` tool on cold resume. Re-supply `AskUserVariantElicitation` with `OnElicitationRequest`; the zero value preserves legacy behavior.
- `GitHubTokenProvider` (GitHubTokenProvider): Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`.

```go
Expand Down Expand Up @@ -770,7 +772,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `SkipPe

## User Input Requests

Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler:
Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler:

```go
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Expand Down
15 changes: 15 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,13 +788,23 @@ func hasManagedSettings(enableManagedSettings *bool, managedSettings *ManagedSet
return (enableManagedSettings != nil && *enableManagedSettings) || managedSettings != nil
}

func validateAskUserVariant(variant AskUserVariant) error {
if variant != "" && variant != AskUserVariantLegacy && variant != AskUserVariantElicitation {
return fmt.Errorf("invalid AskUserVariant %q: expected %q, %q, or unset", variant, AskUserVariantLegacy, AskUserVariantElicitation)
}
return nil
}

func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) {
if config == nil {
config = &SessionConfig{}
}
if config.GitHubToken != "" && config.GitHubTokenProvider != nil {
return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together")
}
if err := validateAskUserVariant(config.AskUserVariant); err != nil {
return nil, err
}

if err := c.ensureConnected(ctx); err != nil {
return nil, err
Expand Down Expand Up @@ -842,6 +852,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.Capi = config.Capi
req.Providers = config.Providers
req.Models = config.Models
req.AskUserVariant = config.AskUserVariant
req.EnableSessionTelemetry = config.EnableSessionTelemetry
req.EnableCitations = config.EnableCitations
req.EnableFileChangeTracking = config.EnableFileChangeTracking
Expand Down Expand Up @@ -1170,6 +1181,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
if config.GitHubToken != "" && config.GitHubTokenProvider != nil {
return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together")
}
if err := validateAskUserVariant(config.AskUserVariant); err != nil {
return nil, err
}

if err := c.ensureConnected(ctx); err != nil {
return nil, err
Expand Down Expand Up @@ -1200,6 +1214,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.Capi = config.Capi
req.Providers = config.Providers
req.Models = config.Models
req.AskUserVariant = config.AskUserVariant
req.EnableSessionTelemetry = config.EnableSessionTelemetry
req.IsExperimentalMode = config.EnableExperimentalMode
req.SkipCustomInstructions = config.SkipCustomInstructions
Expand Down
Loading
Loading