# Copilot SDK for Node.js/TypeScript

TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.

## Prerequisites

To use the SDK, you'll need:

- Node.js ^20.19.0 or >=22.12.0

## 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<void>((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?, env? })` (default) — spawn the runtime and communicate over its stdin/stdout.
    - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args?, env? })` — 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.
    - `RuntimeConnection.forInProcess()` — host the runtime in-process over its native C ABI (FFI). **Experimental.** Because the runtime shares this process, `env`, `telemetry`, and `workingDirectory` are rejected with this transport; set them on the host process instead.
    - The child-process transports (`forStdio`/`forTcp`) also accept a per-connection `env`. Set it there or via the top-level `env` option — not both (setting both throws).
- `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<string, string | undefined>` - 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[]> | 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<void>`

Start the CLI server and establish connection.

##### `stop(): Promise<Error[]>`

Stop the server and close all sessions. Returns a list of any errors encountered during cleanup.

##### `forceStop(): Promise<void>`

Force stop the CLI server without graceful cleanup. Use when `stop()` takes too long.

##### `createSession(config?: SessionConfig): Promise<CopilotSession>`

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<CopilotSession>`

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<SessionMetadata[]>`

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<void>`

Delete a session and its data from disk.

##### `getForegroundSessionId(): Promise<string | undefined>`

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<void>`

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<string>`

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<AssistantMessageEvent | undefined>`

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<void>`

Abort the currently processing message in this session.

##### `getEvents(): Promise<SessionEvent[]>`

Get all events/messages from this session.

##### `disconnect(): Promise<void>`

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<void>` _(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<void>((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 */
    },
});
```

#### Deferring Tools

Set `defer` to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use `"auto"` to allow the tool to be deferred and surfaced through tool search, or `"never"` to force it to always be pre-loaded. Defaults to `"auto"`.

```ts
defineTool("lookup_issue", {
    description: "Fetch issue details",
    parameters: z.object({ id: z.string() }),
    defer: "auto",
    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: `
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
`,
    },
});
```

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: `preamble`, `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.

`identity` and `tool_instructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `preamble` to target just the identity preamble without affecting its sibling sub-sections.

Each section override supports five 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
- **`preserve`** — No-op that opts an individually-addressable section out of a group-level `remove`

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)

### Memory

Sessions can opt in to the memory feature, which lets the agent persist and recall
information across turns. Provide a `memory` configuration on session create or resume;
when omitted, the runtime default applies. In the default `"copilot-cli"` client mode the
SDK leaves `memory` unset so the runtime applies its own default, while `"empty"` mode
defaults `memory` to disabled unless you set it explicitly.
For more background, see [About GitHub Copilot Memory](https://docs.github.com/en/copilot/concepts/agents/copilot-memory).

```typescript
// Enable memory for a session
const session = await client.createSession({
    model: "gpt-5",
    memory: { enabled: true },
});

// Disable memory for a session
const session = await client.createSession({
    model: "gpt-5",
    memory: { enabled: false },
});
```

### 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
- `otlpProtocol?: "http/json" | "http/protobuf"` - OTLP HTTP protocol for all signals
- `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<string, string> = {};
        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);
}
```

## License

MIT
