feat: LLM intent router, direct chat execution, and Memory V2 light intervention

Wire chat intent routing with direct_chat on regular sessions, skill-selected
short-circuit to Agent, memory light/heavy intervention tiers, and fix direct
chat UI stuck streaming after completion.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-04 22:32:57 +08:00
parent bfb6356f7d
commit e45c9300bf
33 changed files with 3412 additions and 105 deletions
+772
View File
@@ -0,0 +1,772 @@
import {
buildChatSkillPrompt,
extractSelectedChatSkillName,
hasExplicitChatSkillPrompt,
} from './chat-skills.mjs';
import { isDirectChatSessionId } from './direct-chat-service.mjs';
import {
memoryLimitForIntervention,
resolveMemoryInterventionMode,
} from './memory-intervention.mjs';
export const CHAT_INTENT_ROUTE = {
DIRECT_CHAT: 'direct_chat',
AGENT: 'agent_orchestration',
};
const AGENT_ORCHESTRATION_HEADER = '【Memind 任务编排】';
const SKILL_PROMPT_KEYS = {
web: 'web',
search: 'search',
'static-page-publish': 'generate-page',
'form-builder': 'form-builder',
'table-viewer': 'table-viewer',
'product-campaign-page': 'product-campaign-page',
};
const OBVIOUS_AGENT_PATTERNS = [
/\bpublic\/[^\s"'<>]+\.html\b/i,
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
/(?:生成|创建|制作|做|写|设计|发布).{0,24}(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:H5|h5|HTML|html|网页|页面).{0,24}(?:生成|创建|制作|做|写|设计|发布)/u,
];
const OBVIOUS_DIRECT_PATTERNS = [
/^(?:你好|您好|在吗|在不在|嗨|hi|hello|hey)[!!。.\s]*$/iu,
/^(?:测试\s*\d*|test\s*\d*)[!!。.\s]*$/iu,
/^[?]+$/u,
];
const MEMORY_RECALL_PATTERNS = [
/你(?:还)?记得(?:我)?/u,
/(?:有没有|是否).{0,6}记住/u,
/你对(?:我)?(?:的)?记忆/u,
/(?:我|之前).{0,16}(?:说过|提到|聊过|告诉)/u,
/(?:我的|之前的)(?:记忆|偏好|计划|目标|想法)/u,
/之前(?:说|提|聊)(?:过|的)/u,
];
export function isMemoryRecallQuestion(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return MEMORY_RECALL_PATTERNS.some((pattern) => pattern.test(normalized));
}
const DEFAULT_ROUTER_TIMEOUT_MS = 1500;
const DEFAULT_ROUTER_MEMORY_LIMIT = 8;
const DEFAULT_ROUTER_MIN_CONFIDENCE = 0.55;
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
function boundedNumber(value, fallback, { min = 0, max = Number.POSITIVE_INFINITY } = {}) {
if (value == null || value === '') return fallback;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
return Math.min(max, Math.max(min, parsed));
}
function pickDefined(source, keys) {
return Object.fromEntries(
keys
.filter((key) => source[key] !== undefined)
.map((key) => [key, source[key]]),
);
}
function truncateText(value, maxLength) {
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
if (!text) return '';
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
}
function firstSentence(value, maxLength) {
const text = truncateText(value, maxLength);
if (!text) return '';
const match = text.match(/^(.{12,}?[。.!?])/u);
return truncateText(match?.[1] ?? text, maxLength);
}
function normalizeMemoryText(item) {
if (typeof item === 'string') return { label: null, text: item };
const text = item?.text ?? item?.memory_text ?? item?.memoryText ?? item?.content ?? item?.summary ?? '';
const label = String(item?.label ?? item?.type ?? '').trim() || null;
return { label, text };
}
function withTimeout(promise, timeoutMs, label) {
const timeout = Number(timeoutMs);
if (!Number.isFinite(timeout) || timeout <= 0) return promise;
let timer = null;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${label} timed out after ${timeout}ms`);
err.code = 'CHAT_INTENT_ROUTER_TIMEOUT';
reject(err);
}, timeout);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timer) clearTimeout(timer);
});
}
function messageDisplayText(message) {
const displayText = message?.metadata?.displayText;
if (typeof displayText === 'string' && displayText.trim()) return displayText.trim();
return extractMessageText(message);
}
function extractMessageText(message) {
const content = message?.content;
if (typeof content === 'string') return content.trim();
if (!Array.isArray(content)) return String(message?.text ?? message?.value ?? '').trim();
return content
.map((item) => {
if (typeof item === 'string') return item;
if (item?.type === 'text') return item.text ?? '';
return '';
})
.join('\n')
.trim();
}
function isTextOnlyUserMessage(message) {
const content = message?.content;
if (!Array.isArray(content)) return Boolean(extractMessageText(message));
if (content.length === 0) return false;
return content.every((item) => {
if (typeof item === 'string') return true;
return item?.type === 'text';
});
}
function readSelectedChatSkillId(message) {
const selected =
message?.metadata?.memindRun?.selectedChatSkill ??
message?.metadata?.selectedChatSkill;
return typeof selected === 'string' && selected.trim() ? selected.trim() : null;
}
function messageHasExplicitChatSkillSelection(message) {
if (!message) return false;
if (readSelectedChatSkillId(message)) return true;
return [extractMessageText(message), messageDisplayText(message)].some((text) =>
hasExplicitChatSkillPrompt(text),
);
}
function buildSkillSelectionClassification(userMessage) {
const selectedSkillId = readSelectedChatSkillId(userMessage);
const suggestedSkill =
selectedSkillId ??
extractSelectedChatSkillName(extractMessageText(userMessage)) ??
extractSelectedChatSkillName(messageDisplayText(userMessage));
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: '用户已选择 skill',
suggested_skill: suggestedSkill,
agent_brief: suggestedSkill ? `使用 ${suggestedSkill} 执行用户任务` : '执行用户选择的 skill 任务',
}, { source: 'rule' });
}
function buildRouterSystemPrompt(grantedSkills = []) {
const skills = Array.isArray(grantedSkills) ? grantedSkills.filter(Boolean) : [];
return [
'你是 TKMind H5 聊天意图路由器,只负责判断用户消息应该走哪条处理通道。',
'',
'系统有两条通道:',
'1. direct_chat — 纯文字交流:问答、解释、闲聊、总结已有文字、给建议,不需要写文件、生成页面、调工具。',
'2. agent_orchestration — 任务编排 Agent:需要实际执行并产出结果的任务,例如生成/修改 HTML 页面、发布到 MindSpace、搜索实时资料、写代码改仓库、表单收集、数据表格、商品页、docx/长图导出等。',
'',
'判断原则:',
'- 用户只要文字回答,不要求“做出来/发布/生成链接/改文件” → direct_chat',
'- 用户要产出可访问页面、文件、链接,或需要工具/skills → agent_orchestration',
'- 不确定时优先 agent_orchestration,避免漏执行',
'- 记忆线索只用于辅助判断本轮意图,不能替用户扩写新需求',
'',
skills.length ? `当前用户已授权 skills${skills.join(', ')}` : '当前用户未授权额外 skills。',
skills.length
? '若走 agent_orchestration,可在 suggested_skill 中填写最匹配的 skill 名称(须来自上述列表),否则填 null。'
: 'suggested_skill 通常填 null。',
'',
'只输出 JSON,不要 markdown,不要解释:',
'{"route":"direct_chat|agent_orchestration","confidence":0.0,"reason":"一句话","suggested_skill":null,"agent_brief":"给 Agent 的执行要点,direct_chat 时可为空"}',
].join('\n');
}
export function buildRouterContext(resolveResult, {
memoryLimit = 5,
semanticLimit = 3,
goalLimit = 2,
memoryTextLimit = 80,
semanticTextLimit = 60,
goalTextLimit = 60,
behaviorTextLimit = 100,
} = {}) {
const result = resolveResult && typeof resolveResult === 'object' ? resolveResult : {};
const lines = [];
let itemsUsed = 0;
const memories = Array.isArray(result.memories) ? result.memories : [];
const memoryLines = memories
.map((item) => normalizeMemoryText(item))
.map(({ label, text }) => {
const clipped = truncateText(text, memoryTextLimit);
if (!clipped) return '';
return `- ${label ? `[${label}] ` : ''}${clipped}`;
})
.filter(Boolean)
.slice(0, memoryLimit);
if (memoryLines.length) {
lines.push('相关记忆:', ...memoryLines);
itemsUsed += memoryLines.length;
}
const semanticLines = (Array.isArray(result.semanticMemories) ? result.semanticMemories : [])
.map((item) => (typeof item === 'string' ? item : normalizeMemoryText(item).text))
.map((text) => truncateText(text, semanticTextLimit))
.filter(Boolean)
.slice(0, semanticLimit)
.map((text) => `- ${text}`);
if (semanticLines.length) {
lines.push('语义线索:', ...semanticLines);
itemsUsed += semanticLines.length;
}
const behaviorSummary = firstSentence(result.behaviorSummary, behaviorTextLimit);
if (behaviorSummary) {
lines.push(`行为摘要:${behaviorSummary}`);
itemsUsed += 1;
}
const goals = Array.isArray(result.activeGoals) && result.activeGoals.length
? result.activeGoals
: (Array.isArray(result.contextGoals) ? result.contextGoals : []);
const goalLines = goals
.map((text) => truncateText(text, goalTextLimit))
.filter(Boolean)
.slice(0, goalLimit)
.map((text) => `- ${text}`);
if (goalLines.length) {
lines.push('进行中目标:', ...goalLines);
itemsUsed += goalLines.length;
}
return {
content: lines.join('\n').trim(),
itemsUsed,
source: result.source ?? null,
degraded: Boolean(result.degraded),
skipped: Boolean(result.skipped),
reason: result.reason ?? null,
};
}
function buildRouterUserPrompt({ text, routerContext }) {
const context = String(routerContext ?? '').trim() || '无';
return [
'[Router Context]',
context,
'',
'[User]',
text || '(empty)',
].join('\n');
}
function parseRouterJson(reply) {
const text = String(reply ?? '').trim();
if (!text) return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = (fenced?.[1] ?? text).trim();
try {
return JSON.parse(candidate);
} catch {
const start = candidate.indexOf('{');
const end = candidate.lastIndexOf('}');
if (start < 0 || end <= start) return null;
try {
return JSON.parse(candidate.slice(start, end + 1));
} catch {
return null;
}
}
}
function normalizeRoute(value) {
const normalized = String(value ?? '').trim().toLowerCase();
if (normalized === CHAT_INTENT_ROUTE.DIRECT_CHAT) return CHAT_INTENT_ROUTE.DIRECT_CHAT;
if (normalized === CHAT_INTENT_ROUTE.AGENT || normalized === 'agent') return CHAT_INTENT_ROUTE.AGENT;
return null;
}
export function resolveChatIntentRouterPolicy({ env = process.env, overrides = {} } = {}) {
const fallbackRoute = normalizeRoute(env?.MEMIND_CHAT_ROUTER_FALLBACK_ROUTE) ?? CHAT_INTENT_ROUTE.AGENT;
return {
enabled: envFlag(env?.MEMIND_CHAT_LLM_ROUTER_ENABLED, false),
modelProviderKeyId: String(env?.MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID ?? '').trim() || null,
model: String(env?.MEMIND_CHAT_ROUTER_MODEL ?? '').trim() || null,
modelApiType: String(env?.MEMIND_CHAT_ROUTER_MODEL_API ?? '').trim() || 'chat',
temperature: boundedNumber(env?.MEMIND_CHAT_ROUTER_TEMPERATURE, 0, { min: 0, max: 2 }),
minConfidence: boundedNumber(
env?.MEMIND_CHAT_ROUTER_MIN_CONFIDENCE,
DEFAULT_ROUTER_MIN_CONFIDENCE,
{ min: 0, max: 1 },
),
memoryResolveEnabled: envFlag(env?.MEMIND_CHAT_ROUTER_MEMORY_ENABLED, true),
memoryResolveLimit: Math.round(boundedNumber(
env?.MEMIND_CHAT_ROUTER_MEMORY_LIMIT,
DEFAULT_ROUTER_MEMORY_LIMIT,
{ min: 1, max: 50 },
)),
timeoutMs: Math.round(boundedNumber(
env?.MEMIND_CHAT_ROUTER_TIMEOUT_MS,
DEFAULT_ROUTER_TIMEOUT_MS,
{ min: 0, max: 30_000 },
)),
fallbackRoute,
...overrides,
};
}
function normalizeClassification(raw, { source, fallbackRoute = CHAT_INTENT_ROUTE.AGENT } = {}) {
const route = normalizeRoute(raw?.route) ?? fallbackRoute;
const confidenceRaw = Number(raw?.confidence);
const confidence = Number.isFinite(confidenceRaw)
? Math.min(1, Math.max(0, confidenceRaw))
: source === 'llm' ? 0.7 : 1;
const suggestedSkill = String(raw?.suggested_skill ?? raw?.suggestedSkill ?? '').trim() || null;
const agentBrief = String(raw?.agent_brief ?? raw?.agentBrief ?? '').trim();
const reason = String(raw?.reason ?? '').trim() || (source === 'llm' ? '模型路由判定' : '规则路由判定');
return {
route,
confidence,
reason,
suggestedSkill,
agentBrief,
source,
providerKeyId: raw?.providerKeyId ?? raw?.provider_key_id ?? null,
model: raw?.model ?? null,
memory: raw?.memory ?? null,
};
}
function resolveSkillPrompt(suggestedSkill, grantedSkills = []) {
const skillName = String(suggestedSkill ?? '').trim();
if (!skillName) return '';
if (grantedSkills.length > 0 && !grantedSkills.includes(skillName)) return '';
const promptKey = SKILL_PROMPT_KEYS[skillName];
if (!promptKey) return '';
return buildChatSkillPrompt(promptKey, skillName);
}
export function buildAgentOrchestrationAgentText({
displayText,
classification,
skillPrompt = '',
}) {
const taskBody = String(displayText ?? '').trim();
const lines = [
`${AGENT_ORCHESTRATION_HEADER}以下为用户任务,请使用工具与技能实际执行并产出结果,不要只做文字描述。`,
`路由判定:${classification.reason}`,
classification.agentBrief ? `执行要点:${classification.agentBrief}` : '',
classification.suggestedSkill ? `建议 skill${classification.suggestedSkill}` : '',
skillPrompt,
'',
'用户任务:',
taskBody,
].filter((line, index, all) => line !== '' || index === all.length - 2);
return lines.join('\n');
}
export function applyAgentOrchestrationToUserMessage(userMessage, classification, { grantedSkills = [] } = {}) {
const displayText = messageDisplayText(userMessage);
const skillPrompt = resolveSkillPrompt(classification?.suggestedSkill, grantedSkills);
const agentText = buildAgentOrchestrationAgentText({
displayText,
classification,
skillPrompt,
});
const content = Array.isArray(userMessage?.content)
? userMessage.content.map((item, index) => {
if (index !== 0) return item;
if (typeof item === 'string') return agentText;
if (item?.type === 'text') return { ...item, text: agentText };
return item;
})
: [{ type: 'text', text: agentText }];
const metadata = {
...(userMessage?.metadata ?? {}),
displayText: displayText || undefined,
chatIntentRoute: CHAT_INTENT_ROUTE.AGENT,
chatIntentSource: classification?.source ?? null,
};
return {
...userMessage,
content,
metadata,
};
}
function hasPriorAgentConversation(sessionId, sessionMessageCount) {
if (!sessionId || isDirectChatSessionId(sessionId)) return false;
if (sessionMessageCount == null) return true;
return Number(sessionMessageCount) > 0;
}
export function classifyWithRules({
text,
forceDeepReasoning = false,
toolMode = 'chat',
sessionId = null,
sessionMessageCount = null,
userMessage = null,
includeIntentPatterns = true,
llmRouterEnabled = false,
} = {}) {
const normalized = String(text ?? '').trim();
if (forceDeepReasoning || toolMode !== 'chat') {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: forceDeepReasoning ? '用户开启深度推理' : '代码任务模式',
}, { source: 'rule' });
}
if (userMessage && messageHasExplicitChatSkillSelection(userMessage)) {
return buildSkillSelectionClassification(userMessage);
}
if (userMessage && !isTextOnlyUserMessage(userMessage)) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: '消息包含非文本内容',
}, { source: 'rule' });
}
if (
!llmRouterEnabled &&
includeIntentPatterns &&
normalized &&
isMemoryRecallQuestion(normalized)
) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
confidence: 0.98,
reason: '用户在询问个人记忆或历史对话',
}, { source: 'rule' });
}
if (!llmRouterEnabled && hasPriorAgentConversation(sessionId, sessionMessageCount)) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: '延续已有 Agent 会话',
}, { source: 'rule' });
}
if (
includeIntentPatterns &&
normalized &&
OBVIOUS_AGENT_PATTERNS.some((pattern) => pattern.test(normalized))
) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 0.95,
reason: '明确需要生成或发布页面/文件',
suggested_skill: 'static-page-publish',
agent_brief: '生成或更新 MindSpace 公开页面,并返回可访问链接。',
}, { source: 'rule' });
}
if (
includeIntentPatterns &&
normalized &&
OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized))
) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
confidence: 0.95,
reason: '简单寒暄或连通性测试',
}, { source: 'rule' });
}
return null;
}
export function createChatIntentRouter(options = {}) {
const {
llmProviderService,
memoryV2 = null,
env = process.env,
logger = console,
} = options;
const policy = options.policy ?? resolveChatIntentRouterPolicy({
env,
overrides: pickDefined(options, [
'enabled',
'modelProviderKeyId',
'model',
'modelApiType',
'temperature',
'minConfidence',
'memoryResolveEnabled',
'memoryResolveLimit',
'timeoutMs',
'fallbackRoute',
]),
});
function getStatus() {
return {
enabled: Boolean(policy.enabled),
modelProviderKeyId: policy.modelProviderKeyId ?? null,
model: policy.model ?? null,
modelApiType: policy.modelApiType ?? 'chat',
minConfidence: policy.minConfidence,
memoryResolveEnabled: Boolean(policy.memoryResolveEnabled),
memoryResolveLimit: policy.memoryResolveLimit,
timeoutMs: policy.timeoutMs,
fallbackRoute: policy.fallbackRoute,
};
}
function isEnabled() {
return Boolean(policy.enabled && llmProviderService?.createChatCompletion);
}
async function resolveRouterContext({ userId, sessionId, text, forceDeepReasoning = false }) {
const intervention = resolveMemoryInterventionMode({
forceDeepReasoning,
recallQuestion: isMemoryRecallQuestion(text),
});
const limit = memoryLimitForIntervention(intervention, { context: 'router' });
if (
limit <= 0 ||
!policy.memoryResolveEnabled ||
!memoryV2?.resolve ||
!userId
) {
return buildRouterContext(null);
}
try {
const resolved = await withTimeout(
memoryV2.resolve({
userId,
sessionId,
query: text,
limit,
}),
policy.timeoutMs,
'Memory V2 router resolve',
);
return buildRouterContext(resolved);
} catch (err) {
logger?.warn?.(
`[chat-intent-router] memory resolve skipped: ${err instanceof Error ? err.message : err}`,
);
return buildRouterContext({
degraded: true,
reason: 'memory_resolve_failed',
});
}
}
async function classify({
userId = null,
userMessage,
sessionId = null,
sessionMessageCount = null,
toolMode = 'chat',
forceDeepReasoning = false,
grantedSkills = [],
} = {}) {
const text = messageDisplayText(userMessage);
const ruleResult = classifyWithRules({
text,
forceDeepReasoning,
toolMode,
sessionId,
sessionMessageCount,
userMessage,
includeIntentPatterns: false,
llmRouterEnabled: Boolean(policy.enabled),
});
if (ruleResult) return ruleResult;
if (!isEnabled()) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0.5,
reason: '意图路由未启用,走默认通道',
}, { source: 'fallback' });
}
const routerContext = await resolveRouterContext({
userId,
sessionId,
text,
forceDeepReasoning,
});
let completion = null;
try {
completion = await withTimeout(
llmProviderService.createChatCompletion({
providerKeyId: policy.modelProviderKeyId || undefined,
model: policy.model || undefined,
modelApiType: policy.modelApiType || undefined,
temperature: policy.temperature,
messages: [
{ role: 'system', content: buildRouterSystemPrompt(grantedSkills) },
{
role: 'user',
content: buildRouterUserPrompt({
text,
routerContext: routerContext.content,
}),
},
],
}),
policy.timeoutMs,
'Chat intent router',
);
} catch (err) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: err instanceof Error ? err.message : '意图路由失败,走默认通道',
memory: routerContext,
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute });
}
if (!completion?.ok) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: completion?.message ?? '意图路由失败,走默认通道',
memory: routerContext,
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute });
}
const parsed = parseRouterJson(completion.reply);
if (!parsed) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: '意图路由响应无法解析,走默认通道',
memory: routerContext,
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute });
}
const classification = {
...normalizeClassification(parsed, { source: 'llm', fallbackRoute: policy.fallbackRoute }),
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId ?? null,
model: completion.model ?? policy.model ?? null,
memory: routerContext,
};
if (
classification.route === CHAT_INTENT_ROUTE.DIRECT_CHAT &&
classification.confidence < policy.minConfidence
) {
return normalizeClassification({
...classification,
route: policy.fallbackRoute,
reason: `${classification.reason}(置信度 ${classification.confidence} 低于阈值,走默认通道)`,
}, { source: 'threshold', fallbackRoute: policy.fallbackRoute });
}
return classification;
}
return {
getStatus,
isEnabled,
classify,
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
};
}
export function createManagedChatIntentRouter({
llmProviderService,
memoryV2 = null,
configService = null,
env = process.env,
logger = console,
} = {}) {
let activeRouter = null;
let activeFingerprint = null;
let activeMeta = { source: 'env', updatedAt: null, updatedBy: null, configError: null };
async function loadRouterState() {
if (!configService?.getRuntimeState) {
return {
source: 'env',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-only',
effectiveEnv: { ...env },
configError: null,
};
}
try {
const state = await configService.getRuntimeState();
return {
source: state?.source ?? 'admin-db',
updatedAt: state?.updatedAt ?? null,
updatedBy: state?.updatedBy ?? null,
fingerprint: state?.fingerprint ?? `admin-db:${Date.now()}`,
effectiveEnv: { ...env, ...(state?.overrides ?? {}) },
configError: null,
};
} catch (err) {
logger?.warn?.(
`[chat-intent-router] admin config unavailable, using process env: ${err instanceof Error ? err.message : err}`,
);
return {
source: 'env-fallback',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-fallback',
effectiveEnv: { ...env },
configError: err instanceof Error ? err.message : String(err),
};
}
}
async function ensureRouter() {
const state = await loadRouterState();
if (activeRouter && activeFingerprint === state.fingerprint) {
activeMeta = state;
return activeRouter;
}
activeRouter = createChatIntentRouter({
llmProviderService,
memoryV2,
env: state.effectiveEnv,
logger,
});
activeFingerprint = state.fingerprint;
activeMeta = state;
return activeRouter;
}
return {
async getStatus() {
const router = await ensureRouter();
return {
...router.getStatus(),
configSource: activeMeta.source,
configUpdatedAt: activeMeta.updatedAt,
configUpdatedBy: activeMeta.updatedBy,
configError: activeMeta.configError,
};
},
async isEnabled() {
const router = await ensureRouter();
return router.isEnabled();
},
async classify(input = {}) {
const router = await ensureRouter();
return router.classify(input);
},
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
};
}