Skip to content

Commit d4b070b

Browse files
committed
pr comments
1 parent 886c556 commit d4b070b

6 files changed

Lines changed: 62 additions & 47 deletions

File tree

src/vs/platform/agentHost/node/agentService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export class AgentService extends Disposable implements IAgentService {
265265
const state = this._stateManager.createSession(summary);
266266
state.config = sessionConfig;
267267
state.turns = sourceTurns;
268+
state.activeClient = config.activeClient;
268269
} else {
269270
// Create empty state for new sessions
270271
const summary: ISessionSummary = {
@@ -280,6 +281,7 @@ export class AgentService extends Disposable implements IAgentService {
280281
};
281282
const state = this._stateManager.createSession(summary);
282283
state.config = sessionConfig;
284+
state.activeClient = config?.activeClient;
283285
}
284286
this._stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: session.toString() });
285287

src/vs/platform/agentHost/node/copilot/copilotAgent.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,10 +449,12 @@ export class CopilotAgent extends Disposable implements IAgent {
449449

450450
const sessionId = config?.session ? AgentSession.id(config.session) : generateUuid();
451451
const sessionUri = AgentSession.uri(this.id, sessionId);
452+
let seededActiveClient = false;
452453
if (config?.activeClient) {
453454
const ac = this._getOrCreateActiveClient(sessionUri);
455+
seededActiveClient = true;
454456
ac.updateTools(config.activeClient.clientId, config.activeClient.tools);
455-
if (config.activeClient.customizations?.length) {
457+
if (config.activeClient.customizations !== undefined) {
456458
await this._plugins.sync(config.activeClient.clientId, config.activeClient.customizations);
457459
}
458460
}
@@ -479,6 +481,9 @@ export class CopilotAgent extends Disposable implements IAgent {
479481
agentSession = this._createAgentSession(factory, sessionId, shellManager, snapshot);
480482
await agentSession.initializeSession();
481483
} catch (error) {
484+
if (seededActiveClient) {
485+
this._activeClients.delete(sessionUri);
486+
}
482487
await this._removeCreatedWorktree(sessionId);
483488
throw error;
484489
}

src/vs/platform/agentHost/node/protocolServerHandler.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ILogService } from '../../log/common/log.js';
1212
import { AHPFileSystemProvider } from '../common/agentHostFileSystemProvider.js';
1313
import { AgentSession, type IAgentService } from '../common/agentService.js';
1414
import type { ICommandMap } from '../common/state/protocol/messages.js';
15-
import { ActionType } from '../common/state/protocol/actions.js';
1615
import { IActionEnvelope, INotification, isSessionAction, isTerminalAction, type ISessionAction } from '../common/state/sessionActions.js';
1716
import { MIN_PROTOCOL_VERSION, PROTOCOL_VERSION } from '../common/state/sessionCapabilities.js';
1817
import {
@@ -24,6 +23,7 @@ import {
2423
isJsonRpcNotification,
2524
isJsonRpcRequest,
2625
JSON_RPC_INTERNAL_ERROR,
26+
JsonRpcErrorCodes,
2727
ProtocolError,
2828
type IAhpServerNotification,
2929
type IInitializeParams,
@@ -350,6 +350,11 @@ export class ProtocolServerHandler extends Disposable {
350350
}
351351
fork = { session: URI.parse(params.fork.session), turnIndex, turnId: params.fork.turnId };
352352
}
353+
// If the client eagerly claimed the active client role, validate
354+
// the clientId matches the connection before forwarding.
355+
if (params.activeClient && params.activeClient.clientId !== _client.clientId) {
356+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `createSession.activeClient.clientId must match the connection's clientId`);
357+
}
353358
try {
354359
createdSession = await this._agentService.createSession({
355360
provider: params.provider,
@@ -358,6 +363,7 @@ export class ProtocolServerHandler extends Disposable {
358363
session: URI.parse(params.session),
359364
fork,
360365
config: params.config,
366+
activeClient: params.activeClient,
361367
});
362368
} catch (err) {
363369
if (err instanceof ProtocolError) {
@@ -369,19 +375,6 @@ export class ProtocolServerHandler extends Disposable {
369375
if (createdSession.toString() !== URI.parse(params.session).toString()) {
370376
this._logService.warn(`[ProtocolServer] createSession: provider returned URI ${createdSession.toString()} but client requested ${params.session}`);
371377
}
372-
// If the client eagerly claimed the active client role, dispatch
373-
// `session/activeClientChanged` now so the claim is atomic with
374-
// creation.
375-
if (params.activeClient) {
376-
if (params.activeClient.clientId !== _client.clientId) {
377-
throw new ProtocolError(JSON_RPC_INTERNAL_ERROR, `createSession.activeClient.clientId must match the connection's clientId`);
378-
}
379-
this._agentService.dispatchAction({
380-
type: ActionType.SessionActiveClientChanged,
381-
session: createdSession.toString(),
382-
activeClient: params.activeClient,
383-
}, _client.clientId, 0);
384-
}
385378
return null;
386379
},
387380
disposeSession: async (_client, params) => {

src/vs/platform/agentHost/test/node/agentService.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,36 @@ suite('AgentService (node dispatcher)', () => {
247247

248248
assert.deepStrictEqual(service.stateManager.getSessionState(session.toString())?.config?.values, config);
249249
});
250+
251+
test('seeds activeClient into the initial session state when provided', async () => {
252+
service.registerProvider(copilotAgent);
253+
254+
const envelopes: IActionEnvelope[] = [];
255+
disposables.add(service.onDidAction(env => envelopes.push(env)));
256+
257+
const activeClient = {
258+
clientId: 'client-eager',
259+
tools: [{ name: 't1', description: 'd', inputSchema: { type: 'object' } }],
260+
customizations: [{ uri: 'file:///plugin-a', displayName: 'A' }],
261+
};
262+
const session = await service.createSession({ provider: 'copilot', activeClient });
263+
264+
assert.deepStrictEqual({
265+
activeClient: service.stateManager.getSessionState(session.toString())?.activeClient,
266+
dispatchedActiveClientChanged: envelopes.some(e => e.action.type === ActionType.SessionActiveClientChanged),
267+
}, {
268+
activeClient,
269+
dispatchedActiveClientChanged: false,
270+
});
271+
});
272+
273+
test('omits activeClient from the initial session state when not provided', async () => {
274+
service.registerProvider(copilotAgent);
275+
276+
const session = await service.createSession({ provider: 'copilot' });
277+
278+
assert.strictEqual(service.stateManager.getSessionState(session.toString())?.activeClient, undefined);
279+
});
250280
});
251281

252282
// ---- authenticate ---------------------------------------------------

src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class MockAgentService implements IAgentService {
7272
readonly browsedUris: URI[] = [];
7373
readonly browseErrors = new Map<string, Error>();
7474
readonly listedSessions: IAgentSessionMetadata[] = [];
75+
readonly createSessionConfigs: (IAgentCreateSessionConfig | undefined)[] = [];
7576

7677
private readonly _onDidAction = new Emitter<import('../../common/state/sessionActions.js').IActionEnvelope>();
7778
readonly onDidAction = this._onDidAction.event;
@@ -91,6 +92,7 @@ class MockAgentService implements IAgentService {
9192
this._stateManager.dispatchClientAction(action, origin);
9293
}
9394
async createSession(config?: IAgentCreateSessionConfig): Promise<URI> {
95+
this.createSessionConfigs.push(config);
9496
const session = config?.session ?? URI.parse('copilot:///new-session');
9597
this._stateManager.createSession({
9698
resource: session.toString(),
@@ -582,14 +584,10 @@ suite('ProtocolServerHandler', () => {
582584

583585
suite('createSession activeClient', () => {
584586

585-
test('forwards activeClient as SessionActiveClientChanged', async () => {
587+
test('forwards activeClient to the agent service', async () => {
586588
const newSession = URI.parse('copilot:///eager-session').toString();
587-
// Pre-create the session so the client can subscribe at handshake
588-
// time. MockAgentService.createSession below is idempotent against
589-
// state-manager duplicates.
590-
stateManager.createSession(makeSessionSummary(newSession));
591589

592-
const transport = connectClient('client-1', [newSession]);
590+
const transport = connectClient('client-1');
593591
transport.sent.length = 0;
594592

595593
const responsePromise = waitForResponse(transport, 2);
@@ -602,21 +600,15 @@ suite('ProtocolServerHandler', () => {
602600
customizations: [{ uri: 'file:///plugin-a', displayName: 'A' }],
603601
},
604602
}));
605-
await responsePromise;
603+
const resp = await responsePromise as { result?: unknown; error?: unknown };
606604

607-
const activeClientMsgs = findNotifications(transport.sent, 'action').filter(m => {
608-
const envelope = m.params as unknown as { action: { type: string } };
609-
return envelope.action.type === ActionType.SessionActiveClientChanged;
610-
});
611-
assert.strictEqual(activeClientMsgs.length, 1, 'should emit exactly one SessionActiveClientChanged');
612-
const envelope = activeClientMsgs[0].params as unknown as { action: { session: string; activeClient: { clientId: string; tools: { name: string }[]; customizations?: { uri: string }[] } } };
605+
assert.strictEqual(resp.error, undefined, 'createSession should succeed');
606+
const config = agentService.createSessionConfigs.at(-1);
613607
assert.deepStrictEqual({
614-
session: envelope.action.session,
615-
clientId: envelope.action.activeClient.clientId,
616-
toolName: envelope.action.activeClient.tools[0].name,
617-
customizationUri: envelope.action.activeClient.customizations?.[0].uri,
608+
clientId: config?.activeClient?.clientId,
609+
toolName: config?.activeClient?.tools[0]?.name,
610+
customizationUri: config?.activeClient?.customizations?.[0].uri,
618611
}, {
619-
session: newSession,
620612
clientId: 'client-1',
621613
toolName: 't1',
622614
customizationUri: 'file:///plugin-a',
@@ -625,9 +617,8 @@ suite('ProtocolServerHandler', () => {
625617

626618
test('rejects createSession when activeClient.clientId mismatches', async () => {
627619
const newSession = URI.parse('copilot:///mismatch-session').toString();
628-
stateManager.createSession(makeSessionSummary(newSession));
629620

630-
const transport = connectClient('client-1', [newSession]);
621+
const transport = connectClient('client-1');
631622
transport.sent.length = 0;
632623

633624
const responsePromise = waitForResponse(transport, 2);
@@ -643,11 +634,7 @@ suite('ProtocolServerHandler', () => {
643634

644635
assert.ok(resp.error, 'response should be an error');
645636
assert.strictEqual(resp.result, undefined);
646-
const activeClientMsgs = findNotifications(transport.sent, 'action').filter(m => {
647-
const envelope = m.params as unknown as { action: { type: string } };
648-
return envelope.action.type === ActionType.SessionActiveClientChanged;
649-
});
650-
assert.strictEqual(activeClientMsgs.length, 0, 'no activeClient notification should have been emitted');
637+
assert.strictEqual(agentService.createSessionConfigs.length, 0, 'agent service should not have been called');
651638
});
652639
});
653640
});

src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,12 @@ class MockAgentHostService extends mock<IAgentHostService>() {
9393
createdAt: Date.now(),
9494
modifiedAt: Date.now(),
9595
};
96-
const initial: ISessionState = { ...createSessionState(summary), lifecycle: SessionLifecycle.Ready };
97-
const action: ISessionAction = {
98-
type: 'session/activeClientChanged' as const,
99-
session: session.toString(),
96+
const state: ISessionState = {
97+
...createSessionState(summary),
98+
lifecycle: SessionLifecycle.Ready,
10099
activeClient: config.activeClient,
101-
} as ISessionAction;
102-
const withClient = sessionReducer(initial, action as Parameters<typeof sessionReducer>[1], () => { });
103-
this.sessionStates.set(session.toString(), withClient);
100+
};
101+
this.sessionStates.set(session.toString(), state);
104102
}
105103
return session;
106104
}

0 commit comments

Comments
 (0)