Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/extension/agents/vscode-node/troubleshootSkillProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export class TroubleshootSkillProvider extends BaseSkillProvider {
lines.push('');

// Provide the debug-logs directory path so the agent can find log files.
// The {{CURRENT_SESSION_LOG}} placeholder may be resolved earlier during prompt
// The {{VSCODE_AGENT_DEBUG_SESSION_LOG_DIR}} placeholder may be resolved earlier during prompt
// rendering (for example by PromptFile.getBodyContent) or later by the read_file
// tool, which has access to the correct session context.
const storageUri = this.extensionContext.storageUri;
if (storageUri) {
lines.push('- Current session log directory: `{{CURRENT_SESSION_LOG}}`');
lines.push('- Current session log directory: `{{VSCODE_AGENT_DEBUG_SESSION_LOG_DIR}}`');
} else {
lines.push('- Debug-logs directory: unavailable in this environment. Abort now and tell the user that troubleshooting is only available if a workspace is open.');
}
Expand Down
2 changes: 1 addition & 1 deletion src/extension/conversation/vscode-node/remoteAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export class RemoteAgentContribution implements IDisposable {
}, agentData ? { type: RequestType.RemoteAgentChat, slug: agentData.slug } : { type: RequestType.RemoteAgentChat });

// This flattens the docs agent's variables and ignores other variable values for now
const resolved = await this.promptVariablesService.resolveVariablesInPrompt(request.prompt, request.references);
const resolved = await this.promptVariablesService.resolvePromptReferencesInPrompt(request.prompt, request.references);

// Collect copilot skills and references to be sent in the request
const copilotReferences = [];
Expand Down
2 changes: 1 addition & 1 deletion src/extension/intents/node/docIntent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class DocPrompt extends PromptElement<Props> {
documentContext={this.props.documentContext}
nodeToDocument={this.props.nodeToDocument}
endpointInfo={this.props.endpointInfo} />
<HistoryWithInstructions inline={true} history={this.props.history} passPriority historyPriority={700}>
<HistoryWithInstructions inline={true} history={this.props.history} passPriority historyPriority={700} promptContext={this.props.promptContext}>
<InstructionMessage>
When user asks you to document something, you must answer in the form of a {language} markdown code block.<br />
</InstructionMessage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ vite.config.ts
<UserMessage flexGrow={2}>
<SetupWorkspaceStructure />
</UserMessage>
<ChatVariablesAndQuery flexGrow={2} priority={900} chatVariables={chatVariables} query={query} embeddedInsideUserMessage={false} />
<ChatVariablesAndQuery flexGrow={2} priority={900} chatVariables={chatVariables} query={query} embeddedInsideUserMessage={false} promptContext={this.props.promptContext} />
</>;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ ${projectNameToken}
<UserMessage flexGrow={2}>
<SetupWorkspaceStructure />
</UserMessage>
<ChatVariablesAndQuery flexGrow={2} priority={900} chatVariables={this.props.promptContext.chatVariables} query={`I want to: ${query}`} embeddedInsideUserMessage={false} />
<ChatVariablesAndQuery flexGrow={2} priority={900} chatVariables={this.props.promptContext.chatVariables} query={`I want to: ${query}`} embeddedInsideUserMessage={false} promptContext={this.props.promptContext} />
</>;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ class Prompt extends PromptElement<Props> {

override async render(state: void, sizing: PromptSizing) {

const { history, query, chatVariables, } = this.props.promptContext;
const { history, query, chatVariables } = this.props.promptContext;
const { context, testExampleFile, testFileToWriteTo, location, alreadyConsumedChatVariable } = this.props;

// get testable node
Expand Down Expand Up @@ -363,7 +363,7 @@ class Prompt extends PromptElement<Props> {
<CopilotIdentityRules /><br />
<SafetyRules />
</SystemMessage>
<HistoryWithInstructions history={history} passPriority historyPriority={700}>
<HistoryWithInstructions history={history} passPriority historyPriority={700} promptContext={this.props.promptContext}>
<InstructionMessage priority={1000}>
{location === ChatLocation.Editor
? <>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio
import { IBuildPromptContext } from '../../../prompt/common/intents';
import { IDocumentContext } from '../../../prompt/node/documentContext';
import { IIntentInvocation, IResponseProcessorContext, ReplyInterpreter, ReplyInterpreterMetaData } from '../../../prompt/node/intents';
import { PseudoStopStartResponseProcessor } from '../../../prompt/node/pseudoStartStopConversationCallback';
import { Test2Impl } from '../../../prompt/node/test2Impl';
import { CopilotIdentityRules } from '../../../prompts/node/base/copilotIdentity';
import { InstructionMessage } from '../../../prompts/node/base/instructionMessage';
Expand All @@ -33,7 +34,6 @@ import { TestDeps } from './testDeps';
import { ITestGenInfo, ITestGenInfoStorage } from './testInfoStorage';
import { TestsIntent } from './testIntent';
import { formatRequestAndUserQuery } from './testPromptUtil';
import { PseudoStopStartResponseProcessor } from '../../../prompt/node/pseudoStartStopConversationCallback';


/**
Expand Down Expand Up @@ -129,7 +129,7 @@ class TestFromTestPrompt extends PromptElement<Props> {

override async render(_state: void, sizing: PromptSizing) {

const { history, query, chatVariables, } = this.props.promptContext;
const { history, query, chatVariables } = this.props.promptContext;
const { context, testGenInfo, alreadyConsumedChatVariable, } = this.props;

if (isNotebookCellOrNotebookChatInput(context.document.uri)) {
Expand Down Expand Up @@ -174,7 +174,7 @@ class TestFromTestPrompt extends PromptElement<Props> {
<CopilotIdentityRules /><br />
<SafetyRules />
</SystemMessage>
<HistoryWithInstructions passPriority history={history} historyPriority={700}>
<HistoryWithInstructions passPriority history={history} historyPriority={700} promptContext={this.props.promptContext}>
<InstructionMessage priority={1000}>
The user has a {context.language.languageId} file opened in a code editor.<br />
The user includes some code snippets from the file.<br />
Expand Down Expand Up @@ -206,7 +206,7 @@ class TestFromTestPrompt extends PromptElement<Props> {
<CodeBlock uri={testGenInfo.uri} languageId={context.language.languageId} code={testedDeclarationExcerpt} />
</Tag>}
<Tag name='userPrompt' priority={900}>
<UserQuery chatVariables={filteredChatVariables} query={requestAndUserQuery} />
<UserQuery chatVariables={filteredChatVariables} query={requestAndUserQuery} promptContext={this.props.promptContext} />
</Tag>
</UserMessage>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/extension/mcp/vscode-node/mcpToolCallingLoopPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class McpToolCallingLoopPrompt extends PromptElement<IMcpToolCallingLoopP

return (
<>
<HistoryWithInstructions flexGrow={1} passPriority historyPriority={700} history={history}>
<HistoryWithInstructions flexGrow={1} passPriority historyPriority={700} history={history} promptContext={undefined}>
<InstructionMessage>
<Tag name='instructions'>
You are an expert in reading documentation and extracting relevant results.<br />
Expand Down
6 changes: 3 additions & 3 deletions src/extension/prompt/node/intentDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export class IntentDetector implements ChatParticipantDetectionProvider {
const endpoint = await this.endpointProvider.getChatEndpoint('copilot-fast');

const { messages: currentSelection } = await renderPromptElement(this.instantiationService, endpoint, CurrentSelection, { document });
const { messages: conversationHistory } = await renderPromptElement(this.instantiationService, endpoint, ConversationHistory, { history, priority: 1000 }, undefined, undefined).catch(() => ({ messages: [] }));
const { messages: conversationHistory } = await renderPromptElement(this.instantiationService, endpoint, ConversationHistory, { history, priority: 1000, promptContext: undefined }, undefined, undefined).catch(() => ({ messages: [] }));

const { history: historyMessages, fileExcerpt, attachedContext, fileExcerptExceedsBudget } = this.prepareInternalTelemetryContext(getTextPart(currentSelection?.[0]?.content), conversationHistory, chatVariables);

Expand Down Expand Up @@ -639,7 +639,7 @@ export class GPT4OIntentDetectionPrompt extends IntentDetectionPrompt {
const { history, chatVariables, userQuestion } = this.props;

return (<>
<HistoryWithInstructions history={history || []} passPriority historyPriority={800}>
<HistoryWithInstructions history={history || []} passPriority historyPriority={800} promptContext={undefined}>
<InstructionMessage>
You are a helpful AI programming assistant to a user who is a software engineer, acting on behalf of the Visual Studio Code editor. Your task is to choose one category from the Markdown table of categories below that matches the user's question. Carefully review the user's question, any previous messages, and any provided context such as code snippets. Respond with just the category name. Your chosen category will help Visual Studio Code provide the user with a higher-quality response, and choosing incorrectly will degrade the user's experience of using Visual Studio Code, so you must choose wisely. If you cannot choose just one category, or if none of the categories seem like they would provide the user with a better result, you must always respond with "unknown".<br />
<br />
Expand All @@ -648,7 +648,7 @@ export class GPT4OIntentDetectionPrompt extends IntentDetectionPrompt {
<ParticipantDescriptions participants={this.props.thirdPartyParticipants ? this.props.thirdPartyParticipants : this.props.builtinParticipants} includeDynamicParticipants={!this.props.thirdPartyParticipants} />
</InstructionMessage>
</HistoryWithInstructions>
{<ChatVariablesAndQuery query={userQuestion} chatVariables={chatVariables} priority={900} embeddedInsideUserMessage={false} />}
{<ChatVariablesAndQuery query={userQuestion} chatVariables={chatVariables} priority={900} embeddedInsideUserMessage={false} promptContext={undefined} />}
</>
);
}
Expand Down
18 changes: 16 additions & 2 deletions src/extension/prompt/node/promptVariablesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,38 @@

import type { ChatLanguageModelToolReference, ChatPromptReference } from 'vscode';
import { createServiceIdentifier } from '../../../util/common/services';
import { URI } from '../../../util/vs/base/common/uri';


export const IPromptVariablesService = createServiceIdentifier<IPromptVariablesService>('IPromptVariablesService');

export interface IPromptVariablesService {
readonly _serviceBrand: undefined;
resolveVariablesInPrompt(message: string, variables: readonly ChatPromptReference[]): Promise<{ message: string }>;
resolvePromptReferencesInPrompt(message: string, variables: readonly ChatPromptReference[]): Promise<{ message: string }>;
resolveToolReferencesInPrompt(message: string, toolReferences: readonly ChatLanguageModelToolReference[]): Promise<string>;

/**
* Replace all known `{{VARIABLE}}` template placeholders in {@link content}.
*
* @param content The raw template string (skill, agent, prompt, or instructions content).
* @param sessionResource The current chat session resource, used for resolving variables that depend on the session context (e.g. `{{VSCODE_AGENT_DEBUG_SESSION_LOG_DIR}}`).
* @returns The content with all resolvable placeholders replaced.
*/
resolveTemplateVariables(content: string, sessionResource: URI | undefined): string;
}

export class NullPromptVariablesService implements IPromptVariablesService {
declare readonly _serviceBrand: undefined;

async resolveVariablesInPrompt(message: string, variables: readonly ChatPromptReference[]): Promise<{ message: string }> {
async resolvePromptReferencesInPrompt(message: string, variables: readonly ChatPromptReference[]): Promise<{ message: string }> {
return { message };
}

async resolveToolReferencesInPrompt(message: string, toolReferences: readonly ChatLanguageModelToolReference[]): Promise<string> {
return message;
}

resolveTemplateVariables(content: string, sessionResource: URI | undefined): string {
return content;
}
}
10 changes: 7 additions & 3 deletions src/extension/prompt/node/todoListContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,30 @@

import { createServiceIdentifier } from '../../../util/common/services';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import { URI } from '../../../util/vs/base/common/uri';
import { LanguageModelTextPart } from '../../../vscodeTypes';
import { ToolName } from '../../tools/common/toolNames';
import { IToolsService } from '../../tools/common/toolsService';

export const ITodoListContextProvider = createServiceIdentifier<ITodoListContextProvider>('ITodoListContextProvider');
export interface ITodoListContextProvider {
getCurrentTodoContext(sessionResource: string): Promise<string | undefined>;
getCurrentTodoContext(sessionResource: URI): Promise<string | undefined>;
}

export class TodoListContextProvider implements ITodoListContextProvider {
constructor(
@IToolsService private readonly toolsService: IToolsService,
) { }

async getCurrentTodoContext(sessionResource: string): Promise<string | undefined> {
async getCurrentTodoContext(sessionResource: URI): Promise<string | undefined> {
try {
const result = await this.toolsService.invokeTool(
ToolName.CoreManageTodoList,
{
input: { operation: 'read', chatSessionResource: sessionResource }
input: {
operation: 'read',
chatSessionResource: sessionResource.toString() // passed as string, not sure if still needed. See https://github.com/microsoft/vscode/blob/0b84fd1b4b00ea54e1c56f296244c2a49f9c334c/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts#L103
},
} as any,
CancellationToken.None
);
Expand Down
46 changes: 45 additions & 1 deletion src/extension/prompt/vscode-node/promptVariablesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,45 @@
*--------------------------------------------------------------------------------------------*/

import type { ChatLanguageModelToolReference, ChatPromptReference } from 'vscode';
import { IChatDebugFileLoggerService } from '../../../platform/chat/common/chatDebugFileLoggerService';
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
import { URI } from '../../../util/vs/base/common/uri';
import { getToolName } from '../../tools/common/toolNames';
import { IPromptVariablesService } from '../node/promptVariablesService';

/**
* Known template variables that can be resolved at runtime.
* Each entry maps a placeholder name (without the `{{ }}` delimiters) to a
* resolver that produces the replacement string, or `undefined` if the
* variable cannot be resolved in the current context.
*/
type VariableResolver = (sessionResource: URI | undefined) => string | undefined;

export class PromptVariablesServiceImpl implements IPromptVariablesService {

declare readonly _serviceBrand: undefined;

async resolveVariablesInPrompt(message: string, variables: ChatPromptReference[]): Promise<{ message: string }> {
private readonly _resolvers: ReadonlyMap<string, VariableResolver>;

constructor(
@IChatDebugFileLoggerService private readonly chatDebugFileLoggerService: IChatDebugFileLoggerService,
@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,
) {
this._resolvers = new Map<string, VariableResolver>([
['VSCODE_AGENT_DEBUG_SESSION_LOG_DIR', sessionResource => {
if (!sessionResource) {
return undefined;
}
const sessionDir = this.chatDebugFileLoggerService.getSessionDirForResource(sessionResource);
if (!sessionDir) {
return undefined;
}
return this.promptPathRepresentationService.getFilePath(sessionDir);
}],
]);
}

async resolvePromptReferencesInPrompt(message: string, variables: ChatPromptReference[]): Promise<{ message: string }> {
for (const variable of this._reverseSortRefsWithRange(variables)) {
message = message.slice(0, variable.range[0]) + `[#${variable.name}](#${variable.name}-context)` + message.slice(variable.range[1]);
}
Expand All @@ -37,6 +68,19 @@ export class PromptVariablesServiceImpl implements IPromptVariablesService {
return message;
}

resolveTemplateVariables(content: string, sessionResource: URI | undefined): string {
for (const [name, resolve] of this._resolvers) {
const placeholder = `{{${name}}}`;
if (content.includes(placeholder)) {
const value = resolve(sessionResource);
if (value !== undefined) {
content = content.replaceAll(placeholder, () => value);
}
}
}
return content;
}

private _reverseSortRefsWithRange<T extends { range?: [number, number] }>(refs: T[]): (T & { range: [number, number] })[] {
const refsWithRange = refs.filter(ref => !!ref.range) as (T & { range: [number, number] })[];
return refsWithRange.sort((a, b) => b.range[0] - a.range[0]);
Expand Down
Loading
Loading