From 1a6107d1ac13e2d9f5b1bf15d0895b2a192a7e29 Mon Sep 17 00:00:00 2001 From: Klabundo Date: Thu, 3 Sep 2026 17:20:29 +0200 Subject: [PATCH 1/4] feat(chat): enable agent to ask interactive multiple-choice clarification questions --- src/agent/AgentManager.ts | 12 +- src/agent/tools/askQuestion.ts | 85 ++++ src/agent/tools/builtInToolDefaults.ts | 11 + src/components/chat/AskQuestionCard.svelte | 465 +++++++++++++++++++ src/components/chat/ToolCallsSection.svelte | 39 +- src/components/chat/toolOutputRenderModel.ts | 44 ++ src/components/chat/toolSummaryModel.ts | 22 +- src/components/modal/AgentEditorModal.svelte | 36 ++ src/components/modal/ToolConfigForm.svelte | 2 +- src/components/modal/ToolsModal.svelte | 7 +- src/stores/pendingQuestionStore.svelte.ts | 169 +++++++ src/types/plugin.ts | 1 + test/agent/tools/askQuestion.test.ts | 65 +++ test/stores/pendingQuestionStore.test.ts | 113 +++++ 14 files changed, 1061 insertions(+), 10 deletions(-) create mode 100644 src/agent/tools/askQuestion.ts create mode 100644 src/components/chat/AskQuestionCard.svelte create mode 100644 src/stores/pendingQuestionStore.svelte.ts create mode 100644 test/agent/tools/askQuestion.test.ts create mode 100644 test/stores/pendingQuestionStore.test.ts diff --git a/src/agent/AgentManager.ts b/src/agent/AgentManager.ts index 854bb2c0..bdc8d694 100644 --- a/src/agent/AgentManager.ts +++ b/src/agent/AgentManager.ts @@ -64,6 +64,7 @@ import { import { getBundledSkill } from "../skills/defaults"; import { extractErrorMessage } from "../utils/errorMessage"; import { LangSmithTelemetry, type Telemetry } from "./telemetry"; +import { createAskQuestionTool } from "./tools/askQuestion"; import { createExecuteJavaScriptTool } from "./tools/executeJavaScript"; import { createPluginApiExecTool } from "./tools/executePluginApi"; import { createFetchUrlTool } from "./tools/fetchUrl"; @@ -809,7 +810,10 @@ export class AgentManager { * reading `toolsConfig[id].enabled` alone, which is only half the condition. */ isToolBound(agentCfg: AgentConfig, toolId: BuiltInToolId): boolean { - return this.attachedToolIds(agentCfg).has(toolId) && (agentCfg.toolsConfig[toolId]?.enabled ?? true); + return ( + (this.attachedToolIds(agentCfg).has(toolId) || toolId === "ask_question") && + (agentCfg.toolsConfig[toolId]?.enabled ?? true) + ); } /** @@ -826,7 +830,10 @@ export class AgentManager { // walked a single time while building the whole tool list. const attached = this.attachedToolIds(agentCfg); const isToolEnabled = (toolId: BuiltInToolId): boolean => { - return attached.has(toolId) && (agentCfg.toolsConfig[toolId]?.enabled ?? true); + return ( + (attached.has(toolId) || toolId === "ask_question") && + (agentCfg.toolsConfig[toolId]?.enabled ?? true) + ); }; // Instantiate vision processor models for read_content. @@ -894,6 +901,7 @@ export class AgentManager { ["fetch_url", () => createFetchUrlTool()], ["web_search", () => createWebSearchTool(agentCfg.id)], ["manage_skills", () => createManageSkillsTool(this.plugin.skillsService, this.plugin.app, agentCfg.id)], + ["ask_question", () => createAskQuestionTool(agentCfg.id)], ]; for (const [toolId, factory] of builtInTools) { diff --git a/src/agent/tools/askQuestion.ts b/src/agent/tools/askQuestion.ts new file mode 100644 index 00000000..8c782c38 --- /dev/null +++ b/src/agent/tools/askQuestion.ts @@ -0,0 +1,85 @@ +import { tool } from "@langchain/core/tools"; +import type { RunnableConfig } from "@langchain/core/runnables"; +import { z } from "zod"; +import { DEFAULT_TOOLS_CONFIG } from "./builtInToolDefaults"; +import { resolveToolAgent } from "./toolAgentContext"; +import { getPendingQuestionStore, type QuestionItem } from "../../stores/pendingQuestionStore.svelte"; +import { genUUIDv7 } from "../../utils/uuid7Validator"; +import { Logger } from "../../utils/logging"; + +const questionItemSchema = z.object({ + id: z + .string() + .optional() + .describe("Optional unique identifier for this question (e.g. 'choice_format' or 'framework')"), + question: z.string().describe("The question text to present to the user"), + options: z.array(z.string()).min(2).describe("Selectable options for the user to choose from"), + is_multi_select: z + .boolean() + .optional() + .describe("Allow user to select multiple options. Default is false (single choice)."), + allow_custom: z.boolean().optional().describe("Allow user to type in a custom write-in response. Default is true."), +}); + +export const askQuestionSchema = z.object({ + questions: z.array(questionItemSchema).min(1).describe("List of multiple-choice questions to ask the user"), +}); + +export type AskQuestionInput = z.infer; + +function getAskQuestionToolConfig(agentId: string) { + const defaultConfig = DEFAULT_TOOLS_CONFIG.ask_question; + if (!agentId) return defaultConfig; + const agent = resolveToolAgent(agentId); + return agent?.toolsConfig?.ask_question ?? defaultConfig; +} + +function resolveThreadIdFromConfig(config: RunnableConfig | undefined): string { + const threadId = config?.configurable?.thread_id; + if (typeof threadId === "string" && threadId.length > 0) { + return threadId; + } + return "default_thread"; +} + +export function createAskQuestionTool(agentId = "") { + const toolConfig = getAskQuestionToolConfig(agentId); + + return tool( + async ({ questions }: AskQuestionInput, config: RunnableConfig & { runId?: string }) => { + const threadId = resolveThreadIdFromConfig(config); + const toolCallId = (config as { toolCallId?: string })?.toolCallId ?? config?.runId ?? genUUIDv7(); + + Logger.log("[ask_question] Prompting user with questions:", { + threadId, + toolCallId, + count: questions.length, + }); + + const formattedQuestions: QuestionItem[] = questions.map((q, index) => ({ + id: q.id?.trim() || `q_${index + 1}`, + question: q.question, + options: q.options, + isMultiSelect: q.is_multi_select ?? false, + allowCustom: q.allow_custom ?? true, + })); + + const store = getPendingQuestionStore(); + const answers = await store.ask(threadId, toolCallId, formattedQuestions, config?.signal); + + return JSON.stringify( + { + status: "answered", + answers, + }, + null, + 2, + ); + }, + { + name: toolConfig?.name ?? DEFAULT_TOOLS_CONFIG.ask_question.name, + description: toolConfig?.description ?? DEFAULT_TOOLS_CONFIG.ask_question.description, + schema: askQuestionSchema, + }, + ); +} diff --git a/src/agent/tools/builtInToolDefaults.ts b/src/agent/tools/builtInToolDefaults.ts index a9cde320..b03b9e34 100644 --- a/src/agent/tools/builtInToolDefaults.ts +++ b/src/agent/tools/builtInToolDefaults.ts @@ -193,6 +193,17 @@ export const BUILT_IN_TOOL_DEFAULTS: Record = "Create new skills, revise your own attached skills, or delete skills you created. Changes apply immediately. A skill's name and plugin link are locked once created; only the body and description can change.", }, }, + ask_question: { + displayName: "Ask Question", + summary: + "Ask the user one or more multiple-choice questions to clarify requirements, solicit preferences, or choose between options. Execution pauses until the user responds in chat.", + config: { + enabled: true, + name: "ask_question", + description: + "Ask the user one or more multiple-choice questions to clarify requirements, solicit preferences, or choose between options. Execution pauses until the user responds in chat.", + }, + }, }; /** diff --git a/src/components/chat/AskQuestionCard.svelte b/src/components/chat/AskQuestionCard.svelte new file mode 100644 index 00000000..dba886a4 --- /dev/null +++ b/src/components/chat/AskQuestionCard.svelte @@ -0,0 +1,465 @@ + + +
+ +
+
+ + Question for you +
+ + {questions.length > 1 ? `${questions.length} questions` : "Response needed"} + +
+ + {#if questions.length === 0} +
Waiting for questions...
+ {:else} +
+ {#each questions as q, qIndex (q.id)} + {@const qId = q.id} + {@const currentSelections = selections[qId] ?? []} + {@const isOtherOpen = Boolean(otherActive[qId])} + +
+ +
+
+ {#if questions.length > 1} + {qIndex + 1}. + {/if} + {q.question} +
+
+ {q.isMultiSelect ? "Select one or more options" : "Select one option"} +
+
+ + +
+ {#each q.options as opt} + {@const isSelected = currentSelections.includes(opt)} + + {/each} + + + {#if q.allowCustom} +
+ + + {#if isOtherOpen} +
+ handleCustomInput(qId, (e.target as HTMLInputElement).value)} + disabled={isSubmitted} + class="other-text-input" + /> +
+ {/if} +
+ {/if} +
+
+ {/each} +
+ + +
+ + {#if canSubmit} + Ready to submit + {:else} + Select an option to continue + {/if} + + + +
+ {/if} +
+ + diff --git a/src/components/chat/ToolCallsSection.svelte b/src/components/chat/ToolCallsSection.svelte index 4ef3e6dd..58303672 100644 --- a/src/components/chat/ToolCallsSection.svelte +++ b/src/components/chat/ToolCallsSection.svelte @@ -14,6 +14,7 @@ import { import { buildToolSummary, buildMergedToolSummary, type MergedCall, type ToolSummary } from "./toolSummaryModel"; import MarkdownRenderer from "../ui/MarkdownRenderer.svelte"; import Icon from "../ui/Icon.svelte"; +import AskQuestionCard from "./AskQuestionCard.svelte"; interface Props { assistantTimeline?: AssistantTimelineEvent[]; @@ -465,6 +466,7 @@ const PROSE_MARKDOWN_CLASS = : undefined} {@const toolSummary = buildToolSummary(tool.name, tool.input, outputModel, tool.status)} {@const isTask = tool.name === "task"} + {@const isAskQuestionRunning = tool.name === "ask_question" && tool.status === "running"} {:else if capturedToolId === "web_search"} diff --git a/src/components/modal/ToolsModal.svelte b/src/components/modal/ToolsModal.svelte index 77c8b4ef..e1386166 100644 --- a/src/components/modal/ToolsModal.svelte +++ b/src/components/modal/ToolsModal.svelte @@ -112,7 +112,8 @@ function openToolConfig(toolId: BuiltInToolId) { {#each BUILT_IN_TOOL_IDS as toolId (toolId)} {@const config = selectedAgent.toolsConfig[toolId]} {@const skills = attachingSkills.get(toolId) ?? []} - {@const attached = skills.length > 0} + {@const isInteractive = toolId === "ask_question"} + {@const attached = skills.length > 0 || isInteractive} {@const enabled = isToolEnabled(toolId)} {@const displayName = getToolDisplayName(toolId, config?.name)} {#snippet badges()} - {#if attached} + {#if attached && !isInteractive} {#each skills as skillName (skillName)} {/each} - {:else} + {:else if !attached} {/if} {/snippet} diff --git a/src/stores/pendingQuestionStore.svelte.ts b/src/stores/pendingQuestionStore.svelte.ts new file mode 100644 index 00000000..6c4bfbd1 --- /dev/null +++ b/src/stores/pendingQuestionStore.svelte.ts @@ -0,0 +1,169 @@ +import { SvelteMap } from "svelte/reactivity"; +import { genUUIDv7 } from "../utils/uuid7Validator"; +import { Logger } from "../utils/logging"; + +export interface QuestionItem { + id: string; + question: string; + options: string[]; + isMultiSelect?: boolean; + allowCustom?: boolean; +} + +export interface QuestionAnswerPayload { + questionId: string; + question: string; + selected: string[]; + customText?: string; +} + +export interface PendingQuestionEntry { + id: string; + threadId: string; + toolCallId: string; + questions: QuestionItem[]; + resolve: (answers: QuestionAnswerPayload[]) => void; + reject: (error: Error) => void; + createdAt: number; +} + +export class PendingQuestionStore { + private pendingByToolCallId = new SvelteMap(); + + /** + * Registers a pending question and returns a Promise that resolves when the user submits answers. + */ + ask( + threadId: string, + toolCallId: string, + questions: QuestionItem[], + signal?: AbortSignal, + ): Promise { + if (signal?.aborted) { + return Promise.reject(new Error("Tool execution was aborted")); + } + + return new Promise((resolve, reject) => { + const id = genUUIDv7(); + const entry: PendingQuestionEntry = { + id, + threadId, + toolCallId, + questions, + resolve: (answers) => { + this.pendingByToolCallId.delete(toolCallId); + resolve(answers); + }, + reject: (err) => { + this.pendingByToolCallId.delete(toolCallId); + reject(err); + }, + createdAt: Date.now(), + }; + + if (signal) { + const onAbort = () => { + signal.removeEventListener("abort", onAbort); + if (this.pendingByToolCallId.has(toolCallId)) { + entry.reject(new Error("Tool execution was aborted")); + } + }; + signal.addEventListener("abort", onAbort, { once: true }); + } + + this.pendingByToolCallId.set(toolCallId, entry); + Logger.debug("[PendingQuestionStore] Question registered", { + threadId, + toolCallId, + questionCount: questions.length, + }); + }); + } + + private findEntry(key: string): PendingQuestionEntry | undefined { + if (!key) { + if (this.pendingByToolCallId.size === 1) { + return this.pendingByToolCallId.values().next().value; + } + return undefined; + } + const direct = this.pendingByToolCallId.get(key); + if (direct) return direct; + + for (const entry of this.pendingByToolCallId.values()) { + if (entry.id === key || entry.toolCallId === key || entry.threadId === key) { + return entry; + } + } + + if (this.pendingByToolCallId.size === 1) { + return this.pendingByToolCallId.values().next().value; + } + return undefined; + } + + /** + * Submits answers for a pending question. + */ + submitAnswers(toolCallId: string, answers: QuestionAnswerPayload[]): boolean { + const entry = this.findEntry(toolCallId); + if (!entry) { + Logger.warn("[PendingQuestionStore] No pending question found for toolCallId", toolCallId); + return false; + } + entry.resolve(answers); + return true; + } + + /** + * Cancels a pending question. + */ + cancel(toolCallId: string, error?: Error): boolean { + const entry = this.findEntry(toolCallId); + if (!entry) return false; + entry.reject(error ?? new Error("Question was cancelled")); + return true; + } + + /** + * Returns the pending question for a specific tool call. + */ + getPending(toolCallId: string): PendingQuestionEntry | undefined { + return this.findEntry(toolCallId); + } + + /** + * Returns any pending question for a given thread. + */ + getPendingForThread(threadId: string): PendingQuestionEntry | undefined { + for (const entry of this.pendingByToolCallId.values()) { + if (entry.threadId === threadId) return entry; + } + return undefined; + } + + /** + * Clears and rejects any pending questions for a thread. + */ + clearForThread(threadId: string): void { + for (const [toolCallId, entry] of Array.from(this.pendingByToolCallId.entries())) { + if (entry.threadId === threadId) { + entry.reject(new Error("Thread session cleared or reset")); + this.pendingByToolCallId.delete(toolCallId); + } + } + } +} + +let _pendingQuestionStore: PendingQuestionStore | null = null; + +export function getPendingQuestionStore(): PendingQuestionStore { + if (!_pendingQuestionStore) { + _pendingQuestionStore = new PendingQuestionStore(); + } + return _pendingQuestionStore; +} + +export function initPendingQuestionStore(store: PendingQuestionStore): void { + _pendingQuestionStore = store; +} diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 011dc411..85ac62aa 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -152,6 +152,7 @@ export const BUILT_IN_TOOL_IDS = [ "fetch_url", "web_search", "manage_skills", + "ask_question", ] as const; export type BuiltInToolId = (typeof BUILT_IN_TOOL_IDS)[number]; diff --git a/test/agent/tools/askQuestion.test.ts b/test/agent/tools/askQuestion.test.ts new file mode 100644 index 00000000..316d4597 --- /dev/null +++ b/test/agent/tools/askQuestion.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { createAskQuestionTool } from "../../../src/agent/tools/askQuestion"; +import { + getPendingQuestionStore, + initPendingQuestionStore, + PendingQuestionStore, +} from "../../../src/stores/pendingQuestionStore.svelte"; + +describe("askQuestion tool", () => { + let store: PendingQuestionStore; + + beforeEach(() => { + store = new PendingQuestionStore(); + initPendingQuestionStore(store); + }); + + it("creates a tool with correct name and schema", () => { + const tool = createAskQuestionTool(); + expect(tool.name).toBe("ask_question"); + expect(tool.description).toContain("multiple-choice"); + }); + + it("executes the tool and waits for user answer", async () => { + const tool = createAskQuestionTool(); + const toolCallId = "call-123"; + + const invokePromise = tool.invoke( + { + questions: [ + { + id: "q_lang", + question: "Which language do you prefer?", + options: ["Python", "Rust", "TypeScript"], + }, + ], + }, + { + toolCallId, + configurable: { thread_id: "thread-abc" }, + } as any, + ); + + // Wait a tick for async validation and execution to reach the tool function + await new Promise((resolve) => setTimeout(resolve, 20)); + + // Verify question is registered in store + const pending = store.getPending(toolCallId); + expect(pending).toBeDefined(); + expect(pending?.questions[0].question).toBe("Which language do you prefer?"); + + // Submit user choice + store.submitAnswers(toolCallId, [ + { + questionId: "q_lang", + question: "Which language do you prefer?", + selected: ["TypeScript"], + }, + ]); + + const resultStr = (await invokePromise) as string; + const result = JSON.parse(resultStr); + expect(result.status).toBe("answered"); + expect(result.answers[0].selected).toEqual(["TypeScript"]); + }); +}); diff --git a/test/stores/pendingQuestionStore.test.ts b/test/stores/pendingQuestionStore.test.ts new file mode 100644 index 00000000..5fed3964 --- /dev/null +++ b/test/stores/pendingQuestionStore.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; +import { PendingQuestionStore } from "../../src/stores/pendingQuestionStore.svelte"; + +describe("PendingQuestionStore", () => { + it("registers a question and resolves when answers are submitted", async () => { + const store = new PendingQuestionStore(); + const threadId = "thread-1"; + const toolCallId = "tool-1"; + const questions = [ + { + id: "q1", + question: "Do you prefer TypeScript or JavaScript?", + options: ["TypeScript", "JavaScript"], + isMultiSelect: false, + }, + ]; + + const askPromise = store.ask(threadId, toolCallId, questions); + expect(store.getPending(toolCallId)).toBeDefined(); + expect(store.getPendingForThread(threadId)).toBeDefined(); + + const submitted = store.submitAnswers(toolCallId, [ + { + questionId: "q1", + question: "Do you prefer TypeScript or JavaScript?", + selected: ["TypeScript"], + }, + ]); + + expect(submitted).toBe(true); + const result = await askPromise; + expect(result).toHaveLength(1); + expect(result[0].selected).toEqual(["TypeScript"]); + expect(store.getPending(toolCallId)).toBeUndefined(); + }); + + it("cancels a pending question and rejects the promise", async () => { + const store = new PendingQuestionStore(); + const threadId = "thread-2"; + const toolCallId = "tool-2"; + + const askPromise = store.ask(threadId, toolCallId, [ + { + id: "q1", + question: "Choose one", + options: ["A", "B"], + }, + ]); + + const cancelled = store.cancel(toolCallId, new Error("User cancelled question")); + expect(cancelled).toBe(true); + + await expect(askPromise).rejects.toThrow("User cancelled question"); + expect(store.getPending(toolCallId)).toBeUndefined(); + }); + + it("aborts when AbortSignal fires", async () => { + const store = new PendingQuestionStore(); + const controller = new AbortController(); + const threadId = "thread-3"; + const toolCallId = "tool-3"; + + const askPromise = store.ask( + threadId, + toolCallId, + [ + { + id: "q1", + question: "Choose one", + options: ["A", "B"], + }, + ], + controller.signal, + ); + + controller.abort(); + + await expect(askPromise).rejects.toThrow("Tool execution was aborted"); + expect(store.getPending(toolCallId)).toBeUndefined(); + }); + + it("immediately rejects if signal is already aborted", async () => { + const store = new PendingQuestionStore(); + const controller = new AbortController(); + controller.abort(); + + await expect( + store.ask( + "thread-4", + "tool-4", + [ + { + id: "q1", + question: "Choose one", + options: ["A", "B"], + }, + ], + controller.signal, + ), + ).rejects.toThrow("Tool execution was aborted"); + }); + + it("clears pending questions for a thread", async () => { + const store = new PendingQuestionStore(); + const askPromise = store.ask("thread-5", "tool-5", [ + { id: "q1", question: "Q", options: ["1", "2"] }, + ]); + + store.clearForThread("thread-5"); + await expect(askPromise).rejects.toThrow("Thread session cleared or reset"); + expect(store.getPendingForThread("thread-5")).toBeUndefined(); + }); +}); From 45e557655369daedecbc15d5b135535798530863 Mon Sep 17 00:00:00 2001 From: Klabundo Date: Wed, 16 Sep 2026 15:30:32 +0200 Subject: [PATCH 2/4] Update src/components/chat/toolOutputRenderModel.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/components/chat/toolOutputRenderModel.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/chat/toolOutputRenderModel.ts b/src/components/chat/toolOutputRenderModel.ts index 365110e0..03354949 100644 --- a/src/components/chat/toolOutputRenderModel.ts +++ b/src/components/chat/toolOutputRenderModel.ts @@ -692,5 +692,11 @@ function parseAskQuestionPayload(raw: string): AskQuestionPayload | undefined { function isAskQuestionPayload(value: unknown): value is AskQuestionPayload { if (!isPlainObject(value)) return false; if (!("answers" in value) || !Array.isArray(value.answers)) return false; - return value.answers.every((item) => isPlainObject(item) && typeof item.question === "string"); + return value.answers.every( + (item) => + isPlainObject(item) && + typeof item.question === "string" && + Array.isArray(item.selected) && + item.selected.every((selected) => typeof selected === "string"), + ); } From 215434a5df45da62a9ebb264c0e038effabc2085 Mon Sep 17 00:00:00 2001 From: Klabundo Date: Wed, 16 Sep 2026 15:33:30 +0200 Subject: [PATCH 3/4] style: format AgentManager and pendingQuestionStore with biome --- src/agent/AgentManager.ts | 3 +-- test/stores/pendingQuestionStore.test.ts | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/agent/AgentManager.ts b/src/agent/AgentManager.ts index e88f1905..8a298918 100644 --- a/src/agent/AgentManager.ts +++ b/src/agent/AgentManager.ts @@ -838,8 +838,7 @@ export class AgentManager { const attached = this.attachedToolIds(agentCfg); const isToolEnabled = (toolId: BuiltInToolId): boolean => { return ( - (attached.has(toolId) || toolId === "ask_question") && - (agentCfg.toolsConfig[toolId]?.enabled ?? true) + (attached.has(toolId) || toolId === "ask_question") && (agentCfg.toolsConfig[toolId]?.enabled ?? true) ); }; diff --git a/test/stores/pendingQuestionStore.test.ts b/test/stores/pendingQuestionStore.test.ts index 5fed3964..8a51e194 100644 --- a/test/stores/pendingQuestionStore.test.ts +++ b/test/stores/pendingQuestionStore.test.ts @@ -102,9 +102,7 @@ describe("PendingQuestionStore", () => { it("clears pending questions for a thread", async () => { const store = new PendingQuestionStore(); - const askPromise = store.ask("thread-5", "tool-5", [ - { id: "q1", question: "Q", options: ["1", "2"] }, - ]); + const askPromise = store.ask("thread-5", "tool-5", [{ id: "q1", question: "Q", options: ["1", "2"] }]); store.clearForThread("thread-5"); await expect(askPromise).rejects.toThrow("Thread session cleared or reset"); From 4b13fddea148d8c7c218ac9743c7d6e662712828 Mon Sep 17 00:00:00 2001 From: Klabundo Date: Wed, 16 Sep 2026 15:37:23 +0200 Subject: [PATCH 4/4] fix(test): remove explicit any in askQuestion test --- test/agent/tools/askQuestion.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/agent/tools/askQuestion.test.ts b/test/agent/tools/askQuestion.test.ts index 316d4597..56a49c91 100644 --- a/test/agent/tools/askQuestion.test.ts +++ b/test/agent/tools/askQuestion.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it } from "vitest"; +import type { RunnableConfig } from "@langchain/core/runnables"; import { createAskQuestionTool } from "../../../src/agent/tools/askQuestion"; import { getPendingQuestionStore, @@ -37,7 +38,7 @@ describe("askQuestion tool", () => { { toolCallId, configurable: { thread_id: "thread-abc" }, - } as any, + } as unknown as RunnableConfig, ); // Wait a tick for async validation and execution to reach the tool function