From 296c48ec5db7e4e6c62e6d6be33c654741a90711 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 23 Mar 2026 17:15:49 -0700 Subject: [PATCH 1/6] [Node] Add onElicitationRequest Callback for Elicitation Provider Support --- nodejs/README.md | 42 ++++++- nodejs/src/client.ts | 50 ++++++++ nodejs/src/index.ts | 2 + nodejs/src/session.ts | 30 +++++ nodejs/src/types.ts | 32 ++++++ nodejs/test/client.test.ts | 46 ++++++++ nodejs/test/e2e/ui_elicitation.test.ts | 152 ++++++++++++++++++++++++- 7 files changed, 350 insertions(+), 4 deletions(-) diff --git a/nodejs/README.md b/nodejs/README.md index ce2754212..f3c60e7e0 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -120,6 +120,7 @@ Create a new conversation session. - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. - `onPermissionRequest: PermissionHandler` - **Required.** Handler called before each tool execution to approve or deny it. Use `approveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. ##### `resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise` @@ -293,6 +294,8 @@ if (session.capabilities.ui?.elicitation) { } ``` +Capabilities may update during the session. For example, when another client joins or disconnects with an elicitation handler. The SDK automatically applies `capabilities.changed` events, so this property always reflects the current state. + ##### `ui: SessionUiApi` Interactive UI methods for showing dialogs to the user. Only available when the CLI host supports elicitation (`session.capabilities.ui?.elicitation === true`). See [UI Elicitation](#ui-elicitation) for full details. @@ -505,9 +508,9 @@ Commands are sent to the CLI on both `createSession` and `resumeSession`, so you ### UI Elicitation -When the CLI is running with a TUI (not in headless mode), the SDK can request interactive form dialogs from the user. The `session.ui` object provides convenience methods built on a single generic `elicitation` RPC. +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 the host advertises support. Always check `session.capabilities.ui?.elicitation` before calling UI methods. +> **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. ```ts const session = await client.createSession({ onPermissionRequest: approveAll }); @@ -899,6 +902,41 @@ const session = await client.createSession({ }); ``` +## 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 dispatches `elicitation.request` RPCs to your client whenever a tool or MCP server needs structured user input. + +```typescript +const session = await client.createSession({ + model: "gpt-5", + onPermissionRequest: 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.log(`Elicitation from ${request.elicitationSource}: ${request.message}`); + + // Present UI to the user and collect their response... + return { + action: "accept", // "accept", "decline", or "cancel" + content: { region: "us-east", dryRun: true }, + }; + }, +}); + +// The session now reports elicitation capability +console.log(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. + ## Session Hooks Hook into session lifecycle events by providing handlers in the `hooks` configuration: diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 5a528488f..5578819af 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -31,6 +31,8 @@ import { getTraceContext } from "./telemetry.js"; import type { ConnectionState, CopilotClientOptions, + ElicitationRequest, + ElicitationResult, ForegroundSessionInfo, GetAuthStatusResponse, GetStatusResponse, @@ -647,6 +649,9 @@ export class CopilotClient { if (config.onUserInputRequest) { session.registerUserInputHandler(config.onUserInputRequest); } + if (config.onElicitationRequest) { + session.registerElicitationHandler(config.onElicitationRequest); + } if (config.hooks) { session.registerHooks(config.hooks); } @@ -688,6 +693,7 @@ export class CopilotClient { provider: config.provider, requestPermission: true, requestUserInput: !!config.onUserInputRequest, + requestElicitation: !!config.onElicitationRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, streaming: config.streaming, @@ -769,6 +775,9 @@ export class CopilotClient { if (config.onUserInputRequest) { session.registerUserInputHandler(config.onUserInputRequest); } + if (config.onElicitationRequest) { + session.registerElicitationHandler(config.onElicitationRequest); + } if (config.hooks) { session.registerHooks(config.hooks); } @@ -810,6 +819,7 @@ export class CopilotClient { provider: config.provider, requestPermission: true, requestUserInput: !!config.onUserInputRequest, + requestElicitation: !!config.onElicitationRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, configDir: config.configDir, @@ -1597,6 +1607,18 @@ export class CopilotClient { await this.handleUserInputRequest(params) ); + this.connection.onRequest( + "elicitation.request", + async (params: { + sessionId: string; + requestId: string; + message: string; + requestedSchema?: unknown; + mode?: "form" | "url"; + elicitationSource?: string; + }): Promise => await this.handleElicitationRequest(params) + ); + this.connection.onRequest( "hooks.invoke", async (params: { @@ -1704,6 +1726,34 @@ export class CopilotClient { return result; } + private async handleElicitationRequest(params: { + sessionId: string; + requestId: string; + message: string; + requestedSchema?: unknown; + mode?: "form" | "url"; + elicitationSource?: string; + }): Promise { + if (!params || typeof params.sessionId !== "string" || typeof params.message !== "string") { + throw new Error("Invalid elicitation request payload"); + } + + const session = this.sessions.get(params.sessionId); + if (!session) { + throw new Error(`Session not found: ${params.sessionId}`); + } + + return await session._handleElicitationRequest( + { + message: params.message, + requestedSchema: params.requestedSchema as ElicitationRequest["requestedSchema"], + mode: params.mode, + elicitationSource: params.elicitationSource, + }, + params.sessionId + ); + } + private async handleHooksInvoke(params: { sessionId: string; hookType: string; diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index c42935a26..4fc1b75fb 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -19,7 +19,9 @@ export type { CopilotClientOptions, CustomAgentConfig, ElicitationFieldValue, + ElicitationHandler, ElicitationParams, + ElicitationRequest, ElicitationResult, ElicitationSchema, ElicitationSchemaField, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 7a0220f6f..11dc8c19f 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -13,8 +13,10 @@ import { createSessionRpc } from "./generated/rpc.js"; import { getTraceContext } from "./telemetry.js"; import type { CommandHandler, + ElicitationHandler, ElicitationParams, ElicitationResult, + ElicitationRequest, InputOptions, MessageOptions, PermissionHandler, @@ -77,6 +79,7 @@ export class CopilotSession { private commandHandlers: Map = new Map(); private permissionHandler?: PermissionHandler; private userInputHandler?: UserInputHandler; + private elicitationHandler?: ElicitationHandler; private hooks?: SessionHooks; private transformCallbacks?: Map; private _rpc: ReturnType | null = null; @@ -414,6 +417,9 @@ export class CopilotSession { args: string; }; void this._executeCommandAndRespond(requestId, commandName, command, args); + } else if ((event as { type: string }).type === "capabilities.changed") { + const data = (event as { data: Partial }).data; + this._capabilities = { ...this._capabilities, ...data }; } } @@ -581,6 +587,30 @@ export class CopilotSession { } } + /** + * Registers the elicitation handler for this session. + * + * @param handler - The handler to invoke when the server dispatches an elicitation request + * @internal This method is typically called internally when creating/resuming a session. + */ + registerElicitationHandler(handler?: ElicitationHandler): void { + this.elicitationHandler = handler; + } + + /** + * Handles an elicitation.request RPC callback from the server. + * @internal + */ + async _handleElicitationRequest( + request: ElicitationRequest, + sessionId: string + ): Promise { + if (!this.elicitationHandler) { + throw new Error("Elicitation requested but no handler registered"); + } + return await this.elicitationHandler(request, { sessionId }); + } + /** * Sets the host capabilities for this session. * diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 96694137d..a84f5089e 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -409,6 +409,30 @@ export interface ElicitationParams { requestedSchema: ElicitationSchema; } +/** + * Request payload passed to an elicitation handler callback. + * Extends ElicitationParams with optional metadata fields. + */ +export interface ElicitationRequest { + /** Message describing what information is needed from the user. */ + message: string; + /** JSON Schema describing the form fields to present. */ + requestedSchema?: ElicitationSchema; + /** Elicitation mode: "form" for structured input, "url" for browser redirect. */ + mode?: "form" | "url"; + /** The source that initiated the request (e.g. MCP server name). */ + elicitationSource?: string; +} + +/** + * Handler invoked when the server dispatches an elicitation request to this client. + * Return an {@link ElicitationResult} with the user's response. + */ +export type ElicitationHandler = ( + request: ElicitationRequest, + invocation: { sessionId: string } +) => Promise | ElicitationResult; + /** * Options for the `input()` convenience method. */ @@ -1082,6 +1106,13 @@ export interface SessionConfig { */ onUserInputRequest?: UserInputHandler; + /** + * Handler for elicitation requests from the agent. + * When provided, the server calls back to this client for form-based UI dialogs. + * Also enables the `elicitation` capability on the session. + */ + onElicitationRequest?: ElicitationHandler; + /** * Hook handlers for intercepting session lifecycle events. * When provided, enables hooks callback allowing custom logic at various points. @@ -1167,6 +1198,7 @@ export type ResumeSessionConfig = Pick< | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" + | "onElicitationRequest" | "hooks" | "workingDirectory" | "configDir" diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 0612cc39e..3037a5c9d 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -897,5 +897,51 @@ describe("CopilotClient", () => { }) ).rejects.toThrow(/not supported/); }); + + it("sends requestElicitation flag when onElicitationRequest is provided", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const rpcSpy = vi.spyOn((client as any).connection!, "sendRequest"); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + onElicitationRequest: async () => ({ + action: "accept" as const, + content: {}, + }), + }); + + const createCall = rpcSpy.mock.calls.find((c) => c[0] === "session.create"); + expect(createCall).toBeDefined(); + expect(createCall![1]).toEqual( + expect.objectContaining({ + requestElicitation: true, + }) + ); + rpcSpy.mockRestore(); + }); + + it("does not send requestElicitation when no handler provided", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const rpcSpy = vi.spyOn((client as any).connection!, "sendRequest"); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + }); + + const createCall = rpcSpy.mock.calls.find((c) => c[0] === "session.create"); + expect(createCall).toBeDefined(); + expect(createCall![1]).toEqual( + expect.objectContaining({ + requestElicitation: false, + }) + ); + rpcSpy.mockRestore(); + }); }); }); diff --git a/nodejs/test/e2e/ui_elicitation.test.ts b/nodejs/test/e2e/ui_elicitation.test.ts index 212f481fb..099aad0c4 100644 --- a/nodejs/test/e2e/ui_elicitation.test.ts +++ b/nodejs/test/e2e/ui_elicitation.test.ts @@ -2,8 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { describe, expect, it } from "vitest"; -import { approveAll } from "../../src/index.js"; +import { afterAll, describe, expect, it } from "vitest"; +import { CopilotClient, approveAll } from "../../src/index.js"; +import type { SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("UI Elicitation", async () => { @@ -19,3 +20,150 @@ describe("UI Elicitation", async () => { await expect(session.ui.confirm("test")).rejects.toThrow(/not supported/); }); }); + +describe("UI Elicitation Callback", async () => { + const ctx = await createSdkTestContext(); + const client = ctx.copilotClient; + + it( + "session created with onElicitationRequest reports elicitation capability", + { timeout: 20_000 }, + async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + onElicitationRequest: async () => ({ action: "accept", content: {} }), + }); + + expect(session.capabilities.ui?.elicitation).toBe(true); + } + ); + + it( + "session created without onElicitationRequest reports no elicitation capability", + { timeout: 20_000 }, + async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + }); + + expect(session.capabilities.ui?.elicitation).toBe(false); + } + ); +}); + +describe("UI Elicitation Multi-Client Capabilities", async () => { + // Use TCP mode so a second client can connect to the same CLI process + const ctx = await createSdkTestContext({ useStdio: false }); + const client1 = ctx.copilotClient; + + // Trigger connection so we can read the port + const initSession = await client1.createSession({ onPermissionRequest: approveAll }); + await initSession.disconnect(); + + const actualPort = (client1 as unknown as { actualPort: number }).actualPort; + const client2 = new CopilotClient({ cliUrl: `localhost:${actualPort}` }); + + afterAll(async () => { + await client2.stop(); + }); + + it( + "capabilities.changed fires when second client joins with elicitation handler", + { timeout: 20_000 }, + async () => { + // Client1 creates session without elicitation + const session1 = await client1.createSession({ + onPermissionRequest: approveAll, + }); + expect(session1.capabilities.ui?.elicitation).toBe(false); + + // Listen for capabilities.changed event + const capChangedPromise = new Promise((resolve) => { + session1.on((event) => { + if ((event as { type: string }).type === "capabilities.changed") { + resolve(event); + } + }); + }); + + // Client2 joins WITH elicitation handler — triggers capabilities.changed + const session2 = await client2.resumeSession(session1.sessionId, { + onPermissionRequest: approveAll, + onElicitationRequest: async () => ({ action: "accept", content: {} }), + disableResume: true, + }); + + const capEvent = await capChangedPromise; + const data = (capEvent as { data: { ui?: { elicitation?: boolean } } }).data; + expect(data.ui?.elicitation).toBe(true); + + // Client1's capabilities should have been auto-updated + expect(session1.capabilities.ui?.elicitation).toBe(true); + + await session2.disconnect(); + } + ); + + it( + "capabilities.changed fires when elicitation provider disconnects", + { timeout: 20_000 }, + async () => { + // Client1 creates session without elicitation + const session1 = await client1.createSession({ + onPermissionRequest: approveAll, + }); + expect(session1.capabilities.ui?.elicitation).toBe(false); + + // Wait for elicitation to become available + const capEnabledPromise = new Promise((resolve) => { + session1.on((event) => { + const data = event as { + type: string; + data: { ui?: { elicitation?: boolean } }; + }; + if ( + data.type === "capabilities.changed" && + data.data.ui?.elicitation === true + ) { + resolve(); + } + }); + }); + + // Use a dedicated client so we can stop it without affecting shared client2 + const client3 = new CopilotClient({ cliUrl: `localhost:${actualPort}` }); + + // Client3 joins WITH elicitation handler + await client3.resumeSession(session1.sessionId, { + onPermissionRequest: approveAll, + onElicitationRequest: async () => ({ action: "accept", content: {} }), + disableResume: true, + }); + + await capEnabledPromise; + expect(session1.capabilities.ui?.elicitation).toBe(true); + + // Now listen for the capability being removed + const capDisabledPromise = new Promise((resolve) => { + session1.on((event) => { + const data = event as { + type: string; + data: { ui?: { elicitation?: boolean } }; + }; + if ( + data.type === "capabilities.changed" && + data.data.ui?.elicitation === false + ) { + resolve(); + } + }); + }); + + // Force-stop client3 — destroys the socket, triggering server-side cleanup + await client3.forceStop(); + + await capDisabledPromise; + expect(session1.capabilities.ui?.elicitation).toBe(false); + } + ); +}); From 8043d3d183dba206662241f224f0ae5d63304c27 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Tue, 24 Mar 2026 17:15:56 -0700 Subject: [PATCH 2/6] React to runtime changes --- nodejs/src/client.ts | 42 ---------------------------------------- nodejs/src/session.ts | 45 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 49 deletions(-) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 5578819af..50715c0eb 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -31,8 +31,6 @@ import { getTraceContext } from "./telemetry.js"; import type { ConnectionState, CopilotClientOptions, - ElicitationRequest, - ElicitationResult, ForegroundSessionInfo, GetAuthStatusResponse, GetStatusResponse, @@ -1607,18 +1605,6 @@ export class CopilotClient { await this.handleUserInputRequest(params) ); - this.connection.onRequest( - "elicitation.request", - async (params: { - sessionId: string; - requestId: string; - message: string; - requestedSchema?: unknown; - mode?: "form" | "url"; - elicitationSource?: string; - }): Promise => await this.handleElicitationRequest(params) - ); - this.connection.onRequest( "hooks.invoke", async (params: { @@ -1726,34 +1712,6 @@ export class CopilotClient { return result; } - private async handleElicitationRequest(params: { - sessionId: string; - requestId: string; - message: string; - requestedSchema?: unknown; - mode?: "form" | "url"; - elicitationSource?: string; - }): Promise { - if (!params || typeof params.sessionId !== "string" || typeof params.message !== "string") { - throw new Error("Invalid elicitation request payload"); - } - - const session = this.sessions.get(params.sessionId); - if (!session) { - throw new Error(`Session not found: ${params.sessionId}`); - } - - return await session._handleElicitationRequest( - { - message: params.message, - requestedSchema: params.requestedSchema as ElicitationRequest["requestedSchema"], - mode: params.mode, - elicitationSource: params.elicitationSource, - }, - params.sessionId - ); - } - private async handleHooksInvoke(params: { sessionId: string; hookType: string; diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 11dc8c19f..0a08c6261 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -417,6 +417,21 @@ export class CopilotSession { args: string; }; void this._executeCommandAndRespond(requestId, commandName, command, args); + } else if ((event as { type: string }).type === "elicitation.requested") { + // TODO: Remove type casts above once session-events codegen includes these event types + if (this.elicitationHandler) { + const data = (event as { data: Record }).data; + void this._handleElicitationRequest( + { + message: data.message as string, + requestedSchema: + data.requestedSchema as ElicitationRequest["requestedSchema"], + mode: data.mode as ElicitationRequest["mode"], + elicitationSource: data.elicitationSource as string | undefined, + }, + data.requestId as string + ); + } } else if ((event as { type: string }).type === "capabilities.changed") { const data = (event as { data: Partial }).data; this._capabilities = { ...this._capabilities, ...data }; @@ -598,17 +613,33 @@ export class CopilotSession { } /** - * Handles an elicitation.request RPC callback from the server. + * Handles an elicitation.requested broadcast event. + * Invokes the registered handler and responds via handlePendingElicitation RPC. * @internal */ - async _handleElicitationRequest( - request: ElicitationRequest, - sessionId: string - ): Promise { + async _handleElicitationRequest(request: ElicitationRequest, requestId: string): Promise { if (!this.elicitationHandler) { - throw new Error("Elicitation requested but no handler registered"); + return; + } + try { + const result = await this.elicitationHandler(request, { sessionId: this.sessionId }); + await this.connection.sendRequest("session.ui.handlePendingElicitation", { + sessionId: this.sessionId, + requestId, + result, + }); + } catch { + // Handler failed — attempt to cancel so the request doesn't hang + try { + await this.connection.sendRequest("session.ui.handlePendingElicitation", { + sessionId: this.sessionId, + requestId, + result: { action: "cancel" }, + }); + } catch { + // Best effort — another client may have already responded + } } - return await this.elicitationHandler(request, { sessionId }); } /** From 75c9be1c421981abaecc920a773b71de8a9b14e3 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 30 Mar 2026 10:58:44 -0700 Subject: [PATCH 3/6] Update to new CLI --- nodejs/package-lock.json | 56 +++++++++++++++++----------------- nodejs/package.json | 2 +- test/harness/package-lock.json | 56 +++++++++++++++++----------------- test/harness/package.json | 2 +- 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 4ddf50a2e..a3a94ac5e 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.12-0", + "@github/copilot": "^1.0.14-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.12-0.tgz", - "integrity": "sha512-tF8GQ5TZTP6ZJsD6J31SLdZAmawg9YnEe9jaf6+lwlOH7mA6XU/m9BLStdhdHd2MySoAu0Sb8IkVyEg/YIcWpg==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.14-0.tgz", + "integrity": "sha512-9eA5sFbvx69OtQnVoeik/8boFqHgGAhylLeUjEACc3kB70aaH1E/cHgxNzSMyYgZDjpXov0/IBXjtx2otpfHBw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.12-0", - "@github/copilot-darwin-x64": "1.0.12-0", - "@github/copilot-linux-arm64": "1.0.12-0", - "@github/copilot-linux-x64": "1.0.12-0", - "@github/copilot-win32-arm64": "1.0.12-0", - "@github/copilot-win32-x64": "1.0.12-0" + "@github/copilot-darwin-arm64": "1.0.14-0", + "@github/copilot-darwin-x64": "1.0.14-0", + "@github/copilot-linux-arm64": "1.0.14-0", + "@github/copilot-linux-x64": "1.0.14-0", + "@github/copilot-win32-arm64": "1.0.14-0", + "@github/copilot-win32-x64": "1.0.14-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.12-0.tgz", - "integrity": "sha512-GJNgo21Kh9fNJBOTF/vSc5YRXzwfGNsNufVFLzCnjppvs9ifN1s9VyPYdz+UOcDOrwh7FGPpRRQgWvm3EhTXAQ==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.14-0.tgz", + "integrity": "sha512-w11Eqmfnu0ihrvgLysTd5Tkq8LuQa9eW63CNTQ/k5copnG1AMCdvd3K/78MxE2DdFJPq2L95KGS5cs9jH1dlIw==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.12-0.tgz", - "integrity": "sha512-pc8f6mNvwDzc4LavH0Baz96WKx75Ti5/3EY0PF8HXOY/kz6x50cywIlRNqHQxK8NsTbTragbrQS7Eh7r6AJf/g==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.14-0.tgz", + "integrity": "sha512-4X/dMSPxCE/rvL6N1tgnwFxBg2uXnPrN63GGgS/FqK/fNi3TtcuojDVv8K1yjmEYpF8PXdkQttDlp6bKc+Nonw==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.12-0.tgz", - "integrity": "sha512-ZlIGo6I2qpkqPXJNgR1+wYF/yMFrENjCz5kh4TIohwkuwPxMfZc4rv+CgMoyRc7OWWjKBUi7Y7IInKWkSkxzVg==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.14-0.tgz", + "integrity": "sha512-A4thcLUoErEvfBO3Hsl/hJASibn44qwZm1ZSeVBPCa1FkpowBwo8fT1eV9EwN/ftKsyks3QkndNFvHkVzjUfxA==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.12-0.tgz", - "integrity": "sha512-4PTBR+cIFhggi6/UsyhgjND+e6tagtBB6w2iJG/Y+ZLbpryaLD8GiGg8xmrzNvMGD81qHdespXCbwiRKplBM/Q==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.14-0.tgz", + "integrity": "sha512-Kwn+Qn8/BqWRKa2DewZipH7rPIO8nDRWzpVy/ZLcRWBAvnIU+6BLWfhnYEU44DsqkD2VeWhKVfQlNmDX23xKKg==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.12-0.tgz", - "integrity": "sha512-Glz0QVGq7sEYReLki4KAVywHnKpxTG+xtJOC3q6aYmfqmrlkGAgo9y/tTbYVNLa2hd8P2gCWcNGIAYlkZQsgfQ==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.14-0.tgz", + "integrity": "sha512-8P5kxcb8YVWSS+Ihs+ykyy8jov1WwQ8GKV4d7mJN268Jpd8y5VI8Peb7uE2VO0lRLgq5c2VcXuZDsLG/1Wgnlw==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.12-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.12-0.tgz", - "integrity": "sha512-SzPRnIkzg5oMlDix/ggEic4IkkDquGAydleQ9wmPSp9LLp97TD+Fw8fV98HPitOiYRgvTHvDtgWtESgh6uKG1A==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.14-0.tgz", + "integrity": "sha512-JWxp08j5o/PUkRZtZVagNYJLjH+KCURCyZRb7BfnC0A3vLeqcJQ70JC5qlYEAlcRnb4uCUJnmnpbWLLOJ+ObrA==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 52ba0b153..1787721a8 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.12-0", + "@github/copilot": "^1.0.14-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 5ab4ae513..d1ee2fa24 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.11", + "@github/copilot": "^1.0.14-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", @@ -462,27 +462,27 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.11.tgz", - "integrity": "sha512-cptVopko/tNKEXyBP174yBjHQBEwg6CqaKN2S0M3J+5LEB8u31bLL75ioOPd+5vubqBrA0liyTdcHeZ8UTRbmg==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.14-0.tgz", + "integrity": "sha512-9eA5sFbvx69OtQnVoeik/8boFqHgGAhylLeUjEACc3kB70aaH1E/cHgxNzSMyYgZDjpXov0/IBXjtx2otpfHBw==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.11", - "@github/copilot-darwin-x64": "1.0.11", - "@github/copilot-linux-arm64": "1.0.11", - "@github/copilot-linux-x64": "1.0.11", - "@github/copilot-win32-arm64": "1.0.11", - "@github/copilot-win32-x64": "1.0.11" + "@github/copilot-darwin-arm64": "1.0.14-0", + "@github/copilot-darwin-x64": "1.0.14-0", + "@github/copilot-linux-arm64": "1.0.14-0", + "@github/copilot-linux-x64": "1.0.14-0", + "@github/copilot-win32-arm64": "1.0.14-0", + "@github/copilot-win32-x64": "1.0.14-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.11.tgz", - "integrity": "sha512-wdKimjtbsVeXqMqQSnGpGBPFEYHljxXNuWeH8EIJTNRgFpAsimcivsFgql3Twq4YOp0AxfsH36icG4IEen30mA==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.14-0.tgz", + "integrity": "sha512-w11Eqmfnu0ihrvgLysTd5Tkq8LuQa9eW63CNTQ/k5copnG1AMCdvd3K/78MxE2DdFJPq2L95KGS5cs9jH1dlIw==", "cpu": [ "arm64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.11.tgz", - "integrity": "sha512-VeuPv8rzBVGBB8uDwMEhcHBpldoKaq26yZ5YQm+G9Ka5QIF+1DMah8ZNRMVsTeNKkb1ji9G8vcuCsaPbnG3fKg==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.14-0.tgz", + "integrity": "sha512-4X/dMSPxCE/rvL6N1tgnwFxBg2uXnPrN63GGgS/FqK/fNi3TtcuojDVv8K1yjmEYpF8PXdkQttDlp6bKc+Nonw==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.11.tgz", - "integrity": "sha512-/d8p6RlFYKj1Va2hekFIcYNMHWagcEkaxgcllUNXSyQLnmEtXUkaWtz62VKGWE+n/UMkEwCB6vI2xEwPTlUNBQ==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.14-0.tgz", + "integrity": "sha512-A4thcLUoErEvfBO3Hsl/hJASibn44qwZm1ZSeVBPCa1FkpowBwo8fT1eV9EwN/ftKsyks3QkndNFvHkVzjUfxA==", "cpu": [ "arm64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.11.tgz", - "integrity": "sha512-UujTRO3xkPFC1CybchBbCnaTEAG6JrH0etIst07JvfekMWgvRxbiCHQPpDPSzBCPiBcGu0gba0/IT+vUCORuIw==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.14-0.tgz", + "integrity": "sha512-Kwn+Qn8/BqWRKa2DewZipH7rPIO8nDRWzpVy/ZLcRWBAvnIU+6BLWfhnYEU44DsqkD2VeWhKVfQlNmDX23xKKg==", "cpu": [ "x64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.11.tgz", - "integrity": "sha512-EOW8HUM+EmnHEZEa+iUMl4pP1+2eZUk2XCbynYiMehwX9sidc4BxEHp2RuxADSzFPTieQEWzgjQmHWrtet8pQg==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.14-0.tgz", + "integrity": "sha512-8P5kxcb8YVWSS+Ihs+ykyy8jov1WwQ8GKV4d7mJN268Jpd8y5VI8Peb7uE2VO0lRLgq5c2VcXuZDsLG/1Wgnlw==", "cpu": [ "arm64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.11.tgz", - "integrity": "sha512-fKGkSNamzs3h9AbmswNvPYJBORCb2Y8CbusijU3C7fT3ohvqnHJwKo5iHhJXLOKZNOpFZgq9YKha410u9sIs6Q==", + "version": "1.0.14-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.14-0.tgz", + "integrity": "sha512-JWxp08j5o/PUkRZtZVagNYJLjH+KCURCyZRb7BfnC0A3vLeqcJQ70JC5qlYEAlcRnb4uCUJnmnpbWLLOJ+ObrA==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 9fe936ea7..f8fe732e4 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^1.0.11", + "@github/copilot": "^1.0.14-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", From 57f7d8d3e13264ae73b79d645e1fa844a7541d32 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 30 Mar 2026 11:12:06 -0700 Subject: [PATCH 4/6] Fix lint --- nodejs/test/client.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3037a5c9d..6183fef9f 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -912,6 +912,7 @@ describe("CopilotClient", () => { content: {}, }), }); + expect(session).toBeDefined(); const createCall = rpcSpy.mock.calls.find((c) => c[0] === "session.create"); expect(createCall).toBeDefined(); @@ -933,6 +934,7 @@ describe("CopilotClient", () => { const session = await client.createSession({ onPermissionRequest: approveAll, }); + expect(session).toBeDefined(); const createCall = rpcSpy.mock.calls.find((c) => c[0] === "session.create"); expect(createCall).toBeDefined(); From f17ed337153a72dec142d931de554c4859b712a8 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 30 Mar 2026 13:05:37 -0700 Subject: [PATCH 5/6] Regen and CCR feedback --- dotnet/src/Generated/Rpc.cs | 46 +++++++ dotnet/src/Generated/SessionEvents.cs | 137 ++++++++++++++++++++- go/generated_session_events.go | 73 ++++++++++- go/rpc/generated_rpc.go | 38 ++++++ nodejs/README.md | 5 +- nodejs/src/generated/rpc.ts | 35 ++++++ nodejs/src/generated/session-events.ts | 133 +++++++++++++++++++- nodejs/src/session.ts | 23 ++-- nodejs/src/types.ts | 2 + nodejs/test/e2e/ui_elicitation.test.ts | 12 +- python/copilot/generated/rpc.py | 87 +++++++++++++ python/copilot/generated/session_events.py | 97 +++++++++++++-- 12 files changed, 653 insertions(+), 35 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index fabe4817e..406a961a2 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -1044,6 +1044,42 @@ internal class SessionUiElicitationRequest public SessionUiElicitationRequestRequestedSchema RequestedSchema { get => field ??= new(); set; } } +/// RPC data type for SessionUiHandlePendingElicitation operations. +public class SessionUiHandlePendingElicitationResult +{ + /// Whether the response was accepted. False if the request was already resolved by another client. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// The elicitation response (accept with form values, decline, or cancel). +public class SessionUiHandlePendingElicitationRequestResult +{ + /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). + [JsonPropertyName("action")] + public SessionUiElicitationResultAction Action { get; set; } + + /// The form values submitted by the user (present when action is 'accept'). + [JsonPropertyName("content")] + public Dictionary? Content { get; set; } +} + +/// RPC data type for SessionUiHandlePendingElicitation operations. +internal class SessionUiHandlePendingElicitationRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// The unique request ID from the elicitation.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// The elicitation response (accept with form values, decline, or cancel). + [JsonPropertyName("result")] + public SessionUiHandlePendingElicitationRequestResult Result { get => field ??= new(); set; } +} + /// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations. public class SessionPermissionsHandlePendingPermissionRequestResult { @@ -1822,6 +1858,13 @@ public async Task ElicitationAsync(string message, S var request = new SessionUiElicitationRequest { SessionId = _sessionId, Message = message, RequestedSchema = requestedSchema }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.ui.elicitation", [request], cancellationToken); } + + /// Calls "session.ui.handlePendingElicitation". + public async Task HandlePendingElicitationAsync(string requestId, SessionUiHandlePendingElicitationRequestResult result, CancellationToken cancellationToken = default) + { + var request = new SessionUiHandlePendingElicitationRequest { SessionId = _sessionId, RequestId = requestId, Result = result }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.ui.handlePendingElicitation", [request], cancellationToken); + } } /// Provides session-scoped Permissions APIs. @@ -1961,6 +2004,9 @@ public async Task KillAsync(string processId, SessionShe [JsonSerializable(typeof(SessionUiElicitationRequest))] [JsonSerializable(typeof(SessionUiElicitationRequestRequestedSchema))] [JsonSerializable(typeof(SessionUiElicitationResult))] +[JsonSerializable(typeof(SessionUiHandlePendingElicitationRequest))] +[JsonSerializable(typeof(SessionUiHandlePendingElicitationRequestResult))] +[JsonSerializable(typeof(SessionUiHandlePendingElicitationResult))] [JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))] [JsonSerializable(typeof(SessionWorkspaceCreateFileResult))] [JsonSerializable(typeof(SessionWorkspaceListFilesRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 73c0bdaa2..6da3de682 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -28,6 +28,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(AssistantTurnEndEvent), "assistant.turn_end")] [JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")] [JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")] +[JsonDerivedType(typeof(CapabilitiesChangedEvent), "capabilities.changed")] [JsonDerivedType(typeof(CommandCompletedEvent), "command.completed")] [JsonDerivedType(typeof(CommandExecuteEvent), "command.execute")] [JsonDerivedType(typeof(CommandQueuedEvent), "command.queued")] @@ -45,6 +46,8 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] [JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")] [JsonDerivedType(typeof(PermissionRequestedEvent), "permission.requested")] +[JsonDerivedType(typeof(SamplingCompletedEvent), "sampling.completed")] +[JsonDerivedType(typeof(SamplingRequestedEvent), "sampling.requested")] [JsonDerivedType(typeof(SessionBackgroundTasksChangedEvent), "session.background_tasks_changed")] [JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")] [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] @@ -60,6 +63,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] [JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")] [JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")] +[JsonDerivedType(typeof(SessionRemoteSteerableChangedEvent), "session.remote_steerable_changed")] [JsonDerivedType(typeof(SessionResumeEvent), "session.resume")] [JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")] [JsonDerivedType(typeof(SessionSkillsLoadedEvent), "session.skills_loaded")] @@ -151,6 +155,19 @@ public partial class SessionResumeEvent : SessionEvent public required SessionResumeData Data { get; set; } } +/// Notifies Mission Control that the session's remote steering capability has changed. +/// Represents the session.remote_steerable_changed event. +public partial class SessionRemoteSteerableChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.remote_steerable_changed"; + + /// The session.remote_steerable_changed event payload. + [JsonPropertyName("data")] + public required SessionRemoteSteerableChangedData Data { get; set; } +} + /// Error details for timeline display including message and optional diagnostic information. /// Represents the session.error event. public partial class SessionErrorEvent : SessionEvent @@ -813,6 +830,32 @@ public partial class ElicitationCompletedEvent : SessionEvent public required ElicitationCompletedData Data { get; set; } } +/// Sampling request from an MCP server; contains the server name and a requestId for correlation. +/// Represents the sampling.requested event. +public partial class SamplingRequestedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "sampling.requested"; + + /// The sampling.requested event payload. + [JsonPropertyName("data")] + public required SamplingRequestedData Data { get; set; } +} + +/// Sampling request completion notification signaling UI dismissal. +/// Represents the sampling.completed event. +public partial class SamplingCompletedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "sampling.completed"; + + /// The sampling.completed event payload. + [JsonPropertyName("data")] + public required SamplingCompletedData Data { get; set; } +} + /// OAuth authentication request for an MCP server. /// Represents the mcp.oauth_required event. public partial class McpOauthRequiredEvent : SessionEvent @@ -917,6 +960,19 @@ public partial class CommandsChangedEvent : SessionEvent public required CommandsChangedData Data { get; set; } } +/// Session capability change notification. +/// Represents the capabilities.changed event. +public partial class CapabilitiesChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "capabilities.changed"; + + /// The capabilities.changed event payload. + [JsonPropertyName("data")] + public required CapabilitiesChangedData Data { get; set; } +} + /// Plan approval request with plan content and available user actions. /// Represents the exit_plan_mode.requested event. public partial class ExitPlanModeRequestedEvent : SessionEvent @@ -1072,8 +1128,8 @@ public partial class SessionStartData /// Whether this session supports remote steering via Mission Control. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("steerable")] - public bool? Steerable { get; set; } + [JsonPropertyName("remoteSteerable")] + public bool? RemoteSteerable { get; set; } } /// Session resume metadata including current context and event count. @@ -1106,6 +1162,19 @@ public partial class SessionResumeData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + + /// Whether this session supports remote steering via Mission Control. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remoteSteerable")] + public bool? RemoteSteerable { get; set; } +} + +/// Notifies Mission Control that the session's remote steering capability has changed. +public partial class SessionRemoteSteerableChangedData +{ + /// Whether this session now supports remote steering via Mission Control. + [JsonPropertyName("remoteSteerable")] + public required bool RemoteSteerable { get; set; } } /// Error details for timeline display including message and optional diagnostic information. @@ -1779,7 +1848,17 @@ public partial class AssistantUsageData [JsonPropertyName("duration")] public double? Duration { get; set; } - /// What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls. + /// Time to first token in milliseconds. Only available for streaming requests. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ttftMs")] + public double? TtftMs { get; set; } + + /// Average inter-token latency in milliseconds. Only available for streaming requests. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interTokenLatencyMs")] + public double? InterTokenLatencyMs { get; set; } + + /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("initiator")] public string? Initiator { get; set; } @@ -2277,6 +2356,30 @@ public partial class ElicitationCompletedData public required string RequestId { get; set; } } +/// Sampling request from an MCP server; contains the server name and a requestId for correlation. +public partial class SamplingRequestedData +{ + /// Unique identifier for this sampling request; used to respond via session.respondToSampling(). + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// Name of the MCP server that initiated the sampling request. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// The JSON-RPC request ID from the MCP protocol. + [JsonPropertyName("mcpRequestId")] + public required object McpRequestId { get; set; } +} + +/// Sampling request completion notification signaling UI dismissal. +public partial class SamplingCompletedData +{ + /// Request ID of the resolved sampling request; clients should dismiss any UI for this request. + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + /// OAuth authentication request for an MCP server. public partial class McpOauthRequiredData { @@ -2397,6 +2500,15 @@ public partial class CommandsChangedData public required CommandsChangedDataCommandsItem[] Commands { get; set; } } +/// Session capability change notification. +public partial class CapabilitiesChangedData +{ + /// UI capability changes. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ui")] + public CapabilitiesChangedDataUi? Ui { get; set; } +} + /// Plan approval request with plan content and available user actions. public partial class ExitPlanModeRequestedData { @@ -3591,6 +3703,16 @@ public partial class CommandsChangedDataCommandsItem public string? Description { get; set; } } +/// UI capability changes. +/// Nested data type for CapabilitiesChangedDataUi. +public partial class CapabilitiesChangedDataUi +{ + /// Whether elicitation is now supported. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("elicitation")] + public bool? Elicitation { get; set; } +} + /// Nested data type for SessionSkillsLoadedDataSkillsItem. public partial class SessionSkillsLoadedDataSkillsItem { @@ -3955,6 +4077,9 @@ public enum SessionExtensionsLoadedDataExtensionsItemStatus [JsonSerializable(typeof(AssistantUsageDataCopilotUsage))] [JsonSerializable(typeof(AssistantUsageDataCopilotUsageTokenDetailsItem))] [JsonSerializable(typeof(AssistantUsageEvent))] +[JsonSerializable(typeof(CapabilitiesChangedData))] +[JsonSerializable(typeof(CapabilitiesChangedDataUi))] +[JsonSerializable(typeof(CapabilitiesChangedEvent))] [JsonSerializable(typeof(CommandCompletedData))] [JsonSerializable(typeof(CommandCompletedEvent))] [JsonSerializable(typeof(CommandExecuteData))] @@ -4005,6 +4130,10 @@ public enum SessionExtensionsLoadedDataExtensionsItemStatus [JsonSerializable(typeof(PermissionRequestWrite))] [JsonSerializable(typeof(PermissionRequestedData))] [JsonSerializable(typeof(PermissionRequestedEvent))] +[JsonSerializable(typeof(SamplingCompletedData))] +[JsonSerializable(typeof(SamplingCompletedEvent))] +[JsonSerializable(typeof(SamplingRequestedData))] +[JsonSerializable(typeof(SamplingRequestedEvent))] [JsonSerializable(typeof(SessionBackgroundTasksChangedData))] [JsonSerializable(typeof(SessionBackgroundTasksChangedEvent))] [JsonSerializable(typeof(SessionCompactionCompleteData))] @@ -4044,6 +4173,8 @@ public enum SessionExtensionsLoadedDataExtensionsItemStatus [JsonSerializable(typeof(SessionModelChangeEvent))] [JsonSerializable(typeof(SessionPlanChangedData))] [JsonSerializable(typeof(SessionPlanChangedEvent))] +[JsonSerializable(typeof(SessionRemoteSteerableChangedData))] +[JsonSerializable(typeof(SessionRemoteSteerableChangedEvent))] [JsonSerializable(typeof(SessionResumeData))] [JsonSerializable(typeof(SessionResumeDataContext))] [JsonSerializable(typeof(SessionResumeEvent))] diff --git a/go/generated_session_events.go b/go/generated_session_events.go index 051fa4eca..8eafb13d0 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -30,6 +30,8 @@ type SessionEvent struct { // // Session resume metadata including current context and event count // + // Notifies Mission Control that the session's remote steering capability has changed + // // Error details for timeline display including message and optional diagnostic information // // Payload indicating the agent is idle; includes any background tasks still in flight @@ -137,6 +139,11 @@ type SessionEvent struct { // // Elicitation request completion notification signaling UI dismissal // + // Sampling request from an MCP server; contains the server name and a requestId for + // correlation + // + // Sampling request completion notification signaling UI dismissal + // // OAuth authentication request for an MCP server // // MCP OAuth request completion notification @@ -153,6 +160,8 @@ type SessionEvent struct { // // SDK command registration change notification // + // Session capability change notification + // // Plan approval request with plan content and available user actions // // Plan mode exit completion notification signaling UI dismissal @@ -173,6 +182,8 @@ type SessionEvent struct { // // # Session resume metadata including current context and event count // +// # Notifies Mission Control that the session's remote steering capability has changed +// // # Error details for timeline display including message and optional diagnostic information // // Payload indicating the agent is idle; includes any background tasks still in flight @@ -280,6 +291,11 @@ type SessionEvent struct { // // # Elicitation request completion notification signaling UI dismissal // +// Sampling request from an MCP server; contains the server name and a requestId for +// correlation +// +// # Sampling request completion notification signaling UI dismissal +// // # OAuth authentication request for an MCP server // // # MCP OAuth request completion notification @@ -296,6 +312,8 @@ type SessionEvent struct { // // # SDK command registration change notification // +// # Session capability change notification +// // # Plan approval request with plan content and available user actions // // Plan mode exit completion notification signaling UI dismissal @@ -319,6 +337,10 @@ type Data struct { // // Reasoning effort level after the model change, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Whether this session supports remote steering via Mission Control + // + // Whether this session now supports remote steering via Mission Control + RemoteSteerable *bool `json:"remoteSteerable,omitempty"` // Model selected at session creation time, if any // // Model currently selected at resume time @@ -329,8 +351,6 @@ type Data struct { SessionID *string `json:"sessionId,omitempty"` // ISO 8601 timestamp when the session was created StartTime *time.Time `json:"startTime,omitempty"` - // Whether this session supports remote steering via Mission Control - Steerable *bool `json:"steerable,omitempty"` // Schema version number for the session event format Version *float64 `json:"version,omitempty"` // Total number of persisted events in the session at the time of resume @@ -534,6 +554,12 @@ type Data struct { // Request ID of the resolved elicitation request; clients should dismiss any UI for this // request // + // Unique identifier for this sampling request; used to respond via + // session.respondToSampling() + // + // Request ID of the resolved sampling request; clients should dismiss any UI for this + // request + // // Unique identifier for this OAuth request; used to respond via // session.respondToMcpOAuth() // @@ -652,10 +678,13 @@ type Data struct { Cost *float64 `json:"cost,omitempty"` // Duration of the API call in milliseconds Duration *float64 `json:"duration,omitempty"` - // What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls + // What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for + // user-initiated calls Initiator *string `json:"initiator,omitempty"` // Number of input tokens consumed InputTokens *float64 `json:"inputTokens,omitempty"` + // Average inter-token latency in milliseconds. Only available for streaming requests + InterTokenLatencyMS *float64 `json:"interTokenLatencyMs,omitempty"` // Model identifier used for this API call // // Model identifier that generated this tool call @@ -666,6 +695,8 @@ type Data struct { Model *string `json:"model,omitempty"` // Per-quota resource usage snapshots, keyed by quota identifier QuotaSnapshots map[string]QuotaSnapshot `json:"quotaSnapshots,omitempty"` + // Time to first token in milliseconds. Only available for streaming requests + TtftMS *float64 `json:"ttftMs,omitempty"` // Reason the current turn was aborted (e.g., "user initiated") Reason *string `json:"reason,omitempty"` // Arguments for the tool invocation @@ -781,6 +812,10 @@ type Data struct { Mode *Mode `json:"mode,omitempty"` // JSON Schema describing the form fields to present to the user (form mode only) RequestedSchema *RequestedSchema `json:"requestedSchema,omitempty"` + // The JSON-RPC request ID from the MCP protocol + MCPRequestID *MCPRequestID `json:"mcpRequestId"` + // Name of the MCP server that initiated the sampling request + // // Display name of the MCP server that requires OAuth // // Name of the MCP server whose status changed @@ -803,6 +838,8 @@ type Data struct { CommandName *string `json:"commandName,omitempty"` // Current list of registered SDK commands Commands []DataCommand `json:"commands,omitempty"` + // UI capability changes + UI *UI `json:"ui,omitempty"` // Available actions the user can take (e.g., approve, edit, reject) Actions []string `json:"actions,omitempty"` // Full content of the plan file @@ -1375,6 +1412,12 @@ type ToolRequest struct { Type *ToolRequestType `json:"type,omitempty"` } +// UI capability changes +type UI struct { + // Whether elicitation is now supported + Elicitation *bool `json:"elicitation,omitempty"` +} + // The agent mode that was active when this message was sent type AgentMode string @@ -1575,6 +1618,7 @@ const ( SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end" SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start" SessionEventTypeAssistantUsage SessionEventType = "assistant.usage" + SessionEventTypeCapabilitiesChanged SessionEventType = "capabilities.changed" SessionEventTypeCommandCompleted SessionEventType = "command.completed" SessionEventTypeCommandExecute SessionEventType = "command.execute" SessionEventTypeCommandQueued SessionEventType = "command.queued" @@ -1592,6 +1636,8 @@ const ( SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" SessionEventTypePermissionCompleted SessionEventType = "permission.completed" SessionEventTypePermissionRequested SessionEventType = "permission.requested" + SessionEventTypeSamplingCompleted SessionEventType = "sampling.completed" + SessionEventTypeSamplingRequested SessionEventType = "sampling.requested" SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" @@ -1607,6 +1653,7 @@ const ( SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" SessionEventTypeSessionModelChange SessionEventType = "session.model_change" SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" + SessionEventTypeSessionRemoteSteerableChanged SessionEventType = "session.remote_steerable_changed" SessionEventTypeSessionResume SessionEventType = "session.resume" SessionEventTypeSessionShutdown SessionEventType = "session.shutdown" SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded" @@ -1681,6 +1728,26 @@ func (x *ErrorUnion) MarshalJSON() ([]byte, error) { return marshalUnion(nil, nil, nil, x.String, false, nil, x.ErrorClass != nil, x.ErrorClass, false, nil, false, nil, false) } +// The JSON-RPC request ID from the MCP protocol +type MCPRequestID struct { + Double *float64 + String *string +} + +func (x *MCPRequestID) UnmarshalJSON(data []byte) error { + object, err := unmarshalUnion(data, nil, &x.Double, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *MCPRequestID) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, x.Double, nil, x.String, false, nil, false, nil, false, nil, false, nil, false) +} + type RepositoryUnion struct { RepositoryClass *RepositoryClass String *string diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index f6232399c..e9042e964 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -528,6 +528,27 @@ type OneOf struct { Title string `json:"title"` } +type SessionUIHandlePendingElicitationResult struct { + // Whether the response was accepted. False if the request was already resolved by another + // client. + Success bool `json:"success"` +} + +type SessionUIHandlePendingElicitationParams struct { + // The unique request ID from the elicitation.requested event + RequestID string `json:"requestId"` + // The elicitation response (accept with form values, decline, or cancel) + Result SessionUIHandlePendingElicitationParamsResult `json:"result"` +} + +// The elicitation response (accept with form values, decline, or cancel) +type SessionUIHandlePendingElicitationParamsResult struct { + // The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + Action Action `json:"action"` + // The form values submitted by the user (present when action is 'accept') + Content map[string]*Content `json:"content,omitempty"` +} + type SessionPermissionsHandlePendingPermissionRequestResult struct { // Whether the permission request was handled successfully Success bool `json:"success"` @@ -1321,6 +1342,23 @@ func (a *UiApi) Elicitation(ctx context.Context, params *SessionUIElicitationPar return &result, nil } +func (a *UiApi) HandlePendingElicitation(ctx context.Context, params *SessionUIHandlePendingElicitationParams) (*SessionUIHandlePendingElicitationResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["result"] = params.Result + } + raw, err := a.client.Request("session.ui.handlePendingElicitation", req) + if err != nil { + return nil, err + } + var result SessionUIHandlePendingElicitationResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + type PermissionsApi sessionApi func (a *PermissionsApi) HandlePendingPermissionRequest(ctx context.Context, params *SessionPermissionsHandlePendingPermissionRequestParams) (*SessionPermissionsHandlePendingPermissionRequestResult, error) { diff --git a/nodejs/README.md b/nodejs/README.md index f3c60e7e0..eee4c2b65 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -508,7 +508,7 @@ Commands are sent to the CLI on both `createSession` and `resumeSession`, so you ### 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. +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. @@ -904,7 +904,7 @@ const session = await client.createSession({ ## 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 dispatches `elicitation.request` RPCs to your client whenever a tool or MCP server needs structured user input. +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. ```typescript const session = await client.createSession({ @@ -933,6 +933,7 @@ console.log(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. diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index dadb9e79d..1db497ae6 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -937,6 +937,39 @@ export interface SessionUiElicitationParams { }; } +export interface SessionUiHandlePendingElicitationResult { + /** + * Whether the response was accepted. False if the request was already resolved by another client. + */ + success: boolean; +} + +export interface SessionUiHandlePendingElicitationParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * The unique request ID from the elicitation.requested event + */ + requestId: string; + /** + * The elicitation response (accept with form values, decline, or cancel) + */ + result: { + /** + * The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + */ + action: "accept" | "decline" | "cancel"; + /** + * The form values submitted by the user (present when action is 'accept') + */ + content?: { + [k: string]: string | number | boolean | string[]; + }; + }; +} + export interface SessionPermissionsHandlePendingPermissionRequestResult { /** * Whether the permission request was handled successfully @@ -1173,6 +1206,8 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin ui: { elicitation: async (params: Omit): Promise => connection.sendRequest("session.ui.elicitation", { sessionId, ...params }), + handlePendingElicitation: async (params: Omit): Promise => + connection.sendRequest("session.ui.handlePendingElicitation", { sessionId, ...params }), }, permissions: { handlePendingPermissionRequest: async (params: Omit): Promise => diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 8a6bec680..5d8e12830 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -94,7 +94,7 @@ export type SessionEvent = /** * Whether this session supports remote steering via Mission Control */ - steerable?: boolean; + remoteSteerable?: boolean; }; } | { @@ -172,6 +172,38 @@ export type SessionEvent = * Whether the session was already in use by another client at resume time */ alreadyInUse?: boolean; + /** + * Whether this session supports remote steering via Mission Control + */ + remoteSteerable?: boolean; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + type: "session.remote_steerable_changed"; + /** + * Notifies Mission Control that the session's remote steering capability has changed + */ + data: { + /** + * Whether this session now supports remote steering via Mission Control + */ + remoteSteerable: boolean; }; } | { @@ -1588,7 +1620,15 @@ export type SessionEvent = */ duration?: number; /** - * What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls + * Time to first token in milliseconds. Only available for streaming requests + */ + ttftMs?: number; + /** + * Average inter-token latency in milliseconds. Only available for streaming requests + */ + interTokenLatencyMs?: number; + /** + * What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ initiator?: string; /** @@ -3025,6 +3065,65 @@ export type SessionEvent = requestId: string; }; } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "sampling.requested"; + /** + * Sampling request from an MCP server; contains the server name and a requestId for correlation + */ + data: { + /** + * Unique identifier for this sampling request; used to respond via session.respondToSampling() + */ + requestId: string; + /** + * Name of the MCP server that initiated the sampling request + */ + serverName: string; + /** + * The JSON-RPC request ID from the MCP protocol + */ + mcpRequestId: string | number; + [k: string]: unknown; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "sampling.completed"; + /** + * Sampling request completion notification signaling UI dismissal + */ + data: { + /** + * Request ID of the resolved sampling request; clients should dismiss any UI for this request + */ + requestId: string; + }; + } | { /** * Unique event identifier (UUID v4), generated when the event is emitted @@ -3291,6 +3390,36 @@ export type SessionEvent = }[]; }; } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "capabilities.changed"; + /** + * Session capability change notification + */ + data: { + /** + * UI capability changes + */ + ui?: { + /** + * Whether elicitation is now supported + */ + elicitation?: boolean; + }; + }; + } | { /** * Unique event identifier (UUID v4), generated when the event is emitted diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 0a08c6261..95f5970a0 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -417,24 +417,23 @@ export class CopilotSession { args: string; }; void this._executeCommandAndRespond(requestId, commandName, command, args); - } else if ((event as { type: string }).type === "elicitation.requested") { - // TODO: Remove type casts above once session-events codegen includes these event types + } else if (event.type === "elicitation.requested") { if (this.elicitationHandler) { - const data = (event as { data: Record }).data; + const { message, requestedSchema, mode, elicitationSource, url, requestId } = + event.data; void this._handleElicitationRequest( { - message: data.message as string, - requestedSchema: - data.requestedSchema as ElicitationRequest["requestedSchema"], - mode: data.mode as ElicitationRequest["mode"], - elicitationSource: data.elicitationSource as string | undefined, + message, + requestedSchema: requestedSchema as ElicitationRequest["requestedSchema"], + mode, + elicitationSource, + url, }, - data.requestId as string + requestId ); } - } else if ((event as { type: string }).type === "capabilities.changed") { - const data = (event as { data: Partial }).data; - this._capabilities = { ...this._capabilities, ...data }; + } else if (event.type === "capabilities.changed") { + this._capabilities = { ...this._capabilities, ...event.data }; } } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index a84f5089e..b4b9e563c 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -422,6 +422,8 @@ export interface ElicitationRequest { mode?: "form" | "url"; /** The source that initiated the request (e.g. MCP server name). */ elicitationSource?: string; + /** URL to open in the user's browser (url mode only). */ + url?: string; } /** diff --git a/nodejs/test/e2e/ui_elicitation.test.ts b/nodejs/test/e2e/ui_elicitation.test.ts index 099aad0c4..ced735d88 100644 --- a/nodejs/test/e2e/ui_elicitation.test.ts +++ b/nodejs/test/e2e/ui_elicitation.test.ts @@ -78,8 +78,9 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { expect(session1.capabilities.ui?.elicitation).toBe(false); // Listen for capabilities.changed event + let unsubscribe: (() => void) | undefined; const capChangedPromise = new Promise((resolve) => { - session1.on((event) => { + unsubscribe = session1.on((event) => { if ((event as { type: string }).type === "capabilities.changed") { resolve(event); } @@ -94,6 +95,7 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { }); const capEvent = await capChangedPromise; + unsubscribe?.(); const data = (capEvent as { data: { ui?: { elicitation?: boolean } } }).data; expect(data.ui?.elicitation).toBe(true); @@ -115,8 +117,9 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { expect(session1.capabilities.ui?.elicitation).toBe(false); // Wait for elicitation to become available + let unsubEnabled: (() => void) | undefined; const capEnabledPromise = new Promise((resolve) => { - session1.on((event) => { + unsubEnabled = session1.on((event) => { const data = event as { type: string; data: { ui?: { elicitation?: boolean } }; @@ -141,11 +144,13 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { }); await capEnabledPromise; + unsubEnabled?.(); expect(session1.capabilities.ui?.elicitation).toBe(true); // Now listen for the capability being removed + let unsubDisabled: (() => void) | undefined; const capDisabledPromise = new Promise((resolve) => { - session1.on((event) => { + unsubDisabled = session1.on((event) => { const data = event as { type: string; data: { ui?: { elicitation?: boolean } }; @@ -163,6 +168,7 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { await client3.forceStop(); await capDisabledPromise; + unsubDisabled?.(); expect(session1.capabilities.ui?.elicitation).toBe(false); } ); diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 14ae307d7..f7ea6dbad 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -1821,6 +1821,72 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionUIHandlePendingElicitationResult: + success: bool + """Whether the response was accepted. False if the request was already resolved by another + client. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionUIHandlePendingElicitationResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return SessionUIHandlePendingElicitationResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + + +@dataclass +class SessionUIHandlePendingElicitationParamsResult: + """The elicitation response (accept with form values, decline, or cancel)""" + + action: Action + """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)""" + + content: dict[str, float | bool | list[str] | str] | None = None + """The form values submitted by the user (present when action is 'accept')""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionUIHandlePendingElicitationParamsResult': + assert isinstance(obj, dict) + action = Action(obj.get("action")) + content = from_union([lambda x: from_dict(lambda x: from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], obj.get("content")) + return SessionUIHandlePendingElicitationParamsResult(action, content) + + def to_dict(self) -> dict: + result: dict = {} + result["action"] = to_enum(Action, self.action) + if self.content is not None: + result["content"] = from_union([lambda x: from_dict(lambda x: from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], self.content) + return result + + +@dataclass +class SessionUIHandlePendingElicitationParams: + request_id: str + """The unique request ID from the elicitation.requested event""" + + result: SessionUIHandlePendingElicitationParamsResult + """The elicitation response (accept with form values, decline, or cancel)""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionUIHandlePendingElicitationParams': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = SessionUIHandlePendingElicitationParamsResult.from_dict(obj.get("result")) + return SessionUIHandlePendingElicitationParams(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = to_class(SessionUIHandlePendingElicitationParamsResult, self.result) + return result + + @dataclass class SessionPermissionsHandlePendingPermissionRequestResult: success: bool @@ -2493,6 +2559,22 @@ def session_ui_elicitation_params_to_dict(x: SessionUIElicitationParams) -> Any: return to_class(SessionUIElicitationParams, x) +def session_ui_handle_pending_elicitation_result_from_dict(s: Any) -> SessionUIHandlePendingElicitationResult: + return SessionUIHandlePendingElicitationResult.from_dict(s) + + +def session_ui_handle_pending_elicitation_result_to_dict(x: SessionUIHandlePendingElicitationResult) -> Any: + return to_class(SessionUIHandlePendingElicitationResult, x) + + +def session_ui_handle_pending_elicitation_params_from_dict(s: Any) -> SessionUIHandlePendingElicitationParams: + return SessionUIHandlePendingElicitationParams.from_dict(s) + + +def session_ui_handle_pending_elicitation_params_to_dict(x: SessionUIHandlePendingElicitationParams) -> Any: + return to_class(SessionUIHandlePendingElicitationParams, x) + + def session_permissions_handle_pending_permission_request_result_from_dict(s: Any) -> SessionPermissionsHandlePendingPermissionRequestResult: return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(s) @@ -2823,6 +2905,11 @@ async def elicitation(self, params: SessionUIElicitationParams, *, timeout: floa params_dict["sessionId"] = self._session_id return SessionUIElicitationResult.from_dict(await self._client.request("session.ui.elicitation", params_dict, **_timeout_kwargs(timeout))) + async def handle_pending_elicitation(self, params: SessionUIHandlePendingElicitationParams, *, timeout: float | None = None) -> SessionUIHandlePendingElicitationResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionUIHandlePendingElicitationResult.from_dict(await self._client.request("session.ui.handlePendingElicitation", params_dict, **_timeout_kwargs(timeout))) + class PermissionsApi: def __init__(self, client: "JsonRpcClient", session_id: str): diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 3dbe5cdf2..c3123102b 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -1741,12 +1741,34 @@ def to_dict(self) -> dict: return result +@dataclass +class UI: + """UI capability changes""" + + elicitation: bool | None = None + """Whether elicitation is now supported""" + + @staticmethod + def from_dict(obj: Any) -> 'UI': + assert isinstance(obj, dict) + elicitation = from_union([from_bool, from_none], obj.get("elicitation")) + return UI(elicitation) + + def to_dict(self) -> dict: + result: dict = {} + if self.elicitation is not None: + result["elicitation"] = from_union([from_bool, from_none], self.elicitation) + return result + + @dataclass class Data: """Session initialization metadata including context and configuration Session resume metadata including current context and event count + Notifies Mission Control that the session's remote steering capability has changed + Error details for timeline display including message and optional diagnostic information Payload indicating the agent is idle; includes any background tasks still in flight @@ -1854,6 +1876,11 @@ class Data: Elicitation request completion notification signaling UI dismissal + Sampling request from an MCP server; contains the server name and a requestId for + correlation + + Sampling request completion notification signaling UI dismissal + OAuth authentication request for an MCP server MCP OAuth request completion notification @@ -1870,6 +1897,8 @@ class Data: SDK command registration change notification + Session capability change notification + Plan approval request with plan content and available user actions Plan mode exit completion notification signaling UI dismissal @@ -1898,6 +1927,11 @@ class Data: Reasoning effort level after the model change, if applicable """ + remote_steerable: bool | None = None + """Whether this session supports remote steering via Mission Control + + Whether this session now supports remote steering via Mission Control + """ selected_model: str | None = None """Model selected at session creation time, if any @@ -1911,9 +1945,6 @@ class Data: start_time: datetime | None = None """ISO 8601 timestamp when the session was created""" - steerable: bool | None = None - """Whether this session supports remote steering via Mission Control""" - version: float | None = None """Schema version number for the session event format""" @@ -2182,6 +2213,12 @@ class Data: Request ID of the resolved elicitation request; clients should dismiss any UI for this request + Unique identifier for this sampling request; used to respond via + session.respondToSampling() + + Request ID of the resolved sampling request; clients should dismiss any UI for this + request + Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth() @@ -2329,11 +2366,15 @@ class Data: """Duration of the API call in milliseconds""" initiator: str | None = None - """What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls""" - + """What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for + user-initiated calls + """ input_tokens: float | None = None """Number of input tokens consumed""" + inter_token_latency_ms: float | None = None + """Average inter-token latency in milliseconds. Only available for streaming requests""" + model: str | None = None """Model identifier used for this API call @@ -2346,6 +2387,9 @@ class Data: quota_snapshots: dict[str, QuotaSnapshot] | None = None """Per-quota resource usage snapshots, keyed by quota identifier""" + ttft_ms: float | None = None + """Time to first token in milliseconds. Only available for streaming requests""" + reason: str | None = None """Reason the current turn was aborted (e.g., "user initiated")""" @@ -2498,8 +2542,13 @@ class Data: requested_schema: RequestedSchema | None = None """JSON Schema describing the form fields to present to the user (form mode only)""" + mcp_request_id: float | str | None = None + """The JSON-RPC request ID from the MCP protocol""" + server_name: str | None = None - """Display name of the MCP server that requires OAuth + """Name of the MCP server that initiated the sampling request + + Display name of the MCP server that requires OAuth Name of the MCP server whose status changed """ @@ -2529,6 +2578,9 @@ class Data: commands: list[DataCommand] | None = None """Current list of registered SDK commands""" + ui: UI | None = None + """UI capability changes""" + actions: list[str] | None = None """Available actions the user can take (e.g., approve, edit, reject)""" @@ -2567,10 +2619,10 @@ def from_dict(obj: Any) -> 'Data': copilot_version = from_union([from_str, from_none], obj.get("copilotVersion")) producer = from_union([from_str, from_none], obj.get("producer")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) + remote_steerable = from_union([from_bool, from_none], obj.get("remoteSteerable")) selected_model = from_union([from_str, from_none], obj.get("selectedModel")) session_id = from_union([from_str, from_none], obj.get("sessionId")) start_time = from_union([from_datetime, from_none], obj.get("startTime")) - steerable = from_union([from_bool, from_none], obj.get("steerable")) version = from_union([from_float, from_none], obj.get("version")) event_count = from_union([from_float, from_none], obj.get("eventCount")) resume_time = from_union([from_datetime, from_none], obj.get("resumeTime")) @@ -2666,8 +2718,10 @@ def from_dict(obj: Any) -> 'Data': duration = from_union([from_float, from_none], obj.get("duration")) initiator = from_union([from_str, from_none], obj.get("initiator")) input_tokens = from_union([from_float, from_none], obj.get("inputTokens")) + inter_token_latency_ms = from_union([from_float, from_none], obj.get("interTokenLatencyMs")) model = from_union([from_str, from_none], obj.get("model")) quota_snapshots = from_union([lambda x: from_dict(QuotaSnapshot.from_dict, x), from_none], obj.get("quotaSnapshots")) + ttft_ms = from_union([from_float, from_none], obj.get("ttftMs")) reason = from_union([from_str, from_none], obj.get("reason")) arguments = obj.get("arguments") tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) @@ -2705,6 +2759,7 @@ def from_dict(obj: Any) -> 'Data': elicitation_source = from_union([from_str, from_none], obj.get("elicitationSource")) mode = from_union([Mode, from_none], obj.get("mode")) requested_schema = from_union([RequestedSchema.from_dict, from_none], obj.get("requestedSchema")) + mcp_request_id = from_union([from_float, from_str, from_none], obj.get("mcpRequestId")) server_name = from_union([from_str, from_none], obj.get("serverName")) server_url = from_union([from_str, from_none], obj.get("serverUrl")) static_client_config = from_union([StaticClientConfig.from_dict, from_none], obj.get("staticClientConfig")) @@ -2714,6 +2769,7 @@ def from_dict(obj: Any) -> 'Data': args = from_union([from_str, from_none], obj.get("args")) command_name = from_union([from_str, from_none], obj.get("commandName")) commands = from_union([lambda x: from_list(DataCommand.from_dict, x), from_none], obj.get("commands")) + ui = from_union([UI.from_dict, from_none], obj.get("ui")) actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions")) plan_content = from_union([from_str, from_none], obj.get("planContent")) recommended_action = from_union([from_str, from_none], obj.get("recommendedAction")) @@ -2724,7 +2780,7 @@ def from_dict(obj: Any) -> 'Data': servers = from_union([lambda x: from_list(Server.from_dict, x), from_none], obj.get("servers")) status = from_union([ServerStatus, from_none], obj.get("status")) extensions = from_union([lambda x: from_list(Extension.from_dict, x), from_none], obj.get("extensions")) - return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, steerable, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, host, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, description, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, duration_ms, total_tokens, total_tool_calls, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, actions, plan_content, recommended_action, skills, agents, errors, warnings, servers, status, extensions) + return Data(already_in_use, context, copilot_version, producer, reasoning_effort, remote_steerable, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, host, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, inter_token_latency_ms, model, quota_snapshots, ttft_ms, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, description, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, duration_ms, total_tokens, total_tool_calls, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, mcp_request_id, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, ui, actions, plan_content, recommended_action, skills, agents, errors, warnings, servers, status, extensions) def to_dict(self) -> dict: result: dict = {} @@ -2738,14 +2794,14 @@ def to_dict(self) -> dict: result["producer"] = from_union([from_str, from_none], self.producer) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) + if self.remote_steerable is not None: + result["remoteSteerable"] = from_union([from_bool, from_none], self.remote_steerable) if self.selected_model is not None: result["selectedModel"] = from_union([from_str, from_none], self.selected_model) if self.session_id is not None: result["sessionId"] = from_union([from_str, from_none], self.session_id) if self.start_time is not None: result["startTime"] = from_union([lambda x: x.isoformat(), from_none], self.start_time) - if self.steerable is not None: - result["steerable"] = from_union([from_bool, from_none], self.steerable) if self.version is not None: result["version"] = from_union([to_float, from_none], self.version) if self.event_count is not None: @@ -2936,10 +2992,14 @@ def to_dict(self) -> dict: result["initiator"] = from_union([from_str, from_none], self.initiator) if self.input_tokens is not None: result["inputTokens"] = from_union([to_float, from_none], self.input_tokens) + if self.inter_token_latency_ms is not None: + result["interTokenLatencyMs"] = from_union([to_float, from_none], self.inter_token_latency_ms) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) if self.quota_snapshots is not None: result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: to_class(QuotaSnapshot, x), x), from_none], self.quota_snapshots) + if self.ttft_ms is not None: + result["ttftMs"] = from_union([to_float, from_none], self.ttft_ms) if self.reason is not None: result["reason"] = from_union([from_str, from_none], self.reason) if self.arguments is not None: @@ -3014,6 +3074,8 @@ def to_dict(self) -> dict: result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode) if self.requested_schema is not None: result["requestedSchema"] = from_union([lambda x: to_class(RequestedSchema, x), from_none], self.requested_schema) + if self.mcp_request_id is not None: + result["mcpRequestId"] = from_union([to_float, from_str, from_none], self.mcp_request_id) if self.server_name is not None: result["serverName"] = from_union([from_str, from_none], self.server_name) if self.server_url is not None: @@ -3032,6 +3094,8 @@ def to_dict(self) -> dict: result["commandName"] = from_union([from_str, from_none], self.command_name) if self.commands is not None: result["commands"] = from_union([lambda x: from_list(lambda x: to_class(DataCommand, x), x), from_none], self.commands) + if self.ui is not None: + result["ui"] = from_union([lambda x: to_class(UI, x), from_none], self.ui) if self.actions is not None: result["actions"] = from_union([lambda x: from_list(from_str, x), from_none], self.actions) if self.plan_content is not None: @@ -3066,6 +3130,7 @@ class SessionEventType(Enum): ASSISTANT_TURN_END = "assistant.turn_end" ASSISTANT_TURN_START = "assistant.turn_start" ASSISTANT_USAGE = "assistant.usage" + CAPABILITIES_CHANGED = "capabilities.changed" COMMANDS_CHANGED = "commands.changed" COMMAND_COMPLETED = "command.completed" COMMAND_EXECUTE = "command.execute" @@ -3083,6 +3148,8 @@ class SessionEventType(Enum): PENDING_MESSAGES_MODIFIED = "pending_messages.modified" PERMISSION_COMPLETED = "permission.completed" PERMISSION_REQUESTED = "permission.requested" + SAMPLING_COMPLETED = "sampling.completed" + SAMPLING_REQUESTED = "sampling.requested" SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed" SESSION_COMPACTION_COMPLETE = "session.compaction_complete" SESSION_COMPACTION_START = "session.compaction_start" @@ -3098,6 +3165,7 @@ class SessionEventType(Enum): SESSION_MODEL_CHANGE = "session.model_change" SESSION_MODE_CHANGED = "session.mode_changed" SESSION_PLAN_CHANGED = "session.plan_changed" + SESSION_REMOTE_STEERABLE_CHANGED = "session.remote_steerable_changed" SESSION_RESUME = "session.resume" SESSION_SHUTDOWN = "session.shutdown" SESSION_SKILLS_LOADED = "session.skills_loaded" @@ -3143,6 +3211,8 @@ class SessionEvent: Session resume metadata including current context and event count + Notifies Mission Control that the session's remote steering capability has changed + Error details for timeline display including message and optional diagnostic information Payload indicating the agent is idle; includes any background tasks still in flight @@ -3250,6 +3320,11 @@ class SessionEvent: Elicitation request completion notification signaling UI dismissal + Sampling request from an MCP server; contains the server name and a requestId for + correlation + + Sampling request completion notification signaling UI dismissal + OAuth authentication request for an MCP server MCP OAuth request completion notification @@ -3266,6 +3341,8 @@ class SessionEvent: SDK command registration change notification + Session capability change notification + Plan approval request with plan content and available user actions Plan mode exit completion notification signaling UI dismissal From c2b99e933cb0cfb8dfd0ebc656dff487f5936d2b Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 30 Mar 2026 15:04:09 -0700 Subject: [PATCH 6/6] Mackinnon feedback --- nodejs/src/session.ts | 16 +++++++--------- nodejs/test/client.test.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 95f5970a0..cb2cf826b 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -622,21 +622,19 @@ export class CopilotSession { } try { const result = await this.elicitationHandler(request, { sessionId: this.sessionId }); - await this.connection.sendRequest("session.ui.handlePendingElicitation", { - sessionId: this.sessionId, - requestId, - result, - }); + await this.rpc.ui.handlePendingElicitation({ requestId, result }); } catch { // Handler failed — attempt to cancel so the request doesn't hang try { - await this.connection.sendRequest("session.ui.handlePendingElicitation", { - sessionId: this.sessionId, + await this.rpc.ui.handlePendingElicitation({ requestId, result: { action: "cancel" }, }); - } catch { - // Best effort — another client may have already responded + } catch (rpcError) { + if (!(rpcError instanceof ConnectionError || rpcError instanceof ResponseError)) { + throw rpcError; + } + // Connection lost or RPC error — nothing we can do } } } diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 6183fef9f..0b98ebcb8 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -945,5 +945,36 @@ describe("CopilotClient", () => { ); rpcSpy.mockRestore(); }); + + it("sends cancel when elicitation handler throws", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + onElicitationRequest: async () => { + throw new Error("handler exploded"); + }, + }); + + const rpcSpy = vi.spyOn((client as any).connection!, "sendRequest"); + + await session._handleElicitationRequest({ message: "Pick a color" }, "req-123"); + + const cancelCall = rpcSpy.mock.calls.find( + (c) => + c[0] === "session.ui.handlePendingElicitation" && + (c[1] as any)?.result?.action === "cancel" + ); + expect(cancelCall).toBeDefined(); + expect(cancelCall![1]).toEqual( + expect.objectContaining({ + requestId: "req-123", + result: { action: "cancel" }, + }) + ); + rpcSpy.mockRestore(); + }); }); });