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:
+258
-6
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user