Skip to content

Commit 547a00d

Browse files
authored
Better craft Copilot CLI identity in VS Code (#315086)
* Better craft Copilot CLI identity in VS Code * match wording * use customize * remove ugly `as const` * Simplify prompt * Add sprinkle of text
1 parent 3f37fd9 commit 547a00d

4 files changed

Lines changed: 120 additions & 6 deletions

File tree

‎extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { INTEGRATION_ID } from '../../../../platform/endpoint/common/licenseAgre
5454

5555
const COPILOT_CLI_WORKSPACE_JSON_FILE_KEY = 'github.copilot.cli.workspaceSessionFile';
5656
const AUTO_MODE_REFRESH_LEAD_TIME_MS = 300 * 1000;
57+
export const COPILOT_CLI_CHAT_PANEL_SYSTEM_MESSAGE = 'You are an AI assistant using Copilot CLI runtime in VS Code. You help users with software engineering tasks. When asked about your identity, you must state that you are an AI assistant using Copilot CLI runtime in VS Code.';
5758

5859
type SDKPackage = Awaited<ReturnType<ICopilotCLISDK['getPackage']>>;
5960
type AutoModeResolveArgs = Parameters<SDKAutoModeSessionManager['resolve']>[0];
@@ -890,7 +891,18 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
890891
]);
891892
const customAgents = agentInfos.map(i => i.agent);
892893
const variablesContext = this._promptVariablesService.buildTemplateVariablesContext(options.sessionId, options.debugTargetSessionIds);
893-
const systemMessage = variablesContext ? { mode: 'append' as const, content: variablesContext } : undefined;
894+
const systemMessage: NonNullable<SessionOptions['systemMessage']> = {
895+
mode: 'customize',
896+
sections: {
897+
identity: {
898+
action: 'replace',
899+
content: COPILOT_CLI_CHAT_PANEL_SYSTEM_MESSAGE,
900+
},
901+
},
902+
};
903+
if (variablesContext) {
904+
systemMessage.content = variablesContext;
905+
}
894906

895907
const allOptions: SessionOptions = {
896908
clientName: 'vscode',
@@ -924,9 +936,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
924936
if (copilotUrl) {
925937
allOptions.copilotUrl = copilotUrl;
926938
}
927-
if (systemMessage) {
928-
allOptions.systemMessage = systemMessage;
929-
}
939+
allOptions.systemMessage = systemMessage;
930940
allOptions.sessionCapabilities = new Set(['plan-mode', 'memory', 'cli-documentation', 'ask-user', 'interactive-mode', 'system-notifications']);
931941
if (options.reasoningEffort && this.configurationService.getConfig(ConfigKey.Advanced.CLIThinkingEffortEnabled)) {
932942
allOptions.reasoningEffort = options.reasoningEffort;

‎extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { IChatDelegationSummaryService } from '../../common/delegationSummarySer
3939
import { getCopilotCLISessionDir } from '../cliHelpers';
4040
import { ICopilotCLISDK } from '../copilotCli';
4141
import { CopilotCLISession, ICopilotCLISession } from '../copilotcliSession';
42-
import { CopilotCLISessionService, CopilotCLISessionWorkspaceTracker, ICopilotCLISessionItem } from '../copilotcliSessionService';
42+
import { COPILOT_CLI_CHAT_PANEL_SYSTEM_MESSAGE, CopilotCLISessionService, CopilotCLISessionWorkspaceTracker, ICopilotCLISessionItem } from '../copilotcliSessionService';
4343
import { CopilotCLIMCPHandler } from '../mcpHandler';
4444
import { MissionControlApiClient } from '../missionControlApiClient';
4545
import { IQuestion, IQuestionAnswer, IUserQuestionHandler } from '../userInputHelpers';
@@ -250,6 +250,43 @@ describe('CopilotCLISessionService', () => {
250250
}));
251251
});
252252

253+
it('passes the VS Code Copilot CLI identity system message to session manager', async () => {
254+
const createSessionSpy = vi.spyOn(manager, 'createSession');
255+
await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None);
256+
257+
const callArgs = createSessionSpy.mock.calls[0][0];
258+
expect(callArgs.systemMessage).toEqual({
259+
mode: 'customize',
260+
sections: {
261+
identity: {
262+
action: 'replace',
263+
content: COPILOT_CLI_CHAT_PANEL_SYSTEM_MESSAGE,
264+
},
265+
},
266+
});
267+
});
268+
269+
it('preserves prompt variable context separately from the VS Code Copilot CLI identity system message', async () => {
270+
const variableContext = 'Resolved template variables are available here.';
271+
vi.spyOn(NullPromptVariablesService.prototype, 'buildTemplateVariablesContext').mockReturnValue(variableContext);
272+
const createSessionSpy = vi.spyOn(manager, 'createSession');
273+
await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None);
274+
275+
const systemMessage = createSessionSpy.mock.calls[0][0].systemMessage;
276+
expect(systemMessage?.mode).toBe('customize');
277+
if (systemMessage?.mode !== 'customize') {
278+
throw new Error('Expected customize-mode system message');
279+
}
280+
const identity = systemMessage.sections?.identity;
281+
expect(identity?.action).toBe('replace');
282+
if (identity?.action !== 'replace') {
283+
throw new Error('Expected replace identity section override');
284+
}
285+
expect(systemMessage.content).toBe(variableContext);
286+
expect(identity.content).toBe(COPILOT_CLI_CHAT_PANEL_SYSTEM_MESSAGE);
287+
expect(identity.content).not.toContain(variableContext);
288+
});
289+
253290
it('passes reasoningEffort to session manager when provided', async () => {
254291
const createSessionSpy = vi.spyOn(manager, 'createSession');
255292
await service.createSession({ model: 'gpt-test', reasoningEffort: 'high', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None);
@@ -283,6 +320,25 @@ describe('CopilotCLISessionService', () => {
283320
}), true);
284321
});
285322

323+
it('passes the VS Code Copilot CLI identity system message when getting an existing session', async () => {
324+
const targetId = 'system-message-get';
325+
manager.sessions.set(targetId, new MockCliSdkSession(targetId, new Date()));
326+
const getSessionSpy = vi.spyOn(manager, 'getSession');
327+
await service.getSession({ sessionId: targetId, model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None);
328+
329+
expect(getSessionSpy).toHaveBeenCalledWith(expect.objectContaining({
330+
systemMessage: {
331+
mode: 'customize',
332+
sections: {
333+
identity: {
334+
action: 'replace',
335+
content: COPILOT_CLI_CHAT_PANEL_SYSTEM_MESSAGE,
336+
},
337+
},
338+
},
339+
}), true);
340+
});
341+
286342
it('does not set reasoningEffort when not provided', async () => {
287343
const targetId = 'no-reasoning-get';
288344
manager.sessions.set(targetId, new MockCliSdkSession(targetId, new Date()));

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ const ThinkingLevelConfigKey = 'thinkingLevel';
8484
const ReasoningEfforts = ['low', 'medium', 'high', 'xhigh'] as const;
8585
type ReasoningEffort = NonNullable<SessionConfig['reasoningEffort']>;
8686

87+
export const COPILOT_AGENT_HOST_SYSTEM_MESSAGE = {
88+
mode: 'customize',
89+
sections: {
90+
identity: {
91+
action: 'replace',
92+
content: 'You are an AI assistant using Copilot CLI runtime in VS Code. You help users with software engineering tasks. When asked about your identity, you must state that you are an AI assistant using Copilot CLI runtime in VS Code.',
93+
},
94+
},
95+
} satisfies NonNullable<ResumeSessionConfig['systemMessage']>;
96+
8797
interface ISerializedModelSelection {
8898
id?: unknown;
8999
config?: unknown;
@@ -1366,6 +1376,7 @@ export class CopilotAgent extends Disposable implements IAgent {
13661376
mcpServers: toSdkMcpServers(plugins.flatMap(p => p.mcpServers)),
13671377
customAgents,
13681378
skillDirectories: toSdkSkillDirectories(plugins.flatMap(p => p.skills)),
1379+
systemMessage: COPILOT_AGENT_HOST_SYSTEM_MESSAGE,
13691380
tools: [...shellTools, ...callbacks.clientTools],
13701381
// Enable infinite sessions so the SDK provisions a workspace
13711382
// directory (containing `plan.md`, `checkpoints/`, `files/`).

‎src/vs/platform/agentHost/test/node/copilotAgent.test.ts‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { AgentConfigurationService, IAgentConfigurationService } from '../../nod
2929
import { AgentHostStateManager } from '../../node/agentHostStateManager.js';
3030
import { IAgentHostGitService } from '../../node/agentHostGitService.js';
3131
import { IAgentHostTerminalManager } from '../../node/agentHostTerminalManager.js';
32-
import { CopilotAgent, getCopilotBranchNameHintFromMessage, getCopilotWorktreeBranchName, getCopilotWorktreeName, getCopilotWorktreesRoot } from '../../node/copilot/copilotAgent.js';
32+
import { COPILOT_AGENT_HOST_SYSTEM_MESSAGE, CopilotAgent, getCopilotBranchNameHintFromMessage, getCopilotWorktreeBranchName, getCopilotWorktreeName, getCopilotWorktreesRoot } from '../../node/copilot/copilotAgent.js';
3333
import { CopilotAgentSession, type SessionWrapperFactory } from '../../node/copilot/copilotAgentSession.js';
3434
import { CopilotSessionWrapper } from '../../node/copilot/copilotSessionWrapper.js';
3535
import { ShellManager } from '../../node/copilot/copilotShellTools.js';
@@ -686,6 +686,43 @@ suite('CopilotAgent', () => {
686686
// Forking a provisional session is no longer a special case: the agent
687687
// service drops `config.fork` for sources with no turns, so the call
688688
// reduces to a plain new-session create.
689+
690+
test('materialization passes VS Code-specific system message to the SDK', async () => {
691+
const sessionDataService = disposables.add(new TestSessionDataService());
692+
const client = new TestCopilotClient([]);
693+
let capturedConfig: Parameters<ITestCopilotClient['createSession']>[0] | undefined;
694+
client.createSession = async config => {
695+
capturedConfig = config;
696+
return new MockCopilotSession() as unknown as CopilotSession;
697+
};
698+
699+
const agent = createTestAgent(disposables, { sessionDataService, copilotClient: client });
700+
try {
701+
await agent.authenticate('https://api.github.com', 'token');
702+
703+
const result = await agent.createSession({
704+
session: AgentSession.uri('copilotcli', 'system-message-session'),
705+
workingDirectory: URI.file('/workspace'),
706+
});
707+
assert.strictEqual(result.provisional, true);
708+
709+
await agent.sendMessage(result.session, 'hello');
710+
711+
assert.ok(capturedConfig, 'SDK createSession should be called during provisional materialization');
712+
const systemMessage = capturedConfig.systemMessage;
713+
assert.deepStrictEqual(systemMessage, COPILOT_AGENT_HOST_SYSTEM_MESSAGE);
714+
if (!systemMessage || systemMessage.mode !== 'customize') {
715+
assert.fail('Expected customize-mode system message');
716+
}
717+
assert.strictEqual(systemMessage.sections?.identity?.action, 'replace');
718+
assert.strictEqual(
719+
systemMessage.sections?.identity?.content,
720+
'You are an AI assistant using Copilot CLI runtime in VS Code. You help users with software engineering tasks. When asked about your identity, you must state that you are an AI assistant using Copilot CLI runtime in VS Code.'
721+
);
722+
} finally {
723+
await disposeAgent(agent);
724+
}
725+
});
689726
});
690727

691728
suite('onClientToolCallComplete', () => {

0 commit comments

Comments
 (0)