Copilot SDK Clojure Examples
June 3, 2026 · View on GitHub
This directory contains example applications demonstrating various features of the Copilot SDK for Clojure.
Prerequisites
-
Copilot CLI: Ensure the GitHub Copilot CLI is installed and accessible in your PATH.
which copilot # Or set COPILOT_CLI_PATH to your CLI location -
Dependencies: The examples use the
:examplesalias fromdeps.edn.
Running Examples
All examples use Clojure's -X invocation, which allows passing parameters directly.
From the project root:
# Basic Q&A conversation
clojure -A:examples -X basic-chat/run
# With custom questions
clojure -A:examples -X basic-chat/run :q1 '"What is Clojure?"' :q2 '"Who created it?"'
# Simple stateless query (helpers API)
clojure -A:examples -X helpers-query/run
clojure -A:examples -X helpers-query/run :prompt '"Explain recursion briefly."'
# Multiple independent queries
clojure -A:examples -X helpers-query/run-multi
clojure -A:examples -X helpers-query/run-multi :questions '["What is Rust?" "What is Go?"]'
# Streaming output
clojure -A:examples -X helpers-query/run-streaming
# Custom tool integration
clojure -A:examples -X tool-integration/run
clojure -A:examples -X tool-integration/run :languages '["clojure" "haskell"]'
# Multi-agent orchestration
clojure -A:examples -X multi-agent/run
clojure -A:examples -X multi-agent/run :topics '["AI safety" "machine learning"]'
# Config directory, skills, and large output
clojure -A:examples -X config-skill-output/run
# Metadata API (list-tools, get-quota, model switching)
clojure -A:examples -X metadata-api/run
# Permission handling
clojure -A:examples -X permission-bash/run
# Session state events monitoring
clojure -A:examples -X session-events/run
# User input handling (ask_user)
clojure -A:examples -X user-input/run
clojure -A:examples -X user-input/run-simple
# Ask user cancellation (simulates Esc)
clojure -A:examples -X ask-user-failure/run
# BYOK provider (requires API key, see example docs)
OPENAI_API_KEY=sk-... clojure -A:examples -X byok-provider/run
clojure -A:examples -X byok-provider/run :provider-name '"ollama"'
# MCP local server (requires npx/Node.js)
clojure -A:examples -X mcp-local-server/run
clojure -A:examples -X mcp-local-server/run-with-custom-tools
# File attachments
clojure -A:examples -X file-attachments/run
# Session resume
clojure -A:examples -X session-resume/run
# Infinite sessions (context compaction)
clojure -A:examples -X infinite-sessions/run
# Lifecycle hooks
clojure -A:examples -X lifecycle-hooks/run
# Reasoning effort
clojure -A:examples -X reasoning-effort/run
# Empty (multitenancy) mode
clojure -A:examples -X empty-mode/run
# Manual tool resume (declaration-only tool, manual pending-call resolution)
clojure -A:examples -X manual-tool-resume/run
Or run all examples:
./run-all-examples.sh
Note:
run-all-examples.shruns 18 example files that need only the Copilot CLI (examples 1–9, 12–19, and 21) — 19 runs in total, sincehelpers-queryruns twice (runandrun-multi). Examples 10 (BYOK), 11 (MCP), and 20 (empty-mode — uses BYOK) require external dependencies (API keys, Node.js) and are run manually.
With a custom CLI path:
COPILOT_CLI_PATH=/path/to/copilot clojure -A:examples -X basic-chat/run
Example 1: Basic Chat (basic_chat.clj)
Difficulty: Beginner
Concepts: Client lifecycle, sessions, message sending
The simplest use case—create a client, start a conversation, and get responses.
What It Demonstrates
- Creating and starting a
CopilotClient - Creating a session with a specific model
- Sending messages with
send-and-wait! - Multi-turn conversation (context is preserved)
- Proper cleanup with
with-client-session
Usage
clojure -A:examples -X basic-chat/run
clojure -A:examples -X basic-chat/run :q1 '"What is Clojure?"' :q2 '"Who created it?"'
Code Walkthrough
(require '[github.copilot-sdk :as copilot])
(require '[github.copilot-sdk.helpers :as h])
;; 1. Create a client and session
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"}]
;; 2. Send a message using query with the session
(println (h/query "What is the capital of France?" :session session))
;; => "The capital of France is Paris."
;; 3. Follow-up question (conversation context preserved)
(println (h/query "What is its population?" :session session)))
;; The model knows "its" refers to Paris
Example 2: Helpers Query (helpers_query.clj)
Difficulty: Beginner
Concepts: Stateless queries, simple API
Shows the simplified helpers API for one-shot queries without managing client/session lifecycle.
What It Demonstrates
query- Simple synchronous query, returns just the answer stringquery-seq!- Returns a bounded lazy sequence (default 256 events) and guarantees session cleanupquery-chan- Returns core.async channel of events for explicit lifecycle control- Automatic client management (created on first use, reused across queries)
- Automatic cleanup via JVM shutdown hook (no manual cleanup needed)
Usage
# Simple query
clojure -A:examples -X helpers-query/run
# With custom prompt
clojure -A:examples -X helpers-query/run :prompt '"What is functional programming?"'
# Streaming output (lazy seq)
clojure -A:examples -X helpers-query/run-streaming
# Streaming output (core.async)
clojure -A:examples -X helpers-query/run-async
# Multiple independent queries
clojure -A:examples -X helpers-query/run-multi
clojure -A:examples -X helpers-query/run-multi :questions '["What is Rust?" "What is Go?"]'
Code Walkthrough
(require '[github.copilot-sdk.helpers :as h])
;; Simplest possible query - just get the answer
(h/query "What is 2+2?" :session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"})
;; => "4"
;; With options
(h/query "What is Clojure?" :session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"})
;; Streaming with multimethod event handling
(defmulti handle-event :type)
(defmethod handle-event :default [_] nil)
(defmethod handle-event :copilot/assistant.message_delta [{{:keys [delta-content]} :data}]
(print delta-content)
(flush))
(defmethod handle-event :copilot/assistant.message [_] (println))
(run! handle-event (h/query-seq! "Tell me a joke" :session {:on-permission-request copilot/approve-all
:model "gpt-5.4" :streaming? true}))
Example 3: Tool Integration (tool_integration.clj)
Difficulty: Intermediate
Concepts: Custom tools, tool handlers, result types
Shows how to let the LLM call back into your application when it needs capabilities you provide.
What It Demonstrates
- Defining tools with
define-tool - JSON Schema parameters for type-safe tool inputs
- Handler functions that execute when tools are invoked
- Different result types:
result-success,result-failure - Overriding built-in tools with
:overrides-built-in-tool
Usage
clojure -A:examples -X tool-integration/run
clojure -A:examples -X tool-integration/run :languages '["clojure" "haskell"]'
Code Walkthrough
(require '[github.copilot-sdk :as copilot])
(require '[github.copilot-sdk.helpers :as h])
;; Define a tool with handler
(def lookup-tool
(copilot/define-tool "lookup_language"
{:description "Look up information about a programming language"
:parameters {:type "object"
:properties {:language {:type "string"
:description "Language name"}}
:required ["language"]}
:handler (fn [args invocation]
;; args = {:language "clojure"}
;; invocation = full invocation context
(let [lang (-> args :language str/lower-case)
info (get knowledge-base lang)]
(if info
(copilot/result-success info)
(copilot/result-failure
(str "No info for: " lang)
"not found"))))}))
;; Create session with tools and use query
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"
:tools [lookup-tool]}]
(println (h/query "Tell me about Clojure using the lookup tool" :session session)))
Tool Result Types
;; Success - return data to the LLM
(copilot/result-success "The answer is 42")
;; Failure - tell LLM the operation failed
(copilot/result-failure "Could not connect to database" "connection timeout")
;; Denied - permission was denied
(copilot/result-denied "User declined permission")
;; Rejected - tool invocation was invalid
(copilot/result-rejected "Invalid parameters")
Overriding Built-In Tools
You can override built-in tools (like grep or edit_file) with custom implementations
by setting :overrides-built-in-tool true:
(def custom-grep
(copilot/define-tool "grep"
{:description "Project-aware grep that only searches source files"
:overrides-built-in-tool true
:parameters {:type "object"
:properties {:query {:type "string"
:description "Search pattern"}}
:required ["query"]}
:handler (fn [{:keys [query]} _]
(copilot/result-success (str "Custom grep for: " query)))}))
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:tools [custom-grep]}]
(println (h/query "Search for 'defn' in the project" :session session)))
Without :overrides-built-in-tool true, defining a tool whose name clashes
with a built-in tool causes an error.
Example 4: Multi-Agent Orchestration (multi_agent.clj)
Difficulty: Advanced
Concepts: Multiple sessions, core.async, concurrent operations, agent coordination
Demonstrates a sophisticated pattern where multiple specialized agents collaborate using core.async channels for coordination.
What It Demonstrates
- Creating multiple sessions with different system prompts (personas)
- Using
core.asyncchannels for concurrent operations - Parallel research queries with
goblocks - Sequential pipeline: Research → Analysis → Synthesis
- Coordinating results from multiple async operations
Usage
clojure -A:examples -X multi-agent/run
clojure -A:examples -X multi-agent/run :topics '["AI safety" "machine learning" "neural networks"]'
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Multi-Agent Workflow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: Parallel Research │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Topic 1 │ │ Topic 2 │ │ Topic 3 │ │
│ │ (go block) │ │ (go block) │ │ (go block) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────────┬┴─────────────────┘ │
│ │ result-ch │
│ ▼ │
│ Phase 2: Analysis ┌──────────────┐ │
│ │ Analyst │ │
│ │ Session │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ Phase 3: Synthesis ┌──────────────┐ │
│ │ Writer │ │
│ │ Session │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ Final Summary │
└─────────────────────────────────────────────────────────────┘
Example 5: Config, Skills, and Large Output (config_skill_output.clj)
Difficulty: Intermediate
Concepts: config-dir overrides, skill directories, disabling skills, large tool output settings
Shows how to:
- set a custom config directory
- provide additional skill directories
- disable specific skills by name
- configure large tool output handling with a custom tool
Usage
clojure -A:examples -X config-skill-output/run
Example 6: Metadata API (metadata_api.clj)
Difficulty: Beginner
Concepts: list-sessions, list-tools, get-quota, get-current-model, switch-model
Demonstrates the metadata API functions introduced in v0.1.24 for inspecting available tools, quota information, and dynamically switching models within a session.
What It Demonstrates
list-sessionswith context filtering (by repository, branch, cwd)list-toolsto enumerate available tools, with optional model-specific overridesget-quotato check account usage and entitlementsget-current-modelto inspect the session's current modelswitch-model!to change the model mid-conversation while maintaining context
Usage
# Run the metadata API demo
clojure -A:examples -X metadata-api/run
Key Points
- list-sessions: Filter sessions by context (
:repository,:branch,:cwd,:git-root) - list-tools: Get tool metadata; pass a model ID for model-specific tool lists
- get-quota: Returns a map of quota type to snapshot (entitlement, used, remaining %)
- switch-model!: Change models dynamically without losing conversation context
Note: Some methods (
tools.list,account.getQuota,session.model.*) may not be supported by all CLI versions. The example gracefully skips unsupported operations.
Example 7: Permission Handling (permission_bash.clj)
Difficulty: Intermediate
Concepts: permission requests, bash tool, approval callback, deny-by-default
The SDK requires an :on-permission-request handler in every session config.
Use copilot/approve-all for blanket approval, or provide a custom handler for
fine-grained control.
Shows how to:
- handle
permission.requestvia:on-permission-request - invoke the built-in shell tool with allow/deny decisions
- log the full permission request payload for inspection
Usage
clojure -A:examples -X permission-bash/run
Example 8: Session Events Monitoring (session_events.clj)
Difficulty: Intermediate
Concepts: Event handling, session lifecycle, state management
Demonstrates how to monitor and handle session state events for debugging, logging, or building custom UIs.
What It Demonstrates
- Monitoring session lifecycle events (start, resume, idle, error)
- Tracking context management events (truncation, compaction)
- Observing usage metrics (token counts, limits)
- Handling
session.snapshot_rewindevents (state rollback) - Formatting events for human-readable display
Session State Events
| Event | Description |
|---|---|
session.start | Session created (note: fires before you can subscribe) |
session.resume | Existing session resumed |
session.idle | Session ready for input |
session.error | Error occurred |
session.usage_info | Token usage metrics |
session.truncation | Context window truncated |
session.compaction_start/complete | Infinite sessions compaction |
session.snapshot_rewind | Session state rolled back |
session.model_change | Model switched |
session.handoff | Session handed off |
Usage
clojure -A:examples -X session-events/run
clojure -A:examples -X session-events/run :prompt '"Explain recursion."'
Code Walkthrough
(require '[clojure.core.async :refer [chan tap go-loop <!]])
(require '[github.copilot-sdk :as copilot])
(def session-state-events
#{:copilot/session.idle :copilot/session.usage_info :copilot/session.error
:copilot/session.truncation :copilot/session.snapshot_rewind
:copilot/session.compaction_start :copilot/session.compaction_complete})
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:streaming? true}]
(let [events-ch (chan 256)
done (promise)]
(tap (copilot/events session) events-ch)
(go-loop []
(when-let [event (<! events-ch)]
;; Log session state events
(when (session-state-events (:type event))
(println "Session event:" (:type event) (:data event)))
;; Handle completion
(when (= :copilot/session.idle (:type event))
(deliver done true))
(recur)))
(copilot/send! session {:prompt "Hello"})
@done))
Example 9: User Input Handling (user_input.clj)
Difficulty: Intermediate
Concepts: User input requests, ask_user tool, interactive sessions
Demonstrates how to handle ask_user requests when the agent needs clarification or input from the user.
What It Demonstrates
- Registering an
:on-user-input-requesthandler - Responding to questions with choices or freeform input
- Interactive decision-making workflows
Usage
# Full interactive example
clojure -A:examples -X user-input/run
# Simpler yes/no example
clojure -A:examples -X user-input/run-simple
Code Walkthrough
(require '[github.copilot-sdk :as copilot])
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"
:on-user-input-request
(fn [request invocation]
;; request contains:
;; - :question - the question being asked
;; - :choices - optional list of choices
;; - :allow-freeform - whether freeform input is allowed
(println "Agent asks:" (:question request))
(when-let [choices (:choices request)]
(doseq [c choices]
(println " -" c)))
;; Return the user's response
;; :answer is required, :was-freeform defaults to true
{:answer (read-line)})}]
(copilot/send-and-wait! session
{:prompt "Ask me what format I prefer for the output, then respond accordingly."}))
Example 10: BYOK Provider (byok_provider.clj)
Difficulty: Intermediate
Concepts: BYOK (Bring Your Own Key), custom providers, API key authentication
Shows how to use the SDK with your own API keys from OpenAI, Azure, Anthropic, or Ollama instead of GitHub Copilot authentication.
What It Demonstrates
- Configuring a
:providermap for BYOK - Connecting to OpenAI, Azure OpenAI, Anthropic, or local Ollama
- Using environment variables for API keys
Prerequisites
Set an environment variable for your provider:
- OpenAI:
OPENAI_API_KEY - Azure:
AZURE_OPENAI_KEY - Anthropic:
ANTHROPIC_API_KEY - Ollama: No key needed (ensure
ollama serveis running)
Usage
# OpenAI (default)
OPENAI_API_KEY=sk-... clojure -A:examples -X byok-provider/run
# Anthropic
ANTHROPIC_API_KEY=sk-... clojure -A:examples -X byok-provider/run :provider-name '"anthropic"'
# Ollama (local, no key)
clojure -A:examples -X byok-provider/run :provider-name '"ollama"'
# Azure
AZURE_OPENAI_KEY=... clojure -A:examples -X byok-provider/run :provider-name '"azure"'
See doc/auth/byok.md for full BYOK documentation.
Example 11: MCP Local Server (mcp_local_server.clj)
Difficulty: Intermediate
Concepts: MCP servers, external tools, filesystem access
Shows how to integrate MCP (Model Context Protocol) servers to extend the assistant's capabilities with external tools.
What It Demonstrates
- Configuring
:mcp-serverswith a local stdio server - Using the
@modelcontextprotocol/server-filesystemMCP server - Combining MCP server tools with custom tools
- Using
copilot/approve-allto permit MCP tool execution (deny-by-default)
Prerequisites
- Node.js and
npxinstalled (for the filesystem MCP server)
Usage
# Basic filesystem access
clojure -A:examples -X mcp-local-server/run
# With custom directory
clojure -A:examples -X mcp-local-server/run :allowed-dir '"/home/user/docs"'
# MCP + custom tools combined
clojure -A:examples -X mcp-local-server/run-with-custom-tools
See doc/mcp/overview.md for full MCP documentation.
Example 12: File Attachments (file_attachments.clj)
Difficulty: Beginner
Concepts: File attachments, message options
Attach files to a prompt so the model can analyze their contents.
What It Demonstrates
- Sending
:attachmentsin message options withsend-and-wait! - File attachment type:
{:type :file :path "/absolute/path"} - Resolving relative paths to absolute with
java.io.File
Usage
# Attach and analyze deps.edn (default)
clojure -A:examples -X file-attachments/run
# Attach a different file
clojure -A:examples -X file-attachments/run :file-path '"README.md"'
Example 13: Session Resume (session_resume.clj)
Difficulty: Intermediate
Concepts: Session persistence, session resume, multi-session lifecycle
Resume a previous session by ID to continue a conversation with preserved context.
What It Demonstrates
- Creating a session and sending a message to store context
- Retrieving the session ID from the session map
- Resuming a session with
copilot/resume-session - Verifying context is preserved across resume
- Manual session lifecycle with
with-client(ensuresstop!/session cleanup),create-session,resume-session
Usage
# Default: remembers "PINEAPPLE"
clojure -A:examples -X session-resume/run
# Custom code word
clojure -A:examples -X session-resume/run :code-word '"MANGO"'
Example 14: Infinite Sessions (infinite_sessions.clj)
Difficulty: Intermediate
Concepts: Infinite sessions, context compaction, long conversations
Enable infinite sessions so the SDK automatically compacts older messages when the context window fills up.
What It Demonstrates
- Configuring
:infinite-sessionswith compaction thresholds :background-compaction-threshold— when background compaction starts (80%):buffer-exhaustion-threshold— when urgent compaction triggers (95%)- Sending multiple prompts in a long-running session
Usage
clojure -A:examples -X infinite-sessions/run
# Custom prompts
clojure -A:examples -X infinite-sessions/run :prompts '["What is Clojure?" "Who created it?" "When?"]'
Example 15: Lifecycle Hooks (lifecycle_hooks.clj)
Difficulty: Intermediate
Concepts: Hooks, callbacks, tool use monitoring
Register callbacks for session lifecycle events: start/end, tool use, prompts, and errors.
What It Demonstrates
- Configuring
:hooksin session config with all 6 hook types :on-session-start— fires when session begins:on-session-end— fires when session ends:on-pre-tool-use— fires before a tool runs (return{:approved true}to allow):on-post-tool-use— fires after a tool completes:on-user-prompt-submitted— fires when user sends a prompt:on-error-occurred— fires on errors- Collecting and summarizing hook events
Usage
clojure -A:examples -X lifecycle-hooks/run
# Custom prompt
clojure -A:examples -X lifecycle-hooks/run :prompt '"List all .md files using glob"'
Example 16: Reasoning Effort (reasoning_effort.clj)
Difficulty: Beginner
Concepts: Reasoning effort, model configuration
Control how much reasoning the model applies with the :reasoning-effort option.
What It Demonstrates
- Setting
:reasoning-effortin session config - Valid values:
"low","medium","high","xhigh" - Lower effort produces faster, more concise responses
Usage
# Default: low reasoning effort
clojure -A:examples -X reasoning-effort/run
# Higher reasoning effort
clojure -A:examples -X reasoning-effort/run :effort '"high"'
Example 17: Ask User Failure (ask_user_failure.clj)
Difficulty: Intermediate
Concepts: User input cancellation, ask_user tool, error handling, event tracing
Demonstrates what happens when a user cancels an ask_user request (simulating pressing Esc). This is a 1:1 port of the upstream basic-example.ts.
What It Demonstrates
- Handling user cancellation by throwing from
:on-user-input-request - Event tracing: subscribing to all events via
tapon the session events mult - Graceful degradation when the user skips a question
- Full event stream logging for debugging
Usage
clojure -A:examples -X ask-user-failure/run
Code Walkthrough
(require '[clojure.core.async :refer [chan tap go-loop <!]])
(require '[github.copilot-sdk :as copilot])
;; Track cancelled requests
(let [cancelled-requests (atom [])]
(copilot/with-client [client]
(copilot/with-session [session client
{:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"
:on-user-input-request
(fn [request _invocation]
(swap! cancelled-requests conj request)
;; Throwing simulates Esc — the SDK sends a failure
;; result back to the ask_user tool automatically.
(throw (RuntimeException. "User skipped question")))}]
;; Subscribe to all events for tracing
(let [events-ch (chan 256)]
(tap (copilot/events session) events-ch)
(go-loop []
(when-let [event (<! events-ch)]
(println event)
(recur)))
(let [result (copilot/send-and-wait! session
{:prompt "Use the ask_user tool to ask me to pick between 'Red' and 'Blue'."})]
(println "Response:" (get-in result [:data :content])))))))
Example 18: Elicitation Provider (elicitation_provider.clj)
Difficulty: Intermediate
Concepts: Elicitation requests, provider callbacks, MCP OAuth, capabilities
Demonstrates how to act as an elicitation provider — handling form-based or URL-based input requests from MCP servers and sub-agents.
What It Demonstrates
- Registering an
:on-elicitation-requesthandler - Inspecting elicitation mode (
"form"vs"url") - Auto-filling form fields from a JSON Schema
- Observing
elicitation.requestedandcapabilities.changedevents
Usage
clojure -A:examples -X elicitation-provider/run
Code Walkthrough
The handler receives a single ElicitationContext map with :session-id, :message, optional :requested-schema (JSON Schema), :mode ("form" or "url"), :elicitation-source, and :url. It returns an ElicitationResult:
{:action "accept" ;; or "decline" or "cancel"
:content {:field-name "value"}}
If the handler throws, the SDK sends {:action "cancel"} to prevent hanging. In a real application, the handler would render a UI dialog or open a browser for OAuth flows.
Example 19: Commands (commands.clj)
Difficulty: Intermediate Features: Slash commands, command definitions, session config
Demonstrates how to register slash commands on a session via the :commands option. Commands allow users to invoke named actions via /command-name in chat.
clojure -A:examples -X commands/run
Code Walkthrough
Each command is defined as a map with :name, optional :description, and a :command-handler function. The handler receives a CommandContext map:
{:session-id "..."
:command-name "status"
:command "/status"
:args ""}
Commands are registered by passing them in the session config:
(copilot/create-session client
{:commands [{:name "status"
:description "Show session status"
:command-handler handle-status}]
:on-permission-request copilot/approve-all})
Example 20: Empty (Multitenancy) Mode (empty_mode.clj)
Description: Run a session under :mode :empty — the hardened
posture for SaaS hosts that run sessions on behalf of multiple users.
Features: :mode :empty, :copilot-home, :session-fs with an
in-memory provider, tool-set/isolated, BYOK provider.
Demonstrates the multitenancy hardening introduced in upstream PR #1428.
The example creates fresh temp directories for :copilot-home, the
session's cwd, and the session state path, supplies an in-memory
:session-fs provider, and runs a single query through a BYOK provider
(empty mode disables the local keychain, so the host must bring its own
auth). In :empty mode the SDK forces COPILOT_DISABLE_KEYTAR=1 on the
spawned CLI, spreads safe session defaults (telemetry off, embeddings
in-memory, host-git off, skills off, ...), strips environment_context
from the system message, and sends a follow-up session.options.update
RPC turning off coauthor / manage-schedule and forcing
installedPlugins [].
Prerequisites: Set OPENAI_API_KEY or ANTHROPIC_API_KEY. Excluded
from run-all-examples.sh because it requires an external API key.
OPENAI_API_KEY=sk-... clojure -A:examples -X empty-mode/run
OPENAI_API_KEY=sk-... clojure -A:examples -X empty-mode/run :prompt '"What is Clojure?"'
See doc/reference/API.md
for the full Client Mode reference.
Example 21: Manual Tool Resume (manual_tool_resume.clj)
Difficulty: Advanced Concepts: Declaration-only tools, manual permission resolution, manual tool-call resolution
Demonstrates the SDK-driven analogue of the upstream manual_tool_resume
sample (Python/.NET): a tool declared without a :handler is
declaration-only — the runtime advertises it to the model but leaves the
call pending for the application to resolve by hand. With no
:on-permission-request handler registered either, the permission prompt is
also left pending. The pending requests are resolved across three separate
client lifecycles via resume-session with :continue-pending-work? true.
What It Demonstrates
- Defining a declaration-only tool (no
:handler) — upstream PR #1308 - Resuming pending work across client lifecycles with
resume-session+:continue-pending-work? true, resolving the original request ids - Reading the
:request-idfrom:copilot/permission.requestedand resolving it after resume withhandle-pending-permission-request!({:kind :approve-once}) - Reading the
:request-idfrom:copilot/external_tool.requestedand supplying the result after another resume withhandle-pending-tool-call! - Subscribing to events before each trigger so no event is missed, with a
bounded (120s)
alts!!wait
Usage
clojure -A:examples -X manual-tool-resume/run
clojure -A:examples -X manual-tool-resume/run :model '"gpt-5.4"'
Each lifecycle ends by suspending the session with
disconnect!rather than force-killing the client. This demonstrates manual pending-work resolution across graceful suspend/resume, not hard crash recovery: on CLI builds where in-flight pending requests are persisted only during graceful teardown, a SIGKILL (force-stop!) can drop the pending request ids before the nextresume-sessioncan continue them.
See doc/reference/API.md for
handle-pending-permission-request! and handle-pending-tool-call!.
Clojure vs JavaScript Comparison
Here's how common patterns compare between the Clojure and JavaScript SDKs:
Client Creation
JavaScript:
import { CopilotClient } from "@github/copilot-sdk";
const client = new CopilotClient({ logLevel: "info" });
await client.start();
Clojure:
(require '[github.copilot-sdk :as copilot])
(copilot/with-client [client]
;; use client
)
Simple Query (Helpers)
JavaScript:
// No direct equivalent - must create client/session
Clojure:
(require '[github.copilot-sdk.helpers :as h])
(h/query "What is 2+2?" :session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"})
;; => "4"
Event Handling
JavaScript:
session.on((event) => {
if (event.type === "assistant.message") {
console.log(event.data.content);
}
});
Clojure:
;; Using helpers with multimethod dispatch
(defmulti handle-event :type)
(defmethod handle-event :copilot/assistant.message [{{:keys [content]} :data}]
(println content))
(run! handle-event (h/query-seq! "Hello" :session {:on-permission-request copilot/approve-all
:model "gpt-5.4" :streaming? true}))
Tool Definition
JavaScript:
import { z } from "zod";
import { defineTool } from "@github/copilot-sdk";
defineTool("lookup", {
description: "Look up data",
parameters: z.object({ id: z.string() }),
handler: async ({ id }) => fetchData(id)
});
Clojure:
(copilot/define-tool "lookup"
{:description "Look up data"
:parameters {:type "object"
:properties {:id {:type "string"}}
:required ["id"]}
:handler (fn [{:keys [id]} _]
(fetch-data id))})
Async Patterns
JavaScript (Promises):
const response = await session.sendAndWait({ prompt: "Hello" });
Clojure (Blocking):
(def response (copilot/send-and-wait! session {:prompt "Hello"}))
Clojure (core.async):
(go
(let [ch (copilot/send-async session {:prompt "Hello"})]
(loop []
(when-let [event (<! ch)]
(println event)
(recur)))))
Troubleshooting
"Connection refused" errors
Ensure the Copilot CLI is installed and accessible:
copilot --version
# Or check your custom path
$COPILOT_CLI_PATH --version
Timeout errors
Increase the timeout for complex queries:
(copilot/send-and-wait! session {:prompt "Complex question"} 300000) ; 5 minutes
Tool not being called
Ensure your prompt explicitly mentions the tool or its capability:
;; Less likely to trigger tool:
{:prompt "Tell me about Clojure"}
;; More likely to trigger tool:
{:prompt "Use the lookup_language tool to tell me about Clojure"}
Memory issues with many sessions
Clean up sessions when done:
(copilot/disconnect! session)
And periodically list/delete orphaned sessions:
(doseq [s (copilot/list-sessions client)]
(copilot/delete-session! client (:session-id s)))