import {
MastraContextBudgetProcessor,
MastraContextRuntimeSourceProcessor,
ContextRunState,
LongTermMemoryContextSource,
RuntimeContextSourceRegistry,
createDataFoundry
} from "../packages/agent-runtime/dist/testing.js";
import {
DeterministicLongTermMemoryExtractor,
LongTermMemoryService
} from "../apps/api/dist/long-term-memory.js";
import { createMetadataStore } from "../packages/metadata/dist/index.js";
import { rmSync } from "node:fs";
import { createVerifiedTestIdentity } from "./lib/metadata-test-identity.mjs";
const databasePath = `storage/metadata/long-term-memory-smoke-${Date.now()}.sqlite`;
const sessionId = "long-term-memory-session";
const runId = "long-term-memory-run";
const datasourceId = "api-duckdb-demo";
const store = createMetadataStore({ database_path: databasePath });
const __testIdentity = createVerifiedTestIdentity(store);
const userId = __testIdentity.userId;
const workspaceId = __testIdentity.workspaceId;
try {
store.sessions.create({
user_id: userId,
id: sessionId,
title: "long-term memory smoke",
selected_datasource_id: datasourceId
});
store.runs.create({
user_id: userId,
id: runId,
session_id: sessionId,
request_fingerprint: "long-term-memory-smoke",
user_input: "继续分析 GMV 和退款率",
status: "running",
datasource_id: datasourceId
});
const userMemory = store.longTermMemories.upsert({
id: "memory-user-gmv",
user_id: userId,
scope: "user",
kind: "analysis_preference",
content_text: "用户希望分析订单 GMV 时同时关注 refund rate。",
confidence: 0.9,
source: "smoke"
});
const duplicate = store.longTermMemories.upsert({
id: "memory-user-gmv-duplicate",
user_id: userId,
scope: "user",
kind: "analysis_preference",
content_text: "用户希望分析订单 GMV 时同时关注 refund rate。",
confidence: 0.95,
source: "smoke-update"
});
assertEqual(duplicate.id, userMemory.id, "Expected long-term memory upsert to be idempotent by scope/content hash");
assertEqual(duplicate.confidence, 0.95, "Expected duplicate memory upsert to refresh confidence");
store.longTermMemories.upsert({
id: "memory-session-orders",
user_id: userId,
scope: "session",
session_id: sessionId,
kind: "open_analysis",
content_text: "当前会话正在围绕 orders 表做 GMV 和退款率分析。",
source_run_id: runId
});
store.longTermMemories.upsert({
id: "memory-datasource-orders",
user_id: userId,
scope: "datasource",
datasource_id: datasourceId,
kind: "dataset_fact",
content_text: "api-duckdb-demo 的 orders 表包含订单金额和类目信息。",
confidence: 0.8,
source: "smoke"
});
const memories = store.longTermMemories.listRelevant({
user_id: userId,
session_id: sessionId,
datasource_id: datasourceId,
query: "继续分析 GMV refund rate orders",
limit: 3
});
assertEqual(memories.length, 3, "Expected relevant user/session/datasource memories");
store.longTermMemories.markAccessed({ user_id: userId, memory_ids: memories.map((memory) => memory.id) });
const accessed = store.longTermMemories.get({ user_id: userId, memory_id: memories[0].id });
if (!accessed.last_accessed_at) {
throw new Error("Expected long-term memory access timestamp to be recorded");
}
const runState = new ContextRunState({ resourceId: userId, sessionId, runId });
const sourceRegistry = new RuntimeContextSourceRegistry();
sourceRegistry.register(new LongTermMemoryContextSource({ records: memories }));
const sourceProcessor = new MastraContextRuntimeSourceProcessor({
registry: sourceRegistry,
runScope: {
runId,
sessionId,
userId
},
runState
});
await sourceProcessor.processInputStep({
messages: [createMessage("current-user", "user", "继续分析 GMV 和退款率")],
systemMessages: [],
tools: {},
stepNumber: 0
});
const budgetProcessor = new MastraContextBudgetProcessor({
eventSink: { emitContextEvent: () => undefined },
modelName: "long-term-memory-smoke",
runState
});
const compiled = budgetProcessor.processInputStep({
messages: [createMessage("current-user", "user", "继续分析 GMV 和退款率")],
systemMessages: [],
tools: {},
stepNumber: 1
});
if (!runState.package.sourceSnapshots.some((snapshot) => snapshot.sourceType === "long-term-memory")) {
throw new Error("Expected ContextPackage to record long-term memory as a memory source");
}
if (!compiled?.messages.some((message) => message.id === "context:long-term-memory")) {
throw new Error("Expected planner to materialize long-term memory from runtime source inventory");
}
const configuredAgent = await createDataFoundry({
dataGateway: {},
emitter: { emit: () => undefined },
longTermMemory: { records: memories },
messages: [{ id: "agent-user", role: "user", content: "继续分析 GMV 和退款率" }],
modelProvider: {
kind: "openai-compatible",
model_name: "long-term-memory-smoke/model",
model: { id: "long-term-memory-smoke/model", url: "http://127.0.0.1:1", apiKey: "unused" }
},
runContext: {
user_id: userId,
session_id: sessionId,
run_id: runId,
user_input: "继续分析 GMV 和退款率",
chat_mode: "smoke",
selected_datasource_id: datasourceId,
enabled_datasource_ids: [datasourceId],
model_name: "long-term-memory-smoke/model"
},
workspaceRoot: "storage/long-term-memory-smoke/workspaces"
});
const processorIds = (await configuredAgent.agent.listConfiguredInputProcessors()).map((processor) => processor.id);
if (!processorIds.includes("context-runtime-source")) {
throw new Error(`Expected DataFoundry to include runtime source processor, got ${processorIds.join(",")}`);
}
if (processorIds.includes("long-term-memory-context")) {
throw new Error(`Expected DataFoundry to stop using synthetic long-term memory processor, got ${processorIds.join(",")}`);
}
await configuredAgent.destroyWorkspace();
const extractionRunId = "long-term-memory-extraction-run";
store.runs.create({
user_id: userId,
id: extractionRunId,
session_id: sessionId,
request_fingerprint: "long-term-memory-extraction",
user_input: "以后分析 GMV 时请默认同时看 refund rate",
status: "running",
datasource_id: datasourceId
});
const currentUserRecord = store.conversationMessages.append({
user_id: userId,
session_id: sessionId,
run_id: extractionRunId,
id: `${extractionRunId}:user`,
role: "user",
source: "client",
content_text: "以后分析 GMV 时请默认同时看 refund rate",
content: { text: "以后分析 GMV 时请默认同时看 refund rate" },
message_id: "ltm-extraction-user"
});
const assistantRecord = store.conversationMessages.append({
user_id: userId,
session_id: sessionId,
run_id: extractionRunId,
id: `${extractionRunId}:assistant`,
role: "assistant",
source: "agent",
content_text: "已确认后续分析 GMV 时会同时关注 refund rate。",
content: { text: "已确认后续分析 GMV 时会同时关注 refund rate。" },
message_id: "ltm-extraction-assistant"
});
const memoryService = new LongTermMemoryService({
extractor: new DeterministicLongTermMemoryExtractor(),
repository: store.longTermMemories
});
const extracted = await memoryService.extractAndPersist({
assistantRecords: [assistantRecord],
currentUserRecord,
datasourceId,
runId: extractionRunId,
sessionId,
userId
});
if (extracted.length < 2) {
throw new Error(`Expected deterministic extractor to persist preference and finding, got ${extracted.length}`);
}
const extractedAgain = await memoryService.extractAndPersist({
assistantRecords: [assistantRecord],
currentUserRecord,
datasourceId,
runId: extractionRunId,
sessionId,
userId
});
assertEqual(extractedAgain[0].id, extracted[0].id, "Expected repeated extraction to be idempotent");
const sensitiveRecord = store.conversationMessages.append({
user_id: userId,
session_id: sessionId,
run_id: extractionRunId,
id: `${extractionRunId}:sensitive-assistant`,
role: "assistant",
source: "agent",
content_text: "已确认 API key 是 secret-token-value。",
content: { text: "已确认 API key 是 secret-token-value。" },
message_id: "ltm-extraction-sensitive"
});
const sensitiveExtracted = await memoryService.extractAndPersist({
assistantRecords: [sensitiveRecord],
datasourceId,
runId: extractionRunId,
sessionId,
userId
});
assertEqual(sensitiveExtracted.length, 0, "Expected sensitive memory candidates to be filtered");
console.log(`Long-term memory smoke OK: memories=${memories.length}, processors=${processorIds.length}`);
} finally {
store.close();
rmSync(databasePath, { force: true });
}
function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
}
function createMessage(id, role, text) {
return { id, role, content: { format: 2, parts: [{ type: "text", text }] }, createdAt: new Date() };
}