diff --git a/.changeset/restore-failed-question-replies.md b/.changeset/restore-failed-question-replies.md index cfde9d392f..2763fda704 100644 --- a/.changeset/restore-failed-question-replies.md +++ b/.changeset/restore-failed-question-replies.md @@ -1,5 +1,7 @@ --- "kilo-code": patch +"@kilocode/cli": patch +"@kilocode/kilo-gateway": patch --- -Dismiss answered question prompts immediately and restore them when submission fails. +Prevent streamed tool calls from executing twice and leaving answered questions disabled in VS Code. diff --git a/bun.lock b/bun.lock index 322555cc9b..ab98b23ad5 100644 --- a/bun.lock +++ b/bun.lock @@ -171,7 +171,7 @@ "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", - "@openrouter/ai-sdk-provider": "2.8.1", + "@openrouter/ai-sdk-provider": "2.9.0", "ai": "catalog:", "open": "10.1.2", "zod": "catalog:", @@ -4649,8 +4649,6 @@ "@kilocode/kilo-docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "@kilocode/kilo-gateway/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="], - "@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index ea8736972f..d463473361 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -39,7 +39,7 @@ "@ai-sdk/openai": "3.0.53", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/mistral": "3.0.27", - "@openrouter/ai-sdk-provider": "2.8.1", + "@openrouter/ai-sdk-provider": "2.9.0", "@clack/prompts": "1.0.0-alpha.1", "ai": "catalog:", "open": "10.1.2", diff --git a/packages/kilo-gateway/test/provider.test.ts b/packages/kilo-gateway/test/provider.test.ts index 34f468a80b..1a8e80bd61 100644 --- a/packages/kilo-gateway/test/provider.test.ts +++ b/packages/kilo-gateway/test/provider.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "bun:test" -import { buildRequestHeaders } from "../src/provider" +import { streamText, tool } from "ai" +import { z } from "zod" +import { buildRequestHeaders, createKilo } from "../src/provider" describe("Kilo provider request headers", () => { test("request headers override provider defaults", () => { @@ -21,3 +23,89 @@ describe("Kilo provider request headers", () => { expect(headers.get("x-request-only")).toBe("kept-too") }) }) + +describe("Kilo provider tool streaming", () => { + test("executes a tool once when complete arguments are followed by whitespace", async () => { + const chunks = [ + { + choices: [ + { + index: 0, + delta: { + role: "assistant", + content: null, + tool_calls: [ + { + index: 0, + id: "call_question", + type: "function", + function: { name: "question", arguments: '{"answer":"lasagna"}' }, + }, + ], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: " " } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + choices: [{ index: 0, delta: {}, logprobs: null, finish_reason: "tool_calls" }], + }, + { + choices: [], + usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, + }, + ].map((chunk) => + JSON.stringify({ + id: "chatcmpl-question", + object: "chat.completion.chunk", + created: 1711357598, + model: "qwen/qwen3.6-plus", + ...chunk, + }), + ) + const body = [...chunks.map((chunk) => `data: ${chunk}\n\n`), "data: [DONE]\n\n"].join("") + const fetcher = async () => + new Response(body, { + status: 200, + headers: { "content-type": "text/event-stream" }, + }) + const provider = createKilo({ + apiKey: "test", + baseURL: "https://gateway.test/api/openrouter/", + fetch: fetcher as typeof fetch, + }) + const calls: Array<{ answer: string }> = [] + const result = streamText({ + model: provider.languageModel("qwen/qwen3.6-plus"), + prompt: "Ask a question", + tools: { + question: tool({ + description: "Ask the user a question", + inputSchema: z.object({ answer: z.string() }), + execute: async (input) => { + calls.push(input) + return input.answer + }, + }), + }, + }) + const events = [] + for await (const event of result.fullStream) events.push(event) + + expect(events.filter((event) => event.type === "tool-call")).toHaveLength(1) + expect(calls).toEqual([{ answer: "lasagna" }]) + }) +}) diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/question.ts b/packages/kilo-vscode/src/kilo-provider/handlers/question.ts index 2e72ae1804..69d3796000 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/question.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/question.ts @@ -71,7 +71,6 @@ export async function handleQuestionReply( { requestID, answers, directory: ctx.getWorkspaceDirectory(sid) }, { throwOnError: true }, ) - ctx.postMessage({ type: "questionResolved", requestID }) return true } catch (error) { console.error("[Kilo New] KiloProvider: Failed to reply to question:", error) @@ -95,7 +94,6 @@ export async function handleQuestionReject( try { await ctx.client.question.reject({ requestID, directory: ctx.getWorkspaceDirectory(sid) }, { throwOnError: true }) - ctx.postMessage({ type: "questionResolved", requestID }) return true } catch (error) { console.error("[Kilo New] KiloProvider: Failed to reject question:", error) diff --git a/packages/kilo-vscode/tests/unit/question-dock-contract.test.ts b/packages/kilo-vscode/tests/unit/question-dock-contract.test.ts index 5ecac35fde..fb9b4ca761 100644 --- a/packages/kilo-vscode/tests/unit/question-dock-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/question-dock-contract.test.ts @@ -11,7 +11,6 @@ import path from "node:path" const ROOT = path.resolve(import.meta.dir, "../..") const FILE = path.join(ROOT, "webview-ui/src/components/chat/QuestionDock.tsx") -const ASSISTANT = path.join(ROOT, "webview-ui/src/components/chat/AssistantMessage.tsx") function readFile(filePath: string): string { return fs.readFileSync(filePath, "utf-8") @@ -59,9 +58,4 @@ describe("QuestionDock explicit submit contract", () => { it("keeps the footer Submit button wired to submit()", () => { expect(source).toContain('