33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6- import type { internal , LocalSession , LocalSessionMetadata , Session , SessionContext , SessionEvent , SessionOptions , SweCustomAgent } from '@github/copilot/sdk' ;
6+ import type { AutoModeSessionManager as SDKAutoModeSessionManager , AutoModeSessionResult , internal , LocalSession , LocalSessionMetadata , Session , SessionContext , SessionEvent , SessionOptions , SweCustomAgent } from '@github/copilot/sdk' ;
77import * as l10n from '@vscode/l10n' ;
88import { createReadStream } from 'node:fs' ;
99import { devNull } from 'node:os' ;
@@ -53,6 +53,195 @@ import { INTEGRATION_ID } from '../../../../platform/endpoint/common/licenseAgre
5353
5454
5555const COPILOT_CLI_WORKSPACE_JSON_FILE_KEY = 'github.copilot.cli.workspaceSessionFile' ;
56+ const AUTO_MODE_REFRESH_LEAD_TIME_MS = 300 * 1000 ;
57+
58+ type SDKPackage = Awaited < ReturnType < ICopilotCLISDK [ 'getPackage' ] > > ;
59+ type AutoModeResolveArgs = Parameters < SDKAutoModeSessionManager [ 'resolve' ] > [ 0 ] ;
60+ type AutoModeResolveResult = Awaited < ReturnType < SDKAutoModeSessionManager [ 'resolve' ] > > ;
61+ type AutoModeListener = Parameters < SDKAutoModeSessionManager [ 'subscribe' ] > [ 0 ] ;
62+
63+ class AutoModeSessionManagerCompat {
64+
65+ private current : AutoModeSessionResult | undefined ;
66+ private previousConcreteModel : string | undefined ;
67+ private inflight : Promise < AutoModeResolveResult > | undefined ;
68+ private readonly listeners = new Set < AutoModeListener > ( ) ;
69+
70+ constructor ( private readonly sdkPackage : Pick < SDKPackage , 'AutoModeUnavailableError' | 'AutoModeUnsupportedError' | 'acquireAutoModeSession' | 'isAutoModel' | 'refreshAutoModeSession' > ) { }
71+
72+ recordPreviousConcreteModel ( modelId : string | undefined ) : void {
73+ if ( modelId && ! this . sdkPackage . isAutoModel ( modelId ) ) {
74+ this . previousConcreteModel = modelId ;
75+ }
76+ }
77+
78+ getLastResolved ( ) : string | undefined {
79+ return this . current ?. selectedModel ;
80+ }
81+
82+ getDiscountPercent ( ) : number | undefined {
83+ const discountedCosts = this . current ?. discountedCosts ;
84+ if ( ! discountedCosts ) {
85+ return undefined ;
86+ }
87+
88+ const selectedModelDiscount = this . current ?. selectedModel ? discountedCosts [ this . current . selectedModel ] : undefined ;
89+ if ( selectedModelDiscount !== undefined ) {
90+ return Math . round ( selectedModelDiscount * 100 ) ;
91+ }
92+
93+ const allDiscounts = Object . values ( discountedCosts ) ;
94+ if ( allDiscounts . length === 0 ) {
95+ return undefined ;
96+ }
97+
98+ return Math . round ( ( allDiscounts . reduce ( ( sum , discount ) => sum + discount , 0 ) / allDiscounts . length ) * 100 ) ;
99+ }
100+
101+ getPreviousConcreteModel ( ) : string | undefined {
102+ return this . previousConcreteModel ;
103+ }
104+
105+ subscribe ( listener : AutoModeListener ) : ( ) => void {
106+ this . listeners . add ( listener ) ;
107+ return ( ) => {
108+ this . listeners . delete ( listener ) ;
109+ } ;
110+ }
111+
112+ async resolve ( args : AutoModeResolveArgs ) : Promise < AutoModeResolveResult > {
113+ if ( this . isFresh ( ) && this . current ) {
114+ const current = this . current ;
115+ this . applySessionToken ( args . settings , current . sessionToken ) ;
116+ return { modelId : current . selectedModel , sessionToken : current . sessionToken } ;
117+ }
118+
119+ if ( this . inflight ) {
120+ const resolved = await this . inflight ;
121+ if ( resolved ) {
122+ this . applySessionToken ( args . settings , resolved . sessionToken ) ;
123+ }
124+
125+ return resolved ;
126+ }
127+
128+ this . inflight = this . doResolve ( args ) . finally ( ( ) => {
129+ this . inflight = undefined ;
130+ } ) ;
131+
132+ return this . inflight ;
133+ }
134+
135+ clear ( settings ?: AutoModeResolveArgs [ 'settings' ] ) : void {
136+ this . current = undefined ;
137+ if ( settings ) {
138+ this . clearSessionToken ( settings ) ;
139+ }
140+ this . notify ( ) ;
141+ }
142+
143+ handleModelChange ( prevModel : string | undefined , nextModel : string , settings ?: AutoModeResolveArgs [ 'settings' ] ) : void {
144+ if ( this . sdkPackage . isAutoModel ( nextModel ) && ! this . sdkPackage . isAutoModel ( prevModel ) ) {
145+ this . recordPreviousConcreteModel ( prevModel ) ;
146+ } else if ( ! this . sdkPackage . isAutoModel ( nextModel ) && this . sdkPackage . isAutoModel ( prevModel ) ) {
147+ this . clear ( settings ) ;
148+ }
149+ }
150+
151+ private notify ( ) : void {
152+ const resolvedModel = this . current ?. selectedModel ;
153+ const discountPercent = this . getDiscountPercent ( ) ;
154+ for ( const listener of this . listeners ) {
155+ try {
156+ listener ( resolvedModel , discountPercent ) ;
157+ } catch {
158+ // Ignore listener failures to mirror the SDK manager behavior.
159+ }
160+ }
161+ }
162+
163+ private async doResolve ( args : AutoModeResolveArgs ) : Promise < AutoModeResolveResult > {
164+ const { logger, settings } = args ;
165+
166+ if ( this . current ) {
167+ try {
168+ const refreshed = await this . sdkPackage . refreshAutoModeSession ( { ...args , existingToken : this . current . sessionToken } ) ;
169+ this . current = refreshed ;
170+ this . applySessionToken ( settings , refreshed . sessionToken ) ;
171+ this . notify ( ) ;
172+ return { modelId : refreshed . selectedModel , sessionToken : refreshed . sessionToken } ;
173+ } catch ( error ) {
174+ if ( this . isUnauthorizedError ( error ) ) {
175+ logger . debug ( 'Auto-mode refresh unauthorized; acquiring a new session' ) ;
176+ } else if ( error instanceof this . sdkPackage . AutoModeUnsupportedError ) {
177+ logger . debug ( `Auto-mode refresh unsupported: ${ error . message } ` ) ;
178+ this . current = undefined ;
179+ this . notify ( ) ;
180+ return undefined ;
181+ } else if ( error instanceof this . sdkPackage . AutoModeUnavailableError ) {
182+ logger . debug ( `Auto-mode unavailable during refresh: ${ error . message } ` ) ;
183+ this . current = undefined ;
184+ this . notify ( ) ;
185+ return undefined ;
186+ } else {
187+ logger . debug ( `Auto-mode refresh failed; reusing last token until expiry: ${ this . formatError ( error ) } ` ) ;
188+ this . applySessionToken ( settings , this . current . sessionToken ) ;
189+ return { modelId : this . current . selectedModel , sessionToken : this . current . sessionToken } ;
190+ }
191+ }
192+ }
193+
194+ try {
195+ const acquired = await this . sdkPackage . acquireAutoModeSession ( args ) ;
196+ this . current = acquired ;
197+ this . applySessionToken ( settings , acquired . sessionToken ) ;
198+ this . notify ( ) ;
199+ logger . debug ( `Auto-mode session acquired: selected_model=${ acquired . selectedModel } ${ acquired . expiresAt ? ` expires_at=${ acquired . expiresAt } ` : '' } ` ) ;
200+ return { modelId : acquired . selectedModel , sessionToken : acquired . sessionToken } ;
201+ } catch ( error ) {
202+ if ( error instanceof this . sdkPackage . AutoModeUnsupportedError ) {
203+ logger . debug ( `Auto-mode unsupported: ${ error . message } ` ) ;
204+ return undefined ;
205+ }
206+
207+ if ( error instanceof this . sdkPackage . AutoModeUnavailableError ) {
208+ logger . debug ( `Auto-mode unavailable: ${ error . message } ` ) ;
209+ return undefined ;
210+ }
211+
212+ logger . debug ( `Auto-mode acquire failed: ${ this . formatError ( error ) } ` ) ;
213+ return undefined ;
214+ }
215+ }
216+
217+ private isFresh ( ) : boolean {
218+ return this . current ? ( this . current . expiresAt ? this . current . expiresAt * 1000 - Date . now ( ) > AUTO_MODE_REFRESH_LEAD_TIME_MS : true ) : false ;
219+ }
220+
221+ private isUnauthorizedError ( error : unknown ) : error is { kind : 'unauthorized' } {
222+ return typeof error === 'object' && error !== null && 'kind' in error && error . kind === 'unauthorized' ;
223+ }
224+
225+ private applySessionToken ( settings : AutoModeResolveArgs [ 'settings' ] , sessionToken : string ) : void {
226+ if ( ! settings ) {
227+ return ;
228+ }
229+
230+ settings . api ??= { } ;
231+ settings . api . copilot ??= { } ;
232+ settings . api . copilot . capiSessionToken = sessionToken ;
233+ }
234+
235+ private clearSessionToken ( settings : AutoModeResolveArgs [ 'settings' ] ) : void {
236+ if ( settings ?. api ?. copilot ) {
237+ delete settings . api . copilot . capiSessionToken ;
238+ }
239+ }
240+
241+ private formatError ( error : unknown ) : string {
242+ return error instanceof Error ? error . message : String ( error ) ;
243+ }
244+ }
56245
57246export interface ICopilotCLISessionItem {
58247 readonly id : string ;
@@ -179,7 +368,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
179368 this . monitorSessionFiles ( ) ;
180369 this . _sessionManager = new Lazy < Promise < internal . LocalSessionManager > > ( async ( ) => {
181370 try {
182- const { internal, createLocalFeatureFlagService, AutoModeSessionManager } = await this . getSDKPackage ( ) ;
371+ const sdkPackage = await this . getSDKPackage ( ) ;
372+ const { internal, createLocalFeatureFlagService } = sdkPackage ;
183373 // Always enable SDK OTel so the debug panel receives native spans via the bridge.
184374 // When user OTel is disabled, we force file exporter to /dev/null so the SDK
185375 // creates OtelSessionTracker (for debug panel) but doesn't export to any collector.
@@ -211,7 +401,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
211401 return new internal . LocalSessionManager ( {
212402 featureFlagService : createLocalFeatureFlagService ( ) ,
213403 telemetryService : new internal . NoopTelemetryService ( ) ,
214- autoModeManager : new AutoModeSessionManager ( ) ,
404+ autoModeManager : this . createAutoModeManager ( sdkPackage ) ,
215405 } , { flushDebounceMs : undefined , settings : undefined , version : undefined } ) ;
216406 }
217407 catch ( error ) {
@@ -222,9 +412,23 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
222412 this . _sessionTracker = this . instantiationService . createInstance ( CopilotCLISessionWorkspaceTracker ) ;
223413 }
224414
225- private async getSDKPackage ( ) {
226- const { internal, LocalSession, createLocalFeatureFlagService, AutoModeSessionManager } = await this . copilotCLISDK . getPackage ( ) ;
227- return { internal, LocalSession, createLocalFeatureFlagService, AutoModeSessionManager } ;
415+ private async getSDKPackage ( ) : Promise < SDKPackage > {
416+ return this . copilotCLISDK . getPackage ( ) ;
417+ }
418+
419+ private createAutoModeManager ( sdkPackage : SDKPackage ) : SDKAutoModeSessionManager {
420+ if ( typeof sdkPackage . AutoModeSessionManager === 'function' ) {
421+ try {
422+ return new sdkPackage . AutoModeSessionManager ( ) ;
423+ } catch ( error ) {
424+ if ( ! ( error instanceof TypeError ) ) {
425+ throw error ;
426+ }
427+ }
428+ }
429+
430+ this . logService . warn ( 'Failed to construct SDK AutoModeSessionManager, using compatibility fallback.' ) ;
431+ return new AutoModeSessionManagerCompat ( sdkPackage ) as unknown as SDKAutoModeSessionManager ;
228432 }
229433
230434 getSessionWorkingDirectory ( sessionId : string ) : Uri | undefined {
0 commit comments