Skip to content
Draft
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
39 changes: 39 additions & 0 deletions dev-caveats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Dev Environment Caveats

What's available (and what isn't) for implementing the SDK gaps on this machine.

---

## Language Runtimes

| Language | Available | Version | Notes |
|----------|-----------|---------|-------|
| **Node.js** | ✅ | v24.13.0 | npm 11.6.3, vitest 4.0.18, `node_modules` + `@github/copilot` present |
| **Python** | ✅ | 3.14.3 | pip 25.3, pytest 9.0.2, SDK installed in editable mode |
| **.NET** | ✅ | 10.0.201 | Builds and restores cleanly, test project compiles |
| **Go** | ❌ | — | `go` and `gofmt` not on PATH. **Cannot build, test, or format Go code.** |

## Test Suites

| SDK | Unit Tests | E2E Tests |
|-----|-----------|-----------|
| **Node.js** | ✅ vitest works | ✅ harness + snapshots available |
| **Python** | ✅ 70/70 pass (ignoring e2e/) | ⚠️ E2E hangs — harness spawns but tests don't connect (likely harness startup race on Windows) |
| **.NET** | ✅ 149 pass, 6 skipped, 0 failed | ✅ Included in main test project |
| **Go** | ❌ Can't run | ❌ Can't run |

## Missing Tools

| Tool | Used For | Impact |
|------|----------|--------|
| `go` | Build, test, `go fmt` | **Cannot work on Go SDK at all** |
| `gofmt` | Format generated Go code | Blocked by missing Go runtime |
| `uv` | Python fast installer (used by `just install`) | Not critical — `pip install -e ".[dev]"` works fine as a substitute |
| `just` | Monorepo task runner | Not critical — can run per-language commands directly |

## Recommendations

1. **Python and .NET are fully workable** — code, unit-test, and iterate without issues.
2. **Go is blocked** — install Go (1.21+) and add it to PATH before attempting Go SDK work.
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.
4. **Node.js** is the reference implementation and fully functional for cross-referencing.
132 changes: 132 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,49 @@ 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 (request, invocation) =>
{
// request.Message - Description of what information is needed
// request.RequestedSchema - JSON Schema describing the form fields
// request.Mode - "form" (structured input) or "url" (browser redirect)
// request.ElicitationSource - Origin of the request (e.g. MCP server name)

Console.WriteLine($"Elicitation from {request.ElicitationSource}: {request.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