Files
vscode/extensions/copilot/src/extension/prompt/node/codebaseToolCalling.ts
T
Zhichao Li 548eda26df Add workspace metadata (git branch, commit, remote, file path) to OTel events and GH telemetry (#4844)
* feat: define workspace metadata OTel attributes and resolver

Add CopilotChatAttr constants for repo.head_branch_name,
repo.head_commit_hash, repo.remote_url, and file.relative_path.

Create WorkspaceOTelMetadata interface and resolveWorkspaceOTelMetadata()
helper that synchronously resolves git metadata from activeRepository.

Refs: microsoft/vscode#306397, microsoft/vscode-internalbacklog#7297

* feat: extend OTel edit events with optional workspace metadata

Add optional WorkspaceOTelMetadata param to emitEditFeedbackEvent,
emitEditHunkActionEvent, emitInlineDoneEvent, emitEditSurvivalEvent.

Existing callers compile unchanged since the new param is optional.

Refs: microsoft/vscode#306397

* feat: add workspace metadata to invoke_agent OTel span

Inject IGitService into ToolCallingLoop and spread resolved workspace
metadata (branch, commit, remote) onto the invoke_agent span attributes.

Refs: microsoft/vscode#306397

* feat: extend EditSurvivalResult with workspace metadata

Add workspace field to EditSurvivalResult interface and populate it
in EditSurvivalReporter._report() using resolveWorkspaceOTelMetadata().

The reporter already injects IGitService and has the document URI,
so no new DI is needed.

Refs: microsoft/vscode#306397

* feat: inject IGitService into UserActions and pass workspace metadata

Add workspace metadata to emitEditFeedbackEvent, emitEditHunkActionEvent,
and emitInlineDoneEvent calls in UserFeedbackService using the file URI
from each event action.

Refs: microsoft/vscode#306397

* feat: pass workspace metadata to OTel survival events

Forward res.workspace from EditSurvivalResult to emitEditSurvivalEvent
at all 4 call sites: inline_chat, code_mapper, apply_patch, replace_string.

Refs: microsoft/vscode#306397

* feat: add workspace metadata to GH telemetry edit events

Add headBranchName, headCommitHash, remoteUrl, fileRelativePath to
sendGHTelemetryEvent/sendEnhancedGHTelemetryEvent calls for:
- inline.trackEditSurvival
- fastApply/trackEditSurvival
- applyPatch/trackEditSurvival
- replaceString/trackEditSurvival
- fastApply/editOutcome

Refs: microsoft/vscode-internalbacklog#7297

* test: add unit tests for workspace metadata resolver and events

Test resolveWorkspaceOTelMetadata (branch, commit, URL, relative path,
edge cases) and workspaceMetadataToOTelAttributes (OTel key mapping).

Add tests for emitEditFeedbackEvent and emitEditSurvivalEvent verifying
workspace metadata is included/omitted correctly.

Refs: microsoft/vscode#306397

* fix: propagate IGitService to ToolCallingLoop subclasses

Pass the new IGitService constructor parameter through to super() in
all 5 ToolCallingLoop subclasses: McpToolCallingLoop,
CodebaseToolCallingLoop, DefaultToolCallingLoop,
ExecutionSubagentToolCallingLoop, SearchSubagentToolCallingLoop.

Refs: microsoft/vscode#306397

* fix: address review - URI-safe path, brace style, trim tests

- Fix path prefix false-positive by using isEqualOrParent/relativePath
  instead of string startsWith (e.g. /repo matching /repo2/file.ts)
- Expand one-line if blocks to multi-line per repo coding standards
- Remove as-any mutation in test, remove trivial conversion tests,
  add test for path prefix false-positive edge case
2026-03-30 23:24:34 +00:00

107 lines
5.3 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { randomUUID } from 'crypto';
import type { CancellationToken, ChatRequest, LanguageModelToolInformation, Progress } from 'vscode';
import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';
import { IChatHookService } from '../../../platform/chat/common/chatHookService';
import { ChatLocation, ChatResponse } from '../../../platform/chat/common/commonTypes';
import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService';
import { IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { IGitService } from '../../../platform/git/common/gitService';
import { ILogService } from '../../../platform/log/common/logService';
import { IOTelService } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes';
import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop';
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
import { CodebaseAgentPrompt } from '../../prompts/node/panel/codebaseAgentPrompt';
import { IToolsService } from '../../tools/common/toolsService';
import { IBuildPromptContext } from '../common/intents';
import { IBuildPromptResult } from './intents';
export interface ICodebaseToolCallingLoopOptions extends IToolCallingLoopOptions {
request: ChatRequest;
location: ChatLocation;
}
export class CodebaseToolCallingLoop extends ToolCallingLoop<ICodebaseToolCallingLoopOptions> {
public static readonly ID = 'codebaseTool';
constructor(
options: ICodebaseToolCallingLoopOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILogService logService: ILogService,
@IRequestLogger requestLogger: IRequestLogger,
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
@IToolsService private readonly toolsService: IToolsService,
@IAuthenticationChatUpgradeService authenticationChatUpgradeService: IAuthenticationChatUpgradeService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@IExperimentationService experimentationService: IExperimentationService,
@IChatHookService chatHookService: IChatHookService,
@ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService,
@IFileSystemService fileSystemService: IFileSystemService,
@IOTelService otelService: IOTelService,
@IGitService gitService: IGitService,
) {
super(options, instantiationService, endpointProvider, logService, requestLogger, authenticationChatUpgradeService, telemetryService, configurationService, experimentationService, chatHookService, sessionTranscriptService, fileSystemService, otelService, gitService);
}
private async getEndpoint(request: ChatRequest) {
let endpoint = await this.endpointProvider.getChatEndpoint(this.options.request);
if (!endpoint.supportsToolCalls) {
endpoint = await this.endpointProvider.getChatEndpoint('copilot-base');
}
return endpoint;
}
protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {
const endpoint = await this.getEndpoint(this.options.request);
const renderer = PromptRenderer.create(
this.instantiationService,
endpoint,
CodebaseAgentPrompt,
{
promptContext: buildPromptContext
}
);
return await renderer.render(progress, token);
}
protected async getAvailableTools(): Promise<LanguageModelToolInformation[]> {
const endpoint = await this.getEndpoint(this.options.request);
return this.toolsService.getEnabledTools(this.options.request, endpoint, tool => tool.tags.includes('vscode_codesearch'));
}
protected async fetch({ messages, finishedCb, requestOptions }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise<ChatResponse> {
const endpoint = await this.getEndpoint(this.options.request);
return endpoint.makeChatRequest(
CodebaseToolCallingLoop.ID,
messages,
finishedCb,
token,
this.options.location,
undefined,
{
...requestOptions,
temperature: 0
},
// This loop is inside a tool called from another request, so never user initiated
false,
{
messageId: randomUUID(), // @TODO@joyceerhl
messageSource: CodebaseToolCallingLoop.ID
},
);
}
}