diff --git a/.lastmerge b/.lastmerge index d40c7b3af..f0d3f0289 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -e42b726ca42bd1b2e099a956c9287ba9435ba3e5 +ced6613253a595769d2e77547f9e1caf6bef6438 diff --git a/pom.xml b/pom.xml index b0b442b61..e25102036 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ reference-impl-sync workflow and deal with the subsequent PR. --> - ^1.0.40-0 + ^1.0.41-0 diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index 73aa2cdc7..b45b9c668 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.40-0", + "@github/copilot": "^1.0.41-0", "json-schema": "^0.4.0", "tsx": "^4.20.6" } @@ -428,26 +428,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.40-0.tgz", - "integrity": "sha512-KIrRqPOCGPcYUq09wvi66qUi5YMFTH5z9fOEzo1BuyLFVQqUen0TtRk0mpbhG6TxArLPqosBY8nDXOdc+NqKKw==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.41-0.tgz", + "integrity": "sha512-gLyCadBZdJeJtHJI3XdN8wAmLMEUdXfCa3EcVnbdbV1NHZDAJhr7h41l7a49pqRAmJyLUKlk1Lokk7U+OD3tgw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.40-0", - "@github/copilot-darwin-x64": "1.0.40-0", - "@github/copilot-linux-arm64": "1.0.40-0", - "@github/copilot-linux-x64": "1.0.40-0", - "@github/copilot-win32-arm64": "1.0.40-0", - "@github/copilot-win32-x64": "1.0.40-0" + "@github/copilot-darwin-arm64": "1.0.41-0", + "@github/copilot-darwin-x64": "1.0.41-0", + "@github/copilot-linux-arm64": "1.0.41-0", + "@github/copilot-linux-x64": "1.0.41-0", + "@github/copilot-win32-arm64": "1.0.41-0", + "@github/copilot-win32-x64": "1.0.41-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.40-0.tgz", - "integrity": "sha512-l905DiMvOB7Jn5MAkS+iEQcM99hmJHQ5yrKaGJ9OteVWBCcxPEO5ctnRYFdeq4NHL+9OnAzJzNc0kwScOAUCzg==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.41-0.tgz", + "integrity": "sha512-lrrH1oMbTOF1W/YxH6rvoEHOymxmXaMx4aDzm190hU0Yh6Cuu0BJGFvgG8nE9bqcv5O8W7eEBr26jDlGtnZiwg==", "cpu": [ "arm64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.40-0.tgz", - "integrity": "sha512-e/Dtc1CjZ5SpNLdtY7vHxzH3hhNKfiMJdyp/2UY/6rzXsSPfbw9xZL01nUBbZt0aYj2FbPBDMTyfqoh5weUSww==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.41-0.tgz", + "integrity": "sha512-4418VtSSkEgn4BcwCFg+0UDhGCfQgGTx16r/PiWbuUOgIBzts3FfVzWMWTuXyxk7kl2Ib8k7KSd/7rNpjcrzBw==", "cpu": [ "x64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.40-0.tgz", - "integrity": "sha512-fR/gVUXFpjwojNf3Pg5lH2vrb8q0Gghv6RK6xx1QS6pEBdPTMGeoPxQVqSSB6dZCR/X3dkK1tIZ5IGnuABDHbA==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.41-0.tgz", + "integrity": "sha512-5xjgp3Ak5QJ68byNbsgBpdK1V6T5t8EGu0pUwEJMNMMXxqvL9f7gPcnCGdTtV2DS4Q3adkziV/gpBSSQ5HY8hg==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.40-0.tgz", - "integrity": "sha512-vDfE5Cxgg+BealbxBjqa0MUrLGJF+zT+XygMFgWXi/4ESVpRvvAet8rUoLeL+7Lgg6QRlS+UMPWfWL6ZAoSp5w==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.41-0.tgz", + "integrity": "sha512-oWPkj0bSjBjtAqonMEZD7EuSByBNXwtceMw8y7uGOfs6jQXfhDGzCCB6NGb+lcftVNtWDKFCUtx+x8Fbt4O37w==", "cpu": [ "x64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.40-0.tgz", - "integrity": "sha512-7aEo1CZ5476lWRtfcym0TX1DhH5l9+PKENHPOr8vfziHKeNlWYkXHVTo7OGIUnCAwIetbCQRyf92nnaFhG/8Jw==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.41-0.tgz", + "integrity": "sha512-MaPg4tFWTiRuyv+j0ymJbZp8UPK+RIXNMpekR7FRf8/Uz+NiJgTTxTDjFi4ytRJU5UNrUezkVAk5Xduq/CaIew==", "cpu": [ "arm64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.40-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.40-0.tgz", - "integrity": "sha512-egnoilEO6TS0qSexe+A26GUSyfeinaqXJRTYL4Qr6eVgAfFhCEpCJtK/gHIZmlDXnq2ex3jGrVvz9+ZIp+JSoA==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.41-0.tgz", + "integrity": "sha512-ykRuDWjJEgSywMFJl1yaefssaklCVSVhprx2NcSVh6tIGupvvzVAM6nL6Mj6nyKpG6FKGHanedBeL6SJc935cw==", "cpu": [ "x64" ], diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index b2af72d6f..d146fd945 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.40-0", + "@github/copilot": "^1.0.41-0", "json-schema": "^0.4.0", "tsx": "^4.20.6" } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java index a6d75a8f6..2ff43e67a 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java @@ -56,6 +56,8 @@ public record AssistantMessageEventData( @JsonProperty("interactionId") String interactionId, /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ @JsonProperty("requestId") String requestId, + /** Identifier for the agent loop turn that produced this message, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java new file mode 100644 index 000000000..8cf1a9c8e --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code assistant.message_start} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantMessageStartEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.message_start"; } + + @JsonProperty("data") + private AssistantMessageStartEventData data; + + public AssistantMessageStartEventData getData() { return data; } + public void setData(AssistantMessageStartEventData data) { this.data = data; } + + /** Data payload for {@link AssistantMessageStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantMessageStartEventData( + /** Message ID this start event belongs to, matching subsequent deltas and assistant.message */ + @JsonProperty("messageId") String messageId, + /** Generation phase this message belongs to for phased-output models */ + @JsonProperty("phase") String phase + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java index 4bb8aa92c..ee3e9f9cf 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java @@ -24,7 +24,7 @@ public record AssistantUsageCopilotUsage( /** Itemized token usage breakdown */ @JsonProperty("tokenDetails") List tokenDetails, - /** Total cost in nano-AIU (AI Units) for this request */ + /** Total cost in nano-AI units for this request */ @JsonProperty("totalNanoAiu") Double totalNanoAiu ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java b/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java index 7e02e4f77..76e5a0ed8 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java +++ b/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java @@ -24,7 +24,7 @@ public record CompactionCompleteCompactionTokensUsedCopilotUsage( /** Itemized token usage breakdown */ @JsonProperty("tokenDetails") List tokenDetails, - /** Total cost in nano-AIU (AI Units) for this request */ + /** Total cost in nano-AI units for this request */ @JsonProperty("totalNanoAiu") Double totalNanoAiu ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java b/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java index b8c0dd93d..b037b82ac 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java +++ b/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java @@ -24,6 +24,8 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, /** Whether this is a public OAuth client */ - @JsonProperty("publicClient") Boolean publicClient + @JsonProperty("publicClient") Boolean publicClient, + /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ + @JsonProperty("grantType") String grantType ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java index 0045393c1..0b8da105e 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java @@ -40,7 +40,7 @@ public record PermissionCompletedEventData( /** Optional tool call ID associated with this permission prompt; clients may use it to correlate UI created from tool-scoped prompts */ @JsonProperty("toolCallId") String toolCallId, /** The result of the permission request */ - @JsonProperty("result") PermissionCompletedResult result + @JsonProperty("result") Object result ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java index 89682baec..02364db4f 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java @@ -54,6 +54,7 @@ @JsonSubTypes.Type(value = AssistantReasoningDeltaEvent.class, name = "assistant.reasoning_delta"), @JsonSubTypes.Type(value = AssistantStreamingDeltaEvent.class, name = "assistant.streaming_delta"), @JsonSubTypes.Type(value = AssistantMessageEvent.class, name = "assistant.message"), + @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), @@ -134,6 +135,7 @@ public abstract sealed class SessionEvent permits AssistantReasoningDeltaEvent, AssistantStreamingDeltaEvent, AssistantMessageEvent, + AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java index 7973e7d17..31bd3b4c3 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java @@ -48,8 +48,12 @@ public record SessionResumeEventData( @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ @JsonProperty("alreadyInUse") Boolean alreadyInUse, + /** True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes — the runtime had to reconstitute the session from its persisted event log. */ + @JsonProperty("sessionWasActive") Boolean sessionWasActive, /** Whether this session supports remote steering via Mission Control */ - @JsonProperty("remoteSteerable") Boolean remoteSteerable + @JsonProperty("remoteSteerable") Boolean remoteSteerable, + /** When true, tool calls and permission requests left in flight by the previous session lifetime remain pending after resume and the agentic loop awaits their results. User sends are queued behind the pending work until all such requests reach a terminal state. When false (the default), any such tool calls and permission requests are immediately marked as interrupted on resume. */ + @JsonProperty("continuePendingWork") Boolean continuePendingWork ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java index 39b6cfcfa..97d5060d3 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java @@ -42,6 +42,10 @@ public record SessionShutdownEventData( @JsonProperty("errorReason") String errorReason, /** Total number of premium API requests used during the session */ @JsonProperty("totalPremiumRequests") Double totalPremiumRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, /** Cumulative time spent in API calls during the session, in milliseconds */ @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, /** Unix timestamp (milliseconds) when the session started */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java index e5cae1476..e7a831ca6 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import javax.annotation.processing.Generated; @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -19,6 +20,10 @@ public record ShutdownModelMetric( /** Request count and cost metrics */ @JsonProperty("requests") ShutdownModelMetricRequests requests, /** Token usage breakdown */ - @JsonProperty("usage") ShutdownModelMetricUsage usage + @JsonProperty("usage") ShutdownModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java new file mode 100644 index 000000000..9f7cde630 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java new file mode 100644 index 000000000..ec5180488 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java index 6481374d1..a1deeeab5 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java @@ -52,6 +52,8 @@ public record ToolExecutionCompleteEventData( @JsonProperty("error") ToolExecutionCompleteError error, /** Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) */ @JsonProperty("toolTelemetry") Map toolTelemetry, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java index 7da757286..5a48cdd97 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java @@ -45,6 +45,8 @@ public record ToolExecutionStartEventData( @JsonProperty("mcpServerName") String mcpServerName, /** Original tool name on the MCP server, when the tool is an MCP tool */ @JsonProperty("mcpToolName") String mcpToolName, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java index 5d156082d..ec5f38246 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java @@ -51,7 +51,9 @@ public record UserMessageEventData( /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, /** CAPI interaction ID for correlating this user message with its turn */ - @JsonProperty("interactionId") String interactionId + @JsonProperty("interactionId") String interactionId, + /** Parent agent task ID for background telemetry correlated to this user turn */ + @JsonProperty("parentAgentTaskId") String parentAgentTaskId ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java new file mode 100644 index 000000000..665cd9694 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code connect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectParams( + /** Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ + @JsonProperty("token") String token +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java new file mode 100644 index 000000000..56666b252 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code connect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectResult( + /** Always true on success */ + @JsonProperty("ok") Boolean ok, + /** Server protocol version number */ + @JsonProperty("protocolVersion") Long protocolVersion, + /** Server package version */ + @JsonProperty("version") String version +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java index d2e316100..b82c49ec2 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java @@ -63,4 +63,12 @@ public CompletableFuture ping(PingParams params) { return caller.invoke("ping", params, PingResult.class); } + /** + * Invokes {@code connect}. + * @since 1.0.0 + */ + public CompletableFuture connect(ConnectParams params) { + return caller.invoke("connect", params, ConnectResult.class); + } + } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java index 1dbd006c9..d4934fc08 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java @@ -103,6 +103,14 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.usage = new SessionUsageApi(caller, sessionId); } + /** + * Invokes {@code session.suspend}. + * @since 1.0.0 + */ + public CompletableFuture suspend() { + return caller.invoke("session.suspend", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + /** * Invokes {@code session.log}. *

diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java new file mode 100644 index 000000000..300b1e4cb --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.suspend} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSuspendParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java index 9cfdbcfc8..cb1897d27 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java @@ -26,6 +26,10 @@ public record SessionUsageGetMetricsResult( @JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost, /** Raw count of user-initiated API requests */ @JsonProperty("totalUserRequests") Long totalUserRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, /** Total time spent in model API calls (milliseconds) */ @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, /** Session start timestamp (epoch milliseconds) */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java index 8334872cb..844d4a1bf 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import javax.annotation.processing.Generated; @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -19,6 +20,10 @@ public record UsageMetricsModelMetric( /** Request count and cost metrics for this model */ @JsonProperty("requests") UsageMetricsModelMetricRequests requests, /** Token usage metrics for this model */ - @JsonProperty("usage") UsageMetricsModelMetricUsage usage + @JsonProperty("usage") UsageMetricsModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java new file mode 100644 index 000000000..51ef4f3ee --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java new file mode 100644 index 000000000..1f763f028 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 07b5eefec..b6009e839 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -33,11 +33,23 @@ final class CliServerManager { private final CopilotClientOptions options; private final StringBuilder stderrBuffer = new StringBuilder(); + private String connectionToken; CliServerManager(CopilotClientOptions options) { this.options = options; } + /** + * Sets the connection token to pass to the CLI process via environment + * variable. + * + * @param connectionToken + * the token, or {@code null} if not applicable + */ + void setConnectionToken(String connectionToken) { + this.connectionToken = connectionToken; + } + /** * Starts the CLI server process. * @@ -115,6 +127,16 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); } + // Set Copilot home directory if configured + if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { + pb.environment().put("COPILOT_HOME", options.getCopilotHome()); + } + + // Set connection token for TCP mode + if (connectionToken != null && !connectionToken.isEmpty()) { + pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); + } + // Set telemetry environment variables if configured if (options.getTelemetry() != null) { var telemetry = options.getTelemetry(); diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index ebe995bd7..555e88861 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -21,6 +21,7 @@ import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.CreateSessionResponse; +import com.github.copilot.sdk.generated.rpc.ConnectParams; import com.github.copilot.sdk.generated.rpc.ServerRpc; import com.github.copilot.sdk.json.DeleteSessionResponse; import com.github.copilot.sdk.json.GetAuthStatusResponse; @@ -83,6 +84,7 @@ public final class CopilotClient implements AutoCloseable { private volatile boolean disposed = false; private final String optionsHost; private final Integer optionsPort; + private final String effectiveConnectionToken; private volatile List modelsCache; private final Object modelsCacheLock = new Object(); @@ -122,6 +124,24 @@ public CopilotClient(CopilotClientOptions options) { "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); } + // Validate tcpConnectionToken + if (this.options.getTcpConnectionToken() != null) { + if (this.options.getTcpConnectionToken().isEmpty()) { + throw new IllegalArgumentException("TcpConnectionToken must be a non-empty string"); + } + if (this.options.isUseStdio()) { + throw new IllegalArgumentException("TcpConnectionToken cannot be used with UseStdio = true"); + } + } + + // Compute effective connection token: use provided, or auto-generate for + // SDK-spawned TCP mode, or null for stdio/external server + boolean sdkSpawnsCli = !this.options.isUseStdio() + && (this.options.getCliUrl() == null || this.options.getCliUrl().isEmpty()); + this.effectiveConnectionToken = this.options.getTcpConnectionToken() != null + ? this.options.getTcpConnectionToken() + : (sdkSpawnsCli ? java.util.UUID.randomUUID().toString() : null); + // Parse CliUrl if provided if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl()); @@ -133,6 +153,7 @@ public CopilotClient(CopilotClientOptions options) { } this.serverManager = new CliServerManager(this.options); + this.serverManager.setConnectionToken(this.effectiveConnectionToken); } /** @@ -202,23 +223,49 @@ private Connection startCoreBody() { } private static final int MIN_PROTOCOL_VERSION = 2; + private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601; private void verifyProtocolVersion(Connection connection) throws Exception { int expectedVersion = SdkProtocolVersion.get(); - var params = new HashMap(); - params.put("message", null); - PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS); + Integer serverVersion; + + try { + // Try the new 'connect' RPC which supports connection tokens + var connectParams = new ConnectParams(effectiveConnectionToken); + var connectResponse = connection.rpc + .invoke("connect", connectParams, com.github.copilot.sdk.generated.rpc.ConnectResult.class) + .get(30, TimeUnit.SECONDS); + serverVersion = connectResponse.protocolVersion() != null + ? connectResponse.protocolVersion().intValue() + : null; + } catch (Exception e) { + // Unwrap CompletionException/ExecutionException to check inner cause + Throwable cause = e; + while (cause instanceof java.util.concurrent.ExecutionException || cause instanceof CompletionException) { + cause = cause.getCause(); + } + if (cause instanceof JsonRpcException rpcEx && rpcEx.getCode() == METHOD_NOT_FOUND_ERROR_CODE) { + // Legacy server without 'connect'; fall back to 'ping'. + // A token, if any, is silently dropped — the legacy server can't enforce one. + var params = new HashMap(); + params.put("message", null); + PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, + TimeUnit.SECONDS); + serverVersion = pingResponse.protocolVersion(); + } else { + throw e; + } + } - if (pingResponse.protocolVersion() == null) { - throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + ", but server does not report a protocol version. " + if (serverVersion == null) { + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server does not report a protocol version. " + "Please update your server to ensure compatibility."); } - int serverVersion = pingResponse.protocolVersion(); if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) { - throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + " (minimum " + MIN_PROTOCOL_VERSION + "), but server reports version " + serverVersion + ". " + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server reports version " + serverVersion + ". " + "Please update your SDK or server to ensure compatibility."); } } diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index de6977ea8..91dbd4642 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -122,6 +122,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setAgent(config.getAgent()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setConfigDir(config.getConfigDir()); request.setEnableConfigDiscovery(config.getEnableConfigDiscovery()); @@ -199,6 +200,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setDefaultAgent(config.getDefaultAgent()); request.setAgent(config.getAgent()); request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setModelCapabilities(config.getModelCapabilities()); diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 3a14d1733..dff72581a 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -44,6 +44,7 @@ public class CopilotClientOptions { private String[] cliArgs; private String cliPath; private String cliUrl; + private String copilotHome; private String cwd; private Map environment; private Executor executor; @@ -53,6 +54,7 @@ public class CopilotClientOptions { private int port; private TelemetryConfig telemetry; private Integer sessionIdleTimeoutSeconds; + private String tcpConnectionToken; private Boolean useLoggedInUser; private boolean useStdio = true; @@ -191,6 +193,37 @@ public CopilotClientOptions setCliUrl(String cliUrl) { return this; } + /** + * Gets the base directory for Copilot data (session state, config, etc.). + * + * @return the Copilot home directory path, or {@code null} to use the CLI + * default ({@code ~/.copilot}) + */ + public String getCopilotHome() { + return copilotHome; + } + + /** + * Sets the base directory for Copilot data (session state, config, etc.). + *

+ * Sets the {@code COPILOT_HOME} environment variable on the spawned CLI + * process. When {@code null}, the {@code COPILOT_HOME} env var is not set on + * the spawned process, so the CLI falls back to its default + * ({@code ~/.copilot}). + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param copilotHome + * the Copilot home directory path, or {@code null} to use the CLI + * default + * @return this options instance for method chaining + */ + public CopilotClientOptions setCopilotHome(String copilotHome) { + this.copilotHome = copilotHome; + return this; + } + /** * Gets the working directory for the CLI process. * @@ -462,6 +495,31 @@ public CopilotClientOptions setSessionIdleTimeoutSeconds(Integer sessionIdleTime return this; } + /** + * Gets the connection token for the headless CLI server (TCP only). + * + * @return the connection token, or {@code null} if not set + */ + public String getTcpConnectionToken() { + return tcpConnectionToken; + } + + /** + * Sets the connection token for the headless CLI server (TCP only). + *

+ * When the SDK spawns its own CLI in TCP mode and this is omitted, a UUID is + * generated automatically so the loopback listener is safe by default. Cannot + * be combined with {@link #setUseStdio(boolean)} = {@code true}. + * + * @param tcpConnectionToken + * the connection token (must not be {@code null} or empty) + * @return this options instance for method chaining + */ + public CopilotClientOptions setTcpConnectionToken(String tcpConnectionToken) { + this.tcpConnectionToken = Objects.requireNonNull(tcpConnectionToken, "tcpConnectionToken must not be null"); + return this; + } + /** * Returns whether to use the logged-in user for authentication. * @@ -533,6 +591,7 @@ public CopilotClientOptions clone() { copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null; copy.cliPath = this.cliPath; copy.cliUrl = this.cliUrl; + copy.copilotHome = this.copilotHome; copy.cwd = this.cwd; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; copy.executor = this.executor; @@ -541,6 +600,7 @@ public CopilotClientOptions clone() { copy.onListModels = this.onListModels; copy.port = this.port; copy.sessionIdleTimeoutSeconds = this.sessionIdleTimeoutSeconds; + copy.tcpConnectionToken = this.tcpConnectionToken; copy.telemetry = this.telemetry; copy.useLoggedInUser = this.useLoggedInUser; copy.useStdio = this.useStdio; diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index ef8d5fda2..5243f99ec 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -91,6 +91,9 @@ public final class CreateSessionRequest { @JsonProperty("skillDirectories") private List skillDirectories; + @JsonProperty("instructionDirectories") + private List instructionDirectories; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -326,6 +329,18 @@ public void setSkillDirectories(List skillDirectories) { this.skillDirectories = skillDirectories; } + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index b4dacf370..3f0a0706d 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -59,6 +59,7 @@ public class ResumeSessionConfig { private DefaultAgentConfig defaultAgent; private String agent; private List skillDirectories; + private List instructionDirectories; private List disabledSkills; private InfiniteSessionConfig infiniteSessions; private Consumer onEvent; @@ -591,6 +592,27 @@ public ResumeSessionConfig setSkillDirectories(List skillDirectories) { return this; } + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + /** * Gets the disabled skills. * @@ -775,6 +797,9 @@ public ResumeSessionConfig clone() { copy.defaultAgent = this.defaultAgent; copy.agent = this.agent; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; copy.onEvent = this.onEvent; diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index e36e90b67..c5931c275 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -98,6 +98,9 @@ public final class ResumeSessionRequest { @JsonProperty("skillDirectories") private List skillDirectories; + @JsonProperty("instructionDirectories") + private List instructionDirectories; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -366,6 +369,18 @@ public void setSkillDirectories(List skillDirectories) { this.skillDirectories = skillDirectories; } + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 09661346a..4d571990c 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -57,6 +57,7 @@ public class SessionConfig { private String agent; private InfiniteSessionConfig infiniteSessions; private List skillDirectories; + private List instructionDirectories; private List disabledSkills; private String configDir; private Boolean enableConfigDiscovery; @@ -550,6 +551,27 @@ public SessionConfig setSkillDirectories(List skillDirectories) { return this; } + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config instance for method chaining + */ + public SessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + /** * Gets the disabled skill names. * @@ -825,6 +847,9 @@ public SessionConfig clone() { copy.agent = this.agent; copy.infiniteSessions = this.infiniteSessions; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDir = this.configDir; copy.enableConfigDiscovery = this.enableConfigDiscovery; diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index a4c4d830a..e1e08a275 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -626,6 +626,31 @@ var session = client.createSession( --- +## Instruction Directories + +Provide additional directories containing custom instruction files. These instructions are automatically included in the system message for all conversations in the session. + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +).get(); +``` + +Instruction files are discovered from `.github/instructions/` subdirectories within each specified path and should use the `.instructions.md` extension. + +This is also supported on session resume: + +```java +var session = client.resumeSession(sessionId, + new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +); +``` + +--- + ## Custom Configuration Directory Use a custom configuration directory for session settings: diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index 10c1cb3d8..224fc1948 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -157,12 +157,28 @@ try (var client = new CopilotClient(options)) { Multiple application instances can share a single CLI server: ```java -// In different parts of your application or different containers -var client1 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); -var client2 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); +// Use an explicit connection token so all clients can authenticate +var token = "my-shared-secret"; +var client1 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); +var client2 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); // Both connect to the same CLI server ``` +### Connection Token (TCP Security) + +When the SDK spawns the CLI in TCP mode, a random connection token is generated automatically +to protect the loopback listener. You can also provide an explicit token: + +```java +var options = new CopilotClientOptions() + .setUseStdio(false) + .setTcpConnectionToken("my-secret-token"); +``` + +> **Note:** `tcpConnectionToken` cannot be used with `useStdio = true`. + ### Deployment Patterns **Container deployment:** @@ -340,10 +356,12 @@ Complete list of `CopilotClientOptions` settings: | `cliPath` | String | Path to CLI executable | `"copilot"` from PATH | | `cliUrl` | String | External CLI server URL | `null` (spawn process) | | `cliArgs` | String[] | Extra CLI arguments | `null` | +| `copilotHome` | String | Base directory for Copilot data | `null` (~/.copilot) | | `gitHubToken` | String | GitHub OAuth token | `null` | | `useLoggedInUser` | Boolean | Use system credentials | `true` | | `useStdio` | boolean | Use stdio transport | `true` | | `port` | int | TCP port for CLI | `0` (random) | +| `tcpConnectionToken` | String | Connection token for TCP mode | `null` (auto-generated) | | `autoStart` | boolean | Auto-start server | `true` | | `autoRestart` | boolean | Auto-restart on crash | `true` | | `logLevel` | String | CLI log level | `"info"` | diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index f4f702f9e..74bee7b50 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -36,6 +36,9 @@ void copilotClientOptionsCloneBasic() { original.setPort(9000); original.setGitHubToken("ghp_test"); original.setUseLoggedInUser(false); + original.setCopilotHome("/custom/copilot/home"); + original.setUseStdio(false); + original.setTcpConnectionToken("my-token-123"); CopilotClientOptions cloned = original.clone(); @@ -44,6 +47,8 @@ void copilotClientOptionsCloneBasic() { assertEquals(original.getPort(), cloned.getPort()); assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); + assertEquals(original.getCopilotHome(), cloned.getCopilotHome()); + assertEquals(original.getTcpConnectionToken(), cloned.getTcpConnectionToken()); } @Test @@ -120,6 +125,7 @@ void sessionConfigListIndependence() { toolList.add("grep"); toolList.add("bash"); original.setAvailableTools(toolList); + original.setInstructionDirectories(new ArrayList<>(List.of("/path/a", "/path/b"))); SessionConfig cloned = original.clone(); @@ -129,6 +135,7 @@ void sessionConfigListIndependence() { // The cloned config should be unaffected by mutations to the original list assertEquals(2, cloned.getAvailableTools().size()); assertEquals(3, original.getAvailableTools().size()); + assertEquals(List.of("/path/a", "/path/b"), cloned.getInstructionDirectories()); } @Test diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 1cc067587..b0430959b 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -199,6 +199,30 @@ void testSessionIdleTimeoutSecondsOptionAccepted() { assertEquals(600, options.getSessionIdleTimeoutSeconds()); } + @Test + void testTcpConnectionTokenWithUseStdioThrows() { + var options = new CopilotClientOptions().setUseStdio(true).setTcpConnectionToken("my-token"); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testTcpConnectionTokenAcceptedInTcpMode() { + var options = new CopilotClientOptions().setUseStdio(false).setTcpConnectionToken("my-token"); + + // Should not throw + try (var client = new CopilotClient(options)) { + assertNotNull(client); + } + } + + @Test + void testCopilotHomeOptionSetOnOptions() { + var options = new CopilotClientOptions().setCopilotHome("/custom/home"); + + assertEquals("/custom/home", options.getCopilotHome()); + } + // ===== onLifecycle tests ===== /** diff --git a/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java new file mode 100644 index 000000000..5dcb36604 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for session configuration features. + */ +public class SessionConfigE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnCreate() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_create"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-create-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-create-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnResume() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_resume"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-resume-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-resume-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_RESUME_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client.createSession(new SessionConfig() + .setWorkingDirectory(projectDir.toString()).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + // Resume with instructionDirectories + CopilotSession session2 = client.resumeSession(session1.getSessionId(), + new ResumeSessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session2.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @SuppressWarnings("unchecked") + private static String getSystemMessage(Map exchange) { + // The exchange structure is: { request: { messages: [...] }, response: ..., + // requestHeaders: ... } + Object requestObj = exchange.get("request"); + if (!(requestObj instanceof Map request)) { + return null; + } + Object messagesObj = request.get("messages"); + if (messagesObj instanceof List messages) { + for (Object msg : messages) { + if (msg instanceof Map msgMap) { + if ("system".equals(msgMap.get("role"))) { + Object content = msgMap.get("content"); + return content != null ? content.toString() : null; + } + } + } + } + return null; + } +} diff --git a/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java b/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java index 2da778358..5a9362f8d 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java @@ -2275,7 +2275,10 @@ void testParsePermissionCompletedEvent() throws Exception { assertNotNull(event); assertEquals("permission.completed", event.getType()); assertEquals("perm-req-456", event.getData().requestId()); - assertEquals(PermissionCompletedKind.APPROVED, event.getData().result().kind()); + assertNotNull(event.getData().result()); + @SuppressWarnings("unchecked") + var result = (java.util.Map) event.getData().result(); + assertEquals("approved", result.get("kind")); } @Test diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index 73ed6fffa..f31ca2d2d 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -865,7 +865,7 @@ private SessionStartEvent createSessionStartEvent(String sessionId) { private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); var data = new AssistantMessageEvent.AssistantMessageEventData(null, content, null, null, null, null, null, - null, null, null, null); + null, null, null, null, null); event.setData(data); return event; } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index abb4447f5..a79c02334 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -441,4 +441,28 @@ void testBuildResumeRequestWithGitHubToken() { assertEquals("ghp_per_session_token", request.getGitHubToken()); } + + // ========================================================================= + // instructionDirectories propagation + // ========================================================================= + + @Test + void testBuildCreateRequestPropagatesInstructionDirectories() { + var dirs = List.of("/path/to/instructions", "/another/path"); + var config = new SessionConfig().setInstructionDirectories(dirs); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(dirs, request.getInstructionDirectories()); + } + + @Test + void testBuildResumeRequestPropagatesInstructionDirectories() { + var dirs = List.of("/resume/instructions", "/other/dir"); + var config = new ResumeSessionConfig().setInstructionDirectories(dirs); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-inst", config); + + assertEquals(dirs, request.getInstructionDirectories()); + } } diff --git a/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 1a181fb24..48ade8ce1 100644 --- a/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -832,8 +832,8 @@ void sessionUiHandlePendingElicitationResult_record() { @Test void sessionUsageGetMetricsResult_nested() { var changes = new UsageMetricsCodeChanges(100L, 50L, 5L); - var result = new SessionUsageGetMetricsResult(0.5, 10L, 2000.0, 1700000000000L, changes, null, "gpt-5", 1000L, - 500L); + var result = new SessionUsageGetMetricsResult(0.5, 10L, null, null, 2000.0, 1700000000000L, changes, null, + "gpt-5", 1000L, 500L); assertEquals(0.5, result.totalPremiumRequestCost()); assertEquals(10L, result.totalUserRequests()); assertNotNull(result.codeChanges());