diff --git a/CHANGELOG.md b/CHANGELOG.md index f41aa57..b6c8d2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Changed (upstream PR #554 sync) +- **BREAKING**: `:on-permission-request` is now **required** when calling `create-session`, `resume-session`, ` "4" ;; With model selection -(h/query "Explain monads in one sentence" :session {:model "claude-sonnet-4.5"}) +(h/query "Explain monads in one sentence" :session {:on-permission-request copilot/approve-all :model "claude-sonnet-4.5"}) ;; With a system prompt -(h/query "What is Clojure?" :session {:system-prompt "You are a helpful assistant. Be concise."}) +(h/query "What is Clojure?" :session {:on-permission-request copilot/approve-all :system-prompt "You are a helpful assistant. Be concise."}) ``` ### More Control @@ -62,7 +62,8 @@ For multi-turn conversations, pass a session instance to `query`: ```clojure (require '[github.copilot-sdk :as copilot]) -(copilot/with-client-session [session {:model "claude-haiku-4.5"}] +(copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5"}] ;; Session maintains context between queries (println (h/query "What is the capital of France?" :session session)) (println (h/query "What is its population?" :session session))) @@ -71,7 +72,8 @@ For multi-turn conversations, pass a session instance to `query`: Or use the full API for maximum flexibility: ```clojure -(copilot/with-client-session [session {:model "claude-haiku-4.5"}] +(copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5"}] (println (-> (copilot/send-and-wait! session {:prompt "What is the capital of France?"}) (get-in [:data :content])))) ``` @@ -86,7 +88,7 @@ Use ` "4" ;; With session options -(h/query "Explain monads" :session {:model "claude-sonnet-4.5"}) +(h/query "Explain monads" :session {:on-permission-request copilot/approve-all :model "claude-sonnet-4.5"}) ;; With system prompt -(h/query "Hello" :session {:system-prompt "Be concise."}) +(h/query "Hello" :session {:on-permission-request copilot/approve-all :system-prompt "Be concise."}) ;; With explicit client (copilot/with-client [client {}] @@ -40,7 +40,7 @@ When `:session` is a CopilotSession instance, the query uses that session direct ;; With explicit session (multi-turn conversation) (copilot/with-client [client {}] - (copilot/with-session [session client {}] + (copilot/with-session [session client {:on-permission-request copilot/approve-all}] (h/query "My name is Alice." :session session) (h/query "What is my name?" :session session))) ;; context preserved! ``` @@ -54,7 +54,7 @@ When `:session` is a CopilotSession instance, the query uses that session direct Execute a query and return a bounded lazy sequence of events with guaranteed cleanup (default: 256 events). ```clojure -(->> (h/query-seq! "Tell me a story" :session {:streaming? true}) +(->> (h/query-seq! "Tell me a story" :session {:on-permission-request copilot/approve-all :streaming? true}) (filter #(= :copilot/assistant.message_delta (:type %))) (map #(get-in % [:data :delta-content])) (run! print)) @@ -70,7 +70,7 @@ Execute a query and return a core.async channel of events. Use this when you nee or want to stop reading early without leaking session resources. ```clojure -(let [ch (h/query-chan "Tell me a story" :session {:streaming? true})] +(let [ch (h/query-chan "Tell me a story" :session {:on-permission-request copilot/approve-all :streaming? true})] (go-loop [] (when-let [event ( **Note:** Not yet implemented in the CLI as of version 0.0.412. Calling this throws until CLI support is added. ```clojure -(copilot/with-client-session [session {:model "gpt-5.2"}] +(copilot/with-client-session [session {:model "gpt-5.2" + :on-permission-request copilot/approve-all}] (println "Before:" (copilot/get-current-model session)) (copilot/switch-model! session "claude-sonnet-4.5") (println "After:" (copilot/get-current-model session))) @@ -881,7 +889,8 @@ copilot/tool-events ### Example: Handling Events ```clojure -(copilot/with-client-session [session {:streaming? true}] +(copilot/with-client-session [session {:streaming? true + :on-permission-request copilot/approve-all}] (let [ch (chan 256)] (tap (copilot/events session) ch) (go-loop [] @@ -910,7 +919,8 @@ Enable streaming to receive assistant response chunks as they're generated: ```clojure (def session (copilot/create-session client {:model "gpt-5.2" - :streaming? true})) + :streaming? true + :on-permission-request copilot/approve-all})) (let [ch (chan 100)] (tap (copilot/events session) ch) @@ -984,7 +994,8 @@ Let the CLI call back into your process when the model needs capabilities you pr (def session (copilot/create-session client {:model "gpt-5.2" - :tools [lookup-tool]})) + :tools [lookup-tool] + :on-permission-request copilot/approve-all})) ``` When Copilot invokes `lookup_issue`, the SDK automatically runs your handler and responds to the CLI. @@ -1013,6 +1024,7 @@ Control the system prompt: ```clojure (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :system-message {:content " @@ -1029,6 +1041,7 @@ For full control (removes all guardrails), use `:mode :replace`: ```clojure (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :system-message {:mode :replace :content "You are a helpful assistant."}}) ``` @@ -1041,6 +1054,7 @@ It does not define custom agents. Custom agents are provided via `:custom-agents ```clojure (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :config-dir "/tmp/copilot-config" :skill-directories ["/path/to/skills" "/opt/team-skills"] :disabled-skills ["legacy-skill" "experimental-skill"]})) @@ -1058,6 +1072,7 @@ Configure how large tool outputs are handled before being sent back to the model ```clojure (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :large-output {:enabled true :max-size-bytes 65536 :output-dir "/tmp/copilot-tool-output"}})) @@ -1090,11 +1105,13 @@ automatically compacts older messages while preserving important context. ```clojure ;; Enable with defaults (enabled by default) (def session (copilot/create-session client - {:model "gpt-5.2"})) + {:model "gpt-5.2" + :on-permission-request copilot/approve-all})) ;; Explicit configuration (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :infinite-sessions {:enabled true :background-compaction-threshold 0.80 :buffer-exhaustion-threshold 0.95}})) @@ -1102,6 +1119,7 @@ automatically compacts older messages while preserving important context. ;; Disable infinite sessions (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :infinite-sessions {:enabled false}})) ``` @@ -1142,8 +1160,8 @@ Sessions emit `:session.compaction_start` and `:session.compaction_complete` eve ### Permission Handling The SDK uses a **deny-by-default** permission model. All permission requests -(file writes, shell commands, URL fetches, etc.) are denied unless your -session config provides an `:on-permission-request` handler. +(file writes, shell commands, URL fetches, custom tool execution, etc.) are denied unless your +session config provides an `:on-permission-request` handler (required). Use `approve-all` to opt into approving everything: @@ -1200,6 +1218,7 @@ handler is called. Return a response map with the user's input: ```clojure (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :on-user-input-request (fn [request invocation] ;; request contains {:question "..." :choices [...] :allow-freeform true/false} @@ -1228,6 +1247,7 @@ Lifecycle hooks allow custom logic at various points during the session: ```clojure (def session (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all :hooks {:on-pre-tool-use (fn [input invocation] @@ -1286,14 +1306,17 @@ For models that support reasoning (like o1), you can control the reasoning effor ;; Create session with reasoning effort (def session (copilot/create-session client {:model "o1" - :reasoning-effort "high"})) ; "low", "medium", "high", or "xhigh" + :reasoning-effort "high" + :on-permission-request copilot/approve-all})) ; "low", "medium", "high", or "xhigh" ``` ### Multiple Sessions ```clojure -(def session1 (copilot/create-session client {:model "gpt-5.2"})) -(def session2 (copilot/create-session client {:model "claude-sonnet-4.5"})) +(def session1 (copilot/create-session client {:model "gpt-5.2" + :on-permission-request copilot/approve-all})) +(def session2 (copilot/create-session client {:model "claude-sonnet-4.5" + :on-permission-request copilot/approve-all})) ;; Both sessions are independent (copilot/send-and-wait! session1 {:prompt "Hello from session 1"}) @@ -1342,7 +1365,8 @@ For models that support reasoning (like o1), you can control the reasoning effor ```clojure (try - (let [session (copilot/create-session client)] + (let [session (copilot/create-session client + {:on-permission-request copilot/approve-all})] (copilot/send! session {:prompt "Hello"})) (catch Exception e (println "Error:" (ex-message e)))) diff --git a/examples/README.md b/examples/README.md index fd82ca0..74ad768 100644 --- a/examples/README.md +++ b/examples/README.md @@ -113,7 +113,8 @@ clojure -A:examples -X basic-chat/run :q1 '"What is Clojure?"' :q2 '"Who created (require '[github.copilot-sdk.helpers :as h]) ;; 1. Create a client and session -(copilot/with-client-session [session {:model "claude-haiku-4.5"}] +(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." @@ -166,11 +167,13 @@ clojure -A:examples -X helpers-query/run-multi :questions '["What is Rust?" "Wha (require '[github.copilot-sdk.helpers :as h]) ;; Simplest possible query - just get the answer -(h/query "What is 2+2?" :session {:model "claude-haiku-4.5"}) +(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 {:model "claude-haiku-4.5"}) +(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) @@ -180,7 +183,8 @@ clojure -A:examples -X helpers-query/run-multi :questions '["What is Rust?" "Wha (flush)) (defmethod handle-event :copilot/assistant.message [_] (println)) -(run! handle-event (h/query-seq! "Tell me a joke" :session {:model "gpt-5.2" :streaming? true})) +(run! handle-event (h/query-seq! "Tell me a joke" :session {:on-permission-request copilot/approve-all + :model "gpt-5.2" :streaming? true})) ``` --- @@ -232,7 +236,8 @@ clojure -A:examples -X tool-integration/run :languages '["clojure" "haskell"]' "not found"))))})) ;; Create session with tools and use query -(copilot/with-client-session [session {:model "claude-haiku-4.5" +(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))) ``` @@ -369,9 +374,9 @@ clojure -A:examples -X metadata-api/run **Difficulty:** Intermediate **Concepts:** permission requests, bash tool, approval callback, deny-by-default -The SDK uses a **deny-by-default** permission model — all permission requests are -denied unless an `:on-permission-request` handler is provided. Use `copilot/approve-all` -for blanket approval, or provide a custom handler for fine-grained control. +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.request` via `:on-permission-request` @@ -434,7 +439,8 @@ clojure -A:examples -X session-events/run :prompt '"Explain recursion."' :copilot/session.truncation :copilot/session.snapshot_rewind :copilot/session.compaction_start :copilot/session.compaction_complete}) -(copilot/with-client-session [session {:streaming? true}] +(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) @@ -481,7 +487,8 @@ clojure -A:examples -X user-input/run-simple ```clojure (require '[github.copilot-sdk :as copilot]) -(copilot/with-client-session [session {:model "claude-haiku-4.5" +(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: @@ -608,7 +615,8 @@ await client.start(); **Clojure:** ```clojure (require '[github.copilot-sdk.helpers :as h]) -(h/query "What is 2+2?" :session {:model "claude-haiku-4.5"}) +(h/query "What is 2+2?" :session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5"}) ;; => "4" ``` @@ -630,7 +638,8 @@ session.on((event) => { (defmethod handle-event :copilot/assistant.message [{{:keys [content]} :data}] (println content)) -(run! handle-event (h/query-seq! "Hello" :session {:model "gpt-5.2" :streaming? true})) +(run! handle-event (h/query-seq! "Hello" :session {:on-permission-request copilot/approve-all + :model "gpt-5.2" :streaming? true})) ``` ### Tool Definition diff --git a/examples/ask_user_failure.clj b/examples/ask_user_failure.clj index 0d0091f..8b44034 100644 --- a/examples/ask_user_failure.clj +++ b/examples/ask_user_failure.clj @@ -16,7 +16,8 @@ (let [cancelled-requests (atom [])] (copilot/with-client [client] (copilot/with-session [session client - {:model "claude-haiku-4.5" + {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5" :on-user-input-request (fn [request _invocation] (swap! cancelled-requests conj request) diff --git a/examples/basic_chat.clj b/examples/basic_chat.clj index 80a7b8c..8a6036f 100644 --- a/examples/basic_chat.clj +++ b/examples/basic_chat.clj @@ -10,7 +10,8 @@ (defn run [{:keys [q1 q2] :or {q1 (:q1 defaults) q2 (:q2 defaults)}}] - (copilot/with-client-session [session {:model "claude-haiku-4.5"}] + (copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5"}] (println "Q1:" q1) (println "🤖:" (h/query q1 :session session)) (println) diff --git a/examples/byok_provider.clj b/examples/byok_provider.clj index e98bcb6..304b6e6 100644 --- a/examples/byok_provider.clj +++ b/examples/byok_provider.clj @@ -85,6 +85,6 @@ (println (str " Base URL: " (get-in config [:provider :base-url]))) (println) (copilot/with-client [client {}] - (copilot/with-session [session client config] + (copilot/with-session [session client (assoc config :on-permission-request copilot/approve-all)] (println "Q: What is 2+2?") (println "🤖:" (h/query "What is 2+2? Answer in one sentence." :session session)))))) diff --git a/examples/config_skill_output.clj b/examples/config_skill_output.clj index 4c27c7c..9faec4b 100644 --- a/examples/config_skill_output.clj +++ b/examples/config_skill_output.clj @@ -31,7 +31,8 @@ (.mkdirs (java.io.File. output-dir)) (write-demo-skill! skill-dir) (println (str "[debug] output-dir: " output-dir)) - (copilot/with-client-session [session {:model "claude-haiku-4.5" + (copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5" :config-dir config-dir :skill-directories [skill-dir] :disabled-skills ["demo-skill"] diff --git a/examples/helpers_query.clj b/examples/helpers_query.clj index 0f9c92b..71fb916 100644 --- a/examples/helpers_query.clj +++ b/examples/helpers_query.clj @@ -9,7 +9,8 @@ {:prompt "What is the capital of Japan? Answer in one sentence."}) (def session-config - {:model "claude-haiku-4.5"}) + {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5"}) (defn run [{:keys [prompt] :or {prompt (:prompt defaults)}}] @@ -37,13 +38,15 @@ [{:keys [prompt] :or {prompt "Explain the concept of immutability in 2-3 sentences."}}] (println "Query:" prompt) (println) - (run! handle-event (h/query-seq! prompt :session {:model "gpt-5.2" :streaming? true}))) + (run! handle-event (h/query-seq! prompt :session {:on-permission-request copilot/approve-all + :model "gpt-5.2" :streaming? true}))) (defn run-async [{:keys [prompt] :or {prompt "Tell me a short joke."}}] (println "Query:" prompt) (println) - (let [ch (h/query-chan prompt :session {:model "gpt-5.2" :streaming? true})] + (let [ch (h/query-chan prompt :session {:on-permission-request copilot/approve-all + :model "gpt-5.2" :streaming? true})] (> (println "Analysis:\n"))))) (def synthesis-prompt "You are a writer. Create clear, engaging prose.") @@ -80,5 +82,6 @@ (with-timing (doto (h/query (str "Write a 3-4 sentence executive summary. ANALYSIS: " analysis) :client client - :session {:system-prompt synthesis-prompt :model "gpt-5.2"}) + :session {:on-permission-request copilot/approve-all + :system-prompt synthesis-prompt :model "gpt-5.2"}) println))) diff --git a/examples/session_events.clj b/examples/session_events.clj index 3e84f04..be82e05 100644 --- a/examples/session_events.clj +++ b/examples/session_events.clj @@ -16,7 +16,8 @@ reasoning-effort "high"}}] (copilot/with-client-session [client {:log-level :debug} - session {:model model + session {:on-permission-request copilot/approve-all + :model model :streaming? true :reasoning-effort reasoning-effort}] (let [events-ch (copilot/subscribe-events session) diff --git a/examples/tool_integration.clj b/examples/tool_integration.clj index a9c85ef..0e43af4 100644 --- a/examples/tool_integration.clj +++ b/examples/tool_integration.clj @@ -34,7 +34,8 @@ (defn run [{:keys [languages] :or {languages (:languages defaults)}}] - (copilot/with-client-session [session {:model "claude-haiku-4.5" + (copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5" :tools [lookup-tool]}] (doseq [lang languages] (println (str "Looking up: " lang)) diff --git a/examples/user_input.clj b/examples/user_input.clj index d8dcc5d..41e4011 100644 --- a/examples/user_input.clj +++ b/examples/user_input.clj @@ -43,7 +43,8 @@ (println "=== User Input Example ===") (println "This example shows how to handle ask_user requests from the agent.\n") - (copilot/with-client-session [session {:model "claude-haiku-4.5" + (copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5" :on-user-input-request (fn [request _invocation] ;; request has :question, :choices, :allow-freeform @@ -96,7 +97,8 @@ (println " The agent may or may not choose to use ask_user.\n") (let [input-requested? (atom false)] - (copilot/with-client-session [session {:model "claude-haiku-4.5" + (copilot/with-client-session [session {:on-permission-request copilot/approve-all + :model "claude-haiku-4.5" :on-user-input-request (fn [{:keys [question choices]} _] (reset! input-requested? true) diff --git a/src/github/copilot_sdk.clj b/src/github/copilot_sdk.clj index 1f081e3..37a3bb9 100644 --- a/src/github/copilot_sdk.clj +++ b/src/github/copilot_sdk.clj @@ -10,7 +10,8 @@ (copilot/start! client) ;; Create a session - (def session (copilot/create-session client {:model \"gpt-5.2\"})) + (def session (copilot/create-session client {:on-permission-request copilot/approve-all + :model \"gpt-5.2\"})) ;; Send a message and wait for response (def response (copilot/send-and-wait! session {:prompt \"What is 2+2?\"})) @@ -350,7 +351,8 @@ (defn create-session "Create a new conversation session. - Config options: + 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.2\", \"claude-sonnet-4.5\") - :tools - Vector of tool definitions (use define-tool) @@ -358,13 +360,6 @@ - :available-tools - List of allowed tool names - :excluded-tools - List of excluded tool names - :provider - Custom provider config (BYOK) - - :on-permission-request - Permission handler function - Must return a map compatible with the permission result payload. - The SDK wraps this into the JSON-RPC response as {:result }: - {:kind :approved} - {:kind :denied-by-rules :rules [{:kind \"shell\" :argument \"echo hi\"}]} - {:kind :denied-no-approval-rule-and-could-not-request-from-user} - {:kind :denied-interactively-by-user :feedback \"optional\"} - :streaming? - Enable streaming deltas - :mcp-servers - MCP server configs map (keyed by server ID) - :custom-agents - Custom agent configs @@ -377,12 +372,11 @@ Example: ```clojure - (def session (copilot/create-session client {:model \"gpt-5.2\"})) + (def session (copilot/create-session client {:on-permission-request copilot/approve-all + :model \"gpt-5.2\"})) ```" - ([client] - (client/create-session client)) - ([client config] - (client/create-session client config))) + [client config] + (client/create-session client config)) (defn \"4\" ;; With options - (h/query \"Explain monads\" :session {:model \"claude-sonnet-4.5\"}) + (h/query \"Explain monads\" :session {:on-permission-request copilot/approve-all + :model \"claude-sonnet-4.5\"}) ``` ## Client Management @@ -118,10 +119,11 @@ (defn- build-session-config "Build session config from options map." [session-opts] - (let [{:keys [model system-prompt tools allowed-tools excluded-tools + (let [{:keys [on-permission-request model system-prompt tools allowed-tools excluded-tools streaming? mcp-servers custom-agents config-dir skill-directories disabled-skills]} session-opts] (cond-> {} + on-permission-request (assoc :on-permission-request on-permission-request) model (assoc :model model) system-prompt (assoc :system-message {:mode :append :content system-prompt}) tools (assoc :tools tools) @@ -188,17 +190,19 @@ Examples: ;; Simple query (shared client, fresh session) - (query \"What is 2+2?\") + (query \"What is 2+2?\" :session {:on-permission-request copilot/approve-all}) ;; With session options - (query \"Explain monads\" :session {:model \"claude-sonnet-4.5\"}) + (query \"Explain monads\" :session {:on-permission-request copilot/approve-all + :model \"claude-sonnet-4.5\"}) ;; With explicit client (copilot/with-client [c {}] - (query \"Hello\" :client c)) + (query \"Hello\" :client c :session {:on-permission-request copilot/approve-all})) ;; With explicit session (multi-turn) - (copilot/with-session [s client {:model \"gpt-5.2\"}] + (copilot/with-session [s client {:on-permission-request copilot/approve-all + :model \"gpt-5.2\"}] (query \"What is 2+2?\" :session s) (query \"And 3+3?\" :session s)) ;; context preserved " @@ -284,7 +288,8 @@ the session becomes idle or errors. Examples: - (let [ch (query-chan \"Tell me a story\" :session {:streaming? true})] + (let [ch (query-chan \"Tell me a story\" :session {:on-permission-request copilot/approve-all + :streaming? true})] (go-loop [] (when-let [event (