feat(memory): add gated agent recall and lifecycle controls
This commit is contained in:
+127
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user