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:
+87
-4
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user