diff --git a/CHANGELOG.md b/CHANGELOG.md index 4208f5c..a9201f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Added (post-v0.2.2 sync) +- **`convert-mcp-call-tool-result`** — new public function in `tools` namespace that converts MCP `CallToolResult` format into the SDK's `ToolResultObject`. Handles text, image, and resource content types. (upstream PR #1049) +- **`default-join-session-permission-handler`** — new permission handler for `resume-session` that returns `{:kind :no-result}`, signaling the CLI to handle permissions itself. Sends `requestPermission: false` on the wire. (upstream PR #1056) +- **MCP config spec aliases** — `::mcp-stdio-server` and `::mcp-http-server` as aliases for `::mcp-local-server` and `::mcp-remote-server` respectively, matching upstream rename from Local→Stdio, Remote→HTTP. Old names kept for backward compatibility. (upstream PR #1051) +- **Per-agent skills field** — `::agent-skills` (vector of strings) on `::custom-agent` spec, allowing skill injection per custom agent. (upstream PR #995) +- **Memory permission event specs** — `::memory-action`, `::memory-direction`, `::memory-reason` specs for enriched memory permission request events. (CLI 1.0.22, upstream PR #1055) +- **New RPC wrappers** in `session` namespace (all experimental): + - `session-name-get`, `session-name-set!` — get/set session display name (CLI 1.0.26, upstream PR #1076) + - `workspace-get-workspace` — get current workspace metadata (CLI 1.0.26, upstream PR #1076) + - `mcp-discover` — discover MCP servers in a working directory (CLI 1.0.22, upstream PR #1055) + - `usage-get-metrics` — get session usage metrics (CLI 1.0.22, upstream PR #1055) +- Integration tests for all new features (18 tests covering convert-mcp-call-tool-result, spec renames, agent skills, requestPermission behavior, new RPCs, and memory specs) + +### Changed (post-v0.2.2 sync) +- **`requestPermission` on resume** — `resume-session` now sends `requestPermission: false` when using `default-join-session-permission-handler`, and `true` when using any other handler (e.g., `approve-all`). Previously always sent `true`. (upstream PR #1056) + ### Added (v0.2.2 sync) - **`enableConfigDiscovery` session option** — new boolean `:enable-config-discovery` on session and resume configs. Auto-discovers `.mcp.json`, `.vscode/mcp.json`, skills, etc. Instruction files are always loaded regardless. (upstream PR #1044) - **`modelCapabilities` override** — new `:model-capabilities` option on session config, resume config, and `switch-model!`/`set-model!`. Pass a partial capabilities map (e.g. `{:model-supports {:supports-vision true}}`) to override model capabilities for the session. (upstream PR #1029) diff --git a/doc/api/API.html b/doc/api/API.html index b108bd8..0240ac7 100644 --- a/doc/api/API.html +++ b/doc/api/API.html @@ -168,9 +168,9 @@

with-clien :available-tools vector List of allowed tool names :excluded-tools vector List of excluded tool names :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 - :mcp-servers map MCP server configs keyed by server ID (see MCP docs). Local servers: :mcp-command, :mcp-args, :mcp-tools. Remote servers: :mcp-server-type (:http/:sse), :mcp-url, :mcp-tools + :mcp-servers map MCP server configs keyed by server ID (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 :commands vector Command definitions (slash commands). See Commands - :custom-agents vector Custom agent configs + :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), :mcp-servers :on-permission-request fn Required. Permission handler function. Use copilot/approve-all to approve everything. :streaming? boolean Enable streaming deltas :config-dir string Override config directory for CLI @@ -186,6 +186,8 @@

with-clien :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 :create-session-fs-handler fn Factory for session filesystem handlers. Required when :session-fs is set on the client. Called as (factory session), returns a map of FS handler functions. See Session Filesystem + :enable-config-discovery boolean Auto-discover .mcp.json, .vscode/mcp.json, skills, etc. Instruction files always load regardless. (upstream PR #1044) + :model-capabilities map Model capabilities override. DeepPartial of model capabilities, e.g. {:model-supports {:supports-vision true}}. (upstream PR #1029)

resume-session

@@ -200,11 +202,16 @@

resume-session :disable-resume? boolean When true, skip emitting the session.resume event (default: false) +

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})
 

<create-session

(copilot/<create-session client config)
@@ -363,6 +370,26 @@ 

get-quota

:reset-date string (optional) ISO 8601 date when quota resets +

mcp-config-list / mcp-config-add! / mcp-config-update! / mcp-config-remove!

+
+

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"})
+

state

(copilot/state client)
 
@@ -578,8 +605,13 @@

get-current-mo

switch-model!

(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”) - :model-capabilities — Model capabilities override map, e.g. {:model-supports {:supports-vision true}}

set-model!

(copilot/set-model! session "claude-sonnet-4.5")
 ;; => "claude-sonnet-4.5"
@@ -649,6 +681,32 @@ 

Experi ;; 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

@@ -686,6 +744,60 @@

Experi

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

@@ -693,11 +805,64 @@

Experi

- + + +
session/plugins-list List plugins.
session/compaction-compact! Trigger manual context compaction.
session/compaction-compact! Trigger manual context compaction (uses session.history.compact RPC).
session/history-truncate! Trigger manual context truncation.
session/sessions-fork! Fork the current session.
session/shell-exec! Execute a shell command.
session/shell-kill! Kill a running shell process.
+

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)
+

UI Elicitation

Request structured user input via interactive dialogs. Check host support before calling.

@@ -808,7 +973,7 @@

Event Reference

:copilot/session.start Session created :copilot/session.resume Session resumed - :copilot/session.error Session error occurred + :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 @@ -1003,6 +1168,41 @@

Tools

(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)

Commands

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
@@ -1221,6 +1421,17 @@ 

Permission Handl :memory Memory storage operation (subject, fact, citations) +

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

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>}:

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
@@ -1254,6 +1465,15 @@ 

approve-all

Pass as the :on-permission-request value in session config:

(copilot/create-session client {:on-permission-request copilot/approve-all})
 
+

default-join-session-permission-handler

+
(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.

User Input Handling

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
diff --git a/doc/api/github.copilot-sdk.client.html b/doc/api/github.copilot-sdk.client.html
index b8f42c7..fc6262a 100644
--- a/doc/api/github.copilot-sdk.client.html
+++ b/doc/api/github.copilot-sdk.client.html
@@ -1,6 +1,6 @@
 
-github.copilot-sdk.client documentation

github.copilot-sdk.client

CopilotClient - manages connection to the Copilot CLI server.

+github.copilot-sdk.client documentation

github.copilot-sdk.client

CopilotClient - manages connection to the Copilot CLI server.

<create-session

(<create-session client config)

Async version of create-session. Returns a channel that delivers a CopilotSession.

Same config options as create-session (:on-permission-request is required). Validation is performed synchronously (throws immediately on invalid config). The RPC call parks instead of blocking, making this safe to use inside go blocks.

On RPC error, delivers an ExceptionInfo to the channel (not nil). Callers should check the result with (instance? Throwable result).

@@ -21,8 +21,13 @@

Options: - :cli-path - Path to CLI executable (default: “copilot”) - :cli-args - Extra arguments for CLI - :cli-url - URL of existing server (e.g., “localhost:8080”) - :cwd - Working directory for CLI process - :port - TCP port (default: 0 for random) - :use-stdio? - Use stdio transport (default: true) - :log-level - :none :error :warning :info :debug :all - :auto-start? - Auto-start on first use (default: true) - :auto-restart? - DEPRECATED: This option has no effect and will be removed in a future release. - :notification-queue-size - Max queued protocol notifications (default: 4096) - :router-queue-size - Max queued non-session notifications (default: 4096) - :tool-timeout-ms - Timeout for tool calls that return a channel (default: 120000) - :env - Environment variables map - :github-token - GitHub token for authentication (sets COPILOT_SDK_AUTH_TOKEN env var) - :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided) - :is-child-process? - When true, SDK is a child of an existing Copilot CLI process and uses stdio to communicate with it (no process spawning) - :on-list-models - Zero-arg fn returning a seq of model info maps; bypasses the RPC call and does not require start! - :telemetry - OpenTelemetry config map with optional keys :otlp-endpoint, :file-path, :exporter-type, :source-name, :capture-content? - :on-get-trace-context - Zero-arg fn returning {:traceparent … :tracestate …} for distributed trace propagation

connect-with-streams!

(connect-with-streams! client in out)

Connect to a server using pre-existing input/output streams. For testing purposes only.

create-session

(create-session client config)

Create a new conversation session.

-

Config options (:on-permission-request is required): - :on-permission-request - Permission handler function (required, e.g. approve-all) - :session-id - Custom session ID - :client-name - Client name to identify the application (included in User-Agent header) - :model - Model to use (e.g., “gpt-5.4”) - :tools - Vector of tool definitions - :commands - Vector of command definitions (slash commands for TUI) - :system-message - System message config - :available-tools - List of allowed tool names - :excluded-tools - List of excluded tool names - :provider - Custom provider config (BYOK) - :streaming? - Enable streaming - :mcp-servers - MCP server configs map - :custom-agents - Custom agent configs - :config-dir - Override config directory for CLI (configDir) - :skill-directories - Additional skill directories to load - :disabled-skills - Disable specific skills by name - :large-output - (Experimental) Tool output handling config {:enabled :max-size-bytes :output-dir} Note: CLI protocol feature, not in official SDK. outputDir may be ignored. - :working-directory - Working directory for the session (tool operations relative to this) - :infinite-sessions - Infinite session config for automatic context compaction {:enabled (default true) :background-compaction-threshold (0.0-1.0, default 0.80) :buffer-exhaustion-threshold (0.0-1.0, default 0.95)} - :reasoning-effort - Reasoning effort level: “low”, “medium”, “high”, or “xhigh” (PR #302) - :on-user-input-request - Handler for ask_user requests (PR #269) - :on-elicitation-request - Handler for elicitation requests from the agent (upstream PRs #908, #960). When provided, sends requestElicitation=true and enables the elicitation capability. Single-arg handler receives an ElicitationContext map with :session-id, :message, :requested-schema, :mode, :elicitation-source, :url. Returns an ElicitationResult map. - :hooks - Lifecycle hooks map (PR #269): {:on-pre-tool-use, :on-post-tool-use, :on-user-prompt-submitted, :on-session-start, :on-session-end, :on-error-occurred} - :on-event - Event handler (1-arg fn) registered before the RPC call. Guarantees early events like session.start are not missed.

+

Config options (:on-permission-request is required): - :on-permission-request - Permission handler function (required, e.g. approve-all) - :session-id - Custom session ID - :client-name - Client name to identify the application (included in User-Agent header) - :model - Model to use (e.g., “gpt-5.4”) - :tools - Vector of tool definitions - :commands - Vector of command definitions (slash commands for TUI) - :system-message - System message config - :available-tools - List of allowed tool names - :excluded-tools - List of excluded tool names - :provider - Custom provider config (BYOK) - :streaming? - Enable streaming - :mcp-servers - MCP server configs map - :custom-agents - Custom agent configs - :config-dir - Override config directory for CLI (configDir) - :skill-directories - Additional skill directories to load - :disabled-skills - Disable specific skills by name - :large-output - (Experimental) Tool output handling config {:enabled :max-size-bytes :output-dir} Note: CLI protocol feature, not in official SDK. outputDir may be ignored. - :working-directory - Working directory for the session (tool operations relative to this) - :infinite-sessions - Infinite session config for automatic context compaction {:enabled (default true) :background-compaction-threshold (0.0-1.0, default 0.80) :buffer-exhaustion-threshold (0.0-1.0, default 0.95)} - :reasoning-effort - Reasoning effort level: “low”, “medium”, “high”, or “xhigh” (PR #302) - :on-user-input-request - Handler for ask_user requests (PR #269) - :on-elicitation-request - Handler for elicitation requests from the agent (upstream PRs #908, #960). When provided, sends requestElicitation=true and enables the elicitation capability. Single-arg handler receives an ElicitationContext map with :session-id, :message, :requested-schema, :mode, :elicitation-source, :url. Returns an ElicitationResult map. - :hooks - Lifecycle hooks map (PR #269): {:on-pre-tool-use, :on-post-tool-use, :on-user-prompt-submitted, :on-session-start, :on-session-end, :on-error-occurred} - :on-event - Event handler (1-arg fn) registered before the RPC call. Guarantees early events like session.start are not missed. - :enable-config-discovery - Boolean. Auto-discover .mcp.json, .vscode/mcp.json, skills, etc. Instruction files are always loaded regardless. (upstream PR #1044) - :model-capabilities - Model capabilities override map (upstream PR #1029). DeepPartial of model capabilities, e.g. {:model-supports {:supports-vision true}}

Returns a CopilotSession.

+

default-join-session-permission-handler

(default-join-session-permission-handler _request _ctx)

Default permission handler for resuming sessions.

+

Returns {:kind :no-result} — the CLI handles permission decisions itself. When used with resume-session, tells the CLI that this client does NOT want to handle permission requests (requestPermission: false on the wire).

+

Use this when reconnecting to a session where the original client already established permission handling:

+

(copilot/resume-session client session-id {:on-permission-request copilot/default-join-session-permission-handler})

+

Equivalent to the upstream defaultJoinSessionPermissionHandler export.

delete-session!

(delete-session! client session-id)

Permanently deletes a session and all its data from disk, including conversation history, planning state, and artifacts. Unlike disconnect!, which only releases in-memory resources and preserves session data for later resumption, this method is irreversible.

force-stop!

(force-stop! client)

Force stop the CLI server without graceful cleanup.

get-auth-status

(get-auth-status client)

Get current authentication status. Returns {:authenticated? :auth-type :host :login :status-message}.

@@ -42,6 +47,10 @@

list-models

(list-models client)

List available models with their metadata. Results are cached per client connection to prevent rate limiting under concurrency. Cache is cleared on stop!/force-stop!. When :on-list-models handler is provided in client options, calls the handler instead of the RPC method. The handler does not require a CLI connection. Requires authentication (unless :on-list-models handler is provided). Returns a vector of model info maps with keys: :id :name :vendor :family :version :max-input-tokens :max-output-tokens :preview? :default-temperature :model-picker-priority :model-capabilities {:model-supports {:supports-vision :supports-reasoning-effort} :model-limits {:max-prompt-tokens :max-context-window-tokens :vision-capabilities {:supported-media-types :max-prompt-images :max-prompt-image-size}}} :model-policy {:policy-state :terms} :model-billing {:multiplier} :supported-reasoning-efforts :default-reasoning-effort :supports-reasoning-effort (legacy flat key) :vision-limits {:supported-media-types :max-prompt-images :max-prompt-image-size} (legacy)

list-sessions

(list-sessions client)(list-sessions client filter-opts)

List all available sessions. Returns a vector of session metadata maps. Optional filter map narrows results by context fields: {:cwd :git-root :repository :branch}

list-tools

(list-tools client)(list-tools client model)

List available tools with their metadata. Optional :model param returns model-specific tool overrides. Returns a vector of tool info maps with keys: :name :namespaced-name :description :parameters :instructions

+

mcp-config-add!

(mcp-config-add! client params)

Add an MCP server configuration. params is a map with server config using plain keys (:name, :command, :args, :tools, etc.) — NOT the :mcp-prefixed keys used in session config :mcp-servers.

+

mcp-config-list

(mcp-config-list client)

List all MCP server configurations. Returns a map with :servers (vector of server config maps).

+

mcp-config-remove!

(mcp-config-remove! client params)

Remove an MCP server configuration. params is a map identifying the server using plain keys (see mcp-config-add!).

+

mcp-config-update!

(mcp-config-update! client params)

Update an MCP server configuration. params is a map with server config using plain keys (see mcp-config-add!).

notifications

(notifications client)

Get the channel that receives non-session notifications. Notifications are dropped if the channel is full.

on-lifecycle-event

(on-lifecycle-event client handler)(on-lifecycle-event client event-type handler)

Subscribe to session lifecycle events.

Two arities: (on-lifecycle-event client handler) Subscribe to ALL lifecycle events. Handler receives the full event map with keys :lifecycle-event-type, :session-id, and optionally :metadata.

@@ -50,7 +59,7 @@

options

(options client)

Get the client options that were used to create this client. Returns the user-provided options merged with defaults. Note: This reflects SDK configuration, not necessarily server state.

ping

(ping client)(ping client message)

Ping the server to check connectivity. Returns {:message :timestamp :protocol-version}.

resume-session

(resume-session client session-id config)

Resume an existing session by ID.

-

Config options (:on-permission-request is required): - :on-permission-request - Permission handler function (required, e.g. approve-all) - :client-name - Client name to identify the application (included in User-Agent header) - :model - Change the model for the resumed session - :tools - Tools exposed to the CLI server - :system-message - System message configuration {:mode :content} - :available-tools - List of tool names to allow - :excluded-tools - List of tool names to disable - :provider - Custom provider configuration (BYOK) - :streaming? - Enable streaming responses - :mcp-servers - MCP server configurations - :custom-agents - Custom agent configurations - :config-dir - Override configuration directory - :skill-directories - Directories to load skills from - :disabled-skills - Skills to disable - :infinite-sessions - Infinite session configuration - :reasoning-effort - Reasoning effort level: “low”, “medium”, “high”, or “xhigh” - :on-user-input-request - Handler for ask_user requests - :on-elicitation-request - Handler for elicitation requests (upstream PRs #908, #960). Single-arg handler receives an ElicitationContext map with :session-id, :message, :requested-schema, :mode, :elicitation-source, :url. Returns an ElicitationResult map. - :hooks - Lifecycle hooks map - :on-event - Event handler (1-arg fn) registered before the RPC call. Guarantees early events like session.start are not missed.

+

Config options (:on-permission-request is required): - :on-permission-request - Permission handler function (required, e.g. approve-all) - :client-name - Client name to identify the application (included in User-Agent header) - :model - Change the model for the resumed session - :tools - Tools exposed to the CLI server - :system-message - System message configuration {:mode :content} - :available-tools - List of tool names to allow - :excluded-tools - List of tool names to disable - :provider - Custom provider configuration (BYOK) - :streaming? - Enable streaming responses - :mcp-servers - MCP server configurations - :custom-agents - Custom agent configurations - :config-dir - Override configuration directory - :skill-directories - Directories to load skills from - :disabled-skills - Skills to disable - :infinite-sessions - Infinite session configuration - :reasoning-effort - Reasoning effort level: “low”, “medium”, “high”, or “xhigh” - :on-user-input-request - Handler for ask_user requests - :on-elicitation-request - Handler for elicitation requests (upstream PRs #908, #960). Single-arg handler receives an ElicitationContext map with :session-id, :message, :requested-schema, :mode, :elicitation-source, :url. Returns an ElicitationResult map. - :hooks - Lifecycle hooks map - :on-event - Event handler (1-arg fn) registered before the RPC call. Guarantees early events like session.start are not missed. - :enable-config-discovery - Boolean. Auto-discover .mcp.json, skills, etc. (upstream PR #1044) - :model-capabilities - Model capabilities override map (upstream PR #1029).

Returns a CopilotSession.

set-foreground-session-id!

(set-foreground-session-id! client session-id)

Set the foreground session (TUI+server mode). Requests the TUI to switch to displaying the specified session.

start!

(start! client)

Start the CLI server and establish connection. Blocks until connected or throws on error.

diff --git a/doc/api/github.copilot-sdk.html b/doc/api/github.copilot-sdk.html index 21ea50c..0172123 100644 --- a/doc/api/github.copilot-sdk.html +++ b/doc/api/github.copilot-sdk.html @@ -1,6 +1,6 @@ -github.copilot-sdk documentation

github.copilot-sdk

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

+github.copilot-sdk documentation

github.copilot-sdk

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

Quick Start:

(require '[github.copilot-sdk :as copilot])
 
@@ -70,12 +70,14 @@
 
(when (copilot/confirm! session "Deploy to production?")
   (println "Deploying..."))
 
+

convert-mcp-call-tool-result

Convert an MCP CallToolResult into the SDK’s ToolResultObject format. See github.copilot-sdk.tools/convert-mcp-call-tool-result.

create-session

(create-session client config)

Create a new conversation session.

Config options (:on-permission-request is required): - :on-permission-request - Permission handler function (required, e.g. approve-all) - :session-id - Custom session ID - :model - Model to use (e.g., “gpt-5.4”, “claude-sonnet-4.5”) - :tools - Vector of tool definitions (use define-tool) - :system-message - {:mode :append/:replace :content “…”} - :available-tools - List of allowed tool names - :excluded-tools - List of excluded tool names - :provider - Custom provider config (BYOK) - :streaming? - Enable streaming deltas - :mcp-servers - MCP server configs map (keyed by server ID) - :custom-agents - Custom agent configs - :config-dir - Override config directory for CLI (configDir) - :skill-directories - Additional skill directories to load - :disabled-skills - Disable specific skills by name - :large-output - (Experimental) Tool output handling config {:enabled :max-size-bytes :output-dir} Note: CLI protocol feature, not in official SDK. outputDir may be ignored. - :working-directory - Working directory for the session (tool operations relative to this)

Example:

(def session (copilot/create-session client {:on-permission-request copilot/approve-all
                                              :model "gpt-5.4"}))
 
+

default-join-session-permission-handler

Default permission handler for resuming sessions. Returns {:kind :no-result} — the CLI handles permissions itself. When used with resume-session, sends requestPermission: false on the wire. See github.copilot-sdk.client/default-join-session-permission-handler.

define-tool

(define-tool name opts)

Define a tool with a handler function.

Arguments: - name - Tool name (string) - opts map: - :description - Tool description - :parameters - JSON schema for parameters - :handler - Function (fn args invocation -> result) - :overrides-built-in-tool - When true, overrides a built-in tool of the same name

The handler receives: - args - The parsed arguments from the LLM - invocation - Map with :session-id, :tool-call-id, :tool-name, :arguments

diff --git a/doc/api/github.copilot-sdk.session.html b/doc/api/github.copilot-sdk.session.html index 43922c8..72206de 100644 --- a/doc/api/github.copilot-sdk.session.html +++ b/doc/api/github.copilot-sdk.session.html @@ -1,14 +1,19 @@ -github.copilot-sdk.session documentation

github.copilot-sdk.session

CopilotSession - session operations using centralized client state.

+github.copilot-sdk.session documentation

github.copilot-sdk.session

CopilotSession - session operations using centralized client state.

All session state is stored in the client’s :state atom under: - :sessions session-id -> {:tool-handlers {} :permission-handler nil :destroyed? false :workspace-path nil} - :session-io session-id -> {:event-chan :event-mult}

Functions take client + session-id, accessing state through the client.

<send!

(<send! session opts)

Send a message and return a channel that delivers the final content string. This is the async equivalent of send-and-wait! - use inside go blocks.

Options: - :timeout-ms - Timeout in milliseconds (default: 300000, set to nil to disable)

The returned channel delivers a single value (the response content) then closes.

abort!

(abort! session)

Abort the currently processing message in this session.

+

agent-deselect!

(agent-deselect! session)

Deselect the current custom agent.

+

agent-get-current

(agent-get-current session)

Get the currently active custom agent for the session. Returns a map with :name (string or nil).

+

agent-list

(agent-list session)

List all custom agents available to the session. Returns a map with :agents (vector of agent info maps).

+

agent-reload!

(agent-reload! session)

Reload all custom agents.

+

agent-select!

(agent-select! session agent-name)

Select a custom agent by name.

capabilities

(capabilities session)

Get the host capabilities reported when the session was created or resumed. Returns a map, e.g. {:ui {:elicitation true}}.

-

compaction-compact!

(compaction-compact! session)

Trigger manual compaction of the session context.

+

compaction-compact!

(compaction-compact! session)

Trigger manual compaction of the session context. Note: renamed from session.compaction.compact to session.history.compact in upstream #1039.

config

(config session)

Get the session configuration that was used to create this session. Returns the user-provided config. Note: This reflects what was requested, not necessarily what the server is using. The session.start event contains the actual selectedModel if validation is needed.

confirm!

(confirm! session message)

Show a confirmation dialog and return the user’s boolean answer. Returns false if the user declines or cancels. Throws if the host does not support elicitation.

create-session

(create-session client session-id {:keys [tools on-permission-request on-user-input-request on-elicitation-request hooks workspace-path on-event config commands]})

Create a new session. Internal use - called by client. Initializes session state in client’s atom and returns a CopilotSession handle. If :on-event is provided, taps a subscriber that forwards events to the handler on a dedicated thread. Uses a sliding buffer, so events may be dropped under extreme backpressure if the handler cannot keep up with the event rate.

@@ -26,6 +31,7 @@

extensions-enable!

(extensions-enable! session extension-id)

Enable an extension by its source-qualified ID.

extensions-list

(extensions-list session)

List all extensions for the session. Returns a map with :extensions (vector of extension info maps).

extensions-reload!

(extensions-reload! session)

Reload all extensions.

+

fleet-start!

(fleet-start! session params)

Start a fleet of parallel sub-sessions. params is a map forwarded to the session.fleet.start RPC.

get-current-model

(get-current-model session)

Get the current model for this session. Returns the model ID string, or nil if none set.

get-messages

(get-messages session)

Get all events/messages from this session’s history.

handle-command-execute!

(handle-command-execute! client session-id {:keys [command-name command args]})

Handle an incoming command.execute event. Returns a channel with the result. Looks up the command handler by name and calls it with a context map. Returns {:result nil} on success or {:error message} on failure.

@@ -38,12 +44,19 @@

handle-tool-call!

(handle-tool-call! client session-id tool-call-id tool-name arguments & {:keys [traceparent tracestate]})

Handle an incoming tool call request. Returns a channel with the result wrapper.

handle-user-input-request!

(handle-user-input-request! client session-id request)

Handle an incoming user input request (ask_user). Returns a channel with the result. PR #269 feature.

The handler should return a map with :answer (string) and optionally :was-freeform (boolean). For backwards compatibility, :response is also accepted as an alias for :answer.

+

history-truncate!

(history-truncate! session)

Trigger manual truncation of the session context (upstream #1039).

input!

(input! session message)(input! session message opts)

Show a text input dialog. Returns the entered text, or nil if the user declines/cancels. opts is an optional map with :title, :description, :min-length, :max-length, :format, and :default keys. Throws if the host does not support elicitation.

log!

(log! session message)(log! session message opts)

Log a message to the session timeline. Options (optional map): - :level - “info”, “warning”, or “error” (default: “info”) - :ephemeral? - when true, message is not persisted to disk (default: false) Returns the event ID string.

mcp-disable!

(mcp-disable! session server-name)

Disable an MCP server by name.

+

mcp-discover

(mcp-discover session)(mcp-discover session opts)

Discover MCP servers in the working directory. opts is an optional map with :working-directory.

mcp-enable!

(mcp-enable! session server-name)

Enable an MCP server by name.

mcp-list

(mcp-list session)

List all MCP servers configured for the session. Returns a map with :servers (vector of server info maps).

mcp-reload!

(mcp-reload! session)

Reload all MCP servers.

+

mode-get

(mode-get session)

Get the current agent mode for the session. Returns a map with :mode (“interactive”, “plan”, or “autopilot”).

+

mode-set!

(mode-set! session mode)

Set the agent mode for the session. mode should be “interactive”, “plan”, or “autopilot”.

+

plan-delete!

(plan-delete! session)

Delete the plan file for the session.

+

plan-read

(plan-read session)

Read the plan file for the session. Returns a map with :exists? (boolean), :content (string or nil), and :file-path (string or nil).

+

plan-update!

(plan-update! session content)

Update the plan file content for the session.

plugins-list

(plugins-list session)

List all plugins for the session. Returns a map with :plugins (vector of plugin info maps).

register-transform-callbacks!

(register-transform-callbacks! client session-id callbacks)

Store system message transform callbacks on a session. Callbacks is a map of wire section ID strings to 1-arity functions that receive current content and return transformed content.

remove-session!

(remove-session! client session-id)

Remove a session from client state. Called on RPC failure during pre-registration.

@@ -57,6 +70,9 @@

Options: - :timeout-ms - Timeout in milliseconds (default: 300000, set to nil to disable)

send-async-with-id

(send-async-with-id session opts)

Send a message and return {:message-id :events-ch}.

session-id

(session-id session)

Get the session ID.

+

session-name-get

(session-name-get session)

Get the session name (or auto-generated summary). Returns a map with :name (string or nil).

+

session-name-set!

(session-name-set! session name)

Set the session name (1–100 characters).

+

sessions-fork!

(sessions-fork! session)

Fork the current session (upstream #1039). This is a server-scoped RPC.

set-capabilities!

(set-capabilities! client session-id capabilities)

Store host capabilities in session state. Called after session.create/session.resume RPC.

set-model!

(set-model! session model-id)(set-model! session model-id opts)

Alias for switch-model!. Matches the upstream SDK’s setModel() API. See switch-model! for details.

set-session-fs-handler!

(set-session-fs-handler! client session-id handler)

Store a sessionFs handler map on a session. Called by client during create/resume when :session-fs is enabled. Handler is a map of keyword→fn for FS operations.

@@ -72,10 +88,15 @@

Drop behavior: If this subscriber’s channel buffer is full when mult tries to deliver an event, that specific event is silently dropped for this subscriber only. Other subscribers with available buffer space still receive the event. The returned channel has a buffer of 1024 events which should be sufficient for most use cases.

This is a convenience wrapper around (tap (events session) ch).

switch-model!

(switch-model! session model-id)(switch-model! session model-id opts)

Switch the model for this session. The new model takes effect for the next message. Conversation history is preserved.

-

Optional opts map: - :reasoning-effort - Reasoning effort level for the new model (“low”, “medium”, “high”, “xhigh”)

+

Optional opts map: - :reasoning-effort - Reasoning effort level for the new model (“low”, “medium”, “high”, “xhigh”) - :model-capabilities - Model capabilities override map (upstream PR #1029) e.g. {:model-supports {:supports-vision true}}

Returns the new model ID string, or nil.

ui-elicitation!

(ui-elicitation! session params)

Request structured user input via an elicitation prompt. params is a map with :message and :requested-schema keys. Throws if the host does not support elicitation.

unsubscribe-events

(unsubscribe-events session ch)

Unsubscribe a channel from session events.

update-capabilities!

(update-capabilities! client session-id capability-changes)

Deep-merge capability changes into the session’s capabilities map. Called when a capabilities.changed broadcast event is received.

+

usage-get-metrics

(usage-get-metrics session)

Get usage metrics for the session.

+

workspace-create-file!

(workspace-create-file! session path content)

Create a file in the session workspace. path is relative to the workspace files directory.

+

workspace-get-workspace

(workspace-get-workspace session)

Get current workspace metadata. Returns a map with :workspace (map or nil).

+

workspace-list-files

(workspace-list-files session)

List files in the session workspace directory. Returns a map with :files (vector of relative file paths).

workspace-path

(workspace-path session)

Get the session workspace path when provided by the CLI.

+

workspace-read-file

(workspace-read-file session path)

Read a file from the session workspace. path is relative to the workspace files directory. Returns a map with :content (string).

\ No newline at end of file diff --git a/doc/api/github.copilot-sdk.tools.html b/doc/api/github.copilot-sdk.tools.html index 0c5dffe..973ba97 100644 --- a/doc/api/github.copilot-sdk.tools.html +++ b/doc/api/github.copilot-sdk.tools.html @@ -1,7 +1,11 @@ -github.copilot-sdk.tools documentation

github.copilot-sdk.tools

Helper functions for defining tools.

-

define-tool

(define-tool name {:keys [description parameters handler overrides-built-in-tool]})

Define a tool with a handler function.

+github.copilot-sdk.tools documentation

github.copilot-sdk.tools

Helper functions for defining tools.

+

convert-mcp-call-tool-result

(convert-mcp-call-tool-result {:keys [content is-error]})

Convert an MCP CallToolResult into the SDK’s ToolResultObject format.

+

The input map should have Clojure-idiomatic keys: - :content - vector of content blocks, each with :type and type-specific fields - :is-error - optional boolean, when true the result-type is “failure”

+

Content block types: - {:type “text” :text “…”} - {:type “image” :data “base64…” :mime-type “image/png”} - {:type “resource” :resource {:uri “…” :text “…” :blob “…” :mime-type “…”}}

+

Returns a ToolResultObject map with :text-result-for-llm, :result-type, and optionally :binary-results-for-llm.

+

define-tool

(define-tool name {:keys [description parameters handler overrides-built-in-tool]})

Define a tool with a handler function.

Arguments: - name - Tool name (string) - opts map: - :description - Tool description - :parameters - JSON schema for parameters (or nil) - :handler - Function (fn args invocation -> result) - :overrides-built-in-tool - When true, explicitly overrides a built-in tool of the same name. Without this flag, name clashes with built-in tools cause an error.

The handler receives: - args - The parsed arguments from the LLM (no key conversion) - invocation - Map with :session-id, :tool-call-id, :tool-name, :arguments

The handler should return one of: - A string (treated as success) - A map with :text-result-for-llm and :result-type - Any other value (JSON-encoded as success) - A core.async channel that will yield one of the above

diff --git a/doc/reference/API.md b/doc/reference/API.md index e8aed62..0e18725 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -238,9 +238,9 @@ Create a client and session together, ensuring both are cleaned up on exit. | `:available-tools` | vector | List of allowed tool names | | `:excluded-tools` | vector | List of excluded tool names | | `:provider` | map | Provider config for BYOK (see [BYOK docs](../auth/byok.md)). Required key: `:base-url`. Optional: `:provider-type` (`:openai`/`:azure`/`:anthropic`), `:wire-api` (`:completions`/`:responses`), `:api-key`, `:bearer-token`, `:azure-options` | -| `:mcp-servers` | map | MCP server configs keyed by server ID (see [MCP docs](../mcp/overview.md)). Local servers: `:mcp-command`, `:mcp-args`, `:mcp-tools`. Remote servers: `:mcp-server-type` (`:http`/`:sse`), `:mcp-url`, `:mcp-tools` | +| `:mcp-servers` | map | MCP server configs keyed by server ID (see [MCP docs](../mcp/overview.md)). 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` | | `:commands` | vector | Command definitions (slash commands). See [Commands](#commands) | -| `:custom-agents` | vector | Custom agent configs | +| `: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), `:mcp-servers` | | `:on-permission-request` | fn | **Required.** Permission handler function. Use `copilot/approve-all` to approve everything. | | `:streaming?` | boolean | Enable streaming deltas | | `:config-dir` | string | Override config directory for CLI | @@ -271,12 +271,18 @@ Resume an existing session by ID. The `config` map accepts the same options as ` |---|---|---| | `:disable-resume?` | boolean | When true, skip emitting the session.resume event (default: false) | +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`. + ```clojure ;; 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}) ``` #### ` {: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 {...}}`. | + +```clojure +(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`. | + +```clojure +(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. | + +```clojure +(session/usage-get-metrics my-session) +``` + --- ## UI Elicitation @@ -1440,6 +1493,39 @@ Set `:overrides-built-in-tool true` to override a built-in tool (e.g., `grep`, ` (copilot/result-rejected "Invalid parameters") ``` +**MCP result conversion:** + +Convert an MCP `CallToolResult` into the SDK's `ToolResultObject` format with `convert-mcp-call-tool-result`: + +```clojure +(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) | + ### Commands 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. @@ -1705,6 +1791,14 @@ The `:permission-kind` field in permission requests identifies the type of actio | `:custom-tool` | SDK-registered custom tool invocation | | `:memory` | Memory storage operation (subject, fact, citations) | +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 | + 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 @@ -1762,6 +1856,23 @@ Pass as the `:on-permission-request` value in session config: (copilot/create-session client {:on-permission-request copilot/approve-all}) ``` +#### `default-join-session-permission-handler` + +```clojure +(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: + +```clojure +(copilot/resume-session client "session-123" + {:on-permission-request copilot/default-join-session-permission-handler}) +``` + +Equivalent to the upstream Node.js SDK `defaultJoinSessionPermissionHandler` export. + ### User Input Handling When the agent needs input from the user (via `ask_user` tool), the `:on-user-input-request` diff --git a/src/github/copilot_sdk.clj b/src/github/copilot_sdk.clj index 532fcdd..3e74b4e 100644 --- a/src/github/copilot_sdk.clj +++ b/src/github/copilot_sdk.clj @@ -1071,8 +1071,19 @@ (def result-failure tools/result-failure) (def result-denied tools/result-denied) (def result-rejected tools/result-rejected) +(def convert-mcp-call-tool-result + "Convert an MCP CallToolResult into the SDK's ToolResultObject format. + See `github.copilot-sdk.tools/convert-mcp-call-tool-result`." + tools/convert-mcp-call-tool-result) ;; Re-export permission helpers (def approve-all "Permission handler that approves all requests. See `github.copilot-sdk.client/approve-all`." client/approve-all) + +(def default-join-session-permission-handler + "Default permission handler for resuming sessions. + Returns `{:kind :no-result}` — the CLI handles permissions itself. + When used with `resume-session`, sends `requestPermission: false` on the wire. + See `github.copilot-sdk.client/default-join-session-permission-handler`." + client/default-join-session-permission-handler) diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index 07b913d..13ba252 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -802,6 +802,23 @@ [_request _ctx] {:kind :approved}) +(defn default-join-session-permission-handler + "Default permission handler for resuming sessions. + + Returns `{:kind :no-result}` — the CLI handles permission decisions itself. + When used with `resume-session`, tells the CLI that this client does NOT + want to handle permission requests (`requestPermission: false` on the wire). + + Use this when reconnecting to a session where the original client already + established permission handling: + + (copilot/resume-session client session-id + {:on-permission-request copilot/default-join-session-permission-handler}) + + Equivalent to the upstream `defaultJoinSessionPermissionHandler` export." + [_request _ctx] + {:kind :no-result}) + (defn start! "Start the CLI server and establish connection. Blocks until connected or throws on error. @@ -1426,7 +1443,9 @@ (:available-tools config) (assoc :available-tools (:available-tools config)) (:excluded-tools config) (assoc :excluded-tools (:excluded-tools config)) wire-provider (assoc :provider wire-provider) - true (assoc :request-permission true) + true (assoc :request-permission + (not (identical? (:on-permission-request config) + default-join-session-permission-handler))) (:streaming? config) (assoc :streaming (:streaming? config)) wire-mcp-servers (assoc :mcp-servers wire-mcp-servers) wire-custom-agents (assoc :custom-agents wire-custom-agents) @@ -1759,7 +1778,7 @@ (let [c (client {:is-child-process? true}) merged-config (cond-> config (not (contains? config :on-permission-request)) - (assoc :on-permission-request (constantly {:kind :no-result})) + (assoc :on-permission-request default-join-session-permission-handler) (not (contains? config :disable-resume?)) (assoc :disable-resume? true))] (try diff --git a/src/github/copilot_sdk/instrument.clj b/src/github/copilot_sdk/instrument.clj index 387c9ea..f5873af 100644 --- a/src/github/copilot_sdk/instrument.clj +++ b/src/github/copilot_sdk/instrument.clj @@ -422,6 +422,35 @@ :args (s/cat :session ::specs/session :params map?) :ret map?) +;; Session name RPC function specs (upstream CLI 1.0.26, PR #1076) +(s/fdef github.copilot-sdk.session/session-name-get + :args (s/cat :session ::specs/session) + :ret map?) + +(s/fdef github.copilot-sdk.session/session-name-set! + :args (s/cat :session ::specs/session :name string?) + :ret map?) + +;; Workspace extended RPC function specs (upstream CLI 1.0.26, PR #1076) +(s/fdef github.copilot-sdk.session/workspace-get-workspace + :args (s/cat :session ::specs/session) + :ret map?) + +;; MCP discovery RPC function spec (upstream CLI 1.0.22, PR #1055) +(s/fdef github.copilot-sdk.session/mcp-discover + :args (s/cat :session ::specs/session :opts (s/? map?)) + :ret map?) + +;; Usage metrics RPC function spec (upstream CLI 1.0.22, PR #1055) +(s/fdef github.copilot-sdk.session/usage-get-metrics + :args (s/cat :session ::specs/session) + :ret map?) + +;; convert-mcp-call-tool-result function spec (upstream PR #1049) +(s/fdef github.copilot-sdk.tools/convert-mcp-call-tool-result + :args (s/cat :call-result map?) + :ret ::specs/tool-result-object) + ;; Client-level MCP config function specs (s/fdef github.copilot-sdk.client/mcp-config-list :args (s/cat :client ::specs/client) @@ -518,6 +547,12 @@ github.copilot-sdk.session/agent-deselect! github.copilot-sdk.session/agent-reload! github.copilot-sdk.session/fleet-start! + github.copilot-sdk.session/session-name-get + github.copilot-sdk.session/session-name-set! + github.copilot-sdk.session/workspace-get-workspace + github.copilot-sdk.session/mcp-discover + github.copilot-sdk.session/usage-get-metrics + github.copilot-sdk.tools/convert-mcp-call-tool-result github.copilot-sdk.client/mcp-config-list github.copilot-sdk.client/mcp-config-add! github.copilot-sdk.client/mcp-config-update! @@ -612,6 +647,12 @@ github.copilot-sdk.session/agent-deselect! github.copilot-sdk.session/agent-reload! github.copilot-sdk.session/fleet-start! + github.copilot-sdk.session/session-name-get + github.copilot-sdk.session/session-name-set! + github.copilot-sdk.session/workspace-get-workspace + github.copilot-sdk.session/mcp-discover + github.copilot-sdk.session/usage-get-metrics + github.copilot-sdk.tools/convert-mcp-call-tool-result github.copilot-sdk.client/mcp-config-list github.copilot-sdk.client/mcp-config-add! github.copilot-sdk.client/mcp-config-update! diff --git a/src/github/copilot_sdk/session.clj b/src/github/copilot_sdk/session.clj index 5c89247..e3c84f1 100644 --- a/src/github/copilot_sdk/session.clj +++ b/src/github/copilot_sdk/session.clj @@ -1292,6 +1292,63 @@ (proto/send-request! conn "session.fleet.start" (assoc (merge {} params) :session-id session-id))))) +;; -- Session Name ----------------------------------------------------------- + +(defn ^:experimental session-name-get + "Get the session name (or auto-generated summary). + Returns a map with :name (string or nil)." + [session] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "session.name.get" {:session-id session-id})))) + +(defn ^:experimental session-name-set! + "Set the session name (1–100 characters)." + [session name] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "session.name.set" + {:session-id session-id :name name})))) + +;; -- Workspace (extended) --------------------------------------------------- + +(defn ^:experimental workspace-get-workspace + "Get current workspace metadata. Returns a map with :workspace (map or nil)." + [session] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "session.workspaces.getWorkspace" + {:session-id session-id})))) + +;; -- MCP Discovery ---------------------------------------------------------- + +(defn ^:experimental mcp-discover + "Discover MCP servers in the working directory. + opts is an optional map with :working-directory." + ([session] (mcp-discover session {})) + ([session opts] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "mcp.discover" + (cond-> {} + (:working-directory opts) + (assoc :working-directory (:working-directory opts)))))))) + +;; -- Usage Metrics ---------------------------------------------------------- + +(defn ^:experimental usage-get-metrics + "Get usage metrics for the session." + [session] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "session.usage.getMetrics" + {:session-id session-id})))) + ;; -- UI Elicitation ---------------------------------------------------------- (defn capabilities diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index c947dd8..39819a5 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -207,6 +207,10 @@ (s/keys :req-un [::mcp-server-type ::mcp-url ::mcp-tools] :opt-un [::mcp-timeout ::mcp-headers])) +;; New canonical names (upstream PR #1051) +(s/def ::mcp-stdio-server ::mcp-local-server) +(s/def ::mcp-http-server ::mcp-remote-server) + (s/def ::mcp-server (s/or :local ::mcp-local-server :remote ::mcp-remote-server)) (s/def ::mcp-servers (s/map-of #(or (keyword? %) (string? %)) ::mcp-server)) @@ -220,11 +224,12 @@ (s/def ::agent-tools (s/nilable (s/coll-of string?))) (s/def ::agent-prompt ::non-blank-string) (s/def ::agent-infer? boolean?) +(s/def ::agent-skills (s/coll-of string?)) (s/def ::custom-agent (s/keys :req-un [::agent-name ::agent-prompt] :opt-un [::agent-display-name ::agent-description ::agent-tools - ::mcp-servers ::agent-infer?])) + ::mcp-servers ::agent-infer? ::agent-skills])) (s/def ::custom-agents (s/coll-of ::custom-agent)) @@ -824,6 +829,12 @@ (s/def ::session-log string?) (s/def ::tool-telemetry map?) +;; Binary result items for tool results (upstream ToolBinaryResult) +;; Each item has :data (base64 string), :mime-type, :type ("image"/"resource"), +;; and optional :description. Uses map? to avoid conflicts with existing specs +;; for ::type (attachment-specific) — binary result items have different semantics. +(s/def ::binary-results-for-llm (s/coll-of map?)) + (s/def ::tool-result-object (s/keys :req-un [::text-result-for-llm ::result-type] :opt-un [::binary-results-for-llm ::error ::session-log ::tool-telemetry])) @@ -838,9 +849,14 @@ (s/def ::permission-kind #{:shell :write :mcp :read :url :custom-tool :memory}) +;; Memory permission event data fields (CLI 1.0.22, upstream PR #1055) +(s/def ::memory-action #{:store :vote}) +(s/def ::memory-direction #{:upvote :downvote}) +(s/def ::memory-reason string?) + (s/def ::permission-request (s/keys :req-un [::permission-kind] - :opt-un [::tool-call-id])) + :opt-un [::tool-call-id ::memory-action ::memory-direction ::memory-reason])) (s/def ::permission-result-kind #{:approved diff --git a/src/github/copilot_sdk/tools.clj b/src/github/copilot_sdk/tools.clj index 215bcb6..1e283d6 100644 --- a/src/github/copilot_sdk/tools.clj +++ b/src/github/copilot_sdk/tools.clj @@ -1,6 +1,7 @@ (ns github.copilot-sdk.tools "Helper functions for defining tools." - (:require [clojure.spec.alpha :as s])) + (:require [clojure.spec.alpha :as s] + [clojure.string :as str])) (defn define-tool "Define a tool with a handler function. @@ -125,3 +126,51 @@ {:text-result-for-llm text :result-type "rejected" :tool-telemetry telemetry})) + +(defn convert-mcp-call-tool-result + "Convert an MCP CallToolResult into the SDK's ToolResultObject format. + + The input map should have Clojure-idiomatic keys: + - :content - vector of content blocks, each with :type and type-specific fields + - :is-error - optional boolean, when true the result-type is \"failure\" + + Content block types: + - {:type \"text\" :text \"...\"} + - {:type \"image\" :data \"base64...\" :mime-type \"image/png\"} + - {:type \"resource\" :resource {:uri \"...\" :text \"...\" :blob \"...\" :mime-type \"...\"}} + + Returns a ToolResultObject map with :text-result-for-llm, :result-type, and + optionally :binary-results-for-llm." + [{:keys [content is-error]}] + (let [text-parts (transient []) + binary-results (transient [])] + (doseq [block content] + (case (:type block) + "text" + (when (string? (:text block)) + (conj! text-parts (:text block))) + + "image" + (when (and (string? (:data block)) + (seq (:data block)) + (string? (:mime-type block))) + (conj! binary-results {:data (:data block) + :mime-type (:mime-type block) + :type "image"})) + + "resource" + (let [resource (:resource block)] + (when (:text resource) + (conj! text-parts (:text resource))) + (when (:blob resource) + (conj! binary-results {:data (:blob resource) + :mime-type (or (:mime-type resource) "application/octet-stream") + :type "resource" + :description (:uri resource)}))) + + ;; Unknown content type — skip + nil)) + (let [binaries (persistent! binary-results)] + (cond-> {:text-result-for-llm (str/join "\n" (persistent! text-parts)) + :result-type (if is-error "failure" "success")} + (seq binaries) (assoc :binary-results-for-llm binaries))))) diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj index 7d54ddb..0c58c87 100644 --- a/test/github/copilot_sdk/integration_test.clj +++ b/test/github/copilot_sdk/integration_test.clj @@ -8,6 +8,7 @@ [github.copilot-sdk.client :as client] [github.copilot-sdk.protocol :as protocol] [github.copilot-sdk.session :as session] + [github.copilot-sdk.tools :as tools] [github.copilot-sdk.mock-server :as mock])) ;; Fixture to manage mock server lifecycle @@ -2293,3 +2294,231 @@ {:on-permission-request sdk/approve-all})] (session/sessions-fork! session) (is (some #(= "sessions.fork" (:method %)) @requests))))) + +;; ============================================================================= +;; Post-v0.2.2 upstream sync tests +;; ============================================================================= + +;; --- convert-mcp-call-tool-result (upstream PR #1049) ----------------------- + +(deftest test-convert-mcp-call-tool-result-text + (testing "converts text content blocks to textResultForLlm" + (let [result (tools/convert-mcp-call-tool-result + {:content [{:type "text" :text "Hello"} + {:type "text" :text "World"}]})] + (is (= "Hello\nWorld" (:text-result-for-llm result))) + (is (= "success" (:result-type result))) + (is (nil? (:binary-results-for-llm result)))))) + +(deftest test-convert-mcp-call-tool-result-image + (testing "converts image content blocks to binaryResultsForLlm" + (let [result (tools/convert-mcp-call-tool-result + {:content [{:type "image" + :data "base64data" + :mime-type "image/png"}]})] + (is (= "" (:text-result-for-llm result))) + (is (= "success" (:result-type result))) + (is (= 1 (count (:binary-results-for-llm result)))) + (is (= "base64data" (:data (first (:binary-results-for-llm result))))) + (is (= "image/png" (:mime-type (first (:binary-results-for-llm result))))) + (is (= "image" (:type (first (:binary-results-for-llm result)))))))) + +(deftest test-convert-mcp-call-tool-result-resource + (testing "converts resource content blocks with text and blob" + (let [result (tools/convert-mcp-call-tool-result + {:content [{:type "resource" + :resource {:uri "file:///test.txt" + :text "file content" + :blob "blobdata" + :mime-type "text/plain"}}]})] + (is (= "file content" (:text-result-for-llm result))) + (is (= 1 (count (:binary-results-for-llm result)))) + (is (= "blobdata" (:data (first (:binary-results-for-llm result))))) + (is (= "text/plain" (:mime-type (first (:binary-results-for-llm result))))) + (is (= "file:///test.txt" (:description (first (:binary-results-for-llm result)))))))) + +(deftest test-convert-mcp-call-tool-result-error + (testing "isError maps to failure result-type" + (let [result (tools/convert-mcp-call-tool-result + {:content [{:type "text" :text "something failed"}] + :is-error true})] + (is (= "failure" (:result-type result))) + (is (= "something failed" (:text-result-for-llm result)))))) + +(deftest test-convert-mcp-call-tool-result-mixed + (testing "mixed content types are handled correctly" + (let [result (tools/convert-mcp-call-tool-result + {:content [{:type "text" :text "preamble"} + {:type "image" :data "img" :mime-type "image/jpeg"} + {:type "resource" :resource {:uri "f" :text "res-text"}}]})] + (is (= "preamble\nres-text" (:text-result-for-llm result))) + (is (= 1 (count (:binary-results-for-llm result))))))) + +(deftest test-convert-mcp-call-tool-result-empty + (testing "empty content array produces empty result" + (let [result (tools/convert-mcp-call-tool-result {:content []})] + (is (= "" (:text-result-for-llm result))) + (is (= "success" (:result-type result))) + (is (nil? (:binary-results-for-llm result)))))) + +;; --- MCP config spec renames (upstream PR #1051) ---------------------------- + +(deftest test-mcp-stdio-server-spec + (testing "::mcp-stdio-server spec validates local/stdio configs" + (is (s/valid? :github.copilot-sdk.specs/mcp-stdio-server + {:mcp-command "node" :mcp-args ["server.js"] :mcp-tools ["read"]})) + (is (s/valid? :github.copilot-sdk.specs/mcp-stdio-server + {:mcp-command "node" :mcp-args ["server.js"] :mcp-tools ["read"] + :mcp-server-type :stdio})))) + +(deftest test-mcp-http-server-spec + (testing "::mcp-http-server spec validates remote/http configs" + (is (s/valid? :github.copilot-sdk.specs/mcp-http-server + {:mcp-server-type :http :mcp-url "https://example.com" :mcp-tools ["*"]})) + (is (s/valid? :github.copilot-sdk.specs/mcp-http-server + {:mcp-server-type :sse :mcp-url "https://example.com" :mcp-tools ["*"]})))) + +;; --- Per-agent skills field (upstream PR #995) ------------------------------ + +(deftest test-custom-agent-skills-spec + (testing "::custom-agent spec accepts optional :agent-skills field" + (is (s/valid? :github.copilot-sdk.specs/custom-agent + {:agent-name "test" :agent-prompt "You are helpful"})) + (is (s/valid? :github.copilot-sdk.specs/custom-agent + {:agent-name "test" :agent-prompt "You are helpful" + :agent-skills ["skill-a" "skill-b"]})))) + +(deftest test-custom-agent-skills-on-wire + (testing "skills field is sent on wire in session.create" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :custom-agents [{:agent-name "test-agent" + :agent-prompt "Hello" + :agent-skills ["my-skill"]}]}) + create-params (get @seen "session.create") + agent (first (:customAgents create-params))] + (is (= ["my-skill"] (:agentSkills agent)))))) + +;; --- requestPermission behavioral change (upstream PR #1056) ---------------- + +(deftest test-request-permission-true-on-create + (testing "session.create always sends requestPermission: true" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all}) + create-params (get @seen "session.create")] + (is (true? (:requestPermission create-params)))))) + +(deftest test-request-permission-false-on-resume-without-handler + (testing "session.resume sends requestPermission: false with default handler" + (let [seen (atom {}) + session-id (sdk/session-id (sdk/create-session *test-client* {:on-permission-request sdk/approve-all})) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/resume-session *test-client* session-id + {:on-permission-request sdk/default-join-session-permission-handler}) + resume-params (get @seen "session.resume")] + (is (false? (:requestPermission resume-params)))))) + +(deftest test-request-permission-true-on-resume-with-handler + (testing "session.resume sends requestPermission: true when handler provided" + (let [seen (atom {}) + session-id (sdk/session-id (sdk/create-session *test-client* {:on-permission-request sdk/approve-all})) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/resume-session *test-client* session-id + {:on-permission-request sdk/approve-all}) + resume-params (get @seen "session.resume")] + (is (true? (:requestPermission resume-params)))))) + +;; --- New RPC wrappers (CLI 1.0.22 / 1.0.26) -------------------------------- + +(deftest test-session-name-get + (testing "session-name-get calls session.name.get RPC" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/session-name-get session) + (is (some #(= "session.name.get" (:method %)) @requests))))) + +(deftest test-session-name-set + (testing "session-name-set! calls session.name.set RPC with name param" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/session-name-set! session "My Session") + (let [req (first (filter #(= "session.name.set" (:method %)) @requests))] + (is (some? req)) + (is (= "My Session" (get-in req [:params :name]))))))) + +(deftest test-workspace-get-workspace + (testing "workspace-get-workspace calls session.workspaces.getWorkspace RPC" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/workspace-get-workspace session) + (is (some #(= "session.workspaces.getWorkspace" (:method %)) @requests))))) + +(deftest test-mcp-discover + (testing "mcp-discover calls mcp.discover RPC" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/mcp-discover session) + (is (some #(= "mcp.discover" (:method %)) @requests))))) + +(deftest test-mcp-discover-with-working-directory + (testing "mcp-discover forwards working-directory param" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/mcp-discover session {:working-directory "/tmp"}) + (let [req (first (filter #(= "mcp.discover" (:method %)) @requests))] + (is (some? req)) + (is (= "/tmp" (get-in req [:params :workingDirectory]))))))) + +(deftest test-usage-get-metrics + (testing "usage-get-metrics calls session.usage.getMetrics RPC" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/usage-get-metrics session) + (is (some #(= "session.usage.getMetrics" (:method %)) @requests))))) + +;; --- Memory permission event data specs (CLI 1.0.22) ----------------------- + +(deftest test-memory-permission-event-specs + (testing "memory action/direction/reason specs exist and validate" + (is (s/valid? :github.copilot-sdk.specs/memory-action :store)) + (is (s/valid? :github.copilot-sdk.specs/memory-action :vote)) + (is (not (s/valid? :github.copilot-sdk.specs/memory-action :invalid))) + (is (s/valid? :github.copilot-sdk.specs/memory-direction :upvote)) + (is (s/valid? :github.copilot-sdk.specs/memory-direction :downvote)) + (is (s/valid? :github.copilot-sdk.specs/memory-reason "some reason")))) diff --git a/test/github/copilot_sdk/mock_server.clj b/test/github/copilot_sdk/mock_server.clj index d57abcd..70ab861 100644 --- a/test/github/copilot_sdk/mock_server.clj +++ b/test/github/copilot_sdk/mock_server.clj @@ -356,6 +356,11 @@ "mcp.config.add" {:success true} "mcp.config.update" {:success true} "mcp.config.remove" {:success true} + "session.name.get" {:name nil} + "session.name.set" {:success true} + "session.workspaces.getWorkspace" {:workspace nil} + "mcp.discover" {:servers []} + "session.usage.getMetrics" {} (throw (ex-info "Method not found" {:code -32601 :method method}))) ;; Merge hook-provided data into result only when hook returns ::merge-response ;; This prevents accidental response mutation from spy hooks (e.g. swap! return values)