# 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 The SDK uses an optional `@github/copilot-sdk-` package containing the Copilot CLI runtime for the host platform. These packages are built from verified `github/copilot-cli` release assets when the SDK is published, so starting the SDK performs no runtime download. Set `COPILOT_CLI_PATH` to use an existing installation instead. The checked-in `copilotCliVersion` in `package.json` and compiled metadata in `src/cliVersion.ts` use a development placeholder. The public SDK snapshot replaces both with the CLI version published for that snapshot. `npm run pack:release` builds the main package and all platform packages. Set `COPILOT_CLI_DOWNLOAD_BASE_URL` to use a release mirror while packaging. Release workflows instead set `COPILOT_SDK_RUNTIME_PACKAGE_DIR` to a directory containing validated runtime npm package roots named for all eight platforms. This keeps `COPILOT_CLI_USE_NPM_PACKAGE` false and embeds those runtime files in the self-contained SDK platform packages. In the runtime repository, packaging uses the prepared same-checkout runtime. Set `COPILOT_SDK_RUNTIME_PLATFORMS` to the available target (for example, `linux-x64`) for both `pack:release` and `verify:release-packages`. SDK CI checks that target only; public release workflows leave this unset to package and verify all eight platforms. ## Installation ```bash npm install @github/copilot-sdk ``` ## Runtime-supervised AHP host (experimental) `startAhpHost()` exposes copilotd's complete Agent Host Protocol server through the same runtime used by the SDK: ```typescript import { CopilotClient } from "@github/copilot-sdk"; await using client = new CopilotClient(); await client.start(); const host = await client.startAhpHost({ localServer: {}, onExit: (exit) => { if (exit.error) console.error(exit.error); }, }); // Connect an AHP client using host.url and host.token. // Treat host.token as a secret; do not log it. // Existing SDK sessions and AHP sessions share this runtime. // Keep client alive while the listener is needed. Leaving this scope disposes // client, and the runtime stops its listener without a separate host.dispose(). ``` The runtime hosts the complete AHP server in-process; the SDK does not launch a second runtime or relay the host's traffic. The host has its own SDK connection and belongs to the client connection that started it. Explicit disposal, connection loss, and runtime shutdown stop the listener and its hosting task without deleting underlying sessions. Reconnecting does not reclaim a host. The optional `onExit` callback reports exits at most once. If the owner connection is lost, it reports that loss rather than claiming that listener cleanup was acknowledged. `host.pid` is absent for in-process listeners. The optional field is retained for separate host process IDs returned by legacy runtimes, never the runtime PID. Use `dispose()` to stop the listener. `reason: "exited"` reports hosting-task failure, not runtime process death, and `exitCode` is absent. Hosting no longer provides process isolation from the runtime. Select at least one transport explicitly: `localServer: {}` enables the local listener, `githubEnvironment: { name: "My app", computeId: "stable-installation-id" }` registers a Mission Control environment and enables remote WPS connections, and both enables both transports. GitHub-only hosting opens no local listener; `host.url` and `host.token` are undefined, while `host.environmentId` identifies the environment. The application supplies a stable compute ID and configures transports only at startup. Dispose and recreate the host to change them. The runtime validates options inside `localServer` and applies their defaults: - `hostname` defaults to `127.0.0.1`. Set it explicitly to request a non-loopback listener, such as `hostname: "0.0.0.0"`, and restrict network access appropriately. - `port` defaults to `0`, which selects an available port. - `requireConnectionToken` defaults to `true`. The runtime generates a random token unless you supply a nonempty `token`. - Set `requireConnectionToken: false` to disable token authentication; `host.token` is then undefined. A supplied `token` cannot be combined with `requireConnectionToken: false`. The listener follows the owning client's lifetime. Call `await host.dispose()` only when you want to stop it earlier; `await using host` also supports a shorter scope. Each disposal call forwards to the runtime, which owns idempotent cleanup. The AHP transport remains owned by the host. ### Application-owned sessions Supply `createSession` to materialize fresh AHP sessions in your application: ```typescript import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk"; await using client = new CopilotClient(); await using host = await client.startAhpHost({ localServer: {}, createSession: ({ config, signal }) => { signal.throwIfAborted(); return client.createSession({ ...config, onPermissionRequest: approveAll, systemMessage: { mode: "append", content: "Use the app's greeting tool." }, tools: [defineTool("greeting", { description: "Get the application's greeting", parameters: { type: "object", properties: {} }, handler: () => "Hello from the application!", })], }); }, onSessionReleased: async (originalSession) => { // Optional: the app decides whether to disconnect, keep, or destroy it. await originalSession.disconnect(); }, }); ``` Preserve the supplied `config`, including its fresh session identity, workspace, and selected host settings. Add your prompt and tools where the host has not explicitly selected those settings; conflicting settings fail rather than silently advertising configuration that was not applied. Return a normal session created by this same client. The creation factory does not adopt an arbitrary existing session or expose unrelated application sessions in the AHP catalog. Only the session ID returns through ordinary SDK RPC. The host attaches to **that same resident session**, adding its own callback/tool registrations without replacing the application's prompt, tool filters, hooks, or tools. Application tool functions continue running in the app while their results stream through the existing AHP projector. No second application connection or function serialization is involved. The SDK retains the original returned object until participation ends and invokes `onSessionReleased` at most once per handoff, including attach failure, hosting-task exit, and owner disconnection. The SDK never automatically disconnects or destroys the app object. The creation callback receives an abort signal; materialization is bounded to 30 seconds and cancellation also releases objects returned late. Graceful host disposal waits for AHP detach before reporting release. Omitting `createSession` preserves copilotd-owned creation. To restore durable application-owned sessions, also supply `resumeSession`. It receives `{ sessionId, config, signal }` (`AhpSessionResumeRequest`). Return the object from this client's `resumeSession(sessionId, { ...config, onPermissionRequest, ... })`, restoring your tools, hooks, and handlers. Alternatively, return a retained original session from this client when it still matches the requested identity and workspace. Only catalog entries marked as application-owned invoke this callback. If the callback is missing, restoring such an entry fails instead of falling back to host-owned creation. Published resident sessions attach directly, without invoking it or replacing their current registrations. Resumed sessions follow the same original-object retention, cancellation, late-result release, and `onSessionReleased` rules. The `copilotd-hosting` library runs inside the runtime provider. Development integrations require a source-built launcher and provider (`COPILOT_RUNTIME_PROVIDER_LIB`). Older runtimes without these RPC operations cannot start a host. ## Run the Sample Try the interactive chat sample (from the repo root): ```bash cd nodejs npm ci npm run build export COPILOT_CLI_PATH="$(npm run --silent prepare:runtime -- --print-path)" 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(); // approveAll is only valid when managed settings are disabled. 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 ``` When targeting MCP tools configured through `mcpServers`, remember the runtime tool name is `-`. For `availableTools` and `excludedTools`, prefer `new ToolSet().addMcp("-")` or the raw `mcp:-` form. For `customAgents[].tools` and `defaultAgent.excludedTools`, use `-` directly. ## 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). - Managed child-process connections materialize the bundled `copilot-runtime` and adjacent `runtime.node`, then launch the wrapper by default. An explicit connection `path` or `COPILOT_CLI_PATH` overrides the bundled runtime. - `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`. - `extensionLaunchProvider?: ExtensionLaunchProvider` - Experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can be created. - `installationConfirmationHandler?: InstallationConfirmationHandler` - Experimental connection-global human review for `installations.confirm`. Receives the typed request and independent request/connection cancellation signals, and returns an explicit decision. Does not enable installation capabilities. - `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`. #### Installation confirmation (experimental) All six SDKs (Node.js, Python, Go, .NET, Java and Rust) provide this receiver with the same semantics: each review gets one cancellation token, concurrent reviews are independent, and a decision returned after cancellation is never sent. Without a configured handler, an `installations.confirm` request is refused, which the runtime treats as no consent. The installation confirmation handler receives the generated `InstallationConfirmationRequest` and a `CancellationToken`. Match `operationId` and `policySessionId` against the exact original action on this connection before presenting the complete review. Refuse unknown operations or incomplete reviews; missing legacy session metadata is not permission to use the current session. Return `"confirm"`, `"decline"` or `"cancel"` only after an explicit human decision. The SDK echoes the original challenge and fingerprint. Concurrent reviews remain independent. The token is cancelled when the runtime retires the request, including runtime-enforced expiry, or when the original connection closes. Observe it to close pending UI. Late handler results cannot approve a retired request. This incoming signal does not cancel outbound installation or OAuth RPCs, and dropping those promises is not cancellation. Use `client.rpc.mcp.prepareInstall` before `applyInstall`: preparation returns an inert runtime-issued `operationId` and original expiry. Register that ID with its captured session on this exact client before applying. Removal uses `planUninstall` then `applyUninstall`; the returned `operationId` identifies the operation, while `planHandle` is the one-use removal input. Never interchange them. Use `client.rpc.mcp.installations.list` and `recover` for owned inventory. Inspect or cancel uncertain work through `status` and `cancel` on the original connection and operation ID, without selecting a replacement session or replaying apply. Owned OAuth similarly uses `session.rpc.mcp.oauth.prepareLogin` to obtain `loginId` before browser, network or cached-reconnect work. Retain that ID with the original session and `expectedInstallationId` for `login` and `cancelLogin`. Prepare freezes reauthentication and display options. Cancelling an incoming confirmation or abandoning a login promise is not a substitute for `cancelLogin`. Manual MCP OAuth retains its direct `login` path. A matching runtime contract and available owned-lifecycle support are required. Capability negotiation does not promise availability; preserve typed refusals instead of falling back to raw configuration writes. Generated presence and transport tests do not establish a working installer, live OAuth or restart safety. #### 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. For an owned stdio runtime, closes stdin and waits up to 10 seconds for host cleanup (including telemetry export) and process exit before falling back to termination. This graceful-exit timeout is separate from the shutdown RPC and post-termination wait. ##### `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.** - `capi?: CapiSessionOptions` - Copilot API options. With `model: "auto"`, set `autoTier` to `"efficiency"`, `"balance"`, `"intelligence"`, or `"fast"` to choose a routing preference. `"fast"` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max"` - 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) - `workingDirectory?: string` - Working directory for the session (default: runtime process cwd). - `refreshCustomInstructions?: boolean` - Invalidates the process-wide custom-instruction discovery cache before creating this session, so instruction-file edits made in the same runtime are read again. Defaults to `false` (cache reuse). Other sessions in this runtime may observe updated instructions on later turns or discovery. This does not watch files or enable disabled instruction loading. Available on `SessionConfig`, not `ResumeSessionConfig`. - `enableSessionStore?: boolean` - Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. - `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`. - `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. `approveAll` approves requests when managed settings are disabled and throws when `enableManagedSettings` is true. Custom handlers can inspect `managedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `onUserInputRequest?: UserInputHandler` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`. - `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. ```typescript const session = await client.createSession({ gitHubTokenProvider: async ({ host }) => ({ kind: "token", accessToken: await acquireTokenForHost(host), expiresIn: 8 * 60 * 60, }), }); ``` Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. ##### `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 - `source?: MessageSource` - `"user"`, `"system"`, or `` `agent-${string}` `` provenance; omitted by default - `attachments?: Array<{type, path, displayName}>` - File attachments - `mode?: "enqueue" | "immediate"` - Delivery mode Returns the message ID. Use `source: "system"` for automated messages from your application: ```typescript await session.send({ prompt: "Context updated", source: "system" }); ``` For a message from another agent, use its trusted sender ID: ```typescript await session.send({ prompt: "Review complete", source: "agent-reviewer-id" }); ``` Source is independent of delivery mode. Leaving it unset preserves the existing human-message payload; it does not set billing flags or use the notification API. ##### `sendAndWait(options: MessageOptions, timeout?: number): Promise` Send a message and wait until the session becomes idle. Sub-agent events are still delivered to listeners, but do not complete the wait or supply its reply. **Options:** - `prompt: string` - The message/prompt to send - `source?: MessageSource` - Same optional provenance as `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. ##### Structured output (preview) Requires a runtime build with `responseFormat` and `originatingMessageId` support. Pass a raw JSON Schema or a Zod schema as `responseSchema` to `send` or `sendAndWait`. As with custom tool parameters, the SDK converts Zod schemas to JSON Schema before sending them: ```typescript import { z } from "zod"; const answerSchema = z.object({ answer: z.number().int() }); const message = await session.sendAndWait({ prompt: "What is 19 + 23?", responseSchema: answerSchema, }); console.log(message?.data.content); // JSON text ``` For a typed result, pass the Zod schema as the **second argument** instead: ```typescript const answer = await session.sendAndWait("What is 19 + 23?", answerSchema); console.log(answer.answer); // number; TResult is inferred from answerSchema ``` `sendAndWait(options, schema, timeout?)` generates the JSON Schema from the schema value, parses the final JSON, and validates it with the schema's `parse` method. TypeScript cannot derive a runtime schema from an erased type parameter alone. Invalid JSON, a schema mismatch, or a completed run without a matching assistant message throws. Do not also set `options.responseSchema` when using the typed overload. The schema belongs to the submitted run, including its tool-call iterations. Internally generated stop-hook corrections retain the schema and originating message ID, so the wait returns the corrected answer. Independent subsequent sends do not inherit it. Ordinary immediate steering inherits the active schema and originating message ID, even when it arrives too late for the current model request and is promoted into a follow-up run. Specifying a schema with `mode: "immediate"` is rejected, even while idle. The generated `session.rpc.send` and `session.rpc.sendMessages` wrappers expose the full `responseFormat` contract when you need to set its name, description, or strict option rather than using the convenience defaults (`name: "response"`, `strict: true`). Each batch starts one run: the final returned message ID is its origin, preceding messages are context, and an empty batch has no origin. An immediate batch steers the active run instead and retains its origin. The schema is not a persisted session default: autonomous resume-pending work after a restart does not restore it. A terminal tool that clears context ends the old run; its fresh seed does not inherit the schema or origin. Such a run can finish without a structured result, in which case the typed wait throws. After a successful terminal tool, the runtime disables tools while the model produces the structured result. Stop-hook corrections remain supported. Remote sessions and known HydraFusion routes reject response formats before admission. Schemas larger than 32 MiB when JSON-encoded are also rejected before admission, using the runtime's existing request-size ceiling. This does not guarantee the schema plus conversation and tools fits the provider's budget. Structured waits select the last root-agent message whose `originatingMessageId` matches the ID returned by their send, then return at a non-autopilot `session.idle`. Other queued work can delay that idle, but cannot replace the selected result. The existing unformatted overload retains its session-wide behavior. `turnId` identifies an individual model/tool iteration, not the whole run; telemetry interaction IDs are not unique run identifiers. For event-driven consumption with `send`, subscribe before sending and collect root `assistant.message` events whose `data.originatingMessageId` matches the ID returned by `send`; events may arrive before that acknowledgement. Wait for `session.idle`, then parse the last matching message without tool requests. An earlier response may be superseded by a stop-hook correction. Handle `session.error` and aborted idle events rather than returning a partial result. Streaming still delivers ordinary text events, including intermediate messages and tool calls. Only the final selected message is parsed by the typed overload; not every event is necessarily a complete schema-conforming JSON document. Provider errors, refusals, cancellation, truncation, session errors, and timeouts can prevent a typed result. A timeout stops waiting, not the runtime's work. Use a model and endpoint that support native structured output. An API-compatible gateway may ignore format fields even when it accepts the request; for example, the Claude Chat-completions compatibility route is not equivalent to Anthropic's native `output_config.format` endpoint. ##### `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(); ``` ##### `setModel(model: string, options?): Promise` Change the model for this session. The new model takes effect for the next message; conversation history is preserved. **Options:** - `reasoningEffort?: string` - Reasoning effort level - `autoTier?: AutoTier | null` - Auto routing preference to stage together with selecting `auto`. Pass `null` to return to the provider's default Auto routing; omit it to leave the current preference unchanged. ##### `setAutoTier(autoTier: AutoTier | null): Promise` Change the Auto routing preference without changing the selected model. Pass `null` to return to the provider's default Auto routing. The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. A failed activation leaves the incumbent effective tier unchanged. Read the authoritative state at any time with `session.rpc.model.getCurrent()`. ```typescript const result = await session.setAutoTier("intelligence"); if (result.status === "pending") { // Accepted, but not yet in effect. } ``` See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. ##### `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 */ }, }); ``` #### 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: ` - 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: `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; when omitted, the runtime uses the GA versionless `v1` route **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 = {}; 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 when managed settings are disabled: ```typescript import { CopilotClient, approveAll } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, }); ``` When `enableManagedSettings` is true for the session, `approveAll` throws. Use a custom handler for managed sessions; request-level `managedApprovalRequired` remains available for human-facing confirmation logic. ### Custom Permission Handler Provide your own function to inspect each request and apply custom logic. Check `managedApprovalRequired` before any automatic approval: ```typescript import type { PermissionRequest, PermissionRequestResult } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", onPermissionRequest: (request: PermissionRequest, invocation): PermissionRequestResult => { if ("managedApprovalRequired" in request && request.managedApprovalRequired === true) { // Leave the request pending for the host's human-facing confirmation flow. return { kind: "no-result" }; } // 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"` | Suppress this SDK client's response so another connected client can answer the pending request | — | ### 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 legacy question-and-answer `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, askUserVariant: "elicitation", 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 ``` Set `askUserVariant: "elicitation"` to expose the structured form as the model's `ask_user` tool. Omit it to retain the legacy SDK behavior. 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" }; }, // Called when the top-level agent naturally stops onAgentStop: async (input, invocation) => { if (!input.stopHookActive && needsMoreWork()) { return { decision: "block", reason: "Run the final validation and fix any failures.", }; } }, }, }); ``` **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. - `onAgentStop` - Observe natural top-level agent completion. Return `{ decision: "block", reason }` to request another turn; use `stopHookActive` to avoid repeated blocks. ## Error Handling ```typescript try { const session = await client.createSession(); await session.send({ prompt: "Hello" }); } catch (error) { console.error("Error:", error.message); } ``` ## Development From the repository root: ```bash cd test/harness npm ci ``` ```bash cd nodejs npm ci npm test ``` ## License MIT