import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAgentOrchestrationToUserMessage, buildRouterContext, buildAgentOrchestrationAgentText, buildRouterNormalizedDecision, CHAT_INTENT_ROUTE, classifyWithRules, coercePageGenerationSkill, createChatIntentRouter, createManagedChatIntentRouter, isNormalizedRouterDecisionEnabled, isNormalizedRouterDecisionShadow, resolveGatewayAgentSessionId, resolveLegacyRouteFromClassification, resolveNormalizedRouterDecisionMode, logRouterDecisionShadow, ROUTER_DECISION_ROUTE, ROUTER_DECISION_MODE, ROUTER_DECISION_SESSION_HINT, isRealtimeInfoQuestion, coerceRealtimeWebSkill, resolveChatIntentRouterPolicy, } from './chat-intent-router.mjs'; test('classifyWithRules routes greetings to direct chat', () => { const result = classifyWithRules({ text: '你好', userMessage: { role: 'user', content: [{ type: 'text', text: '你好' }], metadata: { displayText: '你好' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'rule'); }); test('classifyWithRules routes page generation to agent orchestration', () => { const result = classifyWithRules({ text: '帮我做一个秋夜诗的 H5 页面', userMessage: { role: 'user', content: [{ type: 'text', text: '帮我做一个秋夜诗的 H5 页面' }], metadata: { displayText: '帮我做一个秋夜诗的 H5 页面' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); }); test('classifyWithRules routes implicit travel guide page requests to static-page-publish', () => { const result = classifyWithRules({ text: '苏州攻略页面', userMessage: { role: 'user', content: [{ type: 'text', text: '苏州攻略页面' }], metadata: { displayText: '苏州攻略页面' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); assert.match(result.agentBrief, /不要停在追问/); }); test('coercePageGenerationSkill replaces product-campaign-page for content pages', () => { const coerced = coercePageGenerationSkill( { route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'product-campaign-page', reason: '模型路由判定', agentBrief: '', confidence: 0.9, source: 'llm', }, '苏州攻略页面', { grantedSkills: ['static-page-publish', 'product-campaign-page'] }, ); assert.equal(coerced.suggestedSkill, 'static-page-publish'); assert.match(coerced.reason, /static-page-publish/); }); test('classifyWithRules routes themed article requests to agent orchestration', () => { const result = classifyWithRules({ text: '帮我写个主题文章', userMessage: { role: 'user', content: [{ type: 'text', text: '帮我写个主题文章' }], metadata: { displayText: '帮我写个主题文章' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); }); test('classifyWithRules honors forceDeepReasoning bypass', () => { const result = classifyWithRules({ text: '你好', forceDeepReasoning: true, userMessage: { role: 'user', content: [{ type: 'text', text: '你好' }], }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.match(result.reason, /深度推理/); }); test('classifyWithRules skips session continuation when llm router is enabled', () => { const continued = classifyWithRules({ text: '我想去日本玩', sessionId: '20260704_10', sessionMessageCount: 2, llmRouterEnabled: true, }); assert.equal(continued, null); }); test('classifyWithRules keeps empty pre-created sessions eligible for llm routing', () => { const continued = classifyWithRules({ text: '继续聊聊', sessionId: '20260704_2', sessionMessageCount: 2, }); assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(continued.reason, '延续已有 Agent 会话'); const fresh = classifyWithRules({ text: '我是 John,你对我的记忆知道多少?', sessionId: '20260704_3', sessionMessageCount: 0, }); assert.equal(fresh.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.match(fresh.reason, /记忆/); }); test('classifyWithRules routes memory recall to direct chat even in agent session', () => { const result = classifyWithRules({ text: '你记得我说想去哪儿吗', sessionId: '20260704_9', sessionMessageCount: 4, userMessage: { role: 'user', content: [{ type: 'text', text: '你记得我说想去哪儿吗' }], metadata: { displayText: '你记得我说想去哪儿吗' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.match(result.reason, /记忆/); }); test('classifyWithRules bypasses llm router when user selected summarize skill', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' }; }, }, memoryV2: { async resolve() { assert.fail('memory resolve should not run for explicit skill selection'); }, }, }); const prompt = '请总结以下内容,提炼核心结论、重点信息和可执行建议(条理清晰、中文输出):'; const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: `${prompt}这是待总结的正文` }], metadata: { displayText: `${prompt}这是待总结的正文`, memindRun: { selectedChatSkill: 'summarize' }, }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); assert.equal(result.reason, '用户已选择 skill'); assert.equal(llmCalls.length, 0); }); test('classifyWithRules bypasses llm router for explicit web skill prompt', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '请使用 web 技能:帮我搜索并查阅相关资料(优先官方文档),并给出中文摘要与来源链接。我的问题是:今天 AI 新闻', }], metadata: { displayText: '今天 AI 新闻' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'web'); assert.equal(llmCalls.length, 0); }); test('createChatIntentRouter classifies continued sessions through llm when enabled', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, modelProviderKeyId: 'key-router', model: 'deepseek-chat', llmProviderService: { async createChatCompletion(payload) { llmCalls.push(payload); return { ok: true, reply: JSON.stringify({ route: 'agent_orchestration', confidence: 0.88, reason: '用户要规划日本旅行,需要搜索和生成内容', suggested_skill: 'static-page-publish', agent_brief: '整理日本旅行攻略', }), }; }, }, memoryV2: { async resolve() { return { source: 'legacy-conversation-memory', memories: [{ label: 'goal', text: '用户想去日本旅游' }] }; }, }, }); const result = await router.classify({ userId: 'user-john', sessionId: '20260704_10', sessionMessageCount: 2, userMessage: { role: 'user', content: [{ type: 'text', text: '我想去日本玩' }], metadata: { displayText: '我想去日本玩' }, }, }); assert.equal(llmCalls.length, 1); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'llm'); assert.match(result.reason, /日本旅行/); assert.equal(result.memory?.itemsUsed ?? 0, 0); }); test('createChatIntentRouter skips memory resolve for general conversational analysis', async () => { const llmCalls = []; const resolveCalls = []; const router = createChatIntentRouter({ enabled: true, modelProviderKeyId: 'key-router', model: 'deepseek-chat', llmProviderService: { async createChatCompletion({ messages, providerKeyId, model, temperature }) { llmCalls.push({ messages, providerKeyId, model, temperature }); return { ok: true, providerKeyId, model, reply: JSON.stringify({ route: 'direct_chat', confidence: 0.91, reason: '用户在咨询概念,不需要执行工具', suggested_skill: null, agent_brief: '', }), }; }, }, memoryV2: { async resolve(input) { resolveCalls.push(input); return { source: 'pgvector', memories: [{ label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫' }], activeGoals: ['完善春季促销活动页'], }; }, }, }); const result = await router.classify({ userId: 'user-1', userMessage: { role: 'user', content: [{ type: 'text', text: 'React 19 有哪些新特性?' }], metadata: { displayText: 'React 19 有哪些新特性?' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'llm'); assert.equal(result.providerKeyId, 'key-router'); assert.equal(result.model, 'deepseek-chat'); assert.equal(result.memory?.itemsUsed ?? 0, 0); assert.equal(resolveCalls.length, 0); assert.equal(llmCalls.length, 1); assert.equal(llmCalls[0].providerKeyId, 'key-router'); assert.equal(llmCalls[0].model, 'deepseek-chat'); assert.equal(llmCalls[0].temperature, 0); assert.doesNotMatch(llmCalls[0].messages[1].content, /相关记忆/); }); test('createChatIntentRouter resolves light memory for recall questions', async () => { const resolveCalls = []; const router = createChatIntentRouter({ enabled: true, modelProviderKeyId: 'key-router', model: 'deepseek-chat', llmProviderService: { async createChatCompletion() { return { ok: true, reply: JSON.stringify({ route: 'direct_chat', confidence: 0.95, reason: '用户在询问个人记忆', suggested_skill: null, agent_brief: '', }), }; }, }, memoryV2: { async resolve(input) { resolveCalls.push(input); return { source: 'pgvector', memories: [{ label: 'goal', text: '用户想去日本旅游' }], }; }, }, }); const result = await router.classify({ userId: 'user-john', userMessage: { role: 'user', content: [{ type: 'text', text: '你记得我说想去哪儿吗?' }], metadata: { displayText: '你记得我说想去哪儿吗?' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(resolveCalls.length, 1); assert.equal(resolveCalls[0].limit, 3); assert.equal(result.memory?.itemsUsed, 1); }); test('buildRouterContext trims rich Memory V2 resolve payload for routing', () => { const context = buildRouterContext({ source: 'letta', memories: [ { label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫,尤其在工程排障时希望先给结论。' }, { label: 'habit', text: '用户经常要求生成 H5 活动页并发布到 MindSpace。' }, ], semanticMemories: ['上次正在修改活动页标题样式'], behaviorSummary: '用户倾向先本地验证,再决定是否发布生产。额外句子不应进入轻量摘要。', activeGoals: ['完善春季促销活动页'], profile: { ignored: true }, }); assert.equal(context.source, 'letta'); assert.equal(context.itemsUsed, 5); assert.match(context.content, /相关记忆/); assert.match(context.content, /行为摘要/); assert.match(context.content, /进行中目标/); assert.doesNotMatch(context.content, /ignored/); }); test('createChatIntentRouter fails open when Memory V2 resolve fails', async () => { const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion({ messages }) { assert.match(messages[1].content, /\[Router Context\]\n无/); return { ok: true, reply: JSON.stringify({ route: 'agent_orchestration', confidence: 0.82, reason: '需要工具执行', suggested_skill: 'web', agent_brief: '搜索并整理来源', }), }; }, }, memoryV2: { async resolve() { throw new Error('memory down'); }, }, logger: { warn() {} }, }); const result = await router.classify({ userId: 'user-1', userMessage: { role: 'user', content: [{ type: 'text', text: '你记得我之前说过什么吗?' }], metadata: { displayText: '你记得我之前说过什么吗?' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.memory.degraded, true); }); test('createManagedChatIntentRouter hot-loads admin config and stays closed by default', async () => { const states = [ { fingerprint: 'off', overrides: { MEMIND_CHAT_LLM_ROUTER_ENABLED: '0', }, }, { fingerprint: 'on', overrides: { MEMIND_CHAT_LLM_ROUTER_ENABLED: '1', MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID: 'key-router', MEMIND_CHAT_ROUTER_MODEL: 'deepseek-chat', }, }, ]; const llmCalls = []; const router = createManagedChatIntentRouter({ configService: { async getRuntimeState() { return states[0]; }, }, llmProviderService: { async createChatCompletion(input) { llmCalls.push(input); return { ok: true, providerKeyId: input.providerKeyId, model: input.model, reply: JSON.stringify({ route: 'direct_chat', confidence: 0.9, reason: '纯文字问答', suggested_skill: null, agent_brief: '', }), }; }, }, }); assert.equal(await router.isEnabled(), false); states.shift(); assert.equal(await router.isEnabled(), true); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }], metadata: { displayText: '帮我概括一下这段材料的要点' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(llmCalls[0].providerKeyId, 'key-router'); }); test('createChatIntentRouter escalates low-confidence direct chat to agent', async () => { const router = createChatIntentRouter({ enabled: true, minConfidence: 0.8, llmProviderService: { async createChatCompletion() { return { ok: true, reply: JSON.stringify({ route: 'direct_chat', confidence: 0.42, reason: '可能是简单问答', suggested_skill: null, agent_brief: '', }), }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我整理一下这段材料' }], metadata: { displayText: '帮我整理一下这段材料' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'threshold'); }); test('createChatIntentRouter falls back to agent when LLM fails', async () => { const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { return { ok: false, message: 'router model unavailable' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }], metadata: { displayText: '帮我概括一下这段材料的要点' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); }); test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => { let llmCalls = 0; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls += 1; return { ok: false, message: 'router model unavailable' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我查一下今天的 AI 新闻' }], metadata: { displayText: '帮我查一下今天的 AI 新闻' }, }, grantedSkills: ['web'], }); assert.equal(llmCalls, 0); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); }); test('applyAgentOrchestrationToUserMessage preserves displayText and adds task envelope', () => { const enriched = applyAgentOrchestrationToUserMessage( { role: 'user', content: [{ type: 'text', text: '帮我做一个页面' }], metadata: { displayText: '帮我做一个页面', userVisible: true }, }, { route: CHAT_INTENT_ROUTE.AGENT, confidence: 0.9, reason: '需要生成页面', suggestedSkill: 'static-page-publish', agentBrief: '生成 HTML 并发布', source: 'llm', }, { grantedSkills: ['static-page-publish'] }, ); assert.equal(enriched.metadata.displayText, '帮我做一个页面'); assert.match(enriched.content[0].text, /Memind 任务编排/); assert.match(enriched.content[0].text, /static-page-publish/); assert.match(enriched.content[0].text, /帮我做一个页面/); }); test('buildAgentOrchestrationAgentText includes execution brief', () => { const text = buildAgentOrchestrationAgentText({ displayText: '帮我搜索今天的新闻', classification: { reason: '需要实时搜索', suggestedSkill: 'web', agentBrief: '使用 web skill 搜索并整理来源', }, skillPrompt: '请使用 web 技能:', }); assert.match(text, /需要实时搜索/); assert.match(text, /帮我搜索今天的新闻/); assert.match(text, /请使用 web 技能/); }); test('classifyWithRules attaches normalized decision on every rule result', () => { const result = classifyWithRules({ text: '你好', userMessage: { role: 'user', content: [{ type: 'text', text: '你好' }], metadata: { displayText: '你好' }, }, }); assert.ok(result.decision); assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT); assert.equal(result.decision.mode, ROUTER_DECISION_MODE.SSE); assert.equal(result.decision.session_hint, ROUTER_DECISION_SESSION_HINT.NEW); }); test('buildRouterNormalizedDecision maps legacy routes and session hints', () => { const continued = buildRouterNormalizedDecision( { route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'web' }, { text: '继续', sessionId: '20260704_10', sessionMessageCount: 2, toolMode: 'chat', }, ); assert.equal(continued.route, ROUTER_DECISION_ROUTE.AGENT); assert.equal(continued.session_hint, ROUTER_DECISION_SESSION_HINT.REUSE); assert.ok(continued.flags.includes('selected_skill')); const freshAgentSession = buildRouterNormalizedDecision( { route: CHAT_INTENT_ROUTE.AGENT }, { text: '你好', sessionId: '20260704_11', sessionMessageCount: 0, }, ); assert.equal(freshAgentSession.session_hint, ROUTER_DECISION_SESSION_HINT.NEW); const memoryRecall = buildRouterNormalizedDecision( { route: CHAT_INTENT_ROUTE.DIRECT_CHAT }, { text: '你记得我说过什么吗' }, ); assert.equal(memoryRecall.route, ROUTER_DECISION_ROUTE.CHAT); assert.ok(memoryRecall.flags.includes('memory_recall')); }); test('resolveLegacyRouteFromClassification prefers decision when flag enabled', () => { const classification = { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, decision: { route: ROUTER_DECISION_ROUTE.AGENT }, }; assert.equal( resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.DIRECT_CHAT, ); const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1'; try { assert.equal( resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.AGENT, ); assert.equal(isNormalizedRouterDecisionEnabled(), true); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('resolveGatewayAgentSessionId honors session_hint without breaking hard rules', () => { const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1'; try { assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'sess-1', classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.REUSE }, }, }), 'sess-1', ); assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'sess-1', classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW }, }, }), null, ); assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'h5direct_abc', classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW }, }, }), 'h5direct_abc', ); assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'sess-1', forceDeepReasoning: true, classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW }, }, }), 'sess-1', ); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('resolveNormalizedRouterDecisionMode supports off, shadow, and on', () => { const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; try { delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; assert.equal(resolveNormalizedRouterDecisionMode(), 'off'); process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow'; assert.equal(resolveNormalizedRouterDecisionMode(), 'shadow'); assert.equal(isNormalizedRouterDecisionShadow(), true); assert.equal(isNormalizedRouterDecisionEnabled(), false); process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1'; assert.equal(resolveNormalizedRouterDecisionMode(), 'on'); assert.equal(isNormalizedRouterDecisionEnabled(), true); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('shadow mode keeps legacy route for gateway decisions', () => { const classification = { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, decision: { route: ROUTER_DECISION_ROUTE.AGENT }, }; const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow'; try { assert.equal(resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(isNormalizedRouterDecisionEnabled(), false); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('classifyWithRules fast-paths bedtime story when llm router is enabled', () => { const result = classifyWithRules({ text: '讲一个睡前故事吧', llmRouterEnabled: true, sessionId: '20260706_2', sessionMessageCount: 3, userMessage: { role: 'user', content: [{ type: 'text', text: '讲一个睡前故事吧' }], metadata: { displayText: '讲一个睡前故事吧' }, }, }); assert.equal(result?.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result?.source, 'rule'); assert.equal(result?.decision?.route, ROUTER_DECISION_ROUTE.CHAT); }); test('classifyWithRules fast-paths world cup standings to agent orchestration', () => { const text = '世界杯现在赛况如何'; assert.equal(isRealtimeInfoQuestion(text), true); const result = classifyWithRules({ text, llmRouterEnabled: true, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(result?.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result?.source, 'rule'); assert.equal(result?.suggestedSkill, 'web'); assert.match(result?.reason, /实时/); }); test('createChatIntentRouter fast-paths world cup without calling router LLM', async () => { let llmCalls = 0; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls += 1; throw new Error('router should not be called'); }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }], metadata: { displayText: '世界杯现在赛况如何' }, }, grantedSkills: ['web'], }); assert.equal(llmCalls, 0); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); }); test('resolveChatIntentRouterPolicy coerces direct_chat fallback to agent', () => { const policy = resolveChatIntentRouterPolicy({ env: { MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat', }, }); assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.AGENT); }); test('coerceRealtimeWebSkill replaces search with web for world cup query', () => { const coerced = coerceRealtimeWebSkill( { route: CHAT_INTENT_ROUTE.AGENT, confidence: 0.9, reason: '用户询问实时赛况,需要搜索最新信息', suggestedSkill: 'search', source: 'llm', }, '世界杯现在赛况如何', { grantedSkills: ['web', 'search'] }, ); assert.equal(coerced.suggestedSkill, 'web'); assert.match(coerced.agentBrief, /web_search/); }); test('applyAgentOrchestrationToUserMessage uses web news prompt for realtime query', () => { const enriched = applyAgentOrchestrationToUserMessage( { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }], metadata: { displayText: '世界杯现在赛况如何', userVisible: true }, }, coerceRealtimeWebSkill( { route: CHAT_INTENT_ROUTE.AGENT, confidence: 0.9, reason: '需要搜索实时资料', suggestedSkill: 'web', source: 'rule', }, '世界杯现在赛况如何', { grantedSkills: ['web'] }, ), { grantedSkills: ['web'] }, ); assert.match(enriched.content[0].text, /请使用 web 技能:先搜索今天\/最新相关的新闻与热点/); assert.doesNotMatch(enriched.content[0].text, /search 技能:帮我在工作区中查找代码或文件/); assert.match(enriched.content[0].text, /web_search/); }); test('createChatIntentRouter coerces LLM search route to web for realtime query', async () => { const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { return { ok: true, reply: JSON.stringify({ route: 'agent_orchestration', confidence: 0.92, reason: '用户询问实时赛况,需要搜索最新信息', suggested_skill: 'search', agent_brief: '搜索赛况', }), }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }], metadata: { displayText: '世界杯现在赛况如何' }, }, grantedSkills: ['web', 'search'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'web'); assert.match(result.agentBrief, /web_search/); }); test('createChatIntentRouter timeout fallback prefers agent even when policy requests direct_chat', async () => { const router = createChatIntentRouter({ enabled: true, timeoutMs: 5, fallbackRoute: CHAT_INTENT_ROUTE.DIRECT_CHAT, llmProviderService: { async createChatCompletion() { await new Promise((resolve) => setTimeout(resolve, 20)); return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x","suggested_skill":null,"agent_brief":""}' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我整理一下这段材料' }], metadata: { displayText: '帮我整理一下这段材料' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); }); test('logRouterDecisionShadow emits payload only in shadow mode', () => { const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; const lines = []; const logger = { log: (line) => lines.push(line) }; const classification = { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, source: 'llm', reason: '纯文字', decision: { route: ROUTER_DECISION_ROUTE.AGENT, mode: ROUTER_DECISION_MODE.SSE, session_hint: ROUTER_DECISION_SESSION_HINT.REUSE, flags: [], }, }; try { process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '0'; logRouterDecisionShadow(classification, { logger, requestId: 'req-1' }); assert.equal(lines.length, 0); process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow'; logRouterDecisionShadow(classification, { logger, requestId: 'req-1' }); assert.equal(lines.length, 1); assert.match(lines[0], /\[router-shadow\]/); assert.match(lines[0], /wouldChangeRoute":true/); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('createChatIntentRouter accepts chat and agent route synonyms from llm', async () => { const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { return { ok: true, reply: JSON.stringify({ route: 'chat', confidence: 0.92, reason: '纯问答', suggested_skill: null, agent_brief: '', }), }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '什么是 SSE?' }], metadata: { displayText: '什么是 SSE?' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT); });