Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit f798413

Browse files
authored
Respect "phase" for responses API (#3581)
* Respect "phase" for responses API * Update snapshot
1 parent 0fd5bb3 commit f798413

9 files changed

Lines changed: 106 additions & 3 deletions

File tree

src/extension/intents/node/toolCallingLoop.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
867867
const toolCalls: IToolCall[] = [];
868868
let thinkingItem: ThinkingDataItem | undefined;
869869
const disableThinking = isContinuation && isAnthropicFamily(endpoint) && !ToolCallingLoop.messagesContainThinking(buildPromptResult.messages);
870+
let phase: string | undefined;
870871
const fetchResult = await this.fetch({
871872
messages: this.applyMessagePostProcessing(buildPromptResult.messages),
872873
finishedCb: async (text, index, delta) => {
@@ -892,6 +893,9 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
892893
if (delta.thinking) {
893894
thinkingItem = ThinkingDataItem.createOrUpdate(thinkingItem, delta.thinking);
894895
}
896+
if (delta.phase) {
897+
phase = delta.phase;
898+
}
895899
return stopEarly ? text.length : undefined;
896900
},
897901
requestOptions: {
@@ -974,7 +978,8 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
974978
toolCalls,
975979
toolInputRetry,
976980
statefulMarker,
977-
thinking: thinkingItem
981+
thinking: thinkingItem,
982+
phase
978983
}),
979984
chatResult,
980985
hadIgnoredFiles: buildPromptResult.hasIgnoredFiles,

src/extension/prompt/common/intents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface IToolCallRound {
3939
* the message to show the model about what requirements must be addressed.
4040
*/
4141
hookContext?: string;
42+
/** The phase of the agent loop during which this tool call round occurred. */
43+
phase?: string;
4244
}
4345

4446
export interface InternalToolReference extends vscode.ChatLanguageModelToolReference {

src/extension/prompt/common/toolCallRound.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IToolCall, IToolCallRound } from './intents';
1515
*/
1616
export class ToolCallRound implements IToolCallRound {
1717
public summary: string | undefined;
18+
public phase?: string;
1819

1920
/**
2021
* Creates a ToolCallRound from an existing IToolCallRound object.
@@ -31,6 +32,7 @@ export class ToolCallRound implements IToolCallRound {
3132
params.timestamp,
3233
);
3334
round.summary = params.summary;
35+
round.phase = params.phase;
3436
return round;
3537
}
3638

src/extension/prompt/node/test/__snapshots__/defaultIntentRequestHandler.spec.ts.snap

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
2222
"toolCallRounds": [
2323
ToolCallRound {
2424
"id": "static-id",
25+
"phase": undefined,
2526
"response": "response number 0",
2627
"statefulMarker": undefined,
2728
"summary": undefined,
@@ -38,6 +39,7 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
3839
},
3940
ToolCallRound {
4041
"id": "static-id",
42+
"phase": undefined,
4143
"response": "response number 1",
4244
"statefulMarker": undefined,
4345
"summary": undefined,
@@ -54,6 +56,7 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
5456
},
5557
ToolCallRound {
5658
"id": "static-id",
59+
"phase": undefined,
5760
"response": "response number 2",
5861
"statefulMarker": undefined,
5962
"summary": undefined,
@@ -88,6 +91,7 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
8891
"toolCallRounds": [
8992
ToolCallRound {
9093
"id": "static-id",
94+
"phase": undefined,
9195
"response": "response number 3",
9296
"statefulMarker": undefined,
9397
"summary": undefined,
@@ -104,6 +108,7 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
104108
},
105109
ToolCallRound {
106110
"id": "static-id",
111+
"phase": undefined,
107112
"response": "response number 4",
108113
"statefulMarker": undefined,
109114
"summary": undefined,
@@ -120,6 +125,7 @@ exports[`defaultIntentRequestHandler > ChatResult metadata after multiple turns
120125
},
121126
ToolCallRound {
122127
"id": "static-id",
128+
"phase": undefined,
123129
"response": "response number 5",
124130
"statefulMarker": undefined,
125131
"summary": undefined,
@@ -197,6 +203,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
197203
"toolCallRounds": [
198204
ToolCallRound {
199205
"id": "static-id",
206+
"phase": undefined,
200207
"response": "response number 0",
201208
"statefulMarker": undefined,
202209
"summary": undefined,
@@ -213,6 +220,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
213220
},
214221
ToolCallRound {
215222
"id": "static-id",
223+
"phase": undefined,
216224
"response": "response number 1",
217225
"statefulMarker": undefined,
218226
"summary": undefined,
@@ -229,6 +237,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
229237
},
230238
ToolCallRound {
231239
"id": "static-id",
240+
"phase": undefined,
232241
"response": "response number 2",
233242
"statefulMarker": undefined,
234243
"summary": undefined,
@@ -245,6 +254,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
245254
},
246255
ToolCallRound {
247256
"id": "static-id",
257+
"phase": undefined,
248258
"response": "response number 3",
249259
"statefulMarker": undefined,
250260
"summary": undefined,
@@ -302,6 +312,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
302312
"toolCallRounds": [
303313
ToolCallRound {
304314
"id": "static-id",
315+
"phase": undefined,
305316
"response": "response number 4",
306317
"statefulMarker": undefined,
307318
"summary": undefined,
@@ -318,6 +329,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
318329
},
319330
ToolCallRound {
320331
"id": "static-id",
332+
"phase": undefined,
321333
"response": "response number 5",
322334
"statefulMarker": undefined,
323335
"summary": undefined,
@@ -334,6 +346,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
334346
},
335347
ToolCallRound {
336348
"id": "static-id",
349+
"phase": undefined,
337350
"response": "response number 6",
338351
"statefulMarker": undefined,
339352
"summary": undefined,
@@ -350,6 +363,7 @@ exports[`defaultIntentRequestHandler > confirms on max tool call iterations, and
350363
},
351364
ToolCallRound {
352365
"id": "static-id",
366+
"phase": undefined,
353367
"response": "response number 7",
354368
"statefulMarker": undefined,
355369
"summary": undefined,
@@ -2464,6 +2478,7 @@ exports[`defaultIntentRequestHandler > makes a successful request with a single
24642478
"toolCallRounds": [
24652479
ToolCallRound {
24662480
"id": "static-id",
2481+
"phase": undefined,
24672482
"response": "some response here :)",
24682483
"statefulMarker": undefined,
24692484
"summary": undefined,
@@ -2789,6 +2804,7 @@ exports[`defaultIntentRequestHandler > makes a tool call turn 1`] = `
27892804
"toolCallRounds": [
27902805
ToolCallRound {
27912806
"id": "static-id",
2807+
"phase": undefined,
27922808
"response": "some response here :)",
27932809
"statefulMarker": undefined,
27942810
"summary": undefined,
@@ -2805,6 +2821,7 @@ exports[`defaultIntentRequestHandler > makes a tool call turn 1`] = `
28052821
},
28062822
ToolCallRound {
28072823
"id": "static-id",
2824+
"phase": undefined,
28082825
"response": "response to tool call",
28092826
"statefulMarker": undefined,
28102827
"summary": undefined,

src/extension/prompts/node/panel/toolCalling.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ConfigKey, IConfigurationService } from '../../../../platform/configura
1212
import { modelCanUseMcpResultImageURL } from '../../../../platform/endpoint/common/chatModelCapabilities';
1313
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
1414
import { CacheType } from '../../../../platform/endpoint/common/endpointTypes';
15+
import { PhaseDataContainer } from '../../../../platform/endpoint/common/phaseDataContainer';
1516
import { StatefulMarkerContainer } from '../../../../platform/endpoint/common/statefulMarkerContainer';
1617
import { ThinkingDataContainer } from '../../../../platform/endpoint/common/thinkingDataContainer';
1718
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
@@ -114,10 +115,12 @@ export class ChatToolCalls extends PromptElement<ChatToolCallsProps, void> {
114115
// Don't include this when rendering and triggering summarization
115116
const statefulMarker = round.statefulMarker && <StatefulMarkerContainer statefulMarker={{ modelId: this.promptEndpoint.model, marker: round.statefulMarker }} />;
116117
const thinking = (!this.props.isHistorical) && round.thinking && <ThinkingDataContainer thinking={round.thinking} />;
118+
const phase = round.phase && <PhaseDataContainer phase={round.phase} />;
117119
children.push(
118120
<AssistantMessage toolCalls={assistantToolCalls}>
119121
{statefulMarker}
120122
{thinking}
123+
{phase}
121124
{round.response}
122125
</AssistantMessage>);
123126

src/platform/endpoint/common/endpointTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export namespace CustomDataPartMimeTypes {
88
export const StatefulMarker = 'stateful_marker';
99
export const ThinkingData = 'thinking';
1010
export const ContextManagement = 'context_management';
11+
export const PhaseData = 'phase_data';
1112
}
1213

1314
export const CacheType = 'ephemeral';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
import { BasePromptElementProps, PromptElement, Raw } from '@vscode/prompt-tsx';
6+
import { CustomDataPartMimeTypes } from './endpointTypes';
7+
8+
interface IPhaseDataOpaque {
9+
type: typeof CustomDataPartMimeTypes.PhaseData;
10+
phase: string;
11+
}
12+
13+
export interface IPhaseDataContainerProps extends BasePromptElementProps {
14+
phase: string;
15+
}
16+
17+
/**
18+
* Helper element to embed phase data into assistant messages
19+
* as an opaque content part.
20+
*/
21+
export class PhaseDataContainer extends PromptElement<IPhaseDataContainerProps> {
22+
render() {
23+
const { phase } = this.props;
24+
const container: IPhaseDataOpaque = { type: CustomDataPartMimeTypes.PhaseData, phase };
25+
return <opaque value={container} />;
26+
}
27+
}
28+
29+
/**
30+
* Attempts to parse a Raw opaque content part into a phase string, if the type matches.
31+
*/
32+
export function rawPartAsPhaseData(part: Raw.ChatCompletionContentPartOpaque): string | undefined {
33+
const value = part.value as unknown;
34+
if (!value || typeof value !== 'object') {
35+
return;
36+
}
37+
38+
const data = value as IPhaseDataOpaque;
39+
if (data.type === CustomDataPartMimeTypes.PhaseData && typeof data.phase === 'string') {
40+
return data.phase;
41+
}
42+
return;
43+
}

src/platform/endpoint/node/responsesApi.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { IExperimentationService } from '../../telemetry/common/nullExperimentat
2323
import { ITelemetryService } from '../../telemetry/common/telemetry';
2424
import { TelemetryData } from '../../telemetry/common/telemetryData';
2525
import { getVerbosityForModelSync } from '../common/chatModelCapabilities';
26+
import { rawPartAsPhaseData } from '../common/phaseDataContainer';
2627
import { getStatefulMarkerAndIndex } from '../common/statefulMarkerContainer';
2728
import { rawPartAsThinkingData } from '../common/thinkingDataContainer';
2829

@@ -71,6 +72,14 @@ export function createResponsesRequestBody(accessor: ServicesAccessor, options:
7172
return body;
7273
}
7374

75+
type ResponseOutputMessageWithPhase = OpenAI.Responses.ResponseOutputMessage & {
76+
phase?: string;
77+
};
78+
79+
interface ResponseOutputItemWithPhase {
80+
phase?: string;
81+
}
82+
7483
function rawMessagesToResponseAPI(modelId: string, messages: readonly Raw.ChatMessage[], ignoreStatefulMarker: boolean): { input: OpenAI.Responses.ResponseInputItem[]; previous_response_id?: string } {
7584
const statefulMarkerAndIndex = !ignoreStatefulMarker && getStatefulMarkerAndIndex(modelId, messages);
7685
let previousResponseId: string | undefined;
@@ -87,14 +96,16 @@ function rawMessagesToResponseAPI(modelId: string, messages: readonly Raw.ChatMe
8796
input.push(...extractThinkingData(message.content));
8897
const asstContent = message.content.map(rawContentToResponsesOutputContent).filter(isDefined);
8998
if (asstContent.length) {
90-
input.push({
99+
const assistantMessage: ResponseOutputMessageWithPhase = {
91100
role: 'assistant',
92101
content: asstContent,
93102
// I don't think this needs to be round-tripped.
94103
id: 'msg_123',
95104
status: 'completed',
96105
type: 'message',
97-
} satisfies OpenAI.Responses.ResponseOutputMessage);
106+
phase: extractPhaseData(message.content),
107+
};
108+
input.push(assistantMessage);
98109
}
99110
}
100111
if (message.toolCalls) {
@@ -176,6 +187,18 @@ function extractThinkingData(content: Raw.ChatCompletionContentPart[]): OpenAI.R
176187
}));
177188
}
178189

190+
function extractPhaseData(content: Raw.ChatCompletionContentPart[]): string | undefined {
191+
for (const part of content) {
192+
if (part.type === Raw.ChatCompletionContentPartKind.Opaque) {
193+
const phase = rawPartAsPhaseData(part);
194+
if (phase) {
195+
return phase;
196+
}
197+
}
198+
}
199+
return undefined;
200+
}
201+
179202
/**
180203
* This is an approximate responses input -> raw messages helper, should be used for logging only
181204
*/
@@ -441,6 +464,7 @@ export class OpenAIResponsesProcessor {
441464
name: chunk.item.name,
442465
arguments: chunk.item.arguments,
443466
}],
467+
phase: (chunk.item as ResponseOutputItemWithPhase).phase
444468
});
445469
} else if (chunk.item.type === 'reasoning') {
446470
onProgress({
@@ -454,6 +478,11 @@ export class OpenAIResponsesProcessor {
454478
encrypted: chunk.item.encrypted_content,
455479
} : undefined
456480
});
481+
} else if (chunk.item.type === 'message') {
482+
onProgress({
483+
text: '',
484+
phase: (chunk.item as ResponseOutputItemWithPhase).phase
485+
});
457486
}
458487
return;
459488
case 'response.reasoning_summary_text.delta':

src/platform/networking/common/fetch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export interface IResponseDelta {
152152
_deprecatedCopilotFunctionCalls?: ICopilotFunctionCall[];
153153
copilotConfirmation?: ICopilotConfirmation;
154154
thinking?: ThinkingDelta | EncryptedThinkingDelta;
155+
phase?: string;
155156
retryReason?: FilterReason | 'network_error' | 'server_error';
156157
/** Marker for the current response, which should be presented in `IMakeChatRequestOptions` on the next call */
157158
statefulMarker?: string;

0 commit comments

Comments
 (0)