|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { raceCancellationError } from '../../../../../base/common/async.js'; |
| 7 | +import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; |
| 8 | +import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js'; |
| 9 | +import { Event } from '../../../../../base/common/event.js'; |
| 10 | +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; |
| 11 | +import { autorun, IObservable, waitForState } from '../../../../../base/common/observable.js'; |
| 12 | +import { URI } from '../../../../../base/common/uri.js'; |
| 13 | +import { localize } from '../../../../../nls.js'; |
| 14 | +import { IAgentHostConnectionsService } from '../../../../../platform/agentHost/common/agentHostConnectionsService.js'; |
| 15 | +import { isRemoteAgentHostSessionType } from '../../../../../platform/agentHost/common/agentHostSessionType.js'; |
| 16 | +import { IEvaluationSessionAttachment, IEvaluationSessionAttachmentService, IEvaluationSessionIdentity } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/evaluationSessionAttachmentService.js'; |
| 17 | +import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; |
| 18 | +import { ISession } from '../../../../services/sessions/common/session.js'; |
| 19 | +import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js'; |
| 20 | + |
| 21 | +export interface IEvaluationSessionAttachmentStartupServices { |
| 22 | + readonly sessionsManagementService: Pick<ISessionsManagementService, 'getSession' | 'onDidChangeSessions'>; |
| 23 | + readonly sessionsService: Pick<ISessionsService, 'canOpenSession' | 'openSession'> & { readonly activeSession: IObservable<ISession | undefined>; readonly initialRestoreComplete: IObservable<boolean> }; |
| 24 | + readonly connectionsService: Pick<IAgentHostConnectionsService, 'connections' | 'resolveSessionResource'>; |
| 25 | + readonly attachmentService: IEvaluationSessionAttachmentService; |
| 26 | + readonly whenWorkbenchRestored: Promise<void>; |
| 27 | + readonly reconcileClientToolSets: () => void; |
| 28 | +} |
| 29 | +export function parseEvaluationSessionResource(value: string): URI { |
| 30 | + const resource = URI.parse(value, true); |
| 31 | + const id = resource.path.substring(1); |
| 32 | + if (!isRemoteAgentHostSessionType(resource.scheme) || resource.authority || !id |
| 33 | + || resource.path !== `/${id}` || id.includes('/') || resource.query || resource.fragment |
| 34 | + || resource.toString() !== value) { |
| 35 | + throw new Error(localize('evaluationSessionAttachment.invalidUri', "The evaluation session URI must be a canonical remote session URI.")); |
| 36 | + } |
| 37 | + return resource; |
| 38 | +} |
| 39 | +export async function startEvaluationSessionAttachment(value: string | undefined, getServices: () => IEvaluationSessionAttachmentStartupServices, token: CancellationToken, onFailure: (error: Error) => void = () => { }): Promise<IDisposable | undefined> { |
| 40 | + if (value === undefined) { |
| 41 | + return undefined; |
| 42 | + } |
| 43 | + const resource = parseEvaluationSessionResource(value); |
| 44 | + const services = getServices(); |
| 45 | + let attachment: IEvaluationSessionAttachment | undefined; |
| 46 | + try { |
| 47 | + await waitForState(services.sessionsService.initialRestoreComplete, complete => complete, undefined, token); |
| 48 | + const session = await waitForExactSession(services.sessionsManagementService, resource, token); |
| 49 | + const identity = resolveEvaluationSessionIdentity(resource, session, services.connectionsService); |
| 50 | + if (!await raceCancellationError(services.sessionsService.canOpenSession(session), token)) { |
| 51 | + throw new Error(localize('evaluationSessionAttachment.workspaceNotTrusted', "The evaluation session workspace is not trusted.")); |
| 52 | + } |
| 53 | + attachment = services.attachmentService.attach(identity); |
| 54 | + await raceCancellationError(services.sessionsService.openSession(resource), token); |
| 55 | + await waitForState( |
| 56 | + services.sessionsService.activeSession, |
| 57 | + active => active?.resource.toString() === resource.toString(), |
| 58 | + active => active && active.resource.toString() !== resource.toString() |
| 59 | + ? new Error(localize('evaluationSessionAttachment.differentSessionActivated', "Opening the evaluation session activated a different session.")) |
| 60 | + : false, |
| 61 | + token, |
| 62 | + ); |
| 63 | + await raceCancellationError(services.whenWorkbenchRestored, token); |
| 64 | + if (services.sessionsService.activeSession.get()?.resource.toString() !== resource.toString()) { |
| 65 | + throw new Error(localize('evaluationSessionAttachment.changedBeforePublication', "The active evaluation session changed before publication was ready.")); |
| 66 | + } |
| 67 | + services.reconcileClientToolSets(); |
| 68 | + attachment.markActiveClientPublicationReady(); |
| 69 | + const retained = new DisposableStore(); |
| 70 | + retained.add(attachment); |
| 71 | + attachment = undefined; |
| 72 | + retained.add(autorun(reader => { |
| 73 | + if (services.sessionsService.activeSession.read(reader)?.resource.toString() !== resource.toString()) { |
| 74 | + retained.dispose(); |
| 75 | + onFailure(new Error(localize('evaluationSessionAttachment.activeSessionChanged', "The active evaluation session changed."))); |
| 76 | + } |
| 77 | + })); |
| 78 | + // The SDK supplies the workspace working directory; the driver waits for active-client inventory before sending a turn. |
| 79 | + return retained; |
| 80 | + } catch (error) { |
| 81 | + attachment?.dispose(); |
| 82 | + throw error; |
| 83 | + } |
| 84 | +} |
| 85 | +async function waitForExactSession(service: Pick<ISessionsManagementService, 'getSession' | 'onDidChangeSessions'>, resource: URI, token: CancellationToken): Promise<ISession> { |
| 86 | + for (; ;) { |
| 87 | + if (token.isCancellationRequested) { |
| 88 | + throw new CancellationError(); |
| 89 | + } |
| 90 | + const session = service.getSession(resource); |
| 91 | + if (session?.resource.toString() === resource.toString()) { |
| 92 | + return session; |
| 93 | + } |
| 94 | + const change = Event.toPromise(service.onDidChangeSessions); |
| 95 | + try { |
| 96 | + await raceCancellationError(change, token); |
| 97 | + } finally { |
| 98 | + change.cancel(); |
| 99 | + } |
| 100 | + } |
| 101 | +} |
| 102 | +export function resolveEvaluationSessionIdentity(resource: URI, session: ISession, connectionsService: Pick<IAgentHostConnectionsService, 'connections' | 'resolveSessionResource'>): IEvaluationSessionIdentity { |
| 103 | + const resolution = connectionsService.resolveSessionResource(resource); |
| 104 | + const connection = connectionsService.connections.find(candidate => !candidate.isAmbient && candidate.connection === resolution?.connection); |
| 105 | + const backendSession = (session as ISession & { readonly backendUri?: URI }).backendUri; |
| 106 | + if (!resolution || !connection || session.resource.toString() !== resource.toString() |
| 107 | + || !URI.isUri(backendSession) || backendSession.authority || backendSession.path !== resource.path |
| 108 | + || backendSession.query || backendSession.fragment) { |
| 109 | + throw new Error(localize('evaluationSessionAttachment.inexactRemoteHost', "The evaluation session is not backed by the exact connected remote agent host.")); |
| 110 | + } |
| 111 | + return { connectionAuthority: connection.authority, backendSession }; |
| 112 | +} |
| 113 | +export class EvaluationSessionAttachmentLifecycle extends Disposable { |
| 114 | + private readonly _attachment = this._register(new MutableDisposable<IDisposable>()); |
| 115 | + constructor(value: string | undefined, getServices: () => IEvaluationSessionAttachmentStartupServices, onFailure: (error: Error) => void) { |
| 116 | + super(); |
| 117 | + if (value === undefined) { |
| 118 | + return; |
| 119 | + } |
| 120 | + const cancellation = new CancellationTokenSource(); |
| 121 | + this._register(toDisposable(() => cancellation.dispose(true))); |
| 122 | + void startEvaluationSessionAttachment(value, getServices, cancellation.token, error => { |
| 123 | + this._attachment.clear(); |
| 124 | + onFailure(error); |
| 125 | + }).then(attachment => { |
| 126 | + if (cancellation.token.isCancellationRequested) { |
| 127 | + attachment?.dispose(); |
| 128 | + } else { |
| 129 | + this._attachment.value = attachment; |
| 130 | + } |
| 131 | + }).catch(error => { |
| 132 | + if (!isCancellationError(error) && !cancellation.token.isCancellationRequested) { |
| 133 | + onFailure(error); |
| 134 | + } |
| 135 | + }); |
| 136 | + } |
| 137 | +} |
0 commit comments