* This method calls {@link #resolve()} to locate {@code runtime.node}, then
@@ -144,6 +150,73 @@ public static Path resolveEntrypoint() throws IOException {
return resolveEntrypoint(configuredCli, resolve());
}
+ /**
+ * Resolves an explicitly configured legacy CLI entrypoint, if it has a
+ * compatible adjacent runtime library.
+ *
+ * @return the absolute CLI path, or {@code null} when no compatible override is
+ * configured
+ * @throws IOException
+ * if the configured files cannot be inspected
+ */
+ public static Path resolveConfiguredEntrypoint() throws IOException {
+ String configuredCli = System.getenv(COPILOT_CLI_PATH_ENV);
+ if (configuredCli == null || configuredCli.isBlank()) {
+ return null;
+ }
+ Path configuredPath = Path.of(configuredCli).toAbsolutePath().normalize();
+ return resolveFromCliPath(configuredCli) != null && Files.isRegularFile(configuredPath)
+ && Files.size(configuredPath) > 0 ? configuredPath : null;
+ }
+
+ /**
+ * Resolves the out-of-process runtime wrapper from the platform classifier JAR
+ * and extracts it beside {@code runtime.node}.
+ *
+ * @return absolute path to the runtime wrapper executable
+ * @throws IOException
+ * if the classifier artifacts cannot be extracted
+ */
+ public static Path resolveRuntimeWrapper() throws IOException {
+ ClassLoader loader = NativeRuntimeLoader.class.getClassLoader();
+ String classifier = PlatformDetector.detectClassifier();
+ String version = readVersion(loader);
+ return resolveRuntimeWrapper(defaultCacheBase(), loader, classifier, version);
+ }
+
+ static Path resolveRuntimeWrapper(Path cacheBase, ClassLoader loader, String classifier, String version)
+ throws IOException {
+ Path runtimePath = extractRuntimeToCache(cacheBase, loader, classifier, version, DEFAULT_PUBLISHER, false);
+ Path cacheDir = runtimePath.getParent();
+ String wrapperName = classifier.startsWith("win32-")
+ ? RUNTIME_WRAPPER_FILENAME_WINDOWS
+ : RUNTIME_WRAPPER_FILENAME;
+ Path cachedWrapper = cacheDir.resolve(wrapperName);
+ if (isValidCachedCli(cachedWrapper)) {
+ return cachedWrapper;
+ }
+
+ String resourcePath = "native/" + classifier + "/" + wrapperName;
+ URL resource = loader.getResource(resourcePath);
+ if (resource == null) {
+ throw new FileNotFoundException("Runtime wrapper not found on classpath: " + resourcePath
+ + " — add the matching classifier JAR to the classpath");
+ }
+
+ Path temp = Files.createTempFile(cacheDir, "runtime-wrapper-tmp-", "");
+ try {
+ copyResourceToTemp(resource, resourcePath, temp);
+ makeExecutable(temp);
+ DEFAULT_PUBLISHER.publish(temp, cachedWrapper);
+ } finally {
+ tryDelete(temp);
+ }
+ if (!isValidCachedCli(cachedWrapper)) {
+ throw new IOException("Published runtime wrapper is not a non-empty executable file: " + cachedWrapper);
+ }
+ return cachedWrapper;
+ }
+
static Path resolveEntrypoint(String configuredCli, Path runtimePath) throws IOException {
if (configuredCli != null && !configuredCli.isBlank()) {
Path configuredPath = Path.of(configuredCli).toAbsolutePath().normalize();
@@ -318,6 +391,11 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier
*/
static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier, String version,
AtomicPublisher publisher) throws IOException {
+ return extractRuntimeToCache(cacheBase, loader, classifier, version, publisher, true);
+ }
+
+ private static Path extractRuntimeToCache(Path cacheBase, ClassLoader loader, String classifier, String version,
+ AtomicPublisher publisher, boolean extractCli) throws IOException {
String resourcePath = "native/" + classifier + "/" + RUNTIME_FILENAME;
String nativeVersion = readNativePackageVersion(loader, classifier);
Path cacheDir = cacheBase.resolve(version).resolve(nativeVersion).resolve(classifier);
@@ -325,7 +403,10 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier
// Step 1 — fast path: return an existing valid cache entry.
if (isValidCachedFile(cached)) {
- extractCliToCache(cacheDir, loader, classifier, publisher);
+ extractRuntimeAssetsToCache(cacheDir, loader, classifier, publisher);
+ if (extractCli) {
+ extractCliToCache(cacheDir, loader, classifier, publisher);
+ }
return cached;
}
@@ -348,12 +429,68 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier
tryDelete(temp);
}
- // Step 5 — also extract the copilot CLI executable alongside runtime.node.
- extractCliToCache(cacheDir, loader, classifier, publisher);
+ extractRuntimeAssetsToCache(cacheDir, loader, classifier, publisher);
+ if (extractCli) {
+ extractCliToCache(cacheDir, loader, classifier, publisher);
+ }
return cached;
}
+ private static void extractRuntimeAssetsToCache(Path cacheDir, ClassLoader loader, String classifier,
+ AtomicPublisher publisher) throws IOException {
+ String inventoryResourcePath = "native/" + classifier + "/" + RUNTIME_ASSETS_FILENAME;
+ URL inventoryResource = loader.getResource(inventoryResourcePath);
+ if (inventoryResource == null) {
+ return;
+ }
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(inventoryResource.openStream(), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.isBlank()) {
+ continue;
+ }
+ String[] fields = line.split("\\t", 2);
+ if (fields.length != 2) {
+ throw new IOException("Invalid runtime asset inventory entry: " + line);
+ }
+ boolean executable = (Integer.parseInt(fields[0], 8) & 0111) != 0;
+ Path relative = Path.of(fields[1]).normalize();
+ if (relative.isAbsolute() || relative.startsWith("..")) {
+ throw new IOException("Unsafe runtime asset inventory path: " + fields[1]);
+ }
+ Path cached = cacheDir.resolve(relative).normalize();
+ if (!cached.startsWith(cacheDir)) {
+ throw new IOException("Runtime asset escapes cache directory: " + fields[1]);
+ }
+ if (isValidCachedFile(cached) && (!executable || isWindows() || Files.isExecutable(cached))) {
+ continue;
+ }
+
+ String resourcePath = "native/" + classifier + "/" + fields[1];
+ URL resource = loader.getResource(resourcePath);
+ if (resource == null) {
+ throw new FileNotFoundException("Runtime asset not found on classpath: " + resourcePath);
+ }
+ Files.createDirectories(cached.getParent());
+ Path temp = Files.createTempFile(cached.getParent(), "runtime-asset-tmp-", "");
+ try {
+ copyResourceToTemp(resource, resourcePath, temp);
+ if (executable) {
+ makeExecutable(temp);
+ }
+ publisher.publish(temp, cached);
+ } finally {
+ tryDelete(temp);
+ }
+ }
+ } catch (NumberFormatException ex) {
+ throw new IOException("Invalid runtime asset mode in " + inventoryResourcePath, ex);
+ }
+ }
+
/**
* Extracts the copilot CLI executable from the classpath to the same cache
* directory as {@code runtime.node}. Idempotent — skips extraction if already
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java b/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java
new file mode 100644
index 0000000000..17c6a1333f
--- /dev/null
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java
@@ -0,0 +1,59 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Selects how the built-in {@code ask_user} tool collects user input.
+ */
+public enum AskUserVariant {
+
+ /** Uses the legacy question-and-answer experience. */
+ LEGACY("legacy"),
+
+ /** Uses structured elicitation to collect user input. */
+ ELICITATION("elicitation");
+
+ private final String value;
+
+ AskUserVariant(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the wire-format value.
+ *
+ * @return the value used in JSON serialization
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Creates an {@code AskUserVariant} from its wire-format value.
+ *
+ * @param value
+ * the wire-format value
+ * @return the matching variant, or {@code null} when {@code value} is
+ * {@code null}
+ * @throws IllegalArgumentException
+ * if the value is not {@code legacy} or {@code elicitation}
+ */
+ @JsonCreator
+ public static AskUserVariant fromValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ for (AskUserVariant variant : values()) {
+ if (variant.value.equals(value)) {
+ return variant;
+ }
+ }
+ throw new IllegalArgumentException("Unknown AskUserVariant value: " + value);
+ }
+}
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java
new file mode 100644
index 0000000000..f9117abfb2
--- /dev/null
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java
@@ -0,0 +1,63 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Routing tier for the {@code auto} model with Auto mode V2.
+ *
+ * @see CapiSessionOptions#setAutoTier(AutoTier)
+ */
+public enum AutoTier {
+
+ /** Prioritize efficiency. */
+ EFFICIENCY("efficiency"),
+
+ /** Balance efficiency and intelligence. */
+ BALANCE("balance"),
+
+ /** Prioritize intelligence. */
+ INTELLIGENCE("intelligence");
+
+ private final String value;
+
+ AutoTier(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the JSON value for this routing tier.
+ *
+ * @return the string value used in JSON serialization
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Deserializes a JSON string into its routing tier.
+ *
+ * @param value
+ * the JSON string value
+ * @return the matching tier, or {@code null} if value is {@code null}
+ * @throws IllegalArgumentException
+ * if the value does not match a known routing tier
+ */
+ @JsonCreator
+ public static AutoTier fromValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ for (AutoTier tier : values()) {
+ if (tier.value.equals(value)) {
+ return tier;
+ }
+ }
+ throw new IllegalArgumentException("Unknown AutoTier value: " + value);
+ }
+}
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java
index d94d59f67b..e401762302 100644
--- a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java
@@ -29,9 +29,40 @@
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CapiSessionOptions {
+ @JsonProperty("autoTier")
+ private AutoTier autoTier;
+
@JsonProperty("enableWebSocketResponses")
private Boolean enableWebSocketResponses;
+ /**
+ * Gets the routing tier for the {@code auto} model (Auto mode V2).
+ *
+ * @return the explicit tier, or {@code null} to leave tier selection to the
+ * runtime
+ */
+ public AutoTier getAutoTier() {
+ return autoTier;
+ }
+
+ /**
+ * Sets the routing tier, meaningful only with model {@code auto} (Auto mode
+ * V2). Requires a runtime version that supports {@code capi.autoTier}.
+ *
+ * When omitted, the runtime chooses its default on create and preserves the
+ * persisted or current tier on resume. An explicit tier overrides the persisted
+ * tier on cold resume; the runtime rejects a conflicting tier when resuming a
+ * session already resident in memory.
+ *
+ * @param autoTier
+ * the routing tier, or {@code null} to omit it from the request
+ * @return this config for method chaining
+ */
+ public CapiSessionOptions setAutoTier(AutoTier autoTier) {
+ this.autoTier = autoTier;
+ return this;
+ }
+
/**
* Gets whether CAPI Responses API WebSocket transport is enabled.
*
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java
new file mode 100644
index 0000000000..ee9d97e1e9
--- /dev/null
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java
@@ -0,0 +1,151 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Identity of the integrating application, declared on the
+ * {@code server.connect} handshake.
+ *
+ * Declaring it lets the telemetry the runtime emits on the connection be
+ * attributed to a single, consistent surface (the application and its Copilot
+ * integration) instead of the runtime's own build. All fields are optional; an
+ * empty field is omitted from the handshake.
+ *
+ *
environment;
@@ -192,19 +193,20 @@ public CopilotClientOptions setCliArgs(String[] cliArgs) {
}
/**
- * Gets the path to the Copilot CLI executable.
+ * Gets the path to an explicitly configured Copilot executable.
*
- * @return the CLI path, or {@code null} to use "copilot" from PATH
+ * @return the executable path, or {@code null} to use the bundled runtime
+ * wrapper
*/
public String getCliPath() {
return cliPath;
}
/**
- * Sets the path to the Copilot CLI executable.
+ * Sets the path to the Copilot CLI or runtime wrapper executable.
*
* @param cliPath
- * the path to the CLI executable
+ * the path to the executable
* @return this options instance for method chaining
*/
public CopilotClientOptions setCliPath(String cliPath) {
@@ -677,6 +679,36 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) {
return this;
}
+ /**
+ * Gets the integrating application's declared identity.
+ *
+ * @return the client info, or {@code null}
+ * @since 1.6.0
+ */
+ public ClientInfo getClientInfo() {
+ return clientInfo;
+ }
+
+ /**
+ * Declares the integrating application's identity, forwarded to the runtime on
+ * the {@code server.connect} handshake.
+ *
+ * Declaring it lets the telemetry the runtime emits on this connection be
+ * attributed to a consistent surface (the application and its Copilot
+ * integration) instead of the runtime's own build. All fields on
+ * {@link ClientInfo} are optional; leave this unset to keep the runtime's
+ * default attribution.
+ *
+ * @param clientInfo
+ * the application identity to declare
+ * @return this options instance for method chaining
+ * @since 1.6.0
+ */
+ public CopilotClientOptions setClientInfo(ClientInfo clientInfo) {
+ this.clientInfo = Objects.requireNonNull(clientInfo, "clientInfo must not be null");
+ return this;
+ }
+
/**
* Gets the server-wide idle timeout for sessions in seconds.
*
@@ -829,6 +861,7 @@ public CopilotClientOptions clone() {
copy.cliPath = this.cliPath;
copy.cliUrl = this.cliUrl;
copy.connection = this.connection;
+ copy.clientInfo = this.clientInfo;
copy.copilotHome = this.copilotHome;
copy.cwd = this.cwd;
copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null;
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java
index 403893987d..f6b5001e7a 100644
--- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java
@@ -45,6 +45,9 @@ public final class CreateSessionRequest {
@JsonProperty("contextTier")
private String contextTier;
+ @JsonProperty("askUserVariant")
+ private AskUserVariant askUserVariant;
+
@JsonProperty("tools")
private List tools;
@@ -236,6 +239,9 @@ public final class CreateSessionRequest {
@JsonProperty("expAssignments")
private CopilotExpAssignmentResponse expAssignments;
+ @JsonProperty("featureFlags")
+ private Map featureFlags;
+
@JsonProperty("enableManagedSettings")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean enableManagedSettings;
@@ -309,6 +315,16 @@ public void setContextTier(String contextTier) {
this.contextTier = contextTier;
}
+ /** Gets the ask-user variant. @return the ask-user variant */
+ public AskUserVariant getAskUserVariant() {
+ return askUserVariant;
+ }
+
+ /** Sets the ask-user variant. @param askUserVariant the ask-user variant */
+ public void setAskUserVariant(AskUserVariant askUserVariant) {
+ this.askUserVariant = askUserVariant;
+ }
+
/** Gets the tools. @return the tool definitions */
public List getTools() {
return tools == null ? null : Collections.unmodifiableList(tools);
@@ -1113,6 +1129,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) {
this.expAssignments = expAssignments;
}
+ /** Gets host-resolved feature flags. @return the feature flags */
+ public Map getFeatureFlags() {
+ return featureFlags;
+ }
+
+ /** Sets host-resolved feature flags. @param featureFlags the feature flags */
+ public void setFeatureFlags(Map featureFlags) {
+ this.featureFlags = featureFlags;
+ }
+
/**
* Gets the self-fetch managed settings flag. @return the flag, or {@code null}
* if not set
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java
index a55c3454e7..ac4f71e07b 100644
--- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java
@@ -63,6 +63,7 @@ public class ResumeSessionConfig {
private String reasoningEffort;
private String reasoningSummary;
private String contextTier;
+ private AskUserVariant askUserVariant;
private ModelCapabilitiesOverride modelCapabilities;
private PermissionHandler onPermissionRequest;
private McpAuthHandler onMcpAuthRequest;
@@ -111,6 +112,7 @@ public class ResumeSessionConfig {
private String remoteSession;
private CopilotExpAssignmentResponse expAssignments;
private Boolean enableManagedSettings;
+ private Map featureFlags;
private ManagedSettings managedSettings;
/**
@@ -793,6 +795,31 @@ public ResumeSessionConfig setContextTier(String contextTier) {
return this;
}
+ /**
+ * Gets the experience used by the built-in {@code ask_user} tool.
+ *
+ * @return the ask-user variant, or {@code null} to use the legacy experience
+ */
+ public AskUserVariant getAskUserVariant() {
+ return askUserVariant;
+ }
+
+ /**
+ * Sets the model-facing shape of the built-in {@code ask_user} tool when the
+ * session is resumed by a new client.
+ *
+ * When unset, the option is omitted and the legacy shape is used. Set an
+ * elicitation handler when selecting {@link AskUserVariant#ELICITATION}.
+ *
+ * @param askUserVariant
+ * the ask-user variant
+ * @return this config instance for method chaining
+ */
+ public ResumeSessionConfig setAskUserVariant(AskUserVariant askUserVariant) {
+ this.askUserVariant = askUserVariant;
+ return this;
+ }
+
/**
* Gets the permission request handler.
*
@@ -1969,6 +1996,23 @@ public ResumeSessionConfig setExpAssignments(CopilotExpAssignmentResponse expAss
return this;
}
+ /** Gets host-resolved feature-flag values. @return the feature flags */
+ public Map getFeatureFlags() {
+ return featureFlags;
+ }
+
+ /**
+ * Sets feature-flag values resolved by the host to apply on resume.
+ *
+ * @param featureFlags
+ * the feature flags
+ * @return this config for method chaining
+ */
+ public ResumeSessionConfig setFeatureFlags(Map featureFlags) {
+ this.featureFlags = featureFlags;
+ return this;
+ }
+
/**
* Gets whether the runtime self-fetches enterprise managed settings at session
* bootstrap on resume.
@@ -2052,6 +2096,7 @@ public ResumeSessionConfig clone() {
copy.reasoningEffort = this.reasoningEffort;
copy.reasoningSummary = this.reasoningSummary;
copy.contextTier = this.contextTier;
+ copy.askUserVariant = this.askUserVariant;
copy.modelCapabilities = this.modelCapabilities;
copy.onPermissionRequest = this.onPermissionRequest;
copy.onUserInputRequest = this.onUserInputRequest;
@@ -2102,6 +2147,7 @@ public ResumeSessionConfig clone() {
copy.gitHubToken = this.gitHubToken;
copy.gitHubTokenProvider = this.gitHubTokenProvider;
copy.remoteSession = this.remoteSession;
+ copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null;
copy.expAssignments = this.expAssignments;
copy.enableManagedSettings = this.enableManagedSettings;
copy.managedSettings = this.managedSettings;
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java
index 9b8e897fda..776d58137b 100644
--- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java
@@ -47,6 +47,9 @@ public final class ResumeSessionRequest {
@JsonProperty("contextTier")
private String contextTier;
+ @JsonProperty("askUserVariant")
+ private AskUserVariant askUserVariant;
+
@JsonProperty("tools")
private List tools;
@@ -238,6 +241,9 @@ public final class ResumeSessionRequest {
@JsonProperty("expAssignments")
private CopilotExpAssignmentResponse expAssignments;
+ @JsonProperty("featureFlags")
+ private Map featureFlags;
+
@JsonProperty("enableManagedSettings")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean enableManagedSettings;
@@ -311,6 +317,16 @@ public void setContextTier(String contextTier) {
this.contextTier = contextTier;
}
+ /** Gets the ask-user variant. @return the ask-user variant */
+ public AskUserVariant getAskUserVariant() {
+ return askUserVariant;
+ }
+
+ /** Sets the ask-user variant. @param askUserVariant the ask-user variant */
+ public void setAskUserVariant(AskUserVariant askUserVariant) {
+ this.askUserVariant = askUserVariant;
+ }
+
/** Gets the tools. @return the tool definitions */
public List getTools() {
return tools == null ? null : Collections.unmodifiableList(tools);
@@ -1128,6 +1144,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) {
this.expAssignments = expAssignments;
}
+ /** Gets host-resolved feature flags. @return the feature flags */
+ public Map getFeatureFlags() {
+ return featureFlags;
+ }
+
+ /** Sets host-resolved feature flags. @param featureFlags the feature flags */
+ public void setFeatureFlags(Map featureFlags) {
+ this.featureFlags = featureFlags;
+ }
+
/**
* Gets the self-fetch managed settings flag. @return the flag, or {@code null}
* if not set
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java
index 9f6ddb5efa..cbcacd9771 100644
--- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java
@@ -46,6 +46,7 @@ public class SessionConfig {
private String reasoningEffort;
private String reasoningSummary;
private String contextTier;
+ private AskUserVariant askUserVariant;
private List tools;
private SystemMessageConfig systemMessage;
private List availableTools;
@@ -112,6 +113,7 @@ public class SessionConfig {
private CloudSessionOptions cloud;
private CopilotExpAssignmentResponse expAssignments;
private Boolean enableManagedSettings;
+ private Map featureFlags;
private ManagedSettings managedSettings;
/**
@@ -254,6 +256,30 @@ public SessionConfig setContextTier(String contextTier) {
return this;
}
+ /**
+ * Gets the experience used by the built-in {@code ask_user} tool.
+ *
+ * @return the ask-user variant, or {@code null} to use the legacy experience
+ */
+ public AskUserVariant getAskUserVariant() {
+ return askUserVariant;
+ }
+
+ /**
+ * Sets the model-facing shape of the built-in {@code ask_user} tool.
+ *
+ * When unset, the option is omitted and the legacy shape is used. Set an
+ * elicitation handler when selecting {@link AskUserVariant#ELICITATION}.
+ *
+ * @param askUserVariant
+ * the ask-user variant
+ * @return this config instance for method chaining
+ */
+ public SessionConfig setAskUserVariant(AskUserVariant askUserVariant) {
+ this.askUserVariant = askUserVariant;
+ return this;
+ }
+
/**
* Gets the custom tools for this session.
*
@@ -894,7 +920,9 @@ public UserInputHandler getOnUserInputRequest() {
/**
* Sets a handler for user input requests from the agent.
*
- * When provided, enables the ask_user tool for the agent to request user input.
+ * When provided, enables the legacy question-and-answer form of the
+ * {@code ask_user} tool. Use an elicitation handler with
+ * {@link AskUserVariant#ELICITATION}.
*
* @param onUserInputRequest
* the user input handler
@@ -2097,6 +2125,23 @@ public SessionConfig setExpAssignments(CopilotExpAssignmentResponse expAssignmen
return this;
}
+ /** Gets host-resolved feature-flag values. @return the feature flags */
+ public Map getFeatureFlags() {
+ return featureFlags;
+ }
+
+ /**
+ * Sets feature-flag values resolved by the host for this session.
+ *
+ * @param featureFlags
+ * the feature flags
+ * @return this config instance for method chaining
+ */
+ public SessionConfig setFeatureFlags(Map featureFlags) {
+ this.featureFlags = featureFlags;
+ return this;
+ }
+
/**
* Gets whether the runtime self-fetches enterprise managed settings at session
* bootstrap.
@@ -2173,6 +2218,7 @@ public SessionConfig clone() {
copy.reasoningEffort = this.reasoningEffort;
copy.reasoningSummary = this.reasoningSummary;
copy.contextTier = this.contextTier;
+ copy.askUserVariant = this.askUserVariant;
copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null;
copy.systemMessage = this.systemMessage;
copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null;
@@ -2243,6 +2289,7 @@ public SessionConfig clone() {
copy.gitHubTokenProvider = this.gitHubTokenProvider;
copy.remoteSession = this.remoteSession;
copy.cloud = this.cloud;
+ copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null;
copy.expAssignments = this.expAssignments;
copy.enableManagedSettings = this.enableManagedSettings;
copy.managedSettings = this.managedSettings;
diff --git a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java
index 17e8f131f7..dccb4e9add 100644
--- a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java
@@ -9,12 +9,16 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import com.fasterxml.jackson.databind.JsonNode;
+import com.github.copilot.rpc.AutoTier;
import com.github.copilot.rpc.CapiSessionOptions;
import com.github.copilot.rpc.ResumeSessionConfig;
import com.github.copilot.rpc.SessionConfig;
@@ -29,6 +33,7 @@ void defaultsAreNull() {
var capi = new CapiSessionOptions();
assertNull(capi.getEnableWebSocketResponses());
+ assertNull(capi.getAutoTier());
}
@Test
@@ -37,6 +42,8 @@ void fluentSetterReturnsSameInstance() {
assertSame(capi, capi.setEnableWebSocketResponses(true));
assertEquals(Boolean.TRUE, capi.getEnableWebSocketResponses());
+ assertSame(capi, capi.setAutoTier(AutoTier.BALANCE));
+ assertEquals(AutoTier.BALANCE, capi.getAutoTier());
}
@Test
@@ -46,6 +53,7 @@ void serializesEnableWebSocketResponses() {
JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi);
assertTrue(json.get("enableWebSocketResponses").asBoolean());
+ assertTrue(json.path("autoTier").isMissingNode());
}
@Test
@@ -55,6 +63,44 @@ void omitsUnsetEnableWebSocketResponses() {
JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi);
assertTrue(json.path("enableWebSocketResponses").isMissingNode());
+ assertTrue(json.path("autoTier").isMissingNode());
+ assertEquals(0, json.size());
+ }
+
+ @ParameterizedTest
+ @CsvSource({"EFFICIENCY,efficiency", "BALANCE,balance", "INTELLIGENCE,intelligence"})
+ void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) throws Exception {
+ var mapper = JsonRpcClient.getObjectMapper();
+ var capi = new CapiSessionOptions().setAutoTier(tier);
+ JsonNode json = mapper.valueToTree(capi);
+ assertEquals(value, json.get("autoTier").asText());
+ assertEquals(1, json.size());
+ assertEquals(tier, mapper.treeToValue(json, CapiSessionOptions.class).getAutoTier());
+
+ capi.setEnableWebSocketResponses(false);
+ var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setModel("auto").setCapi(capi),
+ "session-1");
+ var resume = SessionRequestBuilder.buildResumeRequest("session-1", new ResumeSessionConfig().setCapi(capi));
+ for (Object request : new Object[]{create, resume}) {
+ JsonNode requestJson = mapper.valueToTree(request);
+ assertEquals(value, requestJson.get("capi").get("autoTier").asText());
+ assertFalse(requestJson.get("capi").get("enableWebSocketResponses").asBoolean());
+ assertEquals(2, requestJson.get("capi").size());
+ }
+ }
+
+ @Test
+ void autoTierRejectsNoncanonicalValues() {
+ for (String value : new String[]{"balanced", "Balance", "unknown"}) {
+ assertThrows(IllegalArgumentException.class, () -> AutoTier.fromValue(value));
+ }
+ assertNull(AutoTier.fromValue(null));
+ }
+
+ @Test
+ void clearingAutoTierOmitsIt() {
+ var capi = new CapiSessionOptions().setAutoTier(AutoTier.BALANCE).setAutoTier(null);
+ JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi);
assertEquals(0, json.size());
}
@@ -67,6 +113,7 @@ void createRequestIncludesCapiWhenSet() {
assertNotNull(request.getCapi());
assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean());
+ assertTrue(json.get("capi").path("autoTier").isMissingNode());
}
@Test
@@ -89,6 +136,7 @@ void resumeRequestIncludesCapiWhenSet() {
assertNotNull(request.getCapi());
assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean());
+ assertTrue(json.get("capi").path("autoTier").isMissingNode());
}
@Test
diff --git a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java
index 68555a35b4..a227be04b9 100644
--- a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java
@@ -9,9 +9,13 @@
import java.io.IOException;
import java.net.ServerSocket;
import java.net.URI;
+import java.nio.file.Path;
+import java.util.Map;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import com.github.copilot.ffi.NativeRuntimeLoader;
import com.github.copilot.rpc.CopilotClientOptions;
import com.github.copilot.rpc.TelemetryConfig;
@@ -22,6 +26,47 @@
*/
class CliServerManagerTest {
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void explicitCliPathDoesNotRequireRuntimeBundle() throws Exception {
+ Path explicit = tempDir.resolve("copilot");
+ var manager = new CliServerManager(new CopilotClientOptions().setCliPath(explicit.toString()));
+
+ assertEquals(explicit.toString(), manager.resolveCliLaunch().executable());
+ }
+
+ @Test
+ void inheritedCliPathEnvironmentOverrideDoesNotRequireRuntimeBundle() throws Exception {
+ Path inherited = tempDir.resolve("copilot-runtime");
+ var manager = new CliServerManager(new CopilotClientOptions());
+
+ assertEquals(inherited.toString(), manager.resolveCliLaunch(inherited.toString()).executable());
+ }
+
+ @Test
+ void configuredEnvironmentCliPathOverridesInheritedEnvironment() throws Exception {
+ Path inherited = tempDir.resolve("inherited-copilot-runtime");
+ Path configured = tempDir.resolve("configured-copilot-runtime");
+ var options = new CopilotClientOptions()
+ .setEnvironment(Map.of(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV, configured.toString()));
+ var manager = new CliServerManager(options);
+
+ assertEquals(configured.toString(), manager.resolveCliLaunch(inherited.toString()).executable());
+ }
+
+ @Test
+ void explicitCliPathOverridesEnvironment() throws Exception {
+ Path explicit = tempDir.resolve("explicit-copilot-runtime");
+ Path configured = tempDir.resolve("configured-copilot-runtime");
+ var options = new CopilotClientOptions().setCliPath(explicit.toString())
+ .setEnvironment(Map.of(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV, configured.toString()));
+ var manager = new CliServerManager(options);
+
+ assertEquals(explicit.toString(), manager.resolveCliLaunch("inherited-copilot-runtime").executable());
+ }
+
// ===== parseCliUrl tests =====
@Test
diff --git a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java
index 4c5a3fbef0..2433e5f67a 100644
--- a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java
@@ -18,6 +18,7 @@
import com.github.copilot.generated.SessionEvent;
import com.github.copilot.generated.rpc.SessionLimitsConfig;
import com.github.copilot.rpc.AutoModeSwitchResponse;
+import com.github.copilot.rpc.AskUserVariant;
import com.github.copilot.rpc.CopilotClientOptions;
import com.github.copilot.rpc.DefaultAgentConfig;
import com.github.copilot.rpc.ExitPlanModeResult;
@@ -119,6 +120,7 @@ void sessionConfigCloneBasic() {
original.setModel("gpt-4o");
original.setReasoningSummary("detailed");
original.setContextTier("long_context");
+ original.setAskUserVariant(AskUserVariant.ELICITATION);
original.setPluginDirectories(List.of("/plugins/a", "/plugins/b"));
original.setDisabledMcpServers(List.of("local-files", "remote-github"));
original.setLargeOutput(
@@ -133,6 +135,7 @@ void sessionConfigCloneBasic() {
assertEquals(original.getModel(), cloned.getModel());
assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary());
assertEquals(original.getContextTier(), cloned.getContextTier());
+ assertEquals(original.getAskUserVariant(), cloned.getAskUserVariant());
assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories());
assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers());
assertEquals(original.getLargeOutput(), cloned.getLargeOutput());
@@ -198,6 +201,7 @@ void resumeSessionConfigCloneBasic() {
original.setModel("o1");
original.setReasoningSummary("none");
original.setContextTier("long_context");
+ original.setAskUserVariant(AskUserVariant.LEGACY);
original.setPluginDirectories(List.of("/plugins/r"));
original.setDisabledMcpServers(List.of("local-files-r"));
original.setLargeOutput(
@@ -210,6 +214,7 @@ void resumeSessionConfigCloneBasic() {
assertEquals(original.getModel(), cloned.getModel());
assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary());
assertEquals(original.getContextTier(), cloned.getContextTier());
+ assertEquals(original.getAskUserVariant(), cloned.getAskUserVariant());
assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories());
assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers());
assertEquals(original.getLargeOutput(), cloned.getLargeOutput());
diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java
index 3025c64c39..b4159d839c 100644
--- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java
+++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java
@@ -77,11 +77,11 @@ void threadsSessionIdForCapiAndByok() throws Exception {
// BYOK session.
int before = handler.inferenceRequests().size();
ProviderConfig provider = new ProviderConfig().setType("openai").setWireApi("responses")
- .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-4.5")
- .setWireModel("claude-sonnet-4.5");
+ .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-5")
+ .setWireModel("claude-sonnet-5");
CopilotSession byokSession = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
- .setModel("claude-sonnet-4.5").setProvider(provider))
+ .setModel("claude-sonnet-5").setProvider(provider))
.get();
String byokSessionId = byokSession.getSessionId();
diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java
index aa173ef30e..fa2a6354be 100644
--- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java
+++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java
@@ -134,7 +134,7 @@ static String anthropicMessageSseBody(String text) {
startMessage.put("id", "msg_stub_1");
startMessage.put("type", "message");
startMessage.put("role", "assistant");
- startMessage.put("model", "claude-sonnet-4.5");
+ startMessage.put("model", "claude-sonnet-5");
startMessage.put("content", List.of());
startMessage.put("stop_reason", null);
startMessage.put("stop_sequence", null);
@@ -251,7 +251,7 @@ static HttpResponse buildInferenceResponse(String url, String bodyT
body.put("id", "msg_stub_1");
body.put("type", "message");
body.put("role", "assistant");
- body.put("model", "claude-sonnet-4.5");
+ body.put("model", "claude-sonnet-5");
body.put("content", List.of(Map.of("type", "text", "text", text)));
body.put("stop_reason", "end_turn");
body.put("stop_sequence", null);
@@ -301,14 +301,14 @@ static String modelCatalog(List supportedEndpoints) {
Map capabilities = new LinkedHashMap<>();
capabilities.put("type", "chat");
- capabilities.put("family", "claude-sonnet-4.5");
+ capabilities.put("family", "claude-sonnet-5");
capabilities.put("tokenizer", "o200k_base");
capabilities.put("limits", limits);
capabilities.put("supports", supports);
Map model = new LinkedHashMap<>();
- model.put("id", "claude-sonnet-4.5");
- model.put("name", "Claude Sonnet 4.5");
+ model.put("id", "claude-sonnet-5");
+ model.put("name", "Claude Sonnet 5");
model.put("object", "model");
model.put("vendor", "Anthropic");
model.put("version", "1");
@@ -416,7 +416,7 @@ private static Map chatChunkBase() {
base.put("id", "chatcmpl-stub-1");
base.put("object", "chat.completion.chunk");
base.put("created", 1);
- base.put("model", "claude-sonnet-4.5");
+ base.put("model", "claude-sonnet-5");
return base;
}
@@ -459,7 +459,7 @@ private static Map chatCompletion(String text) {
root.put("id", "chatcmpl-stub-1");
root.put("object", "chat.completion");
root.put("created", 1);
- root.put("model", "claude-sonnet-4.5");
+ root.put("model", "claude-sonnet-5");
root.put("choices", List.of(choice));
root.put("usage", chatUsage());
return root;
diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java
index 7b0deb9977..5a6e378b2a 100644
--- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java
@@ -24,6 +24,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.github.copilot.generated.rpc.GitHubTelemetryNotification;
+import com.github.copilot.rpc.ClientInfo;
import com.github.copilot.rpc.CopilotClientOptions;
import com.github.copilot.rpc.PermissionHandler;
import com.github.copilot.rpc.ResumeSessionConfig;
@@ -204,6 +205,96 @@ void clientOmitsForwardingWhenNoHandler() throws Exception {
}
}
+ @Test
+ void connectForwardsDeclaredClientInfo() throws Exception {
+ try (var server = new FakeRuntimeServer();
+ var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url())
+ .setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal")
+ .setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant")
+ .setIntegrationVersion("1.5.0")))) {
+
+ client.start().get(15, TimeUnit.SECONDS);
+
+ JsonNode connectParams = server.awaitConnect();
+ JsonNode clientInfo = connectParams.path("clientInfo");
+ assertEquals(4, clientInfo.size(), "clientInfo should carry only the four declared fields");
+ assertEquals("acme-developer-portal", clientInfo.path("editorName").asText());
+ assertEquals("2.4.0", clientInfo.path("editorVersion").asText());
+ assertEquals("copilot-assistant", clientInfo.path("extensionName").asText());
+ assertEquals("1.5.0", clientInfo.path("extensionVersion").asText());
+ }
+ }
+
+ @Test
+ void connectOmitsClientInfoWhenUnset() throws Exception {
+ try (var server = new FakeRuntimeServer();
+ var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) {
+
+ client.start().get(15, TimeUnit.SECONDS);
+
+ JsonNode connectParams = server.awaitConnect();
+ assertFalse(connectParams.has("clientInfo"),
+ "connect request should omit clientInfo when none was declared");
+ }
+ }
+
+ @Test
+ void connectOmitsEmptyClientInfoFields() throws Exception {
+ try (var server = new FakeRuntimeServer();
+ var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url())
+ .setClientInfo(new ClientInfo().setApplicationName("example-app")))) {
+
+ client.start().get(15, TimeUnit.SECONDS);
+
+ JsonNode connectParams = server.awaitConnect();
+ JsonNode clientInfo = connectParams.path("clientInfo");
+ assertEquals("example-app", clientInfo.path("editorName").asText());
+ assertFalse(clientInfo.has("editorVersion"), "unset editorVersion should be omitted");
+ assertFalse(clientInfo.has("extensionName"), "unset extensionName should be omitted");
+ assertFalse(clientInfo.has("extensionVersion"), "unset extensionVersion should be omitted");
+ }
+ }
+
+ @Test
+ void connectDropsEmptyClientInfoFields() throws Exception {
+ try (var server = new FakeRuntimeServer();
+ var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url())
+ .setClientInfo(new ClientInfo().setApplicationName("example-app").setApplicationVersion("")))) {
+
+ client.start().get(15, TimeUnit.SECONDS);
+
+ JsonNode connectParams = server.awaitConnect();
+ JsonNode clientInfo = connectParams.path("clientInfo");
+ assertEquals(1, clientInfo.size(), "clientInfo should carry only the non-empty field");
+ assertEquals("example-app", clientInfo.path("editorName").asText());
+ assertFalse(clientInfo.has("editorVersion"), "empty editorVersion should be dropped");
+ }
+ }
+
+ @Test
+ void connectOmitsAllEmptyClientInfo() throws Exception {
+ try (var server = new FakeRuntimeServer();
+ var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url())
+ .setClientInfo(new ClientInfo().setApplicationName("").setApplicationVersion("")
+ .setIntegrationName("").setIntegrationVersion("")))) {
+
+ client.start().get(15, TimeUnit.SECONDS);
+
+ JsonNode connectParams = server.awaitConnect();
+ assertFalse(connectParams.has("clientInfo"), "connect request should omit an all-empty clientInfo");
+ }
+ }
+
+ @Test
+ void optionsRetainAndCloneClientInfo() {
+ var info = new ClientInfo().setApplicationName("example-app");
+ var options = new CopilotClientOptions().setClientInfo(info);
+ assertSame(info, options.getClientInfo());
+
+ var copy = options.clone();
+ assertSame(info, copy.getClientInfo());
+ }
+
@Test
void optionsRetainAndCloneTelemetryHandler() {
Function> handler = n -> CompletableFuture
diff --git a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java
index 2393f334b2..6c9753025a 100644
--- a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java
+++ b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java
@@ -135,7 +135,7 @@ void testShouldCallRpcModelsListWithTypedResult() throws Exception {
var result = client.getRpc().models.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertNotNull(result.models());
- assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-4.5".equals(model.id())));
+ assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-5".equals(model.id())));
result.models().forEach(model -> {
assertFalse(model.id().isBlank());
assertFalse(model.name().isBlank());
diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java
index 18045f3e86..f7fc58d429 100644
--- a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java
+++ b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java
@@ -142,7 +142,7 @@ void testShouldUpdateAndClearLiveSubagentSettings() throws Exception {
session.getRpc().tools.updateSubagentSettings(new SessionToolsUpdateSubagentSettingsParams(null,
new SessionToolsUpdateSubagentSettingsParams.SessionToolsUpdateSubagentSettingsParamsSubagents(
Map.of("general-purpose",
- new SubagentSettingsEntry("gpt-5-mini", "low",
+ new SubagentSettingsEntry("gpt-5-mini", null, "low",
SubagentSettingsEntryContextTier.LONG_CONTEXT)),
List.of("legacy-agent"), null, null)))
.get(30, TimeUnit.SECONDS);
diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java
new file mode 100644
index 0000000000..5213cdbb8d
--- /dev/null
+++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java
@@ -0,0 +1,67 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import com.github.copilot.generated.AutoTier;
+import com.github.copilot.generated.SessionEvent;
+import com.github.copilot.generated.SessionResumeEvent;
+import com.github.copilot.generated.SessionStartEvent;
+
+/**
+ * Verifies auto routing preferences on generated session lifecycle events.
+ */
+class SessionAutoTierEventTest {
+
+ private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
+
+ @ParameterizedTest
+ @CsvSource({"session.start,EFFICIENCY,efficiency", "session.start,BALANCE,balance",
+ "session.start,INTELLIGENCE,intelligence", "session.resume,EFFICIENCY,efficiency",
+ "session.resume,BALANCE,balance", "session.resume,INTELLIGENCE,intelligence"})
+ void canonicalAutoTierRoundTrips(String type, AutoTier tier, String value) throws Exception {
+ String json = """
+ {"type":"%s","data":{"selectedModel":"auto","autoTier":"%s"}}
+ """.formatted(type, value);
+
+ var event = MAPPER.readValue(json, SessionEvent.class);
+ assertEquals(tier, autoTier(event, type));
+ String serialized = MAPPER.writeValueAsString(event);
+ assertEquals(value, MAPPER.readTree(serialized).path("data").path("autoTier").asText());
+ assertEquals(tier, autoTier(MAPPER.readValue(serialized, SessionEvent.class), type));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"session.start", "session.resume"})
+ void missingOrNullAutoTierRemainsOptional(String type) throws Exception {
+ for (String data : new String[]{"{}", "{\"autoTier\":null}"}) {
+ String json = """
+ {"type":"%s","data":%s}
+ """.formatted(type, data);
+
+ var event = MAPPER.readValue(json, SessionEvent.class);
+ assertNull(autoTier(event, type));
+ var serialized = MAPPER.readTree(MAPPER.writeValueAsString(event));
+ assertFalse(serialized.path("data").has("autoTier"));
+ }
+ }
+
+ private static AutoTier autoTier(SessionEvent event, String type) {
+ if ("session.start".equals(type)) {
+ return assertInstanceOf(SessionStartEvent.class, event).getData().autoTier();
+ }
+ return assertInstanceOf(SessionResumeEvent.class, event).getData().autoTier();
+ }
+}
diff --git a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java
index 925fd6d873..e786bda994 100644
--- a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java
+++ b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java
@@ -125,7 +125,7 @@ void testShouldForwardProviderWireModel() throws Exception {
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
- .createSession(new SessionConfig().setModel("claude-sonnet-4.5")
+ .createSession(new SessionConfig().setModel("claude-sonnet-5")
.setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl())
.setApiKey("test-provider-key").setWireModel("test-wire-model")
.setMaxOutputTokens(1024))
@@ -149,7 +149,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception {
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"))
+ .setApiKey("test-provider-key").setModelId("claude-sonnet-5"))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS);
@@ -158,7 +158,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception {
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"));
+ assertEquals("claude-sonnet-5", request.get("model"));
}
}
@@ -272,7 +272,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnCreate() throws Excep
var handler = new CopilotRequestTestSupport.RecordingRequestHandler(SYNTHETIC_TEXT);
try (CopilotClient client = newLlmClient(ctx, handler)) {
- CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-4.5")
+ CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5")
.setEnableCitations(true).setProvider(createAnthropicProvider())
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
@@ -296,7 +296,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnResume() throws Excep
CopilotSession session1 = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
CopilotSession session2 = client.resumeSession(session1.getSessionId(),
- new ResumeSessionConfig().setModel("claude-sonnet-4.5").setEnableCitations(true)
+ new ResumeSessionConfig().setModel("claude-sonnet-5").setEnableCitations(true)
.setProvider(createAnthropicProvider())
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL))
.get();
@@ -388,7 +388,7 @@ private static BlobAttachment createPdfAttachment() {
private static ProviderConfig createAnthropicProvider() {
return new ProviderConfig().setType("anthropic").setBaseUrl("https://anthropic-citations.invalid/v1")
- .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5").setWireModel("claude-sonnet-4.5");
+ .setApiKey("test-provider-key").setModelId("claude-sonnet-5").setWireModel("claude-sonnet-5");
}
private static String singleInferenceRequestBody(CopilotRequestTestSupport.RecordingRequestHandler handler) {
diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java
index 529c42f2bb..b75e710720 100644
--- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java
@@ -213,7 +213,7 @@ void testHandlerReceivesCorrectEventData() {
SessionStartEvent startEvent = createSessionStartEvent();
startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null,
- null, null, null, null, null, null, null, null, null, null));
+ null, null, null, null, null, null, null, null, null, null, null));
dispatchEvent(startEvent);
AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content");
@@ -890,7 +890,7 @@ private SessionStartEvent createSessionStartEvent() {
private SessionStartEvent createSessionStartEvent(String sessionId) {
var event = new SessionStartEvent();
var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null,
- null, null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null, null);
event.setData(data);
return event;
}
diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java
index 9d76d18ee2..edc40175d0 100644
--- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java
@@ -13,6 +13,7 @@
import org.junit.jupiter.api.Test;
import com.github.copilot.generated.rpc.SessionLimitsConfig;
+import com.github.copilot.rpc.AskUserVariant;
import com.github.copilot.rpc.AutoModeSwitchResponse;
import com.github.copilot.rpc.CloudSessionOptions;
import com.github.copilot.rpc.CloudSessionRepository;
@@ -78,6 +79,42 @@ void testGitHubTokenProviderResultRedactsToken() {
assertFalse(result.toString().contains("do-not-print"));
}
+ @Test
+ void askUserVariantIsForwardedAndSerializedForCreateAndColdResume() throws Exception {
+ var createRequest = SessionRequestBuilder.buildCreateRequest(
+ new SessionConfig().setAskUserVariant(AskUserVariant.ELICITATION), "create-session");
+ var resumeRequest = SessionRequestBuilder.buildResumeRequest("resume-session",
+ new ResumeSessionConfig().setAskUserVariant(AskUserVariant.LEGACY));
+ var mapper = JsonRpcClient.getObjectMapper();
+
+ assertEquals(AskUserVariant.ELICITATION, createRequest.getAskUserVariant());
+ assertEquals("elicitation",
+ mapper.readTree(mapper.writeValueAsBytes(createRequest)).path("askUserVariant").asText());
+ assertEquals(AskUserVariant.LEGACY, resumeRequest.getAskUserVariant());
+ assertEquals("legacy",
+ mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).path("askUserVariant").asText());
+ }
+
+ @Test
+ void askUserVariantDefaultsToOmittedLegacyBehavior() throws Exception {
+ var mapper = JsonRpcClient.getObjectMapper();
+ var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "create-session");
+ var resumeRequest = SessionRequestBuilder.buildResumeRequest("resume-session", new ResumeSessionConfig());
+
+ assertNull(createRequest.getAskUserVariant());
+ assertFalse(mapper.readTree(mapper.writeValueAsBytes(createRequest)).has("askUserVariant"));
+ assertNull(resumeRequest.getAskUserVariant());
+ assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("askUserVariant"));
+ }
+
+ @Test
+ void askUserVariantAcceptsOnlySupportedWireValues() {
+ assertEquals(AskUserVariant.LEGACY, AskUserVariant.fromValue("legacy"));
+ assertEquals(AskUserVariant.ELICITATION, AskUserVariant.fromValue("elicitation"));
+ assertThrows(IllegalArgumentException.class, () -> AskUserVariant.fromValue("ELICITATION"));
+ assertThrows(IllegalArgumentException.class, () -> AskUserVariant.fromValue("unsupported"));
+ }
+
@Test
void testBuildCreateRequestHooksNonNullButEmpty() {
// Hooks object exists but hasHooks() returns false
@@ -1055,6 +1092,26 @@ void testBuildRequestsOmitExpAssignmentsWhenUnset() throws Exception {
assertFalse(resumeJson.contains("\"expAssignments\""), "expAssignments should be omitted when null");
}
+ @Test
+ void testBuildRequestsPropagateAndSerializeFeatureFlags() throws Exception {
+ var mapper = JsonRpcClient.getObjectMapper();
+ var flags = Map.of("ENABLED_TEST_FLAG", true, "DISABLED_TEST_FLAG", false);
+
+ var createConfig = new SessionConfig().setFeatureFlags(flags);
+ CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createConfig, "session-1");
+ assertEquals(flags, createRequest.getFeatureFlags());
+ var createJson = mapper.readTree(mapper.writeValueAsString(createRequest));
+ assertTrue(createJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean());
+ assertFalse(createJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean());
+
+ var resumeConfig = new ResumeSessionConfig().setFeatureFlags(flags);
+ ResumeSessionRequest resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", resumeConfig);
+ assertEquals(flags, resumeRequest.getFeatureFlags());
+ var resumeJson = mapper.readTree(mapper.writeValueAsString(resumeRequest));
+ assertTrue(resumeJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean());
+ assertFalse(resumeJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean());
+ }
+
@Test
void testClonePreservesAndForwardsExpAssignments() throws Exception {
var mapper = JsonRpcClient.getObjectMapper();
diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java
index 23408faa2a..0294b511ec 100644
--- a/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java
+++ b/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java
@@ -50,10 +50,10 @@
*
*
* Run with {@code mvn verify -Pinprocess} from the {@code java} reactor root,
- * which builds the {@code copilot-sdk-java-runtime} artifact and sets
- * {@code COPILOT_CLI_PATH} to the pinned CLI whose sibling {@code runtime.node}
- * this test loads, and forces {@code forkCount=1} because the FFI host and env
- * guard mutate process-global state.
+ * which builds the {@code copilot-sdk-java-runtime} artifact and sets the
+ * classifier JAR containing {@code runtime.node}, and forces
+ * {@code forkCount=1} because the FFI host and env guard mutate process-global
+ * state.
*
*
* {@link RequireInProcess} disables this test unless the {@code -Pinprocess}
@@ -86,11 +86,6 @@ void shouldStartPingAndStopOverInProcessFfi() throws Exception {
// replay proxy, mirroring how a session-level in-process test would
// redirect COPILOT_API_URL. `ping` never reaches the network, but this
// demonstrates the guard's intended usage for future in-process tests.
- // COPILOT_CLI_PATH is intentionally NOT set here: NativeRuntimeLoader and
- // CopilotClient.resolveInProcessEntrypoint() read it via
- // System.getenv(), which is a JVM-startup-time snapshot that native
- // setenv() calls made after the JVM starts cannot update — it must be
- // set before the JVM starts (see the -Pinprocess Maven profile).
try (InProcessEnvGuard envGuard = new InProcessEnvGuard(Map.of("COPILOT_API_URL", ctx.getProxyUrl()))) {
CopilotClientOptions options = new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess());
try (CopilotClient client = new CopilotClient(options)) {
diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java
new file mode 100644
index 0000000000..19c14a0644
--- /dev/null
+++ b/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java
@@ -0,0 +1,38 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.e2e;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.AllowCopilotExperimental;
+import com.github.copilot.CopilotClient;
+import com.github.copilot.rpc.CopilotClientOptions;
+import com.github.copilot.rpc.PingResponse;
+import com.github.copilot.rpc.RuntimeConnection;
+
+/**
+ * Failsafe smoke test for the managed out-of-process runtime wrapper.
+ */
+@AllowCopilotExperimental
+@RequireInProcess
+class OutOfProcessTransportIT {
+
+ @Test
+ void shouldStartPingAndStopOverStdio() throws Exception {
+ CopilotClientOptions options = new CopilotClientOptions().setConnection(RuntimeConnection.forStdio());
+ try (CopilotClient client = new CopilotClient(options)) {
+ client.start().get();
+
+ PingResponse pong = client.ping("wrapper message").get();
+ assertEquals("pong: wrapper message", pong.message());
+ assertNotNull(pong.timestamp());
+
+ client.stop().get();
+ }
+ }
+}
diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java
index 9321618575..fd3f92100d 100644
--- a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java
+++ b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java
@@ -5,11 +5,9 @@
package com.github.copilot.e2e;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -37,6 +35,8 @@
class RewindIT {
private static final String FILE_NAME = "rewind-sdk.txt";
+ private static final String ORIGINAL_FILE_CONTENT = "Original rewind content";
+ private static final String PREPARED_FILE_CONTENT = "Prepared rewind content";
private static final String FILE_CONTENT = "SDK rewind content";
private static E2ETestContext ctx;
@@ -55,23 +55,27 @@ static void teardown() throws Exception {
@Test
void shouldRestoreTrackedFileAndConversation() throws Exception {
- assumeFalse(System.getProperty("os.name").startsWith("Windows"),
- "blocked on CLI 1.0.81 file-change tracking regression on Windows");
-
ctx.configureForTest("rewind", "should_restore_tracked_file_and_conversation");
Path filePath = ctx.getWorkDir().resolve(FILE_NAME);
+ Files.writeString(filePath, ORIGINAL_FILE_CONTENT);
try (CopilotClient client = ctx.createClient();
- CopilotSession session = client
- .createSession(
- new SessionConfig().setModel("claude-sonnet-4.5").setEnableFileChangeTracking(true)
- .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))
+ CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5")
+ .setEnableFileChangeTracking(true).setOnPermissionRequest(PermissionHandler.APPROVE_ALL))
.get(30, TimeUnit.SECONDS)) {
+ AssistantMessageEvent ready = session
+ .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of "
+ + FILE_NAME + " from " + ORIGINAL_FILE_CONTENT + " to " + PREPARED_FILE_CONTENT
+ + ". After the tool succeeds, reply with exactly SDK_REWIND_READY."), 30_000)
+ .get(60, TimeUnit.SECONDS);
+ assertNotNull(ready);
+ assertEquals("SDK_REWIND_READY", ready.getData().content());
+ assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath));
+
AssistantMessageEvent response = session
- .sendAndWait(new MessageOptions().setPrompt(
- "Use the create tool to create " + FILE_NAME + " containing exactly " + FILE_CONTENT
- + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."),
- 30_000)
+ .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of "
+ + FILE_NAME + " from " + PREPARED_FILE_CONTENT + " to " + FILE_CONTENT
+ + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."), 30_000)
.get(60, TimeUnit.SECONDS);
assertNotNull(response);
@@ -80,8 +84,9 @@ void shouldRestoreTrackedFileAndConversation() throws Exception {
SessionHistoryListRewindPointsResult rewindPoints = waitForRewindPoints(session);
assertTrue(Boolean.TRUE.equals(rewindPoints.fileChangeTrackingEnabled()));
- assertEquals(1, rewindPoints.points().size());
- var rewindPoint = rewindPoints.points().get(0);
+ assertEquals(2, rewindPoints.points().size());
+ var rewindPoint = rewindPoints.points().get(1);
+ assertTrue(Boolean.TRUE.equals(rewindPoint.turnChangedFiles()));
assertTrue(Boolean.TRUE.equals(rewindPoint.canRestoreFiles()));
assertEquals(1L, rewindPoint.fileCount());
@@ -98,7 +103,7 @@ void shouldRestoreTrackedFileAndConversation() throws Exception {
assertTrue(rewind.eventsRemoved() != null && rewind.eventsRemoved() > 0);
assertEquals(1, rewind.restoredFiles().size());
assertSamePath(filePath, rewind.restoredFiles().get(0));
- assertFalse(Files.exists(filePath));
+ assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath));
var events = session.getMessages().get(10, TimeUnit.SECONDS);
assertTrue(events.stream().noneMatch(event -> event.getId().toString().equals(rewindPoint.eventId())));
@@ -110,16 +115,19 @@ private static SessionHistoryListRewindPointsResult waitForRewindPoints(CopilotS
SessionHistoryListRewindPointsResult result;
do {
result = session.getRpc().history.listRewindPoints().get(10, TimeUnit.SECONDS);
- if (result.unavailableReason() == null && !result.points().isEmpty()
- && Boolean.TRUE.equals(result.points().get(0).canRestoreFiles())) {
+ if (result.unavailableReason() == null && result.points().size() == 2
+ && Boolean.TRUE.equals(result.points().get(1).turnChangedFiles())
+ && Boolean.TRUE.equals(result.points().get(1).canRestoreFiles())) {
return result;
}
TimeUnit.MILLISECONDS.sleep(100);
} while (System.nanoTime() < deadline);
assertNull(result.unavailableReason(), "Timed out waiting for rewind points to become available");
- assertFalse(result.points().isEmpty(), "Timed out waiting for a rewind point");
- assertTrue(Boolean.TRUE.equals(result.points().get(0).canRestoreFiles()),
+ assertTrue(result.points().size() >= 2, "Timed out waiting for both rewind points");
+ assertTrue(Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()),
+ "Timed out waiting for the edit turn to capture file changes");
+ assertTrue(Boolean.TRUE.equals(result.points().get(1).canRestoreFiles()),
"Timed out waiting for rewind file restoration to become available");
return result;
}
diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java
index cc98d24f6b..545b316c8c 100644
--- a/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java
@@ -99,6 +99,47 @@ public boolean connectionClose(int connectionId) {
assertEquals("1", env.get("COPILOT_DISABLE_KEYTAR"));
}
+ @Test
+ void startWithoutEntrypointPassesOnlyRuntimeOptions() throws Exception {
+ AtomicReference argvJson = new AtomicReference<>();
+ NativeBinding binding = new NativeBinding() {
+ @Override
+ public int hostStart(byte[] argv, int argvLen, byte[] env, int envLen) {
+ argvJson.set(argv);
+ return 11;
+ }
+
+ @Override
+ public boolean hostShutdown(int serverId) {
+ return true;
+ }
+
+ @Override
+ public int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource,
+ int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen) {
+ return 21;
+ }
+
+ @Override
+ public boolean connectionWrite(int connectionId, byte[] data, int dataLen) {
+ return true;
+ }
+
+ @Override
+ public boolean connectionClose(int connectionId) {
+ return true;
+ }
+ };
+
+ try (FfiRuntimeHost host = new FfiRuntimeHost(binding, "/tmp/runtime.node")) {
+ host.start(null, new CopilotClientOptions().setLogLevel("debug"));
+ }
+
+ List argv = MAPPER.readValue(argvJson.get(), new TypeReference>() {
+ });
+ assertEquals(List.of("--log-level", "debug"), argv);
+ }
+
@Test
void callbackExceptionIsContainedAndDoesNotEscapeAcrossFfiBoundary() {
AtomicBoolean callbackReturned = new AtomicBoolean(false);
diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java
index fc8bd51011..84ef7d8de4 100644
--- a/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java
@@ -34,17 +34,13 @@
class NativeRuntimeLoaderTest {
- private static final String TEST_CLASSIFIER = PlatformDetector.detectClassifier();
- private static final String OTHER_CLASSIFIER = TEST_CLASSIFIER.equals("darwin-arm64")
- ? "linux-x64"
- : "darwin-arm64";
- private static final String TEST_CLI_FILENAME = TEST_CLASSIFIER.startsWith("win32")
- ? NativeRuntimeLoader.CLI_FILENAME_WINDOWS
- : NativeRuntimeLoader.CLI_FILENAME;
+ private static final String TEST_CLASSIFIER = "linux-x64";
+ private static final String OTHER_CLASSIFIER = "darwin-arm64";
private static final String TEST_VERSION = "1.2.3-test";
private static final String TEST_NATIVE_VERSION = "0.0.1-test";
private static final byte[] FAKE_BINARY_CONTENT = "fake runtime.node binary content".getBytes();
private static final byte[] FAKE_CLI_CONTENT = "fake copilot CLI content".getBytes();
+ private static final byte[] FAKE_WRAPPER_CONTENT = "fake runtime wrapper content".getBytes();
private static final byte[] OTHER_BINARY_CONTENT = "other runtime.node binary content".getBytes();
private static final byte[] OTHER_CLI_CONTENT = "other copilot CLI content".getBytes();
@@ -160,19 +156,24 @@ void resolveEntrypointUsesConfiguredCliWhenRuntimeIsInPrebuilds(@TempDir Path te
@Test
void resolveFromCliPathReturnsAbsolutePathForRelativeCliPath() throws Exception {
Path workingDirectory = Path.of("").toAbsolutePath();
- Path fakeCliDir = Files.createTempDirectory(Path.of("target").toAbsolutePath(), "relative-cli-test-");
- Path fakeCliPath = fakeCliDir.resolve("copilot");
- Files.createFile(fakeCliPath);
- Path runtimeNode = fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME);
- Files.write(runtimeNode, FAKE_BINARY_CONTENT);
-
- Path relativeCliPath = workingDirectory.relativize(fakeCliPath);
-
- assertEquals(runtimeNode, NativeRuntimeLoader.resolveFromCliPath(relativeCliPath.toString()));
+ Path fakeCliDir = Files.createTempDirectory(workingDirectory.resolve("target"), "relative-cli-");
+ try {
+ Path fakeCliPath = Files.createFile(fakeCliDir.resolve("copilot"));
+ Path runtimeNode = Files.write(fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME),
+ FAKE_BINARY_CONTENT);
+ Path relativeCliPath = workingDirectory.relativize(fakeCliPath);
+
+ assertEquals(runtimeNode, NativeRuntimeLoader.resolveFromCliPath(relativeCliPath.toString()));
+ } finally {
+ Files.deleteIfExists(fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME));
+ Files.deleteIfExists(fakeCliDir.resolve("copilot"));
+ Files.deleteIfExists(fakeCliDir);
+ }
}
@Test
void cliPathOverrideTakesPriorityOverClasspathExtraction(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
// Create a valid runtime.node alongside the fake CLI path
Path fakeCliDir = tempDir.resolve("cli-dir");
Files.createDirectories(fakeCliDir);
@@ -197,6 +198,7 @@ void cliPathOverrideTakesPriorityOverClasspathExtraction(@TempDir Path tempDir)
@Test
void extractToCacheCopiesResourceToVersionedCacheDirectory(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
@@ -211,6 +213,7 @@ void extractToCacheCopiesResourceToVersionedCacheDirectory(@TempDir Path tempDir
@Test
void extractToCacheReturnsCachedFileOnSecondCall(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
@@ -230,6 +233,7 @@ void extractToCacheReturnsCachedFileOnSecondCall(@TempDir Path tempDir) throws E
@Test
void changedNativeVersionDoesNotReuseCachedArtifactsForSameSdkVersion(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader firstLoader = classLoaderWithNativeArtifacts(tempDir.resolve("native-v1"), TEST_CLASSIFIER, "1.0.0",
FAKE_BINARY_CONTENT, FAKE_CLI_CONTENT);
@@ -241,13 +245,16 @@ void changedNativeVersionDoesNotReuseCachedArtifactsForSameSdkVersion(@TempDir P
assertNotEquals(firstRuntime, secondRuntime, "Different native versions must use different cache entries");
assertBytesEqual(FAKE_BINARY_CONTENT, Files.readAllBytes(firstRuntime));
- assertBytesEqual(FAKE_CLI_CONTENT, Files.readAllBytes(firstRuntime.getParent().resolve(TEST_CLI_FILENAME)));
+ assertBytesEqual(FAKE_CLI_CONTENT,
+ Files.readAllBytes(firstRuntime.getParent().resolve(NativeRuntimeLoader.CLI_FILENAME)));
assertBytesEqual(OTHER_BINARY_CONTENT, Files.readAllBytes(secondRuntime));
- assertBytesEqual(OTHER_CLI_CONTENT, Files.readAllBytes(secondRuntime.getParent().resolve(TEST_CLI_FILENAME)));
+ assertBytesEqual(OTHER_CLI_CONTENT,
+ Files.readAllBytes(secondRuntime.getParent().resolve(NativeRuntimeLoader.CLI_FILENAME)));
}
@Test
void extractToCacheThrowsWhenClasspathResourceMissing(@TempDir Path tempDir) {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader emptyLoader = new URLClassLoader(new URL[0], null);
@@ -257,6 +264,7 @@ void extractToCacheThrowsWhenClasspathResourceMissing(@TempDir Path tempDir) {
@Test
void extractToCacheThrowsWhenNativeMetadataMissing(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path resourceDir = tempDir.resolve("native").resolve(TEST_CLASSIFIER);
Files.createDirectories(resourceDir);
Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), FAKE_BINARY_CONTENT);
@@ -270,6 +278,7 @@ void extractToCacheThrowsWhenNativeMetadataMissing(@TempDir Path tempDir) throws
@Test
void extractedBinaryContentsMatchClasspathResource(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
@@ -281,6 +290,7 @@ void extractedBinaryContentsMatchClasspathResource(@TempDir Path tempDir) throws
@Test
void extractToCacheFiltersClasspathByClassifier(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT);
writeRuntimeResource(tempDir, OTHER_CLASSIFIER, OTHER_BINARY_CONTENT);
@@ -294,6 +304,7 @@ void extractToCacheFiltersClasspathByClassifier(@TempDir Path tempDir) throws Ex
@Test
void extractToCacheRepairsInvalidCacheEntry(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
Path cached = cacheBase.resolve(TEST_VERSION).resolve(TEST_NATIVE_VERSION).resolve(TEST_CLASSIFIER)
.resolve(NativeRuntimeLoader.RUNTIME_FILENAME);
@@ -309,13 +320,13 @@ void extractToCacheRepairsInvalidCacheEntry(@TempDir Path tempDir) throws Except
@Test
void nonExecutableCachedCliIsNotAcceptedAsValid(@TempDir Path tempDir) throws Exception {
- assumeTrue(!TEST_CLASSIFIER.startsWith("win32"));
+ assumeLinuxX64();
assumeTrue(Files.getFileStore(tempDir).supportsFileAttributeView("posix"));
Path cacheBase = tempDir.resolve("cache");
Path cacheDir = cacheBase.resolve(TEST_VERSION).resolve(TEST_NATIVE_VERSION).resolve(TEST_CLASSIFIER);
Files.createDirectories(cacheDir);
Files.write(cacheDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), FAKE_BINARY_CONTENT);
- Path cachedCli = Files.write(cacheDir.resolve(TEST_CLI_FILENAME), FAKE_CLI_CONTENT);
+ Path cachedCli = Files.write(cacheDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT);
Files.setPosixFilePermissions(cachedCli, PosixFilePermissions.fromString("rw-------"));
ClassLoader loader = classLoaderWithRuntimeAndCliResources(tempDir, TEST_CLASSIFIER);
@@ -330,6 +341,7 @@ void nonExecutableCachedCliIsNotAcceptedAsValid(@TempDir Path tempDir) throws Ex
@Test
void bundledCliSiblingIsUsedWhenClasspathResourceAbsent(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path bundledCliDir = tempDir.resolve("bundled-cli");
Files.createDirectories(bundledCliDir);
Path runtimeNode = bundledCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME);
@@ -347,6 +359,7 @@ void bundledCliSiblingIsUsedWhenClasspathResourceAbsent(@TempDir Path tempDir) t
@Test
void classpathResourceWinsOverBundledCliSibling(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
// Source 3: bundled CLI dir with runtime.node (should NOT win)
Path bundledCliDir = tempDir.resolve("bundled-cli");
Files.createDirectories(bundledCliDir);
@@ -368,6 +381,7 @@ void classpathResourceWinsOverBundledCliSibling(@TempDir Path tempDir) throws Ex
@Test
void bundledCliSiblingIsIgnoredWhenRuntimeNodeMissing(@TempDir Path tempDir) {
+ assumeLinuxX64();
Path bundledCliDir = tempDir.resolve("bundled-cli-no-runtime");
// bundledCliDir doesn't even exist — no runtime.node present
@@ -399,12 +413,12 @@ void defaultPublisherMovesSourceToTarget(@TempDir Path tempDir) throws Exception
@Test
void cliIsExecutableBeforeAtomicPublication(@TempDir Path tempDir) throws Exception {
- assumeTrue(!TEST_CLASSIFIER.startsWith("win32"));
+ assumeLinuxX64();
assumeTrue(Files.getFileStore(tempDir).supportsFileAttributeView("posix"));
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeAndCliResources(tempDir, TEST_CLASSIFIER);
NativeRuntimeLoader.AtomicPublisher publisher = (temp, cached) -> {
- if (cached.getFileName().toString().equals(TEST_CLI_FILENAME)) {
+ if (cached.getFileName().toString().equals(NativeRuntimeLoader.CLI_FILENAME)) {
assertTrue(Files.isExecutable(temp), "CLI temp file must be executable before atomic publication");
}
Files.move(temp, cached, StandardCopyOption.REPLACE_EXISTING);
@@ -415,6 +429,7 @@ void cliIsExecutableBeforeAtomicPublication(@TempDir Path tempDir) throws Except
@Test
void extractionCleansUpTempFileWhenPublicationFails(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
@@ -435,6 +450,7 @@ void extractionCleansUpTempFileWhenPublicationFails(@TempDir Path tempDir) throw
@Test
void extractionCleansUpTempFileWhenPublisherThrowsIllegalStateException(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
@@ -462,6 +478,7 @@ void extractionCleansUpTempFileWhenPublisherThrowsIllegalStateException(@TempDir
@Test
void concurrentExtractionByMultipleThreadsBothSucceed(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
int threadCount = 8;
@@ -499,6 +516,7 @@ void concurrentExtractionByMultipleThreadsBothSucceed(@TempDir Path tempDir) thr
@Test
void resolveWithNullCliEnvExtractsFromClasspath(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER);
@@ -509,8 +527,54 @@ void resolveWithNullCliEnvExtractsFromClasspath(@TempDir Path tempDir) throws Ex
assertTrue(Files.size(result) > 0);
}
+ @Test
+ void resolveRuntimeWrapperExtractsAdjacentPairFromAbsentCache(@TempDir Path tempDir) throws Exception {
+ Path cacheBase = tempDir.resolve("cache");
+ assertFalse(Files.exists(cacheBase));
+ ClassLoader loader = classLoaderWithRuntimeWrapperArtifacts(tempDir, TEST_CLASSIFIER, TEST_NATIVE_VERSION);
+
+ Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(cacheBase, loader, TEST_CLASSIFIER, TEST_VERSION);
+
+ assertEquals(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME, wrapper.getFileName().toString());
+ assertTrue(Files.isRegularFile(wrapper));
+ assertTrue(Files.isRegularFile(wrapper.resolveSibling(NativeRuntimeLoader.RUNTIME_FILENAME)));
+ assertFalse(Files.exists(wrapper.resolveSibling(NativeRuntimeLoader.CLI_FILENAME)));
+ }
+
+ @Test
+ void resolveRuntimeWrapperExtractsRetainedRuntimeAssets(@TempDir Path tempDir) throws Exception {
+ Path resourceDir = tempDir.resolve("native").resolve(TEST_CLASSIFIER);
+ writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT);
+ Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT);
+ Path ripgrep = resourceDir.resolve("ripgrep/bin/linux-x64/rg");
+ Files.createDirectories(ripgrep.getParent());
+ Files.writeString(ripgrep, "ripgrep");
+ Files.writeString(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_ASSETS_FILENAME),
+ "644\truntime.node\n" + "755\tcopilot-runtime\n" + "755\tripgrep/bin/linux-x64/rg\n");
+ ClassLoader loader = new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null);
+
+ Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(tempDir.resolve("cache"), loader, TEST_CLASSIFIER,
+ TEST_VERSION);
+
+ Path installedRipgrep = wrapper.getParent().resolve("ripgrep/bin/linux-x64/rg");
+ assertEquals("ripgrep", Files.readString(installedRipgrep));
+ assertTrue(Files.isExecutable(installedRipgrep));
+ }
+
+ @Test
+ void resolveRuntimeWrapperRejectsClassifierWithoutWrapper(@TempDir Path tempDir) throws Exception {
+ writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT);
+ ClassLoader loader = new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null);
+
+ IOException error = assertThrows(IOException.class, () -> NativeRuntimeLoader
+ .resolveRuntimeWrapper(tempDir.resolve("cache"), loader, TEST_CLASSIFIER, TEST_VERSION));
+
+ assertTrue(error.getMessage().contains(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME));
+ }
+
@Test
void resolveThrowsWhenNoSourceIsAvailable(@TempDir Path tempDir) {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader emptyLoader = new URLClassLoader(new URL[0], null);
@@ -521,6 +585,7 @@ void resolveThrowsWhenNoSourceIsAvailable(@TempDir Path tempDir) {
@Test
void resolveFallsBackToRuntimeAlongsideBundledCli(@TempDir Path tempDir) throws Exception {
+ assumeLinuxX64();
Path cacheBase = tempDir.resolve("cache");
ClassLoader emptyLoader = new URLClassLoader(new URL[0], null);
Path bundledCli = tempDir.resolve("copilot");
@@ -538,6 +603,17 @@ void resolveFallsBackToRuntimeAlongsideBundledCli(@TempDir Path tempDir) throws
// Helpers
// -------------------------------------------------------------------------
+ private static void assumeLinuxX64() {
+ String actualClassifier;
+ try {
+ actualClassifier = PlatformDetector.detectClassifier();
+ } catch (IllegalStateException ex) {
+ actualClassifier = "unsupported";
+ }
+ assumeTrue(TEST_CLASSIFIER.equals(actualClassifier),
+ "Requires linux-x64; detected " + actualClassifier + "; see #2323");
+ }
+
private static ClassLoader classLoaderWithVersionResource(Path tempDir, String version) throws IOException {
Path propsFile = tempDir.resolve(NativeRuntimeLoader.VERSION_RESOURCE);
Files.writeString(propsFile, "version=" + version + "\n");
@@ -553,7 +629,7 @@ private static ClassLoader classLoaderWithRuntimeAndCliResources(Path tempDir, S
throws IOException {
writeRuntimeResource(tempDir, classifier, FAKE_BINARY_CONTENT);
Path resourceDir = tempDir.resolve("native").resolve(classifier);
- Files.write(resourceDir.resolve(TEST_CLI_FILENAME), FAKE_CLI_CONTENT);
+ Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT);
return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null);
}
@@ -561,7 +637,19 @@ private static ClassLoader classLoaderWithNativeArtifacts(Path tempDir, String c
byte[] runtimeContent, byte[] cliContent) throws IOException {
writeRuntimeResource(tempDir, classifier, runtimeContent);
Path resourceDir = tempDir.resolve("native").resolve(classifier);
- Files.write(resourceDir.resolve(TEST_CLI_FILENAME), cliContent);
+ Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), cliContent);
+ Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT);
+ Files.writeString(resourceDir.resolve("platform.properties"),
+ "classifier=" + classifier + "\nversion=" + nativeVersion + "\n");
+ return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null);
+ }
+
+ private static ClassLoader classLoaderWithRuntimeWrapperArtifacts(Path tempDir, String classifier,
+ String nativeVersion) throws IOException {
+ writeRuntimeResource(tempDir, classifier, FAKE_BINARY_CONTENT);
+ Path resourceDir = tempDir.resolve("native").resolve(classifier);
+ Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT);
+ Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT);
Files.writeString(resourceDir.resolve("platform.properties"),
"classifier=" + classifier + "\nversion=" + nativeVersion + "\n");
return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null);
diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java
index 602089d012..f86b3dfbcb 100644
--- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java
@@ -326,10 +326,10 @@ void sessionModelGetCurrentParams_record() {
@Test
void sessionModelSwitchToParams_record() {
- var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null, null, null,
- null, null, null, null, null, null, null, null);
+ var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-5", "high", null, null, null, null, null,
+ null, null, null, null, null, null, null);
assertEquals("sess-32", params.sessionId());
- assertEquals("claude-sonnet-4.5", params.modelId());
+ assertEquals("claude-sonnet-5", params.modelId());
assertEquals("high", params.reasoningEffort());
assertNull(params.reasoningSummary());
assertNull(params.verbosity());
@@ -470,7 +470,7 @@ void pingResult_fields() {
@Test
void sessionAgentListResult_with_items() {
var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1", null, null, null, null, null, null,
- null, null);
+ null, null, null, null);
var result = new SessionAgentListResult(List.of(item));
assertEquals(1, result.agents().size());
assertEquals("name1", result.agents().get(0).name());
@@ -482,7 +482,7 @@ void sessionAgentListResult_with_items() {
@Test
void sessionAgentGetCurrentResult_nested() {
var agent = new AgentInfo("agent-1", "Agent One", "Does things", null, null, null, null, null, null, null, null,
- null);
+ null, null, null);
var result = new SessionAgentGetCurrentResult(agent);
assertEquals("agent-1", result.agent().name());
assertEquals("Agent One", result.agent().displayName());
@@ -498,7 +498,8 @@ void sessionAgentGetCurrentResult_null_agent() {
@Test
void sessionAgentReloadResult_with_items() {
- var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null);
+ var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null, null,
+ null);
var result = new SessionAgentReloadResult(List.of(item));
assertEquals(1, result.agents().size());
assertEquals("a", result.agents().get(0).name());
@@ -507,7 +508,7 @@ void sessionAgentReloadResult_with_items() {
@Test
void sessionAgentSelectResult_nested() {
var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected", null, null, null,
- null, null, null, null, null);
+ null, null, null, null, null, null, null);
var result = new SessionAgentSelectResult(agent);
assertEquals("selected", result.agent().name());
}
@@ -656,8 +657,8 @@ void sessionMcpListResult_status_enum_all_values() {
@Test
void sessionModelGetCurrentResult_record() {
- var result = new SessionModelGetCurrentResult("claude-sonnet-4.5", null, null);
- assertEquals("claude-sonnet-4.5", result.modelId());
+ var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null);
+ assertEquals("claude-sonnet-5", result.modelId());
}
@Test
@@ -816,7 +817,7 @@ void modelsListResult_nested() {
var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null);
var capabilities = new ModelCapabilities(supports, limits);
var policy = new ModelPolicy(ModelPolicyState.ENABLED, null);
- var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount");
+ var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount", true);
var billing = new ModelBilling(1.0, null, null, promo);
var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null, null, null,
null, null);
@@ -834,6 +835,7 @@ void modelsListResult_nested() {
assertEquals(Double.valueOf(25.0), result.models().get(0).billing().promo().discountPercent());
assertEquals("2026-08-01T00:00:00Z", result.models().get(0).billing().promo().endsAt());
assertEquals("Summer discount", result.models().get(0).billing().promo().message());
+ assertTrue(result.models().get(0).billing().promo().showBanner());
}
@Test
diff --git a/justfile b/justfile
index c84166862f..69666bc00a 100644
--- a/justfile
+++ b/justfile
@@ -9,7 +9,7 @@ format: format-go format-python format-nodejs format-dotnet format-rust
lint: lint-go lint-python lint-nodejs lint-dotnet lint-rust
# Run tests for all languages
-test: test-go test-python test-nodejs test-dotnet test-rust test-corrections
+test: test-go test-python test-nodejs test-dotnet test-rust test-harness test-corrections
# Format Go code
format-go:
@@ -66,6 +66,11 @@ test-nodejs:
@echo "=== Testing Node.js code ==="
@cd nodejs && npm test
+# Run test harness tests
+test-harness:
+ @echo "=== Testing test harness ==="
+ @cd test/harness && npm test
+
# Test .NET code
test-dotnet:
@echo "=== Testing .NET code ==="
@@ -168,4 +173,3 @@ validate-docs-go:
validate-docs-cs:
@echo "=== Validating C# documentation ==="
@cd scripts/docs-validation && npm run validate:cs
-
diff --git a/nodejs/README.md b/nodejs/README.md
index 93f9c3fa6b..57a8bf484b 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -95,6 +95,7 @@ new CopilotClient(options?: CopilotClientOptions)
- `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections.
- `RuntimeConnection.forInProcess()` — host the runtime in-process over its native C ABI (FFI). **Experimental.** Because the runtime shares this process, `env`, `telemetry`, and `workingDirectory` are rejected with this transport; set them on the host process instead.
- The child-process transports (`forStdio`/`forTcp`) also accept a per-connection `env`. Set it there or via the top-level `env` option — not both (setting both throws).
+ - Managed child-process connections materialize the bundled `copilot-runtime` and adjacent `runtime.node`, then launch the wrapper by default. An explicit connection `path` or `COPILOT_CLI_PATH` overrides the bundled runtime.
- `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`.
- `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd).
- `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`.
@@ -131,6 +132,7 @@ Create a new conversation session.
- `sessionId?: string` - Custom session ID.
- `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.**
+- `capi?: CapiSessionOptions` - Copilot API options. With `model: "auto"`, set `autoTier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics.
- `reasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max"` - Reasoning effort level for models that support it. Use `listModels()` to check which models support this option.
- `tools?: Tool[]` - Custom tools exposed to the CLI. Tools without `handler` are declaration-only and must be resolved via pending tool-call RPCs.
- `systemMessage?: SystemMessageConfig` - System message customization (see below)
@@ -140,7 +142,8 @@ Create a new conversation session.
- `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`.
- `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section.
- `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `approveAll` approves requests when managed settings are disabled and throws when `enableManagedSettings` is true. Custom handlers can inspect `managedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
-- `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section.
+- `onUserInputRequest?: UserInputHandler` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section.
+- `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`.
- `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section.
- `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
@@ -959,7 +962,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `skipPe
## User Input Requests
-Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler:
+Enable the legacy question-and-answer `ask_user` tool by providing an `onUserInputRequest` handler:
```typescript
const session = await client.createSession({
@@ -991,6 +994,7 @@ Register an `onElicitationRequest` handler to let your client act as an elicitat
const session = await client.createSession({
model: "gpt-5",
onPermissionRequest: approveAll,
+ askUserVariant: "elicitation",
onElicitationRequest: async (context) => {
// context.sessionId - Session that triggered the request
// context.message - Description of what information is needed
@@ -1012,6 +1016,9 @@ const session = await client.createSession({
console.log(session.capabilities.ui?.elicitation); // true
```
+Set `askUserVariant: "elicitation"` to expose the structured form as the model's
+`ask_user` tool. Omit it to retain the legacy SDK behavior.
+
When `onElicitationRequest` is provided, the SDK sends `requestElicitation: true` during session create/resume, which enables `session.capabilities.ui.elicitation` on the session.
In multi-client scenarios:
diff --git a/nodejs/docs/factories.md b/nodejs/docs/factories.md
index 903a3de8b1..23b9d0fed3 100644
--- a/nodejs/docs/factories.md
+++ b/nodejs/docs/factories.md
@@ -139,6 +139,8 @@ Run by registered name or handle:
const run = await session.factory.run("review-changed", {
args: { files: ["src/a.ts"] },
limits: { maxAiCredits: 3 },
+ notifyOnComplete: true,
+ logPhaseNames: true,
});
if (run.status === "completed") {
@@ -153,7 +155,12 @@ The name overload is:
```ts
session.factory.run(
name: string,
- options?: { args?: JsonValue; limits?: FactoryLimits },
+ options?: {
+ args?: JsonValue;
+ limits?: FactoryLimits;
+ notifyOnComplete?: boolean;
+ logPhaseNames?: boolean;
+ },
): Promise;
```
@@ -162,6 +169,8 @@ Resume by run ID without resending the name or arguments:
```ts
const run = await session.factory.resume(runId, {
limits: { maxAiCredits: 6 },
+ notifyOnComplete: true,
+ logPhaseNames: true,
});
```
@@ -170,10 +179,16 @@ The signature is:
```ts
session.factory.resume(
runId: string,
- options?: { limits?: FactoryLimits },
+ options?: {
+ limits?: FactoryLimits;
+ notifyOnComplete?: boolean;
+ logPhaseNames?: boolean;
+ },
): Promise;
```
+Set `notifyOnComplete` to `true` for factories that are likely to be invoked by an agent, so the originating session is notified when the factory completes. Set it to `false` for factories intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs.
+
Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome — `completed`, `error`, `halted`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`.
An agent that no longer has a prior run's ID in context can recover it with `factories_manage` and `operation: "runs"`, which lists the session's factory runs with their IDs and statuses. This matters for resume: a run that reached a limit keeps its journal, so resuming it replays completed work for free, while restarting it from scratch pays for that work twice.
@@ -219,8 +234,13 @@ The calling session can inspect its own factory runs:
```ts
const runs = await session.factory.listRuns();
+const runsPage = await session.factory.listRuns({
+ afterSeq,
+ beforeSeq,
+ limit,
+});
const detail = await session.factory.getRunDetail(runId);
-const page = await session.factory.getRunProgress(runId, {
+const progressPage = await session.factory.getRunProgress(runId, {
phaseId,
afterSeq,
beforeSeq,
@@ -228,7 +248,8 @@ const page = await session.factory.getRunProgress(runId, {
});
```
-- `listRuns()` returns the newest default page of this session's durable factory runs.
+- `listRuns()` returns only the runs array from the newest default page of this session's durable factory runs. This overload preserves the original convenience API.
+- `listRuns({ afterSeq, beforeSeq, limit })` returns the full page. Its `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` fields let callers continue paging without raw RPC calls.
- `getRunDetail(runId)` returns phases, prompt-safe agent summaries, and the latest progress page.
- `getRunProgress(runId, options?)` pages progress forward, backward, by phase, or from the latest tail.
diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json
index 3b8e6cb861..bfa5c8a0c6 100644
--- a/nodejs/package-lock.json
+++ b/nodejs/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.0-dev",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.82-0",
+ "@github/copilot": "^1.0.83-3",
"koffi": "^3.1.0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
@@ -658,8 +658,8 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.82-0",
- "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"detect-libc": "^2.1.2"
@@ -668,19 +668,19 @@
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.82-0",
- "@github/copilot-darwin-x64": "1.0.82-0",
- "@github/copilot-linux-arm64": "1.0.82-0",
- "@github/copilot-linux-x64": "1.0.82-0",
- "@github/copilot-linuxmusl-arm64": "1.0.82-0",
- "@github/copilot-linuxmusl-x64": "1.0.82-0",
- "@github/copilot-win32-arm64": "1.0.82-0",
- "@github/copilot-win32-x64": "1.0.82-0"
+ "@github/copilot-darwin-arm64": "1.0.83-3",
+ "@github/copilot-darwin-x64": "1.0.83-3",
+ "@github/copilot-linux-arm64": "1.0.83-3",
+ "@github/copilot-linux-x64": "1.0.83-3",
+ "@github/copilot-linuxmusl-arm64": "1.0.83-3",
+ "@github/copilot-linuxmusl-x64": "1.0.83-3",
+ "@github/copilot-win32-arm64": "1.0.83-3",
+ "@github/copilot-win32-x64": "1.0.83-3"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.82-0",
- "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==",
"cpu": [
"arm64"
],
@@ -694,8 +694,8 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.82-0",
- "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==",
"cpu": [
"x64"
],
@@ -709,8 +709,8 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.82-0",
- "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==",
"cpu": [
"arm64"
],
@@ -724,8 +724,8 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.82-0",
- "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==",
"cpu": [
"x64"
],
@@ -739,8 +739,8 @@
}
},
"node_modules/@github/copilot-linuxmusl-arm64": {
- "version": "1.0.82-0",
- "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==",
"cpu": [
"arm64"
],
@@ -754,8 +754,8 @@
}
},
"node_modules/@github/copilot-linuxmusl-x64": {
- "version": "1.0.82-0",
- "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==",
"cpu": [
"x64"
],
@@ -769,8 +769,8 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.82-0",
- "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==",
"cpu": [
"arm64"
],
@@ -784,8 +784,8 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.82-0",
- "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==",
+ "version": "1.0.83-3",
+ "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==",
"cpu": [
"x64"
],
diff --git a/nodejs/package.json b/nodejs/package.json
index 89863520e0..adbc639cb0 100644
--- a/nodejs/package.json
+++ b/nodejs/package.json
@@ -56,7 +56,7 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.82-0",
+ "@github/copilot": "^1.0.83-3",
"koffi": "^3.1.0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json
index ad675a88c7..09df5b1ff1 100644
--- a/nodejs/samples/package-lock.json
+++ b/nodejs/samples/package-lock.json
@@ -18,7 +18,7 @@
"version": "0.0.0-dev",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.82-0",
+ "@github/copilot": "^1.0.83-3",
"koffi": "^3.1.0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 9b853aa597..83b0a5f483 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -16,7 +16,7 @@ import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { Socket } from "node:net";
-import { dirname, isAbsolute, join } from "node:path";
+import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
createMessageConnection,
@@ -34,6 +34,7 @@ import {
registerClientSessionApiHandlers,
} from "./generated/rpc.js";
import type {
+ ConnectClientInfo,
GitHubTelemetryNotification,
GitHubTokenAcquireRequest,
GitHubTokenAcquireResult,
@@ -43,6 +44,7 @@ import type {
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
import { CopilotSession } from "./session.js";
import type { FfiRuntimeHost } from "./ffiRuntimeHost.js";
+import { materializeRuntimeBundle } from "./runtimeArtifacts.js";
import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js";
import { createCopilotRequestAdapter } from "./copilotRequestHandler.js";
import type { CopilotRequestHandler } from "./copilotRequestHandler.js";
@@ -51,6 +53,7 @@ import { ToolSet } from "./toolSet.js";
import type {
AutoModeSwitchRequest,
AutoModeSwitchResponse,
+ CopilotClientInfo,
CopilotClientMode,
CopilotClientOptions,
CustomAgentConfig,
@@ -258,6 +261,22 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[]
});
}
+/**
+ * Map the public {@link CopilotClientInfo} onto the generated connect wire
+ * shape, dropping empty fields. Returns `undefined` when no field carries a
+ * non-empty value so the caller omits `clientInfo` from the handshake and keeps
+ * the runtime's default attribution.
+ */
+function clientInfoToWire(info: CopilotClientInfo | undefined): ConnectClientInfo | undefined {
+ if (info == null) return undefined;
+ const wire: ConnectClientInfo = {};
+ if (info.applicationName) wire.editorName = info.applicationName;
+ if (info.applicationVersion) wire.editorVersion = info.applicationVersion;
+ if (info.integrationName) wire.extensionName = info.integrationName;
+ if (info.integrationVersion) wire.extensionVersion = info.integrationVersion;
+ return Object.keys(wire).length > 0 ? wire : undefined;
+}
+
/**
* Convert a {@link LargeToolOutputConfig} from the public API shape
* (`outputDirectory`) to the wire shape (`outputDir`).
@@ -365,16 +384,19 @@ function getCliPlatformPackageNames(): string[] {
return variants.map((variant) => `@github/copilot-${variant}-${arch}`);
}
+interface BundledCliPackage {
+ root: string;
+ platform: string;
+}
+
/**
- * Gets the path to the bundled CLI from the platform-specific @github/copilot-*
- * package. Uses index.js directly rather than the native binary so the CLI runs
- * under the current Node.js runtime.
+ * Resolves the current platform package and its npm prebuilds folder.
*
* In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions
* bundled with esbuild format:"cjs"), import.meta is empty so we fall back to
* walking node_modules to find the package.
*/
-function getBundledCliPath(): string {
+function getBundledCliPackage(): BundledCliPackage {
const packageNames = getCliPlatformPackageNames();
if (typeof import.meta.resolve === "function") {
@@ -383,7 +405,10 @@ function getBundledCliPath(): string {
try {
const packageEntryUrl = import.meta.resolve(packageName);
const packageEntryPath = fileURLToPath(packageEntryUrl);
- return join(dirname(packageEntryPath), "index.js");
+ return {
+ root: dirname(packageEntryPath),
+ platform: packageName.slice("@github/copilot-".length),
+ };
} catch {
// Try the next candidate platform package.
}
@@ -400,9 +425,13 @@ function getBundledCliPath(): string {
const searchPaths = req.resolve.paths("@github/copilot") ?? [];
for (const base of searchPaths) {
for (const packageName of packageNames) {
- const candidate = join(base, ...packageName.split("/"), "index.js");
+ const root = join(base, ...packageName.split("/"));
+ const candidate = join(root, "index.js");
if (existsSync(candidate)) {
- return candidate;
+ return {
+ root,
+ platform: packageName.slice("@github/copilot-".length),
+ };
}
}
}
@@ -413,6 +442,14 @@ function getBundledCliPath(): string {
);
}
+function getBundledRuntimePath(): string {
+ const bundled = getBundledCliPackage();
+ return materializeRuntimeBundle({
+ packageRoot: bundled.root,
+ platform: bundled.platform,
+ });
+}
+
/**
* Main client for interacting with the Copilot CLI.
*
@@ -503,6 +540,7 @@ export class CopilotClient {
sessionIdleTimeoutSeconds: number;
enableRemoteSessions: boolean;
mode: CopilotClientMode;
+ clientInfo?: CopilotClientInfo;
};
private isExternalServer: boolean = false;
private forceStopping: boolean = false;
@@ -520,6 +558,7 @@ export class CopilotClient {
private _rpc: ReturnType | null = null;
private _internalRpc: ReturnType | null = null;
private processExitPromise: Promise | null = null; // Rejects when CLI process exits
+ private processTransportError: Error | null = null;
private negotiatedProtocolVersion: number | null = null;
/** Connection-level session filesystem config, set via constructor option. */
private sessionFsConfig: SessionFsConfig | null = null;
@@ -733,10 +772,14 @@ export class CopilotClient {
conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : undefined;
const effectiveEnv = connEnv ?? options.env ?? process.env;
this.resolvedEnv = effectiveEnv;
- this.resolvedCliPath =
- conn.kind === "stdio" || conn.kind === "tcp"
- ? (conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath())
- : undefined;
+ if (conn.kind === "stdio" || conn.kind === "tcp") {
+ const explicitCliPath = conn.path ?? effectiveEnv.COPILOT_CLI_PATH;
+ if (explicitCliPath) {
+ this.resolvedCliPath = explicitCliPath;
+ } else {
+ this.resolvedCliPath = getBundledRuntimePath();
+ }
+ }
// Collect extra CLI args from the connection variant (if any).
const connArgs: readonly string[] =
@@ -754,6 +797,7 @@ export class CopilotClient {
sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
enableRemoteSessions: options.enableRemoteSessions ?? false,
mode: options.mode ?? "copilot-cli",
+ clientInfo: options.clientInfo,
};
// Empty mode: validate at construction time that the app supplied a
@@ -951,6 +995,8 @@ export class CopilotClient {
return;
}
+ this.forceStopping = false;
+ this.processTransportError = null;
this.state = "connecting";
try {
@@ -997,8 +1043,10 @@ export class CopilotClient {
this.state = "connected";
} catch (error) {
+ const startupError = this.processTransportError ?? error;
+ await this.forceStop();
this.state = "error";
- throw error;
+ throw startupError;
}
}
@@ -1678,6 +1726,7 @@ export class CopilotClient {
requestPermission: !!config.onPermissionRequest,
requestUserInput: !!config.onUserInputRequest,
requestElicitation: !!config.onElicitationRequest,
+ askUserVariant: config.askUserVariant,
...(config.enableMcpApps ? { requestMcpApps: true } : {}),
...(config.githubMcpToolConfig != null
? { githubMcpToolConfig: config.githubMcpToolConfig }
@@ -1720,6 +1769,7 @@ export class CopilotClient {
gitHubTokenProviderRegistrationId,
remoteSession: config.remoteSession,
cloud: config.cloud,
+ featureFlags: config.featureFlags,
expAssignments: config.expAssignments,
enableManagedSettings: config.enableManagedSettings,
managedSettings: config.managedSettings,
@@ -1946,6 +1996,7 @@ export class CopilotClient {
config.onPermissionRequest !== defaultJoinSessionPermissionHandler,
requestUserInput: !!config.onUserInputRequest,
requestElicitation: !!config.onElicitationRequest,
+ askUserVariant: config.askUserVariant,
...(config.enableMcpApps ? { requestMcpApps: true } : {}),
...(config.githubMcpToolConfig != null
? { githubMcpToolConfig: config.githubMcpToolConfig }
@@ -1990,6 +2041,7 @@ export class CopilotClient {
gitHubTokenProviderRegistrationId,
remoteSession: config.remoteSession,
openCanvases: config.openCanvases,
+ featureFlags: config.featureFlags,
expAssignments: config.expAssignments,
enableManagedSettings: config.enableManagedSettings,
managedSettings: config.managedSettings,
@@ -2186,6 +2238,7 @@ export class CopilotClient {
const connectParams: {
token?: string;
enableGitHubTelemetryForwarding?: boolean;
+ clientInfo?: ConnectClientInfo;
} = { token: this.effectiveConnectionToken };
// Opt in to GitHub telemetry forwarding at the connection level when a
// handler is registered (mirrors the runtime, which reads this flag on the
@@ -2194,6 +2247,14 @@ export class CopilotClient {
if (this.onGitHubTelemetry != null) {
connectParams.enableGitHubTelemetryForwarding = true;
}
+ // Declare the integrating application's identity so the runtime attributes
+ // the telemetry it emits on this connection to a consistent surface
+ // instead of its own build. Empty fields are dropped, and an
+ // all-empty identity is omitted entirely.
+ const clientInfo = clientInfoToWire(this.options.clientInfo);
+ if (clientInfo != null) {
+ connectParams.clientInfo = clientInfo;
+ }
const result = await raceAgainstExit(this.internalRpc.connect(connectParams));
serverVersion = result.protocolVersion;
} catch (err) {
@@ -2715,27 +2776,27 @@ export class CopilotClient {
// Set up a promise that rejects when the process exits (used to race against RPC calls)
this.processExitPromise = new Promise((_, rejectProcessExit) => {
this.cliProcess!.on("exit", (code) => {
- // Give a small delay for stderr to be fully captured
- setTimeout(() => {
- const stderrOutput = this.stderrBuffer.trim();
- if (stderrOutput) {
- rejectProcessExit(
- new Error(
- `CLI server exited with code ${code}\nstderr: ${stderrOutput}`
- )
- );
- } else {
- rejectProcessExit(
- new Error(`CLI server exited unexpectedly with code ${code}`)
- );
- }
- }, 50);
+ if (this.messageWriter) {
+ this.messageWriter.suppressWriteErrors = true;
+ }
+ const stderrOutput = this.stderrBuffer.trim();
+ if (stderrOutput) {
+ rejectProcessExit(
+ new Error(
+ `CLI server exited with code ${code}\nstderr: ${stderrOutput}`
+ )
+ );
+ } else {
+ rejectProcessExit(
+ new Error(`CLI server exited unexpectedly with code ${code}`)
+ );
+ }
});
});
// Prevent unhandled rejection when process exits normally (we only use this in Promise.race)
this.processExitPromise.catch(() => {});
- this.cliProcess.on("exit", (code) => {
+ this.cliProcess.on("close", (code) => {
if (!resolved) {
resolved = true;
const stderrOutput = this.stderrBuffer.trim();
@@ -2780,7 +2841,15 @@ export class CopilotClient {
/** Starts the in-process FFI runtime with SDK-managed typed options. */
private async startInProcessFfi(): Promise {
- const entrypoint = this.resolveCliPathForFfi();
+ const explicitEntrypoint = this.resolvedEnv.COPILOT_CLI_PATH;
+ const runtimeLibrary = explicitEntrypoint
+ ? join(
+ dirname(resolve(explicitEntrypoint)),
+ "prebuilds",
+ CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint),
+ "runtime.node"
+ )
+ : join(dirname(getBundledRuntimePath()), "runtime.node");
// Load the FFI host lazily so the native `koffi` addon (and its
// platform-specific `koffi.node`) is only loaded on the in-process path;
// out-of-process (stdio/tcp) consumers never touch the native dependency.
@@ -2815,12 +2884,7 @@ export class CopilotClient {
args.push("--remote");
}
- const host = FfiRuntimeHost.create(
- entrypoint,
- CopilotClient.getNapiPrebuildsFolder(entrypoint),
- environment,
- args
- );
+ const host = FfiRuntimeHost.create(runtimeLibrary, explicitEntrypoint, environment, args);
this.ffiHost = host;
await host.start();
}
@@ -2843,20 +2907,6 @@ export class CopilotClient {
this.connection.listen();
}
- /**
- * Resolves the CLI entrypoint used for in-process FFI hosting: `COPILOT_CLI_PATH`
- * when set, otherwise the bundled platform-package entrypoint.
- */
- private resolveCliPathForFfi(): string {
- return this.resolvedEnv.COPILOT_CLI_PATH ?? getBundledCliPath();
- }
-
- /**
- * Returns the napi prebuilds folder name for the current host — the
- * `-` convention (e.g. `win32-x64`, `darwin-arm64`,
- * `linux-x64`, `linuxmusl-x64`) under which the runtime ships
- * `prebuilds//runtime.node`.
- */
private static getNapiPrebuildsFolder(entrypoint: string): string {
const arch = process.arch;
if (arch !== "x64" && arch !== "arm64") {
@@ -2900,6 +2950,10 @@ export class CopilotClient {
}
this.state = "error";
const reason = err instanceof Error ? (err.stack ?? err.message) : String(err);
+ const stderrOutput = this.stderrBuffer.trim();
+ this.processTransportError = new Error(
+ `CLI server connection failed: ${reason}${stderrOutput ? `\nstderr: ${stderrOutput}` : ""}`
+ );
this.logDebug(`stdin pipe error: ${reason}`);
try {
this.connection?.dispose();
diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts
index d756308734..ac0ccdb7ce 100644
--- a/nodejs/src/extension.ts
+++ b/nodejs/src/extension.ts
@@ -85,6 +85,8 @@ export {
type FactoryRunResult,
type FactoryRunStatus,
type FactoryRunSummary,
+ type FactoryListRunsOptions,
+ type FactoryRunsPage,
type FactoryRunDetail,
type FactoryProgressPage,
type FactoryProgressLine,
diff --git a/nodejs/src/factory.ts b/nodejs/src/factory.ts
index 8a6c787471..6212f462b4 100644
--- a/nodejs/src/factory.ts
+++ b/nodejs/src/factory.ts
@@ -4,6 +4,8 @@
import type {
FactoryGetRunProgressRequest,
+ FactoryListRunsRequest,
+ FactoryListRunsResult,
FactoryProgressPage,
FactoryRunDetail,
FactoryRunResult,
@@ -26,6 +28,22 @@ export type {
FactoryRunSummary,
} from "./generated/rpc.js";
+/**
+ * Options for paging durable factory runs.
+ *
+ * @experimental Part of the experimental Agent Factories surface and may
+ * change or be removed in future SDK or CLI releases.
+ */
+export type FactoryListRunsOptions = FactoryListRunsRequest;
+
+/**
+ * A page of durable factory runs and its paging metadata.
+ *
+ * @experimental Part of the experimental Agent Factories surface and may
+ * change or be removed in future SDK or CLI releases.
+ */
+export type FactoryRunsPage = FactoryListRunsResult;
+
/**
* Run statuses a factory run can no longer move away from.
*
@@ -242,6 +260,10 @@ export interface RunOptions {
args?: TArgs;
/** Optional per-invocation resource ceiling overrides. */
limits?: FactoryLimits;
+ /** Whether to notify the originating session when the factory completes. */
+ notifyOnComplete?: boolean;
+ /** Whether to emit factory phase names to the session transcript. */
+ logPhaseNames?: boolean;
/**
* Prior run whose persisted identity, arguments, journal, and accounting should be resumed.
*
@@ -259,6 +281,10 @@ export interface RunOptions {
export interface ResumeOptions {
/** Optional per-invocation resource ceiling overrides. */
limits?: FactoryLimits;
+ /** Whether to notify the originating session when the factory completes. */
+ notifyOnComplete?: boolean;
+ /** Whether to emit factory phase names to the session transcript. */
+ logPhaseNames?: boolean;
}
/**
@@ -328,8 +354,20 @@ export interface SessionFactoryApi {
waitForRun(runId: string, options?: { signal?: AbortSignal }): Promise;
/**
* List the newest default page of this session's durable factory runs.
+ *
+ * This backwards-compatible overload returns only the runs array. Pass
+ * paging options to receive the full page, including its cursors and
+ * truncation metadata.
*/
listRuns(): Promise;
+ /**
+ * Page this session's durable factory runs.
+ *
+ * `afterSeq` and `beforeSeq` are exclusive cursors. The result includes
+ * `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` so callers
+ * can continue paging without using the raw RPC client.
+ */
+ listRuns(options: FactoryListRunsOptions): Promise;
/** Read durable phases, direct agents, and the latest progress tail for a run. */
getRunDetail(runId: string): Promise;
/** Page durable progress forward, backward, or from the latest tail. */
diff --git a/nodejs/src/ffiRuntimeHost.ts b/nodejs/src/ffiRuntimeHost.ts
index a92aa1589a..4795e325ce 100644
--- a/nodejs/src/ffiRuntimeHost.ts
+++ b/nodejs/src/ffiRuntimeHost.ts
@@ -7,10 +7,8 @@
* and speaking JSON-RPC over its C ABI (FFI) instead of spawning a CLI child process
* and communicating over stdio/TCP.
*
- * The native `host_start` export spawns the CLI worker itself
- * (`node --embedded-host` for a `.js` entrypoint, or `
- * --embedded-host` for a packaged binary), so the SDK never launches the worker
- * directly. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI:
+ * The native `host_start` export constructs the Rust server synchronously in this
+ * process. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI:
* writes go to `connection_write`; inbound frames arrive on a native callback that
* feeds {@link FfiRuntimeHost.receiveStream}. The existing `vscode-jsonrpc`
* `StreamMessageReader`/`StreamMessageWriter` handle framing unchanged — this is a
@@ -19,7 +17,7 @@
import { existsSync } from "node:fs";
import koffi from "koffi";
-import { dirname, join, resolve } from "node:path";
+import { resolve } from "node:path";
import { PassThrough, Writable } from "node:stream";
const SYMBOL_PREFIX = "copilot_runtime_";
@@ -97,14 +95,12 @@ function loadLibrary(libraryPath: string): FfiLibrary {
return loadedLibrary;
}
-function buildArgvJson(cliEntrypoint: string, args: readonly string[]): Buffer {
- // A `.js` entrypoint is launched via node; the packaged single-file CLI binary
- // embeds its own Node and is invoked directly. `--no-auto-update` pins the worker
- // to the bundled pkg matching the loaded cdylib, instead of drifting to a newer
- // version installed under the user's `~/.copilot/pkg` (which would cause ABI skew).
- const argv = cliEntrypoint.toLowerCase().endsWith(".js")
- ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"]
- : [cliEntrypoint, "--embedded-host", "--no-auto-update"];
+function buildArgvJson(cliEntrypoint: string | undefined, args: readonly string[]): Buffer {
+ const argv = cliEntrypoint
+ ? cliEntrypoint.toLowerCase().endsWith(".js")
+ ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"]
+ : [cliEntrypoint, "--embedded-host", "--no-auto-update"]
+ : [];
argv.push(...args);
return Buffer.from(JSON.stringify(argv), "utf8");
}
@@ -140,7 +136,7 @@ export class FfiRuntimeHost {
private constructor(
private readonly libraryPath: string,
- private readonly cliEntrypoint: string,
+ private readonly cliEntrypoint: string | undefined,
private readonly environment: Record | undefined,
private readonly args: readonly string[]
) {
@@ -161,41 +157,38 @@ export class FfiRuntimeHost {
}
/**
- * Resolves the cdylib next to the given CLI entrypoint and prepares the FFI host.
- * The cdylib is resolved as `prebuilds//runtime.node` relative to
- * the entrypoint directory (the napi-rs `-` layout, e.g.
- * `linux-x64`). Throws if it cannot be found.
+ * Loads the runtime cdylib at the given path and prepares the FFI host.
*/
static create(
- cliEntrypoint: string,
- prebuildsFolder: string,
+ libraryPath: string,
+ cliEntrypoint: string | undefined,
environment: Record | undefined,
args: readonly string[]
): FfiRuntimeHost {
- const fullEntrypoint = resolve(cliEntrypoint);
- const distDir = dirname(fullEntrypoint);
- const libraryPath = join(distDir, "prebuilds", prebuildsFolder, "runtime.node");
- if (!existsSync(libraryPath)) {
- throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`);
+ const fullLibraryPath = resolve(libraryPath);
+ if (!existsSync(fullLibraryPath)) {
+ throw new Error(`FFI runtime library not found at '${fullLibraryPath}'.`);
}
- return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args);
+ return new FfiRuntimeHost(
+ fullLibraryPath,
+ cliEntrypoint ? resolve(cliEntrypoint) : undefined,
+ environment,
+ args
+ );
}
- /**
- * Starts the in-process runtime: spawns the CLI worker via the native host,
- * waits for readiness, and opens the FFI JSON-RPC connection.
- */
+ /** Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. */
async start(): Promise {
const argvJson = buildArgvJson(this.cliEntrypoint, this.args);
const envJson = buildEnvJson(this.environment);
- // The native host spawns the CLI worker itself and has no cwd parameter, so the
- // worker inherits this process's cwd. A custom working directory is intentionally
+ // The native host has no cwd parameter, so it uses this process's cwd. A custom
+ // working directory is intentionally
// unsupported for the in-process transport (rejected by the client constructor)
// rather than mutating the shared process-global cwd here.
- // host_start blocks until the worker connects back and signals readiness
- // (up to ~30s); run it as an async FFI call so the Node event loop isn't blocked.
+ // host_start constructs the native engine synchronously; run it as an async FFI
+ // call so the Node event loop isn't blocked.
this.serverId = await new Promise((resolvePromise, rejectPromise) => {
this.lib.hostStart.async(
argvJson,
@@ -212,9 +205,7 @@ export class FfiRuntimeHost {
);
});
if (!this.serverId) {
- throw new Error(
- `copilot_runtime_host_start failed (library '${this.libraryPath}', entrypoint '${this.cliEntrypoint}').`
- );
+ throw new Error(`copilot_runtime_host_start failed (library '${this.libraryPath}').`);
}
this.outboundCallback = koffi.register(
diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts
index db0ea63dcc..716f76cddc 100644
--- a/nodejs/src/generated/rpc.ts
+++ b/nodejs/src/generated/rpc.ts
@@ -5,7 +5,7 @@
import type { MessageConnection } from "vscode-jsonrpc/node.js";
-import type { AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js";
+import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js";
/** A value that can be represented losslessly on the SDK JSON wire. */
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
@@ -552,7 +552,13 @@ export type CatalogNetworkFailureReason =
| "tls"
/** The connection was refused or reset. */
| "connection-refused"
- /** The authority returned a status the runtime treats as a failure. */
+ /** The configured proxy returned 407 and requires authentication. */
+ | "proxy-authentication-required"
+ /** The authority rate-limited requests and supplied or implied a bounded cooldown. */
+ | "rate-limited"
+ /** The authority returned a transient 5xx response. */
+ | "service-unavailable"
+ /** The authority returned another status the runtime treats as a failure. */
| "http-status"
/** The response exceeded the permitted size. */
| "response-too-large"
@@ -863,6 +869,64 @@ export type DiscoveredExtensionMode =
| "load_only"
/** Extensions are loaded and the agent can create, reload, and manage them. */
| "load_and_augment";
+/**
+ * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "HookType".
+ */
+/** @experimental */
+export type HookType =
+ /** Runs before a tool is invoked. */
+ | "preToolUse"
+ /** Runs before an MCP tool is invoked. */
+ | "preMcpToolCall"
+ /** Runs after a tool completes successfully. */
+ | "postToolUse"
+ /** Runs after a tool fails. */
+ | "postToolUseFailure"
+ /** Runs after the user submits a prompt. */
+ | "userPromptSubmitted"
+ /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */
+ | "userPromptTransformed"
+ /** Runs when a session starts. */
+ | "sessionStart"
+ /** Runs when a session ends. */
+ | "sessionEnd"
+ /** Runs after an agent result is produced. */
+ | "postResult"
+ /** Runs before a pull request description is generated. */
+ | "prePRDescription"
+ /** Runs when the agent encounters an error. */
+ | "errorOccurred"
+ /** Runs when the agent stops. */
+ | "agentStop"
+ /** Runs when a subagent starts. */
+ | "subagentStart"
+ /** Runs when a subagent stops. */
+ | "subagentStop"
+ /** Runs before conversation context is compacted. */
+ | "preCompact"
+ /** Runs when the agent requests permission. */
+ | "permissionRequest"
+ /** Runs when the agent emits a notification. */
+ | "notification";
+/**
+ * Configuration tier that contributed a discovered hook action.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "HookOrigin".
+ */
+/** @experimental */
+export type HookOrigin =
+ /** Hook loaded from user settings or the user's hook directory. */
+ | "user"
+ /** Hook loaded from repository settings or the repository hook directory. */
+ | "repository"
+ /** Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. */
+ | "plugin"
+ /** Hook enforced by centrally managed policy. */
+ | "policy";
/**
* Server transport type: stdio, http, sse (deprecated), or memory
*
@@ -1127,6 +1191,16 @@ export type FactoryRunFailure =
* Factory failure variant discriminator.
*/
type: "factory_accounting_incomplete";
+ }
+ | {
+ /**
+ * Factory run identifier.
+ */
+ runId: string;
+ /**
+ * Factory failure variant discriminator.
+ */
+ type: "factory_provider_disconnected";
};
/**
* Cumulative resource ceiling that stopped a factory run.
@@ -1332,49 +1406,6 @@ export type HistoryRewindOutcome =
| "checkpoint-cleanup-failed"
/** Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. */
| "snapshot-prune-failed";
-/**
- * Hook event name dispatched through the SDK callback transport.
- *
- * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
- * via the `definition` "HookType".
- */
-/** @experimental */
-/** @internal */
-export type HookType =
- /** Runs before a tool is invoked. */
- | "preToolUse"
- /** Runs before an MCP tool is invoked. */
- | "preMcpToolCall"
- /** Runs after a tool completes successfully. */
- | "postToolUse"
- /** Runs after a tool fails. */
- | "postToolUseFailure"
- /** Runs after the user submits a prompt. */
- | "userPromptSubmitted"
- /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */
- | "userPromptTransformed"
- /** Runs when a session starts. */
- | "sessionStart"
- /** Runs when a session ends. */
- | "sessionEnd"
- /** Runs after an agent result is produced. */
- | "postResult"
- /** Runs before a pull request description is generated. */
- | "prePRDescription"
- /** Runs when the agent encounters an error. */
- | "errorOccurred"
- /** Runs when the agent stops. */
- | "agentStop"
- /** Runs when a subagent starts. */
- | "subagentStart"
- /** Runs when a subagent stops. */
- | "subagentStop"
- /** Runs before conversation context is compacted. */
- | "preCompact"
- /** Runs when the agent requests permission. */
- | "permissionRequest"
- /** Runs when the agent emits a notification. */
- | "notification";
/**
* Source for direct repo installs (when marketplace is empty)
*
@@ -4573,7 +4604,7 @@ export interface AgentGetCurrentResult {
agent?: AgentInfo | null;
}
/**
- * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path.
+ * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path.
*
* This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
* via the `definition` "AgentInfo".
@@ -4613,6 +4644,11 @@ export interface AgentInfo {
* Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference.
*/
model?: string;
+ /**
+ * Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference.
+ */
+ models?: string[];
+ modelPolicy?: AgentModelPolicy;
/**
* MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema.
*
@@ -5487,6 +5523,7 @@ export interface CanvasProviderUnregisterRequest {
*/
/** @experimental */
export interface CapiSessionOptions {
+ autoTier?: AutoTier;
/**
* Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable.
*/
@@ -5833,6 +5870,10 @@ export interface CatalogNetworkFailureError {
* HTTP status code, when the failure was a rejected response.
*/
statusCode?: number;
+ /**
+ * Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback.
+ */
+ retryAfterSeconds?: number;
/**
* Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret.
*/
@@ -5884,7 +5925,7 @@ export interface CatalogPolicyRejectedError {
export interface CatalogSearchRequest {
contract: CatalogClientContract;
/**
- * Free-text search query. Never written to logs or telemetry.
+ * Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry.
*/
query: string;
/**
@@ -6820,6 +6861,37 @@ export interface DiscoveredExtensionsEnableRequest {
*/
ids: string[];
}
+/**
+ * One server-discovered hook action from user, repository, plugin, or managed-policy configuration.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "DiscoveredHook".
+ */
+/** @experimental */
+export interface DiscoveredHook {
+ /**
+ * Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks.
+ */
+ id: string;
+ hookType: HookType;
+ origin: HookOrigin;
+ /**
+ * Human-readable source label, such as a hook file path, settings source, or plugin name.
+ */
+ source?: string;
+ /**
+ * Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions.
+ */
+ projectPath?: string;
+ /**
+ * Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action.
+ */
+ enabled: boolean;
+ /**
+ * Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion.
+ */
+ disableKey?: string;
+}
/**
* MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state.
*
@@ -6859,6 +6931,10 @@ export interface EnqueueCommandParams {
* Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately.
*/
command: string;
+ /**
+ * Optional user-facing text for the queue row. The command string is shown when omitted.
+ */
+ displayText?: string | null;
}
/**
* Indicates whether the command was accepted into the local execution queue.
@@ -8151,6 +8227,10 @@ export interface FactoryRunResult {
* Factory run identifier.
*/
runId: string;
+ /**
+ * One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime.
+ */
+ attempt?: number;
status: FactoryRunStatus;
/**
* Completed factory result.
@@ -8953,6 +9033,44 @@ export interface HookInvokeRequest {
export interface HookInvokeResponse {
output?: JsonValue;
}
+/**
+ * Optional project paths and host-exclusion behavior for server-scoped hook discovery.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "HooksDiscoverRequest".
+ */
+/** @experimental */
+export interface HooksDiscoverRequest {
+ /**
+ * Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion.
+ */
+ projectPaths?: string[];
+ /**
+ * When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings.
+ */
+ excludeHostHooks?: boolean;
+}
+/**
+ * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "HooksDiscoverResult".
+ */
+/** @experimental */
+export interface HooksDiscoverResult {
+ /**
+ * All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key.
+ */
+ hooks: DiscoveredHook[];
+ /**
+ * Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available.
+ */
+ warnings: string[];
+ /**
+ * Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path.
+ */
+ errors: string[];
+}
/**
* Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source.
*
@@ -12351,6 +12469,10 @@ export interface ModelBillingPromo {
* Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present.
*/
message?: string;
+ /**
+ * Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`.
+ */
+ showBanner?: boolean;
}
/**
* Service-published warning text that hosts should display when presenting a model.
@@ -12398,6 +12520,10 @@ export interface ModelApplyStartupOverlayRequest {
* Model required by server-managed policy, when configured.
*/
serverManagedModel?: string;
+ /**
+ * Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins.
+ */
+ policyHelperModel?: string;
/**
* Model selected by repository settings, when configured.
*/
@@ -15837,6 +15963,10 @@ export interface QueuePendingItems {
* Stable opaque id for the canonical queued item. Batch rows share one id.
*/
id: string;
+ /**
+ * Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes.
+ */
+ messageId?: string;
kind: QueuePendingItemsKind;
/**
* Human-readable text to display for this queue entry in the UI
@@ -16042,7 +16172,7 @@ export interface RegisterExtensionToolsParams {
*/
sessionId: string;
/**
- * In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead.
+ * In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface.
*
* @internal
*
@@ -16060,7 +16190,7 @@ export interface RegisterExtensionToolsParams {
/** @experimental */
export interface SessionsRegisterExtensionToolsOnSessionOptions {
/**
- * In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration.
+ * In-process `() => boolean` gating callback used only by the CLI.
*
* @internal
*/
@@ -16076,7 +16206,7 @@ export interface SessionsRegisterExtensionToolsOnSessionOptions {
/** @internal */
export interface RegisterExtensionToolsResult {
/**
- * In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration.
+ * In-process unsubscribe function used only by the CLI.
*
* @internal
*
@@ -16523,7 +16653,7 @@ export interface SandboxConfigUserPolicyNetwork {
/** @experimental */
export interface SandboxConfigUserPolicyNetworkProxy {
/**
- * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is.
+ * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is.
*/
url: string;
/**
@@ -16588,6 +16718,27 @@ export interface SandboxConfigAuth {
*/
gh?: boolean;
}
+/**
+ * Managed sandbox enforcement state for a session.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SandboxEnforcementStatus".
+ */
+/** @experimental */
+export interface SandboxEnforcementStatus {
+ /**
+ * Whether the effective managed policy requires an available sandbox backend.
+ */
+ required: boolean;
+ /**
+ * Whether an enforcement failure has permanently blocked the session.
+ */
+ blocked: boolean;
+ /**
+ * The first sandbox enforcement failure that blocked the session.
+ */
+ reason?: string;
+}
/**
* Register an absolute-time scheduled prompt.
*
@@ -18275,6 +18426,17 @@ export interface SessionOpenOptions {
* Additional directories to search for skills.
*/
skillDirectories?: string[];
+ /**
+ * Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default.
+ */
+ enableSkills?: boolean;
+ /**
+ * Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler.
+ *
+ * @internal
+ * @experimental
+ */
+ hasSkillProvider?: boolean;
/**
* Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available.
*/
@@ -18634,7 +18796,7 @@ export interface SessionsOpenCloud {
owner?: string;
options?: SessionOpenOptions;
/**
- * In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase.
+ * In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary.
*
* @internal
*/
@@ -19438,6 +19600,28 @@ export interface SessionsPruneOldRequest {
*/
excludeSessionIds?: string[];
}
+/**
+ * Pagination options for reading an inactive or active local session's persisted event journal.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SessionsReadPersistedEventsRequest".
+ */
+/** @experimental */
+export interface SessionsReadPersistedEventsRequest {
+ /**
+ * Session ID whose persisted event journal should be read.
+ */
+ sessionId: string;
+ /**
+ * Opaque cursor returned by a previous persisted-event read. Omit on the first call.
+ */
+ cursor?: string;
+ /**
+ * Maximum number of events to return in this batch (1–1000, default 200).
+ */
+ max?: number;
+ direction?: EventsReadDirection;
+}
/**
* Session ID whose in-use lock should be released.
*
@@ -19808,7 +19992,7 @@ export interface SessionUpdateOptionsParams {
*/
enableSessionStore?: boolean;
/**
- * Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset.
+ * Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered.
*/
enableSkills?: boolean;
contextTier?: OptionsUpdateContextTier;
@@ -20029,6 +20213,83 @@ export interface SkillList {
*/
skills: Skill[];
}
+/**
+ * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SkillProviderDescriptor".
+ */
+/** @experimental */
+export interface SkillProviderDescriptor {
+ /**
+ * Invocation and display name.
+ */
+ name: string;
+ /**
+ * Description used in skill catalogs without fetching content.
+ */
+ description: string;
+ /**
+ * Whether users may invoke the skill directly. Defaults to true.
+ */
+ userInvocable?: boolean;
+ /**
+ * Whether model invocation is disabled. Defaults to false.
+ */
+ disableModelInvocation?: boolean;
+ /**
+ * Optional freeform argument hint used by slash-command catalogs.
+ */
+ argumentHint?: string;
+}
+/**
+ * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SkillProviderListResult".
+ */
+/** @experimental */
+/** @internal */
+export interface SkillProviderListResult {
+ /**
+ * Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison.
+ *
+ * @maxItems 1024
+ */
+ skills: SkillProviderDescriptor[];
+}
+/**
+ * Identifies one SDK-provided skill by invocation name.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SkillProviderReadRequest".
+ */
+/** @experimental */
+/** @internal */
+export interface SkillProviderReadRequest {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Invocation name of the skill to read.
+ */
+ name: string;
+}
+/**
+ * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SkillProviderReadResult".
+ */
+/** @experimental */
+/** @internal */
+export interface SkillProviderReadResult {
+ /**
+ * Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit.
+ */
+ markdown: string;
+}
/**
* Skill names to mark as disabled in global configuration, replacing any previous list.
*
@@ -20149,7 +20410,7 @@ export interface SkillsInvokedSkill {
*/
name: string;
/**
- * Path to the SKILL.md file
+ * Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity
*/
path: string;
/**
@@ -20160,6 +20421,10 @@ export interface SkillsInvokedSkill {
* Tools that should be auto-approved when this skill is active, captured at invocation time
*/
allowedTools?: string[];
+ /**
+ * Whether model invocation was disabled when this skill was invoked
+ */
+ disableModelInvocation?: boolean;
/**
* Turn number when the skill was invoked
*/
@@ -20261,6 +20526,7 @@ export interface SlashCommandCompletedResult {
* Optional user-facing message describing the completed command
*/
message?: string;
+ mode?: SessionMode;
/**
* True when the invocation mutated user runtime settings; consumers caching settings should refresh
*/
@@ -20446,6 +20712,7 @@ export interface SubagentSettingsEntry {
* Model override for matching subagents
*/
model?: string;
+ modelPolicy?: AgentModelPolicy;
/**
* Reasoning effort override for matching subagents
*/
@@ -21618,13 +21885,13 @@ export interface UIEphemeralQueryRequest {
*/
question: string;
/**
- * In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer.
+ * In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface.
*
* @internal
*/
onChunk?: OpaqueInProcessValue;
/**
- * In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration.
+ * In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface.
*
* @internal
*/
@@ -22764,6 +23031,19 @@ export interface SessionLimitPredictionPredictRequest {
modelId?: string;
clientType?: SessionLimitPredictionClientType;
}
+/**
+ * Identifies the target session.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "SkillProviderListRequest".
+ */
+/** @experimental */
+export interface SkillProviderListRequest {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
/**
* Identifies the target session.
*
@@ -22793,6 +23073,18 @@ export function createServerRpc(connection: MessageConnection) {
ping: async (params: PingRequest): Promise =>
connection.sendRequest("ping", params),
/** @experimental */
+ hooks: {
+ /**
+ * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources.
+ *
+ * @param params Optional project paths and host-exclusion behavior for server-scoped hook discovery.
+ *
+ * @returns Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources.
+ */
+ discover: async (params: HooksDiscoverRequest): Promise =>
+ connection.sendRequest("hooks.discover", params),
+ },
+ /** @experimental */
models: {
/**
* Lists Copilot models available to the authenticated user.
@@ -22975,7 +23267,7 @@ export function createServerRpc(connection: MessageConnection) {
connection.sendRequest("extensions.disable", params),
},
/**
- * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility.
+ * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher.
*
* @experimental
*/
@@ -23318,6 +23610,15 @@ export function createServerRpc(connection: MessageConnection) {
*/
list: async (params: SessionsListRequest): Promise =>
connection.sendRequest("sessions.list", params),
+ /**
+ * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session.
+ *
+ * @param params Pagination options for reading an inactive or active local session's persisted event journal.
+ *
+ * @returns Batch of session events returned by a read, with cursor and continuation metadata.
+ */
+ readPersistedEvents: async (params: SessionsReadPersistedEventsRequest): Promise =>
+ connection.sendRequest("sessions.readPersistedEvents", params),
/**
* Finds the local session bound to a GitHub task ID, if any.
*
@@ -23625,6 +23926,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
*/
sendMessages: async (params: SendMessagesRequest): Promise =>
connection.sendRequest("session.sendMessages", { sessionId, ...params }),
+ /** @experimental */
+ sandbox: {
+ /**
+ * Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session.
+ *
+ * @returns Managed sandbox enforcement state for a session.
+ */
+ getEnforcementStatus: async (): Promise =>
+ connection.sendRequest("session.sandbox.getEnforcementStatus", { sessionId }),
+ },
/**
* Aborts the current agent turn.
*
diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts
index 9075379982..7d734bf491 100644
--- a/nodejs/src/generated/session-events.ts
+++ b/nodejs/src/generated/session-events.ts
@@ -24,6 +24,7 @@ export type SessionEvent =
| WarningEvent
| ModelChangeEvent
| ModeChangedEvent
+ | ModeNoticeDeliveredEvent
| SessionLimitsChangedEvent
| PermissionsChangedEvent
| PlanChangedEvent
@@ -40,6 +41,7 @@ export type SessionEvent =
| CompactionStartEvent
| CompactionCompleteEvent
| TaskCompleteEvent
+ | CompletionReceiptEvent
| FusionRouteStartedEvent
| FusionRouteFailedEvent
| FusionResolvedEvent
@@ -49,6 +51,7 @@ export type SessionEvent =
| AssistantTurnStartEvent
| AssistantIntentEvent
| AssistantFusionPhaseStartedEvent
+ | AssistantFusionPhaseActivityEvent
| AssistantFusionPhaseCompletedEvent
| AssistantFusionPhaseFailedEvent
| AssistantServerToolProgressEvent
@@ -135,6 +138,16 @@ export type SessionEvent =
| CanvasRemovedEvent
| ExtensionsAttachmentsPushedEvent
| McpAppToolCallCompleteEvent;
+/**
+ * Routing preference used when the session model is `auto`.
+ */
+export type AutoTier =
+ /** Optimize for efficiency. */
+ | "efficiency"
+ /** Balance efficiency and intelligence. */
+ | "balance"
+ /** Optimize for intelligence. */
+ | "intelligence";
/**
* Hosting platform type of the repository (github or ado)
*/
@@ -306,6 +319,30 @@ export type TaskCompletionOutcome =
| "continue"
/** Completion cannot proceed without intervention; the active objective is paused when one is identified. */
| "blocked";
+/**
+ * Structured terminal status from a tool completion event.
+ */
+export type CompletionReceiptToolStatus =
+ /** The tool completed successfully. */
+ | "success"
+ /** The tool failed without a more specific structured status. */
+ | "failure"
+ /** The tool exceeded its time budget. */
+ | "timeout"
+ /** The user rejected the tool call. */
+ | "rejected"
+ /** The permissions service denied the tool call. */
+ | "denied";
+/**
+ * Runtime reason the completion decision was accepted.
+ */
+export type CompletionReceiptStopReason =
+ /** The model reached a natural terminal response. */
+ | "natural"
+ /** A terminal tool ended the interaction. */
+ | "terminal_tool"
+ /** The configured agentStop continuation limit was reached. */
+ | "agent_stop_block_limit";
/**
* Kind of turn for which HydraFusion routing is running.
*/
@@ -335,6 +372,34 @@ export type FusionPattern =
| "cascade"
/** Run a primary draft, a read-only critique, and a revision. */
| "critique";
+/**
+ * HydraFusion phase kind.
+ */
+/** @experimental */
+export type FusionPhaseKind =
+ /** Primary solver phase. */
+ | "primary"
+ /** Read-only cascade judge phase. */
+ | "judge"
+ /** Cascade repair phase. */
+ | "repair"
+ /** Initial critique-pattern draft phase. */
+ | "draft"
+ /** Read-only critique phase. */
+ | "critic"
+ /** Critique-pattern revision phase. */
+ | "revision"
+ /** Follow-up phase continuing from the resolved model. */
+ | "follow_up";
+/**
+ * Conversation scope in which a HydraFusion phase executes.
+ */
+/** @experimental */
+export type FusionConversationScope =
+ /** Canonical root conversation history. */
+ | "root"
+ /** Isolated read-only review history that does not enter the root conversation. */
+ | "review";
/**
* The agent mode that was active when this message was sent
*/
@@ -395,33 +460,16 @@ export type UserMessageDelivery =
/** Enqueued while the agent was busy; processed as its own run afterward. */
| "queued";
/**
- * Conversation scope in which a HydraFusion phase executes.
+ * Content-safe activity observed while a HydraFusion phase is running.
*/
/** @experimental */
-export type FusionConversationScope =
- /** Canonical root conversation history. */
- | "root"
- /** Isolated read-only review history that does not enter the root conversation. */
- | "review";
-/**
- * HydraFusion phase kind.
- */
-/** @experimental */
-export type FusionPhaseKind =
- /** Primary solver phase. */
- | "primary"
- /** Read-only cascade judge phase. */
- | "judge"
- /** Cascade repair phase. */
- | "repair"
- /** Initial critique-pattern draft phase. */
- | "draft"
- /** Read-only critique phase. */
- | "critic"
- /** Critique-pattern revision phase. */
- | "revision"
- /** Follow-up phase continuing from the resolved model. */
- | "follow_up";
+export type FusionPhaseActivityKind =
+ /** The provider produced additional private output bytes. */
+ | "model_output"
+ /** A tool began executing inside the phase. */
+ | "tool_started"
+ /** A tool finished executing inside the phase. */
+ | "tool_completed";
/**
* How a durable phase checkpoint contributes its exact message to canonical root history.
*/
@@ -469,6 +517,10 @@ export type CitationProvider =
*/
/** @experimental */
export type CitationLocation = CitationLocationChar | CitationLocationPage | CitationLocationBlock;
+/**
+ * Hosted program caller type
+ */
+export type AssistantMessageToolRequestCallerType = "program";
/**
* API endpoint used for this model call, matching CAPI supported_endpoints vocabulary
*/
@@ -933,7 +985,9 @@ export type ManagedSettingsResolvedSource =
| "device"
/** Only session-local SDK-host injection contributed. */
| "client"
- /** More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. */
+ /** A policy helper registered by device or server policy contributed. Device registration takes priority when present. */
+ | "policyHelper"
+ /** More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. */
| "mixed"
/** No managed policy is in force (no channel contributed). */
| "none";
@@ -984,7 +1038,7 @@ export type FactoryRunSettledStatus =
/** The run failed, with `failureType` carrying the class when it has one. */
| "error";
/**
- * Source location type (e.g., project, personal-copilot, plugin, builtin)
+ * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk)
*/
export type SkillSource =
/** Skill defined in the current project's skill directories. */
@@ -1000,7 +1054,17 @@ export type SkillSource =
/** Skill loaded from a configured custom skill directory. */
| "custom"
/** Skill bundled with the runtime. */
- | "builtin";
+ | "builtin"
+ /** Pathless skill supplied lazily by an SDK skill provider. */
+ | "sdk";
+/**
+ * Whether configured models are advisory preferences or required constraints
+ */
+export type AgentModelPolicy =
+ /** Treat the authored models as advisory preferences that callers may override. */
+ | "preferred"
+ /** Require subagent execution to use one of the authored models. */
+ | "required";
/**
* Configuration source: user, workspace, plugin, or builtin
*/
@@ -1106,6 +1170,7 @@ export interface StartData {
* Whether the session was already in use by another client at start time
*/
alreadyInUse?: boolean;
+ autoTier?: AutoTier;
context?: WorkingDirectoryContext;
/**
* Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model)
@@ -1258,6 +1323,7 @@ export interface ResumeData {
* Whether the session was already in use by another client at resume time
*/
alreadyInUse?: boolean;
+ autoTier?: AutoTier;
context?: WorkingDirectoryContext;
/**
* Context tier currently selected at resume time; null when no tier is active
@@ -1885,6 +1951,46 @@ export interface ModeChangedData {
newMode: SessionMode;
previousMode: SessionMode;
}
+/**
+ * Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume.
+ */
+export interface ModeNoticeDeliveredEvent {
+ /**
+ * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events.
+ */
+ agentId?: string;
+ data: ModeNoticeDeliveredData;
+ /**
+ * When true, the event is transient and not persisted to the session event log on disk
+ */
+ ephemeral?: boolean;
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * Type discriminator. Always "session.mode_notice_delivered".
+ */
+ type: "session.mode_notice_delivered";
+}
+/**
+ * Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume.
+ */
+export interface ModeNoticeDeliveredData {
+ /**
+ * Model-visible transition notice persisted for a mid-turn delivery
+ */
+ content?: string;
+ mode: SessionMode;
+}
/**
* Session event "session.session_limits_changed". Session limits update details. Null clears the limits.
*/
@@ -3009,6 +3115,97 @@ export interface TaskCompleteData {
*/
summary?: string;
}
+/**
+ * Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted.
+ */
+/** @experimental */
+export interface CompletionReceiptEvent {
+ /**
+ * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events.
+ */
+ agentId?: string;
+ data: CompletionReceiptData;
+ /**
+ * When true, the event is transient and not persisted to the session event log on disk
+ */
+ ephemeral?: boolean;
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * Type discriminator. Always "session.completion_receipt".
+ */
+ type: "session.completion_receipt";
+}
+/**
+ * Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted.
+ */
+/** @experimental */
+export interface CompletionReceiptData {
+ /**
+ * One-based accepted completion receipt ordinal in the durable session history.
+ */
+ attempt: number;
+ eventRange: CompletionReceiptEventRange;
+ /**
+ * Number of failed structured tool completions in the covered range.
+ */
+ failedToolCount: number;
+ finalTool?: CompletionReceiptFinalTool;
+ /**
+ * Version of the completion receipt payload.
+ */
+ schemaVersion: number;
+ /**
+ * Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId.
+ */
+ sourceEventId: string;
+ stopReason: CompletionReceiptStopReason;
+ /**
+ * Number of successful structured tool completions in the covered range.
+ */
+ successfulToolCount: number;
+}
+/**
+ * Inclusive durable event range summarized by a completion receipt.
+ */
+export interface CompletionReceiptEventRange {
+ /**
+ * Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key.
+ */
+ endEventId: string;
+ /**
+ * Identifier of the user message that starts the covered exchange.
+ */
+ startEventId: string;
+}
+/**
+ * Final structured tool completion in the covered event range.
+ */
+export interface CompletionReceiptFinalTool {
+ /**
+ * Process exit code from a structured shell result, when available.
+ */
+ exitCode?: number;
+ status: CompletionReceiptToolStatus;
+ /**
+ * Unique identifier of the completed tool call.
+ */
+ toolCallId: string;
+ /**
+ * Tool name from the matching tool execution start event, when available.
+ */
+ toolName?: string;
+}
/**
* Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn.
*/
@@ -3182,6 +3379,12 @@ export interface FusionResolvedData {
*/
modelUniverseVersion?: string;
pattern: FusionPattern;
+ /**
+ * Presentation-neutral phase plan for clients that render workflow progress.
+ *
+ * @experimental
+ */
+ phasePlan?: FusionPhasePlanStep[];
/**
* Version of the validated execution-plan format.
*/
@@ -3240,6 +3443,22 @@ export interface FusionFollowUpRecommendation {
compactionTurn: FusionFollowUpAction;
userTurn: FusionFollowUpAction;
}
+/**
+ * Presentation-neutral phase planned for a HydraFusion turn.
+ */
+/** @experimental */
+export interface FusionPhasePlanStep {
+ /**
+ * Whether the phase executes only when an earlier phase requests it.
+ */
+ conditional: boolean;
+ kind: FusionPhaseKind;
+ /**
+ * Semantic role assigned to the phase.
+ */
+ role: string;
+ scope: FusionConversationScope;
+}
/**
* Validated HydraFusion routing capability scores.
*/
@@ -3420,6 +3639,10 @@ export interface UserMessageData {
* True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry.
*/
isAutopilotContinuation?: boolean;
+ /**
+ * Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots
+ */
+ messageId?: string;
/**
* Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit
*/
@@ -4059,6 +4282,67 @@ export interface FusionPhaseStartedData {
*/
role: string;
}
+/**
+ * Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase.
+ */
+/** @experimental */
+export interface AssistantFusionPhaseActivityEvent {
+ /**
+ * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events.
+ */
+ agentId?: string;
+ data: FusionPhaseActivityData;
+ /**
+ * Always true for events that are transient and not persisted to the session event log on disk.
+ */
+ ephemeral: true;
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * Type discriminator. Always "assistant.fusion_phase_activity".
+ */
+ type: "assistant.fusion_phase_activity";
+}
+/**
+ * Experimental content-safe activity signal for a running HydraFusion phase.
+ */
+/** @experimental */
+export interface FusionPhaseActivityData {
+ activity: FusionPhaseActivityKind;
+ conversationScope: FusionConversationScope;
+ /**
+ * Identifier of the HydraFusion turn containing the phase.
+ */
+ fusionId: string;
+ pattern: FusionPattern;
+ /**
+ * Stable identifier for the concrete phase.
+ */
+ phaseId: string;
+ phaseKind: FusionPhaseKind;
+ /**
+ * Semantic role assigned to the phase.
+ */
+ role: string;
+ /**
+ * Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events.
+ */
+ toolCallId?: string;
+ /**
+ * Cumulative private response bytes observed for this model call. The event never includes response text.
+ */
+ totalResponseSizeBytes?: number;
+}
/**
* Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint.
*/
@@ -4799,7 +5083,7 @@ export interface FusionAttribution {
/** @experimental */
export interface AssistantMessageReasoningBlocks {
/**
- * Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest.
+ * Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering.
*/
blocks?: JsonValue[];
/**
@@ -4843,6 +5127,7 @@ export interface AssistantMessageToolRequest {
* Arguments to pass to the tool, format depends on the tool
*/
arguments?: JsonValue;
+ caller?: AssistantMessageToolRequestCaller;
/**
* Resolved intention summary describing what this specific call does
*/
@@ -4869,6 +5154,16 @@ export interface AssistantMessageToolRequest {
toolTitle?: string;
type?: AssistantMessageToolRequestType;
}
+/**
+ * Hosted program that requested this client tool call
+ */
+export interface AssistantMessageToolRequestCaller {
+ /**
+ * Provider-assigned identifier for the hosted caller.
+ */
+ callerId: string;
+ type: AssistantMessageToolRequestCallerType;
+}
/**
* Session event "assistant.message_start". Streaming assistant message start metadata
*/
@@ -6542,6 +6837,10 @@ export interface SkillInvokedData {
* Description of the skill from its SKILL.md frontmatter
*/
description?: string;
+ /**
+ * Whether model invocation is disabled for this skill
+ */
+ disableModelInvocation?: boolean;
/**
* Model identifier active when the skill was invoked, when known
*/
@@ -6551,7 +6850,7 @@ export interface SkillInvokedData {
*/
name: string;
/**
- * File path to the SKILL.md definition
+ * File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity
*/
path: string;
/**
@@ -6563,7 +6862,7 @@ export interface SkillInvokedData {
*/
pluginVersion?: string;
/**
- * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill)
+ * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill)
*/
source?: string;
trigger?: SkillInvokedTrigger;
@@ -6768,6 +7067,10 @@ export interface SubagentCompletedData {
* Model used by the sub-agent
*/
model?: string;
+ /**
+ * Why an explicit task-call model did not become the effective model
+ */
+ modelOverrideReason?: string;
/**
* Tool call ID of the parent tool invocation that spawned this sub-agent
*/
@@ -6855,6 +7158,10 @@ export interface SubagentFailedData {
* Model selected for the sub-agent, when known
*/
model?: string;
+ /**
+ * Why an explicit task-call model did not become the effective model
+ */
+ modelOverrideReason?: string;
/**
* Tool call ID of the parent tool invocation that spawned this sub-agent
*/
@@ -6992,7 +7299,7 @@ export interface HookStartData {
*/
hookType: string;
/**
- * Input data passed to the hook
+ * Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself.
*/
input?: JsonValue;
/**
@@ -7519,6 +7826,7 @@ export interface PermissionRequestedEvent {
* Permission request notification requiring client approval with request details
*/
export interface PermissionRequestedData {
+ agentMode?: SessionMode;
permissionRequest: PermissionRequest;
promptRequest?: PermissionPromptRequest;
/**
@@ -10041,7 +10349,7 @@ export interface AutoModeResolvedData {
stickyOverride?: boolean;
}
/**
- * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes.
+ * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes.
*/
/** @experimental */
export interface ManagedSettingsResolvedEvent {
@@ -10072,7 +10380,7 @@ export interface ManagedSettingsResolvedEvent {
type: "session.managed_settings_resolved";
}
/**
- * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes.
+ * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes.
*/
/** @experimental */
export interface ManagedSettingsResolvedData {
@@ -10100,6 +10408,10 @@ export interface ManagedSettingsResolvedData {
* Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`.
*/
permissionsAllowIntersected?: boolean;
+ /**
+ * Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one.
+ */
+ policyHelperManaged?: boolean;
/**
* Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy.
*/
@@ -10724,7 +11036,7 @@ export interface CustomAgentsUpdatedData {
warnings: string[];
}
/**
- * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override.
+ * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration.
*/
export interface CustomAgentsUpdatedAgent {
/**
@@ -10743,6 +11055,11 @@ export interface CustomAgentsUpdatedAgent {
* Model override for this agent, if set
*/
model?: string;
+ modelPolicy?: AgentModelPolicy;
+ /**
+ * Authored model ids in priority order, if configured
+ */
+ models?: string[];
/**
* Internal name of the agent
*/
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index 9d55ab1d10..bf6f2195f9 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -53,6 +53,7 @@ export {
// surface for those six identifiers is preserved unchanged.
export type * from "./generated/session-events.js";
export type {
+ AskUserVariant,
CommandContext,
CommandDefinition,
CommandHandler,
@@ -68,6 +69,7 @@ export type {
UserPromptTransformedHandler,
UserPromptTransformedHookInput,
UserPromptTransformedHookOutput,
+ CopilotClientInfo,
CopilotClientMode,
CopilotClientOptions,
CopilotExpAssignmentResponse,
@@ -119,6 +121,7 @@ export type {
ModelBilling,
ModelBillingTokenPrices,
ModelBillingTokenPricesLongContext,
+ AutoTier,
CapiSessionOptions,
ModelCapabilities,
ModelCapabilitiesOverride,
@@ -209,6 +212,8 @@ export type {
FactoryRunResult,
FactoryRunStatus,
FactoryRunSummary,
+ FactoryListRunsOptions,
+ FactoryRunsPage,
FactoryRunDetail,
FactoryProgressPage,
FactoryProgressLine,
diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts
new file mode 100644
index 0000000000..19d9926250
--- /dev/null
+++ b/nodejs/src/runtimeArtifacts.ts
@@ -0,0 +1,183 @@
+import { createHash } from "node:crypto";
+import {
+ chmodSync,
+ copyFileSync,
+ existsSync,
+ lstatSync,
+ mkdirSync,
+ mkdtempSync,
+ readdirSync,
+ renameSync,
+ rmSync,
+ statSync,
+} from "node:fs";
+import { homedir } from "node:os";
+import { dirname, join, relative, sep } from "node:path";
+
+export interface RuntimeArtifactSources {
+ packageRoot: string;
+ platform: string;
+}
+
+const EXCLUDED_TOP_LEVEL = new Set([
+ "app.js",
+ "assets",
+ "changelog.json",
+ "copilot",
+ "copilot.exe",
+ "copilot-sdk",
+ "foundry-local-sdk",
+ "index.js",
+ "LICENSE.md",
+ "napi-oop-runtime",
+ "npm-loader.js",
+ "package.json",
+ "preloads",
+ "pvrecorder",
+ "queries",
+ "README.md",
+ "sdk",
+ "sea-loader.js",
+ "webview",
+]);
+
+interface RuntimeAsset {
+ source: string;
+ relativePath: string;
+}
+
+function validateFile(path: string, label: string): void {
+ if (!existsSync(path)) {
+ throw new Error(`${label} not found at ${path}.`);
+ }
+ if (statSync(path).size === 0) {
+ throw new Error(`${label} at ${path} is empty.`);
+ }
+}
+
+function validateRuntimeBundle(wrapper: string, runtimeNode: string): void {
+ validateFile(wrapper, "Copilot runtime wrapper");
+ validateFile(runtimeNode, "Copilot runtime.node");
+}
+
+function isExcluded(relativePath: string): boolean {
+ const parts = relativePath.split(sep);
+ const topLevel = parts[0];
+ const fileName = parts.at(-1) ?? "";
+ return (
+ EXCLUDED_TOP_LEVEL.has(topLevel) ||
+ /^tree-sitter.*\.wasm$/.test(topLevel) ||
+ /^voice-.*\.js$/.test(topLevel) ||
+ fileName === "cli-native.node" ||
+ parts.includes("mediaremote-adapter") ||
+ fileName.startsWith("copilot-runtime-bin")
+ );
+}
+
+function collectRuntimeAssets(sources: RuntimeArtifactSources): RuntimeAsset[] {
+ const assets: RuntimeAsset[] = [];
+ const visit = (directory: string): void => {
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
+ const source = join(directory, entry.name);
+ const sourceRelative = relative(sources.packageRoot, source);
+ if (isExcluded(sourceRelative)) {
+ continue;
+ }
+ if (entry.isDirectory()) {
+ visit(source);
+ continue;
+ }
+ if (!entry.isFile() && !entry.isSymbolicLink()) {
+ continue;
+ }
+
+ const parts = sourceRelative.split(sep);
+ let relativePath = sourceRelative;
+ if (parts[0] === "prebuilds") {
+ if (parts[1] !== sources.platform || parts.length < 3) {
+ continue;
+ }
+ relativePath = parts.slice(2).join(sep);
+ }
+ assets.push({ source, relativePath });
+ }
+ };
+ visit(sources.packageRoot);
+ return assets.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
+}
+
+function sourceFingerprint(assets: RuntimeAsset[]): string {
+ const hash = createHash("sha256");
+ for (const asset of assets) {
+ const stat = lstatSync(asset.source);
+ hash.update(asset.relativePath).update("\0");
+ hash.update(`${stat.size}:${stat.mtimeMs}`).update("\0");
+ }
+ return hash.digest("hex").slice(0, 20);
+}
+
+function makeExecutable(path: string): void {
+ if (process.platform === "win32") {
+ return;
+ }
+ const mode = statSync(path).mode;
+ if ((mode & 0o111) === 0) {
+ chmodSync(path, mode | 0o111);
+ }
+}
+
+export function defaultRuntimeCacheRoot(
+ platform = process.platform,
+ home = homedir(),
+ environment: NodeJS.ProcessEnv = process.env
+): string {
+ const cacheDirectory =
+ platform === "win32"
+ ? (environment.LOCALAPPDATA ?? join(home, "AppData", "Local"))
+ : platform === "darwin"
+ ? join(home, "Library", "Caches")
+ : (environment.XDG_CACHE_HOME ?? join(home, ".cache"));
+ return join(cacheDirectory, "github-copilot-sdk", "runtime");
+}
+
+export function materializeRuntimeBundle(
+ sources: RuntimeArtifactSources,
+ cacheRoot = defaultRuntimeCacheRoot()
+): string {
+ const assets = collectRuntimeAssets(sources);
+ const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime";
+ const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperName)?.source;
+ const sourceRuntimeNode = assets.find((asset) => asset.relativePath === "runtime.node")?.source;
+ validateRuntimeBundle(sourceWrapper ?? "", sourceRuntimeNode ?? "");
+
+ const installDir = join(cacheRoot, `${sources.platform}-${sourceFingerprint(assets)}`);
+ const installedWrapper = join(installDir, wrapperName);
+ const installedRuntimeNode = join(installDir, "runtime.node");
+ if (existsSync(installDir)) {
+ validateRuntimeBundle(installedWrapper, installedRuntimeNode);
+ makeExecutable(installedWrapper);
+ return installedWrapper;
+ }
+
+ mkdirSync(cacheRoot, { recursive: true });
+ const stagingDir = mkdtempSync(join(cacheRoot, ".runtime-"));
+ try {
+ for (const asset of assets) {
+ const destination = join(stagingDir, asset.relativePath);
+ mkdirSync(dirname(destination), { recursive: true });
+ copyFileSync(asset.source, destination);
+ }
+ const stagedWrapper = join(stagingDir, wrapperName);
+ makeExecutable(stagedWrapper);
+ renameSync(stagingDir, installDir);
+ } catch (error) {
+ if (!existsSync(installDir)) {
+ throw error;
+ }
+ validateRuntimeBundle(installedWrapper, installedRuntimeNode);
+ } finally {
+ rmSync(stagingDir, { recursive: true, force: true });
+ }
+
+ return installedWrapper;
+}
diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts
index 65ff00921c..eb4c6b7561 100644
--- a/nodejs/src/session.ts
+++ b/nodejs/src/session.ts
@@ -71,6 +71,7 @@ import {
FactoryResumeError,
isFactoryRunTerminal,
type FactoryResumeErrorCode,
+ type FactoryListRunsOptions,
type FactoryRunResult,
type FactoryAgentOptions,
type RunOptions,
@@ -462,6 +463,8 @@ export class CopilotSession {
if (options?.resumeFromRunId !== undefined) {
return this.factory.resume(options.resumeFromRunId, {
limits: options.limits,
+ notifyOnComplete: options.notifyOnComplete,
+ logPhaseNames: options.logPhaseNames,
});
}
const envelope = await this.rpc.factory.run({
@@ -469,6 +472,8 @@ export class CopilotSession {
args: options?.args === undefined ? {} : options.args,
options: {
limits: options?.limits,
+ notifyOnComplete: options?.notifyOnComplete,
+ logPhaseNames: options?.logPhaseNames,
},
});
@@ -481,6 +486,8 @@ export class CopilotSession {
response = await this.rpc.factory.resume({
runId,
limits: options?.limits,
+ notifyOnComplete: options?.notifyOnComplete,
+ logPhaseNames: options?.logPhaseNames,
});
} catch (error) {
if (
@@ -499,7 +506,10 @@ export class CopilotSession {
}) as SessionFactoryApi["resume"],
getRun: async (runId) => this.rpc.factory.getRun({ runId }),
waitForRun: (runId, options) => this.waitForFactoryRun(runId, options?.signal),
- listRuns: async () => (await this.rpc.factory.listRuns({})).runs,
+ listRuns: (async (options?: FactoryListRunsOptions) => {
+ const page = await this.rpc.factory.listRuns(options ?? {});
+ return options === undefined ? page.runs : page;
+ }) as SessionFactoryApi["listRuns"],
getRunDetail: (runId) => this.rpc.factory.getRunDetail({ runId }),
getRunProgress: (runId, options = {}) =>
this.rpc.factory.getRunProgress({ runId, ...options }),
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 616e15a467..128ced3d68 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -11,6 +11,7 @@ import type { Canvas } from "./canvas.js";
import type { SessionFsProvider } from "./sessionFsProvider.js";
import type { CopilotRequestHandler } from "./copilotRequestHandler.js";
import type {
+ AutoTier,
PermissionRequest as GeneratedPermissionRequest,
PermissionRequestedData as GeneratedPermissionRequestedData,
PermissionRequestedEvent as GeneratedPermissionRequestedEvent,
@@ -72,7 +73,7 @@ export type {
export type SessionEvent =
| Exclude
| PermissionRequestedEvent;
-export type { ReasoningSummary } from "./generated/session-events.js";
+export type { AutoTier, ReasoningSummary } from "./generated/session-events.js";
export type { SessionFsProvider } from "./sessionFsProvider.js";
export { createSessionFsAdapter } from "./sessionFsProvider.js";
export type { SessionFsFileInfo } from "./sessionFsProvider.js";
@@ -306,6 +307,37 @@ export type InternalRuntimeConnection = RuntimeConnection | ParentProcessRuntime
*/
export type CopilotClientMode = "empty" | "copilot-cli";
+/**
+ * Identity of the integrating application, declared once on the `server.connect`
+ * handshake so the telemetry the runtime emits on this connection is attributed
+ * to a single, consistent surface rather than to the runtime's own build.
+ *
+ * All fields are optional; omit any of them (or the whole object) to keep the
+ * runtime's default attribution. Version fields are ignored by the runtime
+ * unless they look like a version string.
+ */
+export interface CopilotClientInfo {
+ /**
+ * Name of the application using the SDK, e.g. `"acme-developer-portal"`.
+ */
+ applicationName?: string;
+
+ /**
+ * Version of the application using the SDK, e.g. `"2.4.0"`.
+ */
+ applicationVersion?: string;
+
+ /**
+ * Optional name of a specific integration within the application, such as an extension or plugin.
+ */
+ integrationName?: string;
+
+ /**
+ * Optional version of the integration identified by `integrationName`.
+ */
+ integrationVersion?: string;
+}
+
export interface CopilotClientOptions {
/**
* How to connect to the Copilot runtime. When omitted, defaults to
@@ -477,6 +509,16 @@ export interface CopilotClientOptions {
*/
enableRemoteSessions?: boolean;
+ /**
+ * Identity of the integrating application, forwarded to the runtime on the
+ * `server.connect` handshake. Declaring it lets the telemetry the runtime
+ * emits on this connection be attributed to a single, consistent surface
+ * (e.g. the application and its Copilot integration) instead of the
+ * runtime's own build. All fields are optional; omit it to keep the default
+ * attribution.
+ */
+ clientInfo?: CopilotClientInfo;
+
/**
* @internal Hook used by `joinSession()` to construct a client that talks
* to its parent process over stdio. Not part of the public API.
@@ -1265,7 +1307,7 @@ export const defaultJoinSessionPermissionHandler: PermissionHandler =
// ============================================================================
/**
- * Request for user input from the agent (enables ask_user tool)
+ * Legacy question-and-answer request from the `ask_user` tool.
*/
export interface UserInputRequest {
/**
@@ -2129,6 +2171,17 @@ export interface FactoryMeta {
* provider-level choices are conceptually per-provider rather than global.
*/
export interface CapiSessionOptions {
+ /**
+ * Routing preference used when the session model is `auto`.
+ * Requires a runtime with Auto tier support and V2 Auto routing.
+ *
+ * When omitted on create, the runtime uses its default routing behavior.
+ * The runtime persists this preference across cold resume; an explicit tier
+ * on cold resume overrides the persisted value. For an already-resident
+ * session, omission preserves the current tier and a different tier is rejected.
+ */
+ autoTier?: AutoTier;
+
/**
* Whether to use the WebSocket transport for the CAPI Responses API.
*
@@ -2245,6 +2298,9 @@ export interface ManagedSettings {
permissions?: ManagedSettingsPermissions;
}
+/** Selects the model-facing shape of the built-in `ask_user` tool. */
+export type AskUserVariant = "legacy" | "elicitation";
+
/**
* Shared configuration fields used by both {@link SessionConfig} (for
* creating a new session) and {@link ResumeSessionConfig} (for resuming
@@ -2556,10 +2612,20 @@ export interface SessionConfigBase {
/**
* Handler for user input requests from the agent.
- * When provided, enables the ask_user tool allowing the agent to ask questions.
+ * When provided with the default `legacy` {@link AskUserVariant}, enables the
+ * question-and-answer form of the `ask_user` tool.
*/
onUserInputRequest?: UserInputHandler;
+ /**
+ * Selects the model-facing shape of the built-in `ask_user` tool.
+ *
+ * The default is `"legacy"`. To use `"elicitation"`, also provide
+ * {@link onElicitationRequest} so the host can answer structured forms.
+ * The runtime resolves this option when it creates or cold-resumes the session.
+ */
+ askUserVariant?: AskUserVariant;
+
/**
* Handler for elicitation requests from the agent.
* When provided, the server calls back to this client for form-based UI dialogs.
@@ -2873,6 +2939,12 @@ export interface SessionConfigBase {
*/
createSessionFsProvider?: (session: CopilotSession) => SessionFsProvider;
+ /**
+ * Feature-flag values resolved by the host for this session.
+ * Re-supply them when resuming after a runtime restart.
+ */
+ featureFlags?: Record;
+
/**
* ExP assignment ("flight") data injected by a trusted integrator, in the
* same JSON shape the Copilot CLI fetches from the experimentation service
diff --git a/nodejs/test/client-api-codegen.test.ts b/nodejs/test/client-api-codegen.test.ts
new file mode 100644
index 0000000000..9331ad7689
--- /dev/null
+++ b/nodejs/test/client-api-codegen.test.ts
@@ -0,0 +1,115 @@
+import { describe, expect, it } from "vitest";
+
+import { emitClientSessionApiRegistration as emitGoClientSessionApiRegistration } from "../../scripts/codegen/go.ts";
+import { emitClientSessionApiRegistration as emitPythonClientSessionApiRegistration } from "../../scripts/codegen/python.ts";
+import { emitClientSessionApiRegistration as emitTypeScriptClientSessionApiRegistration } from "../../scripts/codegen/typescript.ts";
+
+const clientSessionSchema: Record = {
+ mixed: {
+ visible: {
+ rpcMethod: "mixed.visible",
+ params: {
+ type: "object",
+ title: "VisibleRequest",
+ properties: {
+ sessionId: { type: "string" },
+ },
+ required: ["sessionId"],
+ },
+ result: {
+ type: "object",
+ title: "VisibleResult",
+ properties: {},
+ },
+ },
+ secret: {
+ rpcMethod: "mixed.secret",
+ visibility: "internal",
+ params: {
+ $ref: "#/definitions/InternalRequest",
+ },
+ result: {
+ $ref: "#/definitions/InternalResult",
+ },
+ },
+ },
+ internalOnly: {
+ hidden: {
+ rpcMethod: "internalOnly.hidden",
+ visibility: "internal",
+ params: {
+ $ref: "#/definitions/InternalRequest",
+ },
+ result: {
+ $ref: "#/definitions/InternalResult",
+ },
+ },
+ },
+};
+
+const allInternalClientSessionSchema: Record = {
+ internalOnly: clientSessionSchema.internalOnly,
+};
+
+function expectOnlyPublicClientSessionHandlers(code: string): void {
+ expect(code).toContain("mixed.visible");
+ expect(code).not.toContain("mixed.secret");
+ expect(code).not.toContain("internalOnly.hidden");
+ expect(code).not.toContain("InternalRequest");
+ expect(code).not.toContain("InternalResult");
+}
+
+describe("client-session API codegen", () => {
+ it("excludes internal methods from TypeScript handlers", () => {
+ const code = emitTypeScriptClientSessionApiRegistration(clientSessionSchema).join("\n");
+ const allInternalCode = emitTypeScriptClientSessionApiRegistration(
+ allInternalClientSessionSchema
+ ).join("\n");
+
+ expectOnlyPublicClientSessionHandlers(code);
+ expect(code).not.toContain("InternalOnlyHandler");
+ expect(allInternalCode).toContain("export interface ClientSessionApiHandlers {");
+ expect(allInternalCode).toContain("export function registerClientSessionApiHandlers(");
+ expect(allInternalCode).not.toContain("InternalOnlyHandler");
+ });
+
+ it("excludes internal methods from Go handlers", () => {
+ const lines: string[] = [];
+ emitGoClientSessionApiRegistration(lines, clientSessionSchema, (name) => name, new Map());
+ const code = lines.join("\n");
+ const allInternalLines: string[] = [];
+ emitGoClientSessionApiRegistration(
+ allInternalLines,
+ allInternalClientSessionSchema,
+ (name) => name,
+ new Map()
+ );
+ const allInternalCode = allInternalLines.join("\n");
+
+ expectOnlyPublicClientSessionHandlers(code);
+ expect(code).not.toContain("InternalOnlyHandler");
+ expect(allInternalCode).toContain("type ClientSessionAPIHandlers struct {");
+ expect(allInternalCode).toContain("func RegisterClientSessionAPIHandlers(");
+ expect(allInternalCode).not.toContain("InternalOnlyHandler");
+ expect(allInternalCode).not.toContain("clientSessionHandlerError");
+ });
+
+ it("excludes internal methods from Python handlers", () => {
+ const lines: string[] = [];
+ emitPythonClientSessionApiRegistration(lines, clientSessionSchema, (name) => name);
+ const code = lines.join("\n");
+ const allInternalLines: string[] = [];
+ emitPythonClientSessionApiRegistration(
+ allInternalLines,
+ allInternalClientSessionSchema,
+ (name) => name
+ );
+ const allInternalCode = allInternalLines.join("\n");
+
+ expectOnlyPublicClientSessionHandlers(code);
+ expect(code).not.toContain("InternalOnlyHandler");
+ expect(allInternalCode).toContain("class ClientSessionApiHandlers:");
+ expect(allInternalCode).toContain("def register_client_session_api_handlers(");
+ expect(allInternalCode).not.toContain("InternalOnlyHandler");
+ });
+});
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index 3ffda2fa71..bd1cf9812f 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -12,6 +12,8 @@ import {
createCanvas,
DisableBypassPermissionsModes,
RuntimeConnection,
+ type CapiSessionOptions,
+ type CopilotClientOptions,
type GitHubTelemetryNotification,
type ManagedSettings,
type ModelInfo,
@@ -60,6 +62,28 @@ describe("approveAll", () => {
});
describe("CopilotClient", () => {
+ it.each([
+ {
+ source: "connection path",
+ connection: RuntimeConnection.forStdio({ path: "/explicit/copilot" }),
+ env: {},
+ expected: "/explicit/copilot",
+ },
+ {
+ source: "COPILOT_CLI_PATH",
+ connection: RuntimeConnection.forStdio(),
+ env: { COPILOT_CLI_PATH: "/environment/copilot" },
+ expected: "/environment/copilot",
+ },
+ ])(
+ "preserves explicit child-process override from $source",
+ ({ connection, env, expected }) => {
+ const client = new CopilotClient({ connection, env });
+
+ expect((client as any).resolvedCliPath).toBe(expected);
+ }
+ );
+
async function startWithMockConnection(
builtinPluginDirectories?: readonly string[]
): Promise> {
@@ -303,6 +327,39 @@ describe("CopilotClient", () => {
});
});
+ it("forwards the ask-user variant on create and cold resume", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => stopClient(client));
+
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.create") return { sessionId: params.sessionId };
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ const onElicitationRequest = async () => ({ action: "decline" as const });
+
+ const session = await client.createSession({
+ askUserVariant: "elicitation",
+ onElicitationRequest,
+ });
+ await client.resumeSession(session.sessionId, {
+ askUserVariant: "elicitation",
+ onElicitationRequest,
+ });
+
+ expect(spy.mock.calls.find(([method]) => method === "session.create")![1]).toMatchObject({
+ askUserVariant: "elicitation",
+ requestElicitation: true,
+ });
+ expect(spy.mock.calls.find(([method]) => method === "session.resume")![1]).toMatchObject({
+ askUserVariant: "elicitation",
+ requestElicitation: true,
+ });
+ });
+
it("omits GitHub MCP tool config when unset", async () => {
const client = new CopilotClient();
await client.start();
@@ -1276,7 +1333,7 @@ describe("CopilotClient", () => {
expect(resumePayload.expAssignments).toBeUndefined();
});
- it("forwards capi options in session.create and session.resume", async () => {
+ it("forwards featureFlags in session.create and session.resume", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => stopClient(client));
@@ -1288,14 +1345,15 @@ describe("CopilotClient", () => {
if (method === "session.resume") return { sessionId: params.sessionId };
throw new Error(`Unexpected method: ${method}`);
});
+ const featureFlags = { ENABLED_TEST_FLAG: true, DISABLED_TEST_FLAG: false };
const session = await client.createSession({
onPermissionRequest: approveAll,
- capi: { enableWebSocketResponses: false },
+ featureFlags,
});
await client.resumeSession(session.sessionId, {
onPermissionRequest: approveAll,
- capi: { enableWebSocketResponses: false },
+ featureFlags,
});
const createPayload = spy.mock.calls.find(
@@ -1304,10 +1362,55 @@ describe("CopilotClient", () => {
const resumePayload = spy.mock.calls.find(
([method]) => method === "session.resume"
)![1] as any;
- expect(createPayload.capi).toEqual({ enableWebSocketResponses: false });
- expect(resumePayload.capi).toEqual({ enableWebSocketResponses: false });
+ expect(createPayload.featureFlags).toEqual(featureFlags);
+ expect(resumePayload.featureFlags).toEqual(featureFlags);
});
+ it.each([
+ undefined,
+ {},
+ { enableWebSocketResponses: false },
+ { enableWebSocketResponses: true },
+ { autoTier: "efficiency" },
+ { autoTier: "balance" },
+ { autoTier: "intelligence" },
+ { autoTier: "balance", enableWebSocketResponses: false },
+ ] satisfies (CapiSessionOptions | undefined)[])(
+ "forwards capi options %j in session.create and session.resume",
+ async (capi) => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => stopClient(client));
+
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.create") return { sessionId: params.sessionId };
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ model: "auto",
+ capi,
+ });
+ await client.resumeSession(session.sessionId, {
+ onPermissionRequest: approveAll,
+ capi,
+ });
+
+ const createPayload = spy.mock.calls.find(
+ ([method]) => method === "session.create"
+ )![1] as any;
+ const resumePayload = spy.mock.calls.find(
+ ([method]) => method === "session.resume"
+ )![1] as any;
+ expect(JSON.parse(JSON.stringify(createPayload)).capi).toEqual(capi);
+ expect(JSON.parse(JSON.stringify(resumePayload)).capi).toEqual(capi);
+ }
+ );
+
it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => {
const client = new CopilotClient();
await client.start();
@@ -4142,3 +4245,81 @@ describe("managedSettings serialization", () => {
});
});
});
+
+describe("connect handshake clientInfo", () => {
+ // Drives verifyProtocolVersion() against a stubbed connection so we can
+ // observe the `connect` params without spawning a runtime. `connect` maps to
+ // connection.sendRequest("connect", params) in the generated internal RPC.
+ async function captureConnectParams(
+ options: Partial> = {}
+ ): Promise> {
+ const client = new CopilotClient({
+ connection: RuntimeConnection.forUri("localhost:1234"),
+ ...options,
+ });
+ const sendRequest = vi.fn(async (method: string, _params?: unknown) => {
+ if (method === "connect") return { protocolVersion: 3 };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ (client as any).connection = { sendRequest };
+
+ await (client as any).verifyProtocolVersion();
+
+ const connectCall = sendRequest.mock.calls.find(([method]) => method === "connect");
+ expect(connectCall, "connect was not called").toBeTruthy();
+ return connectCall![1] as Record;
+ }
+
+ it("forwards a declared client identity on the connect handshake", async () => {
+ const clientInfo = {
+ applicationName: "acme-developer-portal",
+ applicationVersion: "2.4.0",
+ integrationName: "copilot-assistant",
+ integrationVersion: "1.5.0",
+ };
+
+ const params = await captureConnectParams({ clientInfo });
+
+ expect(params.clientInfo).toEqual({
+ editorName: "acme-developer-portal",
+ editorVersion: "2.4.0",
+ extensionName: "copilot-assistant",
+ extensionVersion: "1.5.0",
+ });
+ });
+
+ it("omits clientInfo from the handshake when the host declares none", async () => {
+ const params = await captureConnectParams();
+
+ expect(params).not.toHaveProperty("clientInfo");
+ });
+
+ it("drops empty fields and omits an all-empty identity", async () => {
+ const allEmpty = await captureConnectParams({
+ clientInfo: {
+ applicationName: "",
+ applicationVersion: "",
+ integrationName: "",
+ integrationVersion: "",
+ },
+ });
+ expect(allEmpty).not.toHaveProperty("clientInfo");
+
+ const partial = await captureConnectParams({
+ clientInfo: { applicationName: "example-app", applicationVersion: "" },
+ });
+ expect(partial.clientInfo).toEqual({ editorName: "example-app" });
+ });
+
+ it("keeps telemetry forwarding alongside a declared identity", async () => {
+ const params = await captureConnectParams({
+ clientInfo: { applicationName: "example-app" },
+ onGitHubTelemetry: () => {},
+ });
+
+ expect(params).toMatchObject({
+ clientInfo: { editorName: "example-app" },
+ enableGitHubTelemetryForwarding: true,
+ });
+ });
+});
diff --git a/nodejs/test/e2e/builtin_tools.e2e.test.ts b/nodejs/test/e2e/builtin_tools.e2e.test.ts
index 36b70ea195..39900bc7d6 100644
--- a/nodejs/test/e2e/builtin_tools.e2e.test.ts
+++ b/nodejs/test/e2e/builtin_tools.e2e.test.ts
@@ -130,6 +130,19 @@ describe("Built-in Tools", async () => {
async () => {
await writeFile(join(workDir, "data.txt"), "apple\nbanana\napricot\ncherry\n");
const session = await client.createSession({ onPermissionRequest: approveAll });
+ let grepToolCallId: string | undefined;
+ let grepCompletedSuccessfully = false;
+ session.on((event) => {
+ if (event.type === "tool.execution_start" && event.data.toolName === "grep") {
+ grepToolCallId = event.data.toolCallId;
+ } else if (
+ event.type === "tool.execution_complete" &&
+ event.data.toolCallId === grepToolCallId &&
+ event.data.success
+ ) {
+ grepCompletedSuccessfully = true;
+ }
+ });
const msg = await session.sendAndWait(
{
prompt: "Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.",
@@ -138,6 +151,7 @@ describe("Built-in Tools", async () => {
);
expect(msg?.data.content).toContain("apple");
expect(msg?.data.content).toContain("apricot");
+ expect(grepCompletedSuccessfully).toBe(true);
},
TEST_TIMEOUT_MS
);
diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts
index 35e7440766..bc3421bfa1 100644
--- a/nodejs/test/e2e/client.e2e.test.ts
+++ b/nodejs/test/e2e/client.e2e.test.ts
@@ -184,21 +184,15 @@ describe("Client", () => {
await client.stop();
});
- it("should report error with stderr when CLI fails to start", async () => {
+ it.skipIf(isInProcessTransport)("should report error when CLI fails to start", async () => {
const client = new CopilotClient({
- connection: RuntimeConnection.forStdio({ args: ["--nonexistent-flag-for-testing"] }),
+ connection: RuntimeConnection.forStdio({
+ args: ["--nonexistent-flag-for-testing"],
+ }),
});
onTestFinishedStop(client);
- let initialError: Error | undefined;
- try {
- await client.start();
- expect.fail("Expected start() to throw an error");
- } catch (error) {
- initialError = error as Error;
- expect(initialError.message).toContain("stderr");
- expect(initialError.message).toContain("nonexistent");
- }
+ await expect(client.start()).rejects.toBeInstanceOf(Error);
// Verify subsequent calls also fail (don't hang)
try {
@@ -206,7 +200,7 @@ describe("Client", () => {
await session.send("test");
expect.fail("Expected send() to throw an error after CLI exit");
} catch (error) {
- expect((error as Error).message).toContain("Connection is closed");
+ expect(error).toBeInstanceOf(Error);
}
});
});
diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts
index e3dc41343b..2f1dee6cb5 100644
--- a/nodejs/test/e2e/client_options.e2e.test.ts
+++ b/nodejs/test/e2e/client_options.e2e.test.ts
@@ -467,7 +467,7 @@ describe("Client options", async () => {
});
const session = await client.createSession({
clientName: "advanced-create-client",
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
reasoningEffort: "medium",
reasoningSummary: "detailed",
contextTier: "long_context",
@@ -532,7 +532,7 @@ describe("Client options", async () => {
provider: "create-provider",
id: "create-model",
name: "Create Model",
- modelId: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
wireModel: "create-wire-model",
maxContextWindowTokens: 12_000,
maxPromptTokens: 10_000,
@@ -544,7 +544,7 @@ describe("Client options", async () => {
const createRequest = getCapturedRequest(capturePath, "session.create");
expect(createRequest.clientName).toBe("advanced-create-client");
- expect(createRequest.model).toBe("claude-sonnet-4.5");
+ expect(createRequest.model).toBe("claude-sonnet-5");
expect(createRequest.reasoningEffort).toBe("medium");
expect(createRequest.reasoningSummary).toBe("detailed");
expect(createRequest.contextTier).toBe("long_context");
@@ -609,7 +609,7 @@ describe("Client options", async () => {
await client.start();
const session = await client.createSession({
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
provider: {
type: "azure",
wireApi: "responses",
@@ -619,7 +619,7 @@ describe("Client options", async () => {
bearerToken: "provider-bearer-token",
azure: { apiVersion: "2024-02-15-preview" },
headers: { "X-Provider-Wire": "yes" },
- modelId: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
wireModel: "azure-deployment",
maxPromptTokens: 8192,
maxOutputTokens: 1024,
@@ -636,7 +636,7 @@ describe("Client options", async () => {
expect(provider.bearerToken).toBe("provider-bearer-token");
expect(getObject(provider.azure).apiVersion).toBe("2024-02-15-preview");
expect(getObject(provider.headers)["X-Provider-Wire"]).toBe("yes");
- expect(provider.modelId).toBe("claude-sonnet-4.5");
+ expect(provider.modelId).toBe("claude-sonnet-5");
expect(provider.wireModel).toBe("azure-deployment");
expect(provider.maxPromptTokens).toBe(8192);
expect(provider.maxOutputTokens).toBe(1024);
diff --git a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts
index 69bacd4f6e..b0af6524a1 100644
--- a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts
+++ b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts
@@ -56,8 +56,8 @@ function serveNonInference(url: string): Response {
const MODEL_CATALOG_JSON = JSON.stringify({
data: [
{
- id: "claude-sonnet-4.5",
- name: "Claude Sonnet 4.5",
+ id: "claude-sonnet-5",
+ name: "Claude Sonnet 5",
object: "model",
vendor: "Anthropic",
version: "1",
@@ -65,7 +65,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({
model_picker_enabled: true,
capabilities: {
type: "chat",
- family: "claude-sonnet-4.5",
+ family: "claude-sonnet-5",
tokenizer: "o200k_base",
limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 },
supports: {
diff --git a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts
index 309250d852..04bccb7e08 100644
--- a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts
+++ b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts
@@ -42,8 +42,8 @@ async function startFakeUpstream(): Promise<{
sendJson(res, 200, {
data: [
{
- id: "claude-sonnet-4.5",
- name: "Claude Sonnet 4.5",
+ id: "claude-sonnet-5",
+ name: "Claude Sonnet 5",
object: "model",
vendor: "Anthropic",
version: "1",
@@ -52,7 +52,7 @@ async function startFakeUpstream(): Promise<{
supported_endpoints: ["/responses", "ws:/responses"],
capabilities: {
type: "chat",
- family: "claude-sonnet-4.5",
+ family: "claude-sonnet-5",
tokenizer: "o200k_base",
limits: {
max_context_window_tokens: 200000,
diff --git a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts
index bd070c20ca..9b0dbbb3bc 100644
--- a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts
+++ b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts
@@ -171,7 +171,7 @@ const CHAT_COMPLETION_STREAM_EVENTS: string[] = (() => {
id: "chatcmpl-stub-1",
object: "chat.completion.chunk",
created: 1,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
};
return [
`data: ${JSON.stringify({
@@ -210,7 +210,7 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({
id: "chatcmpl-stub-1",
object: "chat.completion",
created: 1,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
choices: [
{
index: 0,
@@ -224,8 +224,8 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({
const MODEL_CATALOG_JSON = JSON.stringify({
data: [
{
- id: "claude-sonnet-4.5",
- name: "Claude Sonnet 4.5",
+ id: "claude-sonnet-5",
+ name: "Claude Sonnet 5",
object: "model",
vendor: "Anthropic",
version: "1",
@@ -233,7 +233,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({
model_picker_enabled: true,
capabilities: {
type: "chat",
- family: "claude-sonnet-4.5",
+ family: "claude-sonnet-5",
tokenizer: "o200k_base",
limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 },
supports: {
@@ -300,14 +300,14 @@ describe("CopilotRequestHandler threads the runtime session id (CAPI + BYOK)", a
const session = await client.createSession({
onPermissionRequest: approveAll,
// BYOK providers require an explicit model id.
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
provider: {
type: "openai",
wireApi: "responses",
baseUrl: "https://byok.invalid/v1",
apiKey: "byok-secret",
- modelId: "claude-sonnet-4.5",
- wireModel: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
+ wireModel: "claude-sonnet-5",
},
});
const byokSessionId = session.sessionId;
diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts
index 611898f11d..8478f93288 100644
--- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts
+++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts
@@ -154,7 +154,7 @@ const CHAT_COMPLETION_STREAM = [
id: "persisted-session",
object: "chat.completion.chunk",
created: 1,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
choices: [
{
index: 0,
@@ -167,7 +167,7 @@ const CHAT_COMPLETION_STREAM = [
id: "persisted-session",
object: "chat.completion.chunk",
created: 1,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
},
]
@@ -179,7 +179,7 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({
id: "persisted-session",
object: "chat.completion",
created: 1,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
choices: [
{
index: 0,
@@ -193,8 +193,8 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({
const MODEL_CATALOG_JSON = JSON.stringify({
data: [
{
- id: "claude-sonnet-4.5",
- name: "Claude Sonnet 4.5",
+ id: "claude-sonnet-5",
+ name: "Claude Sonnet 5",
object: "model",
vendor: "Anthropic",
version: "1",
@@ -202,7 +202,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({
model_picker_enabled: true,
capabilities: {
type: "chat",
- family: "claude-sonnet-4.5",
+ family: "claude-sonnet-5",
tokenizer: "o200k_base",
limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 },
supports: { streaming: true, tool_calls: true, parallel_tool_calls: true },
diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts
index f034db9051..38210a22fb 100644
--- a/nodejs/test/e2e/extension_env_access.e2e.test.ts
+++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts
@@ -14,9 +14,9 @@ import {
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node.js";
-import { approveAll } from "../../src/index.js";
+import { approveAll, RuntimeConnection } from "../../src/index.js";
import { getSdkProtocolVersion } from "../../src/sdkProtocolVersion.js";
-import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js";
+import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js";
import { retry } from "./harness/sdkTestHelper.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -184,56 +184,47 @@ it("ignores a granted variable the extension never requested", async () => {
expect(run.postjoin).toBe("E2E_SDK_TOKEN=granted-token\nE2E_SDK_SMUGGLED=");
});
-const cliObservations = isInProcessTransport
- ? ""
- : mkdtempSync(join(tmpdir(), "copilot-env-access-cli-"));
+const cliObservations = mkdtempSync(join(tmpdir(), "copilot-env-access-cli-"));
const cliResultFile = join(cliObservations, "result");
-const cliContext = isInProcessTransport
- ? undefined
- : await createSdkTestContext({
- copilotClientOptions: {
- env: {
- COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS",
- EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN",
- EXTENSION_RESULT_FILE: cliResultFile,
- EXTENSION_PREJOIN_FILE: join(cliObservations, "prejoin"),
- EXTENSION_POSTJOIN_FILE: join(cliObservations, "postjoin"),
- },
- },
- });
+const cliContext = await createSdkTestContext({
+ copilotClientOptions: {
+ connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }),
+ env: {
+ COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS",
+ EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN",
+ EXTENSION_RESULT_FILE: cliResultFile,
+ EXTENSION_PREJOIN_FILE: join(cliObservations, "prejoin"),
+ EXTENSION_POSTJOIN_FILE: join(cliObservations, "postjoin"),
+ },
+ },
+});
// The released CLI ignores `requestedEnvironmentVariables`, so this covers the
// half a real CLI can prove today: asking for variables does not break the join.
// It becomes the grant test once `@github/copilot` carries the host half.
-it.skipIf(isInProcessTransport)(
- "joins a real CLI that does not support environment requests",
- async () => {
- if (!cliContext) {
- throw new Error("Extension E2E requires an out-of-process transport");
- }
- const { workDir, copilotClient } = cliContext;
- const extensionDir = join(workDir, ".github", "extensions", "env-access");
- await rm(join(workDir, ".github"), { recursive: true, force: true });
- await rm(cliResultFile, { force: true });
- await mkdir(extensionDir, { recursive: true });
- await copyFile(FIXTURE, join(extensionDir, "extension.mjs"));
- execFileSync("git", ["init", "--quiet"], { cwd: workDir });
-
- await using _session = await copilotClient.createSession({
- requestExtensions: true,
- extensionSdkPath: DIST_DIR,
- onPermissionRequest: approveAll,
- });
+it("joins a real CLI that does not support environment requests", async () => {
+ const { workDir, copilotClient } = cliContext;
+ const extensionDir = join(workDir, ".github", "extensions", "env-access");
+ await rm(join(workDir, ".github"), { recursive: true, force: true });
+ await rm(cliResultFile, { force: true });
+ await mkdir(extensionDir, { recursive: true });
+ await copyFile(FIXTURE, join(extensionDir, "extension.mjs"));
+ execFileSync("git", ["init", "--quiet"], { cwd: workDir });
+
+ await using _session = await copilotClient.createSession({
+ requestExtensions: true,
+ extensionSdkPath: DIST_DIR,
+ onPermissionRequest: approveAll,
+ });
- await retry(
- "wait for the env-access extension to join the session",
- async () => {
- expect(existsSync(cliResultFile)).toBe(true);
- },
- 300,
- 100
- );
+ await retry(
+ "wait for the env-access extension to join the session",
+ async () => {
+ expect(existsSync(cliResultFile)).toBe(true);
+ },
+ 300,
+ 100
+ );
- expect(readFileSync(cliResultFile, "utf-8")).toBe("joined");
- }
-);
+ expect(readFileSync(cliResultFile, "utf-8")).toBe("joined");
+});
diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts
index cddd8e47b0..2bf3ff17fb 100644
--- a/nodejs/test/e2e/factory.e2e.test.ts
+++ b/nodejs/test/e2e/factory.e2e.test.ts
@@ -4,30 +4,25 @@ import { copyFile, mkdir, rm } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it, vi } from "vitest";
-import { approveAll, FactoryResumeError } from "../../src/index.js";
+import { approveAll, FactoryResumeError, RuntimeConnection } from "../../src/index.js";
import {
createSdkTestContext,
DEFAULT_GITHUB_TOKEN,
- isInProcessTransport,
+ getLegacyCliPathForTests,
} from "./harness/sdkTestContext.js";
import { retry } from "./harness/sdkTestHelper.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
-const factoryTestContext = isInProcessTransport
- ? undefined
- : await createSdkTestContext({
- copilotClientOptions: {
- env: {
- COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES",
- },
- },
- });
+const factoryTestContext = await createSdkTestContext({
+ copilotClientOptions: {
+ connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }),
+ env: {
+ COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES",
+ },
+ },
+});
async function setupFactoryExtension(workDir: string, onPermissionRequest = approveAll) {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
-
const { copilotClient, openAiEndpoint } = factoryTestContext;
const extensionDir = join(workDir, ".github", "extensions", "factory-smoke");
const readyFile = join(extensionDir, "ready");
@@ -73,25 +68,20 @@ async function setupFactoryExtension(workDir: string, onPermissionRequest = appr
return session;
}
-it.skipIf(isInProcessTransport)(
- "runs an extension-authored factory across the SDK process boundary",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const result = await session.factory.run("argument-echo", {
- args: { source: "sdk-e2e", count: 11 },
- });
-
- expect(result).toMatchObject({
- status: "completed",
- result: { source: "sdk-e2e", count: 11 },
- });
- }
-);
+it("runs an extension-authored factory across the SDK process boundary", async () => {
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const result = await session.factory.run("argument-echo", {
+ args: { source: "sdk-e2e", count: 11 },
+ notifyOnComplete: false,
+ });
+
+ expect(result).toMatchObject({
+ status: "completed",
+ result: { source: "sdk-e2e", count: 11 },
+ });
+});
// TODO(cli-1.0.81-2): the subagent request is rejected downstream under CLI 1.0.81-2, so the
// fixture reports didThrow: true. Re-enable once the runtime fix ships.
@@ -113,199 +103,271 @@ it.skip("forwards every declared subagent option to the runtime", async () => {
});
}, 60_000);
-it.skipIf(isInProcessTransport)(
- "throws FactoryResumeError with not_found for an unknown run",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const error = await session.factory
- .resume("00000000-0000-0000-0000-000000000000")
- .catch((caught: unknown) => caught);
-
- expect(error).toBeInstanceOf(FactoryResumeError);
- expect((error as FactoryResumeError).code).toBe("not_found");
+it("throws FactoryResumeError with not_found for an unknown run", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "throws FactoryResumeError with non_resumable for a completed run",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const run = await session.factory.run("argument-echo");
- const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught);
-
- expect(error).toBeInstanceOf(FactoryResumeError);
- expect((error as FactoryResumeError).code).toBe("non_resumable");
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const error = await session.factory
+ .resume("00000000-0000-0000-0000-000000000000")
+ .catch((caught: unknown) => caught);
+
+ expect(error).toBeInstanceOf(FactoryResumeError);
+ expect((error as FactoryResumeError).code).toBe("not_found");
+});
+
+it("throws FactoryResumeError with non_resumable for a completed run", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "runs a factory when its session denies every permission request",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- const denyPermissions = vi.fn(() => ({ kind: "reject" as const }));
- await using session = await setupFactoryExtension(workDir, denyPermissions);
-
- await expect(session.factory.run("argument-echo")).resolves.toMatchObject({
- status: "completed",
- });
- expect(denyPermissions).not.toHaveBeenCalled();
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const run = await session.factory.run("argument-echo", { notifyOnComplete: false });
+ const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught);
+
+ expect(error).toBeInstanceOf(FactoryResumeError);
+ expect((error as FactoryResumeError).code).toBe("non_resumable");
+});
+
+it("forwards factory runtime controls across the SDK process boundary", async () => {
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const suppressed = await session.factory.run("phased", {
+ notifyOnComplete: false,
+ logPhaseNames: false,
+ });
+
+ expect(suppressed).toMatchObject({
+ status: "completed",
+ result: "finished",
+ });
+ const progress = await session.factory.getRunProgress(suppressed.runId);
+ expect(progress.records).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ kind: "phase", text: "Collect" }),
+ expect.objectContaining({ kind: "log", text: "Collected" }),
+ expect.objectContaining({ kind: "phase", text: "Summarize" }),
+ expect.objectContaining({ kind: "log", text: "Summarized" }),
+ ])
+ );
+
+ const events = await session.getEvents();
+ expect(
+ events.some(
+ (event) =>
+ event.type === "system.notification" &&
+ event.data.kind.type === "factory_completed" &&
+ event.data.kind.runId === suppressed.runId
+ )
+ ).toBe(false);
+ expect(
+ events.filter(
+ (event) => event.type === "session.info" && event.data.infoType === "factory_phase"
+ )
+ ).toEqual([]);
+});
+
+it("pages factory runs and returns cursor metadata", async () => {
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const first = await session.factory.run("argument-echo", {
+ args: { ordinal: 1 },
+ notifyOnComplete: false,
+ });
+ const second = await session.factory.run("argument-echo", {
+ args: { ordinal: 2 },
+ notifyOnComplete: false,
+ });
+ const third = await session.factory.run("argument-echo", {
+ args: { ordinal: 3 },
+ notifyOnComplete: false,
+ });
+
+ const newest = await session.factory.listRuns({ limit: 1 });
+ expect(newest).toMatchObject({
+ runs: [expect.objectContaining({ runId: third.runId })],
+ hasMoreNewer: false,
+ omittedOlder: 2,
+ });
+ expect(newest.oldestSeq).toBe(newest.newestSeq);
+ expect(newest.oldestSeq).not.toBeNull();
+
+ const older = await session.factory.listRuns({
+ beforeSeq: newest.oldestSeq!,
+ limit: 1,
+ });
+ expect(older).toMatchObject({
+ runs: [expect.objectContaining({ runId: second.runId })],
+ hasMoreNewer: true,
+ omittedOlder: 1,
+ });
+
+ const oldest = await session.factory.listRuns({
+ beforeSeq: older.oldestSeq!,
+ limit: 1,
+ });
+ expect(oldest).toMatchObject({
+ runs: [expect.objectContaining({ runId: first.runId })],
+ hasMoreNewer: true,
+ omittedOlder: 0,
+ });
+});
+
+it("runs a factory when its session denies every permission request", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "resumes a failed factory when its session denies every permission request",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- const denyPermissions = vi.fn(() => ({ kind: "reject" as const }));
- await using session = await setupFactoryExtension(workDir, denyPermissions);
-
- const failedRun = await session.factory.run("fails-once");
- expect(failedRun).toMatchObject({
- status: "error",
- });
-
- await expect(session.factory.resume(failedRun.runId)).resolves.toMatchObject({
- status: "completed",
- result: "resumed",
- });
- expect(denyPermissions).not.toHaveBeenCalled();
+ const { workDir } = factoryTestContext;
+ const denyPermissions = vi.fn(() => ({ kind: "reject" as const }));
+ await using session = await setupFactoryExtension(workDir, denyPermissions);
+
+ await expect(
+ session.factory.run("argument-echo", { notifyOnComplete: false })
+ ).resolves.toMatchObject({
+ status: "completed",
+ });
+ expect(denyPermissions).not.toHaveBeenCalled();
+});
+
+it("resumes a failed factory when its session denies every permission request", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "refuses a factory started through the context session from a factory body",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const result = await session.factory.run("starts-from-context-session");
-
- expect(result).toMatchObject({
- status: "completed",
- result: expect.stringContaining("factory.run and factory.resume"),
- });
- expect((result as { result: string }).result).toContain("factory body");
+ const { workDir } = factoryTestContext;
+ const denyPermissions = vi.fn(() => ({ kind: "reject" as const }));
+ await using session = await setupFactoryExtension(workDir, denyPermissions);
+
+ const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false });
+ expect(failedRun).toMatchObject({
+ status: "error",
+ });
+
+ await expect(
+ session.factory.resume(failedRun.runId, {
+ notifyOnComplete: false,
+ logPhaseNames: false,
+ })
+ ).resolves.toMatchObject({
+ status: "completed",
+ result: "resumed",
+ });
+ expect(denyPermissions).not.toHaveBeenCalled();
+});
+
+it("refuses a factory started through the context session from a factory body", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "refuses a factory started through the module session from a factory body",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const result = await session.factory.run("starts-from-module-session");
-
- expect(result).toMatchObject({
- status: "completed",
- result: expect.stringContaining("factory.run and factory.resume"),
- });
- expect((result as { result: string }).result).toContain("factory body");
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const result = await session.factory.run("starts-from-context-session", {
+ notifyOnComplete: false,
+ });
+
+ expect(result).toMatchObject({
+ status: "completed",
+ result: expect.stringContaining("factory.run and factory.resume"),
+ });
+ expect((result as { result: string }).result).toContain("factory body");
+});
+
+it("refuses a factory started through the module session from a factory body", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "allows a module-level extension watcher to start a factory while another body is parked",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- const extensionDir = join(workDir, ".github", "extensions", "factory-smoke");
- await using session = await setupFactoryExtension(workDir);
-
- const parked = session.factory.run("parked");
- await retry(
- "wait for the parked factory to enter its body",
- async () => {
- expect(existsSync(join(extensionDir, "entered"))).toBe(true);
- },
- 100,
- 100
- );
-
- writeFileSync(join(extensionDir, "start-b"), "start");
- const bResultFile = join(extensionDir, "b-result");
- await retry(
- "wait for the module-level watcher factory run to succeed",
- async () => {
- expect(existsSync(bResultFile)).toBe(true);
- expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({
- status: "success",
- result: {
- status: "completed",
- result: { source: "module-watcher" },
- },
- });
- },
- 100,
- 100
- );
-
- writeFileSync(join(extensionDir, "release"), "release");
- await expect(parked).resolves.toMatchObject({
- status: "completed",
- result: "released",
- });
- },
- 60_000
-);
-
-it.skipIf(isInProcessTransport)(
- "returns an array result from an extension-authored factory",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const result = await session.factory.run("array-result");
-
- expect(result).toMatchObject({
- status: "completed",
- result: [1, "two", false],
- });
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const result = await session.factory.run("starts-from-module-session", {
+ notifyOnComplete: false,
+ });
+
+ expect(result).toMatchObject({
+ status: "completed",
+ result: expect.stringContaining("factory.run and factory.resume"),
+ });
+ expect((result as { result: string }).result).toContain("factory body");
+});
+
+it("allows a module-level extension watcher to start a factory while another body is parked", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
-
-it.skipIf(isInProcessTransport)(
- "passes array factory arguments across the SDK process boundary",
- async () => {
- if (!factoryTestContext) {
- throw new Error("Factory E2E requires the stdio transport");
- }
- const { workDir } = factoryTestContext;
- await using session = await setupFactoryExtension(workDir);
-
- const args = [1, "two", false];
- const result = await session.factory.run("argument-echo", { args });
-
- expect(result).toMatchObject({
- status: "completed",
- result: args,
- });
+ const { workDir } = factoryTestContext;
+ const extensionDir = join(workDir, ".github", "extensions", "factory-smoke");
+ await using session = await setupFactoryExtension(workDir);
+
+ const parked = session.factory.run("parked", { notifyOnComplete: false });
+ await retry(
+ "wait for the parked factory to enter its body",
+ async () => {
+ expect(existsSync(join(extensionDir, "entered"))).toBe(true);
+ },
+ 100,
+ 100
+ );
+
+ writeFileSync(join(extensionDir, "start-b"), "start");
+ const bResultFile = join(extensionDir, "b-result");
+ await retry(
+ "wait for the module-level watcher factory run to succeed",
+ async () => {
+ expect(existsSync(bResultFile)).toBe(true);
+ expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({
+ status: "success",
+ result: {
+ status: "completed",
+ result: { source: "module-watcher" },
+ },
+ });
+ },
+ 100,
+ 100
+ );
+
+ writeFileSync(join(extensionDir, "release"), "release");
+ await expect(parked).resolves.toMatchObject({
+ status: "completed",
+ result: "released",
+ });
+}, 60_000);
+
+it("returns an array result from an extension-authored factory", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
}
-);
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const result = await session.factory.run("array-result", { notifyOnComplete: false });
+
+ expect(result).toMatchObject({
+ status: "completed",
+ result: [1, "two", false],
+ });
+});
+
+it("passes array factory arguments across the SDK process boundary", async () => {
+ if (!factoryTestContext) {
+ throw new Error("Factory E2E requires the stdio transport");
+ }
+ const { workDir } = factoryTestContext;
+ await using session = await setupFactoryExtension(workDir);
+
+ const args = [1, "two", false];
+ const result = await session.factory.run("argument-echo", {
+ args,
+ notifyOnComplete: false,
+ });
+
+ expect(result).toMatchObject({
+ status: "completed",
+ result: args,
+ });
+});
diff --git a/nodejs/test/e2e/fixtures/factory-extension.mjs b/nodejs/test/e2e/fixtures/factory-extension.mjs
index 45227a1bea..fb344b4863 100644
--- a/nodejs/test/e2e/fixtures/factory-extension.mjs
+++ b/nodejs/test/e2e/fixtures/factory-extension.mjs
@@ -40,6 +40,21 @@ const arrayResult = defineFactory({
run: async () => [1, "two", false],
});
+const phased = defineFactory({
+ meta: {
+ name: "phased",
+ description: "Record named phases and ordinary progress.",
+ phases: [{ title: "Collect" }, { title: "Summarize" }],
+ },
+ run: async ({ phase, log }) => {
+ phase("Collect");
+ log("Collected");
+ phase("Summarize");
+ log("Summarized");
+ return "finished";
+ },
+});
+
const forwardsSubagentOptions = defineFactory({
meta: {
name: "forwards-subagent-options",
@@ -141,6 +156,7 @@ session = await joinSession({
factories: [
argumentEcho,
arrayResult,
+ phased,
forwardsSubagentOptions,
startsFromContextSession,
startsFromModuleSession,
@@ -153,6 +169,7 @@ void waitForMarker("start-b", 30_000)
.then(async () => {
const result = await session.factory.run("argument-echo", {
args: { source: "module-watcher" },
+ notifyOnComplete: false,
});
writeFileSync(marker("b-result"), JSON.stringify({ status: "success", result }));
})
diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts
index bf62db4826..58c275c800 100644
--- a/nodejs/test/e2e/harness/sdkTestContext.ts
+++ b/nodejs/test/e2e/harness/sdkTestContext.ts
@@ -50,6 +50,29 @@ function getCliPathForTests(): string | undefined {
return undefined;
}
+function getCliPlatformPackageNames(): string[] {
+ const variants =
+ process.platform === "linux"
+ ? process.report?.getReport().header.glibcVersionRuntime
+ ? ["linux", "linuxmusl"]
+ : ["linuxmusl", "linux"]
+ : [process.platform];
+ return variants.map((variant) => `@github/copilot-${variant}-${process.arch}`);
+}
+
+/** Resolves the legacy SEA only for tests that explicitly exercise Node-hosted features. */
+export function getLegacyCliPathForTests(): string {
+ const cliName = process.platform === "win32" ? "copilot.exe" : "copilot";
+ const githubModules = resolve(__dirname, "../../../node_modules/@github");
+ for (const packageName of getCliPlatformPackageNames()) {
+ const cliPath = join(githubModules, packageName.slice("@github/".length), cliName);
+ if (fs.existsSync(cliPath)) {
+ return cliPath;
+ }
+ }
+ throw new Error("Legacy Copilot CLI binary not found in the installed platform package.");
+}
+
export async function createSdkTestContext({
logLevel,
useStdio,
@@ -286,8 +309,13 @@ export async function createSdkTestContext({
process.chdir(restoreCwd);
restoreCwd = undefined;
}
- // Empty directories but leave them in place for next test
- await rimraf([join(homeDir, "*"), join(workDir, "*")], { glob: true });
+ // The in-process runtime retains open state files until afterAll shuts it down.
+ // Keep its isolated home intact while it is alive; removing open files on POSIX
+ // can leave later tests using unlinked database state.
+ const cleanupPaths = isInProcess
+ ? [join(workDir, "*")]
+ : [join(homeDir, "*"), join(workDir, "*")];
+ await rimraf(cleanupPaths, { glob: true });
});
afterAll(async () => {
diff --git a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts
index af879ea77b..e3b5f75ee4 100644
--- a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts
+++ b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts
@@ -11,9 +11,8 @@ describe("In-process FFI transport", () => {
// exercised by the full E2E suite running under the `inprocess` CI matrix cell,
// not a dedicated test.
it("should start and connect over in-process FFI", async () => {
- // In-process FFI hosting resolves the CLI entrypoint (COPILOT_CLI_PATH or the
- // bundled platform package) and its sibling native runtime library itself. If
- // neither is available, start() throws and the test fails hard.
+ // In-process FFI hosting loads runtime.node directly from the bundled runtime.
+ // If it is unavailable, start() throws and the test fails hard.
const client = new CopilotClient({ connection: RuntimeConnection.forInProcess() });
await client.start();
diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts
index 85abc3a900..7c2906c7b9 100644
--- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts
+++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts
@@ -13,6 +13,7 @@ import type {
PermissionRequestResult,
} from "../../src/index.js";
import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js";
+import { waitForCondition } from "./harness/sdkTestHelper.js";
const PENDING_WORK_TIMEOUT_MS = 60_000;
const TEST_TIMEOUT_MS = 180_000;
@@ -516,7 +517,46 @@ describe("Pending work resume", async () => {
).toBe("beta");
if (scenario.disconnectOriginalClient) {
- await suspendedClient.forceStop();
+ const lockObserver = new CopilotClient({
+ workingDirectory: workDir,
+ env,
+ gitHubToken: DEFAULT_GITHUB_TOKEN,
+ connection: RuntimeConnection.forStdio({
+ path: process.env.COPILOT_CLI_PATH,
+ }),
+ });
+ try {
+ await lockObserver.start();
+ await waitForCondition(
+ async () => {
+ const result = await lockObserver.rpc.sessions.checkInUse({
+ sessionIds: [sessionId],
+ });
+ return result.inUse.includes(sessionId);
+ },
+ {
+ timeoutMs: PENDING_WORK_TIMEOUT_MS,
+ timeoutMessage: `Timed out waiting for session '${sessionId}' to acquire its lock.`,
+ }
+ );
+
+ await suspendedClient.forceStop();
+
+ await waitForCondition(
+ async () => {
+ const result = await lockObserver.rpc.sessions.checkInUse({
+ sessionIds: [sessionId],
+ });
+ return !result.inUse.includes(sessionId);
+ },
+ {
+ timeoutMs: PENDING_WORK_TIMEOUT_MS,
+ timeoutMessage: `Timed out waiting for session '${sessionId}' to release its lock.`,
+ }
+ );
+ } finally {
+ await lockObserver.forceStop();
+ }
}
const resumedClient = createConnectingClient(cliUrl);
diff --git a/nodejs/test/e2e/rewind.e2e.test.ts b/nodejs/test/e2e/rewind.e2e.test.ts
index 49c2b3b8f0..7fdfee94c8 100644
--- a/nodejs/test/e2e/rewind.e2e.test.ts
+++ b/nodejs/test/e2e/rewind.e2e.test.ts
@@ -2,13 +2,15 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
-import { existsSync, readFileSync } from "node:fs";
+import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
const FILE_NAME = "rewind-sdk.txt";
+const ORIGINAL_FILE_CONTENT = "Original rewind content";
+const PREPARED_FILE_CONTENT = "Prepared rewind content";
const FILE_CONTENT = "SDK rewind content";
function expectSamePath(actual: string, expected: string): void {
@@ -24,66 +26,73 @@ function expectSamePath(actual: string, expected: string): void {
describe("Rewind", async () => {
const { copilotClient: client, workDir } = await createSdkTestContext();
- // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes.
- it.skipIf(process.platform === "win32")(
- "should restore tracked file and conversation",
- async () => {
- const filePath = join(workDir, FILE_NAME);
- const session = await client.createSession({
- model: "claude-sonnet-4.5",
- enableFileChangeTracking: true,
- onPermissionRequest: approveAll,
+ it("should restore tracked file and conversation", async () => {
+ const filePath = join(workDir, FILE_NAME);
+ writeFileSync(filePath, ORIGINAL_FILE_CONTENT);
+ const session = await client.createSession({
+ model: "claude-sonnet-5",
+ enableFileChangeTracking: true,
+ onPermissionRequest: approveAll,
+ });
+
+ try {
+ const ready = await session.sendAndWait({
+ prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${ORIGINAL_FILE_CONTENT} to ${PREPARED_FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_READY.`,
});
+ expect(ready?.data.content).toBe("SDK_REWIND_READY");
+ expect(readFileSync(filePath, "utf8")).toBe(PREPARED_FILE_CONTENT);
- try {
- const response = await session.sendAndWait({
- prompt: `Use the create tool to create ${FILE_NAME} containing exactly ${FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_DONE.`,
- });
+ const response = await session.sendAndWait({
+ prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${PREPARED_FILE_CONTENT} to ${FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_DONE.`,
+ });
- expect(response?.data.content).toBe("SDK_REWIND_DONE");
- expect(existsSync(filePath)).toBe(true);
- expect(readFileSync(filePath, "utf8")).toBe(FILE_CONTENT);
+ expect(response?.data.content).toBe("SDK_REWIND_DONE");
+ expect(existsSync(filePath)).toBe(true);
+ expect(readFileSync(filePath, "utf8")).toBe(FILE_CONTENT);
- let rewindPoints = await session.rpc.history.listRewindPoints();
- const deadline = Date.now() + 30_000;
- while (
- Date.now() < deadline &&
- (rewindPoints.unavailableReason !== undefined ||
- !rewindPoints.points[0]?.canRestoreFiles)
- ) {
- await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
- rewindPoints = await session.rpc.history.listRewindPoints();
- }
+ let rewindPoints = await session.rpc.history.listRewindPoints();
+ const deadline = Date.now() + 30_000;
+ while (
+ Date.now() < deadline &&
+ (rewindPoints.unavailableReason !== undefined ||
+ rewindPoints.points.length !== 2 ||
+ !rewindPoints.points[1]?.turnChangedFiles ||
+ !rewindPoints.points[1]?.canRestoreFiles)
+ ) {
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
+ rewindPoints = await session.rpc.history.listRewindPoints();
+ }
- expect(rewindPoints.unavailableReason).toBeUndefined();
- expect(rewindPoints.fileChangeTrackingEnabled).toBe(true);
- expect(rewindPoints.points).toHaveLength(1);
- const rewindPoint = rewindPoints.points[0];
- expect(rewindPoint.canRestoreFiles).toBe(true);
- expect(rewindPoint.fileCount).toBe(1);
+ expect(rewindPoints.unavailableReason).toBeUndefined();
+ expect(rewindPoints.fileChangeTrackingEnabled).toBe(true);
+ expect(rewindPoints.points).toHaveLength(2);
+ const rewindPoint = rewindPoints.points[1];
+ expect(rewindPoint.turnChangedFiles).toBe(true);
+ expect(rewindPoint.canRestoreFiles).toBe(true);
+ expect(rewindPoint.fileCount).toBe(1);
- const preview = await session.rpc.history.previewRewind({
- eventId: rewindPoint.eventId,
- });
- expect(preview.available).toBe(true);
- expect(preview.files).toHaveLength(1);
- expectSamePath(preview.files[0].path, filePath);
+ const preview = await session.rpc.history.previewRewind({
+ eventId: rewindPoint.eventId,
+ });
+ expect(preview.available).toBe(true);
+ expect(preview.files).toHaveLength(1);
+ expectSamePath(preview.files[0].path, filePath);
- const rewind = await session.rpc.history.rewind({
- eventId: rewindPoint.eventId,
- mode: "conversation-and-files",
- });
- expect(rewind.outcome).toBe("success");
- expect(rewind.eventsRemoved).toBeGreaterThan(0);
- expect(rewind.restoredFiles).toHaveLength(1);
- expectSamePath(rewind.restoredFiles[0], filePath);
- expect(existsSync(filePath)).toBe(false);
+ const rewind = await session.rpc.history.rewind({
+ eventId: rewindPoint.eventId,
+ mode: "conversation-and-files",
+ });
+ expect(rewind.outcome).toBe("success");
+ expect(rewind.eventsRemoved).toBeGreaterThan(0);
+ expect(rewind.restoredFiles).toHaveLength(1);
+ expectSamePath(rewind.restoredFiles[0], filePath);
+ expect(existsSync(filePath)).toBe(true);
+ expect(readFileSync(filePath, "utf8")).toBe(PREPARED_FILE_CONTENT);
- const events = await session.getEvents();
- expect(events.some((event) => event.id === rewindPoint.eventId)).toBe(false);
- } finally {
- await session.disconnect();
- }
+ const events = await session.getEvents();
+ expect(events.some((event) => event.id === rewindPoint.eventId)).toBe(false);
+ } finally {
+ await session.disconnect();
}
- );
+ });
});
diff --git a/nodejs/test/e2e/rpc.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts
index f90547da9b..4e93fdbe44 100644
--- a/nodejs/test/e2e/rpc.e2e.test.ts
+++ b/nodejs/test/e2e/rpc.e2e.test.ts
@@ -73,7 +73,7 @@ describe("Session RPC", async () => {
it.skip("should call session.rpc.model.getCurrent", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
});
const result = await session.rpc.model.getCurrent();
@@ -85,7 +85,7 @@ describe("Session RPC", async () => {
it.skip("should call session.rpc.model.switchTo", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
});
// Get initial model
diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts
index 5075ae68d9..13a63875e9 100644
--- a/nodejs/test/e2e/rpc_server.e2e.test.ts
+++ b/nodejs/test/e2e/rpc_server.e2e.test.ts
@@ -144,7 +144,7 @@ describe("Server-scoped RPC", async () => {
const result = await authClient.listModels();
expect(Array.isArray(result)).toBe(true);
- expect(result.some((m) => m.id === "claude-sonnet-4.5")).toBe(true);
+ expect(result.some((m) => m.id === "claude-sonnet-5")).toBe(true);
for (const model of result) {
expect(model.name).toBeTruthy();
}
diff --git a/nodejs/test/e2e/rpc_session_state.e2e.test.ts b/nodejs/test/e2e/rpc_session_state.e2e.test.ts
index 5164f99232..aab08b3bc5 100644
--- a/nodejs/test/e2e/rpc_session_state.e2e.test.ts
+++ b/nodejs/test/e2e/rpc_session_state.e2e.test.ts
@@ -40,7 +40,7 @@ describe("Session-scoped RPC", async () => {
it("should call session rpc model getcurrent", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
});
const result = await session.rpc.model.getCurrent();
@@ -65,7 +65,7 @@ describe("Session-scoped RPC", async () => {
it("should call session rpc model switchto", async () => {
const session = await switchClient.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
});
const before = await session.rpc.model.getCurrent();
@@ -315,14 +315,14 @@ describe("Session-scoped RPC", async () => {
const branch = `rpc-context-${randomUUID()}`;
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
workingDirectory: firstDirectory,
});
try {
const initialSnapshot = await session.rpc.metadata.snapshot();
expect(initialSnapshot.sessionId).toBe(session.sessionId);
expect(initialSnapshot.currentMode).toBe("interactive");
- expect(initialSnapshot.selectedModel).toBe("claude-sonnet-4.5");
+ expect(initialSnapshot.selectedModel).toBe("claude-sonnet-5");
expect(initialSnapshot.isRemote).toBe(false);
expect(initialSnapshot.alreadyInUse).toBe(false);
expect(Date.parse(initialSnapshot.startTime)).not.toBeNaN();
@@ -446,7 +446,7 @@ describe("Session-scoped RPC", async () => {
it("should set reasoning effort and auto name", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
});
try {
const reasoning = await session.rpc.model.setReasoningEffort({
@@ -455,7 +455,7 @@ describe("Session-scoped RPC", async () => {
expect(reasoning.reasoningEffort).toBe("high");
const currentModel = await session.rpc.model.getCurrent();
- expect(currentModel.modelId).toBe("claude-sonnet-4.5");
+ expect(currentModel.modelId).toBe("claude-sonnet-5");
expect(currentModel.reasoningEffort).toBe("high");
const autoName = `Auto Session ${randomUUID()}`;
@@ -734,11 +734,11 @@ describe("Session-scoped RPC", async () => {
const contextInfo = await session.rpc.metadata.contextInfo({
promptTokenLimit: 128_000,
outputTokenLimit: 4_096,
- selectedModel: "claude-sonnet-4.5",
+ selectedModel: "claude-sonnet-5",
});
expect(contextInfo.contextInfo).not.toBeNull();
if (contextInfo.contextInfo) {
- expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-4.5");
+ expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-5");
expect(contextInfo.contextInfo.promptTokenLimit).toBe(128_000);
expect(contextInfo.contextInfo.limit).toBeGreaterThanOrEqual(
contextInfo.contextInfo.promptTokenLimit
@@ -755,7 +755,7 @@ describe("Session-scoped RPC", async () => {
}
const recomputed = await session.rpc.metadata.recomputeContextTokens({
- modelId: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
});
expect(recomputed.systemTokenCount).toBeGreaterThan(0);
expect(recomputed.messagesTokenCount).toBeGreaterThan(0);
diff --git a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts
index 6111809914..7b88af7e2d 100644
--- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts
+++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts
@@ -70,7 +70,7 @@ describe("Session-scoped state extras RPC", async () => {
try {
await authClient.start();
session = await authClient.createSession({
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
onPermissionRequest: approveAll,
});
@@ -79,7 +79,7 @@ describe("Session-scoped state extras RPC", async () => {
expect(Array.isArray(result.list)).toBe(true);
expect(result.list.length).toBeGreaterThan(0);
expect(
- result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-4.5"))
+ result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-5"))
).toBe(true);
} finally {
await disconnect(session);
@@ -126,7 +126,7 @@ describe("Session-scoped state extras RPC", async () => {
provider: providerName,
id: modelId,
name: "SDK Runtime Model",
- modelId: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
wireModel: "wire-sdk-runtime-model",
maxContextWindowTokens: 4096,
maxPromptTokens: 3072,
diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts
index b89221998a..bab0efc687 100644
--- a/nodejs/test/e2e/session.e2e.test.ts
+++ b/nodejs/test/e2e/session.e2e.test.ts
@@ -98,7 +98,7 @@ describe("Sessions", () => {
it("should create and disconnect sessions", async () => {
await using session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
});
expect(session.sessionId).toMatch(/^[a-f0-9-]+$/);
@@ -107,7 +107,7 @@ describe("Sessions", () => {
expect(sessionStartEvents).toMatchObject([
{
type: "session.start",
- data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-4.5" },
+ data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-5" },
},
]);
diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts
index 85137e0ff9..8d041f1ec8 100644
--- a/nodejs/test/e2e/session_config.e2e.test.ts
+++ b/nodejs/test/e2e/session_config.e2e.test.ts
@@ -119,6 +119,7 @@ describe("Session Configuration", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
+ model: "claude-sonnet-5",
modelCapabilities: { supports: { vision: false } },
});
@@ -129,7 +130,7 @@ describe("Session Configuration", async () => {
expect(hasImageUrlContent(t1Messages)).toBe(false);
// Switch vision on (re-specify same model with updated capabilities)
- await session.setModel("claude-sonnet-4.5", {
+ await session.setModel("claude-sonnet-5", {
modelCapabilities: { supports: { vision: true } },
});
@@ -149,6 +150,7 @@ describe("Session Configuration", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
+ model: "claude-sonnet-5",
modelCapabilities: { supports: { vision: true } },
});
@@ -159,7 +161,7 @@ describe("Session Configuration", async () => {
expect(hasImageUrlContent(t1Messages)).toBe(true);
// Switch vision off
- await session.setModel("claude-sonnet-4.5", {
+ await session.setModel("claude-sonnet-5", {
modelCapabilities: { supports: { vision: false } },
});
@@ -342,7 +344,7 @@ describe("Session Configuration", async () => {
id: "msg_stub_1",
type: "message",
role: "assistant",
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
content: [],
stop_reason: null,
stop_sequence: null,
@@ -384,8 +386,8 @@ describe("Session Configuration", async () => {
return json({
data: [
{
- id: "claude-sonnet-4.5",
- name: "Claude Sonnet 4.5",
+ id: "claude-sonnet-5",
+ name: "Claude Sonnet 5",
object: "model",
vendor: "Anthropic",
version: "1",
@@ -393,7 +395,7 @@ describe("Session Configuration", async () => {
model_picker_enabled: true,
capabilities: {
type: "chat",
- family: "claude-sonnet-4.5",
+ family: "claude-sonnet-5",
tokenizer: "o200k_base",
limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 },
supports: {
@@ -423,7 +425,7 @@ describe("Session Configuration", async () => {
id: "msg_stub_1",
type: "message",
role: "assistant",
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
content: [{ type: "text", text: "OK from the synthetic stream." }],
stop_reason: "end_turn",
stop_sequence: null,
@@ -434,7 +436,7 @@ describe("Session Configuration", async () => {
id: "chatcmpl-stub-1",
object: "chat.completion",
created: 1,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
choices: [
{
index: 0,
@@ -462,8 +464,8 @@ describe("Session Configuration", async () => {
type: "anthropic" as const,
baseUrl: "https://anthropic-citations.invalid/v1",
apiKey: "test-provider-key",
- modelId: "claude-sonnet-4.5",
- wireModel: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
+ wireModel: "claude-sonnet-5",
};
}
@@ -563,7 +565,7 @@ describe("Session Configuration", async () => {
try {
const session = await citationClient.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
enableCitations: true,
provider: createAnthropicProvider(),
});
@@ -607,7 +609,7 @@ describe("Session Configuration", async () => {
try {
const session2 = await resumeClient.resumeSession(session1.sessionId, {
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
enableCitations: true,
provider: createAnthropicProvider(),
});
@@ -711,7 +713,7 @@ describe("Session Configuration", async () => {
it("should forward custom provider headers on create", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
provider: createProxyProvider("create-provider-header"),
});
@@ -734,7 +736,7 @@ describe("Session Configuration", async () => {
const session2 = await client.resumeSession(sessionId, {
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
provider: createProxyProvider("resume-provider-header"),
});
@@ -762,7 +764,7 @@ describe("Session Configuration", async () => {
// tests for serialization coverage).
const session = await client.createSession({
onPermissionRequest: approveAll,
- model: "claude-sonnet-4.5",
+ model: "claude-sonnet-5",
provider: {
type: "openai",
baseUrl: openAiEndpoint.url,
@@ -791,7 +793,7 @@ describe("Session Configuration", async () => {
type: "openai",
baseUrl: openAiEndpoint.url,
apiKey: "test-provider-key",
- modelId: "claude-sonnet-4.5",
+ modelId: "claude-sonnet-5",
},
});
@@ -799,7 +801,7 @@ describe("Session Configuration", async () => {
const exchanges = await openAiEndpoint.getExchanges();
expect(exchanges.length).toBe(1);
- expect(exchanges[0].request.model).toBe("claude-sonnet-4.5");
+ expect(exchanges[0].request.model).toBe("claude-sonnet-5");
await session.disconnect();
});
diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts
index 2e85dd5af2..6366db36cb 100644
--- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts
+++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts
@@ -5,7 +5,11 @@
import { afterAll, describe, expect, it } from "vitest";
import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js";
import type { SessionEvent } from "../../src/index.js";
-import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js";
+import {
+ createSdkTestContext,
+ getLegacyCliPathForTests,
+ isInProcessTransport,
+} from "./harness/sdkTestContext.js";
describe("UI Elicitation", async () => {
const { copilotClient: client } = await createSdkTestContext();
@@ -38,6 +42,36 @@ describe("UI Elicitation Callback", async () => {
}
);
+ // In-process sessions do not expose current tool metadata for introspection.
+ it.skipIf(isInProcessTransport)(
+ "session created with the elicitation ask-user variant exposes the structured tool",
+ { timeout: 60_000 },
+ async () => {
+ const legacyClient = ctx.createClient({
+ connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }),
+ });
+ try {
+ const session = await legacyClient.createSession({
+ onPermissionRequest: approveAll,
+ askUserVariant: "elicitation",
+ onElicitationRequest: async () => ({ action: "accept", content: {} }),
+ });
+
+ await session.rpc.tools.initializeAndValidate();
+ const { tools } = await session.rpc.tools.getCurrentMetadata();
+ const askUserSchema = tools?.find((tool) => tool.name === "ask_user")
+ ?.input_schema as { properties?: Record } | undefined;
+
+ expect(askUserSchema?.properties).toHaveProperty("message");
+ expect(askUserSchema?.properties).toHaveProperty("requestedSchema");
+ expect(askUserSchema?.properties).not.toHaveProperty("question");
+ await session.disconnect();
+ } finally {
+ await legacyClient.stop();
+ }
+ }
+ );
+
it(
"session created without onElicitationRequest reports no elicitation capability",
{ timeout: 60_000 },
diff --git a/nodejs/test/factory.test.ts b/nodejs/test/factory.test.ts
index dcf434616a..c9b8f65074 100644
--- a/nodejs/test/factory.test.ts
+++ b/nodejs/test/factory.test.ts
@@ -435,6 +435,7 @@ describe("factories", () => {
const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8");
const publicApi = readFileSync(new URL("../src/factory.ts", import.meta.url), "utf8");
const listRunsPagingWording = "newest default page of this session's durable factory runs";
+ const listRunsMetadata = ["oldestSeq", "newestSeq", "hasMoreNewer", "omittedOlder"];
const resumeCodes = [
"not_found",
"non_resumable",
@@ -458,6 +459,9 @@ describe("factories", () => {
for (const document of [normalizedGuide, normalizedPublicApi]) {
expect(document).toContain(listRunsPagingWording);
+ for (const field of listRunsMetadata) {
+ expect(document).toContain(field);
+ }
}
expect(normalizedGuide).toContain(
@@ -1500,14 +1504,31 @@ describe("factories", () => {
revision: 4,
};
const detail = { ...summary, phases: [], agents: [], progress };
+ const runsPage = {
+ runs: [summary],
+ oldestSeq: 11,
+ newestSeq: 12,
+ hasMoreNewer: true,
+ omittedOlder: 10,
+ };
const sendRequest = vi.fn(async (method: string) => {
- if (method === "session.factory.listRuns") return { runs: [summary] };
+ if (method === "session.factory.listRuns") return runsPage;
if (method === "session.factory.getRunDetail") return detail;
return progress;
});
const session = new CopilotSession("session-observe", { sendRequest } as never);
await expect(session.factory.listRuns()).resolves.toEqual([summary]);
+ const listedPage = await session.factory.listRuns({
+ afterSeq: 10,
+ beforeSeq: 20,
+ limit: 50,
+ });
+ expect(listedPage).toEqual(runsPage);
+ expect(listedPage.oldestSeq).toBe(11);
+ expect(listedPage.newestSeq).toBe(12);
+ expect(listedPage.hasMoreNewer).toBe(true);
+ expect(listedPage.omittedOlder).toBe(10);
await expect(session.factory.getRunDetail("run-observe")).resolves.toEqual(detail);
await expect(
session.factory.getRunProgress("run-observe", {
@@ -1519,11 +1540,17 @@ describe("factories", () => {
expect(sendRequest).toHaveBeenNthCalledWith(1, "session.factory.listRuns", {
sessionId: session.sessionId,
});
- expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.getRunDetail", {
+ expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.listRuns", {
+ sessionId: session.sessionId,
+ afterSeq: 10,
+ beforeSeq: 20,
+ limit: 50,
+ });
+ expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunDetail", {
sessionId: session.sessionId,
runId: "run-observe",
});
- expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunProgress", {
+ expect(sendRequest).toHaveBeenNthCalledWith(4, "session.factory.getRunProgress", {
sessionId: session.sessionId,
runId: "run-observe",
phaseId: "p0",
@@ -2132,6 +2159,8 @@ describe("factories", () => {
await expect(
session.factory.resume("run-prior", {
limits: { maxTotalSubagents: 7 },
+ notifyOnComplete: true,
+ logPhaseNames: true,
})
).resolves.toMatchObject({
status: "completed",
@@ -2141,13 +2170,20 @@ describe("factories", () => {
session.factory.run("by-name", {
args: { value: 1 },
limits: { maxTotalSubagents: 7 },
+ notifyOnComplete: false,
+ logPhaseNames: true,
resumeFromRunId: "run-prior",
})
).resolves.toMatchObject({
status: "completed",
result: { name: "stored-name", persistedArgs: true },
});
- await expect(session.factory.run(factory)).resolves.toMatchObject({
+ await expect(
+ session.factory.run(factory, {
+ notifyOnComplete: true,
+ logPhaseNames: false,
+ })
+ ).resolves.toMatchObject({
status: "completed",
result: { name: "friendly-run" },
});
@@ -2155,17 +2191,25 @@ describe("factories", () => {
sessionId: session.sessionId,
runId: "run-prior",
limits: { maxTotalSubagents: 7 },
+ notifyOnComplete: true,
+ logPhaseNames: true,
});
expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.resume", {
sessionId: session.sessionId,
runId: "run-prior",
limits: { maxTotalSubagents: 7 },
+ notifyOnComplete: false,
+ logPhaseNames: true,
});
expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.run", {
sessionId: session.sessionId,
name: "friendly-run",
args: {},
- options: { limits: undefined },
+ options: {
+ limits: undefined,
+ notifyOnComplete: true,
+ logPhaseNames: false,
+ },
});
});
diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts
new file mode 100644
index 0000000000..4a58789e6e
--- /dev/null
+++ b/nodejs/test/runtimeArtifacts.test.ts
@@ -0,0 +1,109 @@
+import { existsSync, mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join } from "node:path";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { defaultRuntimeCacheRoot, materializeRuntimeBundle } from "../src/runtimeArtifacts.js";
+
+describe("defaultRuntimeCacheRoot", () => {
+ it.each([
+ [
+ "darwin",
+ "/home/test",
+ {},
+ join("/home/test", "Library", "Caches", "github-copilot-sdk", "runtime"),
+ ],
+ ["linux", "/home/test", {}, join("/home/test", ".cache", "github-copilot-sdk", "runtime")],
+ [
+ "linux",
+ "/home/test",
+ { XDG_CACHE_HOME: "/cache" },
+ join("/cache", "github-copilot-sdk", "runtime"),
+ ],
+ [
+ "win32",
+ "C:\\Users\\test",
+ { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" },
+ join("C:\\Users\\test\\AppData\\Local", "github-copilot-sdk", "runtime"),
+ ],
+ ])("uses the %s user cache directory", (platform, home, environment, expected) => {
+ expect(defaultRuntimeCacheRoot(platform, home, environment)).toBe(expected);
+ });
+});
+
+describe("materializeRuntimeBundle", () => {
+ afterEach(() => vi.unstubAllEnvs());
+
+ it("materializes an adjacent pair from an absent cache with a stripped environment", () => {
+ const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-source-"));
+ const cacheRoot = join(sourceDir, "absent-cache");
+ const emptyPath = join(sourceDir, "empty-path");
+ mkdirSync(emptyPath);
+ const wrapperName =
+ process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime";
+ const prebuilds = join(sourceDir, "prebuilds", "test-platform");
+ const wrapper = join(prebuilds, wrapperName);
+ const runtimeNode = join(prebuilds, "runtime.node");
+ mkdirSync(prebuilds, { recursive: true });
+ writeFileSync(wrapper, "wrapper");
+ writeFileSync(runtimeNode, "runtime");
+ mkdirSync(join(sourceDir, "ripgrep", "bin", "test-platform"), { recursive: true });
+ writeFileSync(join(sourceDir, "ripgrep", "bin", "test-platform", "rg"), "ripgrep");
+ mkdirSync(join(sourceDir, "definitions"), { recursive: true });
+ writeFileSync(join(sourceDir, "definitions", "future.json"), "{}");
+ writeFileSync(join(sourceDir, "app.js"), "excluded");
+ writeFileSync(join(sourceDir, "copilot"), "excluded");
+ writeFileSync(join(sourceDir, "copilot.exe"), "excluded");
+ writeFileSync(join(sourceDir, "LICENSE.md"), "excluded");
+ writeFileSync(join(sourceDir, "README.md"), "excluded");
+
+ vi.stubEnv("PATH", emptyPath);
+ vi.stubEnv("COPILOT_CLI_PATH", undefined);
+ vi.stubEnv("COPILOT_RUNTIME_HOST_COMMAND", undefined);
+ vi.stubEnv("COPILOT_RUNTIME_PROVIDER_LIB", undefined);
+
+ expect(process.env.COPILOT_CLI_PATH).toBeUndefined();
+ expect(process.env.COPILOT_RUNTIME_HOST_COMMAND).toBeUndefined();
+ expect(process.env.COPILOT_RUNTIME_PROVIDER_LIB).toBeUndefined();
+
+ const installedWrapper = materializeRuntimeBundle(
+ { packageRoot: sourceDir, platform: "test-platform" },
+ cacheRoot
+ );
+ const installDir = dirname(installedWrapper);
+
+ expect(readFileSync(installedWrapper, "utf8")).toBe("wrapper");
+ expect(readFileSync(join(installDir, "runtime.node"), "utf8")).toBe("runtime");
+ expect(
+ readFileSync(join(installDir, "ripgrep", "bin", "test-platform", "rg"), "utf8")
+ ).toBe("ripgrep");
+ expect(existsSync(join(installDir, "app.js"))).toBe(false);
+ expect(existsSync(join(installDir, "copilot"))).toBe(false);
+ expect(existsSync(join(installDir, "copilot.exe"))).toBe(false);
+ expect(existsSync(join(installDir, "LICENSE.md"))).toBe(false);
+ expect(existsSync(join(installDir, "README.md"))).toBe(false);
+ if (process.platform !== "win32") {
+ expect(statSync(installedWrapper).mode & 0o111).not.toBe(0);
+ }
+ });
+
+ it("fails clearly when the package has no runtime.node", () => {
+ const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-missing-node-"));
+ const wrapperName =
+ process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime";
+ const prebuilds = join(sourceDir, "prebuilds", "test-platform");
+ const wrapper = join(prebuilds, wrapperName);
+ mkdirSync(prebuilds, { recursive: true });
+ writeFileSync(wrapper, "wrapper");
+
+ expect(() =>
+ materializeRuntimeBundle(
+ {
+ packageRoot: sourceDir,
+ platform: "test-platform",
+ },
+ join(sourceDir, "cache")
+ )
+ ).toThrow(/Copilot runtime\.node not found/);
+ });
+});
diff --git a/nodejs/test/session-event-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts
index c84d9bc082..2dad8f4f18 100644
--- a/nodejs/test/session-event-codegen.test.ts
+++ b/nodejs/test/session-event-codegen.test.ts
@@ -3,10 +3,37 @@ import { describe, expect, it } from "vitest";
import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts";
import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts";
-import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts";
+import {
+ generatePythonSessionEventsCode,
+ postProcessExternalRefsForPython,
+} from "../../scripts/codegen/python.ts";
import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts";
describe("session event codegen", () => {
+ it("replaces external reference placeholders regardless of acronym casing", () => {
+ const code = `@dataclass
+class ExternalRefMCPOauthHTTPResponse:
+ external_ref_marker_external_ref_mcp_oauth_http_response: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'ExternalRefMCPOauthHTTPResponse':
+ value = obj.get("__externalRefMarker___ExternalRef_McpOauthHttpResponse")
+ return ExternalRefMCPOauthHTTPResponse(value)
+
+@dataclass
+class ProbeResult:
+ response: ExternalRefMCPOauthHTTPResponse
+`;
+
+ const processed = postProcessExternalRefsForPython(
+ code,
+ new Map([["__ExternalRef_McpOauthHttpResponse", "McpOauthHttpResponse"]])
+ );
+
+ expect(processed).toContain("response: McpOauthHttpResponse");
+ expect(processed).not.toContain("class ExternalRefMCPOauthHTTPResponse");
+ });
+
it("maps special schema formats to the expected Python types", () => {
const schema: JSONSchema7 = {
definitions: {
diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts
index 5c41f2216a..93edebfc80 100644
--- a/nodejs/test/session-event-types.test.ts
+++ b/nodejs/test/session-event-types.test.ts
@@ -21,6 +21,8 @@ import type { FactoryAgentOptions as WireFactoryAgentOptions } from "../src/gene
import type {
// The aggregate union; must still resolve via the package root.
SessionEvent,
+ AutoTier,
+ CapiSessionOptions,
PermissionRequest,
PermissionRequestedData,
PermissionRequestedEvent,
@@ -128,6 +130,32 @@ type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual<
const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true;
describe("Session event type exports (#1156)", () => {
+ it.each(["efficiency", "balance", "intelligence", undefined] satisfies (
+ | AutoTier
+ | undefined
+ )[])("exposes Auto tier %s on start and resume data", (autoTier) => {
+ const start: StartData = {
+ copilotVersion: "1.0.82-1",
+ producer: "copilot-agent",
+ sessionId: "session-1",
+ startTime: "2026-08-28T00:00:00Z",
+ version: 1,
+ autoTier,
+ };
+ const resume: ResumeData = {
+ eventCount: 1,
+ resumeTime: "2026-08-28T00:01:00Z",
+ autoTier,
+ };
+ const capi: CapiSessionOptions = { autoTier: start.autoTier };
+ expect(capi.autoTier).toBe(autoTier);
+ expect(resume.autoTier).toBe(autoTier);
+ if (autoTier === undefined) {
+ expect(JSON.parse(JSON.stringify(start))).not.toHaveProperty("autoTier");
+ expect(JSON.parse(JSON.stringify(resume))).not.toHaveProperty("autoTier");
+ }
+ });
+
it("exposes the headline ToolExecutionStartData type with a usable shape", () => {
// This is the specific type called out in issue #1156. The annotation
// is the compile-time API-surface check; these assertions only validate
diff --git a/python/README.md b/python/README.md
index 61608c16a0..359026df41 100644
--- a/python/README.md
+++ b/python/README.md
@@ -29,8 +29,9 @@ runtime:
python -m copilot download-runtime
```
-This caches the runtime binary locally. If you skip this step, the SDK will
-attempt to download it automatically on first use as a fallback.
+This caches `copilot-runtime`, its adjacent `runtime.node`, and the compatible
+`copilot` host locally. If you skip this step, the SDK downloads the bundle
+automatically on first managed stdio/TCP use.
To pre-provision the native library required by the in-process (FFI) transport
(see [In-process (FFI) transport](#in-process-ffi-transport)), pass `--in-process`:
@@ -39,15 +40,15 @@ To pre-provision the native library required by the in-process (FFI) transport
python -m copilot download-runtime --in-process
```
-This additionally fetches the native runtime library into the versioned runtime
-cache. Stdio/TCP users never download it. When omitted, it is downloaded
-lazily on first use of the in-process transport.
+This instead provisions the compatible CLI artifact and native runtime library
+used by in-process hosting. When omitted, they are downloaded lazily on first
+use of the in-process transport.
| Platform | Cache path |
|----------|-----------|
-| Linux | `~/.cache/github-copilot-sdk/cli//copilot` |
-| macOS | `~/Library/Caches/github-copilot-sdk/cli//copilot` |
-| Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\copilot.exe` |
+| Linux | `~/.cache/github-copilot-sdk/cli//prebuilds//` |
+| macOS | `~/Library/Caches/github-copilot-sdk/cli//prebuilds//` |
+| Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\prebuilds\\` |
### Environment variables
@@ -56,7 +57,8 @@ lazily on first use of the in-process transport.
| `COPILOT_CLI_PATH` | Use this specific binary instead of downloading |
| `COPILOT_CLI_EXTRACT_DIR` | Override the cache directory (binary placed directly here) |
| `COPILOT_SKIP_CLI_DOWNLOAD` | Set to `1` to disable auto-download |
-| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL |
+| `COPILOT_NPM_REGISTRY_URL` | Override the npm registry used for managed out-of-process and in-process runtime downloads |
+| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL used for the root CLI |
## Run the Sample
@@ -223,6 +225,10 @@ All options are kw-only parameters:
- `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`).
- `RuntimeConnection.for_inprocess()` — host the runtime in-process via its native C ABI (FFI). See [In-process (FFI) transport](#in-process-ffi-transport).
+Managed stdio and TCP connections use the downloaded `copilot-runtime`
+executable with adjacent `runtime.node` by default. An explicit connection
+path or `COPILOT_CLI_PATH` overrides the downloaded runtime.
+
Child-process connections (`for_stdio`/`for_tcp`) also expose a per-connection
`env` field for the spawned process. Set it on the returned connection instead of
the client-level `env` — setting both raises:
@@ -272,6 +278,7 @@ finally:
These are passed as keyword arguments to `create_session()`:
- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.**
+- `capi` (CapiSessionOptions): Copilot API options. With `model="auto"`, set `auto_tier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics.
- `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `list_models()` to check which models support this option.
- `session_id` (str): Custom session ID
- `tools` (list): Custom tools exposed to the CLI. Tools with `handler=None` are declaration-only and must be resolved via pending tool-call RPCs.
@@ -283,7 +290,8 @@ These are passed as keyword arguments to `create_session()`:
- `enable_session_store` (bool): Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled.
- `github_token_provider` (callable): Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `github_token`.
- `on_permission_request` (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.approve_all` approves requests when managed settings are disabled and raises an error when `enable_managed_settings` is true. Custom handlers can inspect `managed_approval_required` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
-- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
+- `on_user_input_request` (callable): Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section.
+- `ask_user_variant` (`"legacy"` | `"elicitation"`): Selects the model-facing shape of the `ask_user` tool. Defaults to `"legacy"`; use `"elicitation"` with `on_elicitation_request`. Re-supply this option when cold-resuming a session.
- `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
```python
@@ -899,7 +907,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `skip_p
## User Input Requests
-Enable the agent to ask questions to the user using the `ask_user` tool by providing an `on_user_input_request` handler:
+Enable the legacy question-and-answer `ask_user` tool by providing an `on_user_input_request` handler:
```python
async def handle_user_input(request, invocation):
diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py
index 608dacf253..5d9e3f9500 100644
--- a/python/copilot/__init__.py
+++ b/python/copilot/__init__.py
@@ -29,8 +29,11 @@
OpenCanvasInstance,
)
from .client import (
+ AskUserVariant,
+ AutoTier,
CapiSessionOptions,
ChildProcessRuntimeConnection,
+ ClientInfo,
CloudSessionOptions,
CloudSessionRepository,
CopilotClient,
@@ -229,6 +232,8 @@
"AutoModeSwitchHandler",
"AutoModeSwitchRequest",
"AutoModeSwitchResponse",
+ "AskUserVariant",
+ "AutoTier",
"BUILTIN_TOOLS_ISOLATED",
"CanvasAction",
"CanvasDeclaration",
@@ -240,6 +245,7 @@
"CanvasProviderIdentity",
"CapiSessionOptions",
"ChildProcessRuntimeConnection",
+ "ClientInfo",
"CloudSessionOptions",
"CloudSessionRepository",
"CommandContext",
diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py
index b831e072ad..4477fcfff3 100644
--- a/python/copilot/_cli_download.py
+++ b/python/copilot/_cli_download.py
@@ -27,7 +27,7 @@
import tempfile
import time
import zipfile
-from pathlib import Path
+from pathlib import Path, PurePosixPath
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
@@ -373,6 +373,164 @@ def _extract_runtime_node(data: bytes, npm_platform: str) -> bytes:
raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.")
+def _extract_runtime_wrapper(data: bytes, npm_platform: str) -> bytes:
+ """Extract the SDK out-of-process wrapper from an npm platform tarball."""
+ wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime"
+ target = f"package/prebuilds/{npm_platform}/{wrapper_name}"
+ with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf:
+ for name in tf.getnames():
+ if name == target or name.endswith(f"/prebuilds/{npm_platform}/{wrapper_name}"):
+ member = tf.getmember(name)
+ extracted = tf.extractfile(member)
+ if extracted is not None:
+ return extracted.read()
+ raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.")
+
+
+_HOSTLESS_EXCLUDED_TOP_LEVEL = {
+ "app.js",
+ "assets",
+ "changelog.json",
+ "copilot",
+ "copilot.exe",
+ "copilot-sdk",
+ "foundry-local-sdk",
+ "index.js",
+ "LICENSE.md",
+ "napi-oop-runtime",
+ "npm-loader.js",
+ "package.json",
+ "preloads",
+ "pvrecorder",
+ "queries",
+ "README.md",
+ "sdk",
+ "sea-loader.js",
+ "webview",
+}
+
+
+def _hostless_runtime_path(member_name: str, npm_platform: str) -> Path | None:
+ parts = PurePosixPath(member_name).parts
+ if not parts or parts[0] != "package" or len(parts) < 2:
+ return None
+ relative = parts[1:]
+ top_level = relative[0]
+ file_name = relative[-1]
+ if (
+ top_level in _HOSTLESS_EXCLUDED_TOP_LEVEL
+ or (top_level.startswith("tree-sitter") and top_level.endswith(".wasm"))
+ or (top_level.startswith("voice-") and top_level.endswith(".js"))
+ or file_name == "cli-native.node"
+ or "mediaremote-adapter" in relative
+ or file_name.startswith("copilot-runtime-bin")
+ ):
+ return None
+ if top_level == "prebuilds":
+ if len(relative) < 3 or relative[1] != npm_platform:
+ return None
+ relative = relative[2:]
+ destination = Path(*relative)
+ if destination.is_absolute() or ".." in destination.parts:
+ raise RuntimeError(f"Unsafe runtime package path: {member_name}")
+ return destination
+
+
+def _extract_runtime_bundle(data: bytes, npm_platform: str, destination: Path) -> None:
+ """Extract the hostless runtime tree, retaining unknown package assets by default."""
+ with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive:
+ for member in archive:
+ relative = _hostless_runtime_path(member.name, npm_platform)
+ if relative is None or member.isdir():
+ continue
+ if not member.isfile():
+ raise RuntimeError(f"Unsupported runtime package entry: {member.name}")
+ extracted = archive.extractfile(member)
+ if extracted is None:
+ raise RuntimeError(f"Failed to read runtime package entry: {member.name}")
+ target = destination / relative
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_bytes(extracted.read())
+ if sys.platform != "win32":
+ target.chmod(member.mode & 0o777)
+
+
+def ensure_runtime_wrapper(version: str | None = None, force: bool = False) -> str:
+ """Provision the runtime pair and its retained npm package assets."""
+ ver = version or CLI_VERSION
+ if not ver:
+ raise RuntimeError("No runtime version is pinned.")
+ npm_platform = get_npm_platform()
+ wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime"
+ pair_dir = get_cache_dir(ver) / "prebuilds" / npm_platform
+ wrapper_path = pair_dir / wrapper_name
+ runtime_path = pair_dir / "runtime.node"
+ assets_marker = pair_dir / ".hostless-runtime-assets-v2"
+
+ wrapper_exists = wrapper_path.is_file() and wrapper_path.stat().st_size > 0
+ runtime_exists = runtime_path.is_file() and runtime_path.stat().st_size > 0
+ if wrapper_exists and runtime_exists and assets_marker.is_file() and not force:
+ return str(wrapper_path)
+ if not force and wrapper_exists != runtime_exists:
+ raise RuntimeError(
+ f"Incomplete Copilot runtime bundle in {pair_dir}: "
+ f"{wrapper_name} and runtime.node are required."
+ )
+ if _should_skip_download():
+ raise RuntimeError(
+ f"Copilot runtime bundle is not cached in {pair_dir} "
+ "and automatic downloads are disabled."
+ )
+
+ data = _fetch_url_bytes(get_runtime_lib_url(ver, npm_platform), timeout=600)
+ integrity = _fetch_runtime_integrity(npm_platform, ver)
+ if not integrity:
+ raise RuntimeError(
+ "No Subresource Integrity value available for the Copilot runtime "
+ f"package ({npm_platform}@{ver}); refusing to stage unverified native code."
+ )
+ _verify_integrity(data, integrity)
+ import shutil
+
+ pair_dir.parent.mkdir(parents=True, exist_ok=True)
+ staging_dir = Path(tempfile.mkdtemp(dir=pair_dir.parent, prefix=".runtime-bundle-"))
+ try:
+ _extract_runtime_bundle(data, npm_platform, staging_dir)
+ staged_wrapper = staging_dir / wrapper_name
+ staged_runtime = staging_dir / "runtime.node"
+ if (
+ not staged_wrapper.is_file()
+ or staged_wrapper.stat().st_size == 0
+ or not staged_runtime.is_file()
+ or staged_runtime.stat().st_size == 0
+ ):
+ raise RuntimeError("Copilot runtime wrapper and runtime.node must both be non-empty.")
+ if sys.platform != "win32":
+ staged_wrapper.chmod(
+ staged_wrapper.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
+ )
+ (staging_dir / assets_marker.name).write_text("1\n", encoding="ascii")
+ try:
+ if pair_dir.exists() and (force or not assets_marker.is_file()):
+ shutil.rmtree(pair_dir, ignore_errors=True)
+ staging_dir.replace(pair_dir)
+ except OSError:
+ if (
+ wrapper_path.is_file()
+ and wrapper_path.stat().st_size > 0
+ and runtime_path.is_file()
+ and runtime_path.stat().st_size > 0
+ and assets_marker.is_file()
+ ):
+ return str(wrapper_path)
+ raise
+ finally:
+ if staging_dir.exists():
+ shutil.rmtree(staging_dir, ignore_errors=True)
+
+ return str(wrapper_path)
+
+
def ensure_runtime_library(cli_path: str, version: str | None = None) -> str | None:
"""Ensure the native in-process (FFI) runtime library sits next to ``cli_path``.
@@ -536,7 +694,10 @@ def main() -> None:
print(f"Downloading Copilot runtime v{ver}...")
try:
- path = download_cli(ver, force=args.force)
+ if args.in_process:
+ path = download_cli(ver, force=args.force)
+ else:
+ path = ensure_runtime_wrapper(ver, force=args.force)
print(f"Runtime cached at: {path}")
if args.in_process:
print("Downloading in-process (FFI) runtime library...")
diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py
index e04d1655e6..98aa776600 100644
--- a/python/copilot/_ffi_runtime_host.py
+++ b/python/copilot/_ffi_runtime_host.py
@@ -3,9 +3,9 @@
Instead of spawning the Copilot CLI as a child process and talking JSON-RPC over
stdio/TCP, the in-process transport loads the runtime's native shared library
(``runtime.node`` — a Rust ``cdylib``) into this process and drives JSON-RPC over
-its C ABI (FFI). The native ``host_start`` export spawns the residual worker
-itself, so the SDK never launches the worker directly; it only pumps opaque LSP
-``Content-Length:``-framed JSON-RPC bytes across the boundary:
+its C ABI (FFI). The native ``host_start`` export constructs the Rust server
+synchronously; the SDK only pumps opaque LSP ``Content-Length:``-framed JSON-RPC
+bytes across the boundary:
- client → server frames go to ``copilot_runtime_connection_write``
- server → client frames arrive on a native callback that feeds a thread-safe
@@ -114,8 +114,8 @@ def _natural_library_name() -> str:
return "libcopilot_runtime.so"
-def resolve_library_path(cli_entrypoint: str) -> str | None:
- """Resolve the native runtime library next to the given CLI entrypoint.
+def resolve_library_path(runtime_entrypoint: str) -> str | None:
+ """Resolve the native runtime library next to the given runtime entrypoint.
Checks, in order:
@@ -125,7 +125,7 @@ def resolve_library_path(cli_entrypoint: str) -> str | None:
Returns the absolute path, or ``None`` when neither exists.
"""
- directory = Path(cli_entrypoint).resolve().parent
+ directory = Path(runtime_entrypoint).resolve().parent
flat = directory / _natural_library_name()
if flat.is_file():
@@ -327,15 +327,15 @@ def wait(self, timeout: float | None = None) -> int: # noqa: ARG002
class FfiRuntimeHost:
"""Hosts the Copilot runtime in-process via its native C ABI.
- Construct with :meth:`create`, then :meth:`start` to spawn the worker and open
- the FFI connection. Expose :attr:`process` to :class:`JsonRpcClient`, and call
- :meth:`dispose` to tear everything down.
+ Construct with :meth:`create`, then :meth:`start` to start the native engine
+ and open the FFI connection. Expose :attr:`process` to
+ :class:`JsonRpcClient`, and call :meth:`dispose` to tear everything down.
"""
def __init__(
self,
library_path: str,
- cli_entrypoint: str,
+ cli_entrypoint: str | None,
environment: dict[str, str] | None = None,
args: Sequence[str] = (),
) -> None:
@@ -367,31 +367,30 @@ def process(self) -> _FfiProcessAdapter:
@staticmethod
def create(
- cli_entrypoint: str,
+ library_path: str,
+ cli_entrypoint: str | None = None,
environment: dict[str, str] | None = None,
args: Sequence[str] = (),
) -> FfiRuntimeHost:
- """Resolve the cdylib next to the CLI entrypoint and prepare the host.
+ """Load the runtime cdylib and prepare the host.
Raises:
RuntimeError: If the native runtime library cannot be found.
"""
- full_entrypoint = str(Path(cli_entrypoint).resolve())
- library_path = resolve_library_path(full_entrypoint)
- if library_path is None:
+ full_library_path = str(Path(library_path).resolve())
+ if not Path(full_library_path).is_file():
raise RuntimeError(
- "In-process FFI runtime library not found next to "
- f"'{full_entrypoint}'. Download it with "
- "`python -m copilot download-runtime --in-process`, or set "
- "COPILOT_CLI_PATH to a runtime package that ships it."
+ f"In-process FFI runtime library not found at '{full_library_path}'."
)
- return FfiRuntimeHost(library_path, full_entrypoint, environment, args)
+ full_entrypoint = (
+ str(Path(cli_entrypoint).resolve()) if cli_entrypoint is not None else None
+ )
+ return FfiRuntimeHost(full_library_path, full_entrypoint, environment, args)
def _build_argv(self) -> bytes:
- # A `.js` entrypoint (dev) is launched via node; the packaged single-file
- # CLI embeds its own Node and is invoked directly. `--no-auto-update`
- # pins the worker to the runtime package matching the loaded cdylib.
- if self._cli_entrypoint.lower().endswith(".js"):
+ if self._cli_entrypoint is None:
+ argv: list[str] = []
+ elif self._cli_entrypoint.lower().endswith(".js"):
argv = ["node", self._cli_entrypoint, "--embedded-host", "--no-auto-update"]
else:
argv = [self._cli_entrypoint, "--embedded-host", "--no-auto-update"]
@@ -407,11 +406,9 @@ def _build_env(self) -> bytes | None:
return json.dumps(obj).encode("utf-8")
def start_blocking(self) -> None:
- """Spawn the worker and open the FFI connection (blocks up to ~30s).
+ """Start the native engine and open the FFI connection.
- Must be run off the event loop (e.g. via :func:`asyncio.to_thread`);
- ``host_start`` blocks until the worker connects back and signals
- readiness.
+ Must be run off the event loop (e.g. via :func:`asyncio.to_thread`).
"""
argv = self._build_argv()
env = self._build_env()
@@ -419,8 +416,7 @@ def start_blocking(self) -> None:
self._server_id = self._lib.host_start(argv, len(argv), env, len(env) if env else 0)
if not self._server_id:
raise RuntimeError(
- f"copilot_runtime_host_start failed (library '{self._library_path}', "
- f"entrypoint '{self._cli_entrypoint}')."
+ f"copilot_runtime_host_start failed (library '{self._library_path}')."
)
self._outbound_callback = _OutboundCallback(self._on_outbound)
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 271fad626c..929194c9b7 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -28,6 +28,7 @@
from collections.abc import Awaitable, Callable, Mapping, Sequence
from dataclasses import dataclass, field
from datetime import UTC, datetime
+from pathlib import Path
from types import TracebackType
from typing import Any, ClassVar, Literal, NotRequired, TypedDict, cast, overload
@@ -177,6 +178,8 @@ class GitHubTokenCancelledResult(TypedDict):
_ConnectionState = Literal["disconnected", "connecting", "connected", "error"]
LogLevel = Literal["none", "error", "warning", "info", "debug", "all"]
+AskUserVariant = Literal["legacy", "elicitation"]
+"""Model-facing shape of the runtime's built-in ``ask_user`` tool."""
@dataclass
@@ -260,9 +263,23 @@ def _exp_assignment_response_to_dict(
return wire
+AutoTier = Literal["efficiency", "balance", "intelligence"]
+"""Routing preference used when the session model is ``auto``."""
+
+
class CapiSessionOptions(TypedDict, total=False):
"""Provider-scoped Copilot API (CAPI) session options."""
+ auto_tier: AutoTier
+ """Routing preference used when the session model is ``auto``.
+
+ Requires a runtime with Auto tier support and V2 Auto routing. When omitted
+ on create, the runtime uses its default routing behavior. The runtime persists
+ this preference across cold resume; an explicit tier on cold resume overrides
+ the persisted value. For an already-resident session, omission preserves the
+ current tier and a different tier is rejected.
+ """
+
enable_web_socket_responses: bool
"""Whether to use WebSocket transport for the CAPI Responses API.
@@ -289,6 +306,8 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An
def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]:
wire: dict[str, Any] = {}
+ if "auto_tier" in options:
+ wire["autoTier"] = options["auto_tier"]
if "enable_web_socket_responses" in options:
wire["enableWebSocketResponses"] = options["enable_web_socket_responses"]
return wire
@@ -488,6 +507,46 @@ class TelemetryConfig(TypedDict, total=False):
"""Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501
+class ClientInfo(TypedDict, total=False):
+ """Identity of the integrating application, declared on ``server.connect``.
+
+ Declaring it lets the telemetry the runtime emits on this connection be
+ attributed to a single, consistent surface (the application and its Copilot
+ integration) instead of the runtime's own build. All fields are optional;
+ omit any of them (or the whole object) to keep the default attribution.
+ """
+
+ application_name: str
+ """Name of the application using the SDK, e.g. ``"acme-developer-portal"``."""
+ application_version: str
+ """Version of the application using the SDK, e.g. ``"2.4.0"``."""
+ integration_name: str
+ """Optional name of an application integration, such as an extension or plugin."""
+ integration_version: str
+ """Optional version of the integration named by ``integration_name``."""
+
+
+def _client_info_to_wire(client_info: ClientInfo | None) -> dict[str, str] | None:
+ """Map a snake_case :class:`ClientInfo` onto the camelCase connect wire shape.
+
+ Empty fields are dropped. Returns ``None`` when no field carries a non-empty
+ value so the caller omits the ``clientInfo`` field entirely and keeps the
+ runtime's default attribution.
+ """
+ if not client_info:
+ return None
+ wire: dict[str, str] = {}
+ if client_info.get("application_name"):
+ wire["editorName"] = client_info["application_name"]
+ if client_info.get("application_version"):
+ wire["editorVersion"] = client_info["application_version"]
+ if client_info.get("integration_name"):
+ wire["extensionName"] = client_info["integration_name"]
+ if client_info.get("integration_version"):
+ wire["extensionVersion"] = client_info["integration_version"]
+ return wire or None
+
+
@dataclass
class RuntimeConnection:
"""Discriminated config describing how to reach the Copilot runtime.
@@ -757,6 +816,7 @@ class _CopilotClientOptions:
request_handler: CopilotRequestHandler | None = None
session_idle_timeout_seconds: int | None = None
enable_remote_sessions: bool = False
+ client_info: ClientInfo | None = None
on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None
on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = (
None
@@ -1354,25 +1414,6 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent:
_CLI_PROCESS_EXIT_TIMEOUT_SECONDS = 5
-def _get_or_download_cli(*, include_runtime_lib: bool = False) -> str | None:
- """Get the cached CLI binary, downloading if necessary.
-
- Returns the path to the CLI binary, or None if unavailable (dev install
- with no pinned version, or auto-download disabled).
-
- When ``include_runtime_lib`` is set, also ensures the native in-process FFI
- runtime is available (downloading it on first use).
- """
- from ._cli_download import get_or_download_cli
-
- cli_path = get_or_download_cli()
- if cli_path and include_runtime_lib:
- from ._cli_download import ensure_runtime_library
-
- ensure_runtime_library(cli_path)
- return cli_path
-
-
def _extract_transform_callbacks(
system_message: SystemMessageConfig | dict[str, Any] | None,
) -> tuple[dict[str, Any] | None, dict[str, SectionTransformFn] | None]:
@@ -1530,6 +1571,7 @@ def __init__(
request_handler: CopilotRequestHandler | None = None,
session_idle_timeout_seconds: int | None = None,
enable_remote_sessions: bool = False,
+ client_info: ClientInfo | None = None,
on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None,
on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]]
| None = None,
@@ -1579,6 +1621,11 @@ def __init__(
Control integration). When ``True``, sessions in a GitHub
repository working directory are accessible from GitHub web
and mobile.
+ client_info: Identity of the integrating application, forwarded to the
+ runtime on the ``server.connect`` handshake. Declaring it lets
+ the telemetry the runtime emits on this connection be attributed
+ to a consistent surface instead of the runtime's own build. All
+ fields are optional; omit it to keep the default attribution.
on_list_models: Custom handler for :meth:`list_models`. When
provided, the handler is called instead of querying the runtime
server.
@@ -1616,6 +1663,7 @@ def __init__(
request_handler=request_handler,
session_idle_timeout_seconds=session_idle_timeout_seconds,
enable_remote_sessions=enable_remote_sessions,
+ client_info=client_info,
on_list_models=on_list_models,
on_github_telemetry=on_github_telemetry,
mode=mode,
@@ -1649,6 +1697,7 @@ def __init__(
self._cli_path_source: str | None = None
self._ffi_host: FfiRuntimeHost | None = None
self._inprocess_runtime_path: str | None = None
+ self._inprocess_cli_entrypoint: str | None = None
if isinstance(connection, UriRuntimeConnection):
if connection.connection_token is not None and len(connection.connection_token) == 0:
@@ -1660,9 +1709,7 @@ def __init__(
# In-process (FFI): no child process and no per-connection token.
self._runtime_port = None
self._effective_connection_token = None
- self._inprocess_runtime_path = self._resolve_runtime_entrypoint(
- None, include_runtime_lib=True
- )
+ self._inprocess_runtime_path = self._resolve_inprocess_runtime()
if options.use_logged_in_user is None:
options.use_logged_in_user = not bool(options.github_token)
else:
@@ -1683,7 +1730,7 @@ def __init__(
else:
self._effective_connection_token = None
- # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > downloaded binary.
+ # Resolve runtime path: explicit CLI > COPILOT_CLI_PATH > downloaded runtime.
# Select the environment by identity, not truthiness, so an intentionally
# empty per-connection or client env stays authoritative (the spawned child
# receives that empty mapping) instead of falling back to os.environ and
@@ -1728,52 +1775,47 @@ def _resolve_runtime_entrypoint(
path: str | None,
*,
env: Mapping[str, str] | None = None,
- include_runtime_lib: bool = False,
) -> str:
"""Resolve the runtime executable path (explicit > env > downloaded).
Sets ``self._cli_path_source`` for diagnostics. When
- ``include_runtime_lib`` is set (in-process transport), also ensures the
- native runtime library is downloaded alongside the CLI.
-
Raises:
RuntimeError: If no runtime path can be resolved.
"""
if path is not None:
self._cli_path_source = "explicit"
- return self._ensure_runtime_lib(path) if include_runtime_lib else path
+ return path
lookup = env if env is not None else os.environ
env_cli_path = lookup.get("COPILOT_CLI_PATH")
if env_cli_path:
self._cli_path_source = "environment"
- return self._ensure_runtime_lib(env_cli_path) if include_runtime_lib else env_cli_path
-
- downloaded_path = _get_or_download_cli(include_runtime_lib=include_runtime_lib)
- if downloaded_path:
- self._cli_path_source = "downloaded"
- return downloaded_path
-
- raise RuntimeError(
- "Copilot CLI not found. Install a published wheel (which "
- "auto-downloads the CLI on first use), set COPILOT_CLI_PATH, "
- "or pass an explicit path via "
- "RuntimeConnection.for_stdio(path=...) / "
- "RuntimeConnection.for_tcp(path=...)."
- )
+ return env_cli_path
- @staticmethod
- def _ensure_runtime_lib(cli_path: str) -> str:
- """Ensure the in-process runtime library sits next to a user-supplied CLI.
+ from ._cli_download import ensure_runtime_wrapper
- For explicit/``COPILOT_CLI_PATH`` entrypoints, the native library may
- already be bundled (dev ``prebuilds`` layout); otherwise it is fetched on
- first use. Returns ``cli_path`` unchanged.
- """
- from ._cli_download import ensure_runtime_library
+ self._cli_path_source = "downloaded"
+ return ensure_runtime_wrapper()
- ensure_runtime_library(cli_path)
- return cli_path
+ def _resolve_inprocess_runtime(self) -> str:
+ explicit_cli = os.environ.get("COPILOT_CLI_PATH")
+ if explicit_cli:
+ from ._cli_download import ensure_runtime_library
+
+ runtime_path = ensure_runtime_library(explicit_cli)
+ if runtime_path is None:
+ raise RuntimeError(
+ f"In-process runtime library not found next to '{explicit_cli}'."
+ )
+ self._cli_path_source = "environment"
+ self._inprocess_cli_entrypoint = explicit_cli
+ return runtime_path
+
+ from ._cli_download import ensure_runtime_wrapper
+
+ wrapper_path = Path(ensure_runtime_wrapper())
+ self._cli_path_source = "downloaded"
+ return str(wrapper_path.with_name("runtime.node"))
@property
def rpc(self) -> ServerRpc:
@@ -2209,6 +2251,7 @@ async def create_session(
available_tools: list[str] | ToolSet | None = None,
excluded_tools: list[str] | ToolSet | None = None,
on_user_input_request: UserInputHandler | None = None,
+ ask_user_variant: AskUserVariant | None = None,
hooks: SessionHooks | None = None,
working_directory: str | None = None,
additional_directories: list[str] | None = None,
@@ -2271,6 +2314,7 @@ async def create_session(
extension_info: ExtensionInfo | None = None,
canvas_provider: CanvasProviderIdentity | None = None,
canvas_handler: CanvasHandler | None = None,
+ feature_flags: dict[str, bool] | None = None,
exp_assignments: CopilotExpAssignmentResponse | None = None,
enable_managed_settings: bool | None = None,
github_mcp_tool_config: GitHubMcpToolConfig | None = None,
@@ -2311,10 +2355,16 @@ async def create_session(
including custom tools registered via ``tools=``. Ignored if
``available_tools`` is set.
on_user_input_request: Handler for user input requests.
+ ask_user_variant: Model-facing shape of the ``ask_user`` tool.
+ Accepted values are ``"legacy"`` and ``"elicitation"``. The
+ default is ``"legacy"``. To use ``"elicitation"``, also provide
+ ``on_elicitation_request`` so the host can answer structured forms.
hooks: Lifecycle hooks for the session.
working_directory: Working directory for the session.
provider: Provider configuration for Azure or custom endpoints.
- capi: CAPI provider-scoped options. WebSocket transport is the
+ capi: CAPI provider-scoped options. Set ``auto_tier`` to ``efficiency``,
+ ``balance``, or ``intelligence`` to select an Auto routing preference
+ on a runtime with Auto tier support. WebSocket transport is the
default for the CAPI Responses API whenever the model advertises
the ``ws:/responses`` endpoint. Set
``enable_web_socket_responses=False`` to force the HTTP
@@ -2410,6 +2460,9 @@ async def create_session(
on its own and has no effect unless MCP Apps are enabled for
the session (see ``enable_mcp_apps``). Omitted from the wire
payload entirely when None.
+ feature_flags: Feature-flag values resolved by the host for this
+ session. Re-supply them when resuming after a runtime restart.
+ Sent on the wire as ``featureFlags``.
exp_assignments: ExP assignment ("flight") data injected by a
trusted integrator, in the same JSON shape the Copilot CLI
fetches from the experimentation service
@@ -2465,6 +2518,8 @@ async def create_session(
raise ValueError("on_permission_request must be callable when provided.")
if github_token is not None and github_token_provider is not None:
raise ValueError("github_token and github_token_provider are mutually exclusive")
+ if ask_user_variant not in (None, "legacy", "elicitation"):
+ raise ValueError('ask_user_variant must be "legacy" or "elicitation"')
if not self._client:
await self.start()
@@ -2552,6 +2607,8 @@ async def create_session(
# Enable user input request callback if handler provided
if on_user_input_request:
payload["requestUserInput"] = True
+ if ask_user_variant is not None:
+ payload["askUserVariant"] = ask_user_variant
# Enable elicitation request callback if handler provided
payload["requestElicitation"] = bool(on_elicitation_request)
@@ -2584,6 +2641,9 @@ async def create_session(
if cloud is not None:
payload["cloud"] = _cloud_session_options_to_dict(cloud)
+ if feature_flags is not None:
+ payload["featureFlags"] = feature_flags
+
# Add ExP assignment data if provided (trusted integrator)
if exp_assignments is not None:
payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments)
@@ -2970,6 +3030,7 @@ async def resume_session(
available_tools: list[str] | ToolSet | None = None,
excluded_tools: list[str] | ToolSet | None = None,
on_user_input_request: UserInputHandler | None = None,
+ ask_user_variant: AskUserVariant | None = None,
hooks: SessionHooks | None = None,
working_directory: str | None = None,
additional_directories: list[str] | None = None,
@@ -3033,6 +3094,7 @@ async def resume_session(
canvas_provider: CanvasProviderIdentity | None = None,
canvas_handler: CanvasHandler | None = None,
open_canvases: list[OpenCanvasInstance] | None = None,
+ feature_flags: dict[str, bool] | None = None,
exp_assignments: CopilotExpAssignmentResponse | None = None,
enable_managed_settings: bool | None = None,
github_mcp_tool_config: GitHubMcpToolConfig | None = None,
@@ -3073,10 +3135,17 @@ async def resume_session(
including custom tools registered via ``tools=``. Ignored if
``available_tools`` is set.
on_user_input_request: Handler for user input requests.
+ ask_user_variant: Model-facing shape of the ``ask_user`` tool.
+ Accepted values are ``"legacy"`` and ``"elicitation"``. The
+ default is ``"legacy"``. To use ``"elicitation"``, also provide
+ ``on_elicitation_request`` so the host can answer structured forms.
hooks: Lifecycle hooks for the session.
working_directory: Working directory for the session.
provider: Provider configuration for Azure or custom endpoints.
- capi: CAPI provider-scoped options. WebSocket transport is the
+ capi: CAPI provider-scoped options. Omit ``auto_tier`` to preserve the
+ current or persisted Auto routing preference. An explicit tier
+ overrides it on cold resume, but cannot change it on an
+ already-resident session. WebSocket transport is the
default for the CAPI Responses API whenever the model advertises
the ``ws:/responses`` endpoint. Set
``enable_web_socket_responses=False`` to force the HTTP
@@ -3174,6 +3243,8 @@ async def resume_session(
tool calls or permission prompts that were still pending when the
session was last suspended. When False (the default), the runtime
treats pending work as interrupted on resume.
+ feature_flags: Feature-flag values resolved by the host to apply
+ on resume. Sent on the wire as ``featureFlags``.
exp_assignments: ExP assignment ("flight") data injected by a
trusted integrator, in the same JSON shape the Copilot CLI
fetches from the experimentation service
@@ -3226,6 +3297,8 @@ async def resume_session(
raise ValueError("on_permission_request must be callable when provided.")
if github_token is not None and github_token_provider is not None:
raise ValueError("github_token and github_token_provider are mutually exclusive")
+ if ask_user_variant not in (None, "legacy", "elicitation"):
+ raise ValueError('ask_user_variant must be "legacy" or "elicitation"')
if not self._client:
await self.start()
@@ -3341,6 +3414,8 @@ async def resume_session(
if on_user_input_request:
payload["requestUserInput"] = True
+ if ask_user_variant is not None:
+ payload["askUserVariant"] = ask_user_variant
# Enable elicitation request callback if handler provided
payload["requestElicitation"] = bool(on_elicitation_request)
@@ -3368,6 +3443,9 @@ async def resume_session(
if remote_session is not None:
payload["remoteSession"] = remote_session.value
+ if feature_flags is not None:
+ payload["featureFlags"] = feature_flags
+
# Add ExP assignment data if provided (trusted integrator)
if exp_assignments is not None:
payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments)
@@ -4024,6 +4102,12 @@ async def _verify_protocol_version(self) -> None:
# event is forwarded). Also sent on session.create/resume for older CLIs.
if self._on_github_telemetry is not None:
connect_params["enableGitHubTelemetryForwarding"] = True
+ # Declare the integrating application's identity so the runtime attributes
+ # the telemetry it emits on this connection to a consistent surface
+ # instead of its own build. Omitted when the app didn't supply it.
+ client_info = _client_info_to_wire(self._options.client_info)
+ if client_info is not None:
+ connect_params["clientInfo"] = client_info
connect_result = _ConnectResult.from_dict(
await self._client.request("connect", connect_params)
)
@@ -4286,7 +4370,6 @@ async def _start_cli_server(self) -> None:
env = dict(os.environ)
else:
env = dict(opts.env)
-
# Set auth token in environment if provided
if opts.github_token:
env["COPILOT_SDK_AUTH_TOKEN"] = opts.github_token
@@ -4437,6 +4520,7 @@ async def _start_inprocess_ffi(self) -> None:
host = FfiRuntimeHost.create(
runtime_path,
+ cli_entrypoint=self._inprocess_cli_entrypoint,
environment=environment or None,
args=tuple(args),
)
diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py
index 1d59a55f4a..b393fb6eed 100644
--- a/python/copilot/generated/rpc.py
+++ b/python/copilot/generated/rpc.py
@@ -6,7 +6,7 @@
from typing import ClassVar, TYPE_CHECKING
-from .session_events import AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity
+from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity
if TYPE_CHECKING:
from .._jsonrpc import JsonRpcClient
@@ -1051,6 +1051,11 @@ def to_dict(self) -> dict:
class CapiSessionOptions:
"""Options scoped to the built-in CAPI (Copilot API) provider."""
+ auto_tier: AutoTier | None = None
+ """Routing preference used when the session model is `auto`. The runtime persists the
+ preference across cold resume. When omitted, the default routing behavior is used.
+ Resuming an already-resident session cannot change its preference.
+ """
enable_web_socket_responses: bool | None = None
"""Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when
the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses
@@ -1062,11 +1067,14 @@ class CapiSessionOptions:
@staticmethod
def from_dict(obj: Any) -> 'CapiSessionOptions':
assert isinstance(obj, dict)
+ auto_tier = from_union([AutoTier, from_none], obj.get("autoTier"))
enable_web_socket_responses = from_union([from_bool, from_none], obj.get("enableWebSocketResponses"))
- return CapiSessionOptions(enable_web_socket_responses)
+ return CapiSessionOptions(auto_tier, enable_web_socket_responses)
def to_dict(self) -> dict:
result: dict = {}
+ if self.auto_tier is not None:
+ result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier)
if self.enable_web_socket_responses is not None:
result["enableWebSocketResponses"] = from_union([from_bool, from_none], self.enable_web_socket_responses)
return result
@@ -1323,8 +1331,11 @@ class CatalogNetworkFailureReason(Enum):
DNS = "dns"
HTTP_STATUS = "http-status"
OFFLINE = "offline"
+ PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required"
+ RATE_LIMITED = "rate-limited"
REDIRECT_REJECTED = "redirect-rejected"
RESPONSE_TOO_LARGE = "response-too-large"
+ SERVICE_UNAVAILABLE = "service-unavailable"
TIMEOUT = "timeout"
TLS = "tls"
@@ -1419,12 +1430,15 @@ class CatalogSearchResultReason(Enum):
NO_CREDENTIAL = "no-credential"
OFFLINE = "offline"
PLANNING_UNAVAILABLE = "planning-unavailable"
+ PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required"
PROXY_REJECTED = "proxy-rejected"
+ RATE_LIMITED = "rate-limited"
REDIRECT_REJECTED = "redirect-rejected"
REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address"
RESPONSE_TOO_LARGE = "response-too-large"
SCHEMA_VIOLATION = "schema-violation"
SEARCH_UNAVAILABLE = "search-unavailable"
+ SERVICE_UNAVAILABLE = "service-unavailable"
SIZE_LIMIT_EXCEEDED = "size-limit-exceeded"
TIMEOUT = "timeout"
TLS = "tls"
@@ -2484,6 +2498,42 @@ def to_dict(self) -> dict:
result["ids"] = from_list(from_str, self.ids)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+class HookType(Enum):
+ """Hook event that invokes this action.
+
+ Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally
+ support callback-only events.
+ """
+ AGENT_STOP = "agentStop"
+ ERROR_OCCURRED = "errorOccurred"
+ NOTIFICATION = "notification"
+ PERMISSION_REQUEST = "permissionRequest"
+ POST_RESULT = "postResult"
+ POST_TOOL_USE = "postToolUse"
+ POST_TOOL_USE_FAILURE = "postToolUseFailure"
+ PRE_COMPACT = "preCompact"
+ PRE_MCP_TOOL_CALL = "preMcpToolCall"
+ PRE_PR_DESCRIPTION = "prePRDescription"
+ PRE_TOOL_USE = "preToolUse"
+ SESSION_END = "sessionEnd"
+ SESSION_START = "sessionStart"
+ SUBAGENT_START = "subagentStart"
+ SUBAGENT_STOP = "subagentStop"
+ USER_PROMPT_SUBMITTED = "userPromptSubmitted"
+ USER_PROMPT_TRANSFORMED = "userPromptTransformed"
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+class HookOrigin(Enum):
+ """Configuration tier that contributed this hook action.
+
+ Configuration tier that contributed a discovered hook action.
+ """
+ PLUGIN = "plugin"
+ POLICY = "policy"
+ REPOSITORY = "repository"
+ USER = "user"
+
# Experimental: this type is part of an experimental API and may change or be removed.
class DiscoveredMCPServerType(Enum):
"""Server transport type: stdio, http, sse (deprecated), or memory"""
@@ -2502,16 +2552,21 @@ class EnqueueCommandParams:
"""Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO
with any in-flight items; if the session is idle, processing kicks off immediately.
"""
+ display_text: str | None = None
+ """Optional user-facing text for the queue row. The command string is shown when omitted."""
@staticmethod
def from_dict(obj: Any) -> 'EnqueueCommandParams':
assert isinstance(obj, dict)
command = from_str(obj.get("command"))
- return EnqueueCommandParams(command)
+ display_text = from_union([from_none, from_str], obj.get("displayText"))
+ return EnqueueCommandParams(command, display_text)
def to_dict(self) -> dict:
result: dict = {}
result["command"] = from_str(self.command)
+ if self.display_text is not None:
+ result["displayText"] = from_union([from_none, from_str], self.display_text)
return result
# Experimental: this type is part of an experimental API and may change or be removed.
@@ -2566,6 +2621,9 @@ class EventsReadDirection(Enum):
cursor toward newer events; 'backward' returns the newest window first (tail-first) and
pages toward older events. Events within a returned batch are always chronological
(oldest-to-newest), even for a backward read.
+
+ Direction to page through persisted history. Forward starts at the beginning; backward
+ starts with the newest events. Events in each page remain chronological.
"""
BACKWARD = "backward"
FORWARD = "forward"
@@ -3342,6 +3400,7 @@ class FactoryRunFailureType(Enum):
FACTORY_ACCOUNTING_INCOMPLETE = "factory_accounting_incomplete"
FACTORY_DURABLE_FAILURE = "factory_durable_failure"
FACTORY_LIMIT_REACHED = "factory_limit_reached"
+ FACTORY_PROVIDER_DISCONNECTED = "factory_provider_disconnected"
FACTORY_RESUME_DECLINED = "factory_resume_declined"
# Experimental: this type is part of an experimental API and may change or be removed.
@@ -3989,28 +4048,6 @@ def to_dict(self) -> dict:
class HMACAuthInfoType(Enum):
HMAC = "hmac"
-# Internal: this type is an internal SDK API and is not part of the public surface.
-class _HookType(Enum):
- """Hook event name dispatched through the SDK callback transport."""
-
- AGENT_STOP = "agentStop"
- ERROR_OCCURRED = "errorOccurred"
- NOTIFICATION = "notification"
- PERMISSION_REQUEST = "permissionRequest"
- POST_RESULT = "postResult"
- POST_TOOL_USE = "postToolUse"
- POST_TOOL_USE_FAILURE = "postToolUseFailure"
- PRE_COMPACT = "preCompact"
- PRE_MCP_TOOL_CALL = "preMcpToolCall"
- PRE_PR_DESCRIPTION = "prePRDescription"
- PRE_TOOL_USE = "preToolUse"
- SESSION_END = "sessionEnd"
- SESSION_START = "sessionStart"
- SUBAGENT_START = "subagentStart"
- SUBAGENT_STOP = "subagentStop"
- USER_PROMPT_SUBMITTED = "userPromptSubmitted"
- USER_PROMPT_TRANSFORMED = "userPromptTransformed"
-
# Internal: this type is an internal SDK API and is not part of the public surface.
@dataclass
class _HookInvokeResponse:
@@ -4030,6 +4067,38 @@ def to_dict(self) -> dict:
result["output"] = self.output
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class HooksDiscoverRequest:
+ """Optional project paths and host-exclusion behavior for server-scoped hook discovery."""
+
+ exclude_host_hooks: bool | None = None
+ """When true, omit host-owned user and plugin hook rows and their diagnostics.
+ Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks
+ still contribute to each remaining row's effective enabled state. This filters sources
+ rather than simulating a host with no settings.
+ """
+ project_paths: list[str] | None = None
+ """Optional project directory paths whose trusted repository and project-expanded plugin
+ hooks should be discovered. When omitted or empty, user, managed-policy, and globally
+ enabled installed or explicit plugin hooks are returned without project expansion.
+ """
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'HooksDiscoverRequest':
+ assert isinstance(obj, dict)
+ exclude_host_hooks = from_union([from_bool, from_none], obj.get("excludeHostHooks"))
+ project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths"))
+ return HooksDiscoverRequest(exclude_host_hooks, project_paths)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.exclude_host_hooks is not None:
+ result["excludeHostHooks"] = from_union([from_bool, from_none], self.exclude_host_hooks)
+ if self.project_paths is not None:
+ result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths)
+ return result
+
class InstalledPluginSourceURLSource(Enum):
GITHUB = "github"
LOCAL = "local"
@@ -5576,28 +5645,6 @@ def to_dict(self) -> dict:
result["serverName"] = from_str(self.server_name)
return result
-@dataclass
-class ExternalRefMCPOauthHTTPResponse:
- """HTTP response returned by the server.
-
- HTTP 401 or 403 response returned by the server.
-
- HTTP response returned by the server, when the probe reached the server and captured the
- complete response.
- """
- external_ref_marker_external_ref_mcp_oauth_http_response: str
-
- @staticmethod
- def from_dict(obj: Any) -> 'ExternalRefMCPOauthHTTPResponse':
- assert isinstance(obj, dict)
- external_ref_marker_external_ref_mcp_oauth_http_response = from_str(obj.get("__externalRefMarker___ExternalRef_McpOauthHttpResponse"))
- return ExternalRefMCPOauthHTTPResponse(external_ref_marker_external_ref_mcp_oauth_http_response)
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["__externalRefMarker___ExternalRef_McpOauthHttpResponse"] = from_str(self.external_ref_marker_external_ref_mcp_oauth_http_response)
- return result
-
class Status(Enum):
AUTHENTICATED = "authenticated"
FAILED = "failed"
@@ -5745,7 +5792,9 @@ class MCPPlanInstallResultReason(Enum):
OFFLINE = "offline"
PLANNING_UNAVAILABLE = "planning-unavailable"
POLICY_FORBIDS = "policy-forbids"
+ PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required"
PROXY_REJECTED = "proxy-rejected"
+ RATE_LIMITED = "rate-limited"
REDIRECT_REJECTED = "redirect-rejected"
REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address"
REMOTE_ENUMERATION_UNAVAILABLE = "remote-enumeration-unavailable"
@@ -5753,6 +5802,7 @@ class MCPPlanInstallResultReason(Enum):
RESPONSE_TOO_LARGE = "response-too-large"
SCHEMA_VIOLATION = "schema-violation"
SEARCH_UNAVAILABLE = "search-unavailable"
+ SERVICE_UNAVAILABLE = "service-unavailable"
SIZE_LIMIT_EXCEEDED = "size-limit-exceeded"
STALE = "stale"
TIMEOUT = "timeout"
@@ -6605,6 +6655,13 @@ class ModelBillingPromo:
"""Human-readable promotion message. Does not include the expiry timestamp; consumers may
format endsAt and append it when present.
"""
+ show_banner: bool | None = None
+ """Whether the service asked hosts to give this promotion a prominent surface, such as a
+ dedicated banner, in addition to listing it with the model. `true` requests that surface
+ and `false` asks for the model list only. Absent means the service expressed no
+ preference — for example a response that predates the field — so hosts should apply their
+ own default rather than read it as `false`.
+ """
@staticmethod
def from_dict(obj: Any) -> 'ModelBillingPromo':
@@ -6613,7 +6670,8 @@ def from_dict(obj: Any) -> 'ModelBillingPromo':
ends_at = from_union([from_str, from_none], obj.get("endsAt"))
id = from_union([from_str, from_none], obj.get("id"))
message = from_union([from_str, from_none], obj.get("message"))
- return ModelBillingPromo(discount_percent, ends_at, id, message)
+ show_banner = from_union([from_bool, from_none], obj.get("showBanner"))
+ return ModelBillingPromo(discount_percent, ends_at, id, message, show_banner)
def to_dict(self) -> dict:
result: dict = {}
@@ -6625,6 +6683,8 @@ def to_dict(self) -> dict:
result["id"] = from_union([from_str, from_none], self.id)
if self.message is not None:
result["message"] = from_union([from_str, from_none], self.message)
+ if self.show_banner is not None:
+ result["showBanner"] = from_union([from_bool, from_none], self.show_banner)
return result
# Experimental: this type is part of an experimental API and may change or be removed.
@@ -9184,9 +9244,7 @@ class SessionsRegisterExtensionToolsOnSessionOptions:
# Internal: this field is an internal SDK API and is not part of the public surface.
enabled: Any = None
- """In-process `() => boolean` gating callback (CLI-only optimization). Marked internal:
- replaced by runtime-side enable/disable RPCs in the SDK migration.
- """
+ """In-process `() => boolean` gating callback used only by the CLI."""
@staticmethod
def from_dict(obj: Any) -> 'SessionsRegisterExtensionToolsOnSessionOptions':
@@ -9200,6 +9258,26 @@ def to_dict(self) -> dict:
result["enabled"] = self.enabled
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+# Internal: this type is an internal SDK API and is not part of the public surface.
+@dataclass
+class _RegisterExtensionToolsResult:
+ """Handle for releasing the extension tool registration."""
+
+ unsubscribe: Any
+ """In-process unsubscribe function used only by the CLI."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> '_RegisterExtensionToolsResult':
+ assert isinstance(obj, dict)
+ unsubscribe = obj.get("unsubscribe")
+ return _RegisterExtensionToolsResult(unsubscribe)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["unsubscribe"] = self.unsubscribe
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class ReleaseEventInterestParams:
@@ -9579,22 +9657,25 @@ def to_dict(self) -> dict:
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SandboxConfigUserPolicyNetworkProxy:
- """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and
- cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS.
- Credentials go in the separate `username`/`password` fields. A credential-free http://
- loopback proxy URL is routed through the localhost proxy automatically; an https:// or
- authenticated loopback URL is used as-is.
+ """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint,
+ requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is
+ accepted and routed through the IPv4 gateway), and does not support proxy credentials.
+ macOS relies on applications honoring proxy environment variables. Windows also
+ configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's
+ networking stack. Configure supported credentials in the separate `username` and
+ `password` fields. A credential-free http:// loopback URL uses the localhost proxy form,
+ while an https:// or authenticated loopback URL uses the URL form.
HTTP proxy configuration for sandboxed traffic.
"""
url: str
"""Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the
- scheme's standard port when omitted. Credentials must not be embedded here — a
- `user:pass@` authority is rejected; put them in the separate `username`/`password`
- fields. A credential-free http:// loopback URL is routed through the localhost proxy
- automatically; loopback covers localhost and any *.localhost subdomain, the whole
- 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or
- one with a username/password set, is used as-is.
+ scheme's standard port when omitted; an explicit port must be between 1 and 65535.
+ Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in
+ the separate `username`/`password` fields. A credential-free http:// loopback proxy URL
+ is routed through the localhost proxy automatically; loopback covers localhost and any
+ *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback
+ (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is.
"""
password: str | None = None
"""Optional password for proxy authentication, combined with the URL at spawn time. The
@@ -9664,6 +9745,36 @@ class _SandboxConfigSource(Enum):
USER_DISABLED = "user_disabled"
USER_ENABLED = "user_enabled"
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SandboxEnforcementStatus:
+ """Managed sandbox enforcement state for a session."""
+
+ blocked: bool
+ """Whether an enforcement failure has permanently blocked the session."""
+
+ required: bool
+ """Whether the effective managed policy requires an available sandbox backend."""
+
+ reason: str | None = None
+ """The first sandbox enforcement failure that blocked the session."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SandboxEnforcementStatus':
+ assert isinstance(obj, dict)
+ blocked = from_bool(obj.get("blocked"))
+ required = from_bool(obj.get("required"))
+ reason = from_union([from_str, from_none], obj.get("reason"))
+ return SandboxEnforcementStatus(blocked, required, reason)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["blocked"] = from_bool(self.blocked)
+ result["required"] = from_bool(self.required)
+ if self.reason is not None:
+ result["reason"] = from_union([from_str, from_none], self.reason)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class ScheduleAddAtRequest:
@@ -12614,6 +12725,114 @@ class SkillDiscoveryScope(Enum):
PERSONAL_COPILOT = "personal-copilot"
PROJECT = "project"
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SkillProviderDescriptor:
+ """Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched
+ separately and lazily.
+ """
+ description: str
+ """Description used in skill catalogs without fetching content."""
+
+ name: str
+ """Invocation and display name."""
+
+ argument_hint: str | None = None
+ """Optional freeform argument hint used by slash-command catalogs."""
+
+ disable_model_invocation: bool | None = None
+ """Whether model invocation is disabled. Defaults to false."""
+
+ user_invocable: bool | None = None
+ """Whether users may invoke the skill directly. Defaults to true."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SkillProviderDescriptor':
+ assert isinstance(obj, dict)
+ description = from_str(obj.get("description"))
+ name = from_str(obj.get("name"))
+ argument_hint = from_union([from_str, from_none], obj.get("argumentHint"))
+ disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation"))
+ user_invocable = from_union([from_bool, from_none], obj.get("userInvocable"))
+ return SkillProviderDescriptor(description, name, argument_hint, disable_model_invocation, user_invocable)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["description"] = from_str(self.description)
+ result["name"] = from_str(self.name)
+ if self.argument_hint is not None:
+ result["argumentHint"] = from_union([from_str, from_none], self.argument_hint)
+ if self.disable_model_invocation is not None:
+ result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation)
+ if self.user_invocable is not None:
+ result["userInvocable"] = from_union([from_bool, from_none], self.user_invocable)
+ return result
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SkillProviderListRequest:
+ """Identifies the target session."""
+
+ session_id: str
+ """Target session identifier"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SkillProviderListRequest':
+ assert isinstance(obj, dict)
+ session_id = from_str(obj.get("sessionId"))
+ return SkillProviderListRequest(session_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["sessionId"] = from_str(self.session_id)
+ return result
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+# Internal: this type is an internal SDK API and is not part of the public surface.
+@dataclass
+class _SkillProviderReadRequest:
+ """Identifies one SDK-provided skill by invocation name."""
+
+ name: str
+ """Invocation name of the skill to read."""
+
+ session_id: str
+ """Target session identifier"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> '_SkillProviderReadRequest':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ session_id = from_str(obj.get("sessionId"))
+ return _SkillProviderReadRequest(name, session_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ result["sessionId"] = from_str(self.session_id)
+ return result
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+# Internal: this type is an internal SDK API and is not part of the public surface.
+@dataclass
+class _SkillProviderReadResult:
+ """Complete text-only SKILL.md content returned by an SDK session's skill provider. Related
+ files and assets are not supported.
+ """
+ markdown: str
+ """Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> '_SkillProviderReadResult':
+ assert isinstance(obj, dict)
+ markdown = from_str(obj.get("markdown"))
+ return _SkillProviderReadResult(markdown)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["markdown"] = from_str(self.markdown)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SkillsConfigSetSkillDisabledRequest:
@@ -13697,6 +13916,42 @@ class UIElicitationSchemaPropertyNumberType(Enum):
INTEGER = "integer"
NUMBER = "number"
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class UIEphemeralQueryRequest:
+ """Transient question to answer without adding it to conversation history."""
+
+ question: str
+ """Question to answer from the current conversation context."""
+
+ # Internal: this field is an internal SDK API and is not part of the public surface.
+ abort_signal: Any = None
+ """In-process `AbortSignal` forwarded to the model client to cancel an in-flight request.
+ Internal and excluded from the public SDK surface.
+ """
+ # Internal: this field is an internal SDK API and is not part of the public surface.
+ on_chunk: Any = None
+ """In-process streaming callback `(text) => void` invoked with each token as the model emits
+ it. Internal and excluded from the public SDK surface.
+ """
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'UIEphemeralQueryRequest':
+ assert isinstance(obj, dict)
+ question = from_str(obj.get("question"))
+ abort_signal = obj.get("abortSignal")
+ on_chunk = obj.get("onChunk")
+ return UIEphemeralQueryRequest(question, abort_signal, on_chunk)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["question"] = from_str(self.question)
+ if self.abort_signal is not None:
+ result["abortSignal"] = self.abort_signal
+ if self.on_chunk is not None:
+ result["onChunk"] = self.on_chunk
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class UIEphemeralQueryResult:
@@ -15037,8 +15292,9 @@ class CatalogSearchRequest:
"""Protocol version and capabilities the caller requires."""
query: str
- """Free-text search query. Never written to logs or telemetry."""
-
+ """Free-text search query. Persisted as tool input for session continuity, but omitted from
+ telemetry.
+ """
kinds: list[CatalogCandidateKind] | None = None
"""Restrict results to these candidate kinds. When omitted, every kind the runtime supports
is searched.
@@ -15327,6 +15583,11 @@ class CatalogNetworkFailureError:
reason: CatalogNetworkFailureReason
"""Categorised failure, low cardinality so it can be aggregated without carrying a URL."""
+ retry_after_seconds: int | None = None
+ """Bounded cooldown in seconds before another catalog request should be attempted, when the
+ authority supplied a numeric Retry-After value or the runtime applied its documented
+ fallback.
+ """
status_code: int | None = None
"""HTTP status code, when the failure was a rejected response."""
@@ -15335,14 +15596,17 @@ def from_dict(obj: Any) -> 'CatalogNetworkFailureError':
assert isinstance(obj, dict)
message = from_str(obj.get("message"))
reason = CatalogNetworkFailureReason(obj.get("reason"))
+ retry_after_seconds = from_union([from_int, from_none], obj.get("retryAfterSeconds"))
status_code = from_union([from_int, from_none], obj.get("statusCode"))
- return CatalogNetworkFailureError(message, reason, status_code)
+ return CatalogNetworkFailureError(message, reason, retry_after_seconds, status_code)
def to_dict(self) -> dict:
result: dict = {}
result["kind"] = self.kind
result["message"] = from_str(self.message)
result["reason"] = to_enum(CatalogNetworkFailureReason, self.reason)
+ if self.retry_after_seconds is not None:
+ result["retryAfterSeconds"] = from_union([from_int, from_none], self.retry_after_seconds)
if self.status_code is not None:
result["statusCode"] = from_union([from_int, from_none], self.status_code)
return result
@@ -16223,6 +16487,132 @@ def to_dict(self) -> dict:
result["plugin"] = from_union([lambda x: to_class(DiscoveredExtensionPlugin, x), from_none], self.plugin)
return result
+# Internal: this type is an internal SDK API and is not part of the public surface.
+@dataclass
+class _HookInvokeRequest:
+ """Runtime-owned wire payload for a server-to-client hook callback invocation."""
+
+ hook_type: HookType
+ input: Any
+ session_id: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> '_HookInvokeRequest':
+ assert isinstance(obj, dict)
+ hook_type = HookType(obj.get("hookType"))
+ input = obj.get("input")
+ session_id = from_str(obj.get("sessionId"))
+ return _HookInvokeRequest(hook_type, input, session_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["hookType"] = to_enum(HookType, self.hook_type)
+ result["input"] = self.input
+ result["sessionId"] = from_str(self.session_id)
+ return result
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class DiscoveredHook:
+ """One server-discovered hook action from user, repository, plugin, or managed-policy
+ configuration.
+ """
+ enabled: bool
+ """Whether this action is enabled under the server-side discovery settings. Concrete
+ sessions may differ because they can add session-specific directories, plugins, or trust.
+ False when its disable key is present in the user's disabled-hooks setting or disable-all
+ settings suppress the action.
+ """
+ hook_type: HookType
+ """Hook event that invokes this action."""
+
+ id: str
+ """Deterministic identifier for this server-discovered action row. It remains stable while
+ the project, origin, source, event, action content, and duplicate ordinal are unchanged.
+ This is row identity, not the key persisted in disabledHooks.
+ """
+ origin: HookOrigin
+ """Configuration tier that contributed this hook action."""
+
+ disable_key: str | None = None
+ """Durable content hash used by hook enablement. Identical actions may intentionally share
+ this key. Omitted when changing the user's disabled-hooks setting cannot change the
+ action's current server-discovered state, including managed-policy hooks, session-start
+ prompt actions, actions suppressed by disable-all settings, and projectless plugin
+ actions that require project-directory expansion.
+ """
+ project_path: str | None = None
+ """Input project path for which this server-side action was resolved. Set on every row
+ returned for project-scoped discovery, including repeated user and policy actions.
+ """
+ source: str | None = None
+ """Human-readable source label, such as a hook file path, settings source, or plugin name."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'DiscoveredHook':
+ assert isinstance(obj, dict)
+ enabled = from_bool(obj.get("enabled"))
+ hook_type = HookType(obj.get("hookType"))
+ id = from_str(obj.get("id"))
+ origin = HookOrigin(obj.get("origin"))
+ disable_key = from_union([from_str, from_none], obj.get("disableKey"))
+ project_path = from_union([from_str, from_none], obj.get("projectPath"))
+ source = from_union([from_str, from_none], obj.get("source"))
+ return DiscoveredHook(enabled, hook_type, id, origin, disable_key, project_path, source)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["enabled"] = from_bool(self.enabled)
+ result["hookType"] = to_enum(HookType, self.hook_type)
+ result["id"] = from_str(self.id)
+ result["origin"] = to_enum(HookOrigin, self.origin)
+ if self.disable_key is not None:
+ result["disableKey"] = from_union([from_str, from_none], self.disable_key)
+ if self.project_path is not None:
+ result["projectPath"] = from_union([from_str, from_none], self.project_path)
+ if self.source is not None:
+ result["source"] = from_union([from_str, from_none], self.source)
+ return result
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionsReadPersistedEventsRequest:
+ """Pagination options for reading an inactive or active local session's persisted event
+ journal.
+ """
+ session_id: str
+ """Session ID whose persisted event journal should be read."""
+
+ cursor: str | None = None
+ """Opaque cursor returned by a previous persisted-event read. Omit on the first call."""
+
+ direction: EventsReadDirection | None = None
+ """Direction to page through persisted history. Forward starts at the beginning; backward
+ starts with the newest events. Events in each page remain chronological.
+ """
+ max: int | None = None
+ """Maximum number of events to return in this batch (1–1000, default 200)."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionsReadPersistedEventsRequest':
+ assert isinstance(obj, dict)
+ session_id = from_str(obj.get("sessionId"))
+ cursor = from_union([from_str, from_none], obj.get("cursor"))
+ direction = from_union([EventsReadDirection, from_none], obj.get("direction"))
+ max = from_union([from_int, from_none], obj.get("max"))
+ return SessionsReadPersistedEventsRequest(session_id, cursor, direction, max)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["sessionId"] = from_str(self.session_id)
+ if self.cursor is not None:
+ result["cursor"] = from_union([from_str, from_none], self.cursor)
+ if self.direction is not None:
+ result["direction"] = from_union([lambda x: to_enum(EventsReadDirection, x), from_none], self.direction)
+ if self.max is not None:
+ result["max"] = from_union([from_int, from_none], self.max)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class EventLogReadRequest:
@@ -16894,9 +17284,12 @@ class FactoryRunFailure:
Machine-readable factory run failure.
- Machine-readable failure details for an errored run.
+ Machine-readable failure details for a halted or errored run.
The run stopped because its usage accounting could not be completed.
+
+ The extension that owns the factory disconnected while the run was executing, so the host
+ halted it. The run's journaled subagent results are preserved so a resume can reuse them.
"""
run_id: str
"""Factory run identifier.
@@ -17498,30 +17891,6 @@ def to_dict(self) -> dict:
result["mode"] = to_enum(HistoryRewindMode, self.mode)
return result
-# Internal: this type is an internal SDK API and is not part of the public surface.
-@dataclass
-class _HookInvokeRequest:
- """Runtime-owned wire payload for a server-to-client hook callback invocation."""
-
- hook_type: _HookType
- input: Any
- session_id: str
-
- @staticmethod
- def from_dict(obj: Any) -> '_HookInvokeRequest':
- assert isinstance(obj, dict)
- hook_type = _HookType(obj.get("hookType"))
- input = obj.get("input")
- session_id = from_str(obj.get("sessionId"))
- return _HookInvokeRequest(hook_type, input, session_id)
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["hookType"] = to_enum(_HookType, self.hook_type)
- result["input"] = self.input
- result["sessionId"] = from_str(self.session_id)
- return result
-
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class InstalledPluginSource:
@@ -22278,6 +22647,11 @@ class QueuePendingItems:
kind: QueuePendingItemsKind
"""Whether this item is a queued user message or a queued slash command / model change"""
+ message_id: str | None = None
+ """Stable identity of the queued user message. Present for message rows and absent for slash
+ commands and model changes.
+ """
+
@staticmethod
def from_dict(obj: Any) -> 'QueuePendingItems':
assert isinstance(obj, dict)
@@ -22285,7 +22659,8 @@ def from_dict(obj: Any) -> 'QueuePendingItems':
display_text = from_str(obj.get("displayText"))
id = from_str(obj.get("id"))
kind = QueuePendingItemsKind(obj.get("kind"))
- return QueuePendingItems(agent_mode, display_text, id, kind)
+ message_id = from_union([from_str, from_none], obj.get("messageId"))
+ return QueuePendingItems(agent_mode, display_text, id, kind, message_id)
def to_dict(self) -> dict:
result: dict = {}
@@ -22293,6 +22668,8 @@ def to_dict(self) -> dict:
result["displayText"] = from_str(self.display_text)
result["id"] = from_str(self.id)
result["kind"] = to_enum(QueuePendingItemsKind, self.kind)
+ if self.message_id is not None:
+ result["messageId"] = from_union([from_str, from_none], self.message_id)
return result
# Experimental: this type is part of an experimental API and may change or be removed.
@@ -22302,10 +22679,8 @@ class _RegisterExtensionToolsParams:
"""Params to attach an extension loader's tools to a session."""
loader: Any
- """In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is
- excluded from the public SDK surface. When the CLI migrates to a process-separated SDK,
- extension discovery/launch moves entirely into the runtime — the CLI passes pure config
- (search paths, disabled ids) via SessionOptions instead.
+ """In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK
+ surface.
"""
session_id: str
"""Session to register extension tools on."""
@@ -22557,11 +22932,14 @@ class SandboxConfigUserPolicyNetwork:
"""Whether outbound network traffic is allowed at all."""
proxy: SandboxConfigUserPolicyNetworkProxy | None = None
- """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and
- cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS.
- Credentials go in the separate `username`/`password` fields. A credential-free http://
- loopback proxy URL is routed through the localhost proxy automatically; an https:// or
- authenticated loopback URL is used as-is.
+ """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint,
+ requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is
+ accepted and routed through the IPv4 gateway), and does not support proxy credentials.
+ macOS relies on applications honoring proxy environment variables. Windows also
+ configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's
+ networking stack. Configure supported credentials in the separate `username` and
+ `password` fields. A credential-free http:// loopback URL uses the localhost proxy form,
+ while an https:// or authenticated loopback URL uses the URL form.
"""
@staticmethod
@@ -23364,7 +23742,7 @@ def to_dict(self) -> dict:
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class AgentInfo:
- """Agent metadata, including identifiers, display details, source, tools, model, MCP
+ """Agent metadata, including identifiers, display details, source, tools, model, models, MCP
servers, skills, and file path.
The newly selected custom agent
@@ -23391,6 +23769,13 @@ class AgentInfo:
"""Authored preferred model id for this agent. Runtime model selection may choose a
different model; omitted means no authored preference.
"""
+ model_policy: AgentModelPolicy | None = None
+ """Whether authored models are preferences or required constraints."""
+
+ models: list[str] | None = None
+ """Authored preferred model ids for this agent, in priority order. Runtime model selection
+ chooses the first available model; omitted means no authored preference.
+ """
path: str | None = None
"""Absolute local file path of the agent definition. Only set for file-based agents loaded
from disk; remote agents do not have a path.
@@ -23422,13 +23807,15 @@ def from_dict(obj: Any) -> 'AgentInfo':
name = from_str(obj.get("name"))
mcp_servers = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("mcpServers"))
model = from_union([from_str, from_none], obj.get("model"))
+ model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy"))
+ models = from_union([lambda x: from_list(from_str, x), from_none], obj.get("models"))
path = from_union([from_str, from_none], obj.get("path"))
prompt = from_union([from_str, from_none], obj.get("prompt"))
skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("skills"))
source = from_union([AgentInfoSource, from_none], obj.get("source"))
tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools"))
user_invocable = from_union([from_bool, from_none], obj.get("userInvocable"))
- return AgentInfo(description, display_name, id, name, mcp_servers, model, path, prompt, skills, source, tools, user_invocable)
+ return AgentInfo(description, display_name, id, name, mcp_servers, model, model_policy, models, path, prompt, skills, source, tools, user_invocable)
def to_dict(self) -> dict:
result: dict = {}
@@ -23440,6 +23827,10 @@ def to_dict(self) -> dict:
result["mcpServers"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.mcp_servers)
if self.model is not None:
result["model"] = from_union([from_str, from_none], self.model)
+ if self.model_policy is not None:
+ result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy)
+ if self.models is not None:
+ result["models"] = from_union([lambda x: from_list(from_str, x), from_none], self.models)
if self.path is not None:
result["path"] = from_union([from_str, from_none], self.path)
if self.prompt is not None:
@@ -23507,11 +23898,15 @@ class SkillsInvokedSkill:
"""Unique identifier for the skill"""
path: str
- """Path to the SKILL.md file"""
-
+ """Path to the SKILL.md file, or an empty string for an SDK-provided skill without a
+ filesystem identity
+ """
allowed_tools: list[str] | None = None
"""Tools that should be auto-approved when this skill is active, captured at invocation time"""
+ disable_model_invocation: bool | None = None
+ """Whether model invocation was disabled when this skill was invoked"""
+
@staticmethod
def from_dict(obj: Any) -> 'SkillsInvokedSkill':
assert isinstance(obj, dict)
@@ -23520,7 +23915,8 @@ def from_dict(obj: Any) -> 'SkillsInvokedSkill':
name = from_str(obj.get("name"))
path = from_str(obj.get("path"))
allowed_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("allowedTools"))
- return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools)
+ disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation"))
+ return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools, disable_model_invocation)
def to_dict(self) -> dict:
result: dict = {}
@@ -23530,6 +23926,8 @@ def to_dict(self) -> dict:
result["path"] = from_str(self.path)
if self.allowed_tools is not None:
result["allowedTools"] = from_union([lambda x: from_list(from_str, x), from_none], self.allowed_tools)
+ if self.disable_model_invocation is not None:
+ result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation)
return result
# Experimental: this type is part of an experimental API and may change or be removed.
@@ -23570,6 +23968,29 @@ def to_dict(self) -> dict:
result["projectPath"] = from_union([from_str, from_none], self.project_path)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+# Internal: this type is an internal SDK API and is not part of the public surface.
+@dataclass
+class _SkillProviderListResult:
+ """Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to
+ 1024 descriptors and 1 MiB of aggregate metadata.
+ """
+ skills: list[SkillProviderDescriptor]
+ """Skill descriptors in provider order. Invocation names must be unique under
+ case-insensitive comparison.
+ """
+
+ @staticmethod
+ def from_dict(obj: Any) -> '_SkillProviderListResult':
+ assert isinstance(obj, dict)
+ skills = from_list(SkillProviderDescriptor.from_dict, obj.get("skills"))
+ return _SkillProviderListResult(skills)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["skills"] = from_list(lambda x: to_class(SkillProviderDescriptor, x), self.skills)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SlashCommandAddTimelineEntryResult:
@@ -23664,6 +24085,9 @@ class SlashCommandCompletedResult:
message: str | None = None
"""Optional user-facing message describing the completed command"""
+ mode: SessionMode | None = None
+ """Optional target session mode applied without submitting an agent prompt"""
+
runtime_settings_changed: bool | None = None
"""True when the invocation mutated user runtime settings; consumers caching settings should
refresh
@@ -23673,14 +24097,17 @@ class SlashCommandCompletedResult:
def from_dict(obj: Any) -> 'SlashCommandCompletedResult':
assert isinstance(obj, dict)
message = from_union([from_str, from_none], obj.get("message"))
+ mode = from_union([SessionMode, from_none], obj.get("mode"))
runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged"))
- return SlashCommandCompletedResult(message, runtime_settings_changed)
+ return SlashCommandCompletedResult(message, mode, runtime_settings_changed)
def to_dict(self) -> dict:
result: dict = {}
result["kind"] = self.kind
if self.message is not None:
result["message"] = from_union([from_str, from_none], self.message)
+ if self.mode is not None:
+ result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode)
if self.runtime_settings_changed is not None:
result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed)
return result
@@ -25495,6 +25922,43 @@ def to_dict(self) -> dict:
result["mode"] = to_enum(DiscoveredExtensionMode, self.mode)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class HooksDiscoverResult:
+ """Server-discovered hook actions and partial-load diagnostics from user, repository,
+ plugin, and managed-policy sources. Concrete sessions may include additional
+ session-specific hook sources.
+ """
+ errors: list[str]
+ """Errors for hook sources or actions that could not be loaded, making the result partially
+ incomplete. Other valid actions are still returned. Project-resolution and
+ repository-settings errors are prefixed with their project path.
+ """
+ hooks: list[DiscoveredHook]
+ """All discovered hook actions. Byte-identical actions remain separate rows even when they
+ share a disable key.
+ """
+ warnings: list[str]
+ """Non-fatal source-loading warnings. Discovery remains complete for the affected source,
+ although the source had a recoverable issue. Repository-settings warnings are prefixed
+ with their project path when attribution is available.
+ """
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'HooksDiscoverResult':
+ assert isinstance(obj, dict)
+ errors = from_list(from_str, obj.get("errors"))
+ hooks = from_list(DiscoveredHook.from_dict, obj.get("hooks"))
+ warnings = from_list(from_str, obj.get("warnings"))
+ return HooksDiscoverResult(errors, hooks, warnings)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["errors"] = from_list(from_str, self.errors)
+ result["hooks"] = from_list(lambda x: to_class(DiscoveredHook, x), self.hooks)
+ result["warnings"] = from_list(from_str, self.warnings)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class ExtensionList:
@@ -25967,11 +26431,15 @@ class FactoryRunResult:
status: FactoryRunStatus
"""Current or terminal factory run status."""
+ attempt: int | None = None
+ """One-based execution attempt represented by this envelope. Absent before the first attempt
+ starts or when returned by an older runtime.
+ """
error: str | None = None
"""Error message for an errored run."""
failure: FactoryRunFailure | None = None
- """Machine-readable failure details for an errored run."""
+ """Machine-readable failure details for a halted or errored run."""
reason: str | None = None
"""Reason for a halted or cancelled run."""
@@ -25987,17 +26455,20 @@ def from_dict(obj: Any) -> 'FactoryRunResult':
assert isinstance(obj, dict)
run_id = from_str(obj.get("runId"))
status = FactoryRunStatus(obj.get("status"))
+ attempt = from_union([from_int, from_none], obj.get("attempt"))
error = from_union([from_str, from_none], obj.get("error"))
failure = from_union([FactoryRunFailure.from_dict, from_none], obj.get("failure"))
reason = from_union([from_str, from_none], obj.get("reason"))
result = obj.get("result")
snapshot = obj.get("snapshot")
- return FactoryRunResult(run_id, status, error, failure, reason, result, snapshot)
+ return FactoryRunResult(run_id, status, attempt, error, failure, reason, result, snapshot)
def to_dict(self) -> dict:
result: dict = {}
result["runId"] = from_str(self.run_id)
result["status"] = to_enum(FactoryRunStatus, self.status)
+ if self.attempt is not None:
+ result["attempt"] = from_union([from_int, from_none], self.attempt)
if self.error is not None:
result["error"] = from_union([from_str, from_none], self.error)
if self.failure is not None:
@@ -30840,6 +31311,10 @@ class SessionOpenOptions:
enable_script_safety: bool | None = None
"""Whether shell-script safety heuristics are enabled."""
+ enable_skills: bool | None = None
+ """Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by
+ default.
+ """
enable_streaming: bool | None = None
"""Whether model responses stream as delta events."""
@@ -30870,6 +31345,14 @@ class SessionOpenOptions:
feature_flags: dict[str, bool] | None = None
"""Feature-flag values resolved by the host."""
+ # Internal: this field is an internal SDK API and is not part of the public surface.
+ has_skill_provider: bool | None = None
+ """Whether the requesting SDK session has a skill provider. The provider remains ephemeral
+ and is never persisted in session options or history. When enableSkills is false, it
+ remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw
+ sessions.open flows reject it because they cannot safely pre-register the callback
+ handler.
+ """
included_builtin_agents: list[str] | None = None
"""Built-in subagent names to include in this session. When specified, only these built-ins
are available, subject to runtime availability and exclusions. Custom agents with the
@@ -31018,6 +31501,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions':
enable_managed_settings = from_union([from_bool, from_none], obj.get("enableManagedSettings"))
enable_on_demand_instruction_discovery = from_union([from_bool, from_none], obj.get("enableOnDemandInstructionDiscovery"))
enable_script_safety = from_union([from_bool, from_none], obj.get("enableScriptSafety"))
+ enable_skills = from_union([from_bool, from_none], obj.get("enableSkills"))
enable_streaming = from_union([from_bool, from_none], obj.get("enableStreaming"))
env_value_mode = from_union([MCPSetEnvValueModeDetails, from_none], obj.get("envValueMode"))
events_log_directory = from_union([from_str, from_none], obj.get("eventsLogDirectory"))
@@ -31026,6 +31510,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions':
excluded_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("excludedTools"))
exp_assignments = obj.get("expAssignments")
feature_flags = from_union([lambda x: from_dict(from_bool, x), from_none], obj.get("featureFlags"))
+ has_skill_provider = from_union([from_bool, from_none], obj.get("hasSkillProvider"))
included_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinAgents"))
included_builtin_skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinSkills"))
installed_plugins = from_union([lambda x: from_list(InstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins"))
@@ -31062,7 +31547,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions':
verbosity = from_union([Verbosity, from_none], obj.get("verbosity"))
working_directory = from_union([from_str, from_none], obj.get("workingDirectory"))
working_directory_context = from_union([SessionContext.from_dict, from_none], obj.get("workingDirectoryContext"))
- return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context)
+ return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_skills, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, has_skill_provider, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context)
def to_dict(self) -> dict:
result: dict = {}
@@ -31116,6 +31601,8 @@ def to_dict(self) -> dict:
result["enableOnDemandInstructionDiscovery"] = from_union([from_bool, from_none], self.enable_on_demand_instruction_discovery)
if self.enable_script_safety is not None:
result["enableScriptSafety"] = from_union([from_bool, from_none], self.enable_script_safety)
+ if self.enable_skills is not None:
+ result["enableSkills"] = from_union([from_bool, from_none], self.enable_skills)
if self.enable_streaming is not None:
result["enableStreaming"] = from_union([from_bool, from_none], self.enable_streaming)
if self.env_value_mode is not None:
@@ -31132,6 +31619,8 @@ def to_dict(self) -> dict:
result["expAssignments"] = self.exp_assignments
if self.feature_flags is not None:
result["featureFlags"] = from_union([lambda x: from_dict(from_bool, x), from_none], self.feature_flags)
+ if self.has_skill_provider is not None:
+ result["hasSkillProvider"] = from_union([from_bool, from_none], self.has_skill_provider)
if self.included_builtin_agents is not None:
result["includedBuiltinAgents"] = from_union([lambda x: from_list(from_str, x), from_none], self.included_builtin_agents)
if self.included_builtin_skills is not None:
@@ -31279,8 +31768,9 @@ class SessionUpdateOptionsParams:
"""Whether to enable cross-session store writes and reads."""
enable_skills: bool | None = None
- """Whether to enable skill directory scanning and loading. Falls back to
- enableConfigDiscovery when unset.
+ """Whether skill loading is enabled. Explicit false disables every source, including a bound
+ SDK provider; changing the value invalidates the loaded skill snapshot. When omitted,
+ creation falls back to enableConfigDiscovery unless an SDK skill provider is registered.
"""
enable_streaming: bool | None = None
"""Whether to stream model responses."""
@@ -32942,7 +33432,7 @@ class MCPOauthProbeResult:
status: Status
"""Probe outcome variant discriminator."""
- http_response: ExternalRefMCPOauthHTTPResponse | None = None
+ http_response: McpOauthHttpResponse | None = None
"""HTTP response returned by the server.
HTTP 401 or 403 response returned by the server.
@@ -32963,7 +33453,7 @@ class MCPOauthProbeResult:
def from_dict(obj: Any) -> 'MCPOauthProbeResult':
assert isinstance(obj, dict)
status = Status(obj.get("status"))
- http_response = from_union([ExternalRefMCPOauthHTTPResponse.from_dict, from_none], obj.get("httpResponse"))
+ http_response = from_union([McpOauthHttpResponse.from_dict, from_none], obj.get("httpResponse"))
reason = from_union([MCPOauthProbeNeedsAuthReason, from_none], obj.get("reason"))
www_authenticate_params = from_union([McpOauthWWWAuthenticateParams.from_dict, from_none], obj.get("wwwAuthenticateParams"))
error = from_union([from_str, from_none], obj.get("error"))
@@ -32973,7 +33463,7 @@ def to_dict(self) -> dict:
result: dict = {}
result["status"] = to_enum(Status, self.status)
if self.http_response is not None:
- result["httpResponse"] = from_union([lambda x: to_class(ExternalRefMCPOauthHTTPResponse, x), from_none], self.http_response)
+ result["httpResponse"] = from_union([lambda x: to_class(McpOauthHttpResponse, x), from_none], self.http_response)
if self.reason is not None:
result["reason"] = from_union([lambda x: to_enum(MCPOauthProbeNeedsAuthReason, x), from_none], self.reason)
if self.www_authenticate_params is not None:
@@ -33891,6 +34381,11 @@ class ModelApplyStartupOverlayRequest:
device_managed_model: str | None = None
"""Model required by device-managed policy, when configured."""
+ policy_helper_model: str | None = None
+ """Startup default model from the enterprise policy helper, when configured. Weakest of the
+ managed sources: it applies only when neither device nor server policy names a model, and
+ an explicit user selection still wins.
+ """
repo_context_tier: str | None = None
"""Context tier selected by repository settings, when configured."""
@@ -33909,11 +34404,12 @@ def from_dict(obj: Any) -> 'ModelApplyStartupOverlayRequest':
cli_model = from_union([from_str, from_none], obj.get("cliModel"))
deferred_resume = from_union([from_bool, from_none], obj.get("deferredResume"))
device_managed_model = from_union([from_str, from_none], obj.get("deviceManagedModel"))
+ policy_helper_model = from_union([from_str, from_none], obj.get("policyHelperModel"))
repo_context_tier = from_union([from_str, from_none], obj.get("repoContextTier"))
repo_model = from_union([from_str, from_none], obj.get("repoModel"))
repo_reasoning_effort = from_union([from_str, from_none], obj.get("repoReasoningEffort"))
server_managed_model = from_union([from_str, from_none], obj.get("serverManagedModel"))
- return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model)
+ return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, policy_helper_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model)
def to_dict(self) -> dict:
result: dict = {}
@@ -33923,6 +34419,8 @@ def to_dict(self) -> dict:
result["deferredResume"] = from_union([from_bool, from_none], self.deferred_resume)
if self.device_managed_model is not None:
result["deviceManagedModel"] = from_union([from_str, from_none], self.device_managed_model)
+ if self.policy_helper_model is not None:
+ result["policyHelperModel"] = from_union([from_str, from_none], self.policy_helper_model)
if self.repo_context_tier is not None:
result["repoContextTier"] = from_union([from_str, from_none], self.repo_context_tier)
if self.repo_model is not None:
@@ -34006,8 +34504,8 @@ class ModelSwitchToRequest:
"""When true, evaluate context-window compaction policy before applying the switch."""
source: ModelChangeSource | None = None
- """Origin to record on the effective `session.model_change` event. Defaults to `sdk` when
- omitted.
+ """Origin to record on the effective `session.model_change` event for trusted in-process
+ calls. Transport SDK calls are always recorded as `sdk`, regardless of this value.
"""
verbosity: Verbosity | None = None
"""Output verbosity level to request for supported models"""
@@ -34202,28 +34700,6 @@ def to_dict(self) -> dict:
result["title"] = from_union([from_str, from_none], self.title)
return result
-# Experimental: this type is part of an experimental API and may change or be removed.
-# Internal: this type is an internal SDK API and is not part of the public surface.
-@dataclass
-class _RegisterExtensionToolsResult:
- """Handle for releasing the extension tool registration."""
-
- unsubscribe: Any
- """In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an
- explicit `extensions.unregister` RPC in the SDK migration.
- """
-
- @staticmethod
- def from_dict(obj: Any) -> '_RegisterExtensionToolsResult':
- assert isinstance(obj, dict)
- unsubscribe = obj.get("unsubscribe")
- return _RegisterExtensionToolsResult(unsubscribe)
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["unsubscribe"] = self.unsubscribe
- return result
-
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionLimitPredictionDetails:
@@ -34484,10 +34960,8 @@ class SessionsOpenCloud:
# Internal: this field is an internal SDK API and is not part of the public surface.
on_task_created: Any = None
- """In-process callback invoked when the cloud task is created (before connection). Marked
- internal because a function reference cannot cross the JSON-RPC boundary. Disappears in
- the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from
- 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase.
+ """In-process callback invoked when the cloud task is created, before connection. Internal
+ because function references cannot cross the JSON-RPC boundary.
"""
options: SessionOpenOptions | None = None
"""Session options for cloud session creation."""
@@ -34756,13 +35230,17 @@ class SubagentSettingsEntry:
model: str | None = None
"""Model override for matching subagents"""
+ model_policy: AgentModelPolicy | None = None
+ """Whether the configured model strategy is preferred or required"""
+
@staticmethod
def from_dict(obj: Any) -> 'SubagentSettingsEntry':
assert isinstance(obj, dict)
context_tier = from_union([SubagentSettingsEntryContextTier, from_none], obj.get("contextTier"))
effort_level = from_union([from_str, from_none], obj.get("effortLevel"))
model = from_union([from_str, from_none], obj.get("model"))
- return SubagentSettingsEntry(context_tier, effort_level, model)
+ model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy"))
+ return SubagentSettingsEntry(context_tier, effort_level, model, model_policy)
def to_dict(self) -> dict:
result: dict = {}
@@ -34772,6 +35250,8 @@ def to_dict(self) -> dict:
result["effortLevel"] = from_union([from_str, from_none], self.effort_level)
if self.model is not None:
result["model"] = from_union([from_str, from_none], self.model)
+ if self.model_policy is not None:
+ result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy)
return result
# Experimental: this type is part of an experimental API and may change or be removed.
@@ -34949,44 +35429,6 @@ def to_dict(self) -> dict:
result["tools"] = from_list(lambda x: to_class(ProtocolExternalToolDefinition, x), self.tools)
return result
-# Experimental: this type is part of an experimental API and may change or be removed.
-@dataclass
-class UIEphemeralQueryRequest:
- """Transient question to answer without adding it to conversation history."""
-
- question: str
- """Question to answer from the current conversation context."""
-
- # Internal: this field is an internal SDK API and is not part of the public surface.
- abort_signal: Any = None
- """In-process `AbortSignal` forwarded to the model client to cancel an in-flight request.
- Marked internal: excluded from the public SDK surface. Replaced by an explicit
- cancellation token + cancel RPC in the SDK migration.
- """
- # Internal: this field is an internal SDK API and is not part of the public surface.
- on_chunk: Any = None
- """In-process streaming callback `(text) => void` invoked with each token as the model emits
- it. Marked internal: excluded from the public SDK surface. In a process-separated SDK
- this is replaced by a streaming RPC that yields chunks and a final answer.
- """
-
- @staticmethod
- def from_dict(obj: Any) -> 'UIEphemeralQueryRequest':
- assert isinstance(obj, dict)
- question = from_str(obj.get("question"))
- abort_signal = obj.get("abortSignal")
- on_chunk = obj.get("onChunk")
- return UIEphemeralQueryRequest(question, abort_signal, on_chunk)
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["question"] = from_str(self.question)
- if self.abort_signal is not None:
- result["abortSignal"] = self.abort_signal
- if self.on_chunk is not None:
- result["onChunk"] = self.on_chunk
- return result
-
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class UpdateSubagentSettingsRequest:
@@ -35226,6 +35668,7 @@ class RPC:
discovered_extensions_disable_request: DiscoveredExtensionsDisableRequest
discovered_extensions_enable_request: DiscoveredExtensionsEnableRequest
discovered_extension_source: DiscoveredExtensionSource
+ discovered_hook: DiscoveredHook
discovered_mcp_server: DiscoveredMCPServer
discovered_mcp_server_type: DiscoveredMCPServerType
enqueue_command_params: EnqueueCommandParams
@@ -35348,7 +35791,10 @@ class RPC:
hmac_auth_info: HMACAuthInfo
hook_invoke_request: _HookInvokeRequest
hook_invoke_response: _HookInvokeResponse
- hook_type: _HookType
+ hook_origin: HookOrigin
+ hooks_discover_request: HooksDiscoverRequest
+ hooks_discover_result: HooksDiscoverResult
+ hook_type: HookType
installed_plugin: InstalledPlugin
installed_plugin_info: InstalledPluginInfo
installed_plugin_source: InstalledPluginSource | str
@@ -35849,6 +36295,7 @@ class RPC:
sandbox_config_user_policy_network: SandboxConfigUserPolicyNetwork
sandbox_config_user_policy_network_proxy: SandboxConfigUserPolicyNetworkProxy
sandbox_config_user_policy_seatbelt: SandboxConfigUserPolicySeatbelt
+ sandbox_enforcement_status: SandboxEnforcementStatus
schedule_add_at_request: ScheduleAddAtRequest
schedule_add_cron_request: ScheduleAddCronRequest
schedule_add_request: ScheduleAddRequest
@@ -36020,6 +36467,7 @@ class RPC:
sessions_open_status: SessionsOpenStatus
session_source: SessionSource
sessions_prune_old_request: SessionsPruneOldRequest
+ sessions_read_persisted_events_request: SessionsReadPersistedEventsRequest
sessions_register_extension_tools_on_session_options: SessionsRegisterExtensionToolsOnSessionOptions
sessions_release_lock_request: SessionsReleaseLockRequest
sessions_release_lock_result: SessionsReleaseLockResult
@@ -36059,6 +36507,11 @@ class RPC:
skill_discovery_path_list: SkillDiscoveryPathList
skill_discovery_scope: SkillDiscoveryScope
skill_list: SkillList
+ skill_provider_descriptor: SkillProviderDescriptor
+ skill_provider_list_request: SkillProviderListRequest
+ skill_provider_list_result: _SkillProviderListResult
+ skill_provider_read_request: _SkillProviderReadRequest
+ skill_provider_read_result: _SkillProviderReadResult
skills_config_set_disabled_skills_request: SkillsConfigSetDisabledSkillsRequest
skills_config_set_skill_disabled_request: SkillsConfigSetSkillDisabledRequest
skills_disable_request: SkillsDisableRequest
@@ -36409,6 +36862,7 @@ def from_dict(obj: Any) -> 'RPC':
discovered_extensions_disable_request = DiscoveredExtensionsDisableRequest.from_dict(obj.get("DiscoveredExtensionsDisableRequest"))
discovered_extensions_enable_request = DiscoveredExtensionsEnableRequest.from_dict(obj.get("DiscoveredExtensionsEnableRequest"))
discovered_extension_source = DiscoveredExtensionSource(obj.get("DiscoveredExtensionSource"))
+ discovered_hook = DiscoveredHook.from_dict(obj.get("DiscoveredHook"))
discovered_mcp_server = DiscoveredMCPServer.from_dict(obj.get("DiscoveredMcpServer"))
discovered_mcp_server_type = DiscoveredMCPServerType(obj.get("DiscoveredMcpServerType"))
enqueue_command_params = EnqueueCommandParams.from_dict(obj.get("EnqueueCommandParams"))
@@ -36531,7 +36985,10 @@ def from_dict(obj: Any) -> 'RPC':
hmac_auth_info = HMACAuthInfo.from_dict(obj.get("HMACAuthInfo"))
hook_invoke_request = _HookInvokeRequest.from_dict(obj.get("HookInvokeRequest"))
hook_invoke_response = _HookInvokeResponse.from_dict(obj.get("HookInvokeResponse"))
- hook_type = _HookType(obj.get("HookType"))
+ hook_origin = HookOrigin(obj.get("HookOrigin"))
+ hooks_discover_request = HooksDiscoverRequest.from_dict(obj.get("HooksDiscoverRequest"))
+ hooks_discover_result = HooksDiscoverResult.from_dict(obj.get("HooksDiscoverResult"))
+ hook_type = HookType(obj.get("HookType"))
installed_plugin = InstalledPlugin.from_dict(obj.get("InstalledPlugin"))
installed_plugin_info = InstalledPluginInfo.from_dict(obj.get("InstalledPluginInfo"))
installed_plugin_source = from_union([InstalledPluginSource.from_dict, from_str], obj.get("InstalledPluginSource"))
@@ -37032,6 +37489,7 @@ def from_dict(obj: Any) -> 'RPC':
sandbox_config_user_policy_network = SandboxConfigUserPolicyNetwork.from_dict(obj.get("SandboxConfigUserPolicyNetwork"))
sandbox_config_user_policy_network_proxy = SandboxConfigUserPolicyNetworkProxy.from_dict(obj.get("SandboxConfigUserPolicyNetworkProxy"))
sandbox_config_user_policy_seatbelt = SandboxConfigUserPolicySeatbelt.from_dict(obj.get("SandboxConfigUserPolicySeatbelt"))
+ sandbox_enforcement_status = SandboxEnforcementStatus.from_dict(obj.get("SandboxEnforcementStatus"))
schedule_add_at_request = ScheduleAddAtRequest.from_dict(obj.get("ScheduleAddAtRequest"))
schedule_add_cron_request = ScheduleAddCronRequest.from_dict(obj.get("ScheduleAddCronRequest"))
schedule_add_request = ScheduleAddRequest.from_dict(obj.get("ScheduleAddRequest"))
@@ -37203,6 +37661,7 @@ def from_dict(obj: Any) -> 'RPC':
sessions_open_status = SessionsOpenStatus(obj.get("SessionsOpenStatus"))
session_source = SessionSource(obj.get("SessionSource"))
sessions_prune_old_request = SessionsPruneOldRequest.from_dict(obj.get("SessionsPruneOldRequest"))
+ sessions_read_persisted_events_request = SessionsReadPersistedEventsRequest.from_dict(obj.get("SessionsReadPersistedEventsRequest"))
sessions_register_extension_tools_on_session_options = SessionsRegisterExtensionToolsOnSessionOptions.from_dict(obj.get("SessionsRegisterExtensionToolsOnSessionOptions"))
sessions_release_lock_request = SessionsReleaseLockRequest.from_dict(obj.get("SessionsReleaseLockRequest"))
sessions_release_lock_result = SessionsReleaseLockResult.from_dict(obj.get("SessionsReleaseLockResult"))
@@ -37242,6 +37701,11 @@ def from_dict(obj: Any) -> 'RPC':
skill_discovery_path_list = SkillDiscoveryPathList.from_dict(obj.get("SkillDiscoveryPathList"))
skill_discovery_scope = SkillDiscoveryScope(obj.get("SkillDiscoveryScope"))
skill_list = SkillList.from_dict(obj.get("SkillList"))
+ skill_provider_descriptor = SkillProviderDescriptor.from_dict(obj.get("SkillProviderDescriptor"))
+ skill_provider_list_request = SkillProviderListRequest.from_dict(obj.get("SkillProviderListRequest"))
+ skill_provider_list_result = _SkillProviderListResult.from_dict(obj.get("SkillProviderListResult"))
+ skill_provider_read_request = _SkillProviderReadRequest.from_dict(obj.get("SkillProviderReadRequest"))
+ skill_provider_read_result = _SkillProviderReadResult.from_dict(obj.get("SkillProviderReadResult"))
skills_config_set_disabled_skills_request = SkillsConfigSetDisabledSkillsRequest.from_dict(obj.get("SkillsConfigSetDisabledSkillsRequest"))
skills_config_set_skill_disabled_request = SkillsConfigSetSkillDisabledRequest.from_dict(obj.get("SkillsConfigSetSkillDisabledRequest"))
skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest"))
@@ -37410,7 +37874,7 @@ def from_dict(obj: Any) -> 'RPC':
subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings"))
task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress"))
workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary"))
- return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary)
+ return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary)
def to_dict(self) -> dict:
result: dict = {}
@@ -37592,6 +38056,7 @@ def to_dict(self) -> dict:
result["DiscoveredExtensionsDisableRequest"] = to_class(DiscoveredExtensionsDisableRequest, self.discovered_extensions_disable_request)
result["DiscoveredExtensionsEnableRequest"] = to_class(DiscoveredExtensionsEnableRequest, self.discovered_extensions_enable_request)
result["DiscoveredExtensionSource"] = to_enum(DiscoveredExtensionSource, self.discovered_extension_source)
+ result["DiscoveredHook"] = to_class(DiscoveredHook, self.discovered_hook)
result["DiscoveredMcpServer"] = to_class(DiscoveredMCPServer, self.discovered_mcp_server)
result["DiscoveredMcpServerType"] = to_enum(DiscoveredMCPServerType, self.discovered_mcp_server_type)
result["EnqueueCommandParams"] = to_class(EnqueueCommandParams, self.enqueue_command_params)
@@ -37714,7 +38179,10 @@ def to_dict(self) -> dict:
result["HMACAuthInfo"] = to_class(HMACAuthInfo, self.hmac_auth_info)
result["HookInvokeRequest"] = to_class(_HookInvokeRequest, self.hook_invoke_request)
result["HookInvokeResponse"] = to_class(_HookInvokeResponse, self.hook_invoke_response)
- result["HookType"] = to_enum(_HookType, self.hook_type)
+ result["HookOrigin"] = to_enum(HookOrigin, self.hook_origin)
+ result["HooksDiscoverRequest"] = to_class(HooksDiscoverRequest, self.hooks_discover_request)
+ result["HooksDiscoverResult"] = to_class(HooksDiscoverResult, self.hooks_discover_result)
+ result["HookType"] = to_enum(HookType, self.hook_type)
result["InstalledPlugin"] = to_class(InstalledPlugin, self.installed_plugin)
result["InstalledPluginInfo"] = to_class(InstalledPluginInfo, self.installed_plugin_info)
result["InstalledPluginSource"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str], self.installed_plugin_source)
@@ -38215,6 +38683,7 @@ def to_dict(self) -> dict:
result["SandboxConfigUserPolicyNetwork"] = to_class(SandboxConfigUserPolicyNetwork, self.sandbox_config_user_policy_network)
result["SandboxConfigUserPolicyNetworkProxy"] = to_class(SandboxConfigUserPolicyNetworkProxy, self.sandbox_config_user_policy_network_proxy)
result["SandboxConfigUserPolicySeatbelt"] = to_class(SandboxConfigUserPolicySeatbelt, self.sandbox_config_user_policy_seatbelt)
+ result["SandboxEnforcementStatus"] = to_class(SandboxEnforcementStatus, self.sandbox_enforcement_status)
result["ScheduleAddAtRequest"] = to_class(ScheduleAddAtRequest, self.schedule_add_at_request)
result["ScheduleAddCronRequest"] = to_class(ScheduleAddCronRequest, self.schedule_add_cron_request)
result["ScheduleAddRequest"] = to_class(ScheduleAddRequest, self.schedule_add_request)
@@ -38386,6 +38855,7 @@ def to_dict(self) -> dict:
result["SessionsOpenStatus"] = to_enum(SessionsOpenStatus, self.sessions_open_status)
result["SessionSource"] = to_enum(SessionSource, self.session_source)
result["SessionsPruneOldRequest"] = to_class(SessionsPruneOldRequest, self.sessions_prune_old_request)
+ result["SessionsReadPersistedEventsRequest"] = to_class(SessionsReadPersistedEventsRequest, self.sessions_read_persisted_events_request)
result["SessionsRegisterExtensionToolsOnSessionOptions"] = to_class(SessionsRegisterExtensionToolsOnSessionOptions, self.sessions_register_extension_tools_on_session_options)
result["SessionsReleaseLockRequest"] = to_class(SessionsReleaseLockRequest, self.sessions_release_lock_request)
result["SessionsReleaseLockResult"] = to_class(SessionsReleaseLockResult, self.sessions_release_lock_result)
@@ -38425,6 +38895,11 @@ def to_dict(self) -> dict:
result["SkillDiscoveryPathList"] = to_class(SkillDiscoveryPathList, self.skill_discovery_path_list)
result["SkillDiscoveryScope"] = to_enum(SkillDiscoveryScope, self.skill_discovery_scope)
result["SkillList"] = to_class(SkillList, self.skill_list)
+ result["SkillProviderDescriptor"] = to_class(SkillProviderDescriptor, self.skill_provider_descriptor)
+ result["SkillProviderListRequest"] = to_class(SkillProviderListRequest, self.skill_provider_list_request)
+ result["SkillProviderListResult"] = to_class(_SkillProviderListResult, self.skill_provider_list_result)
+ result["SkillProviderReadRequest"] = to_class(_SkillProviderReadRequest, self.skill_provider_read_request)
+ result["SkillProviderReadResult"] = to_class(_SkillProviderReadResult, self.skill_provider_read_result)
result["SkillsConfigSetDisabledSkillsRequest"] = to_class(SkillsConfigSetDisabledSkillsRequest, self.skills_config_set_disabled_skills_request)
result["SkillsConfigSetSkillDisabledRequest"] = to_class(SkillsConfigSetSkillDisabledRequest, self.skills_config_set_skill_disabled_request)
result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request)
@@ -39054,6 +39529,17 @@ def _patch_model_capabilities(data: dict) -> dict:
return data
+# Experimental: this API group is experimental and may change or be removed.
+class ServerHooksApi:
+ def __init__(self, client: "JsonRpcClient"):
+ self._client = client
+
+ async def discover(self, params: HooksDiscoverRequest, *, timeout: float | None = None) -> HooksDiscoverResult:
+ "Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources.\n\nArgs:\n params: Optional project paths and host-exclusion behavior for server-scoped hook discovery.\n\nReturns:\n Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources."
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ return HooksDiscoverResult.from_dict(await self._client.request("hooks.discover", params_dict, **_timeout_kwargs(timeout)))
+
+
# Experimental: this API group is experimental and may change or be removed.
class ServerModelsApi:
def __init__(self, client: "JsonRpcClient"):
@@ -39466,6 +39952,11 @@ async def list(self, params: SessionsListRequest, *, timeout: float | None = Non
params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
return SessionList.from_dict(await self._client.request("sessions.list", params_dict, **_timeout_kwargs(timeout)))
+ async def read_persisted_events(self, params: SessionsReadPersistedEventsRequest, *, timeout: float | None = None) -> EventsReadResult:
+ "Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session.\n\nArgs:\n params: Pagination options for reading an inactive or active local session's persisted event journal.\n\nReturns:\n Batch of session events returned by a read, with cursor and continuation metadata."
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ return EventsReadResult.from_dict(await self._client.request("sessions.readPersistedEvents", params_dict, **_timeout_kwargs(timeout)))
+
async def find_by_task_id(self, params: SessionsFindByTaskIDRequest, *, timeout: float | None = None) -> SessionsFindByTaskIDResult:
"Finds the local session bound to a GitHub task ID, if any.\n\nArgs:\n params: GitHub task ID to look up.\n\nReturns:\n ID of the local session bound to the given GitHub task, or omitted when none."
params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
@@ -39575,6 +40066,7 @@ class ServerRpc:
"""Typed server-scoped RPC methods."""
def __init__(self, client: "JsonRpcClient"):
self._client = client
+ self.hooks = ServerHooksApi(client)
self.models = ServerModelsApi(client)
self.tools = ServerToolsApi(client)
self.account = ServerAccountApi(client)
@@ -39601,7 +40093,7 @@ async def ping(self, params: PingRequest, *, timeout: float | None = None) -> Pi
return PingResult.from_dict(await self._client.request("ping", params_dict, **_timeout_kwargs(timeout)))
async def register_extension_launch_provider(self, *, timeout: float | None = None) -> None:
- "Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility.\n\n.. warning:: This API is experimental and may change or be removed in future versions."
+ "Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher.\n\n.. warning:: This API is experimental and may change or be removed in future versions."
await self._client.request("registerExtensionLaunchProvider", {}, **_timeout_kwargs(timeout))
@@ -39663,6 +40155,17 @@ async def _connect(self, params: _ConnectRequest, *, timeout: float | None = Non
return _ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout)))
+# Experimental: this API group is experimental and may change or be removed.
+class SandboxApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def get_enforcement_status(self, *, timeout: float | None = None) -> SandboxEnforcementStatus:
+ "Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session.\n\nReturns:\n Managed sandbox enforcement state for a session."
+ return SandboxEnforcementStatus.from_dict(await self._client.request("session.sandbox.getEnforcementStatus", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
# Experimental: this API group is experimental and may change or be removed.
class GitHubAuthApi:
def __init__(self, client: "JsonRpcClient", session_id: str):
@@ -41139,6 +41642,7 @@ class SessionRpc:
def __init__(self, client: "JsonRpcClient", session_id: str):
self._client = client
self._session_id = session_id
+ self.sandbox = SandboxApi(client, session_id)
self.git_hub_auth = GitHubAuthApi(client, session_id)
self.debug = DebugApi(client, session_id)
self.canvas = CanvasApi(client, session_id)
@@ -42030,6 +42534,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"DiscoveredExtensions",
"DiscoveredExtensionsDisableRequest",
"DiscoveredExtensionsEnableRequest",
+ "DiscoveredHook",
"DiscoveredMCPServer",
"DiscoveredMCPServerType",
"EnqueueCommandParams",
@@ -42061,7 +42566,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"ExtensionsApi",
"ExtensionsDisableRequest",
"ExtensionsEnableRequest",
- "ExternalRefMCPOauthHTTPResponse",
"ExternalToolResult",
"ExternalToolTextResultForLlm",
"ExternalToolTextResultForLlmBinaryResultsForLlm",
@@ -42174,6 +42678,10 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"HistorySummarizeForHandoffResult",
"HistoryTruncateRequest",
"HistoryTruncateResult",
+ "HookOrigin",
+ "HookType",
+ "HooksDiscoverRequest",
+ "HooksDiscoverResult",
"HooksHandler",
"Host",
"HostType",
@@ -42766,6 +43274,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"RemoteSessionMode",
"RemoteSessionRepository",
"RunOptions",
+ "SandboxApi",
"SandboxConfig",
"SandboxConfigAuth",
"SandboxConfigUserPolicy",
@@ -42775,6 +43284,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"SandboxConfigUserPolicyNetwork",
"SandboxConfigUserPolicyNetworkProxy",
"SandboxConfigUserPolicySeatbelt",
+ "SandboxEnforcementStatus",
"Saved",
"ScheduleAddAtRequest",
"ScheduleAddCronRequest",
@@ -42806,6 +43316,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"ServerCatalogApi",
"ServerCommandsApi",
"ServerExtensionsApi",
+ "ServerHooksApi",
"ServerInstructionSourceList",
"ServerInstructionsApi",
"ServerLlmInferenceApi",
@@ -42991,6 +43502,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"SessionsOpenResumeLastKind",
"SessionsOpenStatus",
"SessionsPruneOldRequest",
+ "SessionsReadPersistedEventsRequest",
"SessionsRegisterExtensionToolsOnSessionOptions",
"SessionsReleaseLockRequest",
"SessionsReleaseLockResult",
@@ -43027,6 +43539,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None:
"SkillDiscoveryPathList",
"SkillDiscoveryScope",
"SkillList",
+ "SkillProviderDescriptor",
+ "SkillProviderListRequest",
"SkillsApi",
"SkillsConfigSetDisabledSkillsRequest",
"SkillsConfigSetSkillDisabledRequest",
diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py
index c22f9f24fc..51e719bcf9 100644
--- a/python/copilot/generated/session_events.py
+++ b/python/copilot/generated/session_events.py
@@ -137,6 +137,7 @@ class SessionEventType(Enum):
SESSION_WARNING = "session.warning"
SESSION_MODEL_CHANGE = "session.model_change"
SESSION_MODE_CHANGED = "session.mode_changed"
+ SESSION_MODE_NOTICE_DELIVERED = "session.mode_notice_delivered"
SESSION_SESSION_LIMITS_CHANGED = "session.session_limits_changed"
# Experimental: this event is part of an experimental API and may change or be removed.
SESSION_PERMISSIONS_CHANGED = "session.permissions_changed"
@@ -155,6 +156,8 @@ class SessionEventType(Enum):
SESSION_COMPACTION_COMPLETE = "session.compaction_complete"
SESSION_TASK_COMPLETE = "session.task_complete"
# Experimental: this event is part of an experimental API and may change or be removed.
+ SESSION_COMPLETION_RECEIPT = "session.completion_receipt"
+ # Experimental: this event is part of an experimental API and may change or be removed.
SESSION_FUSION_ROUTE_STARTED = "session.fusion_route_started"
# Experimental: this event is part of an experimental API and may change or be removed.
SESSION_FUSION_ROUTE_FAILED = "session.fusion_route_failed"
@@ -171,6 +174,8 @@ class SessionEventType(Enum):
# Experimental: this event is part of an experimental API and may change or be removed.
ASSISTANT_FUSION_PHASE_STARTED = "assistant.fusion_phase_started"
# Experimental: this event is part of an experimental API and may change or be removed.
+ ASSISTANT_FUSION_PHASE_ACTIVITY = "assistant.fusion_phase_activity"
+ # Experimental: this event is part of an experimental API and may change or be removed.
ASSISTANT_FUSION_PHASE_COMPLETED = "assistant.fusion_phase_completed"
# Experimental: this event is part of an experimental API and may change or be removed.
ASSISTANT_FUSION_PHASE_FAILED = "assistant.fusion_phase_failed"
@@ -397,6 +402,60 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class AssistantFusionPhaseActivityData:
+ "Experimental content-safe activity signal for a running HydraFusion phase."
+ activity: FusionPhaseActivityKind
+ conversation_scope: FusionConversationScope
+ fusion_id: str
+ pattern: FusionPattern
+ phase_id: str
+ phase_kind: FusionPhaseKind
+ role: str
+ tool_call_id: str | None = None
+ total_response_size_bytes: int | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> "AssistantFusionPhaseActivityData":
+ assert isinstance(obj, dict)
+ activity = parse_enum(FusionPhaseActivityKind, obj.get("activity"))
+ conversation_scope = parse_enum(FusionConversationScope, obj.get("conversationScope"))
+ fusion_id = from_str(obj.get("fusionId"))
+ pattern = parse_enum(FusionPattern, obj.get("pattern"))
+ phase_id = from_str(obj.get("phaseId"))
+ phase_kind = parse_enum(FusionPhaseKind, obj.get("phaseKind"))
+ role = from_str(obj.get("role"))
+ tool_call_id = from_union([from_none, from_str], obj.get("toolCallId"))
+ total_response_size_bytes = from_union([from_none, from_int], obj.get("totalResponseSizeBytes"))
+ return AssistantFusionPhaseActivityData(
+ activity=activity,
+ conversation_scope=conversation_scope,
+ fusion_id=fusion_id,
+ pattern=pattern,
+ phase_id=phase_id,
+ phase_kind=phase_kind,
+ role=role,
+ tool_call_id=tool_call_id,
+ total_response_size_bytes=total_response_size_bytes,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["activity"] = to_enum(FusionPhaseActivityKind, self.activity)
+ result["conversationScope"] = to_enum(FusionConversationScope, self.conversation_scope)
+ result["fusionId"] = from_str(self.fusion_id)
+ result["pattern"] = to_enum(FusionPattern, self.pattern)
+ result["phaseId"] = from_str(self.phase_id)
+ result["phaseKind"] = to_enum(FusionPhaseKind, self.phase_kind)
+ result["role"] = from_str(self.role)
+ if self.tool_call_id is not None:
+ result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id)
+ if self.total_response_size_bytes is not None:
+ result["totalResponseSizeBytes"] = from_union([from_none, to_int], self.total_response_size_bytes)
+ return result
+
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class AssistantFusionPhaseCompletedData:
@@ -1201,6 +1260,38 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class FusionPhasePlanStep:
+ "Presentation-neutral phase planned for a HydraFusion turn."
+ conditional: bool
+ kind: FusionPhaseKind
+ role: str
+ scope: FusionConversationScope
+
+ @staticmethod
+ def from_dict(obj: Any) -> "FusionPhasePlanStep":
+ assert isinstance(obj, dict)
+ conditional = from_bool(obj.get("conditional"))
+ kind = parse_enum(FusionPhaseKind, obj.get("kind"))
+ role = from_str(obj.get("role"))
+ scope = parse_enum(FusionConversationScope, obj.get("scope"))
+ return FusionPhasePlanStep(
+ conditional=conditional,
+ kind=kind,
+ role=role,
+ scope=scope,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["conditional"] = from_bool(self.conditional)
+ result["kind"] = to_enum(FusionPhaseKind, self.kind)
+ result["role"] = from_str(self.role)
+ result["scope"] = to_enum(FusionConversationScope, self.scope)
+ return result
+
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class FusionPhaseUsage:
@@ -1677,6 +1768,55 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionCompletionReceiptData:
+ "Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted."
+ attempt: int
+ event_range: CompletionReceiptEventRange
+ failed_tool_count: int
+ schema_version: int
+ source_event_id: str
+ stop_reason: CompletionReceiptStopReason
+ successful_tool_count: int
+ final_tool: CompletionReceiptFinalTool | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> "SessionCompletionReceiptData":
+ assert isinstance(obj, dict)
+ attempt = from_int(obj.get("attempt"))
+ event_range = CompletionReceiptEventRange.from_dict(obj.get("eventRange"))
+ failed_tool_count = from_int(obj.get("failedToolCount"))
+ schema_version = from_int(obj.get("schemaVersion"))
+ source_event_id = from_str(obj.get("sourceEventId"))
+ stop_reason = parse_enum(CompletionReceiptStopReason, obj.get("stopReason"))
+ successful_tool_count = from_int(obj.get("successfulToolCount"))
+ final_tool = from_union([from_none, CompletionReceiptFinalTool.from_dict], obj.get("finalTool"))
+ return SessionCompletionReceiptData(
+ attempt=attempt,
+ event_range=event_range,
+ failed_tool_count=failed_tool_count,
+ schema_version=schema_version,
+ source_event_id=source_event_id,
+ stop_reason=stop_reason,
+ successful_tool_count=successful_tool_count,
+ final_tool=final_tool,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["attempt"] = to_int(self.attempt)
+ result["eventRange"] = to_class(CompletionReceiptEventRange, self.event_range)
+ result["failedToolCount"] = to_int(self.failed_tool_count)
+ result["schemaVersion"] = to_int(self.schema_version)
+ result["sourceEventId"] = from_str(self.source_event_id)
+ result["stopReason"] = to_enum(CompletionReceiptStopReason, self.stop_reason)
+ result["successfulToolCount"] = to_int(self.successful_tool_count)
+ if self.final_tool is not None:
+ result["finalTool"] = from_union([from_none, lambda x: to_class(CompletionReceiptFinalTool, x)], self.final_tool)
+ return result
+
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionFusionCompletedData:
@@ -1782,6 +1922,8 @@ class SessionFusionResolvedData:
turn_id: str
follow_up: FusionFollowUpRecommendation | None = None
model_universe_version: str | None = None
+ # Experimental: this field is part of an experimental API and may change or be removed.
+ phase_plan: list[FusionPhasePlanStep] | None = None
plan_version: str | None = None
policy_version: str | None = None
route_source: str | None = None
@@ -1806,6 +1948,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData":
turn_id = from_str(obj.get("turnId"))
follow_up = from_union([from_none, FusionFollowUpRecommendation.from_dict], obj.get("followUp"))
model_universe_version = from_union([from_none, from_str], obj.get("modelUniverseVersion"))
+ phase_plan = from_union([from_none, lambda x: from_list(FusionPhasePlanStep.from_dict, x)], obj.get("phasePlan"))
plan_version = from_union([from_none, from_str], obj.get("planVersion"))
policy_version = from_union([from_none, from_str], obj.get("policyVersion"))
route_source = from_union([from_none, from_str], obj.get("routeSource"))
@@ -1827,6 +1970,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData":
turn_id=turn_id,
follow_up=follow_up,
model_universe_version=model_universe_version,
+ phase_plan=phase_plan,
plan_version=plan_version,
policy_version=policy_version,
route_source=route_source,
@@ -1853,6 +1997,8 @@ def to_dict(self) -> dict:
result["followUp"] = from_union([from_none, lambda x: to_class(FusionFollowUpRecommendation, x)], self.follow_up)
if self.model_universe_version is not None:
result["modelUniverseVersion"] = from_union([from_none, from_str], self.model_universe_version)
+ if self.phase_plan is not None:
+ result["phasePlan"] = from_union([from_none, lambda x: from_list(lambda x: to_class(FusionPhasePlanStep, x), x)], self.phase_plan)
if self.plan_version is not None:
result["planVersion"] = from_union([from_none, from_str], self.plan_version)
if self.policy_version is not None:
@@ -1992,7 +2138,7 @@ def to_dict(self) -> dict:
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionManagedSettingsResolvedData:
- "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes."
+ "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes."
bypass_permissions_disabled: bool
device_managed: bool
fail_closed: bool
@@ -2001,6 +2147,7 @@ class SessionManagedSettingsResolvedData:
source: ManagedSettingsResolvedSource
client_managed: bool | None = None
permissions_allow_intersected: bool | None = None
+ policy_helper_managed: bool | None = None
sandbox_enabled_by_undetermined_policy: bool | None = None
settings: Any = None
@@ -2015,6 +2162,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData":
source = parse_enum(ManagedSettingsResolvedSource, obj.get("source"))
client_managed = from_union([from_none, from_bool], obj.get("clientManaged"))
permissions_allow_intersected = from_union([from_none, from_bool], obj.get("permissionsAllowIntersected"))
+ policy_helper_managed = from_union([from_none, from_bool], obj.get("policyHelperManaged"))
sandbox_enabled_by_undetermined_policy = from_union([from_none, from_bool], obj.get("sandboxEnabledByUndeterminedPolicy"))
settings = obj.get("settings")
return SessionManagedSettingsResolvedData(
@@ -2026,6 +2174,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData":
source=source,
client_managed=client_managed,
permissions_allow_intersected=permissions_allow_intersected,
+ policy_helper_managed=policy_helper_managed,
sandbox_enabled_by_undetermined_policy=sandbox_enabled_by_undetermined_policy,
settings=settings,
)
@@ -2042,6 +2191,8 @@ def to_dict(self) -> dict:
result["clientManaged"] = from_union([from_none, from_bool], self.client_managed)
if self.permissions_allow_intersected is not None:
result["permissionsAllowIntersected"] = from_union([from_none, from_bool], self.permissions_allow_intersected)
+ if self.policy_helper_managed is not None:
+ result["policyHelperManaged"] = from_union([from_none, from_bool], self.policy_helper_managed)
if self.sandbox_enabled_by_undetermined_policy is not None:
result["sandboxEnabledByUndeterminedPolicy"] = from_union([from_none, from_bool], self.sandbox_enabled_by_undetermined_policy)
if self.settings is not None:
@@ -2450,6 +2601,7 @@ class AssistantMessageToolRequest:
name: str
tool_call_id: str
arguments: Any = None
+ caller: AssistantMessageToolRequestCaller | None = None
intention_summary: str | None = None
mcp_server_name: str | None = None
mcp_tool_name: str | None = None
@@ -2462,6 +2614,7 @@ def from_dict(obj: Any) -> "AssistantMessageToolRequest":
name = from_str(obj.get("name"))
tool_call_id = from_str(obj.get("toolCallId"))
arguments = obj.get("arguments")
+ caller = from_union([from_none, AssistantMessageToolRequestCaller.from_dict], obj.get("caller"))
intention_summary = from_union([from_none, from_str], obj.get("intentionSummary"))
mcp_server_name = from_union([from_none, from_str], obj.get("mcpServerName"))
mcp_tool_name = from_union([from_none, from_str], obj.get("mcpToolName"))
@@ -2471,6 +2624,7 @@ def from_dict(obj: Any) -> "AssistantMessageToolRequest":
name=name,
tool_call_id=tool_call_id,
arguments=arguments,
+ caller=caller,
intention_summary=intention_summary,
mcp_server_name=mcp_server_name,
mcp_tool_name=mcp_tool_name,
@@ -2484,6 +2638,8 @@ def to_dict(self) -> dict:
result["toolCallId"] = from_str(self.tool_call_id)
if self.arguments is not None:
result["arguments"] = self.arguments
+ if self.caller is not None:
+ result["caller"] = from_union([from_none, lambda x: to_class(AssistantMessageToolRequestCaller, x)], self.caller)
if self.intention_summary is not None:
result["intentionSummary"] = from_union([from_none, from_str], self.intention_summary)
if self.mcp_server_name is not None:
@@ -2497,6 +2653,29 @@ def to_dict(self) -> dict:
return result
+@dataclass
+class AssistantMessageToolRequestCaller:
+ "Hosted program that requested this client tool call"
+ caller_id: str
+ type: AssistantMessageToolRequestCallerType
+
+ @staticmethod
+ def from_dict(obj: Any) -> "AssistantMessageToolRequestCaller":
+ assert isinstance(obj, dict)
+ caller_id = from_str(obj.get("callerId"))
+ type = parse_enum(AssistantMessageToolRequestCallerType, obj.get("type"))
+ return AssistantMessageToolRequestCaller(
+ caller_id=caller_id,
+ type=type,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["callerId"] = from_str(self.caller_id)
+ result["type"] = to_enum(AssistantMessageToolRequestCallerType, self.type)
+ return result
+
+
@dataclass
class AssistantReasoningData:
"Assistant reasoning content for timeline display with complete thinking text"
@@ -4075,9 +4254,65 @@ def to_dict(self) -> dict:
return result
+@dataclass
+class CompletionReceiptEventRange:
+ "Inclusive durable event range summarized by a completion receipt."
+ end_event_id: str
+ start_event_id: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> "CompletionReceiptEventRange":
+ assert isinstance(obj, dict)
+ end_event_id = from_str(obj.get("endEventId"))
+ start_event_id = from_str(obj.get("startEventId"))
+ return CompletionReceiptEventRange(
+ end_event_id=end_event_id,
+ start_event_id=start_event_id,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["endEventId"] = from_str(self.end_event_id)
+ result["startEventId"] = from_str(self.start_event_id)
+ return result
+
+
+@dataclass
+class CompletionReceiptFinalTool:
+ "Final structured tool completion in the covered event range."
+ status: CompletionReceiptToolStatus
+ tool_call_id: str
+ exit_code: int | None = None
+ tool_name: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> "CompletionReceiptFinalTool":
+ assert isinstance(obj, dict)
+ status = parse_enum(CompletionReceiptToolStatus, obj.get("status"))
+ tool_call_id = from_str(obj.get("toolCallId"))
+ exit_code = from_union([from_none, from_int], obj.get("exitCode"))
+ tool_name = from_union([from_none, from_str], obj.get("toolName"))
+ return CompletionReceiptFinalTool(
+ status=status,
+ tool_call_id=tool_call_id,
+ exit_code=exit_code,
+ tool_name=tool_name,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["status"] = to_enum(CompletionReceiptToolStatus, self.status)
+ result["toolCallId"] = from_str(self.tool_call_id)
+ if self.exit_code is not None:
+ result["exitCode"] = from_union([from_none, to_int], self.exit_code)
+ if self.tool_name is not None:
+ result["toolName"] = from_union([from_none, from_str], self.tool_name)
+ return result
+
+
@dataclass
class CustomAgentsUpdatedAgent:
- "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override."
+ "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration."
description: str
display_name: str
id: str
@@ -4086,6 +4321,8 @@ class CustomAgentsUpdatedAgent:
tools: list[str] | None
user_invocable: bool
model: str | None = None
+ model_policy: AgentModelPolicy | None = None
+ models: list[str] | None = None
@staticmethod
def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent":
@@ -4098,6 +4335,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent":
tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("tools"))
user_invocable = from_bool(obj.get("userInvocable"))
model = from_union([from_none, from_str], obj.get("model"))
+ model_policy = from_union([from_none, lambda x: parse_enum(AgentModelPolicy, x)], obj.get("modelPolicy"))
+ models = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("models"))
return CustomAgentsUpdatedAgent(
description=description,
display_name=display_name,
@@ -4107,6 +4346,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent":
tools=tools,
user_invocable=user_invocable,
model=model,
+ model_policy=model_policy,
+ models=models,
)
def to_dict(self) -> dict:
@@ -4120,6 +4361,10 @@ def to_dict(self) -> dict:
result["userInvocable"] = from_bool(self.user_invocable)
if self.model is not None:
result["model"] = from_union([from_none, from_str], self.model)
+ if self.model_policy is not None:
+ result["modelPolicy"] = from_union([from_none, lambda x: to_enum(AgentModelPolicy, x)], self.model_policy)
+ if self.models is not None:
+ result["models"] = from_union([from_none, lambda x: from_list(from_str, x)], self.models)
return result
@@ -7044,6 +7289,7 @@ class PermissionRequestedData:
"Permission request notification requiring client approval with request details"
permission_request: PermissionRequest
request_id: str
+ agent_mode: SessionMode | None = None
prompt_request: PermissionPromptRequest | None = None
resolved_by_hook: bool | None = None
risk_assessment: Any = None
@@ -7053,12 +7299,14 @@ def from_dict(obj: Any) -> "PermissionRequestedData":
assert isinstance(obj, dict)
permission_request = _load_PermissionRequest(obj.get("permissionRequest"))
request_id = from_str(obj.get("requestId"))
+ agent_mode = from_union([from_none, lambda x: parse_enum(SessionMode, x)], obj.get("agentMode"))
prompt_request = from_union([from_none, _load_PermissionPromptRequest], obj.get("promptRequest"))
resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook"))
risk_assessment = obj.get("riskAssessment")
return PermissionRequestedData(
permission_request=permission_request,
request_id=request_id,
+ agent_mode=agent_mode,
prompt_request=prompt_request,
resolved_by_hook=resolved_by_hook,
risk_assessment=risk_assessment,
@@ -7068,6 +7316,8 @@ def to_dict(self) -> dict:
result: dict = {}
result["permissionRequest"] = self.permission_request.to_dict()
result["requestId"] = from_str(self.request_id)
+ if self.agent_mode is not None:
+ result["agentMode"] = from_union([from_none, lambda x: to_enum(SessionMode, x)], self.agent_mode)
if self.prompt_request is not None:
result["promptRequest"] = from_union([from_none, lambda x: x.to_dict()], self.prompt_request)
if self.resolved_by_hook is not None:
@@ -8109,6 +8359,30 @@ def to_dict(self) -> dict:
return result
+@dataclass
+class SessionModeNoticeDeliveredData:
+ "Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume."
+ mode: SessionMode
+ content: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> "SessionModeNoticeDeliveredData":
+ assert isinstance(obj, dict)
+ mode = parse_enum(SessionMode, obj.get("mode"))
+ content = from_union([from_none, from_str], obj.get("content"))
+ return SessionModeNoticeDeliveredData(
+ mode=mode,
+ content=content,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["mode"] = to_enum(SessionMode, self.mode)
+ if self.content is not None:
+ result["content"] = from_union([from_none, from_str], self.content)
+ return result
+
+
@dataclass
class SessionModelChangeData:
"Model change details including previous and new model identifiers"
@@ -8222,6 +8496,7 @@ class SessionResumeData:
event_count: int
resume_time: datetime
already_in_use: bool | None = None
+ auto_tier: AutoTier | None = None
context: WorkingDirectoryContext | None = None
context_tier: ContextTier | None = None
continue_pending_work: bool | None = None
@@ -8240,6 +8515,7 @@ def from_dict(obj: Any) -> "SessionResumeData":
event_count = from_int(obj.get("eventCount"))
resume_time = from_datetime(obj.get("resumeTime"))
already_in_use = from_union([from_none, from_bool], obj.get("alreadyInUse"))
+ auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier"))
context = from_union([from_none, WorkingDirectoryContext.from_dict], obj.get("context"))
context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier"))
continue_pending_work = from_union([from_none, from_bool], obj.get("continuePendingWork"))
@@ -8255,6 +8531,7 @@ def from_dict(obj: Any) -> "SessionResumeData":
event_count=event_count,
resume_time=resume_time,
already_in_use=already_in_use,
+ auto_tier=auto_tier,
context=context,
context_tier=context_tier,
continue_pending_work=continue_pending_work,
@@ -8274,6 +8551,8 @@ def to_dict(self) -> dict:
result["resumeTime"] = to_datetime(self.resume_time)
if self.already_in_use is not None:
result["alreadyInUse"] = from_union([from_none, from_bool], self.already_in_use)
+ if self.auto_tier is not None:
+ result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier)
if self.context is not None:
result["context"] = from_union([from_none, lambda x: to_class(WorkingDirectoryContext, x)], self.context)
if self.context_tier is not None:
@@ -8566,6 +8845,7 @@ class SessionStartData:
start_time: datetime
version: int
already_in_use: bool | None = None
+ auto_tier: AutoTier | None = None
context: WorkingDirectoryContext | None = None
context_tier: ContextTier | None = None
detached_from_spawning_parent_session_id: str | None = None
@@ -8586,6 +8866,7 @@ def from_dict(obj: Any) -> "SessionStartData":
start_time = from_datetime(obj.get("startTime"))
version = from_int(obj.get("version"))
already_in_use = from_union([from_none, from_bool], obj.get("alreadyInUse"))
+ auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier"))
context = from_union([from_none, WorkingDirectoryContext.from_dict], obj.get("context"))
context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier"))
detached_from_spawning_parent_session_id = from_union([from_none, from_str], obj.get("detachedFromSpawningParentSessionId"))
@@ -8603,6 +8884,7 @@ def from_dict(obj: Any) -> "SessionStartData":
start_time=start_time,
version=version,
already_in_use=already_in_use,
+ auto_tier=auto_tier,
context=context,
context_tier=context_tier,
detached_from_spawning_parent_session_id=detached_from_spawning_parent_session_id,
@@ -8624,6 +8906,8 @@ def to_dict(self) -> dict:
result["version"] = to_int(self.version)
if self.already_in_use is not None:
result["alreadyInUse"] = from_union([from_none, from_bool], self.already_in_use)
+ if self.auto_tier is not None:
+ result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier)
if self.context is not None:
result["context"] = from_union([from_none, lambda x: to_class(WorkingDirectoryContext, x)], self.context)
if self.context_tier is not None:
@@ -9126,6 +9410,7 @@ class SkillInvokedData:
path: str
allowed_tools: list[str] | None = None
description: str | None = None
+ disable_model_invocation: bool | None = None
model: str | None = None
plugin_name: str | None = None
plugin_version: str | None = None
@@ -9140,6 +9425,7 @@ def from_dict(obj: Any) -> "SkillInvokedData":
path = from_str(obj.get("path"))
allowed_tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("allowedTools"))
description = from_union([from_none, from_str], obj.get("description"))
+ disable_model_invocation = from_union([from_none, from_bool], obj.get("disableModelInvocation"))
model = from_union([from_none, from_str], obj.get("model"))
plugin_name = from_union([from_none, from_str], obj.get("pluginName"))
plugin_version = from_union([from_none, from_str], obj.get("pluginVersion"))
@@ -9151,6 +9437,7 @@ def from_dict(obj: Any) -> "SkillInvokedData":
path=path,
allowed_tools=allowed_tools,
description=description,
+ disable_model_invocation=disable_model_invocation,
model=model,
plugin_name=plugin_name,
plugin_version=plugin_version,
@@ -9167,6 +9454,8 @@ def to_dict(self) -> dict:
result["allowedTools"] = from_union([from_none, lambda x: from_list(from_str, x)], self.allowed_tools)
if self.description is not None:
result["description"] = from_union([from_none, from_str], self.description)
+ if self.disable_model_invocation is not None:
+ result["disableModelInvocation"] = from_union([from_none, from_bool], self.disable_model_invocation)
if self.model is not None:
result["model"] = from_union([from_none, from_str], self.model)
if self.plugin_name is not None:
@@ -9244,6 +9533,7 @@ class SubagentCompletedData:
explicit_model_override: str | None = None
first_dispatched_model: str | None = None
model: str | None = None
+ model_override_reason: str | None = None
total_tokens: int | None = None
total_tool_calls: int | None = None
@@ -9261,6 +9551,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData":
explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride"))
first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel"))
model = from_union([from_none, from_str], obj.get("model"))
+ model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason"))
total_tokens = from_union([from_none, from_int], obj.get("totalTokens"))
total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls"))
return SubagentCompletedData(
@@ -9275,6 +9566,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData":
explicit_model_override=explicit_model_override,
first_dispatched_model=first_dispatched_model,
model=model,
+ model_override_reason=model_override_reason,
total_tokens=total_tokens,
total_tool_calls=total_tool_calls,
)
@@ -9300,6 +9592,8 @@ def to_dict(self) -> dict:
result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model)
if self.model is not None:
result["model"] = from_union([from_none, from_str], self.model)
+ if self.model_override_reason is not None:
+ result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason)
if self.total_tokens is not None:
result["totalTokens"] = from_union([from_none, to_int], self.total_tokens)
if self.total_tool_calls is not None:
@@ -9366,6 +9660,7 @@ class SubagentFailedData:
explicit_model_override: str | None = None
first_dispatched_model: str | None = None
model: str | None = None
+ model_override_reason: str | None = None
total_tokens: int | None = None
total_tool_calls: int | None = None
@@ -9383,6 +9678,7 @@ def from_dict(obj: Any) -> "SubagentFailedData":
explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride"))
first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel"))
model = from_union([from_none, from_str], obj.get("model"))
+ model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason"))
total_tokens = from_union([from_none, from_int], obj.get("totalTokens"))
total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls"))
return SubagentFailedData(
@@ -9397,6 +9693,7 @@ def from_dict(obj: Any) -> "SubagentFailedData":
explicit_model_override=explicit_model_override,
first_dispatched_model=first_dispatched_model,
model=model,
+ model_override_reason=model_override_reason,
total_tokens=total_tokens,
total_tool_calls=total_tool_calls,
)
@@ -9421,6 +9718,8 @@ def to_dict(self) -> dict:
result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model)
if self.model is not None:
result["model"] = from_union([from_none, from_str], self.model)
+ if self.model_override_reason is not None:
+ result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason)
if self.total_tokens is not None:
result["totalTokens"] = from_union([from_none, to_int], self.total_tokens)
if self.total_tool_calls is not None:
@@ -10949,6 +11248,7 @@ class UserMessageData:
delivery: UserMessageDelivery | None = None
interaction_id: str | None = None
is_autopilot_continuation: bool | None = None
+ message_id: str | None = None
native_document_path_fallback_paths: list[str] | None = None
parent_agent_task_id: str | None = None
source: str | None = None
@@ -10965,6 +11265,7 @@ def from_dict(obj: Any) -> "UserMessageData":
delivery = from_union([from_none, lambda x: parse_enum(UserMessageDelivery, x)], obj.get("delivery"))
interaction_id = from_union([from_none, from_str], obj.get("interactionId"))
is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation"))
+ message_id = from_union([from_none, from_str], obj.get("messageId"))
native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths"))
parent_agent_task_id = from_union([from_none, from_str], obj.get("parentAgentTaskId"))
source = from_union([from_none, from_str], obj.get("source"))
@@ -10978,6 +11279,7 @@ def from_dict(obj: Any) -> "UserMessageData":
delivery=delivery,
interaction_id=interaction_id,
is_autopilot_continuation=is_autopilot_continuation,
+ message_id=message_id,
native_document_path_fallback_paths=native_document_path_fallback_paths,
parent_agent_task_id=parent_agent_task_id,
source=source,
@@ -10999,6 +11301,8 @@ def to_dict(self) -> dict:
result["interactionId"] = from_union([from_none, from_str], self.interaction_id)
if self.is_autopilot_continuation is not None:
result["isAutopilotContinuation"] = from_union([from_none, from_bool], self.is_autopilot_continuation)
+ if self.message_id is not None:
+ result["messageId"] = from_union([from_none, from_str], self.message_id)
if self.native_document_path_fallback_paths is not None:
result["nativeDocumentPathFallbackPaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.native_document_path_fallback_paths)
if self.parent_agent_task_id is not None:
@@ -11522,6 +11826,17 @@ class FusionPattern(Enum):
CRITIQUE = "critique"
+# Experimental: this enum is part of an experimental API and may change or be removed.
+class FusionPhaseActivityKind(Enum):
+ "Content-safe activity observed while a HydraFusion phase is running."
+ # The provider produced additional private output bytes.
+ MODEL_OUTPUT = "model_output"
+ # A tool began executing inside the phase.
+ TOOL_STARTED = "tool_started"
+ # A tool finished executing inside the phase.
+ TOOL_COMPLETED = "tool_completed"
+
+
# Experimental: this enum is part of an experimental API and may change or be removed.
class FusionPhaseKind(Enum):
"HydraFusion phase kind."
@@ -11637,6 +11952,19 @@ class AgentInterruptedCancelPhase(Enum):
MID_STREAM = "mid_stream"
+class AgentModelPolicy(Enum):
+ "Whether configured models are advisory preferences or required constraints"
+ # Treat the authored models as advisory preferences that callers may override.
+ PREFERRED = "preferred"
+ # Require subagent execution to use one of the authored models.
+ REQUIRED = "required"
+
+
+class AssistantMessageToolRequestCallerType(Enum):
+ "Hosted program caller type"
+ PROGRAM = "program"
+
+
class AssistantMessageToolRequestType(Enum):
"Tool call type: \"function\" for standard tool calls, \"custom\" for grammar-based tool calls. Defaults to \"function\" when absent."
# Standard function-style tool call.
@@ -11695,6 +12023,16 @@ class AutoModeSwitchResponse(Enum):
NO = "no"
+class AutoTier(Enum):
+ "Routing preference used when the session model is `auto`."
+ # Optimize for efficiency.
+ EFFICIENCY = "efficiency"
+ # Balance efficiency and intelligence.
+ BALANCE = "balance"
+ # Optimize for intelligence.
+ INTELLIGENCE = "intelligence"
+
+
class AutopilotObjectiveChangedOperation(Enum):
"The type of operation performed on the autopilot objective state file"
# Autopilot objective state file was created for a new objective.
@@ -11747,6 +12085,30 @@ class CompactionTrigger(Enum):
MODEL_SWITCH = "model_switch"
+class CompletionReceiptStopReason(Enum):
+ "Runtime reason the completion decision was accepted."
+ # The model reached a natural terminal response.
+ NATURAL = "natural"
+ # A terminal tool ended the interaction.
+ TERMINAL_TOOL = "terminal_tool"
+ # The configured agentStop continuation limit was reached.
+ AGENT_STOP_BLOCK_LIMIT = "agent_stop_block_limit"
+
+
+class CompletionReceiptToolStatus(Enum):
+ "Structured terminal status from a tool completion event."
+ # The tool completed successfully.
+ SUCCESS = "success"
+ # The tool failed without a more specific structured status.
+ FAILURE = "failure"
+ # The tool exceeded its time budget.
+ TIMEOUT = "timeout"
+ # The user rejected the tool call.
+ REJECTED = "rejected"
+ # The permissions service denied the tool call.
+ DENIED = "denied"
+
+
class ContextTier(Enum):
"Allowed values for the `ContextTier` enumeration."
# Default context tier with standard context window size.
@@ -11867,7 +12229,9 @@ class ManagedSettingsResolvedSource(Enum):
DEVICE = "device"
# Only session-local SDK-host injection contributed.
CLIENT = "client"
- # More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers.
+ # A policy helper registered by device or server policy contributed. Device registration takes priority when present.
+ POLICY_HELPER = "policyHelper"
+ # More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers.
MIXED = "mixed"
# No managed policy is in force (no channel contributed).
NONE = "none"
@@ -12154,7 +12518,7 @@ class SkillInvokedTrigger(Enum):
class SkillSource(Enum):
- "Source location type (e.g., project, personal-copilot, plugin, builtin)"
+ "Source location type (e.g., project, personal-copilot, plugin, builtin, sdk)"
# Skill defined in the current project's skill directories.
PROJECT = "project"
# Skill discovered from a parent directory in the current workspace tree.
@@ -12169,6 +12533,8 @@ class SkillSource(Enum):
CUSTOM = "custom"
# Skill bundled with the runtime.
BUILTIN = "builtin"
+ # Pathless skill supplied lazily by an SDK skill provider.
+ SDK = "sdk"
class SystemMessageRole(Enum):
@@ -12281,7 +12647,7 @@ class WorkspaceFileChangedOperation(Enum):
UPDATE = "update"
-SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data
+SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data
@dataclass
@@ -12321,6 +12687,7 @@ def from_dict(obj: Any) -> "SessionEvent":
case SessionEventType.SESSION_WARNING: data = SessionWarningData.from_dict(data_obj)
case SessionEventType.SESSION_MODEL_CHANGE: data = SessionModelChangeData.from_dict(data_obj)
case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj)
+ case SessionEventType.SESSION_MODE_NOTICE_DELIVERED: data = SessionModeNoticeDeliveredData.from_dict(data_obj)
case SessionEventType.SESSION_SESSION_LIMITS_CHANGED: data = SessionSessionLimitsChangedData.from_dict(data_obj)
case SessionEventType.SESSION_PERMISSIONS_CHANGED: data = SessionPermissionsChangedData.from_dict(data_obj)
case SessionEventType.SESSION_PLAN_CHANGED: data = SessionPlanChangedData.from_dict(data_obj)
@@ -12337,6 +12704,7 @@ def from_dict(obj: Any) -> "SessionEvent":
case SessionEventType.SESSION_COMPACTION_START: data = SessionCompactionStartData.from_dict(data_obj)
case SessionEventType.SESSION_COMPACTION_COMPLETE: data = SessionCompactionCompleteData.from_dict(data_obj)
case SessionEventType.SESSION_TASK_COMPLETE: data = SessionTaskCompleteData.from_dict(data_obj)
+ case SessionEventType.SESSION_COMPLETION_RECEIPT: data = SessionCompletionReceiptData.from_dict(data_obj)
case SessionEventType.SESSION_FUSION_ROUTE_STARTED: data = SessionFusionRouteStartedData.from_dict(data_obj)
case SessionEventType.SESSION_FUSION_ROUTE_FAILED: data = SessionFusionRouteFailedData.from_dict(data_obj)
case SessionEventType.SESSION_FUSION_RESOLVED: data = SessionFusionResolvedData.from_dict(data_obj)
@@ -12348,6 +12716,7 @@ def from_dict(obj: Any) -> "SessionEvent":
case SessionEventType.AGENT_INTERRUPTED: data = AgentInterruptedData.from_dict(data_obj)
case SessionEventType.ASSISTANT_INTENT: data = AssistantIntentData.from_dict(data_obj)
case SessionEventType.ASSISTANT_FUSION_PHASE_STARTED: data = AssistantFusionPhaseStartedData.from_dict(data_obj)
+ case SessionEventType.ASSISTANT_FUSION_PHASE_ACTIVITY: data = AssistantFusionPhaseActivityData.from_dict(data_obj)
case SessionEventType.ASSISTANT_FUSION_PHASE_COMPLETED: data = AssistantFusionPhaseCompletedData.from_dict(data_obj)
case SessionEventType.ASSISTANT_FUSION_PHASE_FAILED: data = AssistantFusionPhaseFailedData.from_dict(data_obj)
case SessionEventType.ASSISTANT_SERVER_TOOL_PROGRESS: data = AssistantServerToolProgressData.from_dict(data_obj)
@@ -12476,6 +12845,8 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"AgentInterruptedActivity",
"AgentInterruptedCancelPhase",
"AgentInterruptedData",
+ "AgentModelPolicy",
+ "AssistantFusionPhaseActivityData",
"AssistantFusionPhaseCompletedData",
"AssistantFusionPhaseFailedData",
"AssistantFusionPhaseStartedData",
@@ -12487,6 +12858,8 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"AssistantMessageServerTools",
"AssistantMessageStartData",
"AssistantMessageToolRequest",
+ "AssistantMessageToolRequestCaller",
+ "AssistantMessageToolRequestCallerType",
"AssistantMessageToolRequestType",
"AssistantReasoningData",
"AssistantReasoningDeltaData",
@@ -12530,6 +12903,7 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"AutoModeSwitchCompletedData",
"AutoModeSwitchRequestedData",
"AutoModeSwitchResponse",
+ "AutoTier",
"AutopilotObjectiveChangedOperation",
"AutopilotObjectiveChangedStatus",
"BinaryAssetReference",
@@ -12557,6 +12931,10 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"CompactionCompleteCompactionTokensUsed",
"CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail",
"CompactionTrigger",
+ "CompletionReceiptEventRange",
+ "CompletionReceiptFinalTool",
+ "CompletionReceiptStopReason",
+ "CompletionReceiptToolStatus",
"ContextTier",
"CustomAgentsUpdatedAgent",
"Data",
@@ -12586,7 +12964,9 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"FusionFollowUpAction",
"FusionFollowUpRecommendation",
"FusionPattern",
+ "FusionPhaseActivityKind",
"FusionPhaseKind",
+ "FusionPhasePlanStep",
"FusionPhaseStatus",
"FusionPhaseUsage",
"FusionScores",
@@ -12712,6 +13092,7 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"SessionCanvasUnavailableData",
"SessionCompactionCompleteData",
"SessionCompactionStartData",
+ "SessionCompletionReceiptData",
"SessionContextChangedData",
"SessionContextClearedData",
"SessionCustomAgentsUpdatedData",
@@ -12740,6 +13121,7 @@ def session_event_to_dict(x: SessionEvent) -> Any:
"SessionMcpServersLoadedData",
"SessionMode",
"SessionModeChangedData",
+ "SessionModeNoticeDeliveredData",
"SessionModelChangeData",
"SessionPermissionsChangedData",
"SessionPlanChangedData",
diff --git a/python/copilot/session.py b/python/copilot/session.py
index 78afdde139..3c6d3d54a4 100644
--- a/python/copilot/session.py
+++ b/python/copilot/session.py
@@ -504,7 +504,7 @@ class McpAuthContext(TypedDict):
class UserInputRequest(TypedDict, total=False):
- """Request for user input from the agent (enables ask_user tool)"""
+ """Legacy question-and-answer request from the ask_user tool."""
question: str
choices: list[str]
diff --git a/python/e2e/_copilot_request_helpers.py b/python/e2e/_copilot_request_helpers.py
index 2d91bc9bc2..d4073dd197 100644
--- a/python/e2e/_copilot_request_helpers.py
+++ b/python/e2e/_copilot_request_helpers.py
@@ -58,8 +58,8 @@ def _wants_stream(body: bytes) -> bool:
def model_catalog(supported_endpoints: list[str] | None = None) -> dict:
"""The synthetic ``/models`` catalog payload."""
model: dict = {
- "id": "claude-sonnet-4.5",
- "name": "Claude Sonnet 4.5",
+ "id": "claude-sonnet-5",
+ "name": "Claude Sonnet 5",
"object": "model",
"vendor": "Anthropic",
"version": "1",
@@ -67,7 +67,7 @@ def model_catalog(supported_endpoints: list[str] | None = None) -> dict:
"model_picker_enabled": True,
"capabilities": {
"type": "chat",
- "family": "claude-sonnet-4.5",
+ "family": "claude-sonnet-5",
"tokenizer": "o200k_base",
"limits": {"max_context_window_tokens": 200000, "max_output_tokens": 8192},
"supports": {
@@ -191,7 +191,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT)
"id": "chatcmpl-stub-1",
"object": "chat.completion.chunk",
"created": 1,
- "model": "claude-sonnet-4.5",
+ "model": "claude-sonnet-5",
}
chunks = [
{
@@ -234,7 +234,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT)
"id": "msg_stub_1",
"type": "message",
"role": "assistant",
- "model": "claude-sonnet-4.5",
+ "model": "claude-sonnet-5",
"content": [],
"stop_reason": None,
"stop_sequence": None,
@@ -283,7 +283,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT)
"id": "msg_stub_1",
"type": "message",
"role": "assistant",
- "model": "claude-sonnet-4.5",
+ "model": "claude-sonnet-5",
"content": [{"type": "text", "text": text}],
"stop_reason": "end_turn",
"stop_sequence": None,
@@ -300,7 +300,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT)
"id": "chatcmpl-stub-1",
"object": "chat.completion",
"created": 1,
- "model": "claude-sonnet-4.5",
+ "model": "claude-sonnet-5",
"choices": [
{
"index": 0,
diff --git a/python/e2e/conftest.py b/python/e2e/conftest.py
index f441097f32..a789b2b567 100644
--- a/python/e2e/conftest.py
+++ b/python/e2e/conftest.py
@@ -1,10 +1,14 @@
"""Shared pytest fixtures for e2e tests."""
+import json
import os
+from pathlib import Path
import pytest
import pytest_asyncio
+import copilot._cli_download as cli_download
+
from .testharness import E2ETestContext, is_inprocess_transport
# Host-side auth resolution ranks HMAC above the GitHub token, so an ambient
@@ -15,9 +19,16 @@
# .NET's InProcessEnvIsolation [ModuleInitializer] and Node's module-init guard.
# Out-of-process children resolve auth in their own process where the token already
# outranks HMAC. See https://github.com/github/copilot-sdk/issues/1934.
+if not cli_download.CLI_VERSION:
+ package_lock = json.loads(
+ (Path(__file__).parents[2] / "nodejs" / "package-lock.json").read_text()
+ )
+ cli_download.CLI_VERSION = package_lock["packages"]["node_modules/@github/copilot"]["version"]
+
if is_inprocess_transport():
os.environ.pop("COPILOT_HMAC_KEY", None)
os.environ.pop("CAPI_HMAC_KEY", None)
+ os.environ.pop("COPILOT_CLI_PATH", None)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py
index fe1ed54820..4bceabcb5e 100644
--- a/python/e2e/test_client_options_e2e.py
+++ b/python/e2e/test_client_options_e2e.py
@@ -396,7 +396,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request(
await client.start()
session = await client.create_session(
client_name="advanced-create-client",
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
reasoning_effort="medium",
reasoning_summary="detailed",
context_tier="long_context",
@@ -470,7 +470,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request(
"provider": "create-provider",
"id": "create-model",
"name": "Create Model",
- "model_id": "claude-sonnet-4.5",
+ "model_id": "claude-sonnet-5",
"wire_model": "create-wire-model",
"max_context_window_tokens": 12_000,
"max_prompt_tokens": 10_000,
@@ -482,7 +482,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request(
try:
params = _get_captured_request(capture_path, "session.create")
assert params["clientName"] == "advanced-create-client"
- assert params["model"] == "claude-sonnet-4.5"
+ assert params["model"] == "claude-sonnet-5"
assert params["reasoningEffort"] == "medium"
assert params["reasoningSummary"] == "detailed"
assert params["contextTier"] == "long_context"
@@ -538,7 +538,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request(
try:
await client.start()
session = await client.create_session(
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
provider={
"type": "azure",
"wire_api": "responses",
@@ -548,7 +548,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request(
"bearer_token": "provider-bearer-token",
"azure": {"api_version": "2024-02-15-preview"},
"headers": {"X-Provider-Wire": "yes"},
- "model_id": "claude-sonnet-4.5",
+ "model_id": "claude-sonnet-5",
"wire_model": "azure-deployment",
"max_prompt_tokens": 8192,
"max_output_tokens": 1024,
@@ -565,7 +565,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request(
assert provider["bearerToken"] == "provider-bearer-token"
assert provider["azure"]["apiVersion"] == "2024-02-15-preview"
assert provider["headers"]["X-Provider-Wire"] == "yes"
- assert provider["modelId"] == "claude-sonnet-4.5"
+ assert provider["modelId"] == "claude-sonnet-5"
assert provider["wireModel"] == "azure-deployment"
assert provider["maxPromptTokens"] == 8192
assert provider["maxOutputTokens"] == 1024
diff --git a/python/e2e/test_copilot_request_session_id_e2e.py b/python/e2e/test_copilot_request_session_id_e2e.py
index 81624d73d0..75bf15f2a4 100644
--- a/python/e2e/test_copilot_request_session_id_e2e.py
+++ b/python/e2e/test_copilot_request_session_id_e2e.py
@@ -105,14 +105,14 @@ async def test_threads_session_id_into_byok_session(self, session_id_client):
baseline = len(handler.records)
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
provider={
"type": "openai",
"wire_api": "responses",
"base_url": "https://byok.invalid/v1",
"api_key": "byok-secret",
- "model_id": "claude-sonnet-4.5",
- "wire_model": "claude-sonnet-4.5",
+ "model_id": "claude-sonnet-5",
+ "wire_model": "claude-sonnet-5",
},
)
byok_session_id = session.session_id
diff --git a/python/e2e/test_inprocess_ffi_e2e.py b/python/e2e/test_inprocess_ffi_e2e.py
index c119c4ea4e..ea82037b7a 100644
--- a/python/e2e/test_inprocess_ffi_e2e.py
+++ b/python/e2e/test_inprocess_ffi_e2e.py
@@ -15,20 +15,14 @@
from copilot import CopilotClient, RuntimeConnection
from .testharness import E2ETestContext
-from .testharness.context import get_cli_path_for_tests
pytestmark = pytest.mark.asyncio(loop_scope="module")
class TestInProcessFfi:
- async def test_should_start_and_connect_over_in_process_ffi(
- self, ctx: E2ETestContext, monkeypatch: pytest.MonkeyPatch
- ):
- # In-process hosting loads the runtime cdylib next to the resolved CLI
- # entrypoint and lets the native host spawn the worker. ``ping`` is a
- # purely local RPC round-trip, so no auth or replay proxy is involved.
- # If the native library is unavailable, start() raises and the test fails.
- monkeypatch.setenv("COPILOT_CLI_PATH", get_cli_path_for_tests())
+ async def test_should_start_and_connect_over_in_process_ffi(self, ctx: E2ETestContext):
+ # In-process hosting loads runtime.node directly. ``ping`` is a purely local
+ # RPC round-trip, so no auth or replay proxy is involved.
client = CopilotClient(connection=RuntimeConnection.for_inprocess())
await client.start()
diff --git a/python/e2e/test_mode_handlers_e2e.py b/python/e2e/test_mode_handlers_e2e.py
index f6173a4a5e..d5182c453d 100644
--- a/python/e2e/test_mode_handlers_e2e.py
+++ b/python/e2e/test_mode_handlers_e2e.py
@@ -6,6 +6,7 @@
import pytest
+from copilot.rpc import ModeSetRequest
from copilot.session import PermissionHandler
from copilot.session_events import (
AutoModeSwitchCompletedData,
@@ -15,6 +16,7 @@
ExitPlanModeCompletedData,
ExitPlanModeRequestedData,
SessionIdleData,
+ SessionMode,
SessionModelChangeData,
)
@@ -111,10 +113,8 @@ async def on_exit_plan_mode_request(request, invocation):
)
)
- response = await session.send_and_wait(
- PLAN_PROMPT,
- agent_mode="plan",
- )
+ await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN))
+ response = await session.send_and_wait(PLAN_PROMPT)
assert len(exit_plan_mode_requests) == 1
request = exit_plan_mode_requests[0]
diff --git a/python/e2e/test_rewind_e2e.py b/python/e2e/test_rewind_e2e.py
index 10e7bc4dd9..cf7627c1a3 100644
--- a/python/e2e/test_rewind_e2e.py
+++ b/python/e2e/test_rewind_e2e.py
@@ -4,7 +4,6 @@
import asyncio
import os
-import sys
from pathlib import Path
import pytest
@@ -22,6 +21,8 @@
pytestmark = pytest.mark.asyncio(loop_scope="module")
FILE_NAME = "rewind-sdk.txt"
+ORIGINAL_FILE_CONTENT = "Original rewind content"
+PREPARED_FILE_CONTENT = "Prepared rewind content"
FILE_CONTENT = "SDK rewind content"
@@ -31,19 +32,27 @@ def _same_path(left: str | Path, right: str | Path) -> bool:
class TestRewind:
async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestContext):
- if sys.platform == "win32":
- pytest.skip("blocked on CLI 1.0.81 file-change tracking regression on Windows")
-
file_path = Path(ctx.work_dir) / FILE_NAME
+ file_path.write_text(ORIGINAL_FILE_CONTENT, encoding="utf-8")
session = await ctx.client.create_session(
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
enable_file_change_tracking=True,
on_permission_request=PermissionHandler.approve_all,
)
try:
+ ready = await session.send_and_wait(
+ f"Use the edit tool to replace the exact contents of {FILE_NAME} "
+ f"from {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. "
+ "After the tool succeeds, reply with exactly SDK_REWIND_READY."
+ )
+ assert ready is not None
+ assert ready.data.content == "SDK_REWIND_READY"
+ assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT
+
response = await session.send_and_wait(
- f"Use the create tool to create {FILE_NAME} containing exactly {FILE_CONTENT}. "
+ f"Use the edit tool to replace the exact contents of {FILE_NAME} "
+ f"from {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. "
"After the tool succeeds, reply with exactly SDK_REWIND_DONE."
)
@@ -59,16 +68,18 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo
deadline = asyncio.get_running_loop().time() + 30
while asyncio.get_running_loop().time() < deadline and not (
rewind_points.unavailable_reason is None
- and rewind_points.points
- and rewind_points.points[0].can_restore_files
+ and len(rewind_points.points) == 2
+ and rewind_points.points[1].turn_changed_files
+ and rewind_points.points[1].can_restore_files
):
await asyncio.sleep(0.1)
rewind_points = await session.rpc.history.list_rewind_points()
assert rewind_points.unavailable_reason is None
assert rewind_points.file_change_tracking_enabled
- assert len(rewind_points.points) == 1
- rewind_point = rewind_points.points[0]
+ assert len(rewind_points.points) == 2
+ rewind_point = rewind_points.points[1]
+ assert rewind_point.turn_changed_files
assert rewind_point.can_restore_files
assert rewind_point.file_count == 1
@@ -89,7 +100,7 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo
assert rewind.events_removed is not None and rewind.events_removed > 0
assert len(rewind.restored_files) == 1
assert _same_path(rewind.restored_files[0], file_path)
- assert not file_path.exists()
+ assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT
events = await session.get_events()
assert all(str(event.id) != rewind_point.event_id for event in events)
diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py
index 4440635727..c9f08742cd 100644
--- a/python/e2e/test_rpc_e2e.py
+++ b/python/e2e/test_rpc_e2e.py
@@ -82,7 +82,7 @@ class TestSessionRpc:
async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext):
"""Test calling session.rpc.model.getCurrent"""
session = await ctx.client.create_session(
- on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5"
+ on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5"
)
result = await session.rpc.model.get_current()
@@ -96,7 +96,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext
from copilot.rpc import ModelSwitchToRequest
session = await ctx.client.create_session(
- on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5"
+ on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5"
)
# Get initial model
diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py
index e7c4a446ce..fdff3b8004 100644
--- a/python/e2e/test_rpc_server_e2e.py
+++ b/python/e2e/test_rpc_server_e2e.py
@@ -183,7 +183,7 @@ async def test_should_call_rpc_models_list_with_typed_result(self, authed_ctx: E
await client.start()
result = await client.rpc.models.list(ModelsListRequest())
assert result.models is not None
- assert any(model.id == "claude-sonnet-4.5" for model in result.models)
+ assert any(model.id == "claude-sonnet-5" for model in result.models)
assert all((model.name or "").strip() for model in result.models)
finally:
try:
diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py
index f4b03d2e65..622192cfe9 100644
--- a/python/e2e/test_rpc_session_state_e2e.py
+++ b/python/e2e/test_rpc_session_state_e2e.py
@@ -104,7 +104,7 @@ class TestRpcSessionState:
async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
)
try:
result = await session.rpc.model.get_current()
@@ -128,7 +128,7 @@ async def test_should_call_session_rpc_model_switchto(self, ctx: E2ETestContext)
)
session = await isolated_ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
)
try:
before = await session.rpc.model.get_current()
@@ -264,13 +264,13 @@ async def test_should_call_metadata_snapshot_set_working_directory_and_record_co
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
working_directory=first_dir,
)
try:
snapshot = await session.rpc.metadata.snapshot()
assert snapshot.session_id == session.session_id
- assert snapshot.selected_model == "claude-sonnet-4.5"
+ assert snapshot.selected_model == "claude-sonnet-5"
assert snapshot.is_remote is False
assert snapshot.already_in_use is False
assert _path_equals(first_dir, snapshot.working_directory)
@@ -395,7 +395,7 @@ async def snapshot_updated() -> bool:
async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
)
try:
reasoning = await session.rpc.model.set_reasoning_effort(
@@ -403,7 +403,7 @@ async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestConte
)
assert reasoning.reasoning_effort == "high"
current = await session.rpc.model.get_current()
- assert current.model_id == "claude-sonnet-4.5"
+ assert current.model_id == "claude-sonnet-5"
assert current.reasoning_effort == "high"
auto_name = f"Auto Session {uuid.uuid4().hex}"
@@ -637,12 +637,12 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC
MetadataContextInfoRequest(
prompt_token_limit=128_000,
output_token_limit=4_096,
- selected_model="claude-sonnet-4.5",
+ selected_model="claude-sonnet-5",
)
)
if context_info.context_info is not None:
context = context_info.context_info
- assert context.model_name == "claude-sonnet-4.5"
+ assert context.model_name == "claude-sonnet-5"
assert context.prompt_token_limit == 128_000
assert context.limit >= context.prompt_token_limit
assert context.total_tokens > 0
@@ -657,7 +657,7 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC
)
recomputed = await session.rpc.metadata.recompute_context_tokens(
- MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-4.5")
+ MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-5")
)
assert recomputed.system_token_count > 0
assert recomputed.messages_token_count > 0
diff --git a/python/e2e/test_rpc_session_state_extras_e2e.py b/python/e2e/test_rpc_session_state_extras_e2e.py
index 7523059c7b..02ee0cd790 100644
--- a/python/e2e/test_rpc_session_state_extras_e2e.py
+++ b/python/e2e/test_rpc_session_state_extras_e2e.py
@@ -77,7 +77,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext):
client = _make_authed_client(ctx, token)
try:
async with await client.create_session(
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
on_permission_request=PermissionHandler.approve_all,
github_token=token,
) as session:
@@ -86,8 +86,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext):
assert result.list is not None
assert len(result.list) > 0
assert any(
- "claude-sonnet-4.5" in json.dumps(model, sort_keys=True)
- for model in result.list
+ "claude-sonnet-5" in json.dumps(model, sort_keys=True) for model in result.list
)
finally:
await _stop_client(client)
@@ -126,7 +125,7 @@ async def test_should_add_byok_provider_and_model_at_runtime(self, ctx: E2ETestC
provider=provider_name,
id=model_id,
name="SDK Runtime Model",
- model_id="claude-sonnet-4.5",
+ model_id="claude-sonnet-5",
wire_model="wire-sdk-runtime-model",
max_context_window_tokens=4096,
max_prompt_tokens=3072,
diff --git a/python/e2e/test_session_config_e2e.py b/python/e2e/test_session_config_e2e.py
index 62dc671893..4fc78e645d 100644
--- a/python/e2e/test_session_config_e2e.py
+++ b/python/e2e/test_session_config_e2e.py
@@ -167,8 +167,8 @@ def _create_anthropic_provider() -> dict:
"type": "anthropic",
"base_url": "https://anthropic-citations.invalid/v1",
"api_key": "test-provider-key",
- "model_id": "claude-sonnet-4.5",
- "wire_model": "claude-sonnet-4.5",
+ "model_id": "claude-sonnet-5",
+ "wire_model": "claude-sonnet-5",
}
@@ -201,6 +201,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
+ model="claude-sonnet-5",
model_capabilities=ModelCapabilitiesOverride(
supports=ModelSupportsOverride(vision=False)
),
@@ -213,7 +214,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte
# Switch vision on
await session.set_model(
- "claude-sonnet-4.5",
+ "claude-sonnet-5",
model_capabilities=ModelCapabilitiesOverride(
supports=ModelSupportsOverride(vision=True)
),
@@ -234,6 +235,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
+ model="claude-sonnet-5",
model_capabilities=ModelCapabilitiesOverride(
supports=ModelSupportsOverride(vision=True)
),
@@ -246,7 +248,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte
# Switch vision off
await session.set_model(
- "claude-sonnet-4.5",
+ "claude-sonnet-5",
model_capabilities=ModelCapabilitiesOverride(
supports=ModelSupportsOverride(vision=False)
),
@@ -295,7 +297,7 @@ async def test_should_forward_clientname_in_useragent(self, ctx: E2ETestContext)
async def test_should_forward_custom_provider_headers_on_create(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
provider=_make_proxy_provider(ctx.proxy_url, "create-provider-header"),
)
@@ -319,7 +321,7 @@ async def test_should_forward_custom_provider_headers_on_resume(self, ctx: E2ETe
session2 = await ctx.client.resume_session(
session_id,
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
provider=_make_proxy_provider(ctx.proxy_url, "resume-provider-header"),
)
@@ -345,7 +347,7 @@ async def test_should_forward_provider_wire_model(self, ctx: E2ETestContext):
# it directly (see unit tests for serialization coverage).
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
provider={
"type": "openai",
"base_url": ctx.proxy_url,
@@ -374,7 +376,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont
"type": "openai",
"base_url": ctx.proxy_url,
"api_key": "test-provider-key",
- "model_id": "claude-sonnet-4.5",
+ "model_id": "claude-sonnet-5",
},
)
@@ -382,7 +384,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont
exchanges = await ctx.get_exchanges()
assert len(exchanges) == 1
- assert exchanges[0]["request"]["model"] == "claude-sonnet-4.5"
+ assert exchanges[0]["request"]["model"] == "claude-sonnet-5"
await session.disconnect()
@@ -473,7 +475,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_create(
try:
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
enable_citations=True,
provider=_create_anthropic_provider(),
)
@@ -521,7 +523,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_resume(
session2 = await resume_client.resume_session(
session1.session_id,
on_permission_request=PermissionHandler.approve_all,
- model="claude-sonnet-4.5",
+ model="claude-sonnet-5",
enable_citations=True,
provider=_create_anthropic_provider(),
)
diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py
index 08413f228f..a126eabc40 100644
--- a/python/e2e/test_session_e2e.py
+++ b/python/e2e/test_session_e2e.py
@@ -25,7 +25,7 @@
class TestSessions:
async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5"
+ on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5"
)
assert session.session_id
@@ -33,7 +33,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext):
assert len(messages) > 0
assert messages[0].type.value == "session.start"
assert messages[0].data.session_id == session.session_id
- assert messages[0].data.selected_model == "claude-sonnet-4.5"
+ assert messages[0].data.selected_model == "claude-sonnet-5"
await session.disconnect()
diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py
index 2171e25f2d..8eaecf6244 100644
--- a/python/e2e/testharness/context.py
+++ b/python/e2e/testharness/context.py
@@ -191,7 +191,6 @@ def _apply_inprocess_environment(self) -> None:
{
"GH_TOKEN": DEFAULT_GITHUB_TOKEN,
"GITHUB_TOKEN": DEFAULT_GITHUB_TOKEN,
- "COPILOT_CLI_PATH": self.cli_path,
"COPILOT_HMAC_KEY": "",
"CAPI_HMAC_KEY": "",
}
@@ -274,11 +273,13 @@ async def configure_for_test(self, test_file: str, test_name: str):
if self._proxy:
await self._proxy.configure(abs_snapshot_path, self.work_dir)
- # Clear temp directories between tests (but leave them in place)
- # Use ignore_errors=True / suppress(OSError) to handle race conditions
- # where files (e.g., SQLite session-store.db on Windows) may still be
- # held open by a background process during cleanup.
- for base_dir in (self.home_dir, self.work_dir):
+ # Keep the in-process runtime's isolated home intact until teardown stops
+ # the runtime. Removing its open state files on POSIX can leave later tests
+ # using unlinked database state.
+ cleanup_dirs = (
+ (self.work_dir,) if self._client_inprocess else (self.home_dir, self.work_dir)
+ )
+ for base_dir in cleanup_dirs:
base_path = Path(base_dir)
base_path.mkdir(parents=True, exist_ok=True)
for item in base_path.iterdir():
diff --git a/python/test_cli_download.py b/python/test_cli_download.py
index 36952919df..a5a20dce0d 100644
--- a/python/test_cli_download.py
+++ b/python/test_cli_download.py
@@ -4,6 +4,9 @@
import base64
import hashlib
+import io
+import os
+import tarfile
from unittest.mock import patch
import pytest
@@ -16,6 +19,28 @@ def _integrity(data: bytes, algo: str = "sha512") -> str:
return f"{algo}-{base64.b64encode(digest).decode('ascii')}"
+def _runtime_package(npm_platform: str) -> bytes:
+ wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime"
+ members = {
+ f"package/prebuilds/{npm_platform}/{wrapper_name}": b"wrapper",
+ f"package/prebuilds/{npm_platform}/runtime.node": b"runtime",
+ "package/copilot": b"excluded",
+ "package/copilot.exe": b"excluded",
+ f"package/ripgrep/bin/{npm_platform}/rg": b"ripgrep",
+ "package/definitions/future.json": b"{}",
+ "package/app.js": b"excluded",
+ "package/LICENSE.md": b"excluded",
+ "package/README.md": b"excluded",
+ }
+ buffer = io.BytesIO()
+ with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
+ for name, content in members.items():
+ info = tarfile.TarInfo(name)
+ info.size = len(content)
+ archive.addfile(info, io.BytesIO(content))
+ return buffer.getvalue()
+
+
class TestVerifyIntegrity:
def test_accepts_matching_checksum(self):
data = b"native-library-bytes"
@@ -51,3 +76,92 @@ def test_raises_when_integrity_unavailable(self, tmp_path):
# The library bytes must never be extracted/written when verification is impossible.
extract.assert_not_called()
+
+
+class TestEnsureRuntimeWrapper:
+ def test_materializes_pair_from_absent_cache_with_stripped_environment(
+ self, tmp_path, monkeypatch
+ ):
+ npm_platform = "win32-x64" if os.name == "nt" else "linux-x64"
+ wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime"
+ data = _runtime_package(npm_platform)
+ cache_dir = tmp_path / "cache"
+ empty_path = tmp_path / "empty-path"
+ empty_path.mkdir()
+ assert not cache_dir.exists()
+
+ for name in (
+ "COPILOT_CLI_PATH",
+ "COPILOT_RUNTIME_HOST_COMMAND",
+ "COPILOT_RUNTIME_PROVIDER_LIB",
+ ):
+ monkeypatch.delenv(name, raising=False)
+ monkeypatch.setenv("PATH", str(empty_path))
+
+ with (
+ patch.object(_cli_download, "get_cache_dir", return_value=cache_dir),
+ patch.object(_cli_download, "get_npm_platform", return_value=npm_platform),
+ patch.object(_cli_download, "_should_skip_download", return_value=False),
+ patch.object(_cli_download, "_fetch_url_bytes", return_value=data),
+ patch.object(
+ _cli_download,
+ "_fetch_runtime_integrity",
+ return_value=_integrity(data),
+ ),
+ ):
+ wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3")
+
+ install_dir = cache_dir / "prebuilds" / npm_platform
+ assert wrapper == str(install_dir / wrapper_name)
+ assert (install_dir / wrapper_name).read_bytes() == b"wrapper"
+ assert (install_dir / "runtime.node").read_bytes() == b"runtime"
+ assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").read_bytes() == b"ripgrep"
+ assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}"
+ assert not (install_dir / "app.js").exists()
+ assert not (install_dir / "copilot").exists()
+ assert not (install_dir / "copilot.exe").exists()
+ assert (install_dir / ".hostless-runtime-assets-v2").is_file()
+ if os.name != "nt":
+ assert (install_dir / wrapper_name).stat().st_mode & 0o111
+
+ def test_rejects_cached_wrapper_without_runtime_node(self, tmp_path):
+ npm_platform = "win32-x64" if os.name == "nt" else "linux-x64"
+ wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime"
+ cache_dir = tmp_path / "cache"
+ install_dir = cache_dir / "prebuilds" / npm_platform
+ install_dir.mkdir(parents=True)
+ (install_dir / wrapper_name).write_bytes(b"wrapper")
+
+ with (
+ patch.object(_cli_download, "get_cache_dir", return_value=cache_dir),
+ patch.object(_cli_download, "get_npm_platform", return_value=npm_platform),
+ ):
+ with pytest.raises(RuntimeError, match="Incomplete Copilot runtime bundle"):
+ _cli_download.ensure_runtime_wrapper(version="1.2.3")
+
+ def test_upgrades_pair_only_cache_with_retained_assets(self, tmp_path):
+ npm_platform = "win32-x64" if os.name == "nt" else "linux-x64"
+ wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime"
+ cache_dir = tmp_path / "cache"
+ install_dir = cache_dir / "prebuilds" / npm_platform
+ install_dir.mkdir(parents=True)
+ (install_dir / wrapper_name).write_bytes(b"old-wrapper")
+ (install_dir / "runtime.node").write_bytes(b"old-runtime")
+ (install_dir / "copilot").write_bytes(b"legacy-sea")
+ (install_dir / ".hostless-runtime-assets-v1").write_text("1\n", encoding="ascii")
+ data = _runtime_package(npm_platform)
+
+ with (
+ patch.object(_cli_download, "get_cache_dir", return_value=cache_dir),
+ patch.object(_cli_download, "get_npm_platform", return_value=npm_platform),
+ patch.object(_cli_download, "_should_skip_download", return_value=False),
+ patch.object(_cli_download, "_fetch_url_bytes", return_value=data),
+ patch.object(_cli_download, "_fetch_runtime_integrity", return_value=_integrity(data)),
+ ):
+ wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3")
+
+ assert wrapper == str(install_dir / wrapper_name)
+ assert (install_dir / wrapper_name).read_bytes() == b"wrapper"
+ assert not (install_dir / "copilot").exists()
+ assert (install_dir / ".hostless-runtime-assets-v2").is_file()
+ assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").is_file()
diff --git a/python/test_client.py b/python/test_client.py
index a33f0ecd60..bf8de113ae 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -59,6 +59,27 @@ def test_inprocess_connection_has_no_child_process_options():
assert not hasattr(connection, "args")
+def test_explicit_child_process_path_does_not_require_runtime_bundle(tmp_path):
+ explicit = tmp_path / "copilot"
+ connection = RuntimeConnection.for_stdio(path=str(explicit))
+
+ CopilotClient(connection=connection, env={"PATH": str(tmp_path)})
+
+ assert connection.path == str(explicit)
+
+
+def test_copilot_cli_path_does_not_require_runtime_bundle(tmp_path):
+ explicit = tmp_path / "copilot"
+ connection = RuntimeConnection.for_stdio()
+
+ CopilotClient(
+ connection=connection,
+ env={"PATH": str(tmp_path), "COPILOT_CLI_PATH": str(explicit)},
+ )
+
+ assert connection.path == str(explicit)
+
+
class TestBuiltinPluginDirectories:
@staticmethod
async def _start_client(paths=None):
@@ -211,6 +232,61 @@ async def test_resume_session_allows_none_permission_handler(self):
class TestCreateSessionConfig:
+ @pytest.mark.asyncio
+ async def test_ask_user_variant_forwarded_on_create_and_cold_resume(self):
+ client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
+ await client.start()
+ try:
+ captured: list[tuple[str, dict]] = []
+
+ async def mock_request(method, params, **kwargs):
+ captured.append((method, params))
+ result = {"sessionId": params["sessionId"], "workspacePath": None}
+ callback = kwargs.get("on_response_inline")
+ if callback is not None:
+ callback(result)
+ return result
+
+ client._client.request = mock_request
+ await client.create_session(
+ session_id="ask-user-create",
+ ask_user_variant="elicitation",
+ )
+ await client.resume_session(
+ "ask-user-cold-resume",
+ ask_user_variant="legacy",
+ )
+ await client.create_session(session_id="ask-user-default-create")
+ await client.resume_session("ask-user-default-cold-resume")
+
+ payloads = {(method, params["sessionId"]): params for method, params in captured}
+ assert (
+ payloads[("session.create", "ask-user-create")]["askUserVariant"] == "elicitation"
+ )
+ assert (
+ payloads[("session.resume", "ask-user-cold-resume")]["askUserVariant"] == "legacy"
+ )
+ assert "askUserVariant" not in payloads[("session.create", "ask-user-default-create")]
+ assert (
+ "askUserVariant" not in payloads[("session.resume", "ask-user-default-cold-resume")]
+ )
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize("method", ["create", "resume"])
+ async def test_ask_user_variant_rejects_unknown_values(self, method):
+ client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
+
+ with pytest.raises(ValueError, match="ask_user_variant"):
+ if method == "create":
+ await client.create_session(ask_user_variant="unknown") # type: ignore[arg-type]
+ else:
+ await client.resume_session(
+ "ask-user-cold-resume",
+ ask_user_variant="unknown", # type: ignore[arg-type]
+ )
+
@pytest.mark.asyncio
async def test_additional_directories_forwarded_on_create_and_resume(self):
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
@@ -1071,7 +1147,56 @@ async def mock_request(method, params, **kwargs):
await client.force_stop()
@pytest.mark.asyncio
- async def test_create_and_resume_session_forward_capi_options(self):
+ @pytest.mark.parametrize(
+ ("create_capi", "resume_capi", "expected_create", "expected_resume"),
+ [
+ (None, None, None, None),
+ ({}, {}, {}, {}),
+ (
+ {"enable_web_socket_responses": False},
+ {"enable_web_socket_responses": True},
+ {"enableWebSocketResponses": False},
+ {"enableWebSocketResponses": True},
+ ),
+ (
+ {"enable_web_socket_responses": True},
+ {"enable_web_socket_responses": False},
+ {"enableWebSocketResponses": True},
+ {"enableWebSocketResponses": False},
+ ),
+ (
+ {"auto_tier": "efficiency"},
+ {"auto_tier": "efficiency"},
+ {"autoTier": "efficiency"},
+ {"autoTier": "efficiency"},
+ ),
+ (
+ {"auto_tier": "balance"},
+ {"auto_tier": "balance"},
+ {"autoTier": "balance"},
+ {"autoTier": "balance"},
+ ),
+ (
+ {"auto_tier": "intelligence"},
+ {"auto_tier": "intelligence"},
+ {"autoTier": "intelligence"},
+ {"autoTier": "intelligence"},
+ ),
+ (
+ {"auto_tier": "balance", "enable_web_socket_responses": False},
+ {"auto_tier": "balance", "enable_web_socket_responses": True},
+ {"autoTier": "balance", "enableWebSocketResponses": False},
+ {"autoTier": "balance", "enableWebSocketResponses": True},
+ ),
+ ],
+ )
+ async def test_create_and_resume_session_forward_capi_options(
+ self,
+ create_capi: CapiSessionOptions | None,
+ resume_capi: CapiSessionOptions | None,
+ expected_create: dict[str, object] | None,
+ expected_resume: dict[str, object] | None,
+ ):
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
await client.start()
try:
@@ -1088,11 +1213,9 @@ async def mock_request(method, params, **kwargs):
return {}
client._client.request = mock_request
- create_capi: CapiSessionOptions = {"enable_web_socket_responses": False}
- resume_capi: CapiSessionOptions = {"enable_web_socket_responses": True}
-
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
+ model="auto",
capi=create_capi,
)
await client.resume_session(
@@ -1101,12 +1224,14 @@ async def mock_request(method, params, **kwargs):
capi=resume_capi,
)
- assert captured["session.create"]["capi"] == {
- "enableWebSocketResponses": False,
- }
- assert captured["session.resume"]["capi"] == {
- "enableWebSocketResponses": True,
- }
+ for method, expected in (
+ ("session.create", expected_create),
+ ("session.resume", expected_resume),
+ ):
+ if expected is None:
+ assert "capi" not in captured[method]
+ else:
+ assert captured[method]["capi"] == expected
finally:
await client.force_stop()
@@ -1336,6 +1461,41 @@ async def mock_request(method, params, **kwargs):
finally:
await client.force_stop()
+ @pytest.mark.asyncio
+ async def test_create_and_resume_session_forward_feature_flags(self):
+ client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
+ await client.start()
+ try:
+ captured = {}
+
+ async def mock_request(method, params, **kwargs):
+ captured[method] = params
+ if method in ("session.create", "session.resume"):
+ result = {"sessionId": params.get("sessionId") or "session-1"}
+ callback = kwargs.get("on_response_inline")
+ if callback is not None:
+ callback(result)
+ return result
+ return {}
+
+ client._client.request = mock_request
+ feature_flags = {"ENABLED_TEST_FLAG": True, "DISABLED_TEST_FLAG": False}
+
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ feature_flags=feature_flags,
+ )
+ await client.resume_session(
+ session.session_id,
+ on_permission_request=PermissionHandler.approve_all,
+ feature_flags=feature_flags,
+ )
+
+ assert captured["session.create"]["featureFlags"] == feature_flags
+ assert captured["session.resume"]["featureFlags"] == feature_flags
+ finally:
+ await client.force_stop()
+
class TestURLParsing:
def test_parse_port_only_url(self):
@@ -2991,6 +3151,105 @@ async def request(self, method, params, **kwargs):
await client._verify_protocol_version()
assert "enableGitHubTelemetryForwarding" not in captured["connect"]
+ @pytest.mark.asyncio
+ async def test_connect_forwards_client_info(self):
+ client = CopilotClient(
+ connection=RuntimeConnection.for_stdio(path=CLI_PATH),
+ client_info={
+ "application_name": "acme-developer-portal",
+ "application_version": "2.4.0",
+ "integration_name": "copilot-assistant",
+ "integration_version": "1.5.0",
+ },
+ )
+ captured = {}
+
+ class _FakeClient:
+ async def request(self, method, params, **kwargs):
+ captured[method] = params
+ return {"ok": True, "protocolVersion": 3, "version": "test"}
+
+ client._client = _FakeClient()
+ await client._verify_protocol_version()
+ assert captured["connect"]["clientInfo"] == {
+ "editorName": "acme-developer-portal",
+ "editorVersion": "2.4.0",
+ "extensionName": "copilot-assistant",
+ "extensionVersion": "1.5.0",
+ }
+
+ @pytest.mark.asyncio
+ async def test_connect_omits_client_info_when_unset(self):
+ client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
+ captured = {}
+
+ class _FakeClient:
+ async def request(self, method, params, **kwargs):
+ captured[method] = params
+ return {"ok": True, "protocolVersion": 3, "version": "test"}
+
+ client._client = _FakeClient()
+ await client._verify_protocol_version()
+ assert "clientInfo" not in captured["connect"]
+
+ @pytest.mark.asyncio
+ async def test_connect_forwards_partial_client_info_with_forwarding(self):
+ client = CopilotClient(
+ connection=RuntimeConnection.for_stdio(path=CLI_PATH),
+ client_info={"application_name": "example-app"},
+ on_github_telemetry=lambda _notification: None,
+ )
+ captured = {}
+
+ class _FakeClient:
+ async def request(self, method, params, **kwargs):
+ captured[method] = params
+ return {"ok": True, "protocolVersion": 3, "version": "test"}
+
+ client._client = _FakeClient()
+ await client._verify_protocol_version()
+ assert captured["connect"]["clientInfo"] == {"editorName": "example-app"}
+ assert captured["connect"]["enableGitHubTelemetryForwarding"] is True
+
+ @pytest.mark.asyncio
+ async def test_connect_drops_empty_client_info_fields(self):
+ client = CopilotClient(
+ connection=RuntimeConnection.for_stdio(path=CLI_PATH),
+ client_info={"application_name": "example-app", "application_version": ""},
+ )
+ captured = {}
+
+ class _FakeClient:
+ async def request(self, method, params, **kwargs):
+ captured[method] = params
+ return {"ok": True, "protocolVersion": 3, "version": "test"}
+
+ client._client = _FakeClient()
+ await client._verify_protocol_version()
+ assert captured["connect"]["clientInfo"] == {"editorName": "example-app"}
+
+ @pytest.mark.asyncio
+ async def test_connect_omits_all_empty_client_info(self):
+ client = CopilotClient(
+ connection=RuntimeConnection.for_stdio(path=CLI_PATH),
+ client_info={
+ "application_name": "",
+ "application_version": "",
+ "integration_name": "",
+ "integration_version": "",
+ },
+ )
+ captured = {}
+
+ class _FakeClient:
+ async def request(self, method, params, **kwargs):
+ captured[method] = params
+ return {"ok": True, "protocolVersion": 3, "version": "test"}
+
+ client._client = _FakeClient()
+ await client._verify_protocol_version()
+ assert "clientInfo" not in captured["connect"]
+
@pytest.mark.asyncio
async def test_event_routes_to_handler(self):
from copilot.generated.rpc import GitHubTelemetryNotification
diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py
index 2e8015a97d..a42b9994fc 100644
--- a/python/test_event_forward_compatibility.py
+++ b/python/test_event_forward_compatibility.py
@@ -14,6 +14,7 @@
from copilot.session_events import (
AttachmentGitHubReferenceType,
+ AutoTier,
Data,
ElicitationCompletedAction,
ElicitationRequestedMode,
@@ -24,6 +25,8 @@
PermissionRequestMemoryAction,
SessionEventType,
SessionManagedSettingsResolvedData,
+ SessionResumeData,
+ SessionStartData,
SessionTaskCompleteData,
UserMessageAgentMode,
session_event_from_dict,
@@ -34,6 +37,40 @@
class TestEventForwardCompatibility:
"""Test forward compatibility for unknown event types."""
+ @pytest.mark.parametrize("event_type", ["session.start", "session.resume"])
+ @pytest.mark.parametrize("tier", ["efficiency", "balance", "intelligence", None])
+ def test_auto_tier_lifecycle_events_round_trip(self, event_type, tier):
+ timestamp = "2026-08-28T00:00:00Z"
+ data = (
+ {
+ "copilotVersion": "1.0.82-1",
+ "producer": "copilot-agent",
+ "sessionId": str(uuid4()),
+ "startTime": timestamp,
+ "version": 1,
+ }
+ if event_type == "session.start"
+ else {"eventCount": 1, "resumeTime": timestamp}
+ )
+ if tier is not None:
+ data["autoTier"] = tier
+ event = session_event_from_dict(
+ {
+ "id": str(uuid4()),
+ "timestamp": timestamp,
+ "parentId": None,
+ "type": event_type,
+ "data": data,
+ }
+ )
+ assert isinstance(event.data, (SessionStartData, SessionResumeData))
+ assert event.data.auto_tier == (AutoTier(tier) if tier is not None else None)
+ serialized = session_event_to_dict(event)["data"]
+ if tier is None:
+ assert "autoTier" not in serialized
+ else:
+ assert serialized["autoTier"] == tier
+
def test_session_usage_info_is_recognized(self):
"""The session.usage_info event type should be in the enum."""
assert SessionEventType.SESSION_USAGE_INFO.value == "session.usage_info"
@@ -144,6 +181,7 @@ def test_managed_settings_client_provenance_round_trips(self):
"server",
"device",
"client",
+ "policyHelper",
"mixed",
"none",
]
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 8de6797989..b91eebd06c 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -454,6 +454,7 @@ dependencies = [
"tracing",
"ureq",
"uuid",
+ "windows-sys 0.61.2",
"zip",
]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 0f18a9b159..c7a704fd50 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -70,6 +70,12 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c
[target.'cfg(windows)'.dependencies]
zip = { version = "2", default-features = false, features = ["deflate"], optional = true }
+windows-sys = { version = "0.61", default-features = false, features = [
+ "Win32_Foundation",
+ "Win32_System_Diagnostics_ToolHelp",
+ "Win32_System_JobObjects",
+ "Win32_System_Threading",
+] }
[dev-dependencies]
rusqlite = { version = "0.35", features = ["bundled"] }
@@ -90,6 +96,28 @@ required-features = ["test-support"]
name = "protocol_version_test"
required-features = ["test-support"]
+[[test]]
+name = "extension_launch_provider_test"
+required-features = ["test-support"]
+
+[[test]]
+name = "extension_launch_provider_runtime_test"
+required-features = ["test-support"]
+
+[[bin]]
+name = "copilot-extension-test-fixture"
+path = "tests/fixtures/extension_fixture.rs"
+required-features = ["test-support"]
+test = false
+bench = false
+
+[[bin]]
+name = "copilot-host-crash-fixture"
+path = "tests/fixtures/host_crash_fixture.rs"
+required-features = ["test-support"]
+test = false
+bench = false
+
[build-dependencies]
base64 = "0.22"
dirs = "5"
diff --git a/rust/README.md b/rust/README.md
index 323d525d37..b9a1f5b1bc 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -101,8 +101,63 @@ transports.
| `env_remove` | `Vec` | Environment variables to remove |
| `extra_args` | `Vec` | Extra CLI flags |
| `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` |
+| `extension_launch_provider` | `Option>` | Connection-global extension launch resolver |
-With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`.
+With the default `CliProgram::Resolve`, managed stdio and TCP transports resolve an explicit `CliProgram::Path(path)`, `COPILOT_CLI_PATH`, then the bundled `copilot-runtime` wrapper and adjacent `runtime.node`. In-process transport retains its CLI-entrypoint resolution. There is no PATH scanning.
+
+#### Extension launch provider
+
+Hosts that own legacy extension process assets can supply a typed, asynchronous
+launch resolver:
+
+```rust,ignore
+use std::collections::HashMap;
+
+use async_trait::async_trait;
+use github_copilot_sdk::extension_launch_provider::{
+ ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest,
+ ExtensionLaunchProviderResolveResult,
+};
+use github_copilot_sdk::{Client, ClientOptions, Result};
+
+struct AppExtensionLaunchProvider;
+
+#[async_trait]
+impl ExtensionLaunchProvider for AppExtensionLaunchProvider {
+ async fn resolve(
+ &self,
+ request: ExtensionLaunchProviderResolveRequest,
+ ) -> Result {
+ Ok(ExtensionLaunchProviderResolveResult {
+ launch: Some(ExtensionLaunchProfile {
+ executable: "/app/copilot".to_string(),
+ args: vec!["/app/preloads/extension_bootstrap.mjs".to_string()],
+ env: HashMap::from([
+ ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()),
+ ("EXTENSION_PATH".to_string(), request.module_path),
+ ]),
+ }),
+ })
+ }
+}
+
+let client = Client::start(
+ ClientOptions::new().with_extension_launch_provider(AppExtensionLaunchProvider),
+).await?;
+```
+
+`Client::start` registers the provider before returning, and reverse requests
+are routed at the connection level rather than through a session. The SDK
+forwards the returned executable, arguments, and environment unchanged; it
+does not discover or bundle an executable or bootstrap. The runtime owns and
+overrides `COPILOT_SDK_PATH`, `SESSION_ID`, and
+`COPILOT_EXTENSION_PARENT_PID`.
+
+`COPILOT_CLI_DIST_DIR` is only appropriate when the host supplies a complete
+CLI distribution containing `index.js` and its matching preloads. When the
+executable is a version-matched standalone Copilot binary, omit that variable
+and set `COPILOT_AUTO_UPDATE=false` so its embedded distribution remains
+selected.
### Session
@@ -274,6 +329,11 @@ let config = SessionConfig {
let session = client.create_session(config).await?;
```
+Use `with_ask_user_variant(AskUserVariant::Elicitation)` together with
+`with_elicitation_handler(...)` to expose the structured form-based `ask_user`
+tool. The default remains `AskUserVariant::Legacy`. Re-supply the option and
+handler through `ResumeSessionConfig` on a cold resume.
+
For rotating per-session GitHub credentials, install a `GitHubTokenProvider`
instead of setting `github_token`:
@@ -302,6 +362,30 @@ provider errors, and invalid token responses reject that operation instead of
falling back to ambient authentication. Idle sessions refresh only before their
next credential-consuming operation; there is no background refresh timer.
+### Auto routing tiers
+
+Use `CapiSessionOptions::with_auto_tier` to select `AutoTier::Efficiency`,
+`AutoTier::Balance`, or `AutoTier::Intelligence`. This option is meaningful only
+with model `auto` (Auto mode V2).
+It requires a runtime version that supports `capi.autoTier`.
+
+```rust
+use github_copilot_sdk::{AutoTier, CapiSessionOptions, SessionConfig};
+
+let config = SessionConfig::default()
+ .with_model("auto")
+ .with_capi(CapiSessionOptions::new().with_auto_tier(AutoTier::Balance));
+```
+
+The same options work with `ResumeSessionConfig::with_capi` and can be combined
+with `with_enable_web_socket_responses(false)`. The SDK omits an unset tier:
+the runtime chooses its default on create and preserves the persisted/current
+tier on resume. An explicit tier overrides the persisted tier on cold resume;
+the runtime rejects a conflicting tier when the session is already resident
+in memory. The SDK does not choose a default or manage tier persistence.
+See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence)
+for the lifecycle rules.
+
### Session Hooks
Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a `SessionHooks` impl with [`SessionConfig::with_hooks`] — the SDK auto-enables `hooks` in `SessionConfig` when one is set.
@@ -521,6 +605,7 @@ impl ElicitationHandler for MyElicitation {
let config = SessionConfig::default()
.with_permission_handler(Arc::new(ApproveAllHandler))
+ .with_ask_user_variant(AskUserVariant::Elicitation)
.with_elicitation_handler(Arc::new(MyElicitation));
```
@@ -820,6 +905,7 @@ none of them are scheduled for removal.
| File | Description |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` |
+| `extension_launch_provider.rs` | Connection-global `ExtensionLaunchProvider` trait and launch profile DTOs |
| `session.rs` | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session` |
| `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) |
| `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` |
@@ -829,13 +915,15 @@ none of them are scheduled for removal.
| `types.rs` | CLI protocol types (`SessionId`, `SessionEvent`, `SessionConfig`, `Tool`, etc.) |
| `resolve.rs` | Bundled-CLI resolution (`copilot_binary`) |
| `embeddedcli.rs` | Embedded CLI extraction (gated on the default `bundled-cli` feature) |
-| `router.rs` | Internal per-session event demux |
+| `router.rs` | Internal connection-global request dispatch and per-session event demux |
| `jsonrpc.rs` | Internal Content-Length framed JSON-RPC transport |
-## Embedded CLI
+## Bundled runtime artifacts
The SDK provisions its runtime at build time. By default the `bundled-cli`
-feature embeds the verified child-process runtime in your compiled crate.
+feature embeds the verified `copilot-runtime` wrapper and adjacent
+`runtime.node` in your compiled crate. The compatible CLI artifact remains
+available separately for `install_bundled_cli` and in-process hosting.
Enable `bundled-in-process` to additionally embed the native runtime library
and use `Transport::InProcess`:
@@ -853,15 +941,11 @@ For builds that prefer a smaller artifact, disable the `bundled-cli` feature:
github-copilot-sdk = { version = "0.1", default-features = false }
```
-> **You become responsible for supplying the CLI at runtime.** With
-> `bundled-cli` disabled, the produced binary does not contain the CLI
-> and will not search the system for one. You must point it at a
-> compatible CLI via [`CliProgram::Path`] (on `ClientOptions`) or the
-> `COPILOT_CLI_PATH` environment variable, and you are responsible for
-> guaranteeing the supplied CLI version is compatible with this SDK
-> release. Do **not** assume that whatever CLI happens to be installed
-> on the target system will work — the SDK and CLI are versioned
-> together.
+> **You become responsible for supplying the runtime at deployment.** With
+> `bundled-cli` disabled, the produced binary does not contain these artifacts
+> and will not search the system for them. For managed child-process transports,
+> supply a compatible wrapper pair via an explicit [`CliProgram::Path`].
+> `COPILOT_CLI_PATH` remains a direct program override.
>
> **Convenience on the build machine only.** As a special case,
> `build.rs` downloads and integrity-verifies the compatible CLI version and
@@ -870,8 +954,8 @@ github-copilot-sdk = { version = "0.1", default-features = false }
> makes local development and CI ergonomic, but it does **not** carry
> over when you copy the built binary to another machine — distributed
> builds (release artifacts, signed installers, container images, etc.)
-> must either keep `bundled-cli` enabled or ship the CLI alongside and
-> set `CliProgram::Path` / `COPILOT_CLI_PATH`.
+> must either keep `bundled-cli` enabled or ship the runtime pair and set
+> `CliProgram::Path`.
### How it works
@@ -884,17 +968,17 @@ github-copilot-sdk = { version = "0.1", default-features = false }
2. **Build time:** `build.rs` downloads the platform-specific npm package and
verifies its `sha512` integrity against the lockfile or publish snapshot.
Then:
- - **`bundled-cli` on (default):** creates and embeds a minimal archive containing only the CLI executable.
- - **`bundled-in-process` on:** the minimal archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`); no other npm package files are embedded.
- - **`bundled-cli` off:** extracts the binary directly into the platform cache (staging file + atomic rename), idempotent across rebuilds. If the extracted binary is already present at the expected path, the download is skipped entirely — the extracted binary *is* the cache.
+ - **`bundled-cli` on (default):** creates and embeds a minimal archive containing the CLI executable, `copilot-runtime[.exe]`, and `runtime.node`.
+ - **`bundled-in-process` on:** the archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`).
+ - **`bundled-cli` off:** extracts the same artifacts directly into the platform cache using staging files and atomic renames.
-3. **Runtime:** in both modes the binary lives at:
+3. **Runtime:** in both modes the artifacts share one versioned directory:
| OS | Path |
|----|------|
- | macOS | `~/Library/Caches/github-copilot-sdk/cli//copilot` |
- | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//copilot` |
- | Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\copilot.exe` |
+ | macOS | `~/Library/Caches/github-copilot-sdk/cli//` |
+ | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//` |
+ | Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\` |
Old version directories accumulate in siblings; clean them up at your leisure.
@@ -923,18 +1007,20 @@ COPILOT_CLI_EXTRACT_DIR = { value = "vendor/copilot", relative = true, force = t
### Skipping the bundle entirely
-Set `COPILOT_SKIP_CLI_DOWNLOAD=1` at build time to disable the entire download / bundle / cache mechanism — `build.rs` returns immediately without touching the network. Use this when you always supply the CLI at runtime via `ClientOptions::program = CliProgram::Path(...)` or `COPILOT_CLI_PATH`. Works regardless of the `bundled-cli` feature state; runtime resolution falls through to `Error::BinaryNotFound` unless one of those explicit sources resolves.
+Set `COPILOT_SKIP_CLI_DOWNLOAD=1` at build time to disable the entire download / bundle / cache mechanism — `build.rs` returns immediately without touching the network. Use this when you always supply the managed runtime via `ClientOptions::program = CliProgram::Path(...)`. Works regardless of the `bundled-cli` feature state; runtime resolution falls through to `Error::BinaryNotFound` unless an applicable explicit source resolves.
### Resolution priority
-`Client::start` resolves the CLI in this order:
+For managed child-process transports, `Client::start` resolves the program in this order:
1. Explicit `CliProgram::Path(path)` on `ClientOptions::program`.
2. `COPILOT_CLI_PATH` environment variable, if it points at a real file.
-3. **`bundled-cli` on:** the embedded archive, lazily extracted on first call.
-4. **`bundled-cli` off:** the build-time-extracted binary in the per-user cache, located by recomputing the convention from `COPILOT_SDK_CLI_VERSION` + OS + optional `COPILOT_CLI_EXTRACT_DIR`.
+3. **`bundled-cli` on:** the embedded wrapper pair, lazily extracted on first call.
+4. **`bundled-cli` off:** the build-time-extracted wrapper pair in the per-user cache.
-There is no PATH scanning. If none of the above resolves, `Client::start` returns `Error::BinaryNotFound`.
+In-process transport resolves the compatible CLI artifact from
+`COPILOT_CLI_PATH`, the embedded archive, or the build-time cache. There is no
+PATH scanning.
### Reaching the bundled binary without a `Client`
@@ -954,12 +1040,24 @@ if HAS_BUNDLED_CLI {
}
```
-This returns the same path `Client::start` would resolve to for
-`CliProgram::Resolve` with no `COPILOT_CLI_PATH` override and no
-`ClientOptions::bundled_cli_extract_dir` configured. It returns `None`
-when `bundled-cli` is off or the target is unsupported, and (unlike the
-full resolver) does not fall back to the build-time-extracted dev-cache
-path.
+This returns the bundled CLI artifact, preserving the public API's original
+meaning. Managed child-process transports resolve `copilot-runtime` instead.
+The function returns `None` when `bundled-cli` is off or the target is
+unsupported and does not fall back to the build-time extraction cache.
+
+Use [`install_bundled_runtime`] when a health check or intermediate launcher
+needs the managed runtime executable:
+
+```rust,no_run
+use github_copilot_sdk::install_bundled_runtime;
+
+if let Some(path) = install_bundled_runtime() {
+ println!("bundled runtime at {}", path.display());
+}
+```
+
+This extracts `copilot-runtime` together with adjacent `runtime.node`, then
+returns the wrapper path.
### Download cache (build-time, embed mode)
@@ -973,8 +1071,8 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`
| Feature | Default | Description |
| ------- | ------- | ----------- |
-| `bundled-cli` | ✓ | Embeds only the CLI executable. Disable via `default-features = false` when supplying the CLI via `CliProgram::Path` or `COPILOT_CLI_PATH`. |
-| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds only the platform-native runtime library. |
+| `bundled-cli` | ✓ | Embeds the managed wrapper pair and compatible CLI artifact. Disable via `default-features = false` when supplying the runtime explicitly. |
+| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds the platform-native runtime library. |
| `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). |
```toml
diff --git a/rust/build.rs b/rust/build.rs
index d04cf2870b..c01464bb4a 100644
--- a/rust/build.rs
+++ b/rust/build.rs
@@ -1,11 +1,6 @@
-#[cfg(feature = "bundled-in-process")]
#[path = "build/in_process.rs"]
mod implementation;
-#[cfg(not(feature = "bundled-in-process"))]
-#[path = "build/out_of_process.rs"]
-mod implementation;
-
fn main() {
implementation::main();
}
diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs
index 5826fbfa76..e6edcf1b90 100644
--- a/rust/build/in_process.rs
+++ b/rust/build/in_process.rs
@@ -40,7 +40,7 @@ pub(crate) fn main() {
// path source resolves first.
if std::env::var_os("COPILOT_SKIP_CLI_DOWNLOAD").is_some() {
println!(
- "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping CLI download/bundle/cache"
+ "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping runtime download/bundle/cache"
);
return;
}
@@ -95,38 +95,61 @@ pub(crate) fn main() {
if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() {
let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir);
- verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name);
+ verify_runtime_package(&archive, platform, &archive_name);
emit_embedded(out, &archive, platform, include_runtime);
println!("cargo:rustc-cfg=has_bundled_cli");
} else {
- // With `bundled-cli` off the extracted binary *is* the cache.
- // Skip the upstream download entirely when it already exists at
- // the expected path. No two separate caches.
+ // With `bundled-cli` off the extracted runtime pair *is* the cache.
+ // Skip the upstream download entirely when both files already exist.
//
- // Runtime resolution (see `src/resolve.rs::extracted_cli_path`)
+ // Runtime resolution (see `src/resolve.rs::extracted_program`)
// recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the
// OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`,
// so we don't bake an absolute path into the crate.
let install_dir = extracted_install_dir(&version);
- let final_path = install_dir.join(platform.binary_name);
-
- // Invalidate build.rs whenever the cached binary disappears (cache GC,
- // manual rm, OS reset, switching extract dir). Without this, cargo
+ let required_paths = [
+ install_dir.join(platform.runtime_wrapper_name()),
+ install_dir.join("runtime.node"),
+ install_dir.join(".hostless-runtime-assets-v1"),
+ ];
+ let expected_marker = format!("{version}\n{expected_integrity}\n");
+
+ // Invalidate build.rs whenever either cached artifact disappears (cache
+ // GC, manual rm, OS reset, switching extract dir). Without this, cargo
// replays the saved `has_extracted_cli` cfg from its build-script
// output cache even when the file is gone, and runtime resolution
// fails with BinaryNotFound.
- println!("cargo:rerun-if-changed={}", final_path.display());
+ for path in &required_paths {
+ println!("cargo:rerun-if-changed={}", path.display());
+ }
- if !final_path.is_file() {
+ let cache_is_current = required_paths.iter().all(|path| path.is_file())
+ && std::fs::read_to_string(&required_paths[2]).ok().as_deref()
+ == Some(expected_marker.as_str());
+ if !cache_is_current {
+ if install_dir.exists() {
+ std::fs::remove_dir_all(&install_dir).unwrap_or_else(|e| {
+ panic!(
+ "failed to clear stale runtime bundle {}: {e}",
+ install_dir.display()
+ )
+ });
+ }
let archive =
cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir);
- verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name);
- extract_to_cache(&archive, &install_dir, platform);
+ verify_runtime_package(&archive, platform, &archive_name);
+ extract_to_cache(
+ &archive,
+ &install_dir,
+ platform,
+ include_runtime,
+ &expected_marker,
+ );
}
// Re-check after potential download+extract above; not an `else`
// because we need to verify the extraction actually produced the file.
- if final_path.is_file() {
+ if required_paths.iter().all(|path| path.is_file()) {
println!("cargo:rustc-cfg=has_extracted_cli");
}
}
@@ -176,19 +199,8 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b
.mtime(0)
.write(Vec::new(), flate2::Compression::default());
let mut archive = tar::Builder::new(encoder);
- append_archive_file(
- &mut archive,
- platform.binary_name,
- &extract_binary_bytes(package, platform),
- 0o755,
- );
+ let runtime = append_hostless_runtime_tree(&mut archive, package, platform);
if include_runtime {
- let runtime = extract_runtime_library_bytes(package).unwrap_or_else(|| {
- panic!(
- "package `{}` does not contain the native runtime library required by the `bundled-in-process` feature",
- platform.package_name
- )
- });
append_archive_file(
&mut archive,
platform.runtime_library_name(),
@@ -204,6 +216,103 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b
.expect("failed to compress minimal embedded CLI archive")
}
+fn append_hostless_runtime_tree(
+ archive: &mut tar::Builder,
+ package: &[u8],
+ platform: Platform,
+) -> Vec {
+ let decoder = flate2::read::GzDecoder::new(package);
+ let mut source = tar::Archive::new(decoder);
+ let mut runtime = None;
+ for entry in source
+ .entries()
+ .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}"))
+ {
+ let mut entry = entry.unwrap_or_else(|e| panic!("failed to read npm package entry: {e}"));
+ if !entry.header().entry_type().is_file() {
+ continue;
+ }
+ let source_path = entry
+ .path()
+ .unwrap_or_else(|e| panic!("failed to read npm package path: {e}"));
+ let Some(destination) = hostless_runtime_path(&source_path.to_string_lossy(), platform)
+ else {
+ continue;
+ };
+ let mut bytes = Vec::with_capacity(entry.size() as usize);
+ entry
+ .read_to_end(&mut bytes)
+ .unwrap_or_else(|e| panic!("failed to read npm package entry bytes: {e}"));
+ let mode = entry.header().mode().unwrap_or(0o644);
+ if destination == Path::new("runtime.node") {
+ runtime = Some(bytes.clone());
+ }
+ append_archive_file(
+ archive,
+ destination
+ .to_str()
+ .expect("npm package paths are valid UTF-8"),
+ &bytes,
+ mode,
+ );
+ }
+ runtime.unwrap_or_else(|| {
+ panic!(
+ "package `{}` does not contain prebuilds//runtime.node",
+ platform.package_name
+ )
+ })
+}
+
+fn hostless_runtime_path(source: &str, platform: Platform) -> Option {
+ let relative = source.strip_prefix("package/")?;
+ let parts: Vec<&str> = relative.split('/').collect();
+ if parts.iter().any(|part| part.is_empty() || *part == "..") {
+ return None;
+ }
+ let top_level = *parts.first()?;
+ let file_name = *parts.last()?;
+ const EXCLUDED_TOP_LEVEL: &[&str] = &[
+ "app.js",
+ "assets",
+ "changelog.json",
+ "copilot-sdk",
+ "foundry-local-sdk",
+ "index.js",
+ "LICENSE.md",
+ "napi-oop-runtime",
+ "npm-loader.js",
+ "package.json",
+ "preloads",
+ "pvrecorder",
+ "queries",
+ "README.md",
+ "sdk",
+ "sea-loader.js",
+ "webview",
+ ];
+ if EXCLUDED_TOP_LEVEL.contains(&top_level)
+ || (top_level.starts_with("tree-sitter") && top_level.ends_with(".wasm"))
+ || (top_level.starts_with("voice-") && top_level.ends_with(".js"))
+ || file_name == "cli-native.node"
+ || parts.contains(&"mediaremote-adapter")
+ || file_name.starts_with("copilot-runtime-bin")
+ {
+ return None;
+ }
+ if top_level == "prebuilds" {
+ let npm_platform = platform
+ .package_name
+ .strip_prefix("copilot-")
+ .expect("platform package name has copilot- prefix");
+ if parts.get(1) != Some(&npm_platform) || parts.len() < 3 {
+ return None;
+ }
+ return Some(parts[2..].iter().copied().collect());
+ }
+ Some(parts.iter().copied().collect())
+}
+
fn append_archive_file(
archive: &mut tar::Builder,
path: &str,
@@ -315,6 +424,14 @@ struct Platform {
}
impl Platform {
+ fn runtime_wrapper_name(&self) -> &'static str {
+ if self.package_name.contains("win32") {
+ "copilot-runtime.exe"
+ } else {
+ "copilot-runtime"
+ }
+ }
+
fn runtime_library_name(&self) -> &'static str {
if self.package_name.contains("win32") {
"copilot_runtime.dll"
@@ -368,8 +485,8 @@ fn target_platform() -> Option {
}
}
-/// Write the single binary entry from `archive` to
-/// `/` and return the resulting path.
+/// Write the runtime wrapper pair from `archive` to `install_dir` and return
+/// the wrapper path.
/// Idempotent — returns the existing path if a previous build already
/// populated the target.
///
@@ -378,15 +495,13 @@ fn target_platform() -> Option {
/// binary. `fs::rename` for files is atomic on both Unix and Windows
/// (Windows uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`); for
/// directories it is not, which is why we stage at file granularity.
-fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> PathBuf {
- let final_path = install_dir.join(platform.binary_name);
-
- // Caller already gated on `final_path.is_file()`; this is a safety
- // net for any future caller that forgets.
- if final_path.is_file() {
- return final_path;
- }
-
+fn extract_to_cache(
+ archive: &[u8],
+ install_dir: &Path,
+ platform: Platform,
+ include_runtime: bool,
+ marker: &str,
+) -> PathBuf {
std::fs::create_dir_all(install_dir).unwrap_or_else(|e| {
panic!(
"failed to create install dir {}: {e}",
@@ -394,8 +509,96 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P
)
});
- let bytes = extract_binary_bytes(archive, platform);
+ let decoder = flate2::read::GzDecoder::new(archive);
+ let mut source = tar::Archive::new(decoder);
+ let mut runtime = None;
+ for entry in source
+ .entries()
+ .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}"))
+ {
+ let mut entry = entry.unwrap_or_else(|e| panic!("failed to read npm package entry: {e}"));
+ if !entry.header().entry_type().is_file() {
+ continue;
+ }
+ let source_path = entry
+ .path()
+ .unwrap_or_else(|e| panic!("failed to read npm package path: {e}"));
+ let Some(destination) = hostless_runtime_path(&source_path.to_string_lossy(), platform)
+ else {
+ continue;
+ };
+ if destination == Path::new(platform.binary_name) {
+ continue;
+ }
+ let mut bytes = Vec::with_capacity(entry.size() as usize);
+ entry
+ .read_to_end(&mut bytes)
+ .unwrap_or_else(|e| panic!("failed to read npm package entry bytes: {e}"));
+ let executable = entry.header().mode().unwrap_or(0o644) & 0o111 != 0;
+ if destination == Path::new("runtime.node") {
+ runtime = Some(bytes.clone());
+ }
+ install_cached_file_path(install_dir, &destination, &bytes, executable);
+ }
+ let runtime = runtime.expect("verified runtime.node is present");
+ if include_runtime {
+ install_cached_file(
+ install_dir,
+ platform.runtime_library_name(),
+ &runtime,
+ false,
+ );
+ }
+ install_cached_file(
+ install_dir,
+ ".hostless-runtime-assets-v1",
+ marker.as_bytes(),
+ false,
+ );
+
+ let final_path = install_dir.join(platform.runtime_wrapper_name());
+ println!(
+ "cargo:warning=Extracted Copilot runtime bundle to {}",
+ install_dir.display()
+ );
+ final_path
+}
+
+fn install_cached_file(install_dir: &Path, file_name: &str, bytes: &[u8], executable: bool) {
+ install_cached_file_path(install_dir, Path::new(file_name), bytes, executable);
+}
+fn install_cached_file_path(
+ install_dir: &Path,
+ relative_path: &Path,
+ bytes: &[u8],
+ executable: bool,
+) {
+ // `executable` only affects file permissions on Unix (see the `#[cfg(unix)]`
+ // block below); explicitly mark it used elsewhere so non-Unix targets don't
+ // warn about an unused parameter under `-D warnings`.
+ #[cfg(not(unix))]
+ let _ = executable;
+
+ assert!(
+ !relative_path.is_absolute()
+ && !relative_path.components().any(|component| {
+ matches!(
+ component,
+ std::path::Component::Prefix(_)
+ | std::path::Component::RootDir
+ | std::path::Component::ParentDir
+ )
+ }),
+ "unsafe runtime package path: {}",
+ relative_path.display()
+ );
+ let final_path = install_dir.join(relative_path);
+ if final_path.is_file() {
+ return;
+ }
+ std::fs::create_dir_all(final_path.parent().expect("runtime asset has parent"))
+ .unwrap_or_else(|e| panic!("failed to create runtime asset directory: {e}"));
// Staging file is a sibling of the final binary so the rename stays
// on the same filesystem (cross-fs rename is not atomic). PID + nanos
// disambiguate concurrent builds racing on the same cache.
@@ -405,7 +608,10 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P
.unwrap_or(0);
let staging_path = install_dir.join(format!(
".{}.staging-{}-{nanos}",
- platform.binary_name,
+ relative_path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .unwrap_or("runtime-asset"),
std::process::id(),
));
@@ -418,7 +624,7 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P
);
});
- if let Err(e) = f.write_all(&bytes) {
+ if let Err(e) = f.write_all(bytes) {
let _ = std::fs::remove_file(&staging_path);
panic!(
"failed to write staging file {}: {e}",
@@ -427,7 +633,7 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P
}
#[cfg(unix)]
- {
+ if executable {
use std::os::unix::fs::PermissionsExt;
if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) {
let _ = std::fs::remove_file(&staging_path);
@@ -472,32 +678,6 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P
final_path.display()
);
}
-
- // Surface where the binary landed so contributors can find it. Quiet
- // on the hot path: the caller's `is_file()` short-circuit (and the
- // safety net at the top of this function) means this only fires on a
- // true cache miss.
- println!(
- "cargo:warning=Extracted Copilot CLI to {}",
- final_path.display()
- );
-
- final_path
-}
-
-fn extract_runtime_library_bytes(archive: &[u8]) -> Option> {
- let gz = flate2::read::GzDecoder::new(archive);
- let mut tar = tar::Archive::new(gz);
- for entry in tar.entries().ok()? {
- let mut entry = entry.ok()?;
- let name = entry.path().ok()?.to_string_lossy().into_owned();
- if name == "runtime.node" || name.ends_with("/runtime.node") {
- let mut bytes = Vec::with_capacity(entry.size() as usize);
- entry.read_to_end(&mut bytes).ok()?;
- return Some(bytes);
- }
- }
- None
}
/// Replace characters outside `[a-zA-Z0-9._-]` with `_` so the version
@@ -514,37 +694,6 @@ fn sanitize_version(version: &str) -> String {
.collect()
}
-/// Extract the single `binary_name` entry from the npm package archive. Reused
-/// between embed mode's `verify_binary_present_in_archive` and the
-/// `extract_to_cache` path used when `bundled-cli` is off. Panics if the
-/// entry isn't found — callers have already invoked
-/// `verify_binary_present_in_archive`.
-fn extract_binary_bytes(archive: &[u8], platform: Platform) -> Vec {
- let gz = flate2::read::GzDecoder::new(archive);
- let mut tar = tar::Archive::new(gz);
- for entry in tar
- .entries()
- .unwrap_or_else(|e| panic!("failed to read tar entries: {e}"))
- {
- let mut entry = entry.unwrap_or_else(|e| panic!("failed to read tar entry: {e}"));
- let path = entry
- .path()
- .unwrap_or_else(|e| panic!("failed to read tar entry path: {e}"));
- let name = path.to_string_lossy().into_owned();
- if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) {
- let mut bytes = Vec::with_capacity(entry.size() as usize);
- entry
- .read_to_end(&mut bytes)
- .unwrap_or_else(|e| panic!("failed to read tar entry bytes: {e}"));
- return bytes;
- }
- }
- panic!(
- "binary `{}` not found in package `{}`",
- platform.binary_name, platform.package_name
- );
-}
-
/// Read a file from the download cache, or download it (with retries) and save
/// to cache. Verifies npm integrity on every path. Evicts stale/corrupt cache entries
/// automatically. Cache I/O failures are treated as cache misses — they never
@@ -682,15 +831,17 @@ fn try_download(url: &str) -> Result, DownloadError> {
}
}
-/// Walks the downloaded archive at build time to confirm an entry matching
-/// `binary_name` exists. Panics with a clear message if not.
-fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, package_name: &str) {
- let found = archive_contains_tar_entry(archive, binary_name);
- if !found {
+fn verify_runtime_package(archive: &[u8], platform: Platform, package_name: &str) {
+ for file_name in [
+ platform.binary_name,
+ "runtime.node",
+ platform.runtime_wrapper_name(),
+ ] {
+ if archive_contains_tar_entry(archive, file_name) {
+ continue;
+ }
panic!(
- "Copilot CLI package `{package_name}` does not contain an entry named `{binary_name}`. \
- The package layout may have changed; runtime extraction would fail. \
- Update `verify_binary_present_in_archive` in build.rs and the matching `extract_binary` in src/embeddedcli.rs."
+ "Copilot runtime package `{package_name}` does not contain an entry named `{file_name}`"
);
}
}
diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs
index 40900a4d22..3cc527a2e2 100644
--- a/rust/src/embeddedcli.rs
+++ b/rust/src/embeddedcli.rs
@@ -3,10 +3,10 @@
//! feature set).
//!
//! Normal builds embed the platform release archive from GitHub Releases.
-//! Builds with `bundled-in-process` instead embed a minimal archive from the
-//! platform npm package containing the CLI executable and native runtime
-//! library. Extraction to a real on-disk path is deferred until the first call
-//! to [`path`] / [`install_at`].
+//! Builds with `bundled-in-process` instead embed a filtered archive from the
+//! platform npm package containing the CLI executable, runtime wrapper, native
+//! runtime artifacts, and auxiliary runtime assets. Extraction to a real
+//! on-disk path is deferred until the relevant installer is called.
//!
//! The embedded bytes are part of the consumer's signed binary and therefore
//! trusted *as the source of truth* — but the bytes that land on disk are not.
@@ -28,7 +28,7 @@
// off but still needs to exercise them.
#[cfg(any(has_bundled_cli, test))]
use std::fs;
-#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))]
+#[cfg(has_bundled_cli)]
use std::io::Read;
#[cfg(any(has_bundled_cli, test))]
use std::io::Write;
@@ -65,9 +65,19 @@ const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION");
const CLI_BINARY_NAME: &str = "copilot.exe";
#[cfg(all(has_bundled_cli, not(windows)))]
const CLI_BINARY_NAME: &str = "copilot";
+#[cfg(all(has_bundled_cli, windows))]
+const RUNTIME_BINARY_NAME: &str = "copilot-runtime.exe";
+#[cfg(all(has_bundled_cli, not(windows)))]
+const RUNTIME_BINARY_NAME: &str = "copilot-runtime";
+#[cfg(has_bundled_cli)]
+const RUNTIME_NODE_NAME: &str = "runtime.node";
+#[cfg(has_bundled_cli)]
+const RUNTIME_VERSION_MARKER: &str = ".copilot-runtime-version";
#[cfg(feature = "bundled-cli")]
static INSTALLED_PATH: OnceLock