diff --git a/src/agent/index.ts b/src/agent/index.ts index e7eb7966..482fcf93 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -4,8 +4,8 @@ export { Scratchpad } from './scratchpad.js'; export { getCurrentDate, buildSystemPrompt, buildIterationPrompt, DEFAULT_SYSTEM_PROMPT } from './prompts.js'; -export type { - AgentConfig, +export type { + AgentConfig, Message, AgentEvent, ThinkingEvent, @@ -14,13 +14,14 @@ export type { ToolEndEvent, ToolErrorEvent, ToolLimitEvent, + AskUserEvent, AnswerStartEvent, DoneEvent, } from './types.js'; -export type { - ToolCallRecord, - ToolContext, +export type { + ToolCallRecord, + ToolContext, ScratchpadEntry, ToolLimitConfig, ToolUsageStatus, diff --git a/src/agent/tool-executor.ts b/src/agent/tool-executor.ts index 0af1a186..d69c444a 100644 --- a/src/agent/tool-executor.ts +++ b/src/agent/tool-executor.ts @@ -2,6 +2,7 @@ import { AIMessage } from '@langchain/core/messages'; import { StructuredToolInterface } from '@langchain/core/tools'; import { createProgressChannel } from '../utils/progress-channel.js'; import type { + AskUserEvent, ToolEndEvent, ToolErrorEvent, ToolLimitEvent, @@ -15,7 +16,8 @@ type ToolExecutionEvent = | ToolProgressEvent | ToolEndEvent | ToolErrorEvent - | ToolLimitEvent; + | ToolLimitEvent + | AskUserEvent; /** * Executes tool calls and emits streaming tool lifecycle events. @@ -24,7 +26,7 @@ export class AgentToolExecutor { constructor( private readonly toolMap: Map, private readonly signal?: AbortSignal - ) {} + ) { } async *executeAll( response: AIMessage, @@ -65,6 +67,26 @@ export class AgentToolExecutor { const toolStartTime = Date.now(); + // Special handling for ask_user tool: yield event and wait for user response + if (toolName === 'ask_user') { + const question = (toolArgs.question as string) || 'Can you provide more details?'; + let resolveAnswer!: (answer: string) => void; + const answerPromise = new Promise((resolve) => { + resolveAnswer = resolve; + }); + + yield { type: 'ask_user', question, resolve: resolveAnswer } as AskUserEvent; + + const userAnswer = await answerPromise; + const duration = Date.now() - toolStartTime; + const result = `User answered: ${userAnswer}`; + + yield { type: 'tool_end', tool: toolName, args: toolArgs, result, duration }; + ctx.scratchpad.recordToolCall(toolName, question); + ctx.scratchpad.addToolResult(toolName, toolArgs, result); + return; + } + try { const tool = this.toolMap.get(toolName); if (!tool) { diff --git a/src/agent/types.ts b/src/agent/types.ts index db78f7ed..752fcd8c 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -93,6 +93,17 @@ export interface ContextClearedEvent { keptCount: number; } +/** + * Agent is asking the user a clarifying question + */ +export interface AskUserEvent { + type: 'ask_user'; + /** The question to display to the user */ + question: string; + /** Callback to provide the user's answer — resolves the tool's promise */ + resolve: (answer: string) => void; +} + /** * Final answer generation started */ @@ -132,6 +143,7 @@ export type AgentEvent = | ToolEndEvent | ToolErrorEvent | ToolLimitEvent + | AskUserEvent | ContextClearedEvent | AnswerStartEvent | DoneEvent; diff --git a/src/cli.tsx b/src/cli.tsx index d1d6a597..5b0c04ab 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -24,11 +24,11 @@ config({ quiet: true }); export function CLI() { const { exit } = useApp(); - + // Ref to hold setError - avoids TDZ issue since useModelSelection needs to call // setError but useAgentRunner (which provides setError) depends on useModelSelection's outputs const setErrorRef = useRef<((error: string | null) => void) | null>(null); - + // Model selection state and handlers const { selectionState, @@ -44,7 +44,7 @@ export function CLI() { handleApiKeySubmit, isInSelectionFlow, } = useModelSelection((errorMsg) => setErrorRef.current?.(errorMsg)); - + // Agent execution state and handlers const { history, @@ -54,11 +54,12 @@ export function CLI() { runQuery, cancelExecution, setError, + submitAskUserResponse, } = useAgentRunner({ model, modelProvider: provider, maxIterations: 10 }, inMemoryChatHistoryRef); - + // Assign setError to ref so useModelSelection's callback can access it setErrorRef.current = setError; - + // Input history for up/down arrow navigation const { historyValue, @@ -68,7 +69,7 @@ export function CLI() { updateAgentResponse, resetNavigation, } = useInputHistory(); - + // Handle history navigation from Input component const handleHistoryNavigate = useCallback((direction: 'up' | 'down') => { if (direction === 'up') { @@ -77,7 +78,7 @@ export function CLI() { navigateDown(); } }, [navigateUp, navigateDown]); - + // Handle user input submission const handleSubmit = useCallback(async (query: string) => { // Handle exit @@ -86,27 +87,33 @@ export function CLI() { exit(); return; } - + // Handle model selection command if (query === '/model') { startSelection(); return; } - + + // If agent is waiting for user answer, route input there + if (workingState.status === 'ask_user') { + submitAskUserResponse(query); + return; + } + // Ignore if not idle (processing or in selection flow) if (isInSelectionFlow() || workingState.status !== 'idle') return; - + // Save user message to history immediately and reset navigation await saveMessage(query); resetNavigation(); - + // Run query and save agent response when complete const result = await runQuery(query); if (result?.answer) { await updateAgentResponse(result.answer); } - }, [exit, startSelection, isInSelectionFlow, workingState.status, runQuery, saveMessage, updateAgentResponse, resetNavigation]); - + }, [exit, startSelection, isInSelectionFlow, workingState.status, submitAskUserResponse, runQuery, saveMessage, updateAgentResponse, resetNavigation]); + // Handle keyboard shortcuts useInput((input, key) => { // Escape key - cancel selection flows or running agent @@ -120,7 +127,7 @@ export function CLI() { return; } } - + // Ctrl+C - cancel or exit if (key.ctrl && input === 'c') { if (isInSelectionFlow()) { @@ -133,10 +140,10 @@ export function CLI() { } } }); - + // Render selection screens const { appState, pendingProvider, pendingModels } = selectionState; - + if (appState === 'provider_select') { return ( @@ -144,7 +151,7 @@ export function CLI() { ); } - + if (appState === 'model_select' && pendingProvider) { return ( @@ -157,7 +164,7 @@ export function CLI() { ); } - + if (appState === 'model_input' && pendingProvider) { return ( @@ -169,60 +176,60 @@ export function CLI() { ); } - + if (appState === 'api_key_confirm' && pendingProvider) { return ( - ); } - + if (appState === 'api_key_input' && pendingProvider) { const apiKeyName = getApiKeyNameForProvider(pendingProvider) || ''; return ( - ); } - + // Main chat interface return ( - + {/* All history items (queries, events, answers) */} {history.map(item => ( ))} - + {/* Error display */} {error && ( Error: {error} )} - + {/* Working indicator - only show when processing */} {isProcessing && } - + {/* Input */} - - + {/* Debug Panel - set show={false} to hide */} diff --git a/src/components/AgentEventView.tsx b/src/components/AgentEventView.tsx index 308a5abd..897188a2 100644 --- a/src/components/AgentEventView.tsx +++ b/src/components/AgentEventView.tsx @@ -20,15 +20,15 @@ function formatToolName(name: string): string { */ function truncateAtWord(str: string, maxLength: number): string { if (str.length <= maxLength) return str; - + // Find last space before maxLength const lastSpace = str.lastIndexOf(' ', maxLength); - + // If there's a space in a reasonable position (at least 50% of maxLength), use it if (lastSpace > maxLength * 0.5) { return str.slice(0, lastSpace) + '...'; } - + // No good word boundary - truncate at maxLength return str.slice(0, maxLength) + '...'; } @@ -42,7 +42,7 @@ function formatArgs(args: Record): string { const query = String(args.query); return `"${truncateAtWord(query, 60)}"`; } - + // For other tools, format key=value pairs with truncation return Object.entries(args) .map(([key, value]) => { @@ -119,10 +119,10 @@ export function ThinkingView({ message }: ThinkingViewProps) { const trimmedMessage = message.trim(); if (!trimmedMessage) return null; - const displayMessage = trimmedMessage.length > 200 - ? trimmedMessage.slice(0, 200) + '...' + const displayMessage = trimmedMessage.length > 200 + ? trimmedMessage.slice(0, 200) + '...' : trimmedMessage; - + return ( {displayMessage} @@ -168,7 +168,7 @@ interface ToolEndViewProps { export function ToolEndView({ tool, args, result, duration }: ToolEndViewProps) { // Parse result to get a summary let summary = 'Received data'; - + // Special handling for skill tool if (tool === 'skill') { const skillName = args.skill as string; @@ -181,11 +181,11 @@ export function ToolEndView({ tool, args, result, duration }: ToolEndViewProps) summary = `Received ${parsed.data.length} items`; } else if (typeof parsed.data === 'object') { const keys = Object.keys(parsed.data).filter(k => !k.startsWith('_')); // Exclude _errors - + // Tool-specific summaries if (tool === 'financial_search') { - summary = keys.length === 1 - ? `Called 1 data source` + summary = keys.length === 1 + ? `Called 1 data source` : `Called ${keys.length} data sources`; } else if (tool === 'web_search') { summary = `Did 1 search`; @@ -199,7 +199,7 @@ export function ToolEndView({ tool, args, result, duration }: ToolEndViewProps) summary = truncateResult(result, 50); } } - + return ( @@ -298,7 +298,7 @@ function findCurrentBrowserStep(events: DisplayEvent[], activeStepId?: string): if (step) return step; } } - + // Otherwise, find the most recent displayable step (working backwards) for (let i = events.length - 1; i >= 0; i--) { const event = events[i]; @@ -307,7 +307,7 @@ function findCurrentBrowserStep(events: DisplayEvent[], activeStepId?: string): if (step) return step; } } - + return null; } @@ -353,34 +353,48 @@ export function AgentEventView({ event, isActive = false, progressMessage }: Age switch (event.type) { case 'thinking': return ; - + case 'tool_start': return ; - + case 'tool_end': return ; - + case 'tool_error': return ; - + case 'tool_limit': return ; - + case 'context_cleared': return ; - + + case 'ask_user': + return ( + + + + Dexter wants to ask you: + + + + {event.question} + + + ); + case 'answer_start': case 'done': // These are handled separately by the parent component return null; - + default: return null; } } // Event grouping types for consolidated display -type EventGroup = +type EventGroup = | { type: 'browser_session'; id: string; events: DisplayEvent[]; activeStepId?: string } | { type: 'single'; displayEvent: DisplayEvent }; @@ -446,21 +460,21 @@ export function EventListView({ events, activeToolId }: EventListViewProps) { // Render single events as before const { id, event, completed, endEvent, progressMessage } = group.displayEvent; - + // For tool events, show the end state if completed if (event.type === 'tool_start' && completed && endEvent?.type === 'tool_end') { return ( - ); } - + if (event.type === 'tool_start' && completed && endEvent?.type === 'tool_error') { return ( @@ -468,11 +482,11 @@ export function EventListView({ events, activeToolId }: EventListViewProps) { ); } - + return ( - diff --git a/src/components/WorkingIndicator.tsx b/src/components/WorkingIndicator.tsx index c350a180..37067111 100644 --- a/src/components/WorkingIndicator.tsx +++ b/src/components/WorkingIndicator.tsx @@ -10,7 +10,7 @@ import { getRandomThinkingVerb } from '../utils/thinking-verbs.js'; function ShineText({ text, color, shineColor }: { text: string; color: string; shineColor: string }) { const [shinePos, setShinePos] = useState(0); const [isPaused, setIsPaused] = useState(false); - + useEffect(() => { if (isPaused) { // Wait 2 seconds before restarting the shine @@ -20,7 +20,7 @@ function ShineText({ text, color, shineColor }: { text: string; color: string; s }, 2000); return () => clearTimeout(timeout); } - + const interval = setInterval(() => { setShinePos((prev) => { const next = prev + 1; @@ -31,10 +31,10 @@ function ShineText({ text, color, shineColor }: { text: string; color: string; s return next; }); }, 30); - + return () => clearInterval(interval); }, [isPaused, text.length]); - + // Memoize the rendered parts for performance const parts = useMemo(() => { const result: React.ReactNode[] = []; @@ -49,15 +49,16 @@ function ShineText({ text, color, shineColor }: { text: string; color: string; s } return result; }, [text, shinePos, isPaused, color, shineColor]); - + return <>{parts}; } -export type WorkingState = +export type WorkingState = | { status: 'idle' } | { status: 'thinking' } | { status: 'tool'; toolName: string } - | { status: 'answering'; startTime: number }; + | { status: 'answering'; startTime: number } + | { status: 'ask_user'; question: string }; interface WorkingIndicatorProps { state: WorkingState; @@ -70,40 +71,40 @@ export function WorkingIndicator({ state }: WorkingIndicatorProps) { const [elapsed, setElapsed] = useState(0); const [thinkingVerb, setThinkingVerb] = useState(getRandomThinkingVerb); const prevStatusRef = useRef('idle'); - + // Pick a new random verb when transitioning into thinking/tool state useEffect(() => { const isThinking = state.status === 'thinking' || state.status === 'tool'; const wasThinking = prevStatusRef.current === 'thinking' || prevStatusRef.current === 'tool'; - + if (isThinking && !wasThinking) { setThinkingVerb(getRandomThinkingVerb()); } - + prevStatusRef.current = state.status; }, [state.status]); - + // Track elapsed time only when answering useEffect(() => { if (state.status !== 'answering') { setElapsed(0); return; } - + const startTime = state.startTime; setElapsed(Math.floor((Date.now() - startTime) / 1000)); - + const interval = setInterval(() => { setElapsed(Math.floor((Date.now() - startTime) / 1000)); }, 1000); - + return () => clearInterval(interval); }, [state]); - + if (state.status === 'idle') { return null; } - + let statusWord: string; let suffixEnd: string; switch (state.status) { @@ -116,8 +117,12 @@ export function WorkingIndicator({ state }: WorkingIndicatorProps) { statusWord = 'Answering'; suffixEnd = ` to interrupt)`; break; + case 'ask_user': + statusWord = 'Waiting for your answer...'; + suffixEnd = ' to cancel)'; + break; } - + return ( diff --git a/src/hooks/useAgentRunner.ts b/src/hooks/useAgentRunner.ts index f1d30f38..6452dfa0 100644 --- a/src/hooks/useAgentRunner.ts +++ b/src/hooks/useAgentRunner.ts @@ -2,7 +2,7 @@ import { useState, useCallback, useRef } from 'react'; import { Agent } from '../agent/agent.js'; import { InMemoryChatHistory } from '../utils/in-memory-chat-history.js'; import type { HistoryItem, WorkingState } from '../components/index.js'; -import type { AgentConfig, AgentEvent, DoneEvent } from '../agent/index.js'; +import type { AgentConfig, AgentEvent, AskUserEvent, DoneEvent } from '../agent/index.js'; // ============================================================================ // Types @@ -18,11 +18,12 @@ export interface UseAgentRunnerResult { workingState: WorkingState; error: string | null; isProcessing: boolean; - + // Actions runQuery: (query: string) => Promise; cancelExecution: () => void; setError: (error: string | null) => void; + submitAskUserResponse: (answer: string) => void; } // ============================================================================ @@ -36,9 +37,10 @@ export function useAgentRunner( const [history, setHistory] = useState([]); const [workingState, setWorkingState] = useState({ status: 'idle' }); const [error, setError] = useState(null); - + const abortControllerRef = useRef(null); - + const askUserResolveRef = useRef<((answer: string) => void) | null>(null); + // Helper to update the last (processing) history item const updateLastHistoryItem = useCallback(( updater: (item: HistoryItem) => Partial @@ -49,7 +51,7 @@ export function useAgentRunner( return [...prev.slice(0, -1), { ...last, ...updater(last) }]; }); }, []); - + // Handle agent events const handleEvent = useCallback((event: AgentEvent) => { switch (event.type) { @@ -63,7 +65,7 @@ export function useAgentRunner( }], })); break; - + case 'tool_start': { const toolId = `tool-${event.tool}-${Date.now()}`; setWorkingState({ status: 'tool', toolName: event.tool }); @@ -87,35 +89,42 @@ export function useAgentRunner( ), })); break; - + case 'tool_end': setWorkingState({ status: 'thinking' }); updateLastHistoryItem(item => ({ activeToolId: undefined, - events: item.events.map(e => + events: item.events.map(e => e.id === item.activeToolId ? { ...e, completed: true, endEvent: event } : e ), })); break; - + case 'tool_error': setWorkingState({ status: 'thinking' }); updateLastHistoryItem(item => ({ activeToolId: undefined, - events: item.events.map(e => + events: item.events.map(e => e.id === item.activeToolId ? { ...e, completed: true, endEvent: event } : e ), })); break; - + case 'answer_start': setWorkingState({ status: 'answering', startTime: Date.now() }); break; - + + case 'ask_user': { + const askEvent = event as AskUserEvent; + askUserResolveRef.current = askEvent.resolve; + setWorkingState({ status: 'ask_user', question: askEvent.question }); + break; + } + case 'done': { const doneEvent = event as DoneEvent; updateLastHistoryItem(item => { @@ -138,16 +147,16 @@ export function useAgentRunner( } } }, [updateLastHistoryItem, inMemoryChatHistoryRef]); - + // Run a query through the agent const runQuery = useCallback(async (query: string): Promise => { // Create abort controller for this execution const abortController = new AbortController(); abortControllerRef.current = abortController; - + // Track the final answer to return let finalAnswer: string | undefined; - + // Add to history immediately const itemId = Date.now().toString(); const startTime = Date.now(); @@ -159,20 +168,20 @@ export function useAgentRunner( status: 'processing', startTime, }]); - + // Save query to chat history immediately for multi-turn context inMemoryChatHistoryRef.current?.saveUserQuery(query); - + setError(null); setWorkingState({ status: 'thinking' }); - + try { const agent = await Agent.create({ ...agentConfig, signal: abortController.signal, }); const stream = agent.run(query, inMemoryChatHistoryRef.current!); - + for await (const event of stream) { // Capture the final answer from the done event if (event.type === 'done') { @@ -180,7 +189,7 @@ export function useAgentRunner( } handleEvent(event); } - + // Return the answer if we got one if (finalAnswer) { return { answer: finalAnswer }; @@ -196,7 +205,7 @@ export function useAgentRunner( setWorkingState({ status: 'idle' }); return undefined; } - + const errorMsg = e instanceof Error ? e.message : String(e); setError(errorMsg); // Mark the history item as error @@ -211,14 +220,14 @@ export function useAgentRunner( abortControllerRef.current = null; } }, [agentConfig, inMemoryChatHistoryRef, handleEvent]); - + // Cancel the current execution const cancelExecution = useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } - + // Mark current processing item as interrupted setHistory(prev => { const last = prev[prev.length - 1]; @@ -227,10 +236,19 @@ export function useAgentRunner( }); setWorkingState({ status: 'idle' }); }, []); - + // Check if currently processing const isProcessing = history.length > 0 && history[history.length - 1].status === 'processing'; - + + // Submit an answer to an ask_user question + const submitAskUserResponse = useCallback((answer: string) => { + if (askUserResolveRef.current) { + askUserResolveRef.current(answer); + askUserResolveRef.current = null; + setWorkingState({ status: 'thinking' }); + } + }, []); + return { history, workingState, @@ -239,5 +257,6 @@ export function useAgentRunner( runQuery, cancelExecution, setError, + submitAskUserResponse, }; } diff --git a/src/tools/ask-user.ts b/src/tools/ask-user.ts new file mode 100644 index 00000000..a3559952 --- /dev/null +++ b/src/tools/ask-user.ts @@ -0,0 +1,54 @@ +import { DynamicStructuredTool } from '@langchain/core/tools'; +import { z } from 'zod'; + +/** + * Rich description for the ask_user tool. + * Used in the system prompt to guide the LLM on when and how to use this tool. + */ +export const ASK_USER_DESCRIPTION = ` +Ask the user a follow-up or clarifying question to get more context. + +## When to Use + +- When the user's query is ambiguous and could be interpreted multiple ways +- Before embarking on a lengthy research deep dive, to confirm scope or focus +- When you need a specific detail the user hasn't provided (e.g., time period, specific company, metric preference) +- When the user's intent is unclear and guessing could waste effort + +## When NOT to Use + +- When the query is clear and actionable — just do the work +- When you can reasonably infer the user's intent from context +- To confirm obvious next steps — be decisive +- Multiple times in a row — ask one clear question, then proceed with the answer + +## Usage Notes + +- Ask ONE focused question at a time +- Be specific about what you need — avoid vague "can you clarify?" questions +- After receiving the answer, proceed immediately with the research +`.trim(); + +/** + * Ask user tool. + * + * The actual user-interaction is handled by the tool executor and the + * CLI layer. The tool's func is never invoked directly — the executor + * intercepts the "ask_user" tool name, yields an AskUserEvent, and + * waits for the UI to resolve the promise with the user's answer. + * + * This tool instance exists only so the LLM schema-binding works + * correctly. The func below is a fallback that should never run. + */ +export const askUserTool = new DynamicStructuredTool({ + name: 'ask_user', + description: 'Ask the user a clarifying question to get more context before or during research.', + schema: z.object({ + question: z.string().describe('The question to ask the user'), + }), + func: async ({ question }) => { + // This should never be called — the tool executor intercepts ask_user + // and handles it via the AskUserEvent mechanism. + return `[ask_user fallback] ${question}`; + }, +}); diff --git a/src/tools/descriptions/ask-user.ts b/src/tools/descriptions/ask-user.ts new file mode 100644 index 00000000..5e4ac876 --- /dev/null +++ b/src/tools/descriptions/ask-user.ts @@ -0,0 +1,27 @@ +/** + * Rich description for the ask_user tool. + * Used in the system prompt to guide the LLM on when and how to use this tool. + */ +export const ASK_USER_DESCRIPTION = ` +Ask the user a follow-up or clarifying question to get more context. + +## When to Use + +- When the user's query is ambiguous and could be interpreted multiple ways +- Before embarking on a lengthy research deep dive, to confirm scope or focus +- When you need a specific detail the user hasn't provided (e.g., time period, specific company, metric preference) +- When the user's intent is unclear and guessing could waste effort + +## When NOT to Use + +- When the query is clear and actionable — just do the work +- When you can reasonably infer the user's intent from context +- To confirm obvious next steps — be decisive +- Multiple times in a row — ask one clear question, then proceed with the answer + +## Usage Notes + +- Ask ONE focused question at a time +- Be specific about what you need — avoid vague "can you clarify?" questions +- After receiving the answer, proceed immediately with the research +`.trim(); diff --git a/src/tools/descriptions/index.ts b/src/tools/descriptions/index.ts index 08d59cd2..80105fbc 100644 --- a/src/tools/descriptions/index.ts +++ b/src/tools/descriptions/index.ts @@ -8,3 +8,4 @@ export { WEB_SEARCH_DESCRIPTION } from './web-search.js'; export { READ_FILINGS_DESCRIPTION } from './read-filings.js'; export { WEB_FETCH_DESCRIPTION } from './web-fetch.js'; export { BROWSER_DESCRIPTION } from './browser.js'; +export { ASK_USER_DESCRIPTION } from './ask-user.js'; diff --git a/src/tools/registry.ts b/src/tools/registry.ts index d62ccd57..5f47d754 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -2,9 +2,10 @@ import { StructuredToolInterface } from '@langchain/core/tools'; import { createFinancialSearch, createFinancialMetrics, createReadFilings } from './finance/index.js'; import { exaSearch, perplexitySearch, tavilySearch } from './search/index.js'; import { skillTool, SKILL_TOOL_DESCRIPTION } from './skill.js'; +import { askUserTool } from './ask-user.js'; import { webFetchTool } from './fetch/index.js'; import { browserTool } from './browser/index.js'; -import { FINANCIAL_SEARCH_DESCRIPTION, FINANCIAL_METRICS_DESCRIPTION, WEB_SEARCH_DESCRIPTION, WEB_FETCH_DESCRIPTION, READ_FILINGS_DESCRIPTION, BROWSER_DESCRIPTION } from './descriptions/index.js'; +import { FINANCIAL_SEARCH_DESCRIPTION, FINANCIAL_METRICS_DESCRIPTION, WEB_SEARCH_DESCRIPTION, WEB_FETCH_DESCRIPTION, READ_FILINGS_DESCRIPTION, BROWSER_DESCRIPTION, ASK_USER_DESCRIPTION } from './descriptions/index.js'; import { discoverSkills } from '../skills/index.js'; /** @@ -53,6 +54,11 @@ export function getToolRegistry(model: string): RegisteredTool[] { tool: browserTool, description: BROWSER_DESCRIPTION, }, + { + name: 'ask_user', + tool: askUserTool, + description: ASK_USER_DESCRIPTION, + }, ]; // Include web_search if Exa, Perplexity, or Tavily API key is configured (Exa → Perplexity → Tavily)