Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9e7b57a
Close Language Gaps for Commands + Dialogs/Elicitations
MRayermannMSFT Mar 31, 2026
753eb2c
Fix code quality review feedback and formatting issues
MRayermannMSFT Mar 31, 2026
1a5c4ee
Fix Python ruff lint errors: unused imports, import sort order, line …
MRayermannMSFT Mar 31, 2026
d9568d1
Fix Python type checker errors: remove unused type-ignore comments, f…
MRayermannMSFT Mar 31, 2026
51fea3d
Fix Go struct field alignment for go fmt compliance
MRayermannMSFT Mar 31, 2026
b93c2bf
Fix Python E2E tests: use correct snapshot directory 'multi_client'
MRayermannMSFT Mar 31, 2026
da308cc
Skip flaky Python E2E disconnect test: force_stop() doesn't trigger c…
MRayermannMSFT Mar 31, 2026
a70db0d
fix: close makefile wrapper in Python force_stop() to trigger TCP dis…
MRayermannMSFT Mar 31, 2026
4b92b83
fix: address remaining code review feedback
MRayermannMSFT Mar 31, 2026
f64f8f3
fix: use socket.shutdown() in Python force_stop() for reliable discon…
MRayermannMSFT Mar 31, 2026
2e11ed7
chore: remove working markdown files from PR
MRayermannMSFT Mar 31, 2026
34da394
fix: pass full elicitation schema in Go, add schema tests across SDKs
MRayermannMSFT Mar 31, 2026
5f58ed4
fix: Go test compilation errors for schema extraction test
MRayermannMSFT Mar 31, 2026
a66e0c7
fix: resolve staticcheck SA4031 lint in Go schema test
MRayermannMSFT Mar 31, 2026
7553af6
test: add Go command error, unknown command, and elicitation handler …
MRayermannMSFT Mar 31, 2026
8035e3c
fix: remove redundant nil check flagged by staticcheck SA4031
MRayermannMSFT Mar 31, 2026
259fbd5
docs: promote Commands and UI Elicitation to top-level sections in .N…
MRayermannMSFT Mar 31, 2026
bce4b2d
fix: address human review feedback
MRayermannMSFT Apr 1, 2026
3c2abae
refactor: merge ElicitationRequest + ElicitationInvocation into Elici…
MRayermannMSFT Apr 2, 2026
4a51cd2
refactor: apply ElicitationContext rename to Node.js SDK
MRayermannMSFT Apr 2, 2026
3518f99
style: fix formatting (prettier, ruff, trailing newlines)
MRayermannMSFT Apr 2, 2026
dd594c0
style: fix Python import sort order in __init__.py
MRayermannMSFT Apr 2, 2026
d5fd57d
fix: simplify Ui auto-property and remove empty snapshot files
MRayermannMSFT Apr 2, 2026
afc1937
fix: rename misleading command test names
MRayermannMSFT Apr 2, 2026
c191b4d
fix: remove leftover JSDoc from ElicitationRequest rename
MRayermannMSFT Apr 2, 2026
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
133 changes: 133 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,95 @@ var safeLookup = AIFunctionFactory.Create(
});
```

## Commands

Register slash commands so that users of the CLI's TUI can invoke custom actions via `/commandName`. Each command has a `Name`, optional `Description`, and a `Handler` called when the user executes it.

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = PermissionHandler.ApproveAll,
Commands =
[
new CommandDefinition
{
Name = "deploy",
Description = "Deploy the app to production",
Handler = async (context) =>
{
Console.WriteLine($"Deploying with args: {context.Args}");
// Do work here — any thrown error is reported back to the CLI
},
},
],
});
```

When the user types `/deploy staging` in the CLI, the SDK receives a `command.execute` event, routes it to your handler, and automatically responds to the CLI. If the handler throws, the error message is forwarded.

Commands are sent to the CLI on both `CreateSessionAsync` and `ResumeSessionAsync`, so you can update the command set when resuming.

## UI Elicitation

When the session has elicitation support — either from the CLI's TUI or from another client that registered an `OnElicitationRequest` handler (see [Elicitation Requests](#elicitation-requests)) — the SDK can request interactive form dialogs from the user. The `session.Ui` object provides convenience methods built on a single generic elicitation RPC.

> **Capability check:** Elicitation is only available when at least one connected participant advertises support. Always check `session.Capabilities.Ui?.Elicitation` before calling UI methods — this property updates automatically as participants join and leave.

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = PermissionHandler.ApproveAll,
});

if (session.Capabilities.Ui?.Elicitation == true)
{
// Confirm dialog — returns boolean
bool ok = await session.Ui.ConfirmAsync("Deploy to production?");

// Selection dialog — returns selected value or null
string? env = await session.Ui.SelectAsync("Pick environment",
["production", "staging", "dev"]);

// Text input — returns string or null
string? name = await session.Ui.InputAsync("Project name:", new InputOptions
{
Title = "Name",
MinLength = 1,
MaxLength = 50,
});

// Generic elicitation with full schema control
ElicitationResult result = await session.Ui.ElicitationAsync(new ElicitationParams
{
Message = "Configure deployment",
RequestedSchema = new ElicitationSchema
{
Type = "object",
Properties = new Dictionary<string, object>
{
["region"] = new Dictionary<string, object>
{
["type"] = "string",
["enum"] = new[] { "us-east", "eu-west" },
},
["dryRun"] = new Dictionary<string, object>
{
["type"] = "boolean",
["default"] = true,
},
},
Required = ["region"],
},
});
// result.Action: Accept, Decline, or Cancel
// result.Content: { "region": "us-east", "dryRun": true } (when accepted)
}
```

All UI methods throw if elicitation is not supported by the host.

### System Message Customization

Control the system prompt using `SystemMessage` in session config:
Expand Down Expand Up @@ -812,6 +901,50 @@ var session = await client.CreateSessionAsync(new SessionConfig
- `OnSessionEnd` - Cleanup or logging when session ends.
- `OnErrorOccurred` - Handle errors with retry/skip/abort strategies.

## Elicitation Requests

Register an `OnElicitationRequest` handler to let your client act as an elicitation provider — presenting form-based UI dialogs on behalf of the agent. When provided, the server notifies your client whenever a tool or MCP server needs structured user input.

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = PermissionHandler.ApproveAll,
OnElicitationRequest = async (context) =>
{
// context.SessionId - Session that triggered the request
// context.Message - Description of what information is needed
// context.RequestedSchema - JSON Schema describing the form fields
// context.Mode - "form" (structured input) or "url" (browser redirect)
// context.ElicitationSource - Origin of the request (e.g. MCP server name)

Console.WriteLine($"Elicitation from {context.ElicitationSource}: {context.Message}");

// Present UI to the user and collect their response...
return new ElicitationResult
{
Action = SessionUiElicitationResultAction.Accept,
Content = new Dictionary<string, object>
{
["region"] = "us-east",
["dryRun"] = true,
},
};
},
});

// The session now reports elicitation capability
Console.WriteLine(session.Capabilities.Ui?.Elicitation); // True
```

When `OnElicitationRequest` is provided, the SDK sends `RequestElicitation = true` during session create/resume, which enables `session.Capabilities.Ui.Elicitation` on the session.

In multi-client scenarios:

- If no connected client was previously providing an elicitation capability, but a new client joins that can, all clients will receive a `capabilities.changed` event to notify them that elicitation is now possible. The SDK automatically updates `session.Capabilities` when these events arrive.
- Similarly, if the last elicitation provider disconnects, all clients receive a `capabilities.changed` event indicating elicitation is no longer available.
- The server fans out elicitation requests to **all** connected clients that registered a handler — the first response wins.

## Error Handling

```csharp
Expand Down
35 changes: 29 additions & 6 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
session.RegisterCommands(config.Commands);
session.RegisterElicitationHandler(config.OnElicitationRequest);
if (config.OnUserInputRequest != null)
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
Expand Down Expand Up @@ -501,13 +503,16 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions,
traceparent,
tracestate);
Commands: config.Commands?.Select(c => new CommandWireDefinition(c.Name, c.Description)).ToList(),
RequestElicitation: config.OnElicitationRequest != null,
Traceparent: traceparent,
Tracestate: tracestate);

var response = await InvokeRpcAsync<CreateSessionResponse>(
connection.Rpc, "session.create", [request], cancellationToken);

session.WorkspacePath = response.WorkspacePath;
session.SetCapabilities(response.Capabilities);
}
catch
{
Expand Down Expand Up @@ -570,6 +575,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
session.RegisterCommands(config.Commands);
session.RegisterElicitationHandler(config.OnElicitationRequest);
if (config.OnUserInputRequest != null)
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
Expand Down Expand Up @@ -616,13 +623,16 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions,
traceparent,
tracestate);
Commands: config.Commands?.Select(c => new CommandWireDefinition(c.Name, c.Description)).ToList(),
RequestElicitation: config.OnElicitationRequest != null,
Traceparent: traceparent,
Tracestate: tracestate);

var response = await InvokeRpcAsync<ResumeSessionResponse>(
connection.Rpc, "session.resume", [request], cancellationToken);

session.WorkspacePath = response.WorkspacePath;
session.SetCapabilities(response.Capabilities);
}
catch
{
Expand Down Expand Up @@ -1592,6 +1602,8 @@ internal record CreateSessionRequest(
List<string>? SkillDirectories,
List<string>? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions,
List<CommandWireDefinition>? Commands = null,
bool? RequestElicitation = null,
string? Traceparent = null,
string? Tracestate = null);

Expand All @@ -1614,7 +1626,8 @@ public static ToolDefinition FromAIFunction(AIFunction function)

internal record CreateSessionResponse(
string SessionId,
string? WorkspacePath);
string? WorkspacePath,
SessionCapabilities? Capabilities = null);

internal record ResumeSessionRequest(
string SessionId,
Expand All @@ -1640,12 +1653,19 @@ internal record ResumeSessionRequest(
List<string>? SkillDirectories,
List<string>? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions,
List<CommandWireDefinition>? Commands = null,
bool? RequestElicitation = null,
string? Traceparent = null,
string? Tracestate = null);

internal record ResumeSessionResponse(
string SessionId,
string? WorkspacePath);
string? WorkspacePath,
SessionCapabilities? Capabilities = null);

internal record CommandWireDefinition(
string Name,
string? Description);

internal record GetLastSessionIdResponse(
string? SessionId);
Expand Down Expand Up @@ -1782,9 +1802,12 @@ private static LogLevel MapLevel(TraceEventType eventType)
[JsonSerializable(typeof(ProviderConfig))]
[JsonSerializable(typeof(ResumeSessionRequest))]
[JsonSerializable(typeof(ResumeSessionResponse))]
[JsonSerializable(typeof(SessionCapabilities))]
[JsonSerializable(typeof(SessionUiCapabilities))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SystemMessageConfig))]
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
[JsonSerializable(typeof(CommandWireDefinition))]
[JsonSerializable(typeof(ToolCallResponseV2))]
[JsonSerializable(typeof(ToolDefinition))]
[JsonSerializable(typeof(ToolResultAIContent))]
Expand Down
Loading
Loading