Skip to content

Commit 82e3124

Browse files
allquixoticoz-agent
andcommitted
Fix task-local tool state isolation
- Isolate native tool-call parser and provider streaming state per task - Scope todo approvals and partial tool UI state by task/tool call - Bump extension version to 3.53.16 Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 571eaf1 commit 82e3124

26 files changed

Lines changed: 526 additions & 222 deletions

packages/types/src/vscode-extension-host.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ export type AudioType = "notification" | "celebration" | "progress_loop"
424424
export interface UpdateTodoListPayload {
425425
// eslint-disable-next-line @typescript-eslint/no-explicit-any
426426
todos: any[]
427+
taskId?: string
428+
toolCallId?: string
427429
}
428430

429431
export type EditQueuedMessagePayload = Pick<QueuedMessage, "id" | "text" | "images" | "deliveryMode">
@@ -858,6 +860,10 @@ export interface ClineSayTool {
858860
description?: string
859861
// Properties for skill tool
860862
skill?: string
863+
// Properties for updateTodoList tool
864+
todos?: TodoItem[]
865+
taskId?: string
866+
toolCallId?: string
861867
}
862868

863869
export interface ClineAskUseMcpServer {

src/api/providers/__tests__/lmstudio-native-tools.spec.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ vi.mock("openai", () => {
1616
})
1717

1818
import { LmStudioHandler } from "../lm-studio"
19-
import { NativeToolCallParser } from "../../../core/assistant-message/NativeToolCallParser"
2019
import type { ApiHandlerOptions } from "../../../shared/api"
2120

2221
describe("LmStudioHandler Native Tools", () => {
@@ -49,9 +48,6 @@ describe("LmStudioHandler Native Tools", () => {
4948
lmStudioBaseUrl: "http://localhost:1234",
5049
}
5150
handler = new LmStudioHandler(mockOptions)
52-
53-
// Clear NativeToolCallParser state before each test
54-
NativeToolCallParser.clearRawChunkState()
5551
})
5652

5753
describe("Native Tool Calling Support", () => {
@@ -261,16 +257,6 @@ describe("LmStudioHandler Native Tools", () => {
261257

262258
const chunks = []
263259
for await (const chunk of stream) {
264-
// Simulate what Task.ts does: when we receive tool_call_partial,
265-
// process it through NativeToolCallParser to populate rawChunkTracker
266-
if (chunk.type === "tool_call_partial") {
267-
NativeToolCallParser.processRawChunk({
268-
index: chunk.index,
269-
id: chunk.id,
270-
name: chunk.name,
271-
arguments: chunk.arguments,
272-
})
273-
}
274260
chunks.push(chunk)
275261
}
276262

@@ -352,14 +338,6 @@ describe("LmStudioHandler Native Tools", () => {
352338

353339
const chunks = []
354340
for await (const chunk of stream) {
355-
if (chunk.type === "tool_call_partial") {
356-
NativeToolCallParser.processRawChunk({
357-
index: chunk.index,
358-
id: chunk.id,
359-
name: chunk.name,
360-
arguments: chunk.arguments,
361-
})
362-
}
363341
chunks.push(chunk)
364342
}
365343

src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
44

55
import { OpenAiCodexHandler } from "../openai-codex"
66
import type { ApiHandlerOptions } from "../../../shared/api"
7-
import { NativeToolCallParser } from "../../../core/assistant-message/NativeToolCallParser"
87
import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth"
98

109
describe("OpenAiCodexHandler native tool calls", () => {
@@ -13,8 +12,6 @@ describe("OpenAiCodexHandler native tool calls", () => {
1312

1413
beforeEach(() => {
1514
vi.restoreAllMocks()
16-
NativeToolCallParser.clearRawChunkState()
17-
NativeToolCallParser.clearAllStreamingToolCalls()
1815

1916
mockOptions = {
2017
apiModelId: "gpt-5.2-2025-12-11",
@@ -78,15 +75,6 @@ describe("OpenAiCodexHandler native tool calls", () => {
7875
const chunks: any[] = []
7976
for await (const chunk of stream) {
8077
chunks.push(chunk)
81-
if (chunk.type === "tool_call_partial") {
82-
// Simulate Task.ts behavior so finish_reason handling can emit tool_call_end elsewhere
83-
NativeToolCallParser.processRawChunk({
84-
index: chunk.index,
85-
id: chunk.id,
86-
name: chunk.name,
87-
arguments: chunk.arguments,
88-
})
89-
}
9078
}
9179

9280
const toolChunks = chunks.filter((c) => c.type === "tool_call_partial")

src/api/providers/__tests__/openrouter.spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,6 @@ describe("OpenRouterHandler", () => {
299299
})
300300

301301
it("yields tool_call_end events when finish_reason is tool_calls", async () => {
302-
// Import NativeToolCallParser to set up state
303-
const { NativeToolCallParser } = await import("../../../core/assistant-message/NativeToolCallParser")
304-
305-
// Clear any previous state
306-
NativeToolCallParser.clearRawChunkState()
307-
308302
const handler = new OpenRouterHandler(mockOptions)
309303

310304
const mockStream = {
@@ -349,16 +343,6 @@ describe("OpenRouterHandler", () => {
349343
const chunks = []
350344

351345
for await (const chunk of generator) {
352-
// Simulate what Task.ts does: when we receive tool_call_partial,
353-
// process it through NativeToolCallParser to populate rawChunkTracker
354-
if (chunk.type === "tool_call_partial") {
355-
NativeToolCallParser.processRawChunk({
356-
index: chunk.index,
357-
id: chunk.id,
358-
name: chunk.name,
359-
arguments: chunk.arguments,
360-
})
361-
}
362346
chunks.push(chunk)
363347
}
364348

src/api/providers/__tests__/qwen-code-native-tools.spec.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ vi.mock("openai", () => {
2626

2727
import { promises as fs } from "node:fs"
2828
import { QwenCodeHandler } from "../qwen-code"
29-
import { NativeToolCallParser } from "../../../core/assistant-message/NativeToolCallParser"
3029
import type { ApiHandlerOptions } from "../../../shared/api"
3130

3231
describe("QwenCodeHandler Native Tools", () => {
@@ -68,9 +67,6 @@ describe("QwenCodeHandler Native Tools", () => {
6867
apiModelId: "qwen3-coder-plus",
6968
}
7069
handler = new QwenCodeHandler(mockOptions)
71-
72-
// Clear NativeToolCallParser state before each test
73-
NativeToolCallParser.clearRawChunkState()
7470
})
7571

7672
describe("Native Tool Calling Support", () => {
@@ -278,16 +274,6 @@ describe("QwenCodeHandler Native Tools", () => {
278274

279275
const chunks = []
280276
for await (const chunk of stream) {
281-
// Simulate what Task.ts does: when we receive tool_call_partial,
282-
// process it through NativeToolCallParser to populate rawChunkTracker
283-
if (chunk.type === "tool_call_partial") {
284-
NativeToolCallParser.processRawChunk({
285-
index: chunk.index,
286-
id: chunk.id,
287-
name: chunk.name,
288-
arguments: chunk.arguments,
289-
})
290-
}
291277
chunks.push(chunk)
292278
}
293279

@@ -348,14 +334,6 @@ describe("QwenCodeHandler Native Tools", () => {
348334

349335
const chunks = []
350336
for await (const chunk of stream) {
351-
if (chunk.type === "tool_call_partial") {
352-
NativeToolCallParser.processRawChunk({
353-
index: chunk.index,
354-
id: chunk.id,
355-
name: chunk.name,
356-
arguments: chunk.arguments,
357-
})
358-
}
359337
chunks.push(chunk)
360338
}
361339

src/api/providers/__tests__/roo.spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -987,12 +987,6 @@ describe("RooHandler", () => {
987987
})
988988

989989
it("should yield tool_call_end events when finish_reason is tool_calls", async () => {
990-
// Import NativeToolCallParser to set up state
991-
const { NativeToolCallParser } = await import("../../../core/assistant-message/NativeToolCallParser")
992-
993-
// Clear any previous state
994-
NativeToolCallParser.clearRawChunkState()
995-
996990
mockCreate.mockResolvedValueOnce({
997991
[Symbol.asyncIterator]: async function* () {
998992
yield {
@@ -1027,16 +1021,6 @@ describe("RooHandler", () => {
10271021
const stream = handler.createMessage(systemPrompt, messages)
10281022
const chunks: any[] = []
10291023
for await (const chunk of stream) {
1030-
// Simulate what Task.ts does: when we receive tool_call_partial,
1031-
// process it through NativeToolCallParser to populate rawChunkTracker
1032-
if (chunk.type === "tool_call_partial") {
1033-
NativeToolCallParser.processRawChunk({
1034-
index: chunk.index,
1035-
id: chunk.id,
1036-
name: chunk.name,
1037-
arguments: chunk.arguments,
1038-
})
1039-
}
10401024
chunks.push(chunk)
10411025
}
10421026

src/api/providers/lm-studio.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import axios from "axios"
55
import { type ModelInfo, openAiModelInfoSaneDefaults, LMSTUDIO_DEFAULT_TEMPERATURE } from "@roo-code/types"
66

77
import type { ApiHandlerOptions } from "../../shared/api"
8-
9-
import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser"
108
import { TagMatcher } from "../../utils/tag-matcher"
119

1210
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -112,6 +110,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
112110
text: chunk.data,
113111
}) as const,
114112
)
113+
const activeToolCallIds = new Set<string>()
115114

116115
for await (const chunk of results) {
117116
const delta = chunk.choices[0]?.delta
@@ -127,6 +126,9 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
127126
// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
128127
if (delta?.tool_calls) {
129128
for (const toolCall of delta.tool_calls) {
129+
if (toolCall.id) {
130+
activeToolCallIds.add(toolCall.id)
131+
}
130132
yield {
131133
type: "tool_call_partial",
132134
index: toolCall.index,
@@ -138,11 +140,11 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
138140
}
139141

140142
// Process finish_reason to emit tool_call_end events
141-
if (finishReason) {
142-
const endEvents = NativeToolCallParser.processFinishReason(finishReason)
143-
for (const event of endEvents) {
144-
yield event
143+
if (finishReason === "tool_calls" && activeToolCallIds.size > 0) {
144+
for (const id of activeToolCallIds) {
145+
yield { type: "tool_call_end", id }
145146
}
147+
activeToolCallIds.clear()
146148
}
147149
}
148150

src/api/providers/openrouter.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
DEEP_SEEK_DEFAULT_TEMPERATURE,
1111
} from "@roo-code/types"
1212

13-
import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser"
14-
1513
import type { ApiHandlerOptions } from "../../shared/api"
1614

1715
import {
@@ -309,6 +307,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
309307
}
310308

311309
let lastUsage: CompletionUsage | undefined = undefined
310+
const activeToolCallIds = new Set<string>()
312311
// Accumulator for reasoning_details FROM the API.
313312
// We preserve the original shape of reasoning_details to prevent malformed responses.
314313
const reasoningDetailsAccumulator = new Map<
@@ -419,6 +418,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
419418
// Emit raw tool call chunks - NativeToolCallParser handles state management
420419
if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) {
421420
for (const toolCall of delta.tool_calls) {
421+
if (toolCall.id) {
422+
activeToolCallIds.add(toolCall.id)
423+
}
422424
yield {
423425
type: "tool_call_partial",
424426
index: toolCall.index,
@@ -436,11 +438,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
436438

437439
// Process finish_reason to emit tool_call_end events
438440
// This ensures tool calls are finalized even if the stream doesn't properly close
439-
if (finishReason) {
440-
const endEvents = NativeToolCallParser.processFinishReason(finishReason)
441-
for (const event of endEvents) {
442-
yield event
441+
if (finishReason === "tool_calls" && activeToolCallIds.size > 0) {
442+
for (const id of activeToolCallIds) {
443+
yield { type: "tool_call_end", id }
443444
}
445+
activeToolCallIds.clear()
444446
}
445447

446448
if (chunk.usage) {

src/api/providers/qwen-code.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { type ModelInfo, type QwenCodeModelId, qwenCodeModels, qwenCodeDefaultMo
88

99
import type { ApiHandlerOptions } from "../../shared/api"
1010

11-
import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser"
12-
1311
import { convertToOpenAiMessages } from "../transform/openai-format"
1412
import { ApiStream } from "../transform/stream"
1513

@@ -240,6 +238,7 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
240238
const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
241239

242240
let fullContent = ""
241+
const activeToolCallIds = new Set<string>()
243242

244243
for await (const apiChunk of stream) {
245244
const delta = apiChunk.choices[0]?.delta ?? {}
@@ -293,6 +292,9 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
293292
// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
294293
if (delta.tool_calls) {
295294
for (const toolCall of delta.tool_calls) {
295+
if (toolCall.id) {
296+
activeToolCallIds.add(toolCall.id)
297+
}
296298
yield {
297299
type: "tool_call_partial",
298300
index: toolCall.index,
@@ -304,11 +306,11 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
304306
}
305307

306308
// Process finish_reason to emit tool_call_end events
307-
if (finishReason) {
308-
const endEvents = NativeToolCallParser.processFinishReason(finishReason)
309-
for (const event of endEvents) {
310-
yield event
309+
if (finishReason === "tool_calls" && activeToolCallIds.size > 0) {
310+
for (const id of activeToolCallIds) {
311+
yield { type: "tool_call_end", id }
311312
}
313+
activeToolCallIds.clear()
312314
}
313315

314316
if (apiChunk.usage) {

src/api/providers/roo.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import OpenAI from "openai"
44
import { rooDefaultModelId, getApiProtocol, type ImageGenerationApiMethod } from "@roo-code/types"
55
import { CloudService } from "@roo-code/cloud"
66

7-
import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser"
8-
97
import { Package } from "../../shared/package"
108
import type { ApiHandlerOptions } from "../../shared/api"
119
import { ApiStream } from "../transform/stream"
@@ -142,6 +140,7 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
142140
const stream = await this.createStream(systemPrompt, messages, metadata, { headers })
143141

144142
let lastUsage: RooUsage | undefined = undefined
143+
const activeToolCallIds = new Set<string>()
145144
// Accumulator for reasoning_details FROM the API.
146145
// We preserve the original shape of reasoning_details to prevent malformed responses.
147146
const reasoningDetailsAccumulator = new Map<
@@ -257,6 +256,9 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
257256
// Emit raw tool call chunks - NativeToolCallParser handles state management
258257
if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) {
259258
for (const toolCall of delta.tool_calls) {
259+
if (toolCall.id) {
260+
activeToolCallIds.add(toolCall.id)
261+
}
260262
yield {
261263
type: "tool_call_partial",
262264
index: toolCall.index,
@@ -275,11 +277,11 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
275277
}
276278
}
277279

278-
if (finishReason) {
279-
const endEvents = NativeToolCallParser.processFinishReason(finishReason)
280-
for (const event of endEvents) {
281-
yield event
280+
if (finishReason === "tool_calls" && activeToolCallIds.size > 0) {
281+
for (const id of activeToolCallIds) {
282+
yield { type: "tool_call_end", id }
282283
}
284+
activeToolCallIds.clear()
283285
}
284286

285287
if (chunk.usage) {

0 commit comments

Comments
 (0)