From 9c8e9cd546b81e760f331af3f5cfa5ca7cc49f80 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 22 Feb 2026 17:12:30 -0800 Subject: [PATCH] fix(tui): narrow tool inventory query detection --- src/daemon/routing.test.ts | 72 ++++++++++++++++++++++++++++++ src/daemon/routing.ts | 13 ++++-- src/frontends/tui/commands.test.ts | 3 +- src/frontends/tui/commands.ts | 18 ++++---- 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/daemon/routing.test.ts b/src/daemon/routing.test.ts index 48ab970..ed93de1 100644 --- a/src/daemon/routing.test.ts +++ b/src/daemon/routing.test.ts @@ -1134,6 +1134,78 @@ describe('daemon external backend integration', () => { expect(reply).toHaveBeenCalledWith(expect.objectContaining({ text: 'Available tools (authoritative):\n- file.read' })); }); + it('does not force native processing for incidental tool wording', async () => { + const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process') + .mockResolvedValue('native response'); + const history: Array<{ role: 'user' | 'assistant'; content: string }> = []; + const session = { + id: 'telegram:external-incidental-tool', + addMessage: vi.fn((msg: { role: 'user' | 'assistant'; content: string }) => { + history.push(msg); + return msg; + }), + getHistory: vi.fn(() => [...history]), + clear: vi.fn(), + replaceHistory: vi.fn(), + getConfig: vi.fn(() => undefined), + setConfig: vi.fn(), + deleteConfig: vi.fn(), + }; + + const externalBackend = { + name: 'codex', + process: vi.fn(async () => 'external backend response'), + }; + + const router = createMessageRouter({ + sessionManager: { + getSession: vi.fn(() => session), + } as unknown as MessageRouterDeps['sessionManager'], + modelRouter: { + getAvailableTiers: () => ['fast', 'default', 'complex', 'local'], + getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }), + getLabel: (tier: string) => tier, + } as unknown as MessageRouterDeps['modelRouter'], + systemPrompt: 'test prompt', + toolRegistry: { + clone() { return this; }, + register: vi.fn(), + } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], + config: { + agents: { + primary_tier: 'default', + delegation: { + compaction: 'fast', + memory_extraction: 'fast', + classification: 'fast', + tool_summarisation: 'fast', + complex_reasoning: 'complex', + }, + max_delegation_depth: 3, + max_iterations: 10, + }, + compaction: { enabled: false }, + models: { default: { provider: 'anthropic', model: 'claude' } }, + } as unknown as MessageRouterDeps['config'], + externalBackends: { codex: externalBackend } as unknown as MessageRouterDeps['externalBackends'], + defaultName: 'codex', + }); + + const reply = vi.fn(async (_message: OutboundMessage) => {}); + await router.handler({ + id: 'm-external-incidental-tool', + channel: 'telegram', + senderId: 'external-incidental-tool', + text: 'The json by default is up to you, same as for gemini, codex is your tool, so decide what format is best for you to deal with.', + timestamp: Date.now(), + } as MessageRouterInput, reply); + + expect(externalBackend.process).toHaveBeenCalled(); + expect(processSpy).not.toHaveBeenCalled(); + expect(reply).toHaveBeenCalledWith(expect.objectContaining({ text: 'external backend response' })); + }); + it('falls back to native processing when external backend fails', async () => { const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process') .mockResolvedValue('native fallback response'); diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index 2b68757..f7e15d6 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -146,16 +146,21 @@ function shouldForceNativeForCapabilityQuery(text: string): boolean { if (!normalized) { return false; } - return ( + if ( normalized.includes('available tools') || normalized.includes('what tools') || normalized.includes('which tools') || normalized.includes('tool list') || normalized.includes('list tools') - || normalized.includes('your tools') || normalized.includes('what can you do') - || normalized.includes('can you do') - || normalized.includes('capabilities') + ) { + return true; + } + return ( + /\b(?:show|list|check)\s+(?:me\s+)?(?:your\s+)?(?:available\s+|new\s+)?tools?\b/.test(normalized) + || /\b(?:what|which)\s+tools?\b/.test(normalized) + || /\btools?\s+(?:do\s+you\s+have|are\s+available)\b/.test(normalized) + || /\b(?:show|list|what\s+are)\s+(?:your\s+)?capabilities\b/.test(normalized) ); } diff --git a/src/frontends/tui/commands.test.ts b/src/frontends/tui/commands.test.ts index 9ceca9b..7444073 100644 --- a/src/frontends/tui/commands.test.ts +++ b/src/frontends/tui/commands.test.ts @@ -213,7 +213,7 @@ describe('getCommandCompletions', () => { describe('isToolInventoryQuery', () => { it('detects common capability/tool-list prompts', () => { - expect(isToolInventoryQuery('Check out your new tools')).toBe(true); + expect(isToolInventoryQuery('check your new tools')).toBe(true); expect(isToolInventoryQuery('what tools do you have?')).toBe(true); expect(isToolInventoryQuery('show capabilities')).toBe(true); }); @@ -221,5 +221,6 @@ describe('isToolInventoryQuery', () => { it('does not match unrelated prompts', () => { expect(isToolInventoryQuery('write a shell script')).toBe(false); expect(isToolInventoryQuery('summarize this doc')).toBe(false); + expect(isToolInventoryQuery('The json by default is up to you, same as for gemini, codex is your tool, so decide what format is best for you to deal with.')).toBe(false); }); }); diff --git a/src/frontends/tui/commands.ts b/src/frontends/tui/commands.ts index 173ca50..b585644 100644 --- a/src/frontends/tui/commands.ts +++ b/src/frontends/tui/commands.ts @@ -25,20 +25,22 @@ export function isToolInventoryQuery(input: string): boolean { if (!normalized) { return false; } - const hasToolsWord = /\btools?\b/.test(normalized); - const hasInventoryIntent = /\b(check|show|list|what|which|available|new|have)\b/.test(normalized); - return ( + if ( normalized.includes('available tools') || normalized.includes('what tools') || normalized.includes('which tools') || normalized.includes('tool list') || normalized.includes('list tools') - || normalized.includes('new tools') - || normalized.includes('your tools') || normalized.includes('what can you do') - || normalized.includes('can you do') - || normalized.includes('capabilities') - || (hasToolsWord && hasInventoryIntent) + ) { + return true; + } + + return ( + /\b(?:show|list|check)\s+(?:me\s+)?(?:your\s+)?(?:available\s+|new\s+)?tools?\b/.test(normalized) + || /\b(?:what|which)\s+tools?\b/.test(normalized) + || /\btools?\s+(?:do\s+you\s+have|are\s+available)\b/.test(normalized) + || /\b(?:show|list|what\s+are)\s+(?:your\s+)?capabilities\b/.test(normalized) ); }