fix(agent): restore active-task routing and active-mode validation observation
Memind CI / Test, build, and release guards (push) Successful in 5m23s
Memind CI / Test, build, and release guards (push) Successful in 5m23s
Keep correction follow-ups on Agent using session transcript and active task context, and let Page Data validation reach Orchestrator when mode is active instead of requiring shadowEngine. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+234
-15
@@ -16,6 +16,10 @@ import {
|
||||
} from './memory-intervention.mjs';
|
||||
import { filterMemoriesByQuery } from './memory-legacy-fallback.mjs';
|
||||
import { matchDirectChatFaqRule, isExplicitTextOnlyRequest } from './chat-intent-router-rules.mjs';
|
||||
import {
|
||||
deriveAssistantFacingText,
|
||||
deriveUserFacingText,
|
||||
} from './conversation-display.mjs';
|
||||
import { isGoalRunIntent } from './goal-run-intent.mjs';
|
||||
import { pgvectorMemoryBackendInternals } from './memory-v2-pgvector.mjs';
|
||||
|
||||
@@ -84,6 +88,17 @@ const AGENT_SESSION_CONTINUE_PATTERNS = [
|
||||
/^(?:开始吧|按默认做|默认方案|继续|就这样|就这样吧)[!!。.\s]*$/iu,
|
||||
];
|
||||
|
||||
/** Corrections and refinements during an active Agent task must stay on Agent. */
|
||||
const AGENT_TASK_FOLLOWUP_PATTERNS = [
|
||||
/(?:还是|仍然|依然|并没|没有|并未).{0,16}(?:按照|按|理解|改|做|对|听)/u,
|
||||
/(?:不对|错了|有误|有问题|不行|不好|不太对)/u,
|
||||
/(?:更正|修正|修改|改一下|调整|优化|重新|再来|重做|再试)/u,
|
||||
/(?:不听|没听|不按|没按|不照).{0,16}(?:指令|要求|说的|逻辑|意思)/u,
|
||||
/(?:继续|接着).{0,16}(?:做|改|完善|优化|调整|处理|执行)/u,
|
||||
/(?:按我(?:的|说)|照我(?:的|说)|我说的|我讲的|我上面)/u,
|
||||
/(?:没(?:有)?按照|没有按).{0,20}(?:逻辑|要求|方案|口径|框架)/u,
|
||||
];
|
||||
|
||||
/** Pure text chat/creative prompts — fast-path even when LLM router is enabled. */
|
||||
const OBVIOUS_DIRECT_CHAT_PATTERNS = [
|
||||
/(?:讲|说|来|编).{0,10}(?:个|一段|一首|一个)?(?:睡前故事|故事|笑话|段子)/u,
|
||||
@@ -310,6 +325,84 @@ export function isAgentSessionContinueText(text) {
|
||||
return AGENT_SESSION_CONTINUE_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
export function isAgentTaskFollowUpText(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
return AGENT_TASK_FOLLOWUP_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
function isExplicitDirectChatOnlyText(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
if (isMemoryRecallQuestion(normalized)) return true;
|
||||
if (isExplicitTextOnlyRequest(normalized)) return true;
|
||||
if (OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized))) return true;
|
||||
if (OBVIOUS_DIRECT_CHAT_PATTERNS.some((pattern) => pattern.test(normalized))) return true;
|
||||
const faqMatch = matchDirectChatFaqRule(normalized);
|
||||
return Boolean(faqMatch);
|
||||
}
|
||||
|
||||
export function shouldForceActiveAgentTaskContinuation({
|
||||
activeTaskContext = null,
|
||||
text = '',
|
||||
sessionId = null,
|
||||
sessionMessageCount = null,
|
||||
} = {}) {
|
||||
if (!activeTaskContext?.hasRecentAgentTask) return false;
|
||||
if (!hasPriorAgentConversation(sessionId, sessionMessageCount)) return false;
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized || isExplicitDirectChatOnlyText(normalized)) return false;
|
||||
if (activeTaskContext.lastRunFailed === true) return true;
|
||||
return isAgentTaskFollowUpText(normalized);
|
||||
}
|
||||
|
||||
export function shouldDeferActiveTaskRoutingToLlm({
|
||||
activeTaskContext = null,
|
||||
text = '',
|
||||
sessionId = null,
|
||||
sessionMessageCount = null,
|
||||
} = {}) {
|
||||
if (!activeTaskContext?.hasRecentAgentTask) return false;
|
||||
if (!hasPriorAgentConversation(sessionId, sessionMessageCount)) return false;
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized || isExplicitDirectChatOnlyText(normalized)) return false;
|
||||
if (shouldForceActiveAgentTaskContinuation({
|
||||
activeTaskContext,
|
||||
text: normalized,
|
||||
sessionId,
|
||||
sessionMessageCount,
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return activeTaskContext.lastRoute !== 'direct_chat'
|
||||
&& activeTaskContext.lastRoute !== 'chat';
|
||||
}
|
||||
|
||||
/** @deprecated use shouldForceActiveAgentTaskContinuation */
|
||||
export function shouldContinueActiveAgentTask(input = {}) {
|
||||
return shouldForceActiveAgentTaskContinuation(input)
|
||||
|| shouldDeferActiveTaskRoutingToLlm(input);
|
||||
}
|
||||
|
||||
export function buildActiveTaskFollowUpClassification(activeTaskContext, { grantedSkills = [] } = {}) {
|
||||
let suggestedSkill = String(activeTaskContext?.lastSuggestedSkill ?? '').trim() || null;
|
||||
if (suggestedSkill && grantedSkills.length > 0 && !grantedSkills.includes(suggestedSkill)) {
|
||||
suggestedSkill = null;
|
||||
}
|
||||
const agentBrief = suggestedSkill
|
||||
? `继续完成未完成的 ${suggestedSkill} 任务;用户正在纠正或补充要求,必须调用工具执行,不要只回复文字。`
|
||||
: '用户正在纠正或补充上一轮 Agent 任务,必须继续执行并产出结果,不要只回复文字。';
|
||||
return normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
confidence: 0.92,
|
||||
reason: activeTaskContext?.lastRunFailed
|
||||
? '上一轮任务未完成,延续 Agent 执行'
|
||||
: '活跃任务会话中的纠正或补充',
|
||||
suggested_skill: suggestedSkill,
|
||||
agent_brief: agentBrief,
|
||||
}, { source: 'rule' });
|
||||
}
|
||||
|
||||
export function isRealtimeInfoQuestion(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
@@ -541,6 +634,8 @@ function buildRouterSystemPrompt(grantedSkills = []) {
|
||||
'- 用户要产出可访问页面、文件、链接,或需要工具/skills → agent_orchestration',
|
||||
'- 用户询问实时赛况、新闻、天气、行情等需要联网查询的问题 → agent_orchestration,suggested_skill 填 web(不要填 search)',
|
||||
'- 不确定时优先 agent_orchestration,避免漏执行',
|
||||
'- 若 [Active Task] 显示上一轮 Agent 任务失败或进行中,用户当前消息是在纠正、补充、追问进度或要求按前文继续 → agent_orchestration,不要判成 direct_chat',
|
||||
'- [Recent Conversation] 是对话摘要,当前消息可能引用上文(如「不对」「按我说的」「还是没有改」);结合上下文判断,不要孤立看当前一句',
|
||||
'- 记忆线索只用于辅助判断本轮意图,不能替用户扩写新需求',
|
||||
'',
|
||||
skills.length ? `当前用户已授权 skills:${skills.join(', ')}` : '当前用户未授权额外 skills。',
|
||||
@@ -626,15 +721,83 @@ export function buildRouterContext(resolveResult, {
|
||||
};
|
||||
}
|
||||
|
||||
function buildRouterUserPrompt({ text, routerContext }) {
|
||||
const context = String(routerContext ?? '').trim() || '无';
|
||||
return [
|
||||
'[Router Context]',
|
||||
context,
|
||||
'',
|
||||
'[User]',
|
||||
text || '(empty)',
|
||||
].join('\n');
|
||||
export function formatRouterTranscript(messages = [], {
|
||||
maxTurns = 8,
|
||||
maxChars = 4_000,
|
||||
excludeLatestUserText = null,
|
||||
} = {}) {
|
||||
const normalizedExclude = String(excludeLatestUserText ?? '').trim();
|
||||
const lines = [];
|
||||
let usedChars = 0;
|
||||
const visible = [];
|
||||
for (const message of Array.isArray(messages) ? messages : []) {
|
||||
const role = String(message?.role ?? '').trim();
|
||||
if (role !== 'user' && role !== 'assistant') continue;
|
||||
const raw = Array.isArray(message?.content)
|
||||
? message.content
|
||||
.map((item) => (typeof item === 'string' ? item : item?.text ?? ''))
|
||||
.join('\n')
|
||||
: String(message?.content ?? message?.text ?? '');
|
||||
const displayText = String(message?.metadata?.displayText ?? '').trim();
|
||||
const text = role === 'user'
|
||||
? (displayText || deriveUserFacingText(raw))
|
||||
: deriveAssistantFacingText(raw);
|
||||
const trimmed = String(text ?? '').trim();
|
||||
if (!trimmed) continue;
|
||||
if (role === 'user' && normalizedExclude && trimmed === normalizedExclude) continue;
|
||||
visible.push({ role, text: trimmed });
|
||||
}
|
||||
for (let index = visible.length - 1; index >= 0 && lines.length < maxTurns; index -= 1) {
|
||||
const item = visible[index];
|
||||
const line = `${item.role === 'user' ? '用户' : '助手'}:${item.text}`;
|
||||
if (lines.length > 0 && usedChars + line.length > maxChars) break;
|
||||
lines.unshift(line);
|
||||
usedChars += line.length;
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatActiveTaskContextForRouter(activeTaskContext) {
|
||||
if (!activeTaskContext?.hasRecentAgentTask) return '';
|
||||
const parts = [
|
||||
`- 上一轮 Agent 任务状态:${activeTaskContext.lastRunStatus ?? 'unknown'}`,
|
||||
];
|
||||
if (activeTaskContext.lastRunFailed) {
|
||||
parts.push('- 上一轮任务失败,用户可能在纠正、补充或要求重试');
|
||||
}
|
||||
if (activeTaskContext.lastSuggestedSkill) {
|
||||
parts.push(`- 上一轮建议 skill:${activeTaskContext.lastSuggestedSkill}`);
|
||||
}
|
||||
if (activeTaskContext.lastIntentReason) {
|
||||
parts.push(`- 上一轮路由原因:${activeTaskContext.lastIntentReason}`);
|
||||
}
|
||||
if (activeTaskContext.lastErrorMessage) {
|
||||
parts.push(`- 失败摘要:${String(activeTaskContext.lastErrorMessage).slice(0, 200)}`);
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function buildRouterUserPrompt({
|
||||
text,
|
||||
routerContext,
|
||||
recentTranscript = '',
|
||||
activeTaskContext = null,
|
||||
} = {}) {
|
||||
const sections = [];
|
||||
const memoryContext = String(routerContext ?? '').trim();
|
||||
if (memoryContext) {
|
||||
sections.push('[Memory Context]', memoryContext, '');
|
||||
}
|
||||
const taskContext = formatActiveTaskContextForRouter(activeTaskContext);
|
||||
if (taskContext) {
|
||||
sections.push('[Active Task]', taskContext, '');
|
||||
}
|
||||
const transcript = String(recentTranscript ?? '').trim();
|
||||
if (transcript) {
|
||||
sections.push('[Recent Conversation]', transcript, '');
|
||||
}
|
||||
sections.push('[Current User Message]', text || '(empty)');
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
function parseRouterJson(reply) {
|
||||
@@ -1043,6 +1206,8 @@ export function classifyWithRules({
|
||||
sessionMessageCount = null,
|
||||
userMessage = null,
|
||||
includeIntentPatterns = true,
|
||||
activeTaskContext = null,
|
||||
grantedSkills = [],
|
||||
} = {}) {
|
||||
const decisionContext = {
|
||||
text,
|
||||
@@ -1123,6 +1288,21 @@ export function classifyWithRules({
|
||||
reason: 'Agent 会话确认/续聊',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns
|
||||
&& normalized
|
||||
&& shouldForceActiveAgentTaskContinuation({
|
||||
activeTaskContext,
|
||||
text: normalized,
|
||||
sessionId,
|
||||
sessionMessageCount,
|
||||
})
|
||||
) {
|
||||
return finalizeRouterClassification(
|
||||
buildActiveTaskFollowUpClassification(activeTaskContext, { grantedSkills }),
|
||||
decisionContext,
|
||||
);
|
||||
}
|
||||
if (includeIntentPatterns && normalized && isPageDataDevIntent(normalized)) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
@@ -1173,6 +1353,16 @@ export function classifyWithRules({
|
||||
return finalizeRouterClassification(buildRealtimeInfoClassification(), decisionContext);
|
||||
}
|
||||
if (includeIntentPatterns && normalized) {
|
||||
if (
|
||||
shouldDeferActiveTaskRoutingToLlm({
|
||||
activeTaskContext,
|
||||
text: normalized,
|
||||
sessionId,
|
||||
sessionMessageCount,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
confidence: 0.72,
|
||||
@@ -1229,6 +1419,8 @@ export function createChatIntentRouter(options = {}) {
|
||||
text,
|
||||
grantedSkills = [],
|
||||
routerContext = null,
|
||||
recentTranscript = '',
|
||||
activeTaskContext = null,
|
||||
} = {}) {
|
||||
if (typeof llmProviderService?.createChatCompletion !== 'function') return null;
|
||||
try {
|
||||
@@ -1244,6 +1436,8 @@ export function createChatIntentRouter(options = {}) {
|
||||
content: buildRouterUserPrompt({
|
||||
text,
|
||||
routerContext: routerContext?.content ?? null,
|
||||
recentTranscript,
|
||||
activeTaskContext,
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -1496,6 +1690,8 @@ export function createChatIntentRouter(options = {}) {
|
||||
toolMode = 'chat',
|
||||
forceDeepReasoning = false,
|
||||
grantedSkills = [],
|
||||
activeTaskContext = null,
|
||||
recentTranscript = '',
|
||||
} = {}) {
|
||||
const text = messageDisplayText(userMessage);
|
||||
const decisionContext = {
|
||||
@@ -1535,10 +1731,28 @@ export function createChatIntentRouter(options = {}) {
|
||||
sessionMessageCount,
|
||||
userMessage,
|
||||
includeIntentPatterns: true,
|
||||
activeTaskContext,
|
||||
grantedSkills,
|
||||
});
|
||||
if (ruleResult) return finalizeWithCoercion(ruleResult);
|
||||
|
||||
const baseline = buildFallbackClassification();
|
||||
const activeTaskFallback = (
|
||||
shouldForceActiveAgentTaskContinuation({
|
||||
activeTaskContext,
|
||||
text,
|
||||
sessionId,
|
||||
sessionMessageCount,
|
||||
})
|
||||
|| shouldDeferActiveTaskRoutingToLlm({
|
||||
activeTaskContext,
|
||||
text,
|
||||
sessionId,
|
||||
sessionMessageCount,
|
||||
})
|
||||
)
|
||||
? buildActiveTaskFollowUpClassification(activeTaskContext, { grantedSkills })
|
||||
: null;
|
||||
const baseline = activeTaskFallback ?? buildFallbackClassification();
|
||||
if (!llmRouterEligible) {
|
||||
return finalizeWithCoercion(baseline);
|
||||
}
|
||||
@@ -1554,14 +1768,19 @@ export function createChatIntentRouter(options = {}) {
|
||||
.catch(() => buildRouterContext(null))
|
||||
: Promise.resolve(buildRouterContext(null));
|
||||
|
||||
// Memory is optional routing hint; run LLM in parallel to cut serial latency.
|
||||
const [routerContext, llmResult] = await Promise.all([
|
||||
memoryPromise,
|
||||
const llmPromise = memoryPromise.then((routerContext) =>
|
||||
classifyWithLlm({
|
||||
text,
|
||||
grantedSkills,
|
||||
routerContext: null,
|
||||
routerContext,
|
||||
recentTranscript,
|
||||
activeTaskContext,
|
||||
}),
|
||||
);
|
||||
|
||||
const [routerContext, llmResult] = await Promise.all([
|
||||
memoryPromise,
|
||||
llmPromise,
|
||||
]);
|
||||
|
||||
if (policy.shadowMode) {
|
||||
@@ -1592,7 +1811,7 @@ export function createChatIntentRouter(options = {}) {
|
||||
|
||||
if (!llmResult || llmResult.confidence < policy.minConfidence) {
|
||||
return finalizeWithCoercion({
|
||||
...baseline,
|
||||
...(activeTaskFallback ?? baseline),
|
||||
llmSuggestion: llmResult
|
||||
? {
|
||||
route: llmResult.route,
|
||||
|
||||
Reference in New Issue
Block a user