Skip to content

Commit 357a0b0

Browse files
Close Language Gaps for Commands + Dialogs/Elicitations
1 parent 4d26e30 commit 357a0b0

38 files changed

Lines changed: 4962 additions & 27 deletions

dev-caveats.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Dev Environment Caveats
2+
3+
What's available (and what isn't) for implementing the SDK gaps on this machine.
4+
5+
---
6+
7+
## Language Runtimes
8+
9+
| Language | Available | Version | Notes |
10+
|----------|-----------|---------|-------|
11+
| **Node.js** || v24.13.0 | npm 11.6.3, vitest 4.0.18, `node_modules` + `@github/copilot` present |
12+
| **Python** || 3.14.3 | pip 25.3, pytest 9.0.2, SDK installed in editable mode |
13+
| **.NET** || 10.0.201 | Builds and restores cleanly, test project compiles |
14+
| **Go** ||| `go` and `gofmt` not on PATH. **Cannot build, test, or format Go code.** |
15+
16+
## Test Suites
17+
18+
| SDK | Unit Tests | E2E Tests |
19+
|-----|-----------|-----------|
20+
| **Node.js** | ✅ vitest works | ✅ harness + snapshots available |
21+
| **Python** | ✅ 70/70 pass (ignoring e2e/) | ⚠️ E2E hangs — harness spawns but tests don't connect (likely harness startup race on Windows) |
22+
| **.NET** | ✅ 149 pass, 6 skipped, 0 failed | ✅ Included in main test project |
23+
| **Go** | ❌ Can't run | ❌ Can't run |
24+
25+
## Missing Tools
26+
27+
| Tool | Used For | Impact |
28+
|------|----------|--------|
29+
| `go` | Build, test, `go fmt` | **Cannot work on Go SDK at all** |
30+
| `gofmt` | Format generated Go code | Blocked by missing Go runtime |
31+
| `uv` | Python fast installer (used by `just install`) | Not critical — `pip install -e ".[dev]"` works fine as a substitute |
32+
| `just` | Monorepo task runner | Not critical — can run per-language commands directly |
33+
34+
## Recommendations
35+
36+
1. **Python and .NET are fully workable** — code, unit-test, and iterate without issues.
37+
2. **Go is blocked** — install Go (1.21+) and add it to PATH before attempting Go SDK work.
38+
3. **Python E2E tests** may need manual attention on Windows — unit tests are sufficient for validating SDK-layer changes; E2E can be verified in CI.
39+
4. **Node.js** is the reference implementation and fully functional for cross-referencing.

dotnet/README.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,95 @@ var safeLookup = AIFunctionFactory.Create(
488488
});
489489
```
490490

491+
### Commands
492+
493+
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.
494+
495+
```csharp
496+
var session = await client.CreateSessionAsync(new SessionConfig
497+
{
498+
Model = "gpt-5",
499+
OnPermissionRequest = PermissionHandler.ApproveAll,
500+
Commands =
501+
[
502+
new CommandDefinition
503+
{
504+
Name = "deploy",
505+
Description = "Deploy the app to production",
506+
Handler = async (context) =>
507+
{
508+
Console.WriteLine($"Deploying with args: {context.Args}");
509+
// Do work here — any thrown error is reported back to the CLI
510+
},
511+
},
512+
],
513+
});
514+
```
515+
516+
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.
517+
518+
Commands are sent to the CLI on both `CreateSessionAsync` and `ResumeSessionAsync`, so you can update the command set when resuming.
519+
520+
### UI Elicitation
521+
522+
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.
523+
524+
> **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.
525+
526+
```csharp
527+
var session = await client.CreateSessionAsync(new SessionConfig
528+
{
529+
Model = "gpt-5",
530+
OnPermissionRequest = PermissionHandler.ApproveAll,
531+
});
532+
533+
if (session.Capabilities.Ui?.Elicitation == true)
534+
{
535+
// Confirm dialog — returns boolean
536+
bool ok = await session.Ui.ConfirmAsync("Deploy to production?");
537+
538+
// Selection dialog — returns selected value or null
539+
string? env = await session.Ui.SelectAsync("Pick environment",
540+
["production", "staging", "dev"]);
541+
542+
// Text input — returns string or null
543+
string? name = await session.Ui.InputAsync("Project name:", new InputOptions
544+
{
545+
Title = "Name",
546+
MinLength = 1,
547+
MaxLength = 50,
548+
});
549+
550+
// Generic elicitation with full schema control
551+
ElicitationResult result = await session.Ui.ElicitationAsync(new ElicitationParams
552+
{
553+
Message = "Configure deployment",
554+
RequestedSchema = new ElicitationSchema
555+
{
556+
Type = "object",
557+
Properties = new Dictionary<string, object>
558+
{
559+
["region"] = new Dictionary<string, object>
560+
{
561+
["type"] = "string",
562+
["enum"] = new[] { "us-east", "eu-west" },
563+
},
564+
["dryRun"] = new Dictionary<string, object>
565+
{
566+
["type"] = "boolean",
567+
["default"] = true,
568+
},
569+
},
570+
Required = ["region"],
571+
},
572+
});
573+
// result.Action: Accept, Decline, or Cancel
574+
// result.Content: { "region": "us-east", "dryRun": true } (when accepted)
575+
}
576+
```
577+
578+
All UI methods throw if elicitation is not supported by the host.
579+
491580
### System Message Customization
492581

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

904+
## Elicitation Requests
905+
906+
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.
907+
908+
```csharp
909+
var session = await client.CreateSessionAsync(new SessionConfig
910+
{
911+
Model = "gpt-5",
912+
OnPermissionRequest = PermissionHandler.ApproveAll,
913+
OnElicitationRequest = async (request, invocation) =>
914+
{
915+
// request.Message - Description of what information is needed
916+
// request.RequestedSchema - JSON Schema describing the form fields
917+
// request.Mode - "form" (structured input) or "url" (browser redirect)
918+
// request.ElicitationSource - Origin of the request (e.g. MCP server name)
919+
920+
Console.WriteLine($"Elicitation from {request.ElicitationSource}: {request.Message}");
921+
922+
// Present UI to the user and collect their response...
923+
return new ElicitationResult
924+
{
925+
Action = SessionUiElicitationResultAction.Accept,
926+
Content = new Dictionary<string, object>
927+
{
928+
["region"] = "us-east",
929+
["dryRun"] = true,
930+
},
931+
};
932+
},
933+
});
934+
935+
// The session now reports elicitation capability
936+
Console.WriteLine(session.Capabilities.Ui?.Elicitation); // True
937+
```
938+
939+
When `OnElicitationRequest` is provided, the SDK sends `RequestElicitation = true` during session create/resume, which enables `session.Capabilities.Ui.Elicitation` on the session.
940+
941+
In multi-client scenarios:
942+
943+
- 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.
944+
- Similarly, if the last elicitation provider disconnects, all clients receive a `capabilities.changed` event indicating elicitation is no longer available.
945+
- The server fans out elicitation requests to **all** connected clients that registered a handler — the first response wins.
946+
815947
## Error Handling
816948

817949
```csharp

dotnet/src/Client.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
456456
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
457457
session.RegisterTools(config.Tools ?? []);
458458
session.RegisterPermissionHandler(config.OnPermissionRequest);
459+
session.RegisterCommands(config.Commands);
460+
session.RegisterElicitationHandler(config.OnElicitationRequest);
459461
if (config.OnUserInputRequest != null)
460462
{
461463
session.RegisterUserInputHandler(config.OnUserInputRequest);
@@ -501,13 +503,16 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
501503
config.SkillDirectories,
502504
config.DisabledSkills,
503505
config.InfiniteSessions,
504-
traceparent,
505-
tracestate);
506+
Commands: config.Commands?.Select(c => new CommandWireDefinition(c.Name, c.Description)).ToList(),
507+
RequestElicitation: config.OnElicitationRequest != null ? true : false,
508+
Traceparent: traceparent,
509+
Tracestate: tracestate);
506510

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

510514
session.WorkspacePath = response.WorkspacePath;
515+
session.SetCapabilities(response.Capabilities);
511516
}
512517
catch
513518
{
@@ -570,6 +575,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
570575
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
571576
session.RegisterTools(config.Tools ?? []);
572577
session.RegisterPermissionHandler(config.OnPermissionRequest);
578+
session.RegisterCommands(config.Commands);
579+
session.RegisterElicitationHandler(config.OnElicitationRequest);
573580
if (config.OnUserInputRequest != null)
574581
{
575582
session.RegisterUserInputHandler(config.OnUserInputRequest);
@@ -616,13 +623,16 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
616623
config.SkillDirectories,
617624
config.DisabledSkills,
618625
config.InfiniteSessions,
619-
traceparent,
620-
tracestate);
626+
Commands: config.Commands?.Select(c => new CommandWireDefinition(c.Name, c.Description)).ToList(),
627+
RequestElicitation: config.OnElicitationRequest != null ? true : false,
628+
Traceparent: traceparent,
629+
Tracestate: tracestate);
621630

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

625634
session.WorkspacePath = response.WorkspacePath;
635+
session.SetCapabilities(response.Capabilities);
626636
}
627637
catch
628638
{
@@ -1592,6 +1602,8 @@ internal record CreateSessionRequest(
15921602
List<string>? SkillDirectories,
15931603
List<string>? DisabledSkills,
15941604
InfiniteSessionConfig? InfiniteSessions,
1605+
List<CommandWireDefinition>? Commands = null,
1606+
bool? RequestElicitation = null,
15951607
string? Traceparent = null,
15961608
string? Tracestate = null);
15971609

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

16151627
internal record CreateSessionResponse(
16161628
string SessionId,
1617-
string? WorkspacePath);
1629+
string? WorkspacePath,
1630+
SessionCapabilities? Capabilities = null);
16181631

16191632
internal record ResumeSessionRequest(
16201633
string SessionId,
@@ -1640,12 +1653,19 @@ internal record ResumeSessionRequest(
16401653
List<string>? SkillDirectories,
16411654
List<string>? DisabledSkills,
16421655
InfiniteSessionConfig? InfiniteSessions,
1656+
List<CommandWireDefinition>? Commands = null,
1657+
bool? RequestElicitation = null,
16431658
string? Traceparent = null,
16441659
string? Tracestate = null);
16451660

16461661
internal record ResumeSessionResponse(
16471662
string SessionId,
1648-
string? WorkspacePath);
1663+
string? WorkspacePath,
1664+
SessionCapabilities? Capabilities = null);
1665+
1666+
internal record CommandWireDefinition(
1667+
string Name,
1668+
string? Description);
16491669

16501670
internal record GetLastSessionIdResponse(
16511671
string? SessionId);
@@ -1782,9 +1802,12 @@ private static LogLevel MapLevel(TraceEventType eventType)
17821802
[JsonSerializable(typeof(ProviderConfig))]
17831803
[JsonSerializable(typeof(ResumeSessionRequest))]
17841804
[JsonSerializable(typeof(ResumeSessionResponse))]
1805+
[JsonSerializable(typeof(SessionCapabilities))]
1806+
[JsonSerializable(typeof(SessionUiCapabilities))]
17851807
[JsonSerializable(typeof(SessionMetadata))]
17861808
[JsonSerializable(typeof(SystemMessageConfig))]
17871809
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
1810+
[JsonSerializable(typeof(CommandWireDefinition))]
17881811
[JsonSerializable(typeof(ToolCallResponseV2))]
17891812
[JsonSerializable(typeof(ToolDefinition))]
17901813
[JsonSerializable(typeof(ToolResultAIContent))]

0 commit comments

Comments
 (0)