mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-25 17:27:26 +01:00
Fix reasoning bunching on agent host session restore (live path) (#313129)
The live event mapper cleared _currentMarkdownPartId on tool_start but not _currentReasoningPartId. Within a single chat turn, the Copilot SDK emits multiple rounds of (reasoning -> message -> tool calls); every later round's reasoning was appended onto the very first reasoning ResponsePart, which sits before any ToolCall. On restore from in-process state, all reasoning rendered bunched at the top. Symmetric to the history-replay fix in #312559, which only covered the cold-restore path through _buildTurnsFromMessages. Tests added in agentEventMapper.test.ts covering both tool_start and tool_complete as the trigger for a new reasoning round. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -86,9 +86,15 @@ export class AgentEventMapper {
|
||||
}
|
||||
|
||||
case 'tool_start': {
|
||||
// A new tool call invalidates the current markdown part so the
|
||||
// next text delta creates a fresh part after the tool call.
|
||||
// A new tool call invalidates the current markdown and reasoning
|
||||
// parts so the next text/reasoning delta creates fresh parts
|
||||
// after the tool call. The Copilot SDK emits multiple rounds
|
||||
// of (reasoning → message → tool calls) within a single chat
|
||||
// turn; without this, every later round's reasoning would be
|
||||
// appended onto the very first reasoning part and bunch at
|
||||
// the top of the response on restore.
|
||||
this._currentMarkdownPartId.delete(session);
|
||||
this._currentReasoningPartId.delete(session);
|
||||
|
||||
// The Copilot SDK provides full parameters at tool_start time.
|
||||
// We emit both toolCallStart (streaming → created) and toolCallReady
|
||||
|
||||
@@ -238,6 +238,63 @@ suite('AgentEventMapper', () => {
|
||||
assert.strictEqual(reasoning.partId, partId);
|
||||
});
|
||||
|
||||
test('reasoning event after tool_start creates a fresh responsePart', () => {
|
||||
// The Copilot SDK emits multiple rounds of (reasoning → message →
|
||||
// tool calls) within a single chat turn. Each new reasoning batch
|
||||
// after a tool call must produce a fresh Reasoning ResponsePart so
|
||||
// it renders interleaved with the tool calls in the response.
|
||||
// Otherwise the SessionReasoning reducer appends every later round
|
||||
// onto the very first part, causing all reasoning to bunch at the
|
||||
// top of the response when the session is later restored from state.
|
||||
const first: IAgentReasoningEvent = { session, type: 'reasoning', content: 'Round 1 thoughts' };
|
||||
mapper.mapProgressEventToActions(first, session.toString(), turnId);
|
||||
|
||||
const toolStart: IAgentToolStartEvent = {
|
||||
session, type: 'tool_start',
|
||||
toolCallId: 'tc-1', toolName: 'bash', displayName: 'Bash',
|
||||
invocationMessage: 'Running', toolInput: 'ls',
|
||||
};
|
||||
mapper.mapProgressEventToActions(toolStart, session.toString(), turnId);
|
||||
|
||||
const second: IAgentReasoningEvent = { session, type: 'reasoning', content: 'Round 2 thoughts' };
|
||||
const actions = mapToArray(mapper.mapProgressEventToActions(second, session.toString(), turnId));
|
||||
assert.strictEqual(actions.length, 1);
|
||||
assert.strictEqual(actions[0].type, 'session/responsePart');
|
||||
const part = (actions[0] as IResponsePartAction).part;
|
||||
assert.strictEqual(part.kind, 'reasoning');
|
||||
assert.strictEqual(part.content, 'Round 2 thoughts');
|
||||
});
|
||||
|
||||
test('reasoning event after tool_complete creates a fresh responsePart', () => {
|
||||
// Symmetric to the tool_start case: after a tool call finishes,
|
||||
// the next reasoning batch belongs to a new round and must not
|
||||
// be appended onto the previous round's reasoning part.
|
||||
const first: IAgentReasoningEvent = { session, type: 'reasoning', content: 'Round 1 thoughts' };
|
||||
mapper.mapProgressEventToActions(first, session.toString(), turnId);
|
||||
|
||||
const toolStart: IAgentToolStartEvent = {
|
||||
session, type: 'tool_start',
|
||||
toolCallId: 'tc-1', toolName: 'bash', displayName: 'Bash',
|
||||
invocationMessage: 'Running', toolInput: 'ls',
|
||||
};
|
||||
mapper.mapProgressEventToActions(toolStart, session.toString(), turnId);
|
||||
|
||||
const toolComplete: IAgentToolCompleteEvent = {
|
||||
session, type: 'tool_complete',
|
||||
toolCallId: 'tc-1',
|
||||
result: { success: true, content: [{ type: ToolResultContentType.Text, text: 'ok' }], pastTenseMessage: 'Ran' },
|
||||
};
|
||||
mapper.mapProgressEventToActions(toolComplete, session.toString(), turnId);
|
||||
|
||||
const second: IAgentReasoningEvent = { session, type: 'reasoning', content: 'Round 2 thoughts' };
|
||||
const actions = mapToArray(mapper.mapProgressEventToActions(second, session.toString(), turnId));
|
||||
assert.strictEqual(actions.length, 1);
|
||||
assert.strictEqual(actions[0].type, 'session/responsePart');
|
||||
const part = (actions[0] as IResponsePartAction).part;
|
||||
assert.strictEqual(part.kind, 'reasoning');
|
||||
assert.strictEqual(part.content, 'Round 2 thoughts');
|
||||
});
|
||||
|
||||
test('message event with no prior deltas creates responsePart', () => {
|
||||
const event: IAgentMessageEvent = {
|
||||
session,
|
||||
|
||||
Reference in New Issue
Block a user