From a1a8836e7ad96aa1a4259bfdae9787ad16edfa27 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Mon, 18 May 2026 08:23:33 +0200 Subject: [PATCH] feat(custom-agents): add :agent-model field (upstream PR #1309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional :agent-model to entries in :custom-agents. When set, the runtime tries to use that model for the agent, falling back to the parent session model if unavailable. Wire conversion goes through util/clj->wire automatically: :agent-model becomes :agentModel on the wire, sent on both session.create and session.resume custom-agents entries — consistent with the existing :agent-name / :agent-display-name / :agent-skills convention. Upstream parity: matches the new model?: string field added to CustomAgentConfig in nodejs/src/types.ts (upstream commit d0eb531e, PR #1309). No other upstream changes since v1.0.0-beta.4 require porting: - #1295 (remote_session) was already shipped in CLI 1.0.48 sync (PR #103) - E2E test stabilization commits affect upstream test fixtures only - CLI version bumps (1.0.49-*) are prerelease; we stay pinned to 1.0.48 Tests: 3 new integration tests (spec validation, wire payload on session.create/resume, omission when not set). Full suite: 257 tests, 1203 assertions, 0 failures. bb ci:full passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 8 +++ doc/reference/API.md | 2 +- src/github/copilot_sdk/specs.clj | 6 ++- test/github/copilot_sdk/integration_test.clj | 51 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2daeefe..0918531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Added (post-v1.0.0-beta.4 sync) +- **`:agent-model` on custom-agent configs** — Custom agent maps in + `:custom-agents` now accept an optional `:agent-model` string (e.g. + `"claude-haiku-4.5"`). When set, the runtime attempts to use that model + for the agent, falling back to the parent session model if unavailable. + Forwarded on the wire as `agentModel` on each entry in `customAgents` + for both `session.create` and `session.resume`. (upstream PR #1309) + ### Notes (v1.0.0-beta.4 sync) Upstream `v1.0.0-beta.4` shipped no new Node.js SDK API surface relative to `v1.0.0-beta.3` — every SDK-visible change in the upstream diff diff --git a/doc/reference/API.md b/doc/reference/API.md index fafa972..73df036 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -243,7 +243,7 @@ Create a client and session together, ensuring both are cleaned up on exit. | `: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`, `:headers` (map of HTTP header name→value, sent with each provider request — upstream PR #1094), `:model-id` (string — the model identifier to send to the provider; overrides session `:model`), `:wire-model` (string — model name as sent on the provider wire when it differs from `:model-id`), `:max-input-tokens` (integer — input/prompt token cap; serialized as wire `maxPromptTokens`), `:max-output-tokens` (integer — output token cap). The four override fields were added in upstream PR #966 | | `: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. 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` | +| `:custom-agents` | vector | Custom agent configs. Each agent map: `:agent-name` (required), `:agent-prompt` (required), `:agent-display-name`, `:agent-description`, `:agent-tools`, `:agent-infer?`, `:agent-skills` (vector of strings), `:agent-model` (string, e.g. `"claude-haiku-4.5"`; when set the runtime tries this model for the agent, falling back to the parent session model — upstream PR #1309), `:mcp-servers` | | `:default-agent` | map | Built-in/default agent config. Use `{:excluded-tools [...]}` to hide tools from the default agent while leaving them available to custom agents | | `:on-permission-request` | fn | **Required.** Permission handler function. Use `copilot/approve-all` to approve everything. | | `:streaming?` | boolean | Enable streaming deltas | diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index bf74734..368e3d9 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -274,11 +274,15 @@ (s/def ::agent-prompt ::non-blank-string) (s/def ::agent-infer? boolean?) (s/def ::agent-skills (s/coll-of string?)) +;; Model identifier for the agent (e.g. "claude-haiku-4.5"). When set, the +;; runtime will attempt to use this model for the agent, falling back to the +;; parent session model if unavailable. Upstream PR #1309. +(s/def ::agent-model ::non-blank-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? ::agent-skills])) + ::mcp-servers ::agent-infer? ::agent-skills ::agent-model])) (s/def ::custom-agents (s/coll-of ::custom-agent)) diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj index 3461004..723a814 100644 --- a/test/github/copilot_sdk/integration_test.clj +++ b/test/github/copilot_sdk/integration_test.clj @@ -3356,6 +3356,57 @@ agent (first (:customAgents create-params))] (is (= ["my-skill"] (:agentSkills agent)))))) +;; --- Per-agent model field (upstream PR #1309) ------------------------------ + +(deftest test-custom-agent-model-spec + (testing "::custom-agent spec accepts optional :agent-model 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-model "claude-haiku-4.5"})) + (is (not (s/valid? :github.copilot-sdk.specs/custom-agent + {:agent-name "test" :agent-prompt "You are helpful" + :agent-model 42})) + ":agent-model must be a string when provided"))) + +(deftest test-custom-agent-model-on-wire + (testing "model field is sent on wire in session.create and session.resume (upstream PR #1309)" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create" "session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :custom-agents [{:agent-name "haiku-agent" + :agent-prompt "Hello" + :agent-model "claude-haiku-4.5"}]}) + session-id (sdk/get-last-session-id *test-client*) + _ (sdk/resume-session *test-client* session-id + {:on-permission-request sdk/approve-all + :custom-agents [{:agent-name "haiku-agent-2" + :agent-prompt "Hi" + :agent-model "gpt-5.4"}]}) + create-params (get @seen "session.create") + resume-params (get @seen "session.resume")] + (is (= "claude-haiku-4.5" + (get-in create-params [:customAgents 0 :agentModel]))) + (is (= "gpt-5.4" + (get-in resume-params [:customAgents 0 :agentModel])))))) + +(deftest test-custom-agent-model-omitted-when-not-set + (testing ":agent-model is omitted from wire when not provided" + (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 "no-model" + :agent-prompt "Hi"}]}) + agent (first (get-in @seen ["session.create" :customAgents]))] + (is (not (contains? agent :agentModel)))))) + ;; --- Default agent config (upstream PR #1098) -------------------------------- (deftest test-default-agent-excluded-tools-on-wire