feat(memory): add gated agent recall and lifecycle controls

This commit is contained in:
john
2026-07-16 21:05:27 +08:00
parent c1012da120
commit 6e460336f7
13 changed files with 655 additions and 3 deletions
+127 -1
View File
@@ -650,6 +650,18 @@ export function resolveChatIntentRouterPolicy({ env = process.env, overrides = {
};
}
function normalizeAgentInjectionMode(value) {
const mode = String(value ?? 'off').trim().toLowerCase();
return ['off', 'shadow', 'canary', 'active'].includes(mode) ? mode : 'off';
}
function normalizeAgentCanaryUserIds(value) {
return [...new Set(String(value ?? '')
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean))].slice(0, 1000);
}
function normalizeClassification(raw, { source, fallbackRoute = CHAT_INTENT_ROUTE.AGENT } = {}) {
const route = normalizeRoute(raw?.route) ?? fallbackRoute;
const confidenceRaw = Number(raw?.confidence);
@@ -688,14 +700,26 @@ export function buildAgentOrchestrationAgentText({
displayText,
classification,
skillPrompt = '',
memoryContext = null,
}) {
const taskBody = String(displayText ?? '').trim();
const memoryLines = memoryContext?.injectionEnabled
? (Array.isArray(memoryContext.memories) ? memoryContext.memories : [])
.map((item) => normalizeMemoryText(item))
.map(({ label, text }) => {
const clipped = truncateText(text, 120);
return clipped ? `- ${label ? `[${label}] ` : ''}${clipped}` : '';
})
.filter(Boolean)
.slice(0, 8)
: [];
const lines = [
`${AGENT_ORCHESTRATION_HEADER}以下为用户任务,请使用工具与技能实际执行并产出结果,不要只做文字描述。`,
`路由判定:${classification.reason}`,
classification.agentBrief ? `执行要点:${classification.agentBrief}` : '',
classification.suggestedSkill ? `建议 skill${classification.suggestedSkill}` : '',
skillPrompt,
memoryLines.length ? ['[Memory Context]', ...memoryLines].join('\n') : '',
'',
'用户任务:',
taskBody,
@@ -703,13 +727,18 @@ export function buildAgentOrchestrationAgentText({
return lines.join('\n');
}
export function applyAgentOrchestrationToUserMessage(userMessage, classification, { grantedSkills = [] } = {}) {
export function applyAgentOrchestrationToUserMessage(
userMessage,
classification,
{ grantedSkills = [], memoryContext = null } = {},
) {
const displayText = messageDisplayText(userMessage);
const skillPrompt = resolveSkillPrompt(classification?.suggestedSkill, grantedSkills, displayText);
const agentText = buildAgentOrchestrationAgentText({
displayText,
classification,
skillPrompt,
memoryContext,
});
const content = Array.isArray(userMessage?.content)
? userMessage.content.map((item, index) => {
@@ -885,6 +914,22 @@ export function createChatIntentRouter(options = {}) {
'fallbackRoute',
]),
});
const agentMemoryPolicy = {
enabled: envFlag(env?.MEMORY_AGENT_RESOLVE_ENABLED, false),
mode: normalizeAgentInjectionMode(env?.MEMORY_AGENT_INJECTION_MODE),
canaryUserIds: normalizeAgentCanaryUserIds(env?.MEMORY_AGENT_CANARY_USER_IDS),
limit: Math.round(boundedNumber(env?.MEMORY_AGENT_RESOLVE_LIMIT, 3, { min: 1, max: 50 })),
timeoutMs: Math.round(boundedNumber(env?.MEMORY_AGENT_RESOLVE_TIMEOUT_MS, 1200, { min: 0, max: 30_000 })),
};
const agentMemoryMetrics = {
resolveStarted: 0,
resolved: 0,
skipped: 0,
degraded: 0,
injected: 0,
lastLatencyMs: null,
lastReason: null,
};
function getStatus() {
return {
@@ -902,6 +947,11 @@ export function createChatIntentRouter(options = {}) {
fallbackRoute: policy.fallbackRoute,
normalizedDecisionEnabled: isNormalizedRouterDecisionEnabled(env),
normalizedDecisionMode: resolveNormalizedRouterDecisionMode(env),
agentMemory: {
...agentMemoryPolicy,
injectionEnabled: agentMemoryPolicy.mode === 'active',
metrics: { ...agentMemoryMetrics },
},
};
}
@@ -966,6 +1016,76 @@ export function createChatIntentRouter(options = {}) {
return buildRouterContext({ memories, source: 'router-resolve' });
}
async function resolveAgentMemoryContext({ userId, sessionId, text, forceDeepReasoning = false } = {}) {
const injectionEnabled = agentMemoryPolicy.mode === 'active'
|| (agentMemoryPolicy.mode === 'canary' && agentMemoryPolicy.canaryUserIds.includes(String(userId ?? '').trim()));
const base = {
enabled: agentMemoryPolicy.enabled,
mode: agentMemoryPolicy.mode,
injectionEnabled,
skipped: true,
degraded: false,
reason: null,
source: null,
memories: [],
latencyMs: 0,
};
agentMemoryMetrics.resolveStarted += 1;
if (!agentMemoryPolicy.enabled || agentMemoryPolicy.mode === 'off' || !userId || !memoryV2?.resolve) {
agentMemoryMetrics.skipped += 1;
agentMemoryMetrics.lastReason = 'disabled';
return base;
}
const intervention = resolveMemoryInterventionMode({
forceDeepReasoning,
recallQuestion: isMemoryRecallQuestion(text),
context: 'agent',
});
const limit = Math.min(
agentMemoryPolicy.limit,
memoryLimitForIntervention(intervention, { context: 'agent' }),
);
if (limit <= 0) {
agentMemoryMetrics.skipped += 1;
agentMemoryMetrics.lastReason = 'intervention_skip';
return { ...base, reason: 'intervention_skip' };
}
const startedAt = Date.now();
try {
const resolved = await withTimeout(
memoryV2.resolve({ userId, sessionId, query: text, limit }),
agentMemoryPolicy.timeoutMs,
'Memory V2 agent resolve',
);
const result = {
...base,
skipped: false,
source: resolved?.source ?? null,
degraded: Boolean(resolved?.degraded),
reason: resolved?.reason ?? null,
memories: Array.isArray(resolved?.memories) ? resolved.memories.slice(0, limit) : [],
latencyMs: Date.now() - startedAt,
};
agentMemoryMetrics.resolved += 1;
agentMemoryMetrics.lastLatencyMs = result.latencyMs;
agentMemoryMetrics.lastReason = result.reason;
if (result.degraded) agentMemoryMetrics.degraded += 1;
if (result.injectionEnabled && result.memories.length) agentMemoryMetrics.injected += 1;
return result;
} catch (err) {
const result = {
...base,
degraded: true,
reason: err?.code === 'CHAT_INTENT_ROUTER_TIMEOUT' ? 'timeout' : 'resolve_failed',
latencyMs: Date.now() - startedAt,
};
agentMemoryMetrics.degraded += 1;
agentMemoryMetrics.lastLatencyMs = result.latencyMs;
agentMemoryMetrics.lastReason = result.reason;
return result;
}
}
async function classify({
userId = null,
userMessage,
@@ -1022,6 +1142,7 @@ export function createChatIntentRouter(options = {}) {
getStatus,
isEnabled,
classify,
resolveAgentMemoryContext,
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
};
}
@@ -1114,6 +1235,11 @@ export function createManagedChatIntentRouter({
return router.classify(input);
},
async resolveAgentMemoryContext(input = {}) {
const router = await ensureRouter();
return router.resolveAgentMemoryContext(input);
},
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
};
}