Skip to content
Draft
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
12 changes: 12 additions & 0 deletions extensions/copilot/src/extension/intents/node/toolCallingLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,8 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
const shouldDisableThinking = isContinuation && isAnthropicFamily(endpoint) && !ToolCallingLoop.messagesContainThinking(effectiveBuildPromptResult.messages);
const enableThinking = !shouldDisableThinking;
let phase: string | undefined;
let responseOutputIndex: number | undefined;
const responseOutputItems: NonNullable<IToolCallRound['responseOutputItems']> = [];
let compaction: OpenAIContextManagementResponse | undefined;
markChatExt(this.options.conversation.sessionId, ChatExtPerfMark.WillFetch);
const fetchOptions: ToolCallingLoopFetchOptions = {
Expand All @@ -1601,6 +1603,14 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
if (delta.phase) {
phase = delta.phase;
}
if (delta.responseOutputIndex !== undefined) {
responseOutputIndex = responseOutputIndex === undefined
? delta.responseOutputIndex
: Math.min(responseOutputIndex, delta.responseOutputIndex);
}
if (delta.responseOutputItem) {
responseOutputItems.push(delta.responseOutputItem);
}
if (delta.contextManagement && isOpenAIContextManagementResponse(delta.contextManagement)) {
compaction = delta.contextManagement;
}
Expand Down Expand Up @@ -1734,6 +1744,8 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
thinking: thinkingItem,
phase,
modelId: endpoint.model,
responseOutputIndex,
responseOutputItems: responseOutputItems.length ? responseOutputItems : undefined,
compaction,
}),
chatResult,
Expand Down
5 changes: 5 additions & 0 deletions extensions/copilot/src/extension/prompt/common/intents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type * as vscode from 'vscode';
import { NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot';
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
import type { IResponseOutputItem } from '../../../platform/networking/common/fetch';
import { OpenAIContextManagementResponse } from '../../../platform/networking/common/openai';
import { ThinkingData } from '../../../platform/thinking/common/thinking';
import { createServiceIdentifier } from '../../../util/common/services';
Expand Down Expand Up @@ -53,6 +54,10 @@ export interface IToolCallRound {
phase?: string;
/** The model ID. */
modelId?: string;
/** The first Responses API output index represented by `response`. */
responseOutputIndex?: number;
/** Individual Responses API assistant messages retained for ordered replay. */
responseOutputItems?: IResponseOutputItem[];
}

export interface InternalToolReference extends vscode.ChatLanguageModelToolReference {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class ToolCallRound implements IToolCallRound {
public summary: string | undefined;
public phase?: string;
public modelId?: string;
public responseOutputItems?: IToolCallRound['responseOutputItems'];

/**
* Creates a ToolCallRound from an existing IToolCallRound object.
Expand All @@ -34,10 +35,12 @@ export class ToolCallRound implements IToolCallRound {
params.timestamp,
params.compaction,
params.statefulMarkerSummarizedAtRoundId,
params.responseOutputIndex,
);
round.summary = params.summary;
round.phase = params.phase;
round.modelId = params.modelId;
round.responseOutputItems = params.responseOutputItems;
return round;
}

Expand All @@ -60,6 +63,7 @@ export class ToolCallRound implements IToolCallRound {
public readonly timestamp: number = Date.now(),
public readonly compaction?: OpenAIContextManagementResponse,
public readonly statefulMarkerSummarizedAtRoundId?: string,
public readonly responseOutputIndex?: number,
) { }

private static generateID(): string {
Expand All @@ -73,6 +77,7 @@ export class ThinkingDataItem implements ThinkingData {
public tokens?: number;
public encrypted?: string;
public redacted?: boolean;
public outputIndex?: number;

static createOrUpdate(item: ThinkingDataItem | undefined, delta: ThinkingDelta) {
if (!item) {
Expand Down Expand Up @@ -119,6 +124,9 @@ export class ThinkingDataItem implements ThinkingData {
if (delta.metadata) {
this.metadata = delta.metadata;
}
if (delta.outputIndex !== undefined) {
this.outputIndex = delta.outputIndex;
}
}

public updateWithFetchResult(fetchResult: FetchSuccess<unknown>): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 0",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -49,6 +51,8 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 1",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -69,6 +73,8 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 2",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand Down Expand Up @@ -110,6 +116,8 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 3",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -130,6 +138,8 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 4",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -150,6 +160,8 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 5",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand Down Expand Up @@ -234,6 +246,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 0",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -254,6 +268,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 1",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -274,6 +290,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 2",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -294,6 +312,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 3",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand Down Expand Up @@ -358,6 +378,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 4",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -378,6 +400,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 5",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -398,6 +422,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 6",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -418,6 +444,8 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response number 7",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand Down Expand Up @@ -2571,6 +2599,8 @@ exports[`defaultIntentRequestHandler > makes a successful request with a single
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "some response here :)",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand Down Expand Up @@ -2907,6 +2937,8 @@ exports[`defaultIntentRequestHandler > makes a tool call turn 1`] = `
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "some response here :)",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand All @@ -2927,6 +2959,8 @@ exports[`defaultIntentRequestHandler > makes a tool call turn 1`] = `
"modelId": "gpt-4.1-2025-04-14",
"phase": undefined,
"response": "response to tool call",
"responseOutputIndex": undefined,
"responseOutputItems": undefined,
"statefulMarker": undefined,
"statefulMarkerSummarizedAtRoundId": undefined,
"summary": undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Raw } from '@vscode/prompt-tsx';
import { describe, expect, test } from 'vitest';
import type * as vscode from 'vscode';
import { IChatHookService, type IPreToolUseHookResult } from '../../../../../platform/chat/common/chatHookService';
import { ConfigKey, IConfigurationService } from '../../../../../platform/configuration/common/configurationService';
import { rawPartAsCompactionData } from '../../../../../platform/endpoint/common/compactionDataContainer';
import { IEndpointProvider } from '../../../../../platform/endpoint/common/endpointProvider';
import { rawPartAsPhaseData } from '../../../../../platform/endpoint/common/phaseDataContainer';
import { rawPartAsThinkingData } from '../../../../../platform/endpoint/common/thinkingDataContainer';
import type { IChatEndpoint } from '../../../../../platform/networking/common/networking';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry';
import { SpyingTelemetryService } from '../../../../../platform/telemetry/node/spyingTelemetryService';
import { DeferredPromise } from '../../../../../util/vs/base/common/async';
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
import { Event } from '../../../../../util/vs/base/common/event';
import { constObservable } from '../../../../../util/vs/base/common/observable';
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry';
import { SpyingTelemetryService } from '../../../../../platform/telemetry/node/spyingTelemetryService';
import { LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolResult } from '../../../../../vscodeTypes';
import { ChatVariablesCollection } from '../../../../prompt/common/chatVariablesCollection';
import type { Conversation } from '../../../../prompt/common/conversation';
Expand Down Expand Up @@ -222,6 +226,80 @@ class ParallelAwareToolsService implements IToolsService {
}

describe('ChatToolCalls (toolCalling.tsx)', () => {
test('preserves interleaved Responses API output item order', async () => {
const testingServiceCollection = createExtensionUnitTestingServices();
const accessor = testingServiceCollection.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const endpointProvider = accessor.get(IEndpointProvider);
const endpoint = await endpointProvider.getChatEndpoint('copilot-utility');
const round: IToolCallRound = {
id: 'round-1',
modelId: endpoint.model,
response: 'commentary\n\nfinal answer',
responseOutputIndex: 0,
responseOutputItems: [
{ text: 'commentary', phase: 'commentary', outputIndex: 0 },
{ text: 'final answer', phase: 'final_answer', outputIndex: 2 },
],
thinking: {
id: 'rs_after_commentary',
text: '',
encrypted: 'enc_reasoning',
outputIndex: 1,
},
compaction: {
type: 'compaction',
id: 'cmp_after_messages',
encrypted_content: 'enc_compaction',
outputIndex: 3,
},
toolInputRetry: 0,
toolCalls: [],
};
const promptContext: IBuildPromptContext = {
query: 'test',
history: [],
chatVariables: new ChatVariablesCollection(),
conversation: { sessionId: 'session-order' } as unknown as Conversation,
request: {} as vscode.ChatRequest,
tools: {
toolReferences: [],
toolInvocationToken: {} as vscode.ChatParticipantToolToken,
availableTools: [],
},
};

const { messages } = await renderPromptElement(instantiationService, endpoint, ChatToolCalls, {
promptContext,
toolCallRounds: [round],
toolCallResults: undefined,
});
const assistantMessage = messages.find(message => message.role === Raw.ChatRole.Assistant);

expect(assistantMessage?.content.map(part => {
if (part.type === Raw.ChatCompletionContentPartKind.Text) {
return `message:${part.text}`;
}
if (part.type === Raw.ChatCompletionContentPartKind.Opaque && rawPartAsThinkingData(part)) {
return 'reasoning';
}
if (part.type === Raw.ChatCompletionContentPartKind.Opaque && rawPartAsPhaseData(part)) {
return `phase:${rawPartAsPhaseData(part)}`;
}
if (part.type === Raw.ChatCompletionContentPartKind.Opaque && rawPartAsCompactionData(part)) {
return 'compaction';
}
return 'other';
})).toEqual([
'phase:commentary',
'message:commentary',
'reasoning',
'phase:final_answer',
'message:final answer',
'compaction',
]);
});

test('starts multiple sub-agent tool calls in parallel', async () => {
const toolName = ToolName.CoreRunSubagent;
const firstCallId = 'subagent-call-1';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,30 @@ export class ChatToolCalls extends PromptElement<ChatToolCallsProps, void> {
const thinking = includeThinking && round.thinking && <ThinkingDataContainer thinking={round.thinking} />;
const phase = (round.phase && roundModelId === this.promptEndpoint.model) ? <PhaseDataContainer phase={round.phase} /> : undefined;
const compaction = round.compaction && <CompactionDataContainer compaction={round.compaction} />;
const responseItems = round.responseOutputItems?.length
? round.responseOutputItems.map(item => ({
outputIndex: item.outputIndex,
legacyOrder: 2,
content: <>{item.phase && sameModelAsEndpoint ? <PhaseDataContainer phase={item.phase} /> : undefined}{item.text}</>,
}))
: [{ outputIndex: round.responseOutputIndex, legacyOrder: 2, content: <>{phase}{round.response}</> }];
const replayItems = [
...responseItems,
{ outputIndex: round.thinking?.outputIndex, legacyOrder: 0, content: thinking },
{ outputIndex: round.compaction?.outputIndex, legacyOrder: 1, content: compaction },
].filter(item => item.content).sort((a, b) => {
if (a.outputIndex !== undefined && b.outputIndex !== undefined) {
return a.outputIndex - b.outputIndex;
}
if (a.outputIndex !== undefined || b.outputIndex !== undefined) {
return a.outputIndex !== undefined ? -1 : 1;
}
return a.legacyOrder - b.legacyOrder;
});
children.push(
<AssistantMessage toolCalls={assistantToolCalls}>
{statefulMarker}
{thinking}
{phase}
{compaction}
{round.response}
{replayItems.map(item => item.content)}
</AssistantMessage>);

// Tool call elements should be rendered with the later elements first, allowed to grow to fill the available space
Expand Down
Loading
Loading