diff --git a/.github/scripts/reference-impl-sync/sync-codegen-version.sh b/.github/scripts/reference-impl-sync/sync-codegen-version.sh index 09e182bb2..f3baa0ab5 100755 --- a/.github/scripts/reference-impl-sync/sync-codegen-version.sh +++ b/.github/scripts/reference-impl-sync/sync-codegen-version.sh @@ -63,9 +63,17 @@ if [[ ! -f "$CODEGEN_PKG" ]]; then fi # Update scripts/codegen/package.json with the new version and regenerate the lock file. -# Intentionally omit --save-exact to preserve the version specifier used by the reference -# implementation (e.g. a caret range like '^1.0.36-0' rather than an exact pin '1.0.36-0'). +# Write the version string directly into package.json to preserve the exact specifier +# used by the reference implementation (e.g. '^1.0.43-0'). npm install normalises +# caret ranges and would silently strip the prerelease suffix, causing a mismatch +# with pom.xml. echo "▸ Updating scripts/codegen/package.json: @github/copilot → ${CLI_VERSION}" cd "$CODEGEN_DIR" -npm install "@github/copilot@${CLI_VERSION}" +node -e " +const fs = require('fs'); +const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); +pkg.dependencies['@github/copilot'] = process.argv[1]; +fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); +" "$CLI_VERSION" +npm install echo "▸ Updated scripts/codegen to @github/copilot@${CLI_VERSION}" diff --git a/.lastmerge b/.lastmerge index 4a35fe8da..b3b7aa13d 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -c063458ecc3d606766f04cf203b11b08de672cc8 +06bfc5d41d72b76527456dee0bd78fe4697bac86 diff --git a/pom.xml b/pom.xml index 50fd909ae..ff35b8ee2 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ reference-impl-sync workflow and deal with the subsequent PR. --> - ^1.0.41-0 + ^1.0.43-0 @@ -321,6 +321,16 @@ ${testExecutionAgentArgs} ${surefire.jvm.args} + + 2 ${copilot.tests.dir} ${copilot.sdk.clone.dir} diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index b45b9c668..a8be99843 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.41-0", + "@github/copilot": "^1.0.43-0", "json-schema": "^0.4.0", "tsx": "^4.20.6" } @@ -428,26 +428,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.41-0.tgz", - "integrity": "sha512-gLyCadBZdJeJtHJI3XdN8wAmLMEUdXfCa3EcVnbdbV1NHZDAJhr7h41l7a49pqRAmJyLUKlk1Lokk7U+OD3tgw==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.43.tgz", + "integrity": "sha512-2FO825Aq4bwmHcXVyplW+CpZaJFUYjqtqjBGnueM31gu4ufn6ReurzB2swBQ6bn4Pquyy2KeodMRPpT6JaLMhw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.41-0", - "@github/copilot-darwin-x64": "1.0.41-0", - "@github/copilot-linux-arm64": "1.0.41-0", - "@github/copilot-linux-x64": "1.0.41-0", - "@github/copilot-win32-arm64": "1.0.41-0", - "@github/copilot-win32-x64": "1.0.41-0" + "@github/copilot-darwin-arm64": "1.0.43", + "@github/copilot-darwin-x64": "1.0.43", + "@github/copilot-linux-arm64": "1.0.43", + "@github/copilot-linux-x64": "1.0.43", + "@github/copilot-win32-arm64": "1.0.43", + "@github/copilot-win32-x64": "1.0.43" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.41-0.tgz", - "integrity": "sha512-lrrH1oMbTOF1W/YxH6rvoEHOymxmXaMx4aDzm190hU0Yh6Cuu0BJGFvgG8nE9bqcv5O8W7eEBr26jDlGtnZiwg==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.43.tgz", + "integrity": "sha512-VMaWfoUwIt19TGzmvTv/In5ITgFWfu91ZILt4Lb77gSmVbwbs+DahP2lbvM1s/GtGwOknwhOJLp4q2WMgK3CoQ==", "cpu": [ "arm64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.41-0.tgz", - "integrity": "sha512-4418VtSSkEgn4BcwCFg+0UDhGCfQgGTx16r/PiWbuUOgIBzts3FfVzWMWTuXyxk7kl2Ib8k7KSd/7rNpjcrzBw==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.43.tgz", + "integrity": "sha512-lCxg75zbgtWggb1p+IHhlhmulQj7BKIc+9pUsTwJ3Mjt51kiUYmD6LF2BD1/Ed4M3GMumSu4UTSIv9pL96n0Wg==", "cpu": [ "x64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.41-0.tgz", - "integrity": "sha512-5xjgp3Ak5QJ68byNbsgBpdK1V6T5t8EGu0pUwEJMNMMXxqvL9f7gPcnCGdTtV2DS4Q3adkziV/gpBSSQ5HY8hg==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.43.tgz", + "integrity": "sha512-Jr1rvt/Syz5oVSiU53keRKEV8f5xTLkmiB2qasAV6Emk7K82/BZW5HfW8cDadfHnlAS1+UVPpoO+8ykgx4dikw==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.41-0.tgz", - "integrity": "sha512-oWPkj0bSjBjtAqonMEZD7EuSByBNXwtceMw8y7uGOfs6jQXfhDGzCCB6NGb+lcftVNtWDKFCUtx+x8Fbt4O37w==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.43.tgz", + "integrity": "sha512-uyWyPpcwMC1tIHJTA1OPzIm1i/Eeku0NjFzPYnFvpfGug9pkL4xcUr8A2UNl8cVvxq/VQMwtYckV3G12ySJTFw==", "cpu": [ "x64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.41-0.tgz", - "integrity": "sha512-MaPg4tFWTiRuyv+j0ymJbZp8UPK+RIXNMpekR7FRf8/Uz+NiJgTTxTDjFi4ytRJU5UNrUezkVAk5Xduq/CaIew==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.43.tgz", + "integrity": "sha512-vY3rwkW1h7ixSqYd9XT/xGjxkepvQVJK2q1sYhSPBN4Wvuk37fRjN7ox3QU1lhpxlYQMc/g+ZR58u5cx31n1lw==", "cpu": [ "arm64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.41-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.41-0.tgz", - "integrity": "sha512-ykRuDWjJEgSywMFJl1yaefssaklCVSVhprx2NcSVh6tIGupvvzVAM6nL6Mj6nyKpG6FKGHanedBeL6SJc935cw==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.43.tgz", + "integrity": "sha512-AjGsebDbJmBxe9FFf2McSN5r2Joi8cFY879lxaQaXxfGjzOHblzvbhflbsX9x2GnvCfDdDiow413WvOGqKDH8g==", "cpu": [ "x64" ], diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index d146fd945..103db2913 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.41-0", + "@github/copilot": "^1.0.43-0", "json-schema": "^0.4.0", "tsx": "^4.20.6" } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java index d45803b66..e185a01fa 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java @@ -33,6 +33,8 @@ public record AssistantMessageToolRequest( @JsonProperty("toolTitle") String toolTitle, /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ @JsonProperty("mcpServerName") String mcpServerName, + /** Original tool name on the MCP server, when the tool is an MCP tool */ + @JsonProperty("mcpToolName") String mcpToolName, /** Resolved intention summary describing what this specific call does */ @JsonProperty("intentionSummary") String intentionSummary ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java b/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java index 74637aaa8..0d9dbc295 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java @@ -27,7 +27,7 @@ public record CustomAgentsUpdatedAgent( @JsonProperty("description") String description, /** Source location: user, project, inherited, remote, or plugin */ @JsonProperty("source") String source, - /** List of tool names available to this agent */ + /** List of tool names available to this agent, or null when all tools are available */ @JsonProperty("tools") List tools, /** Whether the agent can be selected by the user */ @JsonProperty("userInvocable") Boolean userInvocable, diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java index 02364db4f..0f348fe86 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java @@ -31,6 +31,8 @@ @JsonSubTypes.Type(value = SessionErrorEvent.class, name = "session.error"), @JsonSubTypes.Type(value = SessionIdleEvent.class, name = "session.idle"), @JsonSubTypes.Type(value = SessionTitleChangedEvent.class, name = "session.title_changed"), + @JsonSubTypes.Type(value = SessionScheduleCreatedEvent.class, name = "session.schedule_created"), + @JsonSubTypes.Type(value = SessionScheduleCancelledEvent.class, name = "session.schedule_cancelled"), @JsonSubTypes.Type(value = SessionInfoEvent.class, name = "session.info"), @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), @@ -112,6 +114,8 @@ public abstract sealed class SessionEvent permits SessionErrorEvent, SessionIdleEvent, SessionTitleChangedEvent, + SessionScheduleCreatedEvent, + SessionScheduleCancelledEvent, SessionInfoEvent, SessionWarningEvent, SessionModelChangeEvent, diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java new file mode 100644 index 000000000..01cd6e461 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code session.schedule_cancelled} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleCancelledEvent extends SessionEvent { + + @Override + public String getType() { return "session.schedule_cancelled"; } + + @JsonProperty("data") + private SessionScheduleCancelledEventData data; + + public SessionScheduleCancelledEventData getData() { return data; } + public void setData(SessionScheduleCancelledEventData data) { this.data = data; } + + /** Data payload for {@link SessionScheduleCancelledEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionScheduleCancelledEventData( + /** Id of the scheduled prompt that was cancelled */ + @JsonProperty("id") Long id + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java new file mode 100644 index 000000000..aba8650c8 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code session.schedule_created} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleCreatedEvent extends SessionEvent { + + @Override + public String getType() { return "session.schedule_created"; } + + @JsonProperty("data") + private SessionScheduleCreatedEventData data; + + public SessionScheduleCreatedEventData getData() { return data; } + public void setData(SessionScheduleCreatedEventData data) { this.data = data; } + + /** Data payload for {@link SessionScheduleCreatedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionScheduleCreatedEventData( + /** Sequential id assigned to the scheduled prompt within the session */ + @JsonProperty("id") Long id, + /** Interval between ticks in milliseconds */ + @JsonProperty("intervalMs") Long intervalMs, + /** Prompt text that gets enqueued on every tick */ + @JsonProperty("prompt") String prompt + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java new file mode 100644 index 000000000..df458acda --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code remote} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionRemoteApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionRemoteApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Invokes {@code session.remote.enable}. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enable() { + return caller.invoke("session.remote.enable", java.util.Map.of("sessionId", this.sessionId), SessionRemoteEnableResult.class); + } + + /** + * Invokes {@code session.remote.disable}. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture disable() { + return caller.invoke("session.remote.disable", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java new file mode 100644 index 000000000..2de2190ec --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.remote.disable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteDisableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java new file mode 100644 index 000000000..d8b7917b7 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.remote.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteEnableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java new file mode 100644 index 000000000..bd8ccc48b --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.remote.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteEnableResult( + /** Mission Control frontend URL for this session */ + @JsonProperty("url") String url, + /** Whether remote steering is enabled */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java index d4934fc08..60a741224 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java @@ -70,6 +70,8 @@ public final class SessionRpc { public final SessionHistoryApi history; /** API methods for the {@code usage} namespace. */ public final SessionUsageApi usage; + /** API methods for the {@code remote} namespace. */ + public final SessionRemoteApi remote; /** * Creates a new session RPC client. @@ -101,6 +103,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.shell = new SessionShellApi(caller, sessionId); this.history = new SessionHistoryApi(caller, sessionId); this.usage = new SessionUsageApi(caller, sessionId); + this.remote = new SessionRemoteApi(caller, sessionId); } /** diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index ebf6a96a6..3087966d3 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -102,6 +102,10 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { args.add(String.valueOf(options.getSessionIdleTimeoutSeconds())); } + if (options.isRemote()) { + args.add("--remote"); + } + List command = resolveCliCommand(cliPath, args); var pb = new ProcessBuilder(command); diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 4d0522162..58d7b71dc 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -187,9 +187,9 @@ private CompletableFuture startCore() { } private Connection startCoreBody() { + Process process = null; try { JsonRpcClient rpc; - Process process = null; if (optionsHost != null && optionsPort != null) { // External server (TCP) @@ -215,6 +215,10 @@ private Connection startCoreBody() { LOG.info("Copilot client connected"); return connection; } catch (Exception e) { + // Clean up the spawned process if connection setup failed + if (process != null) { + cleanupCliProcess(process); + } String stderr = serverManager.getStderrOutput(); if (!stderr.isEmpty()) { throw new CompletionException(new IOException( @@ -352,21 +356,28 @@ private CompletableFuture cleanupConnection() { } if (connection.process != null) { - try { - if (connection.process.isAlive()) { - Process destroyedProcess = connection.process.destroyForcibly(); - if (!destroyedProcess.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - LOG.fine("Process did not terminate within force kill timeout"); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.log(Level.FINE, "Interrupted while killing process", e); - } catch (Exception e) { - LOG.log(Level.FINE, "Error killing process", e); + cleanupCliProcess(connection.process); + } + }).exceptionally(ex -> { + LOG.log(Level.FINE, "Ignoring failed Copilot client startup during cleanup", ex); + return null; + }); + } + + private static void cleanupCliProcess(Process process) { + try { + if (process.isAlive()) { + Process destroyedProcess = process.destroyForcibly(); + if (!destroyedProcess.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + LOG.fine("Process did not terminate within force kill timeout"); } } - }).exceptionally(ex -> null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.log(Level.FINE, "Interrupted while killing process", e); + } catch (Exception e) { + LOG.log(Level.FINE, "Error killing process", e); + } } /** diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index dff72581a..5517df631 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -54,6 +54,7 @@ public class CopilotClientOptions { private int port; private TelemetryConfig telemetry; private Integer sessionIdleTimeoutSeconds; + private boolean remote; private String tcpConnectionToken; private Boolean useLoggedInUser; private boolean useStdio = true; @@ -438,6 +439,37 @@ public CopilotClientOptions setPort(int port) { return this; } + /** + * Returns whether remote session support (Mission Control integration) is + * enabled. + *

+ * When {@code true}, sessions in a GitHub repository working directory are + * accessible from GitHub web and mobile. + * + * @return {@code true} if remote sessions are enabled + */ + public boolean isRemote() { + return remote; + } + + /** + * Enables remote session support (Mission Control integration). + *

+ * When {@code true}, sessions in a GitHub repository working directory are + * accessible from GitHub web and mobile. + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param remote + * {@code true} to enable remote sessions + * @return this options instance for method chaining + */ + public CopilotClientOptions setRemote(boolean remote) { + this.remote = remote; + return this; + } + /** * Gets the OpenTelemetry configuration for the CLI server. * @@ -599,6 +631,7 @@ public CopilotClientOptions clone() { copy.logLevel = this.logLevel; copy.onListModels = this.onListModels; copy.port = this.port; + copy.remote = this.remote; copy.sessionIdleTimeoutSeconds = this.sessionIdleTimeoutSeconds; copy.tcpConnectionToken = this.tcpConnectionToken; copy.telemetry = this.telemetry; diff --git a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java index 3b2995681..8947696c9 100644 --- a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java @@ -57,6 +57,18 @@ public class ProviderConfig { @JsonProperty("headers") private Map headers; + @JsonProperty("modelId") + private String modelId; + + @JsonProperty("wireModel") + private String wireModel; + + @JsonProperty("maxPromptTokens") + private Integer maxPromptTokens; + + @JsonProperty("maxOutputTokens") + private Integer maxOutputTokens; + /** * Gets the provider type. * @@ -225,4 +237,109 @@ public ProviderConfig setHeaders(Map headers) { this.headers = headers; return this; } + + /** + * Gets the well-known model name used by the runtime. + *

+ * Used to look up agent configuration (tools, prompts, reasoning behavior) and + * default token limits. Also used as the wire model when + * {@link #getWireModel()} is not set. + * + * @return the model ID, or {@code null} if not set + */ + public String getModelId() { + return modelId; + } + + /** + * Sets the well-known model name used by the runtime. + *

+ * Used to look up agent configuration (tools, prompts, reasoning behavior) and + * default token limits. Also used as the wire model when + * {@link #getWireModel()} is not set. Falls back to + * {@link SessionConfig#getModel()}. + * + * @param modelId + * the model ID + * @return this config for method chaining + */ + public ProviderConfig setModelId(String modelId) { + this.modelId = modelId; + return this; + } + + /** + * Gets the model name sent to the provider API for inference. + * + * @return the wire model name, or {@code null} if not set + */ + public String getWireModel() { + return wireModel; + } + + /** + * Sets the model name sent to the provider API for inference. + *

+ * Use this when the provider's model name (e.g. an Azure deployment name or a + * custom fine-tune name) differs from {@link #getModelId()}. Falls back to + * {@link #getModelId()}, then {@link SessionConfig#getModel()}. + * + * @param wireModel + * the wire model name + * @return this config for method chaining + */ + public ProviderConfig setWireModel(String wireModel) { + this.wireModel = wireModel; + return this; + } + + /** + * Gets the maximum prompt token override. + * + * @return the max prompt tokens, or {@code null} if not set + */ + public Integer getMaxPromptTokens() { + return maxPromptTokens; + } + + /** + * Sets the maximum prompt tokens override. + *

+ * Overrides the resolved model's default max prompt tokens. The runtime + * triggers conversation compaction before sending a request when the prompt + * (system message, history, tool definitions, user message) would exceed this + * limit. + * + * @param maxPromptTokens + * the max prompt tokens + * @return this config for method chaining + */ + public ProviderConfig setMaxPromptTokens(Integer maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + /** + * Gets the maximum output token override. + * + * @return the max output tokens, or {@code null} if not set + */ + public Integer getMaxOutputTokens() { + return maxOutputTokens; + } + + /** + * Sets the maximum output tokens override. + *

+ * Overrides the resolved model's default max output tokens. When hit, the model + * stops generating and returns a truncated response. + * + * @param maxOutputTokens + * the max output tokens + * @return this config for method chaining + */ + public ProviderConfig setMaxOutputTokens(Integer maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + return this; + } } diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index e1e08a275..ccf386640 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -383,6 +383,29 @@ var session = client.createSession( > **Note:** The `bearerToken` option accepts a **static token string** only. The SDK does not refresh this token automatically. If your token expires, requests will fail and you'll need to create a new session with a fresh token. +### Model Overrides + +Use `modelId` and `wireModel` to control model resolution and the model name on the wire: + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setProvider(new ProviderConfig() + .setType("openai") + .setBaseUrl("https://api.openai.com/v1") + .setApiKey("sk-...") + .setModelId("gpt-4o") // Runtime config lookup + .setWireModel("my-finetune-v3") // Sent to the provider API + .setMaxPromptTokens(100_000) // Override max prompt tokens + .setMaxOutputTokens(4096)) // Override max output tokens +).get(); +``` + +- **`modelId`** — Well-known model name used by the runtime to look up agent configuration (tools, prompts, reasoning behavior) and default token limits. Also used as the wire model when `wireModel` is not set. +- **`wireModel`** — Model name sent to the provider API for inference. Use when the provider's model name (e.g., an Azure deployment name or a custom fine-tune name) differs from `modelId`. +- **`maxPromptTokens`** — Overrides the resolved model's default max prompt tokens. The runtime triggers conversation compaction when the prompt would exceed this limit. +- **`maxOutputTokens`** — Overrides the resolved model's default max output tokens. + ### Microsoft Foundry Local [Microsoft Foundry Local](https://foundrylocal.ai) lets you run AI models locally on your own device with an OpenAI-compatible API. Install it via the Foundry Local CLI, then point the SDK at your local endpoint: @@ -1262,6 +1285,39 @@ This is more efficient than `listSessions()` when you already know the session I --- +## Remote Sessions + +Remote sessions enable Mission Control integration, making sessions accessible from GitHub web and mobile. When enabled, sessions in a GitHub repository working directory receive a remote URL. + +### Enabling Remote Sessions + +Set `remote(true)` on the client options to enable remote session support for all sessions: + +```java +var options = new CopilotClientOptions() + .setRemote(true) + .setCwd("/path/to/github-repo"); + +try (var client = new CopilotClient(options)) { + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + ).get(); + + // Listen for the remote URL info event + session.on(SessionInfoEvent.class, event -> { + System.out.println("Remote URL: " + event.getData()); + }); +} +``` + +### Prerequisites + +- The user must be authenticated (GitHub token or logged-in user) +- The session's working directory must be a GitHub repository +- This option is only used when the SDK spawns the CLI process; it is ignored when connecting to an external server via `setCliUrl()` + +--- + ## Next Steps - 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort diff --git a/src/test/java/com/github/copilot/sdk/CapiProxy.java b/src/test/java/com/github/copilot/sdk/CapiProxy.java index 30f843436..d91762d51 100644 --- a/src/test/java/com/github/copilot/sdk/CapiProxy.java +++ b/src/test/java/com/github/copilot/sdk/CapiProxy.java @@ -56,10 +56,12 @@ public class CapiProxy implements AutoCloseable { private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)"); + private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)(?:\\s+(\\{.*\\}))?$"); private Process process; private String proxyUrl; + private String connectProxyUrl; + private String caFilePath; private final HttpClient httpClient; private BufferedReader stdoutReader; @@ -137,7 +139,24 @@ public String start() throws IOException, InterruptedException { throw new IOException("Unexpected proxy output: " + line); } - proxyUrl = matcher.group(1); + String url = matcher.group(1); + + // Parse optional metadata (CONNECT proxy details) + String metadata = matcher.group(2); + if (metadata != null && !metadata.isEmpty()) { + try { + Map meta = MAPPER.readValue(metadata, new TypeReference>() { + }); + connectProxyUrl = meta.get("connectProxyUrl"); + caFilePath = meta.get("caFilePath"); + } catch (Exception e) { + process.destroyForcibly(); + throw new IOException("Failed to parse proxy startup metadata: " + metadata, e); + } + } + + // Only set proxyUrl after all parsing succeeds to avoid inconsistent state + proxyUrl = url; return proxyUrl; } @@ -329,6 +348,8 @@ public void stop(boolean skipWritingCache) throws IOException, InterruptedExcept process = null; proxyUrl = null; + connectProxyUrl = null; + caFilePath = null; } /** @@ -340,6 +361,24 @@ public String getProxyUrl() { return proxyUrl; } + /** + * Gets the CONNECT proxy URL for HTTPS interception. + * + * @return the CONNECT proxy URL, or null if not available + */ + public String getConnectProxyUrl() { + return connectProxyUrl; + } + + /** + * Gets the CA file path for trusting the CONNECT proxy's certificate. + * + * @return the CA file path, or null if not available + */ + public String getCaFilePath() { + return caFilePath; + } + /** * Checks if the proxy process is still alive and responsive. This does both a * process alive check AND an HTTP health check. diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index 74bee7b50..b060c278a 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -37,6 +37,8 @@ void copilotClientOptionsCloneBasic() { original.setGitHubToken("ghp_test"); original.setUseLoggedInUser(false); original.setCopilotHome("/custom/copilot/home"); + original.setRemote(true); + original.setSessionIdleTimeoutSeconds(600); original.setUseStdio(false); original.setTcpConnectionToken("my-token-123"); @@ -48,6 +50,8 @@ void copilotClientOptionsCloneBasic() { assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); assertEquals(original.getCopilotHome(), cloned.getCopilotHome()); + assertEquals(original.isRemote(), cloned.isRemote()); + assertEquals(original.getSessionIdleTimeoutSeconds(), cloned.getSessionIdleTimeoutSeconds()); assertEquals(original.getTcpConnectionToken(), cloned.getTcpConnectionToken()); } diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 3d9180bf9..58e8400e3 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -250,8 +250,37 @@ public Map getEnvironment() { Map env = new HashMap<>(System.getenv()); env.put("COPILOT_API_URL", proxyUrl); env.put("COPILOT_HOME", homeDir.toString()); + env.put("GH_CONFIG_DIR", homeDir.toString()); env.put("XDG_CONFIG_HOME", homeDir.toString()); env.put("XDG_STATE_HOME", homeDir.toString()); + + // Configure CONNECT proxy for HTTPS interception if available + String connectUrl = proxy.getConnectProxyUrl(); + String caFile = proxy.getCaFilePath(); + if (connectUrl != null && !connectUrl.isEmpty() && caFile != null && !caFile.isEmpty()) { + String noProxy = "127.0.0.1,localhost,::1"; + env.put("HTTP_PROXY", connectUrl); + env.put("HTTPS_PROXY", connectUrl); + env.put("http_proxy", connectUrl); + env.put("https_proxy", connectUrl); + env.put("NO_PROXY", noProxy); + env.put("no_proxy", noProxy); + env.put("NODE_EXTRA_CA_CERTS", caFile); + env.put("SSL_CERT_FILE", caFile); + env.put("REQUESTS_CA_BUNDLE", caFile); + env.put("CURL_CA_BUNDLE", caFile); + env.put("GIT_SSL_CAINFO", caFile); + env.put("GH_TOKEN", ""); + env.put("GITHUB_TOKEN", ""); + env.put("GH_ENTERPRISE_TOKEN", ""); + env.put("GITHUB_ENTERPRISE_TOKEN", ""); + } + + if ("true".equals(System.getenv("GITHUB_ACTIONS"))) { + env.put("GH_TOKEN", "fake-token-for-e2e-tests"); + env.put("GITHUB_TOKEN", "fake-token-for-e2e-tests"); + } + return env; } diff --git a/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java b/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java index d3e18010b..e7602619c 100644 --- a/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java +++ b/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java @@ -386,4 +386,52 @@ void testResumeSessionConfigWithoutProviderOmitsField() throws Exception { assertTrue(json.path("provider").isMissingNode(), "provider field should be omitted when null"); } + + // ========================================================================= + // Provider model and token limit overrides + // ========================================================================= + + @Test + void testProviderModelIdAndWireModelSerialization() throws Exception { + var provider = new ProviderConfig().setBaseUrl("https://example.com/provider") + .setHeaders(java.util.Map.of("Authorization", "Bearer provider-token")).setModelId("gpt-4o") + .setWireModel("my-finetune-v3").setMaxPromptTokens(100_000).setMaxOutputTokens(4096); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("https://example.com/provider", json.get("baseUrl").asText()); + assertEquals("Bearer provider-token", json.get("headers").get("Authorization").asText()); + assertEquals("gpt-4o", json.get("modelId").asText()); + assertEquals("my-finetune-v3", json.get("wireModel").asText()); + assertEquals(100_000, json.get("maxPromptTokens").asInt()); + assertEquals(4096, json.get("maxOutputTokens").asInt()); + + // Round-trip + ProviderConfig deserialized = MAPPER.readValue(MAPPER.writeValueAsString(provider), ProviderConfig.class); + assertEquals("gpt-4o", deserialized.getModelId()); + assertEquals("my-finetune-v3", deserialized.getWireModel()); + assertEquals(100_000, deserialized.getMaxPromptTokens()); + assertEquals(4096, deserialized.getMaxOutputTokens()); + } + + @Test + void testProviderModelFieldsDefaultToNull() { + var provider = new ProviderConfig(); + assertNull(provider.getModelId()); + assertNull(provider.getWireModel()); + assertNull(provider.getMaxPromptTokens()); + assertNull(provider.getMaxOutputTokens()); + } + + @Test + void testProviderModelFieldsOmittedWhenNull() throws Exception { + var provider = new ProviderConfig().setType("openai"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertTrue(json.path("modelId").isMissingNode()); + assertTrue(json.path("wireModel").isMissingNode()); + assertTrue(json.path("maxPromptTokens").isMissingNode()); + assertTrue(json.path("maxOutputTokens").isMissingNode()); + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java index 5dcb36604..4c2691d86 100644 --- a/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java +++ b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java @@ -18,6 +18,7 @@ import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ProviderConfig; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.SessionConfig; @@ -106,6 +107,49 @@ void testShouldApplyInstructionDirectoriesOnResume() throws Exception { } } + @Test + void testShouldForwardProviderWireModel() throws Exception { + ctx.configureForTest("session_config", "should_forward_provider_wire_model"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setModel("claude-sonnet-4.5") + .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) + .setApiKey("test-provider-key").setWireModel("test-wire-model") + .setMaxOutputTokens(1024)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + @SuppressWarnings("unchecked") + Map request = (Map) exchanges.get(0).get("request"); + assertEquals("test-wire-model", request.get("model")); + } + } + + @Test + void testShouldUseProviderModelIdAsWireModel() throws Exception { + ctx.configureForTest("session_config", "should_use_provider_model_id_as_wire_model"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) + .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5")) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + @SuppressWarnings("unchecked") + Map request = (Map) exchanges.get(0).get("request"); + assertEquals("claude-sonnet-4.5", request.get("model")); + } + } + @SuppressWarnings("unchecked") private static String getSystemMessage(Map exchange) { // The exchange structure is: { request: { messages: [...] }, response: ...,