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
+258 -6
View File
@@ -258,20 +258,36 @@ test('agent run starts a session and marks submitted reply as succeeded', async
test('agent run uses direct chat service for eligible chat messages', async () => {
const pool = createFakePool();
const directRuns = [];
const submitted = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
assert.fail('backend session should not start for direct chat');
return { id: 'agent-session-should-not-run' };
},
async submitSessionReplyForUser() {
assert.fail('backend reply should not be submitted for direct chat');
async submitSessionReplyForUser(...args) {
submitted.push(args);
},
},
chatIntentRouter: {
isEnabled() {
return true;
},
async classify() {
return {
route: 'direct_chat',
confidence: 0.92,
reason: '普通问候',
source: 'llm',
};
},
},
directChatService: {
canHandle({ toolMode, userMessage }) {
return toolMode === 'chat' && userMessage?.content?.[0]?.text === 'hi';
canHandle({ toolMode, userMessage, routingDecision }) {
return toolMode === 'chat'
&& routingDecision === 'direct_chat'
&& userMessage?.content?.[0]?.text === 'hi';
},
async run(input) {
directRuns.push(input);
@@ -297,10 +313,246 @@ test('agent run uses direct chat service for eligible chat messages', async () =
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(pool.runs.get(run.id).agent_session_id, 'h5direct_session-1');
assert.equal(directRuns.length, 1);
assert.equal(submitted.length, 0);
assert.equal(directRuns[0].requestId, 'req-direct');
assert.ok(pool.events.some((event) => event.eventType === 'direct_chat_completed'));
});
test('agent run uses direct chat on regular agent sessions when llm routes direct_chat', async () => {
const pool = createFakePool();
const directRuns = [];
const submitted = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async submitSessionReplyForUser(...args) {
submitted.push(args);
},
},
chatIntentRouter: {
isEnabled() {
return true;
},
async classify() {
return {
route: 'direct_chat',
confidence: 0.95,
reason: '问答',
source: 'llm',
};
},
},
directChatService: {
canHandle({ routingDecision }) {
return routingDecision === 'direct_chat';
},
explainCanHandle({ routingDecision }) {
return { ok: routingDecision === 'direct_chat', reason: null };
},
async run(input) {
directRuns.push(input);
return {
sessionId: input.sessionId ?? '20260704_11',
providerId: 'custom_deepseek',
model: 'deepseek-chat',
billing: { ok: true },
};
},
getStatus() {
return { enabled: true };
},
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
sessionId: '20260704_11',
requestId: 'req-taihu',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '我想对太湖有更多的了解' }],
metadata: { displayText: '我想对太湖有更多的了解' },
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(pool.runs.get(run.id).agent_session_id, '20260704_11');
assert.equal(directRuns.length, 1);
assert.equal(submitted.length, 0);
assert.ok(pool.events.some((event) => event.eventType === 'direct_chat_completed'));
});
test('agent run records direct_chat_skipped when execution is unavailable', async () => {
const pool = createFakePool();
const submitted = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async submitSessionReplyForUser(...args) {
submitted.push(args);
},
},
chatIntentRouter: {
isEnabled() {
return true;
},
async classify() {
return {
route: 'direct_chat',
confidence: 0.95,
reason: '问答',
source: 'llm',
};
},
},
directChatService: {
canHandle() {
return false;
},
explainCanHandle() {
return { ok: false, reason: 'session_not_direct_chat' };
},
getStatus() {
return { enabled: true };
},
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
sessionId: '20260704_11',
requestId: 'req-skipped',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你好' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(submitted.length, 1);
const skipped = pool.events.find((event) => event.eventType === 'direct_chat_skipped');
const skippedData = typeof skipped?.dataJson === 'string'
? JSON.parse(skipped.dataJson)
: skipped?.dataJson;
assert.equal(skippedData?.reason, 'session_not_direct_chat');
});
test('agent run falls back to backend session when router is disabled', async () => {
const pool = createFakePool();
const submitted = [];
const directRuns = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'agent-session-1' };
},
async submitSessionReplyForUser(userId, sessionId, requestId, userMessage) {
submitted.push({ userId, sessionId, requestId, userMessage });
},
},
directChatService: {
canHandle() {
return true;
},
async run(input) {
directRuns.push(input);
return { sessionId: 'h5direct_session-1' };
},
getStatus() {
return { enabled: true };
},
},
chatIntentRouter: {
isEnabled() {
return false;
},
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-agent-fallback',
userMessage: { role: 'user', content: [{ type: 'text', text: 'hi' }] },
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(pool.runs.get(run.id).agent_session_id, 'agent-session-1');
assert.equal(directRuns.length, 0);
assert.equal(submitted.length, 1);
assert.equal(submitted[0].sessionId, 'agent-session-1');
});
test('agent run uses chat intent router to enrich agent orchestration messages', async () => {
const pool = createFakePool();
const submitted = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {
async getUserCapabilities() {
return { grantedSkills: ['static-page-publish'] };
},
},
tkmindProxy: {
async startSessionForUser(userId) {
assert.equal(userId, 'user-1');
return { id: 'agent-session-1' };
},
async submitSessionReplyForUser(userId, sessionId, requestId, userMessage) {
submitted.push({ userId, sessionId, requestId, userMessage });
},
},
chatIntentRouter: {
isEnabled() {
return true;
},
async classify() {
return {
route: 'agent_orchestration',
confidence: 0.93,
reason: '需要生成页面',
suggestedSkill: 'static-page-publish',
agentBrief: '生成并发布 HTML',
source: 'llm',
};
},
applyAgentOrchestration(userMessage, classification, { grantedSkills = [] }) {
const displayText = userMessage?.metadata?.displayText ?? userMessage?.content?.[0]?.text ?? '';
return {
...userMessage,
content: [{
type: 'text',
text: `【Memind 任务编排】${classification.reason}\n用户任务:${displayText}`,
}],
metadata: {
...(userMessage.metadata ?? {}),
displayText,
},
};
},
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-router-agent',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我做一个页面' }],
metadata: { displayText: '帮我做一个页面' },
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(submitted.length, 1);
assert.match(submitted[0].userMessage.content[0].text, /Memind 任务编排/);
assert.match(submitted[0].userMessage.content[0].text, /帮我做一个页面/);
assert.ok(pool.events.some((event) => event.eventType === 'intent_routed'));
});
test('agent run escalates direct sessions to a new backend session when forced', async () => {
const pool = createFakePool();
const submitted = [];
@@ -365,7 +617,7 @@ test('agent run with code tool mode starts and submits with code policy', async
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.deepEqual(submitted.map((item) => item.options), [{ toolMode: 'code' }]);
assert.deepEqual(submitted.map((item) => item.options), [{ toolMode: 'code', forceDeepReasoning: false }]);
assert.equal(submitted[0].userMessage.metadata.memindRun.toolMode, 'code');
});