Skip to content

Commit e283bc0

Browse files
authored
fix(provider): stabilize reasoning replay across tool turns (#39)
- Replay DeepSeek reasoning_content with stable keys for assistant tool-call and post-tool final turns. - Restore cancellation abort behavior without treating cancellation as normal completion, and expand cache-trace diagnostics for rollback, retry, thinking parts, and post-tool reasoning states.
1 parent 05fac93 commit e283bc0

6 files changed

Lines changed: 259 additions & 61 deletions

File tree

src/client.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ export class DeepSeekClient {
2727
cancellationToken?: CancellationToken,
2828
): Promise<void> {
2929
const controller = new AbortController();
30-
3130
const cancelListener = cancellationToken?.onCancellationRequested(() => {
3231
controller.abort();
3332
});
33+
if (cancellationToken?.isCancellationRequested) {
34+
controller.abort();
35+
}
3436

3537
try {
3638
// Request usage stats in streaming responses so we can calibrate token counting.
@@ -75,7 +77,7 @@ export class DeepSeekClient {
7577
while (true) {
7678
if (cancellationToken?.isCancellationRequested) {
7779
controller.abort();
78-
break;
80+
return;
7981
}
8082

8183
const { done, value } = await reader.read();
@@ -172,8 +174,7 @@ export class DeepSeekClient {
172174

173175
callbacks.onDone();
174176
} catch (error) {
175-
if (error instanceof Error && error.name === 'AbortError') {
176-
callbacks.onDone();
177+
if (isAbortError(error) && cancellationToken?.isCancellationRequested) {
177178
return;
178179
}
179180
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
@@ -182,3 +183,7 @@ export class DeepSeekClient {
182183
}
183184
}
184185
}
186+
187+
function isAbortError(error: unknown): boolean {
188+
return error instanceof Error && error.name === 'AbortError';
189+
}

src/provider/cache.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,27 @@ import { MAX_CACHE_SIZE } from '../consts';
55
* can inject reasoning_content back into prior assistant messages.
66
*
77
* Key strategy (per DeepSeek docs):
8-
* - Non-tool-call turns: reasoning_content does NOT need to be passed back.
9-
* - Tool-call turns: reasoning_content MUST be in ALL subsequent requests.
8+
* - Plain non-tool turns: reasoning_content does NOT need to be passed back.
9+
* - Tool-call turns and their post-tool final turns: reasoning_content MUST be
10+
* in ALL subsequent requests.
1011
*
11-
* We cache by tool_call IDs so we can look up which reasoning goes with which
12-
* tool-call-bearing assistant message when reconstructing the message history.
12+
* We cache by stable history keys so we can look up which reasoning goes with
13+
* tool-call-bearing assistant messages and final post-tool assistant messages
14+
* when reconstructing the message history.
1315
*/
1416
export interface ReasoningEntry {
1517
text: string;
1618
timestamp: number;
1719
}
1820

21+
export function createToolReasoningKey(toolCallId: string): string {
22+
return `tool:${toolCallId}`;
23+
}
24+
25+
export function createPostToolReasoningKey(toolCallIds: readonly string[]): string {
26+
return `post-tool:${JSON.stringify(toolCallIds)}`;
27+
}
28+
1929
export function pruneReasoningCache(cache: Map<string, ReasoningEntry>, clearAll: boolean): void {
2030
if (clearAll) {
2131
cache.clear();

src/provider/convert.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import vscode from 'vscode';
22
import type { DeepSeekMessage, DeepSeekTool, DeepSeekToolCall } from '../types';
3-
import type { ReasoningEntry } from './cache';
3+
import { createPostToolReasoningKey, createToolReasoningKey, type ReasoningEntry } from './cache';
44

55
/**
66
* Convert VS Code chat messages to DeepSeek format.
7-
* Injects cached reasoning_content for assistant messages that had tool calls
8-
* in prior turns.
7+
* Injects cached reasoning_content for assistant tool-call messages and final
8+
* assistant messages after tool results.
99
*/
1010
export function convertMessages(
1111
messages: readonly vscode.LanguageModelChatRequestMessage[],
1212
isThinkingModel: boolean,
1313
reasoningCache: Map<string, ReasoningEntry>,
1414
): DeepSeekMessage[] {
1515
const result: DeepSeekMessage[] = [];
16+
let recentToolResultIds: string[] = [];
1617

1718
for (const message of messages) {
1819
const role = mapRole(message.role);
@@ -53,12 +54,19 @@ export function convertMessages(
5354
let reasoningContent: string | undefined;
5455
if (isThinkingModel && toolCalls.length > 0) {
5556
for (const tc of toolCalls) {
56-
const cached = reasoningCache.get(tc.id);
57+
// Prefer new `tool:<callId>` key; fallback to bare `callId` for entries written
58+
// before the stable-key change (read-only compat, no new bare-key writes).
59+
const cached =
60+
reasoningCache.get(createToolReasoningKey(tc.id)) ?? reasoningCache.get(tc.id);
5761
if (cached) {
5862
reasoningContent = cached.text;
5963
break;
6064
}
6165
}
66+
} else if (isThinkingModel && recentToolResultIds.length > 0) {
67+
reasoningContent = reasoningCache.get(
68+
createPostToolReasoningKey(recentToolResultIds),
69+
)?.text;
6270
}
6371

6472
if (content || toolCalls.length > 0) {
@@ -76,12 +84,18 @@ export function convertMessages(
7684
}
7785

7886
result.push(msg);
87+
recentToolResultIds = [];
88+
}
89+
} else {
90+
if (content) {
91+
recentToolResultIds = [];
92+
result.push({
93+
role: role as 'user' | 'assistant',
94+
content: content,
95+
});
96+
} else if (toolResults.length === 0) {
97+
recentToolResultIds = [];
7998
}
80-
} else if (content) {
81-
result.push({
82-
role: role as 'user' | 'assistant',
83-
content: content,
84-
});
8599
}
86100

87101
// Tool result messages follow their associated assistant message
@@ -91,6 +105,7 @@ export function convertMessages(
91105
content: tr.content,
92106
tool_call_id: tr.callId,
93107
});
108+
recentToolResultIds.push(tr.callId);
94109
}
95110
}
96111

0 commit comments

Comments
 (0)