feat(h5-session): Session Broker、run SSE replay 与 Finish 竞态修复
落地 H5 Session 架构 Patch 1–5(Broker 收口、Router decision、SSE taxonomy、goosed 边界检查), 并新增可选 MEMIND_RUN_STREAM_REPLAY run 事件回放与 H5 假交付 guard;修复 Finish 先于 agent-run gate 导致 UI 永久 loading 的竞态,接入 verify:h5-session-patches 回归脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,10 +4,16 @@ import {
|
||||
applyAgentOrchestrationToUserMessage,
|
||||
buildRouterContext,
|
||||
buildAgentOrchestrationAgentText,
|
||||
buildRouterNormalizedDecision,
|
||||
CHAT_INTENT_ROUTE,
|
||||
classifyWithRules,
|
||||
createChatIntentRouter,
|
||||
createManagedChatIntentRouter,
|
||||
isNormalizedRouterDecisionEnabled,
|
||||
resolveLegacyRouteFromClassification,
|
||||
ROUTER_DECISION_ROUTE,
|
||||
ROUTER_DECISION_MODE,
|
||||
ROUTER_DECISION_SESSION_HINT,
|
||||
} from './chat-intent-router.mjs';
|
||||
|
||||
test('classifyWithRules routes greetings to direct chat', () => {
|
||||
@@ -527,3 +533,104 @@ test('buildAgentOrchestrationAgentText includes execution brief', () => {
|
||||
assert.match(text, /帮我搜索今天的新闻/);
|
||||
assert.match(text, /请使用 web 技能/);
|
||||
});
|
||||
|
||||
test('classifyWithRules attaches normalized decision on every rule result', () => {
|
||||
const result = classifyWithRules({
|
||||
text: '你好',
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '你好' }],
|
||||
metadata: { displayText: '你好' },
|
||||
},
|
||||
});
|
||||
assert.ok(result.decision);
|
||||
assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT);
|
||||
assert.equal(result.decision.mode, ROUTER_DECISION_MODE.SSE);
|
||||
assert.equal(result.decision.session_hint, ROUTER_DECISION_SESSION_HINT.NEW);
|
||||
});
|
||||
|
||||
test('buildRouterNormalizedDecision maps legacy routes and session hints', () => {
|
||||
const continued = buildRouterNormalizedDecision(
|
||||
{ route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'web' },
|
||||
{
|
||||
text: '继续',
|
||||
sessionId: '20260704_10',
|
||||
sessionMessageCount: 2,
|
||||
toolMode: 'chat',
|
||||
},
|
||||
);
|
||||
assert.equal(continued.route, ROUTER_DECISION_ROUTE.AGENT);
|
||||
assert.equal(continued.session_hint, ROUTER_DECISION_SESSION_HINT.REUSE);
|
||||
assert.ok(continued.flags.includes('selected_skill'));
|
||||
|
||||
const freshAgentSession = buildRouterNormalizedDecision(
|
||||
{ route: CHAT_INTENT_ROUTE.AGENT },
|
||||
{
|
||||
text: '你好',
|
||||
sessionId: '20260704_11',
|
||||
sessionMessageCount: 0,
|
||||
},
|
||||
);
|
||||
assert.equal(freshAgentSession.session_hint, ROUTER_DECISION_SESSION_HINT.NEW);
|
||||
|
||||
const memoryRecall = buildRouterNormalizedDecision(
|
||||
{ route: CHAT_INTENT_ROUTE.DIRECT_CHAT },
|
||||
{ text: '你记得我说过什么吗' },
|
||||
);
|
||||
assert.equal(memoryRecall.route, ROUTER_DECISION_ROUTE.CHAT);
|
||||
assert.ok(memoryRecall.flags.includes('memory_recall'));
|
||||
});
|
||||
|
||||
test('resolveLegacyRouteFromClassification prefers decision when flag enabled', () => {
|
||||
const classification = {
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
decision: { route: ROUTER_DECISION_ROUTE.AGENT },
|
||||
};
|
||||
assert.equal(
|
||||
resolveLegacyRouteFromClassification(classification),
|
||||
CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
);
|
||||
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
|
||||
try {
|
||||
assert.equal(
|
||||
resolveLegacyRouteFromClassification(classification),
|
||||
CHAT_INTENT_ROUTE.AGENT,
|
||||
);
|
||||
assert.equal(isNormalizedRouterDecisionEnabled(), true);
|
||||
} finally {
|
||||
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||||
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test('createChatIntentRouter accepts chat and agent route synonyms from llm', async () => {
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
ok: true,
|
||||
reply: JSON.stringify({
|
||||
route: 'chat',
|
||||
confidence: 0.92,
|
||||
reason: '纯问答',
|
||||
suggested_skill: null,
|
||||
agent_brief: '',
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await router.classify({
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '什么是 SSE?' }],
|
||||
metadata: { displayText: '什么是 SSE?' },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user