This directory contains example applications demonstrating various features of the Copilot SDK for Clojure.
-
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.
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/runOr run all examples:
./run-all-examples.shNote:
run-all-examples.shruns 14 examples that need only the Copilot CLI (examples 1–9 and 12–16). Examples 10 (BYOK) and 11 (MCP) require external dependencies (API keys, Node.js) and must be run manually.
With a custom CLI path:
COPILOT_CLI_PATH=/path/to/copilot clojure -A:examples -X basic-chat/runDifficulty: Beginner
Concepts: Client lifecycle, sessions, message sending
The simplest use case—create a client, start a conversation, and get responses.
- 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
clojure -A:examples -X basic-chat/run
clojure -A:examples -X basic-chat/run :q1 '"What is Clojure?"' :q2 '"Who created it?"'(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 ParisDifficulty: Beginner
Concepts: Stateless queries, simple API
Shows the simplified helpers API for one-shot queries without managing client/session lifecycle.
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)
# 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?"]'(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.2" :streaming? true}))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.
- 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
clojure -A:examples -X tool-integration/run
clojure -A:examples -X tool-integration/run :languages '["clojure" "haskell"]'(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)));; 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")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.
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.
- 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
clojure -A:examples -X multi-agent/run
clojure -A:examples -X multi-agent/run :topics '["AI safety" "machine learning" "neural networks"]'┌─────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────┘
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
clojure -A:examples -X config-skill-output/runDifficulty: 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.
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
# Run the metadata API demo
clojure -A:examples -X metadata-api/run- 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.
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
clojure -A:examples -X permission-bash/runDifficulty: Intermediate
Concepts: Event handling, session lifecycle, state management
Demonstrates how to monitor and handle session state events for debugging, logging, or building custom UIs.
- 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
| 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 |
clojure -A:examples -X session-events/run
clojure -A:examples -X session-events/run :prompt '"Explain recursion."'(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))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.
- Registering an
:on-user-input-requesthandler - Responding to questions with choices or freeform input
- Interactive decision-making workflows
# Full interactive example
clojure -A:examples -X user-input/run
# Simpler yes/no example
clojure -A:examples -X user-input/run-simple(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."}))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.
- Configuring a
:providermap for BYOK - Connecting to OpenAI, Azure OpenAI, Anthropic, or local Ollama
- Using environment variables for API keys
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)
# 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.
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.
- 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)
- Node.js and
npxinstalled (for the filesystem MCP server)
# 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-toolsSee doc/mcp/overview.md for full MCP documentation.
Difficulty: Beginner
Concepts: File attachments, message options
Attach files to a prompt so the model can analyze their contents.
- Sending
:attachmentsin message options withsend-and-wait! - File attachment type:
{:type :file :path "/absolute/path"} - Resolving relative paths to absolute with
java.io.File
# 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"'Difficulty: Intermediate
Concepts: Session persistence, session resume, multi-session lifecycle
Resume a previous session by ID to continue a conversation with preserved context.
- 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
# Default: remembers "PINEAPPLE"
clojure -A:examples -X session-resume/run
# Custom secret word
clojure -A:examples -X session-resume/run :secret-word '"MANGO"'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.
- 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
clojure -A:examples -X infinite-sessions/run
# Custom prompts
clojure -A:examples -X infinite-sessions/run :prompts '["What is Clojure?" "Who created it?" "When?"]'Difficulty: Intermediate
Concepts: Hooks, callbacks, tool use monitoring
Register callbacks for session lifecycle events: start/end, tool use, prompts, and errors.
- 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
clojure -A:examples -X lifecycle-hooks/run
# Custom prompt
clojure -A:examples -X lifecycle-hooks/run :prompt '"List all .md files using glob"'Difficulty: Beginner
Concepts: Reasoning effort, model configuration
Control how much reasoning the model applies with the :reasoning-effort option.
- Setting
:reasoning-effortin session config - Valid values:
"low","medium","high","xhigh" - Lower effort produces faster, more concise responses
# Default: low reasoning effort
clojure -A:examples -X reasoning-effort/run
# Higher reasoning effort
clojure -A:examples -X reasoning-effort/run :effort '"high"'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.
- 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
clojure -A:examples -X ask-user-failure/run(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])))))))Here's how common patterns compare between the Clojure and JavaScript SDKs:
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
)JavaScript:
// No direct equivalent - must create client/sessionClojure:
(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"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.2" :streaming? true}))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))})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)))))Ensure the Copilot CLI is installed and accessible:
copilot --version
# Or check your custom path
$COPILOT_CLI_PATH --versionIncrease the timeout for complex queries:
(copilot/send-and-wait! session {:prompt "Complex question"} 300000) ; 5 minutesEnsure 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"}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)))