# Copilot SDK for Node.js/TypeScript TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. ## Installation ```bash npm install @github/copilot-sdk ``` ## Run the Sample Try the interactive chat sample (from the repo root): ```bash cd nodejs npm ci npm run build cd samples npm install npm start ``` ## Quick Start ```typescript import { CopilotClient, approveAll } from "@github/copilot-sdk"; // Create and start client const client = new CopilotClient(); await client.start(); // Create a session (onPermissionRequest is optional; approveAll allows every tool) const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, }); // Wait for the response using typed event handlers const done = new Promise((resolve) => { session.on("assistant.message", (event) => { console.log(event.data.content); }); session.on("session.idle", () => { resolve(); }); }); // Send a message and wait for completion await session.send({ prompt: "What is 2+2?" }); await done; // Clean up await session.disconnect(); await client.stop(); ``` Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+ / Node.js 20+): ```typescript await using session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, }); // session is automatically disconnected when leaving scope ``` ## API Reference ### CopilotClient #### Constructor ```typescript new CopilotClient(options?: CopilotClientOptions) ``` **Options:** - `connection?: RuntimeConnection` - How to connect to the Copilot runtime. Construct via the factory functions on `RuntimeConnection`: - `RuntimeConnection.forStdio({ path?, args? })` (default) — spawn the runtime and communicate over its stdin/stdout. - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })` — spawn the runtime as a TCP server. - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections. - `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`. - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd). - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. - `logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"` - Log level. When omitted, the runtime uses its own default (currently `"info"`). - `env?: Record` - Environment variables for the runtime process. When omitted, inherits `process.env`. - `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. - `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `gitHubToken` is provided). Cannot be used with `RuntimeConnection.forUri`. - `onListModels?: () => Promise | ModelInfo[]` - Optional model-list provider, useful when using a custom provider. - `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the runtime process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. - `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the runtime's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. - `sessionFs?: SessionFsConfig` - Custom session filesystem provider. - `sessionIdleTimeoutSeconds?: number` - Server-wide idle timeout for sessions in seconds. Ignored when connecting via `RuntimeConnection.forUri`. - `enableRemoteSessions?: boolean` - Enable Mission Control remote session support. Ignored when connecting via `RuntimeConnection.forUri`. #### Methods ##### `start(): Promise` Start the CLI server and establish connection. ##### `stop(): Promise` Stop the server and close all sessions. Returns a list of any errors encountered during cleanup. ##### `forceStop(): Promise` Force stop the CLI server without graceful cleanup. Use when `stop()` takes too long. ##### `createSession(config?: SessionConfig): Promise` Create a new conversation session. **Config:** - `sessionId?: string` - Custom session ID. - `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** - `reasoningEffort?: "low" | "medium" | "high" | "xhigh"` - Reasoning effort level for models that support it. Use `listModels()` to check which models support this option. - `tools?: Tool[]` - Custom tools exposed to the CLI. Tools without `handler` are declaration-only and must be resolved via pending tool-call RPCs. - `systemMessage?: SystemMessageConfig` - System message customization (see below) - `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below) - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. - `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. 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` Resume an existing session. Returns the session with `workspacePath` populated if infinite sessions were enabled. ##### `ping(message?: string): Promise<{ message: string; timestamp: string }>` Ping the server to check connectivity. ##### `listSessions(filter?: SessionListFilter): Promise` List all available sessions. Optionally filter by working directory context. **SessionMetadata:** - `sessionId: string` - Unique session identifier - `startTime: Date` - When the session was created - `modifiedTime: Date` - When the session was last modified - `summary?: string` - Optional session summary - `isRemote: boolean` - Whether the session is remote - `context?: SessionContext` - Working directory context from session creation **SessionContext:** - `cwd: string` - Working directory where the session was created - `gitRoot?: string` - Git repository root (if in a git repo) - `repository?: string` - GitHub repository in "owner/repo" format - `branch?: string` - Current git branch ##### `deleteSession(sessionId: string): Promise` Delete a session and its data from disk. ##### `getForegroundSessionId(): Promise` Get the ID of the session currently displayed in the TUI. Only available when connecting to a server running in TUI+server mode (`--ui-server`). ##### `setForegroundSessionId(sessionId: string): Promise` Request the TUI to switch to displaying the specified session. Only available in TUI+server mode. ##### `onLifecycle(eventType: SessionLifecycleEventType, handler): () => void` Subscribe to a specific session lifecycle event type. Returns an unsubscribe function. ```typescript const unsubscribe = client.onLifecycle("session.foreground", (event) => { console.log(`Session ${event.sessionId} is now in foreground`); }); ``` ##### `onLifecycle(handler: SessionLifecycleHandler): () => void` Subscribe to all session lifecycle events. Returns an unsubscribe function. ```typescript const unsubscribe = client.onLifecycle((event) => { console.log(`${event.type}: ${event.sessionId}`); }); ``` **Lifecycle Event Types:** - `session.created` - A new session was created - `session.deleted` - A session was deleted - `session.updated` - A session was updated (e.g., new messages) - `session.foreground` - A session became the foreground session in TUI - `session.background` - A session is no longer the foreground session --- ### CopilotSession Represents a single conversation session. #### Properties ##### `sessionId: string` The unique identifier for this session. ##### `workspacePath?: string` Path to the session workspace directory when infinite sessions are enabled. Contains `checkpoints/`, `plan.md`, and `files/` subdirectories. Undefined if infinite sessions are disabled. #### Methods ##### `send(options: MessageOptions): Promise` Send a message to the session. Returns immediately after the message is queued; use event handlers or `sendAndWait()` to wait for completion. **Options:** - `prompt: string` - The message/prompt to send - `attachments?: Array<{type, path, displayName}>` - File attachments - `mode?: "enqueue" | "immediate"` - Delivery mode Returns the message ID. ##### `sendAndWait(options: MessageOptions, timeout?: number): Promise` Send a message and wait until the session becomes idle. **Options:** - `prompt: string` - The message/prompt to send - `attachments?: Array<{type, path, displayName}>` - File attachments - `mode?: "enqueue" | "immediate"` - Delivery mode - `timeout?: number` - Optional timeout in milliseconds Returns the final assistant message event, or undefined if none was received. ##### `on(eventType: string, handler: TypedSessionEventHandler): () => void` Subscribe to a specific event type. The handler receives properly typed events. ```typescript // Listen for specific event types with full type inference session.on("assistant.message", (event) => { console.log(event.data.content); // TypeScript knows about event.data.content }); session.on("session.idle", () => { console.log("Session is idle"); }); // Listen to streaming events session.on("assistant.message_delta", (event) => { process.stdout.write(event.data.deltaContent); }); ``` ##### `on(handler: SessionEventHandler): () => void` Subscribe to all session events. Returns an unsubscribe function. ```typescript const unsubscribe = session.on((event) => { // Handle any event type console.log(event.type, event); }); // Later... unsubscribe(); ``` ##### `abort(): Promise` Abort the currently processing message in this session. ##### `getEvents(): Promise` Get all events/messages from this session. ##### `disconnect(): Promise` Disconnect the session and free resources. Session data on disk is preserved for later resumption. ##### `capabilities: SessionCapabilities` Host capabilities reported when the session was created or resumed. Use this to check feature support before calling capability-gated APIs. ```typescript if (session.capabilities.ui?.elicitation) { const ok = await session.ui.confirm("Deploy?"); } ``` 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. ##### `destroy(): Promise` _(deprecated)_ Deprecated — use `disconnect()` instead. --- ## Event Types Sessions emit various events during processing: - `user.message` - User message added - `assistant.message` - Assistant response - `assistant.message_delta` - Streaming response chunk - `tool.execution_start` - Tool execution started - `tool.execution_complete` - Tool execution completed - `command.execute` - Command dispatch request (handled internally by the SDK) - `commands.changed` - Command registration changed - And more... See `SessionEvent` type in the source for full details. ## Image Support The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```typescript // File attachment — runtime reads from disk await session.send({ prompt: "What's in this image?", attachments: [ { type: "file", path: "/path/to/image.jpg", }, ], }); // Blob attachment — provide base64 data directly await session.send({ prompt: "What's in this image?", attachments: [ { type: "blob", data: base64ImageData, mimeType: "image/png", }, ], }); ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: ```typescript await session.send({ prompt: "What does the most recent jpg in this directory portray?" }); ``` ## Streaming Enable streaming to receive assistant response chunks as they're generated: ```typescript const session = await client.createSession({ model: "gpt-5", streaming: true, }); // Wait for completion using typed event handlers const done = new Promise((resolve) => { session.on("assistant.message_delta", (event) => { // Streaming message chunk - print incrementally process.stdout.write(event.data.deltaContent); }); session.on("assistant.reasoning_delta", (event) => { // Streaming reasoning chunk (if model supports reasoning) process.stdout.write(event.data.deltaContent); }); session.on("assistant.message", (event) => { // Final message - complete content console.log("\n--- Final message ---"); console.log(event.data.content); }); session.on("assistant.reasoning", (event) => { // Final reasoning content (if model supports reasoning) console.log("--- Reasoning ---"); console.log(event.data.content); }); session.on("session.idle", () => { // Session finished processing resolve(); }); }); await session.send({ prompt: "Tell me a short story" }); await done; // Wait for streaming to complete ``` When `streaming: true`: - `assistant.message_delta` events are sent with `deltaContent` containing incremental text - `assistant.reasoning_delta` events are sent with `deltaContent` for reasoning/chain-of-thought (model-dependent) - Accumulate `deltaContent` values to build the full response progressively - The final `assistant.message` and `assistant.reasoning` events contain the complete content Note: `assistant.message` and `assistant.reasoning` (final events) are always sent regardless of streaming setting. ## Advanced Usage ### Manual Server Control ```typescript const client = new CopilotClient({}); // Start manually await client.start(); // Use client... // Stop manually await client.stop(); ``` ### Tools You can let the CLI call back into your process when the model needs capabilities you own. Use `defineTool` with Zod schemas for type-safe tool definitions: ```ts import { z } from "zod"; import { CopilotClient, defineTool } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", tools: [ defineTool("lookup_issue", { description: "Fetch issue details from our tracker", parameters: z.object({ id: z.string().describe("Issue identifier"), }), handler: async ({ id }) => { const issue = await fetchIssue(id); return issue; }, }), ], }); ``` When Copilot invokes `lookup_issue`, the client automatically runs your handler and responds to the CLI. Handlers can return any JSON-serializable value (automatically wrapped), a simple string, or a `ToolResultObject` for full control over result metadata. Raw JSON schemas are also supported if Zod isn't desired. #### Overriding Built-in Tools If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the SDK will throw an error unless you explicitly opt in by setting `overridesBuiltInTool: true`. This flag signals that you intend to replace the built-in tool with your custom implementation. ```ts defineTool("edit_file", { description: "Custom file editor with project-specific validation", parameters: z.object({ path: z.string(), content: z.string() }), overridesBuiltInTool: true, handler: async ({ path, content }) => { /* your logic */ }, }); ``` #### Skipping Permission Prompts Set `skipPermission: true` on a tool definition to allow it to execute without triggering a permission prompt: ```ts defineTool("safe_lookup", { description: "A read-only lookup that needs no confirmation", parameters: z.object({ id: z.string() }), skipPermission: true, handler: async ({ id }) => { /* your logic */ }, }); ``` ### Commands Register slash commands so that users of the CLI's TUI can invoke custom actions via `/commandName`. Each command has a `name`, optional `description`, and a `handler` called when the user executes it. ```ts const session = await client.createSession({ onPermissionRequest: approveAll, commands: [ { name: "deploy", description: "Deploy the app to production", handler: async ({ commandName, args }) => { console.log(`Deploying with args: ${args}`); // Do work here — any thrown error is reported back to the CLI }, }, ], }); ``` When the user types `/deploy staging` in the CLI, the SDK receives a `command.execute` event, routes it to your handler, and automatically responds to the CLI. If the handler throws, the error message is forwarded. Commands are sent to the CLI on both `createSession` and `resumeSession`, so you can update the command set when resuming. ### UI Elicitation When the session has elicitation support — either from the CLI's TUI or from another client that registered an `onElicitationRequest` handler (see [Elicitation Requests](#elicitation-requests)) — the SDK can request interactive form dialogs from the user. The `session.ui` object provides convenience methods built on a single generic `elicitation` RPC. > **Capability check:** Elicitation is only available when at least one connected participant advertises support. Always check `session.capabilities.ui?.elicitation` before calling UI methods — this property updates automatically as participants join and leave. ```ts const session = await client.createSession({ onPermissionRequest: approveAll }); if (session.capabilities.ui?.elicitation) { // Confirm dialog — returns boolean const ok = await session.ui.confirm("Deploy to production?"); // Selection dialog — returns selected value or null const env = await session.ui.select("Pick environment", ["production", "staging", "dev"]); // Text input — returns string or null const name = await session.ui.input("Project name:", { title: "Name", minLength: 1, maxLength: 50, }); // Generic elicitation with full schema control const result = await session.ui.elicitation({ message: "Configure deployment", requestedSchema: { type: "object", properties: { region: { type: "string", enum: ["us-east", "eu-west"] }, dryRun: { type: "boolean", default: true }, }, required: ["region"], }, }); // result.action: "accept" | "decline" | "cancel" // result.content: { region: "us-east", dryRun: true } (when accepted) } ``` All UI methods throw if elicitation is not supported by the host. ### System Message Customization Control the system prompt using `systemMessage` in session config: ```typescript const session = await client.createSession({ model: "gpt-5", systemMessage: { content: ` - Always check for security vulnerabilities - Suggest performance improvements when applicable `, }, }); ``` The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"` or `mode: "customize"`. #### Customize Mode Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest: ```typescript import { SYSTEM_MESSAGE_SECTIONS } from "@github/copilot-sdk"; import type { SectionOverride, SystemMessageSection } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", systemMessage: { mode: "customize", sections: { // Replace the tone/style section tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations.", }, // Remove coding-specific rules code_change_rules: { action: "remove" }, // Append to existing guidelines guidelines: { action: "append", content: "\n* Always cite data sources" }, }, // Additional instructions appended after all sections content: "Focus on financial analysis and reporting.", }, }); ``` Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`. Use the `SYSTEM_MESSAGE_SECTIONS` constant for descriptions of each section. Each section override supports four actions: - **`replace`** — Replace the section content entirely - **`remove`** — Remove the section from the prompt - **`append`** — Add content after the existing section - **`prepend`** — Add content before the existing section Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. #### Replace Mode For full control (removes all guardrails), use `mode: "replace"`: ```typescript const session = await client.createSession({ model: "gpt-5", systemMessage: { mode: "replace", content: "You are a helpful assistant.", }, }); ``` ### Infinite Sessions By default, sessions use **infinite sessions** which automatically manage context window limits through background compaction and persist state to a workspace directory. ```typescript // Default: infinite sessions enabled with default thresholds const session = await client.createSession({ model: "gpt-5" }); // Access the workspace path for checkpoints and files console.log(session.workspacePath); // => ~/.copilot/session-state/{sessionId}/ // Custom thresholds const session = await client.createSession({ model: "gpt-5", infiniteSessions: { enabled: true, backgroundCompactionThreshold: 0.8, // Start compacting at 80% context usage bufferExhaustionThreshold: 0.95, // Block at 95% until compaction completes }, }); // Disable infinite sessions const session = await client.createSession({ model: "gpt-5", infiniteSessions: { enabled: false }, }); ``` When enabled, sessions emit compaction events: - `session.compaction_start` - Background compaction started - `session.compaction_complete` - Compaction finished (includes token counts) ### Multiple Sessions ```typescript const session1 = await client.createSession({ model: "gpt-5" }); const session2 = await client.createSession({ model: "claude-sonnet-4.5" }); // Both sessions are independent await session1.sendAndWait({ prompt: "Hello from session 1" }); await session2.sendAndWait({ prompt: "Hello from session 2" }); ``` ### Custom Session IDs ```typescript const session = await client.createSession({ sessionId: "my-custom-session-id", model: "gpt-5", }); ``` ### File Attachments ```typescript await session.send({ prompt: "Analyze this file", attachments: [ { type: "file", path: "/path/to/file.js", displayName: "My File", }, ], }); ``` ### Custom Providers The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own Key), including local providers like Ollama. When using a custom provider, you must specify the `model` explicitly. **ProviderConfig:** - `type?: "openai" | "azure" | "anthropic"` - Provider type (default: "openai") - `baseUrl: string` - API endpoint URL (required) - `apiKey?: string` - API key (optional for local providers like Ollama) - `bearerToken?: string` - Bearer token for authentication (takes precedence over apiKey) - `wireApi?: "completions" | "responses"` - API format for OpenAI/Azure (default: "completions") - `azure?.apiVersion?: string` - Azure API version (default: "2024-10-21") **Example with Ollama:** ```typescript const session = await client.createSession({ model: "deepseek-coder-v2:16b", // Required when using custom provider provider: { type: "openai", baseUrl: "http://localhost:11434/v1", // Ollama endpoint // apiKey not required for Ollama }, }); await session.sendAndWait({ prompt: "Hello!" }); ``` **Example with custom OpenAI-compatible API:** ```typescript const session = await client.createSession({ model: "gpt-4", provider: { type: "openai", baseUrl: "https://my-api.example.com/v1", apiKey: process.env.MY_API_KEY, }, }); ``` **Example with Azure OpenAI:** ```typescript const session = await client.createSession({ model: "gpt-4", provider: { type: "azure", // Must be "azure" for Azure endpoints, NOT "openai" baseUrl: "https://my-resource.openai.azure.com", // Just the host, no path apiKey: process.env.AZURE_OPENAI_KEY, azure: { apiVersion: "2024-10-21", }, }, }); ``` > **Important notes:** > > - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified. > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`. > - The `baseUrl` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically. ## Telemetry The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export from the CLI process — this is all most users need: ```typescript const client = new CopilotClient({ telemetry: { otlpEndpoint: "http://localhost:4318", }, }); ``` With just this configuration, the CLI emits spans for every session, message, and tool call to your collector. No additional dependencies or setup required. **TelemetryConfig options:** - `otlpEndpoint?: string` - OTLP HTTP endpoint URL - `filePath?: string` - File path for JSON-lines trace output - `exporterType?: string` - `"otlp-http"` or `"file"` - `sourceName?: string` - Instrumentation scope name - `captureContent?: boolean` - Whether to capture message content ### Advanced: Trace Context Propagation > **You don't need this for normal telemetry collection.** The `telemetry` config above is sufficient to get full traces from the CLI. `onGetTraceContext` is only needed if your application creates its own OpenTelemetry spans and you want them to appear in the **same distributed trace** as the CLI's spans — for example, to nest a "handle tool call" span inside the CLI's "execute tool" span, or to show the SDK call as a child of your application's request-handling span. If you're already using `@opentelemetry/api` in your app and want this linkage, provide a callback: ```typescript import { propagation, context } from "@opentelemetry/api"; const client = new CopilotClient({ telemetry: { otlpEndpoint: "http://localhost:4318" }, onGetTraceContext: () => { const carrier: Record = {}; propagation.inject(context.active(), carrier); return carrier; }, }); ``` Inbound trace context from the CLI is available on the `ToolInvocation` object passed to tool handlers as `traceparent` and `tracestate` fields. See the [OpenTelemetry guide](../docs/observability/opentelemetry.md) for a full wire-up example. ## Permission Handling An `onPermissionRequest` handler is optional when you create or resume a session. When provided, it is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and returns a decision. When omitted, permission requests are emitted as events and left pending for the consumer to resolve with the pending permission RPC. ### Approve All (simplest) Use the built-in `approveAll` helper to allow every tool call without any checks: ```typescript import { CopilotClient, approveAll } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, }); ``` ### Custom Permission Handler Provide your own function to inspect each request and apply custom logic: ```typescript import type { PermissionRequest, PermissionRequestResult } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", onPermissionRequest: (request: PermissionRequest, invocation): PermissionRequestResult => { // request.kind — what type of operation is being requested: // "shell" — executing a shell command // "write" — writing or editing a file // "read" — reading a file // "mcp" — calling an MCP tool // "custom-tool" — calling one of your registered tools // "url" — fetching a URL // "memory" — storing or retrieving persistent session memory // "hook" — invoking a server-side hook or integration // (additional kinds may be added; include a default case in handlers) // request.toolCallId — the tool call that triggered this request // request.toolName — name of the tool (for custom-tool / mcp) // request.fileName — file being written (for write) // request.fullCommandText — full shell command (for shell) if (request.kind === "shell") { // Deny shell commands, optionally telling the model why return { kind: "reject", feedback: "Shell commands are not allowed." }; } return { kind: "approve-once" }; }, }); ``` ### Permission Result Kinds The handler must return one of the `PermissionDecision` shapes (or `{ kind: "no-result" }`). Approval scopes are present-tense — they describe the decision to apply, not the outcome reported back on session events: | Kind | Meaning | Extra fields | | ------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `"approve-once"` | Allow this single request | — | | `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | | `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | | `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | | `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | | `"user-not-available"` | Deny the request because no user is available to confirm it | — | | `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | — | ### Resuming Sessions You may pass `onPermissionRequest` when resuming a session too: ```typescript const session = await client.resumeSession("session-id", { onPermissionRequest: approveAll, }); ``` ### Per-Tool Skip Permission To let a specific custom tool bypass the permission prompt entirely, set `skipPermission: true` on the tool definition. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler: ```typescript const session = await client.createSession({ model: "gpt-5", onUserInputRequest: async (request, invocation) => { // request.question - The question to ask // request.choices - Optional array of choices for multiple choice // request.allowFreeform - Whether freeform input is allowed (default: true) console.log(`Agent asks: ${request.question}`); if (request.choices) { console.log(`Choices: ${request.choices.join(", ")}`); } // Return the user's response return { answer: "User's answer here", wasFreeform: true, // Whether the answer was freeform (not from choices) }; }, }); ``` ## Elicitation Requests Register an `onElicitationRequest` handler to let your client act as an elicitation provider — presenting form-based UI dialogs on behalf of the agent. When provided, the server notifies your client whenever a tool or MCP server needs structured user input. ```typescript const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, onElicitationRequest: async (context) => { // context.sessionId - Session that triggered the request // context.message - Description of what information is needed // context.requestedSchema - JSON Schema describing the form fields // context.mode - "form" (structured input) or "url" (browser redirect) // context.elicitationSource - Origin of the request (e.g. MCP server name) console.log(`Elicitation from ${context.elicitationSource}: ${context.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: ```typescript const session = await client.createSession({ model: "gpt-5", hooks: { // Called before each tool execution onPreToolUse: async (input, invocation) => { console.log(`About to run tool: ${input.toolName}`); // Return permission decision and optionally modify args return { permissionDecision: "allow", // "allow", "deny", or "ask" modifiedArgs: input.toolArgs, // Optionally modify tool arguments additionalContext: "Extra context for the model", }; }, // Called after each successful tool execution onPostToolUse: async (input, invocation) => { console.log(`Tool ${input.toolName} completed`); // Optionally modify the result or add context return { additionalContext: "Post-execution notes", }; }, // Called after a tool execution whose result was "failure". // onPostToolUse does NOT fire for failed tool calls — register this // hook to observe them. Input includes `error` (the failure message // extracted from the tool's result), not the full result object. onPostToolUseFailure: async (input, invocation) => { console.log(`Tool ${input.toolName} failed: ${input.error}`); // Optionally append hidden guidance to the model. return { additionalContext: "Suggest checking inputs and retrying." }; }, // Called when user submits a prompt onUserPromptSubmitted: async (input, invocation) => { console.log(`User prompt: ${input.prompt}`); return { modifiedPrompt: input.prompt, // Optionally modify the prompt }; }, // Called when session starts onSessionStart: async (input, invocation) => { console.log(`Session started from: ${input.source}`); // "startup", "resume", "new" return { additionalContext: "Session initialization context", }; }, // Called when session ends onSessionEnd: async (input, invocation) => { console.log(`Session ended: ${input.reason}`); }, // Called when an error occurs onErrorOccurred: async (input, invocation) => { console.error(`Error in ${input.errorContext}: ${input.error}`); return { errorHandling: "retry", // "retry", "skip", or "abort" }; }, }, }); ``` **Available hooks:** - `onPreToolUse` - Intercept tool calls before execution. Can allow/deny or modify arguments. - `onPostToolUse` - Process tool results after **successful** execution. Can modify results or add context. - `onPostToolUseFailure` - Observe and append hidden guidance to the model after tool executions whose result was `"failure"`. Register this in addition to `onPostToolUse` to see failed tool calls. - `onUserPromptSubmitted` - Intercept user prompts. Can modify the prompt before processing. - `onSessionStart` - Run logic when a session starts or resumes. - `onSessionEnd` - Cleanup or logging when session ends. - `onErrorOccurred` - Handle errors with retry/skip/abort strategies. ## Error Handling ```typescript try { const session = await client.createSession(); await session.send({ prompt: "Hello" }); } catch (error) { console.error("Error:", error.message); } ``` ## Requirements - Node.js ^20.19.0 or >=22.12.0 - GitHub Copilot CLI installed and in PATH (or provide a custom `connection`) ## License MIT