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
+87 -4
View File
@@ -173,6 +173,13 @@ function withRunMetadata(userMessage, { toolMode = 'chat', taskType = null, forc
return { ...message, metadata };
}
function normalizeSessionMessageCount(value) {
if (value == null || value === '') return null;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) return null;
return Math.floor(parsed);
}
function getRunOptionsFromMessage(userMessage) {
const metadata = userMessage?.metadata;
const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
@@ -187,6 +194,9 @@ function getRunOptionsFromMessage(userMessage) {
taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType),
forceDeepReasoning: runMetadata?.forceDeepReasoning === true || metadata?.forceDeepReasoning === true,
validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation),
sessionMessageCount: normalizeSessionMessageCount(
runMetadata?.sessionMessageCount ?? metadata?.sessionMessageCount,
),
};
}
@@ -213,6 +223,7 @@ export function createAgentRunGateway({
tkmindProxy,
toolGateway = null,
directChatService = null,
chatIntentRouter = null,
retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS,
autoDispatch = envFlag(process.env.MEMIND_AGENT_RUN_AUTODISPATCH, true),
maxConcurrentRuns = positiveInteger(
@@ -387,21 +398,83 @@ export function createAgentRunGateway({
}
}
async function resolveGrantedSkills(userId) {
if (!userId) return [];
if (userAuth?.getUserSkills) {
const skills = await userAuth.getUserSkills(userId).catch(() => null);
if (Array.isArray(skills?.grantedSkills)) return skills.grantedSkills;
}
if (!userAuth?.getUserCapabilities) return [];
const caps = await userAuth.getUserCapabilities(userId).catch(() => null);
return Array.isArray(caps?.grantedSkills) ? caps.grantedSkills : [];
}
async function resolveRunRouting(row, userMessage, runOptions) {
if (!chatIntentRouter?.classify) return null;
const enabled = chatIntentRouter.isEnabled
? await Promise.resolve(chatIntentRouter.isEnabled()).catch(() => false)
: true;
if (!enabled) return null;
const grantedSkills = await resolveGrantedSkills(row.user_id);
return chatIntentRouter.classify({
userId: row.user_id,
userMessage,
sessionId: row.agent_session_id ?? null,
sessionMessageCount: runOptions.sessionMessageCount,
toolMode: runOptions.toolMode,
forceDeepReasoning: runOptions.forceDeepReasoning,
grantedSkills,
});
}
async function executeRun(row, runId) {
const userMessage = safeJsonParse(row.user_message_json, {});
let userMessage = safeJsonParse(row.user_message_json, {});
const runOptions = getRunOptionsFromMessage(userMessage);
const toolGatewayStatus = toolGateway?.getStatus ? toolGateway.getStatus() : null;
if (!runOptions.forceDeepReasoning && directChatService?.canHandle?.({
const routing = await resolveRunRouting(row, userMessage, runOptions);
if (routing) {
await appendEvent(runId, 'intent_routed', routing);
if (routing.route === 'agent_orchestration' && chatIntentRouter?.applyAgentOrchestration) {
const grantedSkills = await resolveGrantedSkills(row.user_id);
userMessage = chatIntentRouter.applyAgentOrchestration(userMessage, routing, { grantedSkills });
}
}
const routingDecision = routing?.route ?? null;
const preferDirectChat =
routingDecision === 'direct_chat' ||
(isDirectChatSessionId(row.agent_session_id ?? null) && !runOptions.forceDeepReasoning);
const directChatInput = {
sessionId: row.agent_session_id ?? null,
toolMode: runOptions.toolMode,
userMessage,
})) {
routingDecision,
};
const canDirectChat = Boolean(
preferDirectChat && directChatService?.canHandle?.(directChatInput),
);
if (preferDirectChat && !canDirectChat) {
const rejection = directChatService?.explainCanHandle?.(directChatInput);
await appendEvent(runId, 'direct_chat_skipped', {
sessionId: row.agent_session_id ?? null,
routingDecision,
reason: rejection?.reason ?? 'can_handle_false',
});
}
if (canDirectChat) {
try {
const result = await directChatService.run({
userId: row.user_id,
sessionId: row.agent_session_id ?? null,
requestId: row.request_id,
userMessage,
routingDecision,
routingMemory: routing?.memory ?? null,
onSessionReady: async (activeSessionId) => {
await pool.query(
`UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
[activeSessionId, nowMs(), runId],
);
},
});
await appendEvent(runId, 'direct_chat_completed', {
sessionId: result.sessionId,
@@ -495,7 +568,10 @@ export function createAgentRunGateway({
sessionId,
row.request_id,
userMessage,
{ toolMode: runOptions.toolMode },
{
toolMode: runOptions.toolMode,
forceDeepReasoning: runOptions.forceDeepReasoning,
},
);
return { sessionId };
}
@@ -610,6 +686,12 @@ export function createAgentRunGateway({
) h ON h.run_id = r.id
WHERE r.status = 'running' AND h.latest_heartbeat_at IS NULL`,
);
const chatIntentRouterStatus = chatIntentRouter?.getStatus
? await Promise.resolve(chatIntentRouter.getStatus()).catch((err) => ({
enabled: false,
error: err instanceof Error ? err.message : String(err),
}))
: null;
return {
autoDispatch,
maxConcurrentRuns,
@@ -635,6 +717,7 @@ export function createAgentRunGateway({
terminalStatuses: [...TERMINAL_STATUSES],
toolGateway: toolGateway?.getStatus ? toolGateway.getStatus() : null,
directChat: directChatService?.getStatus ? directChatService.getStatus() : null,
chatIntentRouter: chatIntentRouterStatus,
};
}