Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx
13405 views
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 { RequestMetadata, RequestType } from '@vscode/copilot-api';
7
import { AssistantMessage, BasePromptElementProps, PromptRenderer as BasePromptRenderer, Chunk, IfEmpty, Image, JSONTree, PromptElement, PromptElementProps, PromptMetadata, PromptPiece, PromptSizing, TokenLimit, ToolCall, ToolMessage, useKeepWith, UserMessage } from '@vscode/prompt-tsx';
8
import type { ChatParticipantToolToken, LanguageModelToolInvocationOptions, LanguageModelToolResult2, LanguageModelToolTokenizationOptions } from 'vscode';
9
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
10
import { IChatHookService, IPreToolUseHookResult } from '../../../../platform/chat/common/chatHookService';
11
import { ISessionTranscriptService } from '../../../../platform/chat/common/sessionTranscriptService';
12
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
13
import { modelCanUseMcpResultImageURL } from '../../../../platform/endpoint/common/chatModelCapabilities';
14
import { CompactionDataContainer } from '../../../../platform/endpoint/common/compactionDataContainer';
15
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
16
import { CacheType } from '../../../../platform/endpoint/common/endpointTypes';
17
import { PhaseDataContainer } from '../../../../platform/endpoint/common/phaseDataContainer';
18
import { StatefulMarkerContainer } from '../../../../platform/endpoint/common/statefulMarkerContainer';
19
import { ThinkingDataContainer } from '../../../../platform/endpoint/common/thinkingDataContainer';
20
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
21
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
22
import { IImageService } from '../../../../platform/image/common/imageService';
23
import { ILogService } from '../../../../platform/log/common/logService';
24
import { IOTelService } from '../../../../platform/otel/common/otelService';
25
import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
26
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
27
import { toErrorMessage } from '../../../../util/common/errorMessage';
28
import { ITokenizer } from '../../../../util/common/tokenizer';
29
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
30
import { isCancellationError } from '../../../../util/vs/base/common/errors';
31
import { getExtensionForMimeType } from '../../../../util/vs/base/common/mime';
32
import { URI, UriComponents } from '../../../../util/vs/base/common/uri';
33
import { IInstantiationService, ServicesAccessor } from '../../../../util/vs/platform/instantiation/common/instantiation';
34
import { ServiceCollection } from '../../../../util/vs/platform/instantiation/common/serviceCollection';
35
import { LanguageModelDataPart, LanguageModelDataPart2, LanguageModelPartAudience, LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelTextPart2, LanguageModelToolMCPSource, LanguageModelToolResult } from '../../../../vscodeTypes';
36
import { isImageDataPart } from '../../../conversation/common/languageModelChatMessageHelpers';
37
import { IResultMetadata } from '../../../prompt/common/conversation';
38
import { IBuildPromptContext, IToolCall, IToolCallRound } from '../../../prompt/common/intents';
39
import { toJsonSchema } from '../../../tools/common/toJsonSchema';
40
import { ToolName } from '../../../tools/common/toolNames';
41
import { CopilotToolMode } from '../../../tools/common/toolsRegistry';
42
import { IToolsService } from '../../../tools/common/toolsService';
43
import { IChatDiskSessionResources } from '../../common/chatDiskSessionResources';
44
import { IPromptEndpoint } from '../base/promptRenderer';
45
import { Tag } from '../base/tag';
46
47
export interface ChatToolCallsProps extends BasePromptElementProps {
48
readonly promptContext: IBuildPromptContext;
49
readonly toolCallRounds: readonly IToolCallRound[] | undefined;
50
readonly toolCallResults: Record<string, LanguageModelToolResult2> | undefined;
51
readonly isHistorical?: boolean;
52
readonly toolCallMode?: CopilotToolMode;
53
readonly enableCacheBreakpoints?: boolean;
54
readonly truncateAt?: number;
55
}
56
57
const MAX_INPUT_VALIDATION_RETRIES = 5;
58
59
/**
60
* Render one round of the assistant response's tool calls.
61
* One assistant response "turn" which contains multiple rounds of assistant message text, tool calls, and tool results.
62
*/
63
export class ChatToolCalls extends PromptElement<ChatToolCallsProps, void> {
64
constructor(
65
props: PromptElementProps<ChatToolCallsProps>,
66
@IToolsService private readonly toolsService: IToolsService,
67
@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint,
68
@IInstantiationService private readonly instantiationService: IInstantiationService
69
) {
70
super(props);
71
}
72
73
override async render(state: void, sizing: PromptSizing, _progress?: unknown, token?: CancellationToken): Promise<PromptPiece<any, any> | undefined> {
74
if (!this.props.promptContext.tools || !this.props.toolCallRounds?.length) {
75
return;
76
}
77
78
// Create a child instantiation service with IBuildPromptContext registered
79
const hydratedInstantiationService = this.instantiationService.createChild(
80
new ServiceCollection([IBuildPromptContext, this.props.promptContext])
81
);
82
83
// Shared budget to limit total image data across all tool results in this turn.
84
// Prevents 413 errors when many image-returning tools run in parallel.
85
const sharedImageBudget: SharedImageBudget = { remaining: CAPI_IMAGE_BUDGET_BYTES };
86
87
const toolCallRounds = this.props.toolCallRounds.flatMap((round, i) => {
88
return this.renderOneToolCallRound(round, i, this.props.toolCallRounds!.length, hydratedInstantiationService, sharedImageBudget, token);
89
});
90
if (!toolCallRounds.length) {
91
return;
92
}
93
94
const KeepWith = useKeepWith();
95
return <>
96
<KeepWith priority={1} flexGrow={1}>
97
{toolCallRounds}
98
</KeepWith>
99
</>;
100
}
101
102
/**
103
* Render one round of tool calling: the assistant message text, its tool calls, and the results of those tool calls.
104
*/
105
private renderOneToolCallRound(round: IToolCallRound, index: number, total: number, hydratedInstantiationService: IInstantiationService, sharedImageBudget: SharedImageBudget, token?: CancellationToken): PromptElement[] {
106
let fixedNameToolCalls = round.toolCalls.map(tc => ({ ...tc, name: this.toolsService.validateToolName(tc.name) ?? tc.name }));
107
if (this.props.isHistorical) {
108
fixedNameToolCalls = fixedNameToolCalls.filter(tc => tc.id && this.props.toolCallResults?.[tc.id]);
109
}
110
111
if (round.toolCalls.length && !fixedNameToolCalls.length) {
112
return [];
113
}
114
115
const assistantToolCalls: Required<ToolCall>[] = fixedNameToolCalls.map(tc => ({
116
type: 'function',
117
function: { name: tc.name, arguments: tc.arguments },
118
id: tc.id!,
119
keepWith: useKeepWith(),
120
}));
121
const children: PromptElement[] = [];
122
123
// Don't include this when rendering and triggering summarization
124
const statefulMarker = round.statefulMarker && <StatefulMarkerContainer statefulMarker={{ modelId: this.promptEndpoint.model, marker: round.statefulMarker }} />;
125
const thinking = (!this.props.isHistorical) && round.thinking && <ThinkingDataContainer thinking={round.thinking} />;
126
const phase = (round.phase && round.phaseModelId === this.promptEndpoint.model) ? <PhaseDataContainer phase={round.phase} /> : undefined;
127
const compaction = round.compaction && <CompactionDataContainer compaction={round.compaction} />;
128
children.push(
129
<AssistantMessage toolCalls={assistantToolCalls}>
130
{statefulMarker}
131
{thinking}
132
{phase}
133
{compaction}
134
{round.response}
135
</AssistantMessage>);
136
137
// Tool call elements should be rendered with the later elements first, allowed to grow to fill the available space
138
// Each tool 'reserves' 1/(N*4) of the available space just so that newer tool calls don't completely elimate
139
// older tool calls.
140
const reserve1N = (1 / (total * 4)) / fixedNameToolCalls.length;
141
// todo@connor4312: historical tool calls don't need to reserve and can all be flexed together
142
for (const [i, toolCall] of fixedNameToolCalls.entries()) {
143
const KeepWith = assistantToolCalls[i].keepWith;
144
children.push(
145
<KeepWith priority={index} flexGrow={index + 1} flexReserve={`/${1 / reserve1N}`}>
146
{hydratedInstantiationService.invokeFunction(buildToolResultElement, {
147
toolCall: toolCall,
148
toolInvocationToken: this.props.promptContext.tools!.toolInvocationToken,
149
toolCallResult: this.props.toolCallResults?.[toolCall.id!],
150
allowInvokingTool: !this.props.isHistorical,
151
validateInput: round.toolInputRetry < MAX_INPUT_VALIDATION_RETRIES,
152
requestId: this.props.promptContext.requestId,
153
toolCallMode: this.props.toolCallMode ?? CopilotToolMode.PartialContext,
154
isLast: !this.props.isHistorical && i === fixedNameToolCalls.length - 1 && index === total - 1,
155
enableCacheBreakpoints: this.props.enableCacheBreakpoints ?? false,
156
truncateAt: this.props.truncateAt,
157
sessionId: this.props.promptContext.request?.sessionId,
158
// Strip images from historical turns to avoid 413 errors
159
stripImages: !!this.props.isHistorical,
160
sharedImageBudget,
161
token: token ?? CancellationToken.None,
162
})}
163
</KeepWith>,
164
);
165
}
166
167
// If a hook added context after this round, render it as a user message
168
if (round.hookContext) {
169
children.push(<UserMessage>{round.hookContext}</UserMessage>);
170
}
171
172
return children;
173
}
174
}
175
176
/**
177
* Half the CAPI body-size limit (5 MB), used to cap image data so the rest
178
* of the prompt still fits. Shared by both the per-tool and cross-tool budgets.
179
*/
180
const CAPI_IMAGE_BUDGET_BYTES = (5 * 1024 * 1024) / 2;
181
182
/**
183
* Shared mutable counter that limits the total image data rendered across
184
* all tool results within a turn, preventing 413 (request too large) errors
185
* when many image-returning tools (e.g. view_image) run in parallel.
186
*/
187
interface SharedImageBudget {
188
remaining: number;
189
}
190
191
interface ToolResultOpts {
192
readonly toolCall: IToolCall;
193
readonly toolInvocationToken: ChatParticipantToolToken | undefined;
194
readonly toolCallResult: LanguageModelToolResult2 | undefined;
195
readonly allowInvokingTool?: boolean;
196
readonly validateInput?: boolean;
197
readonly requestId?: string;
198
readonly toolCallMode: CopilotToolMode;
199
readonly isLast: boolean;
200
readonly enableCacheBreakpoints: boolean;
201
readonly truncateAt?: number;
202
readonly sessionId: string | undefined;
203
readonly stripImages?: boolean;
204
readonly sharedImageBudget?: SharedImageBudget;
205
readonly token: CancellationToken;
206
}
207
208
const toolErrorSuffix = '\nPlease check your input and try again.';
209
210
/**
211
* Creates a <ToolResult /> element. Eagerly starts the tool call if we know
212
* that the tool will not need/consume sizing information (e.g. MCP calls) and
213
* therefore don't need to wait for other elements to sequentially render.
214
*/
215
function buildToolResultElement(accessor: ServicesAccessor, props: ToolResultOpts) {
216
const toolsService: IToolsService = accessor.get(IToolsService);
217
const logService: ILogService = accessor.get(ILogService);
218
const telemetryService: ITelemetryService = accessor.get(ITelemetryService);
219
const endpointProvider: IEndpointProvider = accessor.get(IEndpointProvider);
220
const promptEndpoint: IPromptEndpoint = accessor.get(IPromptEndpoint);
221
const promptContext: IBuildPromptContext = accessor.get(IBuildPromptContext);
222
const sessionTranscriptService = accessor.get(ISessionTranscriptService);
223
const chatHookService = accessor.get(IChatHookService);
224
const otelService = accessor.get(IOTelService);
225
const tool = toolsService.getTool(props.toolCall.name);
226
227
async function getToolResult(sizing: PromptSizing) {
228
const tokenizationOptions: LanguageModelToolTokenizationOptions = {
229
tokenBudget: sizing.tokenBudget,
230
countTokens: async (content: string) => sizing.countTokens(content),
231
};
232
233
if (!props.toolCallResult && !props.allowInvokingTool) {
234
throw new Error(`Missing tool call result for "${props.toolCall.id}" (${props.toolCall.name})`);
235
}
236
237
const extraMetadata: PromptMetadata[] = [];
238
let isCancelled = false;
239
let toolResult = props.toolCallResult;
240
const copilotTool = toolsService.getCopilotTool(props.toolCall.name as ToolName);
241
if (toolResult === undefined) {
242
let inputObj: unknown;
243
let validation: ToolValidationOutcome = ToolValidationOutcome.Unknown;
244
if (props.validateInput) {
245
const validationResult = toolsService.validateToolInput(props.toolCall.name, props.toolCall.arguments);
246
if ('error' in validationResult) {
247
validation = ToolValidationOutcome.Invalid;
248
extraMetadata.push(new ToolFailureEncountered(props.toolCall.id));
249
toolResult = textToolResult(validationResult.error + toolErrorSuffix);
250
} else {
251
validation = ToolValidationOutcome.Valid;
252
inputObj = validationResult.inputObj;
253
}
254
} else {
255
inputObj = JSON.parse(props.toolCall.arguments);
256
}
257
258
let outcome: ToolInvocationOutcome = toolResult === undefined ? ToolInvocationOutcome.Success : ToolInvocationOutcome.InvalidInput;
259
if (toolResult === undefined) {
260
try {
261
if (promptContext.tools && !promptContext.tools.availableTools.find(t => t.name === props.toolCall.name)) {
262
outcome = ToolInvocationOutcome.DisabledByUser;
263
throw new Error(`Tool ${props.toolCall.name} is currently disabled by the user, and cannot be called.`);
264
}
265
266
if (copilotTool?.resolveInput) {
267
inputObj = await copilotTool.resolveInput(inputObj, promptContext, props.toolCallMode);
268
}
269
270
// Execute preToolUse hook before invoking the tool
271
const hookResult = await chatHookService.executePreToolUseHook(
272
props.toolCall.name, inputObj, props.toolCall.id,
273
promptContext.request?.hooks, promptContext.conversation?.sessionId,
274
props.token,
275
promptContext.stream
276
);
277
278
// Apply updatedInput from hook (input modification takes effect before invocation)
279
if (hookResult?.updatedInput) {
280
inputObj = hookResult.updatedInput;
281
}
282
283
const subAgentInvocationId = promptContext.request?.subAgentInvocationId;
284
// Capture the active trace context (from the invoke_agent span) so that
285
// the execute_tool span is properly parented even when async context
286
// propagation doesn't carry the active span.
287
const parentTraceContext = otelService.getActiveTraceContext();
288
const invocationOptions: LanguageModelToolInvocationOptions<unknown> = {
289
input: inputObj,
290
toolInvocationToken: props.toolInvocationToken,
291
tokenizationOptions,
292
chatRequestId: props.requestId,
293
subAgentInvocationId,
294
// Split on `__vscode` so it's the chat stream id
295
// TODO @lramos15 - This is a gross hack
296
chatStreamToolCallId: props.toolCall.id.split('__vscode')[0],
297
preToolUseResult: hookResult ? {
298
permissionDecision: hookResult.permissionDecision,
299
permissionDecisionReason: hookResult.permissionDecisionReason,
300
updatedInput: hookResult.updatedInput,
301
} : undefined,
302
};
303
// Attach trace context for span parenting (not in the VS Code API type)
304
(invocationOptions as { parentTraceContext?: { traceId: string; spanId: string } }).parentTraceContext = parentTraceContext;
305
306
const transcriptSessionId = promptContext.conversation?.sessionId;
307
if (transcriptSessionId) {
308
let parsedArgs: unknown;
309
try { parsedArgs = JSON.parse(props.toolCall.arguments); } catch { parsedArgs = props.toolCall.arguments; }
310
sessionTranscriptService.logToolExecutionStart(transcriptSessionId, props.toolCall.id, props.toolCall.name, parsedArgs);
311
}
312
313
toolResult = await toolsService.invokeToolWithEndpoint(props.toolCall.name, invocationOptions, promptEndpoint, props.token);
314
sendInvokedToolTelemetry(promptEndpoint.acquireTokenizer(), telemetryService, props.toolCall.name, toolResult);
315
316
// Run hook context handling after tool execution
317
appendHookContext(toolResult, hookResult, chatHookService, props, inputObj, promptContext);
318
319
if (transcriptSessionId) {
320
sessionTranscriptService.logToolExecutionComplete(transcriptSessionId, props.toolCall.id, true);
321
}
322
} catch (err) {
323
const errResult = toolCallErrorToResult(err);
324
toolResult = errResult.result;
325
isCancelled = errResult.isCancelled ?? false;
326
if (errResult.isCancelled) {
327
outcome = ToolInvocationOutcome.Cancelled;
328
} else {
329
outcome = outcome === ToolInvocationOutcome.DisabledByUser ? outcome : ToolInvocationOutcome.Error;
330
extraMetadata.push(new ToolFailureEncountered(props.toolCall.id));
331
logService.error(`Error from tool ${props.toolCall.name} with args ${props.toolCall.arguments}`, toErrorMessage(err, true));
332
}
333
if (promptContext.conversation?.sessionId) {
334
sessionTranscriptService.logToolExecutionComplete(promptContext.conversation.sessionId, props.toolCall.id, false);
335
}
336
}
337
}
338
339
sendToolCallTelemetry(props, promptContext, outcome, validation, endpointProvider, telemetryService);
340
}
341
342
return { toolResult, isCancelled, extraMetadata };
343
}
344
345
let call: IToolResultElementActualProps['call'];
346
if (tool?.source instanceof LanguageModelToolMCPSource || tool?.name && toolsCalledInParallel.has(tool.name as ToolName)) {
347
const promise = getToolResult({ tokenBudget: 1, countTokens: () => 1, endpoint: { modelMaxPromptTokens: 1 } });
348
call = () => promise;
349
} else {
350
call = getToolResult;
351
}
352
353
return <ToolResultElement
354
call={call}
355
enableCacheBreakpoints={props.enableCacheBreakpoints}
356
truncateAt={props.truncateAt}
357
toolCall={props.toolCall}
358
isLast={props.isLast}
359
sessionId={props.sessionId}
360
stripImages={props.stripImages}
361
sharedImageBudget={props.sharedImageBudget}
362
/>;
363
}
364
365
const toolsCalledInParallel = new Set<ToolName>([
366
ToolName.CoreRunSubagent,
367
ToolName.ReadFile,
368
ToolName.FindFiles,
369
ToolName.FindTextInFiles,
370
ToolName.ListDirectory,
371
ToolName.Codebase,
372
ToolName.GetErrors,
373
ToolName.GetScmChanges,
374
ToolName.GetNotebookSummary,
375
ToolName.ReadCellOutput,
376
ToolName.InstallExtension,
377
ToolName.FetchWebPage,
378
]);
379
380
async function sendToolCallTelemetry(props: ToolResultOpts, promptContext: IBuildPromptContext, invokeOutcome: ToolInvocationOutcome, validateOutcome: ToolValidationOutcome, endpointProvider: IEndpointProvider, telemetryService: ITelemetryService) {
381
const model = promptContext.request?.model && (await endpointProvider.getChatEndpoint(promptContext.request?.model)).model;
382
const toolName = props.toolCall.name;
383
384
/* __GDPR__
385
"toolInvoke" : {
386
"owner": "donjayamanne",
387
"comment": "Details about invocation of tools",
388
"validateOutcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the tool input validation. valid, invalid and unknown" },
389
"invokeOutcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the tool Invokcation. invalidInput, disabledByUser, success, error, cancelled" },
390
"toolName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The name of the tool being invoked." },
391
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" }
392
}
393
*/
394
telemetryService.sendMSFTTelemetryEvent('toolInvoke',
395
{
396
validateOutcome,
397
invokeOutcome,
398
toolName,
399
model
400
}
401
);
402
403
if (toolName === ToolName.EditNotebook) {
404
sendNotebookEditToolValidationTelemetry(invokeOutcome, validateOutcome, props.toolCall.arguments, telemetryService, model);
405
}
406
}
407
408
interface IToolResultElementActualProps {
409
call(sizing: PromptSizing): Promise<{
410
toolResult: LanguageModelToolResult2;
411
isCancelled: boolean;
412
extraMetadata: PromptMetadata[];
413
}>;
414
enableCacheBreakpoints: boolean;
415
truncateAt: number | undefined;
416
toolCall: IToolCall;
417
sessionId: string | undefined;
418
isLast: boolean;
419
stripImages?: boolean;
420
sharedImageBudget?: SharedImageBudget;
421
}
422
423
function buildImageUri(sessionId: string | undefined, toolCallId: string | undefined, imageIndex: number | undefined, mimeType: string): string | undefined {
424
if (!sessionId || !toolCallId || imageIndex === undefined) {
425
return undefined;
426
}
427
const coreToolCallId = toolCallId.split('__vscode')[0];
428
return buildToolImageResourceUri(sessionId, coreToolCallId, imageIndex, getExtensionForMimeType(mimeType) ?? '.bin');
429
}
430
431
/**
432
* Replaces image data parts with text placeholders in tool results.
433
* Used for historical turns to prevent large base64 image data from
434
* accumulating and causing 413 (request too large) errors from the API.
435
*/
436
function replaceImagesWithPlaceholders(
437
content: LanguageModelToolResult2['content'],
438
toolCallId: string | undefined,
439
sessionId: string | undefined,
440
): LanguageModelToolResult2['content'] {
441
if (!content.some(part => isImageDataPart(part))) {
442
return content;
443
}
444
return content.map((part, index) => {
445
if (!isImageDataPart(part)) {
446
return part;
447
}
448
const uri = buildImageUri(sessionId, toolCallId, index, part.mimeType);
449
const uriRef = uri ? ` Image URI: ${uri}` : '';
450
return new LanguageModelTextPart(`[Image was previously shown to you.${uriRef}]`);
451
});
452
}
453
454
/**
455
* One tool call result, which either comes from the cache or from invoking the tool.
456
*/
457
class ToolResultElement extends PromptElement<IToolResultElementActualProps & BasePromptElementProps, void> {
458
async render(state: void, sizing: PromptSizing) {
459
const { extraMetadata, toolResult, isCancelled } = await this.props.call(sizing);
460
461
// For historical turns, replace image data with text placeholders
462
// to avoid accumulating large base64 payloads across conversation turns (413 errors)
463
const content = this.props.stripImages
464
? replaceImagesWithPlaceholders(toolResult.content, this.props.toolCall.id, this.props.sessionId)
465
: toolResult.content;
466
467
const toolResultElement = this.props.enableCacheBreakpoints ?
468
<>
469
<Chunk>
470
<ToolResult content={content} truncate={this.props.truncateAt} toolCallId={this.props.toolCall.id} sessionId={this.props.sessionId} toolName={this.props.toolCall.name} sharedImageBudget={this.props.sharedImageBudget} />
471
</Chunk>
472
</> :
473
<ToolResult content={content} truncate={this.props.truncateAt} toolCallId={this.props.toolCall.id} sessionId={this.props.sessionId} toolName={this.props.toolCall.name} sharedImageBudget={this.props.sharedImageBudget} />;
474
475
return (
476
<ToolMessage toolCallId={this.props.toolCall.id!}>
477
<meta value={new ToolResultMetadata(this.props.toolCall.id!, toolResult, isCancelled)} />
478
{...extraMetadata.map(m => <meta value={m} />)}
479
{toolResultElement}
480
{this.props.isLast && this.props.enableCacheBreakpoints && <cacheBreakpoint type={CacheType} />}
481
</ToolMessage>
482
);
483
}
484
}
485
486
export function sendInvokedToolTelemetry(tokenizer: ITokenizer, telemetry: ITelemetryService, toolName: string, toolResult: LanguageModelToolResult2) {
487
new BasePromptRenderer(
488
{ modelMaxPromptTokens: Infinity },
489
class extends PromptElement {
490
render() {
491
return <UserMessage><PrimitiveToolResult content={toolResult.content} /></UserMessage>;
492
}
493
},
494
{},
495
tokenizer,
496
).render().then(({ tokenCount }) => {
497
/* __GDPR__
498
"agent.tool.responseLength" : {
499
"owner": "connor4312",
500
"comment": "Counts the number of tokens generated by tools",
501
"toolName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The name of the tool being invoked." },
502
"tokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of tokens used.", "isMeasurement": true }
503
}
504
*/
505
telemetry.sendMSFTTelemetryEvent('agent.tool.responseLength', { toolName }, { tokenCount });
506
});
507
}
508
509
enum ToolValidationOutcome {
510
Valid = 'valid',
511
Invalid = 'invalid',
512
Unknown = 'unknown'
513
}
514
515
enum ToolInvocationOutcome {
516
InvalidInput = 'invalidInput',
517
DisabledByUser = 'disabledByUser',
518
Success = 'success',
519
Error = 'error',
520
Cancelled = 'cancelled',
521
}
522
523
export async function imageDataPartToTSX(part: LanguageModelDataPart, githubToken?: string, urlOrRequestMetadata?: string | RequestMetadata, logService?: ILogService, imageService?: IImageService) {
524
if (isImageDataPart(part)) {
525
let imageData: Uint8Array = part.data;
526
let mimeType = part.mimeType;
527
528
if (imageService) {
529
try {
530
const resized = await imageService.resizeImage(imageData, mimeType);
531
imageData = resized.data;
532
mimeType = resized.mimeType;
533
} catch (error) {
534
logService?.warn(`Image resize failed, using original: ${error}`);
535
}
536
}
537
538
const base64 = Buffer.from(imageData).toString('base64');
539
let imageSource = `data:${mimeType};base64,${base64}`;
540
const isChatRequest = typeof urlOrRequestMetadata !== 'string' && (
541
urlOrRequestMetadata?.type === RequestType.ChatCompletions ||
542
urlOrRequestMetadata?.type === RequestType.ChatResponses ||
543
urlOrRequestMetadata?.type === RequestType.ChatMessages);
544
if (githubToken && isChatRequest && imageService) {
545
try {
546
const uri = await imageService.uploadChatImageAttachment(imageData, 'tool-result-image', mimeType ?? 'image/png', githubToken);
547
if (uri) {
548
imageSource = uri.toString();
549
}
550
} catch (error) {
551
if (logService) {
552
logService.warn(`Image upload failed, using base64 fallback: ${error}`);
553
}
554
}
555
}
556
557
return <Image src={imageSource} mimeType={mimeType} />;
558
}
559
}
560
561
/**
562
* Appends hook context to a tool result after execution.
563
* Handles preToolUse additionalContext and executes the postToolUse hook,
564
* appending block messages and additionalContext as `<*-context>` tags.
565
*/
566
async function appendHookContext(
567
toolResult: LanguageModelToolResult2,
568
preHookResult: IPreToolUseHookResult | undefined,
569
chatHookService: IChatHookService,
570
props: ToolResultOpts,
571
toolInput: unknown,
572
promptContext: IBuildPromptContext,
573
): Promise<void> {
574
// Append additional context from preToolUse hook
575
if (preHookResult?.additionalContext) {
576
for (const context of preHookResult.additionalContext) {
577
toolResult.content.push(new LanguageModelTextPart('\n<PreToolUse-context>\n' + context + '\n</PreToolUse-context>'));
578
}
579
}
580
581
// Skip postToolUse hook if preToolUse denied the tool — no tool actually ran
582
if (preHookResult?.permissionDecision === 'deny') {
583
return;
584
}
585
586
// Execute postToolUse hook after successful tool execution
587
const postHookResult = await chatHookService.executePostToolUseHook(
588
props.toolCall.name,
589
toolInput,
590
toolResultToText(toolResult),
591
props.toolCall.id,
592
promptContext.request?.hooks,
593
promptContext.conversation?.sessionId,
594
props.token,
595
promptContext.stream
596
);
597
if (postHookResult?.decision === 'block') {
598
const blockReason = postHookResult.reason ?? 'Hook blocked tool result';
599
const blockMessage = `The PostToolUse hook blocked this tool result. Reason: ${blockReason}`;
600
toolResult.content.push(new LanguageModelTextPart('\n<PostToolUse-context>\n' + blockMessage + '\n</PostToolUse-context>'));
601
}
602
if (postHookResult?.additionalContext) {
603
for (const context of postHookResult.additionalContext) {
604
toolResult.content.push(new LanguageModelTextPart('\n<PostToolUse-context>\n' + context + '\n</PostToolUse-context>'));
605
}
606
}
607
}
608
609
function toolResultToText(result: LanguageModelToolResult2): string {
610
return result.content
611
.filter((part): part is LanguageModelTextPart | LanguageModelTextPart2 =>
612
part instanceof LanguageModelTextPart || part instanceof LanguageModelTextPart2)
613
.map(part => part.value)
614
.join('\n');
615
}
616
617
function textToolResult(text: string): LanguageModelToolResult {
618
return new LanguageModelToolResult([new LanguageModelTextPart(text)]);
619
}
620
621
export function toolCallErrorToResult(err: unknown) {
622
if (isCancellationError(err)) {
623
return { result: textToolResult('The user cancelled the tool call.'), isCancelled: true };
624
} else {
625
const errorMessage = err instanceof Error ? err.message : String(err);
626
return { result: textToolResult(`ERROR while calling tool: ${errorMessage}${toolErrorSuffix}`) };
627
}
628
}
629
630
export class ToolFailureEncountered extends PromptMetadata {
631
constructor(
632
public toolCallId: string
633
) {
634
super();
635
}
636
}
637
638
export class ToolResultMetadata extends PromptMetadata {
639
constructor(
640
public readonly toolCallId: string,
641
public readonly result: LanguageModelToolResult2,
642
public isCancelled?: boolean
643
) {
644
super();
645
}
646
}
647
648
// Some MCP servers return a ton of resources as a 'download' action.
649
// Only include them all eagerly if we have a manageable number.
650
const DONT_INCLUDE_RESOURCE_CONTENT_IF_TOOL_HAS_MORE_THAN = 9;
651
652
class McpLinkedResourceToolResult extends PromptElement<{ resourceUri: URI; mimeType: string | undefined; count: number } & BasePromptElementProps> {
653
public static readonly mimeType = 'application/vnd.code.resource-link';
654
private static MAX_PREVIEW_LINES = 500;
655
656
constructor(
657
props: { resourceUri: URI; mimeType: string | undefined; count: number } & BasePromptElementProps,
658
@IFileSystemService private readonly fileSystemService: IFileSystemService,
659
@IIgnoreService private readonly ignoreService: IIgnoreService,
660
) {
661
super(props);
662
}
663
664
async render() {
665
if (await this.ignoreService.isCopilotIgnored(this.props.resourceUri)) {
666
return null;
667
}
668
669
if (this.props.count > DONT_INCLUDE_RESOURCE_CONTENT_IF_TOOL_HAS_MORE_THAN) {
670
return <Tag name='resource' attrs={{ uri: this.props.resourceUri.toString() }} />;
671
}
672
673
let contents: Uint8Array;
674
try {
675
contents = await this.fileSystemService.readFile(this.props.resourceUri);
676
} catch (e) {
677
const isNotFound = e instanceof Error && ('code' in e && (e.code === 'FileNotFound' || e.code === 'EntryNotFound'));
678
const message = isNotFound
679
? 'resource not found - the file may have been deleted or become inaccessible'
680
: `failed to read resource - ${toErrorMessage(e)}`;
681
return <Tag name='resource' attrs={{ uri: this.props.resourceUri.toString() }}>
682
{message}
683
</Tag>;
684
}
685
const lines = new TextDecoder().decode(contents).split(/\r?\n/g);
686
const maxLines = McpLinkedResourceToolResult.MAX_PREVIEW_LINES;
687
688
return <>
689
<Tag name='resource' attrs={{ uri: this.props.resourceUri.toString(), isTruncated: lines.length > maxLines }}>
690
{lines.slice(0, maxLines).join('\n')}
691
</Tag>
692
</>;
693
}
694
}
695
696
interface IPrimitiveToolResultProps extends BasePromptElementProps {
697
content: LanguageModelToolResult2['content'];
698
/**
699
* Shared budget limiting total image data across all tool results in a turn.
700
*/
701
sharedImageBudget?: SharedImageBudget;
702
}
703
704
class PrimitiveToolResult<T extends IPrimitiveToolResultProps> extends PromptElement<T> {
705
protected readonly linkedResources: LanguageModelDataPart[];
706
707
/**
708
* Some models do not yet support CAPI image uploads. For these cases,
709
* track the number of images bytes we're sending and truncate any images
710
* that would exceed that budget.
711
*/
712
private imageSizeBudgetLeft = CAPI_IMAGE_BUDGET_BYTES;
713
714
constructor(
715
props: T,
716
@IPromptEndpoint protected readonly endpoint: IPromptEndpoint,
717
@IAuthenticationService private readonly authService: IAuthenticationService,
718
@ILogService private readonly logService?: ILogService,
719
@IImageService private readonly imageService?: IImageService,
720
@IConfigurationService private readonly configurationService?: IConfigurationService,
721
@IExperimentationService private readonly experimentationService?: IExperimentationService
722
) {
723
super(props);
724
this.linkedResources = this.props.content.filter((c): c is LanguageModelDataPart => c instanceof LanguageModelDataPart && c.mimeType === McpLinkedResourceToolResult.mimeType);
725
}
726
727
async render(): Promise<PromptPiece | undefined> {
728
729
return (
730
<>
731
<IfEmpty alt='(empty)'>
732
{await Promise.all(this.props.content.filter(part => this.hasAssistantAudience(part)).map(async part => {
733
if (part instanceof LanguageModelTextPart) {
734
return await this.onText(part.value);
735
} else if (part instanceof LanguageModelPromptTsxPart) {
736
return await this.onTSX(part.value as JSONTree.PromptElementJSON);
737
} else if (isImageDataPart(part)) {
738
return await this.onImage(part, this.props.content.indexOf(part));
739
} else if (part instanceof LanguageModelDataPart) {
740
return await this.onData(part);
741
}
742
}))}
743
{this.linkedResources.length > 0 && `\n\nHint: you can read the full contents of any ${this.linkedResources.length > DONT_INCLUDE_RESOURCE_CONTENT_IF_TOOL_HAS_MORE_THAN ? '' : 'truncated '}resources by passing their URIs as the absolutePath to the ${ToolName.ReadFile}.\n`}
744
</IfEmpty>
745
</>
746
);
747
}
748
749
private hasAssistantAudience(part: LanguageModelTextPart2 | LanguageModelPromptTsxPart | LanguageModelDataPart2 | unknown): boolean {
750
if (part instanceof LanguageModelPromptTsxPart) {
751
return true;
752
}
753
if (!(part instanceof LanguageModelDataPart2 || part instanceof LanguageModelTextPart2) || !part.audience) {
754
return true;
755
}
756
return part.audience.includes(LanguageModelPartAudience.Assistant);
757
}
758
759
protected async onData(part: LanguageModelDataPart) {
760
if (part.mimeType === McpLinkedResourceToolResult.mimeType) {
761
return this.onResourceLink(new TextDecoder().decode(part.data));
762
} else {
763
return '';
764
}
765
}
766
767
protected async onImage(part: LanguageModelDataPart, _imageIndex?: number) {
768
if (!this.endpoint.supportsVision) {
769
return '[Image content is not available because vision is not supported by the current model or is disabled by your organization.]';
770
}
771
772
const uploadsEnabled = this.configurationService && this.experimentationService
773
? this.configurationService.getExperimentBasedConfig(ConfigKey.EnableChatImageUpload, this.experimentationService)
774
: false;
775
776
// Anthropic (from CAPI) currently does not support image uploads from tool calls.
777
const canUpload = uploadsEnabled && modelCanUseMcpResultImageURL(this.endpoint);
778
779
// Enforce image budgets only when images will be inlined as base64.
780
// When uploads are available, the request body stays small (URL reference).
781
if (!canUpload) {
782
// Enforce shared cross-tool budget (prevents 413s when many tools return images)
783
const sharedBudget = this.props.sharedImageBudget;
784
if (sharedBudget) {
785
if (sharedBudget.remaining < 0) {
786
return this.sharedBudgetPlaceholder();
787
} else if (part.data.length > sharedBudget.remaining) {
788
sharedBudget.remaining = -1;
789
return this.sharedBudgetPlaceholder();
790
}
791
sharedBudget.remaining -= part.data.length;
792
}
793
794
// Enforce per-tool budget
795
if (this.imageSizeBudgetLeft < 0) {
796
return ''; // already exceeded and messages about it
797
} else if (part.data.length > this.imageSizeBudgetLeft) {
798
this.imageSizeBudgetLeft = -1; // just now exceeding
799
return 'Additional images are available, but there is no more space in the context. Try requesting a smaller amount of data, if possible.';
800
} else {
801
this.imageSizeBudgetLeft -= part.data.length; // bookkeep
802
}
803
}
804
805
// Only call getGitHubSession when uploads are potentially available
806
let uploadToken: string | undefined;
807
if (canUpload) {
808
uploadToken = (await this.authService.getGitHubSession('any', { silent: true }))?.accessToken;
809
}
810
811
return Promise.resolve(imageDataPartToTSX(part, uploadToken, this.endpoint.urlOrRequestMetadata, this.logService, this.imageService));
812
}
813
814
protected onTSX(part: JSONTree.PromptElementJSON) {
815
return Promise.resolve(<elementJSON data={part} />);
816
}
817
818
protected onText(part: string) {
819
return Promise.resolve(part);
820
}
821
822
protected onResourceLink(data: string) {
823
return '';
824
}
825
826
protected sharedBudgetPlaceholder(): string {
827
return '[Image omitted — context image budget exceeded. Try viewing fewer images at once.]';
828
}
829
}
830
831
export interface IToolResultProps extends IPrimitiveToolResultProps {
832
/**
833
* Number of tokens at which truncation will be triggered for string content.
834
*/
835
truncate?: number;
836
/**
837
* The tool call associated with this result.
838
*/
839
toolCallId: string | undefined;
840
/**
841
* The session ID associated with this result.
842
*/
843
sessionId?: string;
844
/**
845
* The name of the tool that produced this result.
846
*/
847
toolName?: string;
848
}
849
850
851
/**
852
* Inlined from prompt-tsx. In prompt-tsx it does `require('vscode)` for the instanceof checks which breaks in vitest
853
* and unfortunately I can't figure out how to work around that with the tools we have!
854
*/
855
export class ToolResult extends PrimitiveToolResult<IToolResultProps> {
856
constructor(
857
props: IToolResultProps,
858
@IPromptEndpoint endpoint: IPromptEndpoint,
859
@IAuthenticationService authService: IAuthenticationService,
860
@ILogService private readonly _logService: ILogService,
861
@IImageService imageService: IImageService,
862
@IConfigurationService private readonly _configurationService: IConfigurationService,
863
@IExperimentationService private readonly _experimentationService: IExperimentationService,
864
@IChatDiskSessionResources private readonly diskSessionResources: IChatDiskSessionResources,
865
) {
866
super(props, endpoint, authService, _logService, imageService, _configurationService, _experimentationService);
867
}
868
869
protected override async onTSX(part: JSONTree.PromptElementJSON): Promise<any> {
870
if (this.props.truncate) {
871
return <TokenLimit max={this.props.truncate}>{await super.onTSX(part)}</TokenLimit>;
872
}
873
874
return super.onTSX(part);
875
}
876
877
protected override async onImage(part: LanguageModelDataPart, imageIndex?: number): Promise<PromptPiece | undefined> {
878
const image = await super.onImage(part, imageIndex);
879
if (!image || imageIndex === undefined || !this.props.toolCallId || !this.props.sessionId) {
880
return image;
881
}
882
const uri = buildImageUri(this.props.sessionId, this.props.toolCallId, imageIndex, part.mimeType);
883
return <>{image}{uri && `\n[Image URI: ${uri}]`}</>;
884
}
885
886
protected override sharedBudgetPlaceholder(): string {
887
return '[Image omitted — context image budget exceeded. Try viewing fewer images at once or reference this image by URI.]';
888
}
889
890
protected override async onText(content: string): Promise<string> {
891
const isDiskCachingEnabled = this._configurationService.getExperimentBasedConfig(
892
ConfigKey.Advanced.LargeToolResultsToDiskEnabled,
893
this._experimentationService
894
);
895
// Exempt the search and execution subagents and memory tool from disk caching as their results are often ignored if not written directly to the conversation
896
if (isDiskCachingEnabled && this.diskSessionResources && this.props.toolCallId && this.props.sessionId && this.props.toolName !== ToolName.SearchSubagent && this.props.toolName !== ToolName.ExecutionSubagent && this.props.toolName !== ToolName.Memory) {
897
const thresholdBytes = this._configurationService.getExperimentBasedConfig(
898
ConfigKey.Advanced.LargeToolResultsToDiskThreshold,
899
this._experimentationService
900
);
901
902
if (content.length > thresholdBytes) {
903
try {
904
const sessionId = this.props.sessionId ?? 'unknown';
905
const toolCallId = this.props.toolCallId;
906
907
let contentFile = 'content.txt';
908
let schema: string | undefined;
909
try {
910
const parsed = JSON.parse(content);
911
schema = JSON.stringify(toJsonSchema(parsed));
912
// re-stringify it as it's more friendly to line-based offsets in the read_file tool
913
content = JSON.stringify(parsed, null, 2);
914
contentFile = 'content.json';
915
} catch {
916
// ignored
917
}
918
919
const fileUri = await this.diskSessionResources.ensure(
920
sessionId,
921
toolCallId,
922
{ [contentFile]: content, 'schema.json': schema },
923
);
924
925
const filePath = fileUri.fsPath;
926
const contentFileUri = URI.joinPath(fileUri, contentFile);
927
const schemaFileUri = schema ? URI.joinPath(fileUri, 'schema.json') : undefined;
928
this._logService?.debug(`[ToolResult] Large tool result (${content.length} bytes) written to disk: ${filePath}`);
929
930
return `Large tool result (${Math.round(content.length / 1024)}KB) written to file. Use the ${ToolName.ReadFile} tool to access the content at: ${contentFileUri.fsPath}${schemaFileUri ? `\n\nData schema found at: ${schemaFileUri.fsPath}` : ''}`;
931
} catch (err) {
932
this._logService?.warn(`[ToolResult] Failed to write large tool result to disk: ${toErrorMessage(err)}`);
933
// Fall through to normal truncation
934
}
935
}
936
}
937
938
// Standard truncation logic
939
const truncateAtTokens = this.props.truncate;
940
if (!truncateAtTokens || content.length < truncateAtTokens) { // always >=1 character per token, early bail-out
941
return content;
942
}
943
944
const tokens = await this.endpoint.acquireTokenizer().tokenLength(content);
945
if (tokens < truncateAtTokens) {
946
return content;
947
}
948
949
const approxCharsPerToken = content.length / tokens;
950
const removedMessage = '\n[Tool response was too long and was truncated.]\n';
951
const targetChars = Math.round(approxCharsPerToken * (truncateAtTokens - removedMessage.length));
952
953
const keepInFirstHalf = Math.round(targetChars * 0.4);
954
const keepInSecondHalf = targetChars - keepInFirstHalf;
955
956
return content.slice(0, keepInFirstHalf) + removedMessage + content.slice(-keepInSecondHalf);
957
}
958
959
protected override onResourceLink(data: string) {
960
// https://github.com/microsoft/vscode/blob/34e38b4a78a751d006b99acee1a95d76117fec7b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts#L846
961
let parsed: {
962
uri: UriComponents;
963
underlyingMimeType?: string;
964
};
965
966
try {
967
parsed = JSON.parse(data);
968
} catch {
969
return null;
970
}
971
972
return <McpLinkedResourceToolResult resourceUri={URI.revive(parsed.uri)} mimeType={parsed.underlyingMimeType} count={this.linkedResources.length} />;
973
}
974
}
975
976
export interface IToolCallResultWrapperProps extends BasePromptElementProps {
977
toolCallResults: IResultMetadata['toolCallResults'];
978
}
979
980
// Wrapper around ToolResult to allow rendering prompts
981
export class ToolCallResultWrapper extends PromptElement<IToolCallResultWrapperProps> {
982
async render(): Promise<PromptPiece | undefined> {
983
return (
984
<>
985
{Object.entries(this.props.toolCallResults ?? {}).map(([toolCallId, toolCallResult]) => (
986
<ToolMessage toolCallId={toolCallId}>
987
<ToolResult content={toolCallResult.content} toolCallId={undefined} />
988
</ToolMessage>
989
))}
990
</>
991
);
992
}
993
}
994
995
function sendNotebookEditToolValidationTelemetry(invokeOutcome: ToolInvocationOutcome, validationResult: ToolValidationOutcome, toolArgs: string, telemetryService: ITelemetryService, model?: string): void {
996
let editType: 'insert' | 'delete' | 'edit' | 'unknown' = 'unknown';
997
let explanation: 'provided' | 'empty' | 'unknown' = 'unknown';
998
let newCodeType: 'string' | 'string[]' | 'object' | 'object[]' | 'unknown' | '' = 'unknown';
999
let cellId: 'TOP' | 'BOTTOM' | 'cellid' | 'unknown' | 'empty' = 'unknown';
1000
let inputParsed = 0;
1001
const knownProps = ['editType', 'explanation', 'newCode', 'cellId', 'filePath', 'language'];
1002
let missingProps: string[] = [];
1003
let unknownProps: string[] = [];
1004
try {
1005
const args = JSON.parse(toolArgs);
1006
if (args && typeof args === 'object' && !Array.isArray(args) && Object.keys(args).length > 0) {
1007
const props = Object.keys(args);
1008
unknownProps = props.filter(key => !knownProps.includes(key));
1009
unknownProps.sort();
1010
missingProps = knownProps.filter(key => !props.includes(key));
1011
missingProps.sort();
1012
}
1013
inputParsed = 1;
1014
if (args.editType) {
1015
editType = args.editType;
1016
}
1017
if (args.explanation) {
1018
explanation = 'provided';
1019
} else {
1020
explanation = 'empty';
1021
}
1022
if (args.newCode || typeof args.newCode === 'string') {
1023
if (typeof args.newCode === 'string') {
1024
newCodeType = 'string';
1025
} else if (Array.isArray(args.newCode) && (args.newCode as any[]).every(item => typeof item === 'string')) {
1026
newCodeType = 'string[]';
1027
} else if (Array.isArray(args.newCode)) {
1028
newCodeType = 'object[]';
1029
} else if (typeof args.newCode === 'object') {
1030
newCodeType = 'object';
1031
}
1032
}
1033
if (editType === 'delete') {
1034
newCodeType = '';
1035
}
1036
const cellIdValue = args.cellId;
1037
if (typeof cellIdValue === 'string') {
1038
if (cellIdValue === 'TOP' || cellIdValue === 'BOTTOM') {
1039
cellId = cellIdValue;
1040
} else {
1041
cellId = cellIdValue.trim().length === 0 ? 'cellid' : 'empty';
1042
}
1043
}
1044
} catch {
1045
//
1046
}
1047
1048
/* __GDPR__
1049
"editNotebook.validation" : {
1050
"owner": "donjayamanne",
1051
"comment": "Validation failure for a Edit Notebook tool invocation",
1052
"validationResult": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The result of the tool input validation. valid, invalid and unknown" },
1053
"invokeOutcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The result of the tool Invocation. invalidInput, disabledByUser, success, error, cancelled" },
1054
"editType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The type of edit that was attempted. insert, delete, edit or unknown" },
1055
"unknownProps": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "List of unknown properties in the input" },
1056
"missingProps": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "List of missing properties in the input" },
1057
"newCodeType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The type of code, whether its string, string[], object, object[] or unknown" },
1058
"cellId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the cell, TOP, BOTTOM, cellid, empty or unknown" },
1059
"explanation": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The explanation for the edit. provided, empty and unknown" },
1060
"inputParsed": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the input was parsed as JSON" },
1061
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" }
1062
}
1063
*/
1064
1065
telemetryService.sendMSFTTelemetryEvent('editNotebook.validation',
1066
{
1067
validationResult,
1068
invokeOutcome,
1069
editType,
1070
newCodeType,
1071
cellId,
1072
explanation,
1073
model,
1074
unknownProps: unknownProps.join(','),
1075
missingProps: missingProps.join(','),
1076
},
1077
{
1078
inputParsed,
1079
}
1080
);
1081
}
1082
1083
export function buildToolImageResourceUri(sessionId: string, coreToolCallId: string, imageIndex: number, ext: string): string {
1084
const sessionResource = `vscode-chat-session://local/${Buffer.from(sessionId).toString('base64url')}`;
1085
const authority = Buffer.from(sessionResource).toString('hex');
1086
return `vscode-chat-response-resource://${authority}/tool/${coreToolCallId}/${imageIndex}/file${ext}`;
1087
}
1088
1089