Skip to content

Commit 6e28f0d

Browse files
authored
Merge pull request #1561 from mnfst/fix/short-greeting-with-tools
fix(routing): route short greetings to simple tier when tools attached
2 parents 7eb8982 + 43590ad commit 6e28f0d

File tree

6 files changed

+56
-6
lines changed

6 files changed

+56
-6
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"manifest": patch
3+
---
4+
5+
Route short greetings to the `simple` tier even when the agent attaches tools. The scorer's short-message fast path was gated on `!hasTools`, so personal AI agents like OpenClaw (which always send a `tools` array) skipped it entirely and fell into full scoring, where session momentum could pull a one-word `hi` up to `complex`. Dropping the gate lets short, non-technical prompts short-circuit to `simple` before momentum kicks in. Short technical prompts like `Debug this function` still fall through to full scoring.

packages/backend/src/routing/resolve.service.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,16 @@ describe('ResolveService', () => {
394394
mockProviderKeyService.getEffectiveModel.mockResolvedValue('gpt-4o');
395395
mockPricingCache.getByModel.mockReturnValue({ provider: 'OpenAI' });
396396

397+
// Message is long enough to bypass the short-message fast path so the
398+
// tools-floor branch in applyTierFloors is exercised.
397399
const result = await service.resolve(
398400
'agent-1',
399-
[{ role: 'user', content: 'hi' }],
401+
[
402+
{
403+
role: 'user',
404+
content: 'Please list the items you know about and summarise what they do.',
405+
},
406+
],
400407
[{ name: 'search' }],
401408
'auto',
402409
);

packages/backend/src/scoring/__tests__/score-request-advanced.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ describe('scoreRequest — estimateTotalTokens branches', () => {
7474
});
7575
});
7676

77+
describe('scoreRequest — short greeting with tools and momentum', () => {
78+
it('keeps a tool-bearing short greeting at SIMPLE under complex momentum', () => {
79+
const tools = Array.from({ length: 10 }, (_, i) => ({
80+
type: 'function' as const,
81+
function: { name: `tool_${i}`, description: 'noop', parameters: { type: 'object' } },
82+
}));
83+
const result = scoreRequest({ messages: [{ role: 'user', content: 'hi' }], tools }, undefined, {
84+
recentTiers: ['complex', 'complex', 'complex'],
85+
});
86+
expect(result.tier).toBe('simple');
87+
expect(result.reason).toBe('short_message');
88+
});
89+
});
90+
7791
describe('scoreRequest — ambiguous fallback', () => {
7892
it('falls back to standard/ambiguous when confidence is low and reason is scored', () => {
7993
const result = scoreRequest(

packages/backend/src/scoring/__tests__/score-request.spec.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,29 @@ describe('scoreRequest — hard overrides', () => {
7878
expect(result.reason).not.toBe('short_message');
7979
});
8080

81-
it('does NOT return SIMPLE for short message with tools', () => {
81+
it('returns SIMPLE for short greeting even with tools attached', () => {
82+
const tools = Array.from({ length: 10 }, (_, i) => ({
83+
type: 'function' as const,
84+
function: { name: `tool_${i}`, description: 'noop', parameters: { type: 'object' } },
85+
}));
8286
const result = scoreRequest({
8387
messages: [{ role: 'user', content: 'hi' }],
84-
tools: [{}, {}, {}, {}, {}],
88+
tools,
8589
});
86-
expect(result.tier).not.toBe('simple');
90+
expect(result.tier).toBe('simple');
91+
expect(result.reason).toBe('short_message');
92+
});
93+
94+
it('does NOT return SIMPLE for short technical prompt with tools', () => {
95+
const tools = Array.from({ length: 10 }, (_, i) => ({
96+
type: 'function' as const,
97+
function: { name: `tool_${i}`, description: 'noop', parameters: { type: 'object' } },
98+
}));
99+
const result = scoreRequest({
100+
messages: [{ role: 'user', content: 'Debug this function' }],
101+
tools,
102+
});
103+
expect(result.reason).not.toBe('short_message');
87104
});
88105

89106
it('does NOT return SIMPLE for short message with momentum (no simple indicator)', () => {

packages/backend/src/scoring/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function scoreRequest(
190190

191191
const hasTools = tools && tools.length > 0;
192192
const hasMomentum = momentum?.recentTiers && momentum.recentTiers.length > 0;
193-
if (lastUserText.length > 0 && lastUserText.length < 50 && !hasTools) {
193+
if (lastUserText.length > 0 && lastUserText.length < 50) {
194194
const lastMatches = trie.scan(lastUserText);
195195
const hasSimpleIndicator = lastMatches.some((m) => m.dimension === 'simpleIndicators');
196196
const hasComplexSignal = lastMatches.some(

packages/backend/test/routing-flow.e2e-spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,16 @@ describe('Routing enabled → scorer routes by query complexity', () => {
234234
});
235235

236236
it('tools floor query to at least standard tier', async () => {
237+
// Long enough to bypass the short-message fast path so the tools-floor
238+
// branch in applyTierFloors is the thing being exercised.
237239
const res = await bearer(api().post('/api/v1/routing/resolve'))
238240
.send({
239-
messages: [{ role: 'user', content: 'search for cats' }],
241+
messages: [
242+
{
243+
role: 'user',
244+
content: 'Search the web for recent news about cats and summarise the top results.',
245+
},
246+
],
240247
tools: [{ name: 'web_search' }],
241248
tool_choice: 'auto',
242249
})

0 commit comments

Comments
 (0)