The helpers namespace provides simple, stateless query functions with automatic client management.
(require '[github.copilot-sdk.helpers :as h])(h/query prompt & {:keys [client session timeout-ms]})Execute a query and return the response text.
Options:
:client- Client options map (cli-path, log-level, cwd, env) OR a CopilotClient instance:session- Session options map (model, system-prompt, tools, etc.) OR a CopilotSession instance:timeout-ms- Timeout in milliseconds (default: 180000)
When :session is a CopilotSession instance, the query uses that session directly (enabling multi-turn conversations). When :client is a CopilotClient instance, it uses that client directly.
;; Simple query (shared client, fresh session)
(h/query "What is 2+2?")
;; => "4"
;; With session options
(h/query "Explain monads" :session {:on-permission-request copilot/approve-all :model "claude-sonnet-4.5"})
;; With system prompt
(h/query "Hello" :session {:on-permission-request copilot/approve-all :system-prompt "Be concise."})
;; With explicit client
(copilot/with-client [client {}]
(h/query "What is Clojure?" :client client))
;; With explicit session (multi-turn conversation)
(copilot/with-client [client {}]
(copilot/with-session [session client {:on-permission-request copilot/approve-all}]
(h/query "My name is Alice." :session session)
(h/query "What is my name?" :session session))) ;; context preserved!(h/query-seq! prompt & {:keys [client session max-events]})Execute a query and return a bounded lazy sequence of events (default: 256 events).
Warning: cleanup (session disconnect) runs only when the sequence is consumed to its natural end — a
:copilot/session.idle / :copilot/session.error event, or the events channel closing (detected when the
next read yields nil, the end-of-stream sentinel — not an emitted element). Abandoning the seq before it
reaches a terminal event (e.g. (first ...) or (take 1 ...) when the first element isn't already terminal),
or hitting a positive :max-events bound before that end of stream, leaks
the session and its event tap (the sole exception is :max-events 0, which disconnects
immediately without emitting anything). Consume the whole seq, or use query-chan (explicit lifecycle — safe
to stop early provided you close the returned channel) or query when you may stop reading early.
(->> (h/query-seq! "Tell me a story" :session {:on-permission-request copilot/approve-all :streaming? true})
(filter #(= :copilot/assistant.message_delta (:type %)))
(map #(get-in % [:data :delta-content]))
(run! print))(h/query-chan prompt & {:keys [client session buffer]})Execute a query and return a core.async channel of events. Use this when you need an explicit lifecycle or want to stop reading early without leaking session resources.
(let [ch (h/query-chan "Tell me a story" :session {:on-permission-request copilot/approve-all :streaming? true})]
(go-loop []
(when-let [event (<! ch)]
(when (= :copilot/assistant.message_delta (:type event))
(print (get-in event [:data :delta-content])))
(recur))))(h/shutdown!)Explicitly shutdown the shared client. Safe to call multiple times.
(h/client-info)
;; => {:client-opts {:log-level :info, ...} :connected? true}Get information about the current shared client state. Returns nil if no shared client exists, otherwise a map with :client-opts and :connected? keys.
(require '[github.copilot-sdk :as copilot])The Clojure SDK maintains strict API parity with the official Node.js SDK
(@github/copilot-sdk), but a handful of names and return shapes are
adapted to Clojure idioms. When translating Node.js examples, use this map:
| Clojure | Official Node.js SDK | Notes |
|---|---|---|
:disable-resume? |
suppressResumeEvent |
Config key on resume-session / join-session. When true, skips emitting the session.resume event. Defaults to true in join-session (matching upstream), false elsewhere. |
:max-input-tokens |
maxPromptTokens |
BYOK provider/model config key (input/prompt token cap). Serialized back to maxPromptTokens on the wire. |
join-session return {:client :session} |
joinSession() returns CopilotSession |
Clojure has no implicit/global client, so it returns both so the caller can own the client lifecycle. See join-session. |
These are the only cases where a public Clojure key or return value does
not map 1:1 to the upstream name. Everything else follows the standard
kebab-case ↔ camelCase wire convention (e.g. :working-directory ↔
workingDirectory), which is applied automatically and needs no lookup.
(copilot/client options)Options:
| Key | Type | Default | Description |
|---|---|---|---|
:cli-path |
string | "copilot" |
Path to CLI executable. Falls back to COPILOT_CLI_PATH env var when not set |
:cli-args |
vector | [] |
Extra arguments prepended before SDK-managed flags |
:cli-url |
string | nil | URL of existing CLI server (e.g., "localhost:8080"). When provided, no CLI process is spawned |
:port |
number | 0 |
Server port (0 = random) |
:use-stdio? |
boolean | true |
Use stdio transport instead of TCP |
:log-level |
keyword | :info |
One of :none :error :warning :info :debug :all |
:auto-start? |
boolean | true |
Auto-start server on first operation |
:auto-restart? |
boolean | true |
Auto-restart on crash |
:notification-queue-size |
number | 4096 |
Max queued protocol notifications |
:router-queue-size |
number | 4096 |
Max queued non-session notifications |
:tool-timeout-ms |
number | 120000 |
Timeout for tool handlers returning channels |
:cwd |
string | nil | Working directory for CLI process |
:env |
map | nil | Environment variables |
:github-token |
string | nil | GitHub token for authentication. Sets COPILOT_SDK_AUTH_TOKEN env var and passes --auth-token-env flag |
:use-logged-in-user? |
boolean | true |
Use logged-in user auth. Defaults to false when :github-token is provided. Cannot be used with :cli-url |
:copilot-home |
string | nil | Base directory for Copilot data files. Sets COPILOT_HOME env var on the spawned CLI. (upstream PR #1191) |
:tcp-connection-token |
string | nil | Connection token for the headless CLI server (TCP only). When the SDK spawns its own CLI in TCP mode and this is omitted, a UUID is generated automatically so the loopback listener is safe by default. The token is sent to the CLI via COPILOT_CONNECTION_TOKEN and forwarded over the wire on the new connect handshake. Rejected when combined with :use-stdio? true. (upstream PR #1176) |
:remote? |
boolean | false |
When true, append --remote to the spawned CLI args so the CLI exposes the session over a GitHub-hosted remote endpoint. Ignored when :cli-url is set. (upstream PR #1192) |
:session-idle-timeout-seconds |
integer | 0 (disabled) |
Server-wide session idle timeout in seconds. When > 0, append --session-idle-timeout <n> to the spawned CLI so idle sessions are cleaned up after the given duration. |
:on-list-models |
fn | nil | Zero-arg function returning model info maps. Bypasses models.list RPC; does not require start!. Results are cached the same way as RPC results |
:telemetry |
map | nil | OpenTelemetry export config, applied as environment variables to the spawned CLI (ignored when connecting to an existing server via :cli-url or a parent process via :is-child-process?, since no CLI is spawned). When present, enables OTel. Keys (all optional): :otlp-endpoint (OTLP HTTP endpoint), :otlp-protocol ("http/json" or "http/protobuf" — sets OTEL_EXPORTER_OTLP_PROTOCOL), :file-path (write spans to a file), :exporter-type (exporter selection), :source-name (service/source name), :capture-content? (boolean — capture prompt/response content; off by default for privacy). See Observability. (upstream PR #785, PR #1648) |
:on-get-trace-context |
fn | nil | Zero-arg function returning {:traceparent "..." :tracestate "..."}, called per request (session create/resume and each message send) to propagate a distributed-trace context. Only :traceparent and :tracestate are forwarded. See Observability |
:on-github-telemetry |
fn | nil | @experimental / Internal. One-arg callback receiving each forwarded GitHub telemetry notification. Registering it adds enableGitHubTelemetryForwarding: true to the connect handshake (so the first session's un-replayable session.start telemetry is forwarded — upstream PR #1909) as well as to the wire params of session.create and session.resume; the runtime then emits connection-global gitHubTelemetry.event notifications. A throwing handler is caught and logged (WARN) and cannot corrupt dispatch. Not a stable public SDK surface. See Observability. (PR #1835) |
:is-child-process? |
boolean | false |
When true, connect via own stdio to a parent Copilot CLI process (no process spawning). Requires :use-stdio? true; mutually exclusive with :cli-url |
:session-fs |
map | nil | Session filesystem provider config. Keys: :initial-cwd (string, required), :session-state-path (string, required), :conventions ("windows" or "posix", required). When set, the client calls sessionFs.setProvider on connect and routes filesystem operations through per-session handlers. See Session Filesystem |
:mode |
keyword | :copilot-cli |
Client multitenancy mode: :copilot-cli (default — preserve historical CLI behavior) or :empty (multi-tenant SaaS hosts that must isolate sessions from local machine state). In :empty mode the SDK requires at least one tenant-scoped storage root (:copilot-home, :session-fs, :cli-url, or :is-child-process?), sets COPILOT_DISABLE_KEYTAR=1 on the spawned CLI, spreads 10 safe defaults under caller session config, forces installedPlugins [], and normalizes :system-message to strip environment_context. See Client Mode. (upstream PR #1428) |
(copilot/start! client)Start the CLI server and establish connection. Blocks until connected.
(copilot/with-client [client {:log-level :info}]
;; use client
)Create a client, start it, and ensure stop! runs on exit.
(copilot/stop! client)Stop the server and close all sessions gracefully.
For SDK-spawned processes (not :external-server?), stop! issues a
runtime.shutdown RPC before closing the connection, giving the CLI a chance to
flush state and exit cleanly. The call is bounded by a 10-second timeout; on
timeout or error the SDK falls back to terminating the process (SIGTERM, then
SIGKILL). Connecting to an external server (:cli-url) skips the shutdown RPC and
the process is left running. (upstream PR #1667)
(copilot/force-stop! client)Force stop the CLI server without graceful cleanup. Use when stop! takes too long.
(copilot/client-options client)
;; => {:log-level :info, :use-stdio? true, :auto-start? true, ...}Get the options that were used to create this client.
(copilot/create-session client config)Create a new conversation session.
(copilot/with-session [session client {:model "gpt-5.4"
:on-permission-request copilot/approve-all}]
;; use session
)Create a session and ensure disconnect! runs on exit.
;; Form 1: [session session-opts] - anonymous client with default options
(copilot/with-client-session [session {:model "gpt-5.4"
:on-permission-request copilot/approve-all}]
;; use session
)
;; Form 2: [client-opts session session-opts] - anonymous client with custom options
(copilot/with-client-session [{:log-level :debug} session {:model "gpt-5.4"
:on-permission-request copilot/approve-all}]
;; use session
)
;; Form 3: [client session session-opts] - named client with default options
(copilot/with-client-session [client session {:model "gpt-5.4"
:on-permission-request copilot/approve-all}]
;; use client and session
)
;; Form 4: [client client-opts session session-opts] - named client with custom options
(copilot/with-client-session [client {:log-level :debug} session {:model "gpt-5.4"
:on-permission-request copilot/approve-all}]
;; use client and session
)Create a client and session together, ensuring both are cleaned up on exit.
Config:
| Key | Type | Description |
|---|---|---|
:session-id |
string | Custom session ID (optional) |
:client-name |
string | Client name to identify the application (included in User-Agent header) |
:model |
string | Model to use ("gpt-5.4", "claude-sonnet-4.5", etc.) |
:tools |
vector | Custom tools exposed to the CLI |
:system-message |
map | System message customization (see below) |
:available-tools |
vector | List of allowed tool names |
:excluded-tools |
vector | List of excluded tool names |
:tool-search |
map | Configure runtime tool search for create-session, resume-session, and join-session. Optional keys: :enabled (boolean) and :defer-threshold (integer). This session-level configuration is distinct from a tool definition's :defer policy; omit it to use runtime defaults. |
:provider |
map | Provider config for BYOK (see BYOK docs). Required key: :base-url. Optional: :provider-type (:openai/:azure/:anthropic), :wire-api (:completions/:responses), :api-key, :bearer-token, :azure-options, :headers (map of HTTP header name→value, sent with each provider request — upstream PR #1094), :model-id (string — the model identifier to send to the provider; overrides session :model), :wire-model (string — model name as sent on the provider wire when it differs from :model-id), :max-input-tokens (integer — input/prompt token cap; serialized as wire maxPromptTokens), :max-output-tokens (integer — output token cap), :transport (:http/:websockets — provider transport; serialized as wire transport — upstream PR #1711), :bearer-token-provider (fn — dynamic bearer-token callback, see BYOK docs — upstream PR #1748). The four override fields were added in upstream PR #966 |
:providers |
vector | (Experimental) Multi-provider BYOK registry — a vector of named providers. Each entry takes the connection fields of :provider — :base-url (required), :provider-type, :wire-api, :api-key, :bearer-token, :azure-options, :headers, :bearer-token-provider — plus a required :name (the registry key, no /). Unlike the singular :provider, a named provider does not accept :transport or the inline model-override fields (:model-id, :wire-model, :max-input-tokens, :max-output-tokens); model overrides are declared in :models instead. Pairs with :models to declare a model catalog. Cannot be combined with the singular :provider. (upstream PR #1718) |
:models |
vector | (Experimental) Model catalog referencing the :providers registry. Each entry: :id (required, provider-local model id), :provider (required, a :name in :providers), and optional override fields (:model-id, :wire-model, :capabilities, :max-input-tokens, :max-context-window-tokens, :max-output-tokens). The full model selection id is "providerName/id". Cannot be combined with the singular :provider. (upstream PR #1718) |
:capi |
map | CAPI (Copilot API) session options. {:enable-web-socket-responses boolean} — serialized as wire capi.enableWebSocketResponses. (upstream PR #1711) |
:excluded-builtin-agents |
vector | Names of built-in agents to hide/exclude from the session. Serialized as wire excludedBuiltinAgents. (upstream PR #1865) |
:enable-citations |
boolean | (Experimental) Opt into native model citations. Gated on some? — an explicit false is forwarded; an absent key is omitted. Serialized as wire enableCitations. (upstream PR #1865) |
:session-limits |
map | (Experimental) Session AI-credit limits. {:max-ai-credits <number>} — serialized as wire sessionLimits.maxAiCredits. (upstream PR #1865) |
:enable-managed-settings? |
boolean | Opt-in. When true, the runtime self-fetches enterprise managed settings (bypass-permissions policy) at session bootstrap using the session's :github-token (required; the runtime fails closed if omitted). Gated on some? — an explicit false is forwarded verbatim; an absent key is omitted. Serialized as wire enableManagedSettings. (upstream PR #1925) |
:managed-settings |
map | Structured enterprise managed-settings payload, supplied by the caller instead of (or alongside) :enable-managed-settings?. Optional key :permissions: {:disable-bypass-permissions-mode :disable, :deny [...], :ask [...], :allow [...]} — :disable-bypass-permissions-mode is serialized as wire permissions.disableBypassPermissionsMode (string "disable"); :deny/:ask/:allow are vectors of non-blank permission-rule strings forwarded verbatim. Presence of this key (or :enable-managed-settings? true) sets the permission-handler context's :managed-settings-enabled? to true — see approve-all. Valid on create-session, resume-session, and join-session. (upstream PR #2139) |
:canvas-provider |
map | Canvas provider identity for the session. {:id "..." :name "..."} (:name optional) — serialized as wire canvasProvider.{id,name}. (upstream PR #1847) |
:exp-assignments |
map | (@internal) Exact CopilotExpAssignmentResponse contract using PascalCase string keys. Required: "Features" (string vector), "Flights" (string-to-string map), "Configs" (vector of closed maps containing exactly "Id" (string) and "Parameters" (map of string keys to string, number, boolean, or nil values)), and "AssignmentContext" (string). Optional: "ParameterGroups" (opaque), "FlightingVersion" (number), and "ImpressionId" (string). The Clojure spec rejects unknown top-level and config-entry keys. The map is forwarded unchanged on create-session, resume-session, and join-session (join-session delegates to resume). Serialized as expAssignments. (upstream PR #2033) |
:mcp-servers |
map | MCP server configs keyed by opaque string or keyword server IDs; keyword IDs preserve their full spelling without the leading colon (for example, :srv-1 becomes "srv-1" and :team/srv-1 becomes "team/srv-1"). See MCP docs. Local (stdio) servers: :mcp-command, :mcp-args, :mcp-tools. Remote (HTTP/SSE) servers: :mcp-server-type (:http/:sse), :mcp-url, :mcp-tools. Spec aliases: ::mcp-stdio-server = ::mcp-local-server, ::mcp-http-server = ::mcp-remote-server |
:disabled-mcp-servers |
vector | Names of configured MCP servers (from :mcp-servers or on-disk .mcp.json) to suppress for this session. Vector of non-blank strings. Serialized as wire disabledMcpServers. (upstream PR #2260) |
:github-mcp-tool-config |
map | Configures the built-in GitHub MCP server's tool surface. Optional keys: :enable-all-tools? (boolean, gated on some?), :additional-toolsets (vector of strings), :additional-tools (vector of strings), :enable-insiders-mode? (boolean, gated on some?), :disable-form-deferral? (boolean, gated on some?). Serialized as wire githubMcpToolConfig.{enableAllTools,additionalToolsets,additionalTools,enableInsidersMode,disableFormDeferral}. (upstream PR #2112) |
:commands |
vector | Command definitions (slash commands). See Commands |
:custom-agents |
vector | Custom agent configs. Each agent map: :agent-name (required), :agent-prompt (required), :agent-display-name, :agent-description, :agent-tools, :agent-infer?, :agent-skills (vector of strings), :agent-model (string, e.g. "claude-haiku-4.5"; when set the runtime tries this model for the agent, falling back to the parent session model — upstream PR #1309), :agent-reasoning-effort ("low", "medium", "high", "xhigh", or "max"), :mcp-servers. Nested :mcp-servers follow the same config and opaque server-ID rules as session-level MCP servers. :agent-reasoning-effort is serialized as reasoningEffort on both session.create and session.resume. When omitted, the runtime resolves effort from the selected model's configuration; the parent session's reasoning effort is inherited only when the custom agent uses the same model as the parent. (upstream PR #2064) |
:default-agent |
map | Built-in/default agent config. Use {:excluded-tools [...]} to hide tools from the default agent while leaving them available to custom agents |
:on-permission-request |
fn | Permission handler function. Optional (upstream PR #1308). When omitted, permission requests are not auto-resolved; resolve them manually via handle-pending-permission-request!. Use copilot/approve-all to approve everything. |
:streaming? |
boolean | Enable streaming deltas |
:config-dir |
string | Override config directory for CLI |
:skill-directories |
vector | Additional skill directories to load |
:instruction-directories |
vector | Additional directories to search for custom instruction files. Forwarded as instructionDirectories on session.create and session.resume. (upstream PR #1190) |
:additional-directories |
vector | Extra directories the runtime is allowed to read/write outside the session's working directory. Vector of non-blank strings. Serialized as wire additionalDirectories. Re-supply the vector when resuming a session. (upstream PR #2180) |
:disabled-skills |
vector | Disable specific skills by name |
:large-output |
map | (Experimental) Tool output handling config. CLI protocol feature, not in official SDK. |
:working-directory |
string | Working directory for the session (tool operations relative to this) |
:infinite-sessions |
map | Infinite session config (see below) |
:reasoning-effort |
string | Reasoning effort level: "low", "medium", "high", "xhigh", or "max" (upstream PR #2228) |
:github-token |
string | GitHub token for this session. Sent as gitHubToken on session.create; use this for per-session authentication when one client manages sessions for different GitHub users |
:on-user-input-request |
fn | Handler for ask_user requests (see below) |
:hooks |
map | Lifecycle hooks (see below) |
:agent |
string | Name of a custom agent to activate at session start. Must match a name in :custom-agents. Equivalent to calling agent.select after creation. |
:on-event |
fn | Event handler (1-arg fn receiving event maps). Registered before the RPC call, guaranteeing early events like session.start are not missed. |
:on-elicitation-request |
fn | Handler for elicitation requests from the agent. When provided, advertises requestElicitation=true and handles elicitation.requested broadcast events. Single-arg handler receives an ElicitationContext map with :session-id, :message, :requested-schema, :mode, :elicitation-source, :url. Returns an ElicitationResult map {:action "accept"/"decline"/"cancel" :content {...}}. See Elicitation Provider |
:on-mcp-auth-request |
fn | Handler for interactive MCP OAuth requests. When provided, the SDK registers interest in mcp.oauth_required (on both create and resume) so the runtime delegates browser-based OAuth to this handler instead of silently using a cached token. 2-arg handler (fn [request ctx]) receives an McpAuthRequest map and {:session-id ...}; may return a channel. See MCP OAuth Handler. (upstream PR #1669) |
:on-exit-plan-mode |
fn | Handler for exitPlanMode.request RPCs — invoked when the agent asks to leave plan mode. When provided, advertises requestExitPlanMode=true. Receives the request map; returns the approval result. (upstream PR #1228) |
:on-auto-mode-switch |
fn | Handler for autoModeSwitch.request RPCs — invoked when the agent asks to switch autonomy mode. When provided, advertises requestAutoModeSwitch=true. Receives the request map; returns the approval result. (upstream PR #1228) |
:enable-session-telemetry? |
boolean | Enable/disable the CLI's internal session telemetry (distinct from the client :telemetry OpenTelemetry export). Defaults to enabled for GitHub-authenticated sessions; always disabled when a BYOK :provider is set; defaulted to false in :mode :empty (caller can override). Wire-encoded as enableSessionTelemetry. See Observability. (upstream PR #1224) |
:create-session-fs-handler |
fn | Factory for session filesystem providers. Required when :session-fs is set on the client. Called as (factory session), returns a provider-style map or a low-level handler map. See Session Filesystem |
:enable-config-discovery |
boolean | Auto-discover .mcp.json, .vscode/mcp.json, skills, etc. Instruction files always load regardless. (upstream PR #1044) |
:enable-experimental-mode? |
boolean | Opt into CLI-side experimental features. Gated on some? — an explicit false is forwarded verbatim; an absent key is omitted. Serialized as wire isExperimentalMode. Defaulted to false in :empty mode. (upstream PR #1600) |
:model-capabilities |
map | Model capabilities override. DeepPartial of model capabilities, e.g. {:model-supports {:supports-vision true}}. (upstream PR #1029) |
:include-sub-agent-streaming-events? |
boolean | Forward streaming events from sub-agents to the parent session's event stream. Defaults to true on the wire. (upstream PR #1108) |
:remote-session |
keyword | Per-session Mission Control mode: :off, :export, or :on. When omitted, the CLI applies its default. :off disables remote, :export exports session events to Mission Control without enabling remote steering, :on enables both. Forwarded as remoteSession. (upstream PR #1295, CLI 1.0.48) |
:cloud |
map | (create-session only) Creates a remote cloud session. Shape: {:repository {:owner "octocat" :name "hello-world" :branch "main"}} — :owner and :name are required non-blank strings; :branch is optional. Forwarded as cloud.repository.* on session.create. Not accepted on resume-session (matches upstream ResumeSessionConfig). When :cloud is set and :session-id is omitted, the SDK defers id assignment to the server and registers the session under the server-returned id (upstream PR #1479). (upstream PR #1306) |
:mcp-oauth-token-storage |
keyword | Controls where MCP OAuth tokens are persisted. #{:persistent :in-memory}. Default is server-side (persistent). Set to :in-memory in multi-tenant hosts that must not leak tokens to disk. Wire-encoded as mcpOAuthTokenStorage. (upstream PR #1326) |
:embedding-cache-storage |
keyword | #{:persistent :in-memory}. Controls where the embedding cache lives. Wire-encoded as embeddingCacheStorage. (upstream PR #1474) |
:skip-embedding-retrieval |
boolean | Skip embedding-based context retrieval. (upstream PR #1474) |
:organization-custom-instructions |
string | Organization-wide instructions injected by the host. (upstream PR #1474) |
:enable-on-demand-instruction-discovery |
boolean | Auto-discover instruction files on demand. (upstream PR #1474) |
:enable-file-hooks |
boolean | Enable file-watcher-style lifecycle hooks. (upstream PR #1474) |
:enable-host-git-operations |
boolean | Allow the CLI to run git operations through the host. (upstream PR #1474) |
:enable-session-store |
boolean | Enable the disk-backed session store. (upstream PR #1474) |
:enable-skills |
boolean | Enable skills discovery and loading. (upstream PR #1474) |
:plugin-directories |
vector | Extra plugin directories loaded even when :enable-config-discovery is false. Wire-encoded as pluginDirectories. (upstream PR #1482) |
:reasoning-summary |
string | "none" / "concise" / "detailed". Controls inclusion/granularity of reasoning summaries on assistant turns. Wire-encoded as reasoningSummary. String-valued for consistency with :reasoning-effort. |
:context-tier |
keyword | nil |
#{:default :long-context} selects the long-context model variant; nil explicitly clears any prior tier (wire-encoded as JSON null). Omit the key entirely to leave the current setting untouched. Wire-encoded as contextTier with values "default" / "long_context". |
:skip-custom-instructions |
boolean | Skip loading user-level custom instruction files. Forwarded via session.options.update (NOT session.create). Defaulted to true in :empty mode. (upstream PR #1428) |
:custom-agents-local-only |
boolean | Restrict custom-agent loading to caller-supplied configs only (no on-disk discovery). Forwarded via session.options.update, and — since (upstream PR #1899) — also sent directly (gated on some?) on session.create and session.resume as wire custom-agents-local-only. Defaulted to true in :empty mode. (upstream PR #1428) |
:coauthor-enabled |
boolean | Add a Copilot Co-authored-by trailer to commits made by the CLI. Forwarded via session.options.update. Defaulted to false in :empty mode. (upstream PR #1428) |
:manage-schedule-enabled |
boolean | Enable the built-in schedule-management tools. Forwarded via session.options.update. Defaulted to false in :empty mode. (upstream PR #1428) |
:open-canvases |
vector | (resume-session / join-session only) Seed the open-canvases snapshot when reconnecting. Each entry requires :instance-id, :extension-id, and :canvas-id; optional keys are :extension-name, :title, :status, :url, :input, and :icon (a host-local PNG path). Caller-defined :input keys are preserved verbatim through wire conversion (no kebab→camel re-casing). See open-canvases. (upstream PR #1604) |
:memory |
map | Persistent-memory configuration. Shape: {:enabled boolean}. Sent on both session.create and session.resume; omitted entirely when the key is absent (never wire null). Wire-encoded as memory. In :mode :empty it is defaulted to {:enabled false} (caller can override). (upstream PR #1617) |
(copilot/resume-session client session-id config)Resume an existing session by ID. The config map accepts the same options as create-session (except :session-id), including per-session :github-token, plus:
| Option | Type | Description |
|---|---|---|
:disable-resume? |
boolean | When true, skip emitting the session.resume event (default: false) |
:continue-pending-work? |
boolean | When true, the runtime re-emits any pending permission.requested and external tool calls so handlers can re-respond on resume; default false treats pending work as interrupted. Forwarded as continuePendingWork on session.resume. |
:large-output |
map | (Experimental) Tool output handling config. Now also forwarded on session.resume (matching upstream client.ts:1308). |
When :on-permission-request is set to default-join-session-permission-handler, the SDK sends requestPermission: false on the wire, telling the CLI that this client does not handle permission requests. Any other handler sends requestPermission: true.
;; Resume with a different model and reasoning effort
(copilot/resume-session client "session-123"
{:model "claude-sonnet-4"
:reasoning-effort "high"
:on-permission-request copilot/approve-all})
;; Resume without handling permissions (join-style)
(copilot/resume-session client "session-123"
{:on-permission-request copilot/default-join-session-permission-handler})(copilot/<create-session client config)Async version of create-session. Returns a channel that delivers a CopilotSession.
Validation is synchronous (throws immediately on invalid config). The RPC call parks instead of blocking, making this safe inside go blocks. On RPC error, delivers an ExceptionInfo to the channel instead of a session — check with (instance? Throwable result).
(require '[clojure.core.async :refer [go <!]])
(go
(let [result (<! (copilot/<create-session client {:model "gpt-5.4"
:on-permission-request copilot/approve-all}))]
(if (instance? Throwable result)
(println "Error:" (ex-message result))
(let [answer (<! (copilot/<send! result {:prompt "Hello"}))]
(println answer)))))(copilot/<resume-session client session-id config)Async version of resume-session. Returns a channel that delivers a CopilotSession.
Same config options as resume-session. Safe for use inside go blocks. On RPC error, delivers an ExceptionInfo to the channel — check with (instance? Throwable result).
(go
(let [session (<! (copilot/<resume-session client "session-123"
{:on-permission-request copilot/approve-all}))]
;; use resumed session
))(copilot/join-session config)Join the current foreground session from an extension running as a child process of the Copilot CLI. Reads the SESSION_ID environment variable, creates a child-process client, and resumes the session with :disable-resume? defaulting to true.
Returns a map with :client and :session keys. The caller is responsible for stopping the client when done.
Throws if SESSION_ID is not set in the environment.
In addition to the resume-session config options, join-session accepts:
| Option | Type | Description |
|---|---|---|
:factories |
vector | define-factory handles to register as Agent Factories (Experimental) for this session. Join-only — not accepted by create-session or resume-session. |
(let [{:keys [client session]} (copilot/join-session
{:on-permission-request copilot/approve-all
:tools [my-tool]})]
;; use session...
(copilot/stop! client))Return-shape difference vs the official SDK. The Node.js
joinSession(config)returns theCopilotSessiondirectly and hides the client it creates internally. Clojure has no implicit/global client, sojoin-sessionreturns both the:clientand the:session: the caller owns the client's lifecycle and must callstop!on it when finished. Bind the returned map's:sessionwhere a Node.js caller would use the awaited return value, and keep the:clientfor cleanup. See Naming and shape differences vs the official SDK.
(copilot/ping client)
(copilot/ping client message)Ping the server to check connectivity. Returns {:message "..." :timestamp ... :protocol-version ...}.
(copilot/get-status client)Get CLI status including version and protocol information. Returns {:version "0.0.389" :protocol-version 2}.
(copilot/get-auth-status client)Get current authentication status. Returns:
{:authenticated? true
:auth-type :user ; :user | :env | :gh-cli | :hmac | :api-key | :token
:host "github.com"
:login "username"
:status-message "Authenticated as username"}(copilot/list-models client)List available models with their metadata. Results are cached per client connection.
When :on-list-models handler is provided in client options, calls the handler
instead of the RPC method (no connection required).
Requires authentication (unless :on-list-models is provided). Returns a vector of model info maps:
[{:id "gpt-5.4"
:name "GPT-5.4"
:vendor "openai"
:family "gpt-5.4"
:version "gpt-5.4"
:max-input-tokens 128000
:max-output-tokens 16384
:preview? false
:model-capabilities {:model-supports {:supports-vision true
:supports-reasoning-effort false}
:model-limits {:max-prompt-tokens 128000
:max-context-window-tokens 128000
:vision-capabilities
{:supported-media-types ["image/png" "image/jpeg"]
:max-prompt-images 10
:max-prompt-image-size 20971520}}}
:model-policy {:policy-state "enabled"
:terms "..."}
:model-billing {:multiplier 1.0
:token-prices {:input-price 0.00000125
:output-price 0.00001
:cache-price 0.0000003125
:long-context {:input-price 0.0000025
:output-price 0.00002}}
:promo {:ends-at "2026-08-01T00:00:00Z"
:id "summer-promo"
:discount-percent 25
:message "25% off until August"}}
;; Model picker categorization (CLI 1.0.46+):
:model-picker-category "powerful" ;; "lightweight" | "versatile" | "powerful"
:model-picker-price-category "very_high" ;; "low" | "medium" | "high" | "very_high"
;; For models supporting reasoning:
:supported-reasoning-efforts ["low" "medium" "high" "xhigh"]
:default-reasoning-effort "medium"}
...]The optional :promo billing map contains:
| Key | Type | Required | Description |
|---|---|---|---|
:ends-at |
string | yes | Promotion end time |
:id |
string | no | Promotion identifier |
:discount-percent |
number from 0 through 100 | no | Percentage discount |
:message |
string | no | Display message |
List all models with their billing multiplier:
(require '[github.copilot-sdk :as copilot])
(copilot/with-client [client]
(doseq [m (copilot/list-models client)]
(println (:id m) (str "x" (get-in m [:model-billing :multiplier])))))
;; prints:
;; gpt-5.4 x1.0
;; claude-sonnet-4.5 x1.0
;; o1 x2.0
;; ...(copilot/list-tools client)
(copilot/list-tools client "gpt-5.4")List available tools with their metadata. Pass an optional model string to get model-specific tool overrides.
(copilot/list-tools client)
;; => [{:name "read_file"
;; :namespaced-name "builtin.read_file"
;; :description "Read a file from disk"
;; :parameters {...}
;; :instructions "..."}
;; ...]
;; Print all tool names
(doseq [tool (copilot/list-tools client)]
(println (:name tool) "-" (:description tool)))Each tool info map contains:
| Key | Type | Required | Description |
|---|---|---|---|
:name |
string | yes | Short tool name |
:namespaced-name |
string | no | Fully qualified tool name |
:description |
string | yes | Human-readable description |
:parameters |
map | no | JSON Schema of tool parameters |
:instructions |
string | no | Usage instructions for the tool |
(copilot/get-quota client)Get account quota information. Returns a map of quota type (string) to quota snapshot maps.
(copilot/get-quota client)
;; => {"chat" {:entitlement-requests 1000
;; :used-requests 42
;; :remaining-percentage 95.8
;; :overage 0
;; :overage-allowed-with-exhausted-quota? false
;; :reset-date "2025-02-01T00:00:00Z"}}
(let [quotas (copilot/get-quota client)]
(doseq [[type snapshot] quotas]
(println type ":" (:remaining-percentage snapshot) "% remaining")))Each quota snapshot map contains:
| Key | Type | Description |
|---|---|---|
:entitlement-requests |
number | Total allowed requests |
:used-requests |
number | Requests used so far |
:remaining-percentage |
number | Percentage of quota remaining |
:overage |
number | Number of requests over quota |
:overage-allowed-with-exhausted-quota? |
boolean | Whether overage is allowed when quota is exhausted |
:reset-date |
string (optional) | ISO 8601 date when quota resets |
Experimental: These wrap server-level MCP configuration RPCs and may change.
;; List configured MCP servers
(copilot/mcp-config-list client)
;; => {:servers [...]}
;; Add a new MCP server config
(copilot/mcp-config-add! client {:name "my-server"
:command "npx"
:args ["-y" "@modelcontextprotocol/server-filesystem" "/tmp"]
:tools ["*"]})
;; Update an existing config
(copilot/mcp-config-update! client {:name "my-server" :tools ["read_file"]})
;; Remove a config
(copilot/mcp-config-remove! client {:name "my-server"})(copilot/state client)Get current connection state: :disconnected | :connecting | :connected | :error
(copilot/notifications client)Get a channel that receives non-session notifications. The channel is buffered; notifications are dropped if it fills.
;; Subscribe to all lifecycle events
(def unsub (copilot/on-lifecycle-event client
(fn [event]
(println (:lifecycle-event-type event) (:session-id event)))))
;; Subscribe to a specific event type
(def unsub (copilot/on-lifecycle-event client :session.created
(fn [event]
(println "New session:" (:session-id event)))))
;; Unsubscribe
(unsub)Subscribe to session lifecycle events dispatched by the CLI server. The handler receives an event map with:
| Key | Type | Description |
|---|---|---|
:lifecycle-event-type |
keyword | One of :session.created, :session.deleted, :session.updated, :session.foreground, :session.background |
:session-id |
string | The session ID |
:metadata |
map (optional) | Contains :start-time, :modified-time, and optionally :summary |
Two arities:
(on-lifecycle-event client handler)— wildcard, receives all lifecycle events(on-lifecycle-event client event-type handler)— receives only events matchingevent-type
Returns an unsubscribe function. Call it with no arguments to remove the handler.
Handlers are called synchronously on the notification router's go-loop. Keep handlers fast; offload heavy work to another thread or channel.
(copilot/list-sessions client)
(copilot/list-sessions client {:repository "owner/repo" :branch "main"})List available sessions. Pass an optional filter map to narrow results by context fields.
Filter options:
| Key | Type | Description |
|---|---|---|
:cwd |
string | Filter by working directory |
:git-root |
string | Filter by git repository root |
:repository |
string | Filter by repository (e.g., "owner/repo") |
:branch |
string | Filter by branch name |
Returns a vector of session metadata maps with :start-time and :modified-time as java.time.Instant. Sessions may include a :context map with the session's working directory and repository info.
(copilot/list-sessions client)
;; => [{:session-id "abc-123"
;; :start-time #inst "2025-01-15T10:00:00Z"
;; :modified-time #inst "2025-01-15T10:05:00Z"
;; :summary "Refactoring auth module"
;; :remote? false
;; :context {:cwd "/home/user/project"
;; :git-root "/home/user/project"
;; :repository "owner/repo"
;; :branch "main"}}
;; ...](copilot/get-session-metadata client session-id)Get metadata for a specific session by ID. Returns the session metadata map if found, or nil if the session does not exist. Provides an efficient O(1) lookup instead of calling list-sessions and filtering client-side.
The returned map has the same shape as entries returned by list-sessions:
:session-id— session ID string:start-time—java.time.Instantwhen the session was created:modified-time—java.time.Instantof last modification:remote?— boolean, true if the session is remote:summary— optional summary string:context— optional map with:cwdand optional:git-root,:repository,:branch
(def metadata (copilot/get-session-metadata client "session-abc123"))
;; => {:session-id "session-abc123"
;; :start-time #object[java.time.Instant 0x... "2025-01-15T10:00:00Z"]
;; :modified-time #object[java.time.Instant 0x... "2025-01-15T10:05:00Z"]
;; :remote? false
;; :summary "Refactoring auth module"
;; :context {:cwd "/home/user/project"}}
(copilot/get-session-metadata client "non-existent-id")
;; => nil(copilot/delete-session! client session-id)Delete a session and its data from disk. Unlike disconnect! (which gracefully closes an active session), delete-session! removes persisted session data by ID.
(copilot/get-last-session-id client)Get the ID of the most recently updated session.
(copilot/get-foreground-session-id client)Get the foreground session ID. Returns the session ID or nil. Only applicable in TUI+server mode.
(copilot/set-foreground-session-id! client session-id)Set the foreground session. Requests the TUI to switch to displaying the specified session. Only applicable in TUI+server mode.
Represents a single conversation session.
(copilot/send! session options)Send a message to the session. Returns immediately with the message ID.
Options:
| Key | Type | Description |
|---|---|---|
:prompt |
string | The message/prompt to send |
:attachments |
vector | File attachments (see below) |
:mode |
keyword | :enqueue or :immediate |
:agent-mode |
keyword | #{:interactive :plan :autopilot :shell}. Per-message agent mode. Wire-encoded as agentMode. (upstream PR #1438) |
:display-prompt |
string | Alternate prompt shown in the timeline UI instead of :prompt. Useful when the model-facing prompt contains machinery or context that should not be surfaced to the end user. Wire-encoded as displayPrompt. (upstream PR #1470) |
:request-headers |
map | Extra HTTP headers (string→string) forwarded to the model provider for this request. Merged with provider-level :headers. (upstream PR #1094) |
Attachment types:
| Type | Required Keys | Optional Keys | Description |
|---|---|---|---|
:file |
:type, :path |
:display-name, :line-range |
File attachment |
:directory |
:type, :path |
:display-name, :line-range |
Directory attachment |
:selection |
:type, :file-path, :display-name |
:selection-range, :text |
Code selection attachment |
:github-reference |
:type, :number, :title, :reference-type, :state, :url |
— | GitHub issue, PR, or discussion reference |
:blob |
:type, :data, :mime-type |
:display-name |
Inline base64-encoded data (e.g. images) |
:line-range is a map with :start and :end line numbers (zero-based) to restrict the attachment to a range of lines:
(copilot/send! session
{:prompt "Explain this function"
:attachments [{:type :file
:path "/path/to/file.clj"
:line-range {:start 10 :end 25}}]})Selection range is a map with :start and :end positions, each containing :line and :character:
(copilot/send! session
{:prompt "Explain this code"
:attachments [{:type :selection
:file-path "/path/to/file.clj"
:display-name "my-fn"
:selection-range {:start {:line 10 :character 0}
:end {:line 25 :character 0}}
:text "(defn my-fn [...] ...)"}]})(copilot/send-and-wait! session options)
(copilot/send-and-wait! session options timeout-ms)Send a message and block until the session becomes idle. Returns the final assistant message event.
Default timeout is 300000 ms (5 minutes).
(copilot/send-async session options)Send a message and return a core.async channel that receives all events for this message, closing when idle.
Safe for use inside go blocks — no blocking operations.
Supports :timeout-ms in options (default: 300000) to force cleanup on long-running requests.
(copilot/send-async-with-id session options)Send a message and return {:message-id :events-ch} for correlating responses.
Supports :timeout-ms in options (default: 300000).
(copilot/<send! session options)Async equivalent of send-and-wait! for use inside go blocks. Returns a channel that yields the final content string.
Supports :timeout-ms in options (default: 300000).
Combined with <create-session, enables fully non-blocking pipelines:
(go
(let [session (<! (copilot/<create-session client {:model "gpt-5.4"
:on-permission-request copilot/approve-all}))
answer (<! (copilot/<send! session {:prompt "Explain monads"}))]
(println answer)))(copilot/<send-and-wait! session options)Async equivalent of send-and-wait! for use inside go blocks. Returns a channel that yields the final assistant message event — the same shape as send-and-wait!'s successful return value (content lives under [:data :content]), or closes with nothing if no assistant message was received.
Supports :timeout-ms in options (default: 300000, set to nil to disable).
Error semantics differ from send-and-wait!: where send-and-wait! throws on :copilot/session.error or timeout, this variant never surfaces those — the channel closes (delivering the last assistant message if one arrived, otherwise nothing), consistent with <send!.
(go
(let [session (<! (copilot/<create-session client {:on-permission-request copilot/approve-all}))
event (<! (copilot/<send-and-wait! session {:prompt "Explain monads"}))]
(println (get-in event [:data :content]))))Use <send! when you only need the content string; use <send-and-wait! when you need the full event (metadata, id, etc.).
(copilot/events session)Get the core.async mult for session events. Use tap to subscribe:
(let [ch (chan 100)]
(tap (copilot/events session) ch)
(go-loop []
(when-let [event (<! ch)]
(println event)
(recur))))(copilot/events->chan session {:buffer 256
:xf (filter #(= :copilot/assistant.message (:type %)))})Subscribe to session events with optional buffer size and transducer.
(copilot/subscribe-events session)Subscribe to session events. Returns a channel (sliding buffer, size 1024) that receives events.
This is a convenience wrapper around (tap (copilot/events session) ch).
Session events are delivered via core.async mult to a per-subscriber sliding-buffer
channel. Because a sliding buffer never blocks on put!, mult is never stalled by a slow
subscriber. If a subscriber falls behind and its buffer fills, the oldest buffered events
are dropped for that subscriber only to make room for new ones.
Key points:
- Per-subscriber: Each subscriber is independent. A slow subscriber drops its own oldest events without affecting delivery to other subscribers.
- Oldest-first: When the buffer is full, the oldest buffered events are dropped, not the newest — subscribers always see the most recent events.
- Silent: No error, warning, or indication that a drop occurred.
- Not recoverable: Dropped events are gone for that subscriber.
With the default 1024 buffer, drops are unlikely unless a subscriber completely stops reading. For most use cases, this is not a concern.
(copilot/unsubscribe-events! session ch)Unsubscribe a channel from session events.
(copilot/abort! session)Abort the currently processing message.
(copilot/get-messages session)Get all events/messages from this session.
(copilot/handle-pending-tool-call! session
{:request-id "tool-req-7"
:result "STATUS_OK"})
;; or async:
(copilot/<handle-pending-tool-call! session {:request-id "tool-req-7"
:error "lookup failed"})Resolve a tool call that was not auto-handled (because :handler was omitted
from define-tool). The args map accepts :request-id plus either :result
(string or full result map) or :error (string). Sent on the wire as
session.tools.handlePendingToolCall. (upstream PR #1308)
(copilot/handle-pending-permission-request! session
{:request-id "perm-req-3"
:result {:kind :approve-once}})Resolve a permission request that was not auto-handled (because
:on-permission-request was omitted from the session config). The result map
must contain a :kind other than :no-result. Sent on the wire as
session.permissions.handlePendingPermissionRequest. (upstream PR #1308)
(copilot/get-current-model session)
;; => "gpt-5.4"Get the current model for this session. Returns the model ID string, or nil if none set.
(copilot/switch-model! session "claude-sonnet-4.5")
;; => "claude-sonnet-4.5"
;; With model capabilities override (upstream PR #1029):
(copilot/switch-model! session "gpt-5.4"
{:model-capabilities {:model-supports {:supports-vision true}}})Switch the model for this session mid-conversation. Returns the new model ID string, or nil.
Optional opts map:
:reasoning-effort— Reasoning effort level ("low", "medium", "high", "xhigh", or "max"):reasoning-summary— Reasoning summary mode ("none", "concise", "detailed"). Wire-encoded asreasoningSummary.:context-tier— Context window tier for models that support it::defaultor:long-context(upstream PR #1522). Wire-encoded ascontextTierwith values"default"/"long_context".:model-capabilities— Model capabilities override map, e.g.{:model-supports {:supports-vision true}}
(copilot/set-model! session "claude-sonnet-4.5")
;; => "claude-sonnet-4.5"Alias for switch-model!, matching the upstream SDK's setModel() API.
(copilot/with-client-session [session {:model "gpt-5.4"
:on-permission-request copilot/approve-all}]
(println "Before:" (copilot/get-current-model session))
(copilot/set-model! session "claude-sonnet-4.5")
(println "After:" (copilot/get-current-model session)))
;; prints:
;; Before: gpt-5.4
;; After: claude-sonnet-4.5(copilot/log! session "Processing started")
(copilot/log! session "Something went wrong" {:level "error"})
(copilot/log! session "Temporary note" {:ephemeral? true})Log a message to the session timeline. Returns the event ID string.
Options (optional map):
| Key | Type | Default | Description |
|---|---|---|---|
:level |
string | "info" |
Log severity: "info", "warning", or "error" |
:ephemeral? |
boolean | false |
When true, the message is transient and not persisted to disk |
(copilot/disconnect! session)Disconnect the session and free resources. This is the preferred way to close a session.
(copilot/destroy! session)Deprecated. Use disconnect! instead. destroy! delegates to disconnect! and will be removed in a future release.
(copilot/session-id session)Get the session's unique identifier.
(copilot/workspace-path session)Get the session workspace path when provided by the CLI (may be nil).
(copilot/session-config session)
;; => {:model "gpt-5.4", :streaming? true, :reasoning-effort "high", ...}Get the configuration that was used to create this session.
(copilot/client session)Get the client that owns this session.
Note: These are experimental APIs wrapping emerging CLI RPC methods. They may change in future releases.
(require '[github.copilot-sdk.session :as session])
;; List available skills
(session/skills-list my-session)
;; => {:skills [{:name "update-docs" :source-location "project" ...} ...]}
;; Enable/disable MCP servers
(session/mcp-enable! my-session "my-server")
(session/mcp-disable! my-session "my-server")
;; Get/set agent mode
(session/mode-get my-session)
;; => {:mode "interactive"}
(session/mode-set! my-session "plan")
;; Read/update session plan
(session/plan-read my-session)
;; => {:exists? true :content "# Plan\n..." :file-path "/path/to/plan.md"}
(session/plan-update! my-session "# Updated Plan\n...")
(session/plan-delete! my-session)
;; Workspace file operations
(session/workspace-list-files my-session)
;; => {:files ["notes.md" "data.json"]}
(session/workspace-read-file my-session "notes.md")
;; => {:content "..."}
(session/workspace-create-file! my-session "output.txt" "result data")
;; Custom agent management
(session/agent-list my-session)
;; => {:agents [{:name "researcher" ...} ...]}
(session/agent-select! my-session "researcher")
(session/agent-get-current my-session)
;; => {:name "researcher"}
(session/agent-deselect! my-session)Skills
| Function | Description |
|---|---|
session/skills-list |
List available skills. Returns map with :skills. |
session/skills-enable! |
Enable a skill by name. |
session/skills-disable! |
Disable a skill by name. |
session/skills-reload! |
Reload all skills. |
Queued commands
The CLI emits :copilot/command.queued events when a slash-command is
dispatched for client-side execution. Each event carries a :request-id
and :command. Clients respond via respond-to-queued-command!:
;; Inside an event handler that observes :copilot/command.queued
(session/respond-to-queued-command! session
{:request-id (:request-id event-data)
:handled? true
:stop-processing-queue? false})
;; Or, to let the CLI fall back to default handling:
(session/respond-to-queued-command! session
{:request-id (:request-id event-data)
:handled? false})| Function | Description |
|---|---|
session/respond-to-queued-command! |
Acknowledge a command.queued event (experimental). |
MCP Servers
| Function | Description |
|---|---|
session/mcp-list |
List configured MCP servers. |
session/mcp-enable! |
Enable an MCP server by name. |
session/mcp-disable! |
Disable an MCP server by name. |
session/mcp-reload! |
Reload all MCP servers. |
Extensions
| Function | Description |
|---|---|
session/extensions-list |
List extensions. |
session/extensions-enable! |
Enable an extension by ID. |
session/extensions-disable! |
Disable an extension by ID. |
session/extensions-reload! |
Reload all extensions. |
Mode
| Function | Description |
|---|---|
session/mode-get |
Get current agent mode. Returns {:mode "interactive"|"plan"|"autopilot"}. |
session/mode-set! |
Set agent mode. Accepts "interactive", "plan", or "autopilot". |
Plan
| Function | Description |
|---|---|
session/plan-read |
Read the session plan file. Returns {:exists? :content :file-path}. |
session/plan-update! |
Update the plan file content. |
session/plan-delete! |
Delete the plan file. |
Workspace
| Function | Description |
|---|---|
session/workspace-list-files |
List files in the session workspace. Returns {:files [...]}. |
session/workspace-read-file |
Read a workspace file by relative path. Returns {:content "..."}. |
session/workspace-create-file! |
Create a file in the workspace with given path and content. |
Agents
| Function | Description |
|---|---|
session/agent-list |
List available custom agents. Returns {:agents [...]}. |
session/agent-get-current |
Get the currently selected agent. Returns {:name "..."} or {:name nil}. |
session/agent-select! |
Select a custom agent by name. |
session/agent-deselect! |
Deselect the current custom agent. |
session/agent-reload! |
Reload all custom agents. |
Fleet
| Function | Description |
|---|---|
session/fleet-start! |
Start parallel sub-sessions. Accepts a params map. |
Other
| Function | Description |
|---|---|
session/plugins-list |
List plugins. |
session/compaction-compact! |
Trigger manual context compaction (uses session.history.compact RPC). |
session/history-truncate! |
Trigger manual context truncation. |
session/history-clear-context! |
Clear conversation context and start a new one with a given prompt. Also available as the top-level copilot/history-clear-context! facade wrapper. |
session/sessions-fork! |
Fork the current session. |
session/shell-exec! |
Execute a shell command. |
session/shell-kill! |
Kill a running shell process. |
;; session/history-clear-context! or copilot/history-clear-context!
(session/history-clear-context! my-session "Let's start fresh: focus on the auth module.")
;; => {:messages-cleared 42}Session Name
| Function | Description |
|---|---|
session/session-name-get |
Get the session name (or auto-generated summary). Returns {:name "..."}. |
session/session-name-set! |
Set the session name (1–100 characters). |
(session/session-name-get my-session)
;; => {:name "My debugging session"}
(session/session-name-set! my-session "Refactoring auth module")Workspace (Extended)
| Function | Description |
|---|---|
session/workspace-get-workspace |
Get current workspace metadata. Returns {:workspace {...}}. |
(session/workspace-get-workspace my-session)
;; => {:workspace {:path "/home/user/project" ...}}MCP Discovery
| Function | Description |
|---|---|
session/mcp-discover |
Discover MCP servers in a directory. Accepts optional opts map with :working-directory. |
(session/mcp-discover my-session)
(session/mcp-discover my-session {:working-directory "/path/to/project"})Usage Metrics
| Function | Description |
|---|---|
session/usage-get-metrics |
Get usage metrics for the session. |
(session/usage-get-metrics my-session)Remote Sessions (experimental, upstream PR #1192)
| Function | Description |
|---|---|
session/remote-enable |
Enable remote steerability for the session. Returns {:url <string?> :remote-steerable <boolean>}. Optional 2-arity opts map accepts :mode set to :off, :export, or :on (upstream CLI 1.0.48-1). |
session/remote-disable |
Disable remote steerability for the session. Returns nil. |
(session/remote-enable my-session)
;; => {:url "https://copilot-remote.test/abc" :remote-steerable true}
;; Optional per-session mode (upstream CLI 1.0.48-1):
;; - :off — disable remote
;; - :export — export session events to Mission Control without remote steering
;; - :on — export + enable remote steering
(session/remote-enable my-session {:mode :export})
;; => {:remote-steerable false}
(session/remote-disable my-session)
;; => nilRequest structured user input via interactive dialogs. Check host support before calling.
(require '[github.copilot-sdk :as copilot])(copilot/capabilities session)
;; => {:ui {:elicitation true}}Get the host capabilities map reported when the session was created or resumed.
(copilot/open-canvases session)
;; => [{:instance-id "i1" :canvas-id "diff" :extension-id "ext.x"
;; :icon "/tmp/diff.png"}]Get the current open-canvases snapshot for session. Returns a vector of
canvas-instance maps. The snapshot is initialized from session.resume and
updated by :copilot/session.canvas.opened / :copilot/session.canvas.closed
events. session.create does NOT populate it (matches upstream Node.js).
Each entry has required keys :instance-id, :extension-id, :canvas-id,
and optional :extension-name, :title, :status, :url, :input, and
:icon (a host-local PNG path). Closing an instance that's not in the snapshot
is a silent no-op (idempotent); malformed payloads (missing required field or
wrong type) log a warning and leave the snapshot unchanged.
The :input map (caller-defined opaque data on each canvas) is preserved
verbatim through wire conversion. Keys you receive (e.g. via the canvas
opened event or after a resume) round-trip back to the CLI without
camelCasing — including snake_case and nested keys.
To restore canvases after reconnecting, pass :open-canvases to
resume-session or join-session. The
shape mirrors what (open-canvases session) returned previously:
(let [snap (copilot/open-canvases old-session)]
(copilot/resume-session client session-id
{:on-permission-request copilot/approve-all
:open-canvases snap}))The SDK preserves caller-defined :input keys verbatim on the wire (they are
sent as JSON object fields with the original key names, unchanged by Clojure's
kebab-case conversion).
(copilot/elicitation-supported? session)
;; => trueReturn true if the CLI host supports interactive elicitation dialogs.
(copilot/confirm! session message)Show a confirmation dialog. Returns true if the user confirms, false if they decline or cancel. Throws if elicitation is not supported.
(when (copilot/elicitation-supported? session)
(when (copilot/confirm! session "Deploy to production?")
(println "Deploying...")))(copilot/select! session message options)Show a selection dialog with the given options. Returns the selected value as a string, or nil if the user declines or cancels. Throws if elicitation is not supported.
(when-let [env (copilot/select! session "Choose environment" ["staging" "production"])]
(println "Selected:" env))(copilot/input! session message)
(copilot/input! session message opts)Show a text input dialog. Returns the entered text as a string, or nil if the user declines or cancels. Throws if elicitation is not supported.
Options:
| Key | Type | Description |
|---|---|---|
:title |
string | Title for the input field |
:description |
string | Description text |
:min-length |
integer | Minimum input length |
:max-length |
integer | Maximum input length |
:format |
string | Input format ("email", "uri", "date", "date-time") |
:default |
string | Default value |
(when-let [name (copilot/input! session "Enter your name"
{:min-length 1
:max-length 100})]
(println "Hello," name))(copilot/ui-elicitation! session params)Raw elicitation request for custom JSON schemas. params is a map with :message and :requested-schema keys. Returns a map with :action ("accept", "decline", or "cancel") and :content. Throws if elicitation is not supported.
(copilot/ui-elicitation! session
{:message "Configure deployment"
:requested-schema {:type "object"
:properties {"env" {:type "string" :enum ["staging" "production"]}
"replicas" {:type "number" :default 3}}
:required ["env"]}})
;; => {:action "accept", :content {:env "staging", :replicas 3}}Some MCP servers require interactive (browser-based) OAuth. Register an
:on-mcp-auth-request handler to take over that flow; without it, the runtime
falls back to a browserless cached-token path and never prompts. (upstream PR #1669)
(require '[github.copilot-sdk :as copilot])
(def session
(copilot/create-session
client
{:on-permission-request copilot/approve-all
:on-mcp-auth-request
(fn [request ctx]
;; request is the McpAuthRequest; ctx is {:session-id "..."}
(let [token (acquire-oauth-token! (:server-url request))]
(if token
{:access-token token :token-type "Bearer" :expires-in 3600}
{:kind :cancelled})))}))When the handler is provided, the SDK registers interest in the
mcp.oauth_required event (before the session.create/session.resume runtime
work begins) so the runtime delegates the OAuth request to your handler. The
handler is invoked with two arguments:
- An
McpAuthRequestmap (the event data). - A context map
{:session-id "..."}.
The handler may return the result directly or a core.async channel yielding it.
McpAuthRequest fields:
| Key | Type | Description |
|---|---|---|
:request-id |
string | Opaque id correlating the request with its response. |
:server-name |
string | Configured name of the MCP server. |
:server-url |
string | URL of the MCP server requiring auth. |
:reason |
string | Why authentication is needed. |
:www-authenticate-params |
map | (optional) Parsed WWW-Authenticate challenge params. |
:resource-metadata |
string | (optional) Raw OAuth protected-resource metadata document. |
:static-client-config |
map | (optional) Pre-registered OAuth client config. |
Result mapping: Return a map with :access-token (plus optional
:token-type and :expires-in) to answer with a token. Return nil,
{:kind :cancelled}, or throw to cancel the request — a thrown handler never
wedges the pending request (errors are swallowed and treated as a cancel,
matching upstream).
Sessions emit various events during processing. All event types are namespaced keywords prefixed with copilot/.
;; All event types
copilot/event-types
;; => #{:copilot/session.idle :copilot/assistant.message ...}
;; Session lifecycle events
copilot/session-events
;; => #{:copilot/session.start :copilot/session.idle ...}
;; Assistant response events
copilot/assistant-events
;; => #{:copilot/assistant.message :copilot/assistant.message_delta ...}
;; Tool execution events
copilot/tool-events
;; => #{:copilot/tool.execution_start :copilot/tool.execution_complete ...}
;; Interaction flow events (permission, user input, elicitation)
copilot/interaction-events
;; => #{:copilot/permission.requested :copilot/permission.completed
;; :copilot/user_input.requested :copilot/user_input.completed
;; :copilot/elicitation.requested :copilot/elicitation.completed
;; :copilot/external_tool.requested :copilot/external_tool.completed
;; :copilot/mcp.oauth_required :copilot/mcp.oauth_completed
;; :copilot/command.queued :copilot/command.execute
;; :copilot/command.completed :copilot/commands.changed
;; :copilot/exit_plan_mode.requested :copilot/exit_plan_mode.completed}For schema 1.0.73, :copilot/assistant.server_tool_progress also belongs to
copilot/assistant-events. :copilot/session.managed_settings_enforced and
:copilot/session.managed_settings_resolved belong to copilot/session-events.
:copilot/tool_search.activated intentionally belongs only to the master
copilot/event-types set, not copilot/interaction-events or copilot/tool-events.
The generated wire schemas also contain the internal assistant.turn_retry
(additional model inference metadata within an existing turn) and
model.call_start (model API dispatch metadata) events. They are wire-only and
intentionally excluded from every curated public event set.
(copilot/evt :session.info) ;; => :copilot/session.info
(copilot/evt :assistant.message) ;; => :copilot/assistant.messageConvert an unqualified event keyword to a namespace-qualified :copilot/ keyword. Throws IllegalArgumentException if the keyword is not a valid event type.
| Event Type | Description |
|---|---|
:copilot/session.start |
Session created |
:copilot/session.resume |
Session resumed |
:copilot/session.error |
Session error occurred; data: {:error-type "..." :message "..." :stack "..." :status-code 429 :provider-call-id "..." :url "..."} (:stack, :status-code, :provider-call-id, :url optional) |
:copilot/session.idle |
Session finished processing |
:copilot/session.info |
Informational session update |
:copilot/session.model_change |
Session model changed |
:copilot/session.handoff |
Session handed off to another agent; data: {:remote-session-id "..." :host "https://github.com"} (both optional) |
:copilot/session.usage_info |
Token usage information |
:copilot/session.context_changed |
Session context (cwd, repo, branch) changed |
:copilot/session.title_changed |
Session title updated |
:copilot/session.warning |
Session warning (e.g., quota limits) |
:copilot/session.shutdown |
Session is shutting down |
:copilot/session.truncation |
Context window truncated |
:copilot/session.snapshot_rewind |
Session state rolled back |
:copilot/session.context_cleared |
Conversation context cleared and restarted with a new prompt (via history-clear-context!); data: {:messages-cleared N} (required) with optional :initial-message (the prompt used to start the new context) (upstream PR #2129) |
:copilot/session.compaction_start |
Context compaction started (infinite sessions); data: {:model "..." :current-tokens N :token-limit N :trigger "..."} (all optional). :trigger is one of "threshold", "context_limit_retry", "manual", "memory_pressure", "model_switch" (upstream schema 1.0.79-5/6) |
:copilot/session.compaction_complete |
Context compaction completed (infinite sessions); data: {:success bool} (required) with optional :error "...", :status-code N, :token-limit N, :trigger "..." (same :trigger enum as compaction_start) (upstream schema 1.0.79-5/6) |
:copilot/session.mode_changed |
Session agent mode changed; data: {:previous-mode "...", :new-mode "..."} |
:copilot/session.plan_changed |
Session plan created/updated/deleted; data: {:operation "create"/"update"/"delete"} |
:copilot/session.workspace_file_changed |
Workspace file created or updated; data: {:path "...", :operation "create"/"update"} |
:copilot/session.task_complete |
Task completed by the session agent; data (all optional): :summary "...", :aborted? false, :outcome ("completed", "continue", or "blocked"), :objective-id, :reason, :success (upstream schema 1.0.79-5/6) |
:copilot/session.todos_changed |
Signal-only: the agent's todos / todo-deps table was written. No payload. Events arrive in order; debounce on arrival if needed (upstream schema 1.0.63) |
:copilot/session.schedule_created |
Scheduled prompt registered via /every; data: {:id <pos-int> :interval-ms <pos-int> :prompt "..."} (upstream schema 1.0.42) |
:copilot/session.schedule_cancelled |
Scheduled prompt cancelled from the schedule manager dialog; data: {:id <pos-int>} (upstream schema 1.0.42) |
:copilot/session.autopilot_objective_changed |
Autopilot objective lifecycle events; data: {:operation #{"create" "update" "delete"}} (required) with optional :id (integer) and :status (upstream schema 1.0.56). The :status enum is widened to include "active", "paused", "cap_reached", "completed". |
:copilot/session.permissions_changed |
Per-session permission flags changed; data: {:allow-all-permissions boolean :previous-allow-all-permissions boolean} with optional :allow-all-permission-mode / :previous-allow-all-permission-mode (tri-state #{"off" "auto" "on"}, experimental, upstream schema 1.0.70) (upstream schema 1.0.56). |
:copilot/session.session_limits_changed |
Session limits changed; data: {:session-limits {:max-ai-credits <number>}}, where a nil :session-limits clears the active limits (upstream schema 1.0.67) |
:copilot/session.usage_checkpoint |
Durable usage checkpoint for reconstructing aggregate accounting on resume; data: {:total-nano-aiu <number>} with optional :total-premium-requests <number> (upstream schema 1.0.67) |
:copilot/session.auto_mode_resolved |
Auto model-selection resolved the model for the first prompt of an auto-mode session; data includes :chosen-model, optional :candidate-models, :category-scores, :confidence, :predicted-label, :reasoning-bucket (experimental; upstream schema 1.0.70-0) |
:copilot/session.managed_settings_enforced |
Experimental ephemeral enforcement of enterprise managed settings for a concrete user- or host-initiated governed action. Data: {:action "bypass_permissions_blocked" :setting <string> :fail-closed <boolean> :message <string>} with optional :escalation in #{"allow_all" "approve_all" "auto_approval" "unrestricted_paths" "unrestricted_urls"}. |
:copilot/session.managed_settings_resolved |
Experimental ephemeral snapshot of effective enterprise managed settings and their authority, emitted when policy is applied or reapplied at session start, on resume, or on account switch. Data: {:source #{"server" "device" "none"} :server-managed <boolean> :device-managed <boolean> :fail-closed <boolean> :bypass-permissions-disabled <boolean> :managed-keys [<string> ...]} with optional opaque JSON :settings. |
:copilot/session.schedule_rearmed |
Self-paced schedule re-armed for its next run |
:copilot/session.binary_asset |
Canonical bytes for a content-addressed binary asset shared by reference across events |
:copilot/session.extensions.attachments_pushed |
Extension pushed attachments into the session |
:copilot/skill.invoked |
Skill invocation triggered; data includes :name, :path, :content, optional :description, :plugin-name, :plugin-version |
:copilot/user.message |
User message added |
:copilot/pending_messages.modified |
Pending message queue updated |
:copilot/assistant.turn_start |
Assistant turn started |
:copilot/assistant.intent |
Assistant intent update |
:copilot/assistant.reasoning |
Model reasoning (if supported); optional data: :rte (opaque round-trip encrypted reasoning token, for providers that require it to be replayed back) (upstream schema 1.0.79-5/6) |
:copilot/assistant.reasoning_delta |
Streaming reasoning chunk |
:copilot/assistant.message_start |
Streaming assistant message start metadata |
:copilot/assistant.message |
Complete assistant response; optional data: :chunk-index, :chunk-count (position/count when the response was split across multiple messages), :rte (upstream schema 1.0.79-5/6) |
:copilot/assistant.message_delta |
Streaming response chunk |
:copilot/assistant.streaming_delta |
Response size update during streaming; data: {:total-response-size-bytes N} |
:copilot/assistant.turn_end |
Assistant turn completed |
:copilot/assistant.usage |
Token usage and cost for an individual API call. Required: :model (string). Optional: :input-tokens, :output-tokens, :reasoning-tokens, :cache-read-tokens, :cache-write-tokens, :cache-expires-at (java.time.Instant — when the prompt cache expires), :service-request-id (string — x-copilot-service-request-id for CAPI log correlation), :api-endpoint, :api-call-id, :provider-call-id, :content-filter-triggered (boolean), :finish-reason (string), :cost, :duration, :time-to-first-token-ms, :ttft-ms, :inter-token-latency-ms, :reasoning-effort, :initiator, :parent-tool-call-id (deprecated), :copilot-usage, :quota-snapshots, :interaction-type, :rte (upstream PR #2074; :interaction-type/:rte added in upstream schema 1.0.79-5/6) |
:copilot/assistant.idle |
Main agent's processing loop went idle, including while related background work (running sub-agents or in-flight attached shell commands) is still pending (upstream schema 1.0.66) |
:copilot/assistant.tool_call_delta |
Streaming tool-call argument input chunk; data includes :tool-call-id, :input-delta, optional :tool-name, :tool-type (upstream schema 1.0.69-3) |
:copilot/assistant.server_tool_progress |
Ephemeral live progress for a provider-hosted server tool before the finalized serverTools envelope arrives on the terminal assistant.message. Data: {:output-index <integer> :kind <string> :status <string>}; only "web_search" is currently emitted for :kind, and :status is "in_progress", "searching", or "completed". |
:copilot/model.call_failure |
Failed LLM API call metadata for telemetry |
:copilot/abort |
Current message aborted |
:copilot/tool.user_requested |
Tool execution requested by user |
:copilot/tool.execution_start |
Tool execution started; data includes :tool-call-id, :tool-name, optional :arguments, :parent-tool-call-id, :mcp-server-name, :mcp-tool-name, :model |
:copilot/tool.execution_progress |
Tool execution progress update |
:copilot/tool.execution_partial_result |
Tool execution partial result |
:copilot/tool.execution_complete |
Tool execution completed; data may include optional :structured-content (arbitrary structured tool result) (upstream schema 1.0.63) |
:copilot/tool_search.activated |
Persisted generic client-side tool activations restored when a session resumes. Data: {:strategy <string> :tool-names [<string> ...]}. |
:copilot/subagent.started |
Subagent started; data includes :tool-call-id, :agent-name, :agent-display-name, :agent-description, and optional :model (upstream PR #2072) |
:copilot/subagent.completed |
Subagent completed; data includes :tool-call-id, :agent-name, :agent-display-name, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
:copilot/subagent.failed |
Subagent failed; data includes :tool-call-id, :agent-name, :agent-display-name, :error, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
:copilot/subagent.selected |
Subagent selected |
:copilot/subagent.deselected |
Subagent deselected |
:copilot/hook.start |
Hook invocation started |
:copilot/hook.progress |
Ephemeral progress update from a long-running hook; data: {:message "..."} (upstream schema 1.0.56). |
:copilot/hook.end |
Hook invocation finished |
:copilot/system.message |
System message emitted |
:copilot/system.notification |
System notification with structured :kind discriminator (e.g. agent_completed, shell_completed, shell_detached_completed) |
:copilot/permission.requested |
Permission request initiated; data includes :resolved-by-hook when already handled by a hook |
:copilot/permission.completed |
Permission request resolved |
:copilot/user_input.requested |
User input requested from agent |
:copilot/user_input.completed |
User input received |
:copilot/elicitation.requested |
Elicitation request initiated |
:copilot/elicitation.completed |
Elicitation request resolved |
:copilot/external_tool.requested |
External tool call requested (v3) |
:copilot/external_tool.completed |
External tool call completed (v3) |
:copilot/mcp.oauth_required |
MCP server requires OAuth authentication |
:copilot/mcp.oauth_completed |
MCP OAuth authentication completed |
:copilot/mcp.headers_refresh_required |
Dynamic headers refresh request for a remote MCP server (upstream schema 1.0.66) |
:copilot/mcp.headers_refresh_completed |
MCP headers refresh request completed (upstream schema 1.0.66) |
:copilot/mcp.tools.list_changed |
Remote MCP server signalled its tool list changed; data includes :server-name (upstream schema 1.0.70) |
:copilot/mcp.resources.list_changed |
Remote MCP server signalled its resource list changed; data includes :server-name (upstream schema 1.0.70) |
:copilot/mcp.prompts.list_changed |
Remote MCP server signalled its prompt list changed; data includes :server-name (upstream schema 1.0.70) |
:copilot/command.queued |
Command queued for execution |
:copilot/command.execute |
Command execution started |
:copilot/command.completed |
Command execution completed |
:copilot/commands.changed |
Available commands list changed |
:copilot/exit_plan_mode.requested |
Exit from plan mode requested |
:copilot/exit_plan_mode.completed |
Exit from plan mode completed |
:copilot/auto_mode_switch.requested |
Auto mode switch request requiring user approval |
:copilot/auto_mode_switch.completed |
Auto mode switch completed |
:copilot/session.tools_updated |
Session tools list updated (e.g., after model change) |
:copilot/session.background_tasks_changed |
Background tasks status changed |
:copilot/session.skills_loaded |
Skills loaded for the session |
:copilot/session.mcp_servers_loaded |
MCP servers loaded for the session |
:copilot/session.mcp_server_status_changed |
MCP server status changed |
:copilot/session.extensions_loaded |
Extensions loaded for the session |
:copilot/session.custom_agents_updated |
Custom agents list updated |
:copilot/session.custom_notification |
Custom Skill notification (Notify block); ephemeral. Data: {:source "<ext-id>" :name "<event>" :payload <any> :subject {<k> <v>} :version <pos-int>} (:subject and :version are optional; :subject keys are preserved verbatim — see PR #1292, CLI 1.0.48) |
:copilot/sampling.requested |
MCP sampling request initiated; ephemeral |
:copilot/sampling.completed |
MCP sampling request completed; ephemeral |
:copilot/session_limits_exhausted.requested |
Session AI-credit limit reached; the runtime requests a limit decision (add/set/unset/cancel); ephemeral observable event (upstream schema 1.0.67) |
:copilot/session_limits_exhausted.completed |
Session-limit decision completed; ephemeral observable event (upstream schema 1.0.67) |
:copilot/session.remote_steerable_changed |
Session remote steering capability changed; data: {:remote-steerable true/false} |
:copilot/capabilities.changed |
Session capabilities dynamically changed (e.g., elicitation support); ephemeral. Data: {:ui {:elicitation true/false}} |
:copilot/mcp_app.tool_call_complete |
An MCP App tool call completed (upstream schema 1.0.52-4, SEP-1865); ephemeral. Data: {:server-name ... :tool-name ... :duration-ms ... :success bool :arguments {...} :result {...}} — :arguments and :result are opaque source-defined maps whose keys are preserved verbatim (not kebab-cased). |
:copilot/session.canvas.opened |
A canvas (auxiliary UI surface) was opened in the session; ephemeral. Data: `{:instance-id ... :canvas-id ... :extension-id ... :reopen bool :availability "ready" |
:copilot/session.canvas.closed |
A canvas was closed; ephemeral. Data: {:instance-id ... :canvas-id ... :extension-id ...}. The SDK removes the matching entry from the open-canvases snapshot before publishing. (upstream PR #1604) |
:copilot/session.canvas.registry_changed |
The set of canvases the host can offer changed; ephemeral. |
:copilot/session.canvas.unavailable |
An open canvas instance's provider dropped (e.g. the extension is reloading mid-session); ephemeral. The host should keep the panel mounted and surface a reconnecting state. (upstream schema 1.0.66) |
:copilot/session.canvas.recorded |
Durable record that a canvas instance is open, used to restore open canvases on cold session resume. Omits the transient :url and :availability. |
:copilot/factory.run_updated |
An Agent Factory run's status changed; data: {:run-id "..." :revision N} (both required). Consumed internally by wait-for-run!/<wait-for-run! to detect terminal status. (upstream PR #2114) |
:copilot/session.canvas.removed |
Durable record that a canvas instance was closed, superseding a prior canvas.recorded during resume replay. |
(copilot/with-client-session [session {:streaming? true
:on-permission-request copilot/approve-all}]
(let [ch (chan 256)]
(tap (copilot/events session) ch)
(go-loop []
(when-let [event (<! ch)]
(case (:type event)
:copilot/assistant.message_delta
(print (get-in event [:data :delta-content]))
:copilot/session.usage_info
(println "Tokens:" (get-in event [:data :current-tokens]))
:copilot/session.idle
(println "\nDone!")
nil)
(recur)))
(copilot/send! session {:prompt "Hello"})))Enable streaming to receive assistant response chunks as they're generated:
(def session (copilot/create-session client
{:model "gpt-5.4"
:streaming? true
:on-permission-request copilot/approve-all}))
(let [ch (chan 100)]
(tap (copilot/events session) ch)
(go-loop []
(when-let [event (<! ch)]
(case (:type event)
:copilot/assistant.message_delta
;; Streaming chunk - print incrementally
(print (get-in event [:data :delta-content]))
:copilot/assistant.reasoning_delta
;; Streaming reasoning (model-dependent). Send to stderr.
(binding [*out* *err*]
(print (get-in event [:data :delta-content])))
:copilot/assistant.reasoning
(binding [*out* *err*]
(println "\n--- Final Reasoning ---")
(println (get-in event [:data :content])))
:copilot/assistant.message
;; Final complete message
(println "\n--- Final ---")
(println (get-in event [:data :content]))
nil)
(recur))))
(copilot/send! session {:prompt "Solve a logic puzzle and show your reasoning."})When :streaming? true:
:copilot/assistant.message_deltaevents contain incremental text in:delta-content:copilot/assistant.reasoning_deltaevents contain incremental reasoning in:delta-content(model-dependent)- Accumulate delta values to build the full response progressively
- The final
:copilot/assistant.messageevent always contains the complete content
The SDK supports two independent telemetry mechanisms.
Pass a :telemetry map in the client options to enable OpenTelemetry export on the
spawned CLI. Presence of the map enables OTel; all sub-keys are optional:
(def client
(copilot/client
{:telemetry {:otlp-endpoint "http://localhost:4318"
:exporter-type "otlp"
:source-name "my-app"
:capture-content? false}}))| Key | Type | Description | CLI env var |
|---|---|---|---|
:otlp-endpoint |
string | OTLP HTTP endpoint to export spans to | OTEL_EXPORTER_OTLP_ENDPOINT |
:otlp-protocol |
string | OTLP wire protocol: "http/json" or "http/protobuf" |
OTEL_EXPORTER_OTLP_PROTOCOL |
:file-path |
string | Write spans to a local file instead of/alongside OTLP | COPILOT_OTEL_FILE_EXPORTER_PATH |
:exporter-type |
string | Exporter selection | COPILOT_OTEL_EXPORTER_TYPE |
:source-name |
string | Service / source name attached to spans | COPILOT_OTEL_SOURCE_NAME |
:capture-content? |
boolean | Capture prompt/response content in spans. Defaults to off — only enable in trusted environments, as it records message content | OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT |
When :telemetry is present the SDK sets COPILOT_OTEL_ENABLED=true on the CLI process.
(upstream PR #785, PR #1648)
To stitch CLI spans into a caller-managed distributed trace, provide a zero-arg
:on-get-trace-context function in the client options. The SDK calls it per request
to capture a fresh trace context — on session create, session resume, and every message
send — forwarding only :traceparent and :tracestate:
(def client
(copilot/client
{:on-get-trace-context
(fn [] {:traceparent "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
:tracestate "rojo=00f067aa0ba902b7"})}))@experimental / Internal. This mirrors the official SDK's option set but is not a stable public surface. Shapes may change upstream.
Register a one-arg :on-github-telemetry callback in the client options to
receive the runtime's forwarded GitHub telemetry. Registering the callback is what
opts in: the SDK adds enableGitHubTelemetryForwarding: true to the connect
handshake — so the first session's un-replayable session.start telemetry is
forwarded (upstream PR #1909) — as well as to the wire params of both
session.create and session.resume (the flag is omitted entirely when no
callback is set — false is never sent). The runtime then emits connection-global
gitHubTelemetry.event notifications, each passed to the callback:
(def client
(copilot/client
{:on-github-telemetry
(fn [notification]
;; notification => {:session-id "..." :restricted false :event {...}}
(tap> notification))}))The callback runs on the client's notification loop. A throwing callback is caught and logged (WARN) — it cannot corrupt JSON-RPC dispatch. No reply is sent (these are notifications, not requests).
Notification shape:
| Key | Type | Description |
|---|---|---|
:session-id |
string | Session the event originated from |
:restricted |
boolean | Whether the event is restricted |
:event |
map | The telemetry event (see below) |
Event shape — scalar keys are kebab-cased from the wire (:kind, :created-at,
:model-call-id, :session-id, :copilot-tracking-id, :exp-assignment-context),
plus an optional :client map of client-info scalars (:cli-version, :os-platform,
:os-version, :os-arch, :node-version, :copilot-plan, :client-type,
:client-name, :is-staff, :dev-device-id).
Three event sub-maps are opaque source-defined data and pass through verbatim — their keys are not kebab-cased:
| Sub-map | Wire value type | Note |
|---|---|---|
:properties |
string → string | Keys preserved exactly as sent |
:metrics |
string → number | Keys preserved exactly as sent |
:features |
string → string | Keys preserved exactly as sent |
Do not rely on those keys being Clojure-idiomatic; treat them as an opaque bag keyed by the upstream-defined strings-as-keywords. (upstream PR #1835)
:enable-session-telemetry? is a session config flag that controls the CLI's own
internal usage telemetry — independent of the OpenTelemetry export above. It defaults to
enabled for GitHub-authenticated sessions and is always disabled when a BYOK
:provider is configured. In :mode :empty it is defaulted to false as one of the
multi-tenant hardening defaults (the caller can still set it explicitly). Set it to
false to opt out:
(def session
(copilot/create-session client
{:enable-session-telemetry? false
:on-permission-request copilot/approve-all}))(upstream PR #1224)
(def client (copilot/client {:auto-start? false}))
;; Start manually
(copilot/start! client)
;; Use client...
;; Stop manually
(copilot/stop! client):mode :empty configures the client for multi-tenant SaaS hosts that must
isolate sessions from the local machine — no on-disk state from a
specific user account leaks into a session. The default :copilot-cli
mode preserves historical CLI behavior. (upstream PR #1428)
(require '[github.copilot-sdk :as copilot]
'[github.copilot-sdk.tool-set :as tool-set])
(def client
(copilot/client
{:mode :empty
;; At least ONE of :copilot-home / :session-fs / :cli-url /
;; :is-child-process? is required so the CLI has a tenant-scoped
;; storage root. Using both is fine and common:
:copilot-home "/srv/tenants/acme/copilot-home"
:session-fs {:initial-cwd "/srv/tenants/acme/cwd"
:session-state-path "/srv/tenants/acme/state"
:conventions "posix"}}))
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
;; Required in :empty mode (use [] to allow nothing — the key must
;; be present so silently-empty filters can't happen):
:available-tools tool-set/isolated
;; Required when client has :session-fs:
:create-session-fs-handler (fn [_session] my-fs-handler)}))What :empty mode enforces (vs :copilot-cli):
- Constructor validation: at least one of
:copilot-home,:session-fs,:cli-url, or:is-child-process?must be supplied (so the CLI never falls back to the user's home directory). The SDK also forcesCOPILOT_DISABLE_KEYTAR=1on the spawned CLI. - Session validation: every
create-session/resume-sessioncall must provide:available-tools(an empty vector is legitimate). When the client has:session-fs,:create-session-fs-handleris also required (this applies to both modes). - Safe session defaults (spread UNDER caller config — caller always wins):
:enable-session-telemetry? false,:mcp-oauth-token-storage :in-memory,:skip-embedding-retrieval true,:embedding-cache-storage :in-memory,:enable-on-demand-instruction-discovery false,:enable-file-hooks false,:enable-host-git-operations false,:enable-session-store false,:enable-skills false,:memory {:enabled false}. - System message normalization: the SDK strips the
environment_contextsection from the system message (or promotes:appendto:customize) so no host-environment context leaks. If the caller already provides their ownenvironment_contextoverride in:customizemode, it is preserved verbatim. - Post-create options: a follow-up
session.options.updateRPC sets:skip-custom-instructions true,:custom-agents-local-only true,:coauthor-enabled false,:manage-schedule-enabled false, and forces:installed-plugins []. On failure, the SDK cleans up the half-configured session before propagating the error.
Both modes always emit :tool-filter-precedence "excluded" on
session.create and session.resume, and reject bare "*" in
:available-tools / :excluded-tools at the SDK boundary.
Use github.copilot-sdk.tool-set to construct :available-tools /
:excluded-tools lists with built-in helpers. Mirrors the upstream
BuiltInTools constants. (upstream PR #1428)
(require '[github.copilot-sdk.tool-set :as tool-set])
;; Source-qualified single tool — patterns are "<source>:<name>",
;; source is one of "builtin", "mcp", or "custom":
(tool-set/builtin "ask_user") ; => "builtin:ask_user"
(tool-set/builtin "*") ; => "builtin:*" (all built-ins)
(tool-set/mcp "*") ; => "mcp:*" (all MCP tools)
(tool-set/custom "my_tool") ; => "custom:my_tool"
;; Vector of patterns:
(tool-set/builtins ["task" "skill"])
;; => ["builtin:task" "builtin:skill"]
;; The "Isolated" preset matches BuiltInTools.Isolated upstream —
;; every built-in that is safely session-bounded (no host I/O):
tool-set/isolated
;; => ["builtin:ask_user" "builtin:task_complete" "builtin:exit_plan_mode" ...]The constructors enforce well-formed entries: a bare "*" is rejected (the SDK
also rejects it in :available-tools / :excluded-tools at the session
boundary — apps must explicitly opt into a source so an absent source can
never silently grant access to unexpected tools). The runtime always receives
:tool-filter-precedence "excluded" on session.create / session.resume
so the ordering between allow and deny lists is deterministic.
Let the CLI call back into your process when the model needs capabilities you provide:
(require '[github.copilot-sdk :as copilot])
(def lookup-tool
(copilot/define-tool "lookup_issue"
{:description "Fetch issue details from our tracker"
:metadata {:owner "issues-team"}
:parameters {:type "object"
:properties {:id {:type "string"
:description "Issue identifier"}}
:required ["id"]}
:handler (fn [{:keys [id]} invocation]
(let [issue (fetch-issue id)]
(copilot/result-success issue)))}))
(def session (copilot/create-session client
{:model "gpt-5.4"
:tools [lookup-tool]
:on-permission-request copilot/approve-all}))When Copilot invokes lookup_issue, the SDK automatically runs your handler and responds to the CLI.
Both define-tool and define-tool-from-spec accept these common options:
| Key | Type | Default | Description |
|---|---|---|---|
:defer |
:auto or :never |
runtime default | Per-tool lazy-loading policy; distinct from session :tool-search configuration |
:description |
string or nil |
nil |
Human-readable tool description |
:handler |
fn | nil |
Two-argument function receiving parsed arguments and the invocation map |
:is-terminal? |
boolean | nil |
When true, a successful call ends the agent turn instead of feeding the tool result back to the model. A failed call leaves the loop running so the model can inspect the error and retry. Gated on some?; explicit false is forwarded and an absent key is omitted. Serialized as wire isTerminal. (upstream PR #2129) |
:metadata |
map | nil |
Opaque host-defined metadata forwarded with the tool definition. An explicit {} is preserved; nil or an absent key is omitted |
:overrides-built-in-tool |
boolean | nil |
Allow this definition to replace a built-in tool with the same name |
Use :parameters with define-tool or :spec with define-tool-from-spec.
Metadata is not interpreted. String keys preserve their exact spelling; keyword
keys follow the SDK's normal kebab-case to camelCase wire conversion.
The handler invocation map contains:
| Key | Type | Required | Description |
|---|---|---|---|
:session-id |
string | yes | Session identifier |
:tool-call-id |
string | yes | Tool call identifier |
:tool-name |
string | yes | Invoked tool name |
:arguments |
any | yes | Parsed arguments, without key conversion |
:available-tools |
vector | no | Current tool metadata snapshot, provided only to the tool_search_tool handler |
:traceparent |
string | no | W3C trace parent |
:tracestate |
string | no | W3C trace state |
Each :available-tools entry contains:
| Key | Type | Required | Description |
|---|---|---|---|
:name |
string | yes | Tool name |
:description |
string | yes | Human-readable description |
:namespaced-name |
string | no | Fully qualified tool name |
:mcp-server-name |
string | no | MCP server name |
:mcp-tool-name |
string | no | MCP tool name |
:input-schema |
map | no | Tool input JSON Schema |
:defer-loading |
boolean | no | Whether the runtime deferred loading the tool |
Failure to fetch the current metadata snapshot does not fail the tool call. The
SDK invokes tool_search_tool without :available-tools instead.
Declaration-only tools (manual resolution):
The :handler key is optional (upstream PR #1308). When omitted, the SDK does not auto-respond to tool calls — the call surfaces as a :copilot/external_tool.requested event with a pending request id, and the application resolves it later via handle-pending-tool-call!. Useful for human-in-the-loop UIs or out-of-process tool execution.
(def manual-tool
(copilot/define-tool "manual_lookup"
{:description "Look up status manually"
:parameters {:type "object"
:properties {:id {:type "string"}}
:required ["id"]}}))
;; …later, after a human reviews the request:
(copilot/handle-pending-tool-call! session
{:request-id "tool-req-7"
:result "STATUS_OK"})Overriding built-in tools:
Set :overrides-built-in-tool true to override a built-in tool (e.g., grep, edit_file). Without this flag, defining a tool whose name clashes with a built-in tool causes an error.
(def custom-grep
(copilot/define-tool "grep"
{:description "Custom grep with project-specific filtering"
:overrides-built-in-tool true
:parameters {:type "object"
:properties {:pattern {:type "string"
:description "Search pattern"}}
:required ["pattern"]}
:handler (fn [{:keys [pattern]} _invocation]
(copilot/result-success (my-custom-grep pattern)))}))Deferring tools:
Set :defer to :auto or :never (upstream PR #1632) to control whether a tool may be deferred — loaded lazily via tool search rather than always pre-loaded into the model's context. :auto (the default) lets the runtime defer the tool; :never forces it to be pre-loaded. Deferring large tool sets keeps the active context smaller.
(def always-loaded
(copilot/define-tool "critical_action"
{:description "A tool that must always be available"
:defer :never
:parameters {:type "object" :properties {}}
:handler (fn [_ _] (copilot/result-success "done"))}))The keyword is converted to the wire string (:auto -> "auto", :never -> "never"); when :defer is omitted the field is not sent and the runtime applies its default.
Handler return values:
| Return Type | Description |
|---|---|
| String | Automatically wrapped as success result |
Map with :result-type |
Full control over result metadata |
| core.async channel | Async result (yields string or map) |
Object tool results contain:
| Key | Type | Required | Description |
|---|---|---|---|
:text-result-for-llm |
string | yes | Text returned to the model |
:result-type |
keyword or string | yes | One of success, failure, rejected, denied, or timeout |
:binary-results-for-llm |
vector | no | Binary image or resource results |
:error |
string | no | Error details |
:session-log |
string | no | Session log text |
:tool-telemetry |
map | no | Tool telemetry fields |
:tool-references |
collection of strings | no | Tool names referenced by the result |
Result helpers:
(copilot/result-success "It worked!")
(copilot/result-failure "It failed" "error details")
(copilot/result-denied "Permission denied")
(copilot/result-rejected "Invalid parameters")MCP result conversion:
Convert an MCP CallToolResult into the SDK's ToolResultObject format with convert-mcp-call-tool-result:
(require '[github.copilot-sdk.tools :as tools])
(tools/convert-mcp-call-tool-result
{:content [{:type "text" :text "Hello from MCP"}]
:is-error false})
;; => {:text-result-for-llm "Hello from MCP", :result-type "success"}
(tools/convert-mcp-call-tool-result
{:content [{:type "text" :text "Something went wrong"}]
:is-error true})
;; => {:text-result-for-llm "Something went wrong", :result-type "failure"}The input map uses Clojure-idiomatic keys:
| Key | Type | Description |
|---|---|---|
:content |
vector | Content blocks, each with :type and type-specific fields |
:is-error |
boolean | When true, the result-type is "failure" |
Supported content block types:
| Type | Fields | Description |
|---|---|---|
"text" |
:text |
Text content, joined with newlines |
"image" |
:data, :mime-type |
Base64-encoded image, added to :binary-results-for-llm |
"resource" |
:resource with :uri, :text, :blob, :mime-type |
Resource content (text and/or binary) |
Register slash commands that users can invoke in the TUI. Define each command as a map with :name, :description, and :command-handler, then pass them via :commands in session config.
(def my-commands
[{:name "deploy"
:description "Deploy the current project"
:command-handler (fn [{:keys [session-id command-name args]}]
(println "Deploying with args:" args))}
{:name "status"
:description "Show project status"
:command-handler (fn [{:keys [session-id command-name args]}]
(println "All systems operational"))}])
(def session (copilot/create-session client
{:model "gpt-5.4"
:commands my-commands
:on-permission-request copilot/approve-all}))Command definition keys:
| Key | Type | Required | Description |
|---|---|---|---|
:name |
string | yes | Command name (without leading slash) |
:description |
string | no | Description shown in TUI command list |
:command-handler |
fn | yes | Handler function |
The handler receives a context map:
| Key | Description |
|---|---|
:session-id |
The session ID |
:command |
Full command string |
:command-name |
Matched command name |
:args |
Arguments after the command name |
The handler may return nil or a core.async channel (awaited automatically).
Control the system prompt:
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:system-message
{: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. Your :content is appended after SDK-managed sections.
For full control (removes all guardrails), use :mode :replace:
(copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:system-message {:mode :replace
:content "You are a helpful assistant."}})The :customize mode enables section-level overrides of the system prompt. Twelve sections are configurable:
| Section | Description |
|---|---|
:preamble |
Agent identity preamble and mode statement (upstream PR #1713) |
:identity |
Section group covering the identity preamble and its sibling sub-sections (tone, tool efficiency, etc.) |
:tone |
Response style, conciseness rules, output formatting |
:tool-efficiency |
Tool usage patterns, parallel calling, batching |
:environment-context |
CWD, OS, git root, directory listing, available tools |
:code-change-rules |
Coding rules, linting/testing, ecosystem tools, style |
:guidelines |
Tips, behavioral best practices |
:safety |
Environment limitations, prohibited actions, security |
:tool-instructions |
Per-tool usage instructions |
:custom-instructions |
Repository and organization custom instructions |
:runtime-instructions |
Runtime-provided context (system notifications, memories, mode-specific instructions, content-exclusion policy) — added in upstream PR #1377 |
:last-instructions |
End-of-prompt instructions |
Each section supports static actions (:replace, :remove, :append, :prepend, :preserve) and transform callbacks (1-arity functions). :preserve is a no-op marker that opts an individually-addressable section out of a group-level :remove — e.g. keep :tone when removing the :identity group (upstream PR #1713).
(require '[github.copilot-sdk :as copilot])
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:system-message
{:mode :customize
:sections {:identity {:action :replace
:content "You are Acme Assistant."}
:tone {:action :append
:content "\nAlways respond in bullet points."}
:code-change-rules {:action :remove}}
:content "Additional instructions here."}}))Transform callbacks receive the current section content and return the replacement:
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:system-message
{:mode :customize
:sections {:identity {:action (fn [current]
(clojure.string/replace current
"GitHub Copilot" "Acme Assistant"))}}}}))Inspect available sections with the system-prompt-sections constant:
copilot/system-prompt-sections
;; => {:preamble {:description "Agent identity preamble and mode statement"}
;; :identity {:description "Section group covering the identity preamble ..."}
;; :tone {:description "Response style, conciseness rules, ..."}
;; :runtime-instructions {:description "Runtime instructions injected ..."} ...}Available section keys: :preamble (added in upstream PR #1713), :identity,
:tone, :tool-efficiency, :environment-context, :code-change-rules,
:guidelines, :safety, :tool-instructions, :custom-instructions,
:runtime-instructions (added in upstream PR #1377), :last-instructions.
Naming note — Upstream renamed
SystemPromptSection→SystemMessageSectionin the TypeScript SDK. The Clojure SDK keepssystem-prompt-sectionsas the canonical name (for back-compat) and exposessystem-message-sectionsas an alias.
Unknown section keywords are allowed — they gracefully fall back to appending content to additional instructions.
Hide tools from the built-in/default agent while keeping them available to custom agents with :default-agent.
(require '[github.copilot-sdk :as copilot])
(def repo-index-tool
(copilot/define-tool "repo_index_search"
{:description "Search the private repository index"
:parameters {:type "object"
:properties {:query {:type "string"}}
:required ["query"]}
:handler (fn [{:keys [query]} _]
(str "index results for " query))}))
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:tools [repo-index-tool]
:custom-agents [{:agent-name "repo-auditor"
:agent-prompt "Audit repository changes."
:agent-tools ["repo_index_search"]
:agent-reasoning-effort "high"}]
:default-agent {:excluded-tools ["repo_index_search"]}}))The default agent cannot call repo_index_search. The repo-auditor custom agent can still call it because custom-agent tool assignment is independent of :default-agent. Its reasoning effort is overridden to "high"; omit :agent-reasoning-effort to let the runtime resolve effort from the model configuration, inheriting the parent session's effort only when the agent uses the same model.
config-dir overrides where the CLI reads its config and state (e.g., ~/.copilot).
It does not define custom agents. Custom agents are provided via :custom-agents.
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:config-dir "/tmp/copilot-config"
:skill-directories ["/path/to/skills" "/opt/team-skills"]
:disabled-skills ["legacy-skill" "experimental-skill"]}))Note: This is a CLI protocol feature not exposed in the official
@github/copilot-sdk. TheoutputDirandmaxSizeBytessettings may be ignored by some CLI versions due to a known issue where session-level config is not applied duringsession.sendexecution. The CLI's default behavior (30KB threshold, system tmpdir) applies regardless.
Configure how large tool outputs are handled before being sent back to the model:
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:large-output {:enabled true
:max-size-bytes 65536
:output-dir "/tmp/copilot-tool-output"}}))When a tool output exceeds the configured size, the CLI writes the full output to a temp file,
and the tool result delivered to the model contains a short message with the file path and preview.
You can see this message in :tool.execution_complete events:
(let [events (copilot/subscribe-events session)]
(go-loop []
(when-let [event (<! events)]
(when (= :tool.execution_complete (:type event))
(when-let [content (get-in event [:data :result :content])]
(println "Tool output message:\n" content)))
(recur))))Note: large output handling is applied by the CLI for built-in tools (like the shell tool). For external tools you define in the SDK, consider handling oversized outputs yourself (e.g., write to a file and return a short preview).
Infinite sessions enable automatic context compaction, allowing conversations to continue beyond the model's context window limit. When the context approaches capacity, the CLI automatically compacts older messages while preserving important context.
;; Enable with defaults (enabled by default)
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all}))
;; Explicit configuration
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:infinite-sessions {:enabled true
:background-compaction-threshold 0.80
:buffer-exhaustion-threshold 0.95}}))
;; Disable infinite sessions
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:infinite-sessions {:enabled false}}))Configuration options:
| Key | Type | Default | Description |
|---|---|---|---|
:enabled |
boolean | true |
Enable infinite sessions |
:background-compaction-threshold |
number | 0.80 |
Context utilization (0.0-1.0) at which background compaction starts |
:buffer-exhaustion-threshold |
number | 0.95 |
Context utilization (0.0-1.0) at which session blocks until compaction completes |
How it works:
- When context reaches the background threshold (default 80%), compaction starts asynchronously
- The session continues processing while compaction runs in the background
- If context reaches the buffer exhaustion threshold (default 95%), the session blocks until compaction completes
- Compaction preserves essential context while removing older, less relevant messages
Compaction events:
Sessions emit :session.compaction_start and :session.compaction_complete events during compaction:
(let [ch (copilot/subscribe-events session)]
(go-loop []
(when-let [event (<! ch)]
(case (:type event)
:session.compaction_start
(println "Compaction started...")
:session.compaction_complete
(println "Compaction complete")
nil)
(recur))))Note: Agent Factories are
@experimentalupstream (upstream PR #2114). The API may change in future releases.
An Agent Factory is an extension-authored, named workflow that a session can run: it
declares its own phases and limits, executes with reverse-RPC access to the parent
session (spawn nested agent turns, run journaled/idempotent steps, fan out work in
parallel or as a pipeline), and reports durable, resumable progress back to the CLI.
Factories are registered per-session via join-session's
:factories option and approved via the :factory permission kind.
Most of this API is namespace-qualified only — require the namespace directly:
(require '[github.copilot-sdk.factory :as factory]
'[github.copilot-sdk.session :as session])Defining a factory
(def summarize-repo
(factory/define-factory
{:meta {:name "summarize-repo"
:description "Summarize a repository's structure and recent activity"
:phases [{:title "Scan" :detail "List top-level files and directories"}
{:title "Summarize"}]
:limits {:max-concurrent-subagents 4
:timeout-seconds 300}}
:run (fn [{:keys [args phase log step agent parallel] session :session}]
(phase "Scan")
(let [files (step "list-files" #(session/workspace-list-files session))]
(log (str "Found " (count (:files files)) " files"))
(phase "Summarize")
(let [summaries (parallel
(mapv (fn [f]
#(agent (str "Summarize " f)))
(:files files)))]
{:file-count (count (:files files))
:summaries summaries})))}))define-factory is also exposed as a top-level facade, copilot/define-factory
(synchronous only — no <define-factory twin, since validation does no I/O).
:meta is validated eagerly by define-factory:
| Field | Type | Required? | Notes |
|---|---|---|---|
:name |
string | yes | Non-blank. Must be unique within a join-session call's :factories vector. |
:description |
string | yes | Non-blank. |
:phases |
vector of maps | yes | Each phase is {:title string :detail string (optional)}. :title must be non-blank and unique across phases. |
:limits |
map (optional) | no | Validated only when present; see below. |
| Limit key | Type | Constraint |
|---|---|---|
:max-concurrent-subagents |
positive integer | — |
:max-total-subagents |
positive integer | — |
:timeout-seconds |
positive finite number | ≤ 2147483.647 |
:max-ai-credits |
positive finite number | must round to a positive nano-AIU value |
:run must be a function of one argument, the context map
described below, and returns (directly or via a core.async channel, Future, promise,
or delay) a JSON-safe value — nil, a string, boolean, finite number, or a nested
vector/map thereof. Return factory/json-null to report an explicit JSON null
result (as opposed to nil, which means "no result").
Other namespace-qualified-only helpers:
| Function | Description |
|---|---|
factory/factory-handle? |
Return true when value is a handle created by define-factory. |
factory/factory-meta |
Return a handle's :meta map. |
factory/terminal-status? |
Return true when a run :status (keyword or string) is terminal (:completed, :halted, :cancelled, :error). Also exposed as copilot/factory-terminal-status?. |
factory/json-null |
Sentinel value for an explicit JSON null factory result. |
Driving factory execution
Register handles via join-session's :factories option, then drive execution from any
client connected to that session (typically the parent CLI, but any joined client can
call these). Each function has a top-level copilot facade wrapper (all ^:experimental)
and an async <-prefixed twin returning a core.async channel:
(require '[github.copilot-sdk :as copilot])
;; By name (the factory must be registered on the joined session) or by handle:
(copilot/run-factory! session "summarize-repo" {:args {:path "."}})
;; => {:run-id "..." :status :completed :result {...} ...}
;; Async twin — channel yields the result, or the caught Throwable directly:
(let [ch (copilot/<run-factory! session "summarize-repo" {:args {:path "."}})]
(let [result (<!! ch)]
(if (instance? Throwable result)
(throw result)
result)))Sync function (factory/...) |
Top-level facade | Wire method | Description |
|---|---|---|---|
run! |
run-factory! |
session.factory.run |
Start a factory by name or handle. 2-arity [session name-or-handle] or 3-arity with an options map: :args (passed to :run as :args), :limits (overrides declared limits, re-validated), :resume-from-run-id (delegates to resume!). Waits for terminal status via wait-for-run! if the initial response is non-terminal. |
resume! |
resume-factory! |
session.factory.resume |
Resume a durable, previously-started run by run-id. 2-arity [session run-id] or 3-arity with {:limits ...}. See error classification below. |
get-run |
get-factory-run |
session.factory.getRun |
Read the latest durable envelope for a run. [session run-id]. |
wait-for-run! |
wait-for-factory-run! |
(polls get-run + listens for :copilot/factory.run_updated) |
Block until a run reaches a terminal status. 2-arity or 3-arity with {:cancel-chan :poll-interval-ms} (:poll-interval-ms default 5000; :cancel-chan aborts the wait, not the run, throwing ex-info {:type :factory-wait-cancelled :run-id ...}). Requires the session to be connected (has an active event stream). |
list-runs |
list-factory-runs |
session.factory.listRuns |
List all durable runs for the session, in creation order. [session]. |
get-run-detail |
get-factory-run-detail |
session.factory.getRunDetail |
Read durable phases, agent turns, and recent progress for a run. [session run-id]. |
get-run-progress |
get-factory-run-progress |
session.factory.getRunProgress |
Page durable progress lines. 2-arity or 3-arity with an options map merged into the wire params (e.g. pagination cursors). |
cancel! |
cancel-factory-run! |
session.factory.cancel |
Request cancellation from the CLI. The runtime's reverse factory.abort request then marks active local executions cancelled and closes their :cancel-chan. Returns the resulting terminal envelope. [session run-id]. |
All of the above return a run envelope map with at least :run-id and a keywordized
:status (one of :running, :completed, :halted, :cancelled, :error, or other
non-terminal statuses reported by the CLI).
Async twins: every function above has a <-prefixed twin (e.g. factory/<run!,
copilot/<run-factory!) with the same arities, running the call on a thread pool and
returning a core.async channel. The channel yields the successful result or the
caught Throwable directly (not wrapped) — always check (instance? Throwable result)
before using the value.
resume!/<resume!/resume-factory!/<resume-factory! reclassify specific wire
error codes into a stable ex-info shape: {:type :factory-resume-error :code <kebab-keyword>}
(original exception preserved as the cause). Other errors are rethrown unchanged.
| Wire code | :code |
|---|---|
"not_found" |
:not-found |
"non_resumable" |
:non-resumable |
"already_active" |
:already-active |
"reapproval_declined" |
:reapproval-declined |
"no_approval_provider" |
:no-approval-provider |
(try
(factory/resume! session run-id)
(catch clojure.lang.ExceptionInfo e
(case (:code (ex-data e))
:reapproval-declined (println "Approver declined re-approval")
:non-resumable (println "This run cannot be resumed")
(throw e))))Every factory's :run function receives a single context map, built fresh per
execution:
| Key | Type | Description |
|---|---|---|
:run-id |
string | The durable run's ID. |
:args |
JSON value | The original run! arguments (default {}), persisted across resume!. |
:session |
CopilotSession |
The session the factory is running within. |
:cancel-chan |
channel | Closes when the run is cancelled/aborted. |
:cancelled? |
(fn []) |
Returns true once the run has been cancelled/aborted. |
:agent |
(fn [prompt] ...) / (fn [prompt options] ...) |
Runs a nested agent turn and returns its result. options may include :label, :schema, :model. |
:step |
(fn [key producer] ...) / (fn [key producer options] ...) |
Runs producer (a 0-arg fn) once per unique key and journals the JSON-safe result for replay on resume; a repeated key returns the cached result without re-running producer. Pass {:volatile? true} to skip journaling/caching entirely. |
:parallel |
(fn [thunks] ...) |
Runs a vector of 0-arg thunks concurrently (bounded at 4096 items) and returns their results in order; a non-fatal thunk error yields nil in its slot, a fatal error (abort, or an inner :agent/:step RPC failure) rethrows. |
:pipeline |
(fn [items & stages] ...) |
Threads each item in items (bounded at 4096) sequentially through stages, each called as (stage previous-value original-item index); same fatal/non-fatal error semantics as :parallel. |
:phase |
(fn [text] ...) |
Buffers a phase-transition progress line; the SDK flushes before :agent/:step calls and when the run finishes. |
:log |
(fn [text] ...) |
Buffers a log progress line; the SDK flushes before :agent/:step calls and when the run finishes. |
:agent, :step, :parallel, and :pipeline all throw ex-info with
{:type :factory-aborted :run-id ...} if the run is cancelled while they're executing.
Values returned from :step producers and from :run itself are validated as JSON-safe
before being journaled or returned; a non-JSON-safe value throws ex-info with
{:value-type "..."}.
The SDK uses a deny-by-default permission model. All permission requests
(file writes, shell commands, URL fetches, custom tool execution, etc.) are denied unless your
session config provides an :on-permission-request handler. The handler is
optional on create-session, resume-session, and join-session (upstream
PR #1308): when omitted, permission requests are not auto-resolved and
applications must resolve them via handle-pending-permission-request!.
join-session historically defaulted to {:kind :no-result} and continues to
behave that way.
Use approve-all to opt into approving everything:
(def session (copilot/create-session client
{:on-permission-request copilot/approve-all}))The :permission-kind field in permission requests identifies the type of action requiring approval:
| Permission Kind | Description |
|---|---|
:shell |
Shell command execution |
:write |
File system write operation |
:mcp |
MCP tool invocation |
:read |
File system read operation |
:url |
URL fetch / HTTP request |
:custom-tool |
SDK-registered custom tool invocation |
:memory |
Memory storage operation (subject, fact, citations) |
:hook |
Hook-triggered permission check |
:factory |
Agent Factory run or authoring approval (see Agent Factories (Experimental)) |
Memory permission events include additional data fields (specs ::memory-action, ::memory-direction, ::memory-reason):
| Field | Type | Description |
|---|---|---|
:memory-action |
:store or :vote |
The memory operation type |
:memory-direction |
:upvote or :downvote |
Vote direction (when action is :vote) |
:memory-reason |
string | Reason for the memory operation |
Factory permission requests (:permission-kind :factory) include additional data fields describing the run or authoring request awaiting approval (upstream PR #2114):
| Field | Type | Description |
|---|---|---|
:operation |
"run" or "author" |
Whether the factory is being run or authored |
:name |
string | Factory name |
:description |
string | Factory description |
:phases |
vector of maps | {:title string, :detail string (optional)} — the phases the factory declares |
:approval-key |
string | Stable key for persisting an approval decision across runs |
:can-persist-approval |
boolean | Whether :approve-for-session/:approve-for-location are meaningful for this request |
:max-concurrent-subagents, :max-total-subagents, :timeout-seconds, :max-ai-credits |
number (optional) | Effective runtime limits for this run |
:declared-max-concurrent-subagents, :declared-max-total-subagents, :declared-timeout-seconds, :declared-max-ai-credits |
number (optional) | The limits as declared by the factory definition, shown to approvers alongside the effective limits above |
For fine-grained control, provide your own handler. When the CLI needs
approval, it sends a JSON-RPC permission.request to the SDK. Your
:on-permission-request callback must return a map compatible with the
permission result payload; the SDK wraps this into the JSON-RPC response
as {:result <your-map>}:
Your handler receives two arguments: the request map (the permission
request payload) and a ctx map with:
| Key | Type | Description |
|---|---|---|
:session-id |
string | The session the request originated from |
:managed-settings-enabled? |
boolean | true when the session has enterprise-managed settings active (:managed-settings config or :enable-managed-settings?); false otherwise |
ctx is passed to every :on-permission-request handler, including
approve-all and default-join-session-permission-handler.
(upstream PR #2139)
The permission_bash.clj example demonstrates both an allowed and a denied
shell command and prints the full permission request payload so you can inspect
fields like :full-command-text, :commands, and :possible-paths.
;; Approve this request once
{:kind :approve-once}
;; Approve and remember for the session
{:kind :approve-for-session
:approval {:kind :commands
:command-identifiers ["echo"]}}
;; Approve and persist for the project location
{:kind :approve-for-location
:approval {:kind :write}
:location-key "/path/to/project"}
;; Reject with optional user-facing detail
{:kind :reject
:feedback "Not allowed"}
;; No user confirmation is available
{:kind :user-not-available}
;; Extension declines to answer (another handler may respond)
{:kind :no-result}Legacy Clojure permission result kinds such as :approved and
:denied-by-rules remain accepted and are normalized before the SDK sends the
decision to the CLI.
When the runtime resolves a permission request via a permissionRequest hook, the
permission.requested event includes :resolved-by-hook true. The SDK automatically
skips the client's :on-permission-request handler and does not send the
handlePendingPermissionRequest RPC — the event is still published to event subscribers
for observability.
(copilot/approve-all request ctx)A convenience permission handler that approves all permission requests by
returning {:kind :approve-once}. Equivalent to the upstream Node.js SDK
approveAll export.
Fail-closed under managed settings: when ctx reports
:managed-settings-enabled? true (see :managed-settings /
:enable-managed-settings? in the session-config table), approve-all
throws ex-info with message "approve-all cannot be used when managed settings are enabled" and {:session-id (:session-id ctx)} — host-side
auto-approval must not bypass managed policy. When the request itself sets
:managed-approval-required true (or omits false), approve-all returns
{:kind :no-result} instead of approving, deferring the decision to managed
approval. Otherwise it returns {:kind :approve-once} as before.
(upstream PR #2139)
Pass as the :on-permission-request value in session config:
(copilot/create-session client {:on-permission-request copilot/approve-all})(copilot/default-join-session-permission-handler request ctx)Returns {:kind :no-result} — the CLI handles permission decisions itself. When used with resume-session, the SDK sends requestPermission: false on the wire, telling the CLI that this client does not want to handle permission requests.
Use this when reconnecting to a session where the original client already established permission handling:
(copilot/resume-session client "session-123"
{:on-permission-request copilot/default-join-session-permission-handler})Equivalent to the upstream Node.js SDK defaultJoinSessionPermissionHandler export.
When the agent needs input from the user (via ask_user tool), the :on-user-input-request
handler is called. Return a response map with the user's input:
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:on-user-input-request
(fn [request invocation]
;; request contains {:question "..." :choices [...] :allow-freeform true/false}
(println "Agent asks:" (:question request))
(when-let [choices (:choices request)]
(println "Choices:" choices))
;; Return user's response
;; :answer is required, :was-freeform defaults to true
{:answer (read-line)
:was-freeform true})}))The request map includes:
:question- The question being asked:choices- Optional list of choices for multiple choice questions:allow-freeform- Whether freeform text input is allowed
The response map should include:
:answer- The user's answer (string, required).:responseis also accepted for convenience.:was-freeform- Whether the answer was freeform (boolean, defaults to true)
Provide a handler for elicitation requests from the agent. This enables the SDK client to act as a UI provider for form-based dialogs.
(require '[github.copilot-sdk :as copilot])
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:on-elicitation-request
(fn [{:keys [session-id message requested-schema mode]}]
(println "Elicitation for session" session-id ":" message)
{:action "accept"
:content {:name "user-input"}})}))The handler receives a single ElicitationContext map:
| Key | Type | Description |
|---|---|---|
:session-id |
string | Session that triggered the request |
:message |
string | What information is needed from the user |
:requested-schema |
map | JSON Schema describing form fields (optional) |
:mode |
string | "form" for structured input, "url" for browser redirect (optional) |
:elicitation-source |
string | Source that initiated the request, e.g. MCP server name (optional) |
:url |
string | URL to open in browser, url mode only (optional) |
Return an ElicitationResult map:
| Key | Type | Description |
|---|---|---|
:action |
string | "accept", "decline", or "cancel" |
:content |
map | Field values when action is "accept" |
If the handler throws, the SDK sends {:action "cancel"} to prevent the request from hanging.
When :on-elicitation-request is set, the session advertises requestElicitation=true in the create/resume RPC. Capabilities are updated dynamically via capabilities.changed events.
Virtualize per-session storage with custom filesystem handlers. The runtime routes all session-scoped file I/O (event logs, large outputs, checkpoints) through the provided callbacks.
Configure the client with :session-fs:
(require '[github.copilot-sdk :as copilot])
(def client
(copilot/client {:session-fs {:initial-cwd "/home/user/project"
:session-state-path "/sessions"
:conventions "posix"}}))Provide a provider factory per session:
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:create-session-fs-handler
(fn [_session]
(let [store (atom {})]
{:read-file (fn [path]
(or (get @store path)
(throw (ex-info "missing file" {:code "ENOENT"}))))
:write-file (fn [path content _mode]
(swap! store assoc path content))
:append-file (fn [path content _mode]
(swap! store update path str content))
:exists (fn [path]
(contains? @store path))
:stat (fn [path]
{:is-file true
:is-directory false
:size (count (get @store path ""))
:mtime "2026-01-01T00:00:00Z"
:birthtime "2026-01-01T00:00:00Z"})
:mkdir (fn [_path _recursive _mode] nil)
:readdir (fn [_path] [])
:readdir-with-types (fn [_path] [])
:rm (fn [path _recursive _force]
(swap! store dissoc path))
:rename (fn [src dest]
(swap! store
(fn [s]
(-> s
(assoc dest (get s src ""))
(dissoc src)))))}))}))Provider functions use direct arguments and throw on failure. Errors with {:code "ENOENT"} become structured SessionFsError maps with code "ENOENT"; all other exceptions become "UNKNOWN". create-session and resume-session automatically adapt provider-style factory returns to the low-level RPC handler contract.
Use create-session-fs-adapter when you need the low-level handler map explicitly:
(require '[clojure.java.io :as io])
(def provider
{:read-file slurp
:write-file (fn [path content _mode] (spit path content))
:append-file (fn [path content _mode] (spit path content :append true))
:exists (fn [path] (.exists (io/file path)))
:stat (fn [path] {:is-file (.isFile (io/file path))
:is-directory (.isDirectory (io/file path))
:size (.length (io/file path))})
:mkdir (fn [path _recursive _mode] (.mkdirs (io/file path)))
:readdir (fn [path] (vec (.list (io/file path))))
:readdir-with-types (fn [_path] [])
:rm (fn [path _recursive _force] (clojure.java.io/delete-file path true))
:rename (fn [src dest] (.renameTo (io/file src) (io/file dest)))})
(def handler
(copilot/create-session-fs-adapter provider))The low-level handler map requires the 10 core FS operations below. The three
:sqlite-* keys are optional and only required when the client advertises
:capabilities {:sqlite true} on its :session-fs config (see
SQLite support).
| Key | Params | Returns |
|---|---|---|
:read-file |
{:session-id :path} |
{:content "..."} |
:write-file |
{:session-id :path :content :mode} |
nil |
:append-file |
{:session-id :path :content :mode} |
nil |
:exists |
{:session-id :path} |
{:exists true/false} |
:stat |
{:session-id :path} |
{:is-file :is-directory :size :mtime :birthtime} |
:mkdir |
{:session-id :path :recursive :mode} |
nil |
:readdir |
{:session-id :path} |
{:entries [...]} |
:readdir-with-types |
{:session-id :path} |
{:entries [...]} |
:rm |
{:session-id :path :recursive :force} |
nil |
:rename |
{:session-id :src :dest} |
nil |
:sqlite-query (optional) |
{:session-id :query-type :query :params} |
{:rows [...] :columns [...] :rows-affected n} |
:sqlite-exists (optional) |
{:session-id} |
{:exists true/false} |
:sqlite-transaction (optional) |
{:session-id :statements [...]} |
{:results [...]} or {:results [] :error {:error-class ... :message ...}} (upstream PR #2140) |
Handler functions may return values directly or via core.async channels.
To handle sessionFs.sqliteQuery and sessionFs.sqliteExists (upstream PR #1299),
add a nested :sqlite map to the provider and advertise the capability on the
client config:
(def client
(copilot/client {:session-fs {:initial-cwd "/home/user/project"
:session-state-path "/sessions"
:conventions "posix"
:capabilities {:sqlite true}}}))
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:create-session-fs-handler
(fn [_session]
{;; ... all 10 fs operations above ...
:sqlite {:query (fn [query-type sql params]
;; query-type is one of :exec, :query, :run
;; params is the raw bind-parameter map (keys preserved verbatim, e.g. :$userId)
{:rows [{:n 1}] :columns ["n"] :rows-affected 0})
:exists (fn [] true)
:transaction (fn [statements]
;; statements is a vector of
;; {:query-type :query :query :params} maps to
;; run atomically; return one result map per statement.
;; Throw github.copilot-sdk.session/session-fs-sqlite-transaction-failure
;; to report a classified failure (see below).
(mapv (fn [_stmt] {:rows [] :columns [] :rows-affected 1})
statements))}})})):transaction is optional — a provider that omits it responds to
sessionFs.sqliteTransaction with {:results [] :error {:error-class "fatal" :message "SQLite transactions are not supported by this provider"}}.
Use session-fs-sqlite-transaction-failure to report a classified failure from
inside a :transaction handler, and session-fs-sqlite-transaction-failure? to
recognize one:
(require '[github.copilot-sdk.session :as session])
(session/session-fs-sqlite-transaction-failure
"database is locked" :busy-or-locked)
;; => an ex-info the handler can throw; ex-data has
;; {:type :session-fs-sqlite-transaction-failure :error-class :busy-or-locked}
(session/session-fs-sqlite-transaction-failure? some-exception) ;; => true/falseerror-class is one of :busy-or-locked, :post-commit-ambiguous, or the
default :fatal (single-arity call). Classified failures are wire-mapped to
"busyOrLocked", "postCommitAmbiguous", and "fatal" respectively; any
uncaught exception thrown from :transaction is also reported as "fatal"
with its message. (upstream PR #2140)
Notes:
:capabilities {:sqlite true}is required when sqlite is advertised; declaring it without supplying:sqlitein the provider throws at session creation.- SQL bind-parameter map keys (e.g.
$userId) bypass kebab-case conversion and arrive at the handler verbatim. - Result row column-name keys (e.g.
:user_id,:created_at) round-trip verbatim on the outgoing wire path — they are not converted to camelCase, matching upstream Node.js semantics where provider rows are forwarded untouched. - SQLite handler exceptions propagate as JSON-RPC errors (not wrapped as
SessionFsError).
Lifecycle hooks allow custom logic at various points during the session:
(def session (copilot/create-session client
{:model "gpt-5.4"
:on-permission-request copilot/approve-all
:hooks
{:on-pre-tool-use
(fn [input invocation]
;; Called before each tool execution
;; input contains {:tool-name "..." :arguments {...}}
(println "About to use tool:" (:tool-name input))
;; Return nil to proceed, or a modified input map
nil)
:on-post-tool-use
(fn [input invocation]
;; Called after each *successful* tool execution
;; input contains {:tool-name "..." :tool-args {...} :tool-result {...}}
;; For failed tool calls, register :on-post-tool-use-failure below.
(println "Tool completed:" (:tool-name input))
nil)
:on-post-tool-use-failure
(fn [input invocation]
;; Called after a tool execution whose result was `"failure"`
;; (upstream PR #1421). :on-post-tool-use only fires for
;; successful results, so register this handler to observe
;; failed tool outcomes. Note: `"rejected"`, `"denied"`, and
;; `"timeout"` results do NOT currently trigger this hook —
;; only `"failure"` does.
;; input contains {:tool-name "..." :tool-args {...}
;; :error "failure message string"
;; :session-id "..." :timestamp 12345}
;; Optional return: {:additional-context "..."} is appended as
;; hidden guidance to the model alongside the failed result.
;; Other fields (e.g. :modified-result, :suppress-output) are
;; not honored for failure hooks.
(println "Tool failed:" (:tool-name input) (:error input))
{:additional-context "Tip: try `ls` first to see available files."})
:on-pre-mcp-tool-call
(fn [input invocation]
;; Called before each MCP tool call is dispatched (upstream PR #1366).
;; input contains
;; {:server-name "..." :tool-name "..." :arguments {...}
;; :tool-call-id "..." :_meta {...} :session-id "..."
;; :timestamp 12345}
;; :arguments and :_meta are opaque MCP payloads and are
;; passed through verbatim (NOT kebab-cased recursively).
;; Return nil/{} to preserve the existing _meta on the
;; outgoing MCP request, {:meta-to-use {...}} to replace
;; it, or {:meta-to-use nil} to remove it.
(println "Pre-MCP call:" (:server-name input) (:tool-name input))
{:meta-to-use {:traceId "my-trace-id"}})
:on-user-prompt-submitted
(fn [input invocation]
;; Called when user sends a prompt
(println "User prompt:" (:prompt input))
nil)
:on-user-prompt-transformed
(fn [input invocation]
;; Called after the runtime transforms a submitted prompt
;; (e.g. slash-command/skill expansion) but before it is sent
;; to the model (upstream PR #2254).
;; input contains {:prompt "..." :transformed-prompt "..."
;; :timestamp 12345 :cwd "..."}
;; Return {:modified-transformed-prompt "..."} (wire
;; modifiedTransformedPrompt) to further rewrite the prompt
;; actually sent to the model, or nil to leave it as-is.
(println "Transformed prompt:" (:transformed-prompt input))
nil)
:on-session-start
(fn [input invocation]
(println "Session started")
nil)
:on-session-end
(fn [input invocation]
(println "Session ended")
nil)
:on-agent-stop
(fn [{:keys [stop-hook-active]} _invocation]
(when-not stop-hook-active
{:decision "block"
:reason "Run the final validation and fix any failures."}))
:on-error-occurred
(fn [input invocation]
(println "Error:" (:error input))
nil)}}))All hooks receive an input map (contents vary by hook type) and an invocation map
containing {:session-id ...}. Hooks may return nil to proceed normally, or in some
cases return a modified value.
:on-agent-stop fires when the top-level agent reaches a natural terminal stop. Its
input contains base :timestamp (Unix milliseconds) and :cwd (string), SDK-added
:session-id, and optional kebab-cased :stop-reason, :transcript-path, and
:stop-hook-active; its invocation map is {:session-id ...}. Return
{:decision "block" :reason "..."} to keep the agent running and enqueue the reason.
Return nil, or throw from the handler, to let the agent stop. When
:stop-hook-active is true, a previous block already forced a continuation; use it to
avoid indefinite re-blocking.
(upstream PR #2054)
For models that support reasoning (like o1), you can control the reasoning effort level:
;; Check model capabilities
(let [models (copilot/list-models client)]
(doseq [m models
:when (:supports-reasoning-effort m)]
(println (:name m) "supports reasoning:"
(:supported-reasoning-efforts m)
"default:" (:default-reasoning-effort m))))
;; Create session with reasoning effort
(def session (copilot/create-session client
{:model "o1"
:reasoning-effort "high"
:on-permission-request copilot/approve-all})) ; "low", "medium", "high", "xhigh", or "max"(def session1 (copilot/create-session client {:model "gpt-5.4"
:on-permission-request copilot/approve-all}))
(def session2 (copilot/create-session client {:model "claude-sonnet-4.5"
:on-permission-request copilot/approve-all}))
;; Both sessions are independent
(copilot/send-and-wait! session1 {:prompt "Hello from session 1"})
(copilot/send-and-wait! session2 {:prompt "Hello from session 2"});; File attachment
(copilot/send! session
{:prompt "Analyze this file"
:attachments [{:type :file
:path "/path/to/file.clj"
:display-name "My File"}]})
;; File attachment with line range (restrict to lines 10-25)
(copilot/send! session
{:prompt "Explain this section"
:attachments [{:type :file
:path "/path/to/file.clj"
:line-range {:start 10 :end 25}}]})
;; Selection attachment (code range)
(copilot/send! session
{:prompt "What does this function do?"
:attachments [{:type :selection
:file-path "/path/to/file.clj"
:display-name "my-function"
:selection-range {:start {:line 10 :character 0}
:end {:line 25 :character 0}}
:text "(defn my-function [...] ...)"}]})Send inline base64-encoded data (e.g. images) without writing to disk:
;; Blob attachment (inline base64 data)
(copilot/send! session
{:prompt "Describe this image"
:attachments [{:type :blob
:data "iVBORw0KGgoAAAANSUhEUg..."
:mime-type "image/png"
:display-name "screenshot.png"}]});; Connect to an existing CLI server (no process spawned)
(def client (copilot/client {:cli-url "localhost:8080"}))
(copilot/start! client)(try
(let [session (copilot/create-session client
{:on-permission-request copilot/approve-all})]
(copilot/send! session {:prompt "Hello"}))
(catch Exception e
(println "Error:" (ex-message e))))