@illuma-ai/agents 1.1.25 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs +20 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/spawnPath.cjs +104 -0
- package/dist/cjs/common/spawnPath.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +87 -31
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
- package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +4 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +115 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/memory/citations.cjs +69 -0
- package/dist/cjs/memory/citations.cjs.map +1 -0
- package/dist/cjs/memory/compositeBackend.cjs +60 -0
- package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
- package/dist/cjs/memory/constants.cjs +232 -0
- package/dist/cjs/memory/constants.cjs.map +1 -0
- package/dist/cjs/memory/embeddings.cjs +151 -0
- package/dist/cjs/memory/embeddings.cjs.map +1 -0
- package/dist/cjs/memory/factory.cjs +95 -0
- package/dist/cjs/memory/factory.cjs.map +1 -0
- package/dist/cjs/memory/migrate.cjs +81 -0
- package/dist/cjs/memory/migrate.cjs.map +1 -0
- package/dist/cjs/memory/mmr.cjs +138 -0
- package/dist/cjs/memory/mmr.cjs.map +1 -0
- package/dist/cjs/memory/paths.cjs +217 -0
- package/dist/cjs/memory/paths.cjs.map +1 -0
- package/dist/cjs/memory/pgvectorStore.cjs +225 -0
- package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
- package/dist/cjs/memory/recallTracking.cjs +98 -0
- package/dist/cjs/memory/recallTracking.cjs.map +1 -0
- package/dist/cjs/memory/schema.sql +51 -0
- package/dist/cjs/memory/temporalDecay.cjs +118 -0
- package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
- package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
- package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
- package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
- package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
- package/dist/cjs/run.cjs +16 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/AskUser.cjs +6 -1
- package/dist/cjs/tools/AskUser.cjs.map +1 -1
- package/dist/cjs/tools/BrowserTools.cjs +1 -1
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +127 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/approval/constants.cjs +2 -2
- package/dist/cjs/tools/approval/constants.cjs.map +1 -1
- package/dist/cjs/tools/memory/index.cjs +58 -0
- package/dist/cjs/tools/memory/index.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/shared.cjs +106 -0
- package/dist/cjs/tools/memory/shared.cjs.map +1 -0
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/childAgentContext.cjs +242 -0
- package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -4
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/finishReasons.cjs +44 -0
- package/dist/cjs/utils/finishReasons.cjs.map +1 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/logging.cjs +34 -0
- package/dist/cjs/utils/logging.cjs.map +1 -0
- package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
- package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +20 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/spawnPath.mjs +95 -0
- package/dist/esm/common/spawnPath.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +87 -31
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
- package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +4 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +21 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/memory/citations.mjs +64 -0
- package/dist/esm/memory/citations.mjs.map +1 -0
- package/dist/esm/memory/compositeBackend.mjs +58 -0
- package/dist/esm/memory/compositeBackend.mjs.map +1 -0
- package/dist/esm/memory/constants.mjs +198 -0
- package/dist/esm/memory/constants.mjs.map +1 -0
- package/dist/esm/memory/embeddings.mjs +148 -0
- package/dist/esm/memory/embeddings.mjs.map +1 -0
- package/dist/esm/memory/factory.mjs +93 -0
- package/dist/esm/memory/factory.mjs.map +1 -0
- package/dist/esm/memory/migrate.mjs +78 -0
- package/dist/esm/memory/migrate.mjs.map +1 -0
- package/dist/esm/memory/mmr.mjs +130 -0
- package/dist/esm/memory/mmr.mjs.map +1 -0
- package/dist/esm/memory/paths.mjs +207 -0
- package/dist/esm/memory/paths.mjs.map +1 -0
- package/dist/esm/memory/pgvectorStore.mjs +223 -0
- package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
- package/dist/esm/memory/recallTracking.mjs +94 -0
- package/dist/esm/memory/recallTracking.mjs.map +1 -0
- package/dist/esm/memory/schema.sql +51 -0
- package/dist/esm/memory/temporalDecay.mjs +110 -0
- package/dist/esm/memory/temporalDecay.mjs.map +1 -0
- package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
- package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
- package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
- package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
- package/dist/esm/run.mjs +16 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/AskUser.mjs +6 -1
- package/dist/esm/tools/AskUser.mjs.map +1 -1
- package/dist/esm/tools/BrowserTools.mjs +1 -1
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +128 -11
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/approval/constants.mjs +2 -2
- package/dist/esm/tools/approval/constants.mjs.map +1 -1
- package/dist/esm/tools/memory/index.mjs +46 -0
- package/dist/esm/tools/memory/index.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
- package/dist/esm/tools/memory/shared.mjs +98 -0
- package/dist/esm/tools/memory/shared.mjs.map +1 -0
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/childAgentContext.mjs +237 -0
- package/dist/esm/utils/childAgentContext.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -5
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/finishReasons.mjs +41 -0
- package/dist/esm/utils/finishReasons.mjs.map +1 -0
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/logging.mjs +31 -0
- package/dist/esm/utils/logging.mjs.map +1 -0
- package/dist/esm/utils/toolCallNormalization.mjs +247 -0
- package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
- package/dist/types/common/index.d.ts +1 -0
- package/dist/types/common/spawnPath.d.ts +59 -0
- package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
- package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
- package/dist/types/memory/citations.d.ts +39 -0
- package/dist/types/memory/compositeBackend.d.ts +30 -0
- package/dist/types/memory/constants.d.ts +121 -0
- package/dist/types/memory/embeddings.d.ts +15 -0
- package/dist/types/memory/factory.d.ts +23 -0
- package/dist/types/memory/index.d.ts +21 -0
- package/dist/types/memory/migrate.d.ts +14 -0
- package/dist/types/memory/mmr.d.ts +50 -0
- package/dist/types/memory/paths.d.ts +107 -0
- package/dist/types/memory/pgvectorStore.d.ts +56 -0
- package/dist/types/memory/recallTracking.d.ts +30 -0
- package/dist/types/memory/temporalDecay.d.ts +53 -0
- package/dist/types/memory/types.d.ts +182 -0
- package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/AskUser.d.ts +1 -1
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/tools/approval/constants.d.ts +2 -2
- package/dist/types/tools/memory/index.d.ts +39 -0
- package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
- package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
- package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
- package/dist/types/tools/memory/shared.d.ts +106 -0
- package/dist/types/types/graph.d.ts +16 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/logging.d.ts +2 -0
- package/dist/types/utils/toolCallNormalization.d.ts +44 -0
- package/package.json +6 -4
- package/src/agents/AgentContext.ts +26 -3
- package/src/common/__tests__/enum.test.ts +4 -2
- package/src/common/__tests__/spawnPath.test.ts +110 -0
- package/src/common/index.ts +1 -0
- package/src/common/spawnPath.ts +101 -0
- package/src/graphs/Graph.ts +94 -43
- package/src/graphs/HandoffRegistry.ts +199 -0
- package/src/graphs/MultiAgentGraph.ts +694 -226
- package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
- package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
- package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
- package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
- package/src/graphs/contextManagement.e2e.test.ts +1 -1
- package/src/graphs/index.ts +1 -0
- package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
- package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
- package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
- package/src/graphs/phases/flushLoop.ts +303 -0
- package/src/graphs/phases/memoryFlushPhase.ts +209 -0
- package/src/index.ts +30 -1
- package/src/llm/bedrock/index.ts +4 -5
- package/src/memory/__tests__/citations.test.ts +61 -0
- package/src/memory/__tests__/compositeBackend.test.ts +79 -0
- package/src/memory/__tests__/isolation.test.ts +206 -0
- package/src/memory/__tests__/mmr.test.ts +148 -0
- package/src/memory/__tests__/mockBackend.ts +161 -0
- package/src/memory/__tests__/paths.test.ts +168 -0
- package/src/memory/__tests__/recallTracking.test.ts +96 -0
- package/src/memory/__tests__/temporalDecay.test.ts +151 -0
- package/src/memory/citations.ts +80 -0
- package/src/memory/compositeBackend.ts +99 -0
- package/src/memory/constants.ts +229 -0
- package/src/memory/embeddings.ts +188 -0
- package/src/memory/factory.ts +111 -0
- package/src/memory/index.ts +46 -0
- package/src/memory/migrate.ts +116 -0
- package/src/memory/mmr.ts +161 -0
- package/src/memory/paths.ts +258 -0
- package/src/memory/pgvectorStore.ts +324 -0
- package/src/memory/recallTracking.ts +127 -0
- package/src/memory/schema.sql +51 -0
- package/src/memory/temporalDecay.ts +134 -0
- package/src/memory/types.ts +185 -0
- package/src/nodes/ApprovalGateNode.ts +4 -10
- package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
- package/src/prompts/memoryFlushPrompt.ts +78 -0
- package/src/run.ts +17 -6
- package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
- package/src/specs/agent-handoffs.test.ts +8 -2
- package/src/stream.ts +4 -6
- package/src/tools/AskUser.ts +7 -2
- package/src/tools/BrowserTools.ts +3 -5
- package/src/tools/ToolNode.ts +150 -13
- package/src/tools/__tests__/ToolApproval.test.ts +22 -9
- package/src/tools/approval/__tests__/constants.test.ts +4 -4
- package/src/tools/approval/constants.ts +2 -2
- package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
- package/src/tools/memory/index.ts +96 -0
- package/src/tools/memory/memoryAppendTool.ts +101 -0
- package/src/tools/memory/memoryGetTool.ts +53 -0
- package/src/tools/memory/memorySearchTool.ts +80 -0
- package/src/tools/memory/shared.ts +169 -0
- package/src/tools/search/search.test.ts +6 -1
- package/src/types/graph.ts +16 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/finishReasons.test.ts +55 -0
- package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
- package/src/utils/childAgentContext.ts +259 -0
- package/src/utils/events.ts +37 -4
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the 8-path whitelist and tier utilities.
|
|
3
|
+
*
|
|
4
|
+
* These tests pin the vocabulary in place — if someone renames a
|
|
5
|
+
* canonical path without updating routes/UI/prompts, the build breaks
|
|
6
|
+
* here first instead of at runtime with cryptic whitelist rejections.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
MEMORY_AGENT_PATHS,
|
|
10
|
+
MEMORY_USER_PATHS,
|
|
11
|
+
MEMORY_ALL_PATHS,
|
|
12
|
+
MEMORY_WRITABLE_PATHS,
|
|
13
|
+
assertWritablePath,
|
|
14
|
+
getTierForPath,
|
|
15
|
+
getPathDescriptor,
|
|
16
|
+
getWritablePathsForScope,
|
|
17
|
+
renderPathsRubric,
|
|
18
|
+
} from '../paths';
|
|
19
|
+
|
|
20
|
+
describe('memory/paths', () => {
|
|
21
|
+
describe('whitelist shape', () => {
|
|
22
|
+
it('has exactly 4 agent-tier + 4 user-tier paths = 8 total', () => {
|
|
23
|
+
expect(MEMORY_AGENT_PATHS).toHaveLength(4);
|
|
24
|
+
expect(MEMORY_USER_PATHS).toHaveLength(4);
|
|
25
|
+
expect(MEMORY_ALL_PATHS).toHaveLength(8);
|
|
26
|
+
expect(MEMORY_WRITABLE_PATHS.size).toBe(8);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('every agent-tier path starts with memory/agent/', () => {
|
|
30
|
+
for (const p of MEMORY_AGENT_PATHS) {
|
|
31
|
+
expect(p.path).toMatch(/^memory\/agent\//);
|
|
32
|
+
expect(p.tier).toBe('agent');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('every user-tier path starts with memory/user/', () => {
|
|
37
|
+
for (const p of MEMORY_USER_PATHS) {
|
|
38
|
+
expect(p.path).toMatch(/^memory\/user\//);
|
|
39
|
+
expect(p.tier).toBe('user');
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('every descriptor has a non-empty label and description', () => {
|
|
44
|
+
for (const p of MEMORY_ALL_PATHS) {
|
|
45
|
+
expect(p.label.length).toBeGreaterThan(0);
|
|
46
|
+
expect(p.description.length).toBeGreaterThan(20);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('paths are unique', () => {
|
|
51
|
+
const paths = MEMORY_ALL_PATHS.map((p) => p.path);
|
|
52
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getTierForPath', () => {
|
|
57
|
+
it('returns agent for memory/agent/* paths', () => {
|
|
58
|
+
expect(getTierForPath('memory/agent/playbook.md')).toBe('agent');
|
|
59
|
+
expect(getTierForPath('memory/agent/pitfalls.md')).toBe('agent');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns user for memory/user/* paths', () => {
|
|
63
|
+
expect(getTierForPath('memory/user/profile.md')).toBe('user');
|
|
64
|
+
expect(getTierForPath('memory/user/preferences.md')).toBe('user');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('falls back to prefix inspection for unknown paths', () => {
|
|
68
|
+
expect(getTierForPath('memory/user/legacy.md')).toBe('user');
|
|
69
|
+
expect(getTierForPath('memory/2026-04-14.md')).toBe('agent');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('getPathDescriptor', () => {
|
|
74
|
+
it('returns the descriptor for a canonical path', () => {
|
|
75
|
+
const d = getPathDescriptor('memory/agent/playbook.md');
|
|
76
|
+
expect(d).toBeDefined();
|
|
77
|
+
expect(d!.tier).toBe('agent');
|
|
78
|
+
expect(d!.label).toBe('Playbook');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns undefined for non-whitelisted paths', () => {
|
|
82
|
+
expect(getPathDescriptor('memory/random.md')).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getWritablePathsForScope', () => {
|
|
87
|
+
it('returns all 8 when scope has a userId', () => {
|
|
88
|
+
const paths = getWritablePathsForScope({ userId: 'alice' });
|
|
89
|
+
expect(paths).toHaveLength(8);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns only 4 agent-tier paths when userId is missing', () => {
|
|
93
|
+
const paths = getWritablePathsForScope({ userId: null });
|
|
94
|
+
expect(paths).toHaveLength(4);
|
|
95
|
+
expect(paths.every((p) => p.tier === 'agent')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('treats empty-string userId as isolated', () => {
|
|
99
|
+
const paths = getWritablePathsForScope({ userId: '' });
|
|
100
|
+
expect(paths).toHaveLength(4);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('assertWritablePath', () => {
|
|
105
|
+
const scoped = { userId: 'alice' };
|
|
106
|
+
const isolated = { userId: null };
|
|
107
|
+
|
|
108
|
+
it('accepts a valid agent-tier write from any scope', () => {
|
|
109
|
+
expect(() =>
|
|
110
|
+
assertWritablePath('memory/agent/playbook.md', scoped)
|
|
111
|
+
).not.toThrow();
|
|
112
|
+
expect(() =>
|
|
113
|
+
assertWritablePath('memory/agent/playbook.md', isolated)
|
|
114
|
+
).not.toThrow();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('accepts a valid user-tier write from a scoped caller', () => {
|
|
118
|
+
expect(() =>
|
|
119
|
+
assertWritablePath('memory/user/preferences.md', scoped)
|
|
120
|
+
).not.toThrow();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects a user-tier write from an isolated caller', () => {
|
|
124
|
+
expect(() =>
|
|
125
|
+
assertWritablePath('memory/user/preferences.md', isolated)
|
|
126
|
+
).toThrow(/requires a caller userId/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('rejects paths without the memory/ prefix', () => {
|
|
130
|
+
expect(() => assertWritablePath('foo.md', scoped)).toThrow(
|
|
131
|
+
/must start with "memory\//
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('rejects non-whitelisted paths inside memory/', () => {
|
|
136
|
+
expect(() =>
|
|
137
|
+
assertWritablePath('memory/random-notes.md', scoped)
|
|
138
|
+
).toThrow(/not on the canonical whitelist/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects legacy date-keyed paths', () => {
|
|
142
|
+
expect(() => assertWritablePath('memory/2026-04-14.md', scoped)).toThrow(
|
|
143
|
+
/not on the canonical whitelist/
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('renderPathsRubric', () => {
|
|
149
|
+
it('renders every writable path for a scoped caller', () => {
|
|
150
|
+
const text = renderPathsRubric({ userId: 'alice' });
|
|
151
|
+
for (const p of MEMORY_ALL_PATHS) {
|
|
152
|
+
expect(text).toContain(p.path);
|
|
153
|
+
}
|
|
154
|
+
expect(text).toContain('[Playbook, shared]');
|
|
155
|
+
expect(text).toContain('[Profile, private]');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('omits user-tier paths for an isolated caller', () => {
|
|
159
|
+
const text = renderPathsRubric({ userId: null });
|
|
160
|
+
for (const p of MEMORY_AGENT_PATHS) {
|
|
161
|
+
expect(text).toContain(p.path);
|
|
162
|
+
}
|
|
163
|
+
for (const p of MEMORY_USER_PATHS) {
|
|
164
|
+
expect(text).not.toContain(p.path);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PgvectorRecallTracker,
|
|
3
|
+
NullRecallTracker,
|
|
4
|
+
RECALL_TABLE,
|
|
5
|
+
} from '../recallTracking';
|
|
6
|
+
import type { Pool } from 'pg';
|
|
7
|
+
|
|
8
|
+
interface FakeQuery {
|
|
9
|
+
text: string;
|
|
10
|
+
values?: unknown[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeFakePool(): Pool & { calls: FakeQuery[] } {
|
|
14
|
+
const calls: FakeQuery[] = [];
|
|
15
|
+
const p = {
|
|
16
|
+
calls,
|
|
17
|
+
async query(text: string, values?: unknown[]) {
|
|
18
|
+
calls.push({ text, values });
|
|
19
|
+
return { rows: [] };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return p as unknown as Pool & { calls: FakeQuery[] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('PgvectorRecallTracker.migrate', () => {
|
|
26
|
+
it('creates table + indexes idempotently', async () => {
|
|
27
|
+
const pool = makeFakePool();
|
|
28
|
+
const t = new PgvectorRecallTracker(pool);
|
|
29
|
+
await t.migrate();
|
|
30
|
+
const joined = pool.calls.map((c) => c.text).join(' ');
|
|
31
|
+
expect(joined).toContain(`CREATE TABLE IF NOT EXISTS ${RECALL_TABLE}`);
|
|
32
|
+
expect(joined).toContain(`${RECALL_TABLE}_agent_day_idx`);
|
|
33
|
+
expect(joined).toContain(`${RECALL_TABLE}_memory_idx`);
|
|
34
|
+
expect(joined).toContain(`${RECALL_TABLE}_dedupe_idx`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('PgvectorRecallTracker.record', () => {
|
|
39
|
+
it('no-ops on empty hits / missing agent / empty query', async () => {
|
|
40
|
+
const pool = makeFakePool();
|
|
41
|
+
const t = new PgvectorRecallTracker(pool);
|
|
42
|
+
await t.record({ agentId: 'a', query: 'q', hits: [] });
|
|
43
|
+
await t.record({
|
|
44
|
+
agentId: '',
|
|
45
|
+
query: 'q',
|
|
46
|
+
hits: [{ id: '1', path: 'p', score: 0.5 }],
|
|
47
|
+
});
|
|
48
|
+
await t.record({
|
|
49
|
+
agentId: 'a',
|
|
50
|
+
query: ' ',
|
|
51
|
+
hits: [{ id: '1', path: 'p', score: 0.5 }],
|
|
52
|
+
});
|
|
53
|
+
expect(pool.calls).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('emits a single upsert per record call with one row per hit', async () => {
|
|
57
|
+
const pool = makeFakePool();
|
|
58
|
+
const t = new PgvectorRecallTracker(pool);
|
|
59
|
+
await t.record({
|
|
60
|
+
agentId: 'poc-sales',
|
|
61
|
+
query: 'pricing tiers',
|
|
62
|
+
hits: [
|
|
63
|
+
{ id: '1', path: 'memory/2026-04-13.md', score: 0.9 },
|
|
64
|
+
{ id: '2', path: 'memory/2026-04-12.md', score: 0.8 },
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
expect(pool.calls).toHaveLength(1);
|
|
68
|
+
const sql = pool.calls[0].text;
|
|
69
|
+
expect(sql).toContain('INSERT INTO');
|
|
70
|
+
expect(sql).toContain('ON CONFLICT');
|
|
71
|
+
// Two value groups → 16 bound parameters (7 per hit + clause).
|
|
72
|
+
expect(pool.calls[0].values).toHaveLength(14);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('dedupe per (agent, memory, query_hash, day_bucket) via ON CONFLICT DO UPDATE', async () => {
|
|
76
|
+
const pool = makeFakePool();
|
|
77
|
+
const t = new PgvectorRecallTracker(pool);
|
|
78
|
+
await t.record({
|
|
79
|
+
agentId: 'a',
|
|
80
|
+
query: 'q',
|
|
81
|
+
hits: [{ id: '1', path: 'p', score: 0.5 }],
|
|
82
|
+
});
|
|
83
|
+
expect(pool.calls[0].text).toMatch(
|
|
84
|
+
/ON CONFLICT \(agent_id, memory_id, query_hash, day_bucket\)/
|
|
85
|
+
);
|
|
86
|
+
expect(pool.calls[0].text).toMatch(/GREATEST/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('NullRecallTracker', () => {
|
|
91
|
+
it('no-ops without errors', async () => {
|
|
92
|
+
const t = new NullRecallTracker();
|
|
93
|
+
await expect(t.migrate()).resolves.toBeUndefined();
|
|
94
|
+
await expect(t.record()).resolves.toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyTemporalDecayToHits,
|
|
3
|
+
applyTemporalDecayToScore,
|
|
4
|
+
calculateTemporalDecayMultiplier,
|
|
5
|
+
DEFAULT_TEMPORAL_DECAY_CONFIG,
|
|
6
|
+
isEvergreenMemoryPath,
|
|
7
|
+
parseMemoryDateFromPath,
|
|
8
|
+
} from '../temporalDecay';
|
|
9
|
+
|
|
10
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
describe('calculateTemporalDecayMultiplier', () => {
|
|
13
|
+
it('is 1 at age=0', () => {
|
|
14
|
+
expect(
|
|
15
|
+
calculateTemporalDecayMultiplier({ ageInDays: 0, halfLifeDays: 30 })
|
|
16
|
+
).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is exactly 0.5 at half-life', () => {
|
|
20
|
+
expect(
|
|
21
|
+
calculateTemporalDecayMultiplier({ ageInDays: 30, halfLifeDays: 30 })
|
|
22
|
+
).toBeCloseTo(0.5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns 1 for non-positive half life (no decay)', () => {
|
|
26
|
+
expect(
|
|
27
|
+
calculateTemporalDecayMultiplier({ ageInDays: 100, halfLifeDays: 0 })
|
|
28
|
+
).toBe(1);
|
|
29
|
+
expect(
|
|
30
|
+
calculateTemporalDecayMultiplier({ ageInDays: 100, halfLifeDays: -1 })
|
|
31
|
+
).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('clamps negative age to 0', () => {
|
|
35
|
+
expect(
|
|
36
|
+
calculateTemporalDecayMultiplier({ ageInDays: -5, halfLifeDays: 30 })
|
|
37
|
+
).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('applyTemporalDecayToScore', () => {
|
|
42
|
+
it('halves score at half-life', () => {
|
|
43
|
+
expect(
|
|
44
|
+
applyTemporalDecayToScore({ score: 0.8, ageInDays: 30, halfLifeDays: 30 })
|
|
45
|
+
).toBeCloseTo(0.4);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('parseMemoryDateFromPath', () => {
|
|
50
|
+
it('parses dated memory files', () => {
|
|
51
|
+
const d = parseMemoryDateFromPath('memory/2026-04-13.md');
|
|
52
|
+
expect(d).not.toBeNull();
|
|
53
|
+
expect(d!.getUTCFullYear()).toBe(2026);
|
|
54
|
+
expect(d!.getUTCMonth()).toBe(3);
|
|
55
|
+
expect(d!.getUTCDate()).toBe(13);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects invalid dates', () => {
|
|
59
|
+
expect(parseMemoryDateFromPath('memory/2026-02-30.md')).toBeNull();
|
|
60
|
+
expect(parseMemoryDateFromPath('memory/notes.md')).toBeNull();
|
|
61
|
+
expect(parseMemoryDateFromPath('MEMORY.md')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles windows-style separators', () => {
|
|
65
|
+
expect(parseMemoryDateFromPath('memory\\2026-04-13.md')).not.toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('isEvergreenMemoryPath', () => {
|
|
70
|
+
it('treats MEMORY.md as evergreen', () => {
|
|
71
|
+
expect(isEvergreenMemoryPath('MEMORY.md')).toBe(true);
|
|
72
|
+
expect(isEvergreenMemoryPath('memory.md')).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('treats dated files as non-evergreen', () => {
|
|
76
|
+
expect(isEvergreenMemoryPath('memory/2026-04-13.md')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('treats topic files as evergreen', () => {
|
|
80
|
+
expect(isEvergreenMemoryPath('memory/topics.md')).toBe(true);
|
|
81
|
+
expect(isEvergreenMemoryPath('memory/architecture.md')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('treats non-memory paths as non-evergreen', () => {
|
|
85
|
+
expect(isEvergreenMemoryPath('notes.md')).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('applyTemporalDecayToHits', () => {
|
|
90
|
+
const nowMs = Date.UTC(2026, 3, 13); // 2026-04-13
|
|
91
|
+
|
|
92
|
+
it('is a no-op when disabled', () => {
|
|
93
|
+
const hits = [
|
|
94
|
+
{ path: 'memory/2026-03-14.md', score: 0.9, createdAt: new Date(nowMs) },
|
|
95
|
+
];
|
|
96
|
+
expect(applyTemporalDecayToHits(hits, { enabled: false }, nowMs)).toEqual(
|
|
97
|
+
hits
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('decays dated files by date in path', () => {
|
|
102
|
+
const hits = [
|
|
103
|
+
{ path: 'memory/2026-03-14.md', score: 1.0, createdAt: new Date(nowMs) }, // 30 days old
|
|
104
|
+
];
|
|
105
|
+
const out = applyTemporalDecayToHits(
|
|
106
|
+
hits,
|
|
107
|
+
{ enabled: true, halfLifeDays: 30 },
|
|
108
|
+
nowMs
|
|
109
|
+
);
|
|
110
|
+
expect(out[0].score).toBeCloseTo(0.5, 2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does NOT decay evergreen files', () => {
|
|
114
|
+
const hits = [
|
|
115
|
+
{
|
|
116
|
+
path: 'memory/architecture.md',
|
|
117
|
+
score: 0.9,
|
|
118
|
+
createdAt: new Date(nowMs - 100 * DAY_MS),
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
const out = applyTemporalDecayToHits(
|
|
122
|
+
hits,
|
|
123
|
+
{ enabled: true, halfLifeDays: 30 },
|
|
124
|
+
nowMs
|
|
125
|
+
);
|
|
126
|
+
expect(out[0].score).toBe(0.9);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('falls back to createdAt for non-dated non-evergreen paths', () => {
|
|
130
|
+
const hits = [
|
|
131
|
+
{
|
|
132
|
+
path: 'notes.md',
|
|
133
|
+
score: 1.0,
|
|
134
|
+
createdAt: new Date(nowMs - 30 * DAY_MS),
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
const out = applyTemporalDecayToHits(
|
|
138
|
+
hits,
|
|
139
|
+
{ enabled: true, halfLifeDays: 30 },
|
|
140
|
+
nowMs
|
|
141
|
+
);
|
|
142
|
+
expect(out[0].score).toBeCloseTo(0.5, 2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('default config is disabled with 30 day half-life', () => {
|
|
146
|
+
expect(DEFAULT_TEMPORAL_DECAY_CONFIG).toEqual({
|
|
147
|
+
enabled: false,
|
|
148
|
+
halfLifeDays: 30,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation decoration — Phase 2.
|
|
3
|
+
*
|
|
4
|
+
* Ported from upstream `extensions/memory-core/src/tools.citations.ts`.
|
|
5
|
+
* Decorates memory_search hits with `[path#L{start}-L{end}]` markers so
|
|
6
|
+
* the model can attribute claims back to specific memory files when it
|
|
7
|
+
* uses them in its answer.
|
|
8
|
+
*
|
|
9
|
+
* Since our backend stores files as single rows (not line-chunked), we
|
|
10
|
+
* compute line ranges from the returned content block on the fly:
|
|
11
|
+
* - `startLine` = 1 (line 1 of the file)
|
|
12
|
+
* - `endLine` = total number of lines in the block
|
|
13
|
+
* This matches upstream's output format exactly while keeping the pg
|
|
14
|
+
* schema chunk-free.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type MemoryCitationsMode = 'on' | 'off' | 'auto';
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_CITATIONS_MODE: MemoryCitationsMode = 'auto';
|
|
20
|
+
|
|
21
|
+
export function resolveMemoryCitationsMode(
|
|
22
|
+
raw: string | undefined | null
|
|
23
|
+
): MemoryCitationsMode {
|
|
24
|
+
if (raw === 'on' || raw === 'off' || raw === 'auto') return raw;
|
|
25
|
+
return DEFAULT_CITATIONS_MODE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CitationCandidate {
|
|
29
|
+
path: string;
|
|
30
|
+
content: string;
|
|
31
|
+
startLine?: number;
|
|
32
|
+
endLine?: number;
|
|
33
|
+
citation?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function countLines(text: string): number {
|
|
37
|
+
if (!text) return 1;
|
|
38
|
+
return Math.max(1, text.split('\n').length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatCitation(
|
|
42
|
+
path: string,
|
|
43
|
+
startLine: number,
|
|
44
|
+
endLine: number
|
|
45
|
+
): string {
|
|
46
|
+
if (startLine === endLine) return `${path}#L${startLine}`;
|
|
47
|
+
return `${path}#L${startLine}-L${endLine}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decorate each hit with a citation marker. Mirrors upstream's behavior:
|
|
52
|
+
* appends `\n\nSource: <citation>` to the content and sets `citation`.
|
|
53
|
+
* When `include=false`, clears any existing citation field.
|
|
54
|
+
*/
|
|
55
|
+
export function decorateCitations<T extends CitationCandidate>(
|
|
56
|
+
hits: T[],
|
|
57
|
+
include: boolean
|
|
58
|
+
): T[] {
|
|
59
|
+
if (!include) return hits.map((h) => ({ ...h, citation: undefined }));
|
|
60
|
+
return hits.map((h) => {
|
|
61
|
+
const start = Math.max(1, Math.floor(h.startLine ?? 1));
|
|
62
|
+
const end = Math.max(start, Math.floor(h.endLine ?? countLines(h.content)));
|
|
63
|
+
const citation = formatCitation(h.path, start, end);
|
|
64
|
+
const content = `${(h.content ?? '').trimEnd()}\n\nSource: ${citation}`;
|
|
65
|
+
return { ...h, citation, content, startLine: start, endLine: end };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether citations should be emitted for this call.
|
|
71
|
+
*
|
|
72
|
+
* Upstream keys `auto` off the session type (direct/group/channel). In
|
|
73
|
+
* Phase 1 we only have direct chat, so `auto` => `on`. Callers that
|
|
74
|
+
* later distinguish session types can pass `mode` explicitly.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldIncludeCitations(mode: MemoryCitationsMode): boolean {
|
|
77
|
+
if (mode === 'on') return true;
|
|
78
|
+
if (mode === 'off') return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite memory backend — the architectural seam for a future graph layer.
|
|
3
|
+
*
|
|
4
|
+
* Today, memory is pgvector only. Tomorrow, a Graphiti or Neo4j-agent-memory
|
|
5
|
+
* adapter can implement the same {@link MemoryBackend} interface and be
|
|
6
|
+
* composed with the vector store here — without touching the tool
|
|
7
|
+
* definitions, prompts, or host wiring.
|
|
8
|
+
*
|
|
9
|
+
* Fan-out strategy (simple on purpose for Phase 1):
|
|
10
|
+
* - `search`: query every backend in parallel, merge, dedupe by id, sort by
|
|
11
|
+
* score, cap at maxResults. Graph hits and vector hits are interleaved by
|
|
12
|
+
* score — the LLM sees one ranked list.
|
|
13
|
+
* - `get`: the vector store owns file paths; graph stores don't. First
|
|
14
|
+
* backend that returns a non-null result wins. In practice this will almost
|
|
15
|
+
* always be the vector store, since append paths live there.
|
|
16
|
+
* - `append`: fan out to every backend. Vector store persists the note,
|
|
17
|
+
* graph backend runs its own extraction pipeline. An append failure in any
|
|
18
|
+
* single backend is surfaced — we fail loud rather than silently dropping
|
|
19
|
+
* writes.
|
|
20
|
+
*/
|
|
21
|
+
import type {
|
|
22
|
+
MemoryAppendInput,
|
|
23
|
+
MemoryBackend,
|
|
24
|
+
MemoryEntry,
|
|
25
|
+
MemoryGetOptions,
|
|
26
|
+
MemoryHealth,
|
|
27
|
+
MemoryReadResult,
|
|
28
|
+
MemoryScope,
|
|
29
|
+
MemorySearchOptions,
|
|
30
|
+
} from './types';
|
|
31
|
+
|
|
32
|
+
export class CompositeMemoryBackend implements MemoryBackend {
|
|
33
|
+
readonly kind = 'composite' as const;
|
|
34
|
+
private readonly backends: MemoryBackend[];
|
|
35
|
+
|
|
36
|
+
constructor(backends: MemoryBackend[]) {
|
|
37
|
+
if (backends.length === 0) {
|
|
38
|
+
throw new Error('CompositeMemoryBackend requires at least one backend');
|
|
39
|
+
}
|
|
40
|
+
this.backends = backends;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async search(
|
|
44
|
+
scope: MemoryScope,
|
|
45
|
+
query: string,
|
|
46
|
+
opts?: MemorySearchOptions
|
|
47
|
+
): Promise<MemoryEntry[]> {
|
|
48
|
+
const perBackend = await Promise.all(
|
|
49
|
+
this.backends.map((backend) => backend.search(scope, query, opts))
|
|
50
|
+
);
|
|
51
|
+
const merged = new Map<string, MemoryEntry>();
|
|
52
|
+
for (const entries of perBackend) {
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const key = `${entry.source ?? 'unknown'}:${entry.id}`;
|
|
55
|
+
const existing = merged.get(key);
|
|
56
|
+
if (!existing || entry.score > existing.score) {
|
|
57
|
+
merged.set(key, entry);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const limit = Math.max(1, opts?.maxResults ?? 10);
|
|
62
|
+
return Array.from(merged.values())
|
|
63
|
+
.sort((a, b) => b.score - a.score)
|
|
64
|
+
.slice(0, limit);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async get(
|
|
68
|
+
scope: MemoryScope,
|
|
69
|
+
opts: MemoryGetOptions
|
|
70
|
+
): Promise<MemoryReadResult | null> {
|
|
71
|
+
for (const backend of this.backends) {
|
|
72
|
+
const result = await backend.get(scope, opts);
|
|
73
|
+
if (result) return result;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async append(scope: MemoryScope, input: MemoryAppendInput): Promise<void> {
|
|
79
|
+
// Serial fan-out — a failure in an earlier backend means we stop and
|
|
80
|
+
// bubble. We do NOT want a partial write where the vector row landed but
|
|
81
|
+
// the graph backend quietly dropped the note.
|
|
82
|
+
for (const backend of this.backends) {
|
|
83
|
+
await backend.append(scope, input);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async health(): Promise<MemoryHealth> {
|
|
88
|
+
const healths = await Promise.all(this.backends.map((b) => b.health()));
|
|
89
|
+
const failed = healths.find((h) => !h.ok);
|
|
90
|
+
if (failed) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
backend: 'composite',
|
|
94
|
+
error: `${failed.backend}: ${failed.error ?? 'unhealthy'}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, backend: 'composite' };
|
|
98
|
+
}
|
|
99
|
+
}
|