@illuma-ai/agents 1.1.28 → 1.3.1
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.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 +89 -45
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
- 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 +117 -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/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/errors.cjs +113 -0
- package/dist/cjs/utils/errors.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -7
- 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.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 +89 -45
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
- 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/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/errors.mjs +109 -0
- package/dist/esm/utils/errors.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -8
- 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 +24 -7
- package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
- 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 +10 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/errors.d.ts +37 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/index.d.ts +1 -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 +12 -4
- 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 +95 -61
- package/src/graphs/HandoffRegistry.ts +48 -17
- package/src/graphs/MultiAgentGraph.ts +588 -327
- package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
- 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/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/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 +1 -1
- 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 +10 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/errors.test.ts +136 -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/errors.ts +115 -0
- package/src/utils/events.ts +37 -7
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/index.ts +1 -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,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var clientBedrockRuntime = require('@aws-sdk/client-bedrock-runtime');
|
|
4
|
+
var constants = require('./constants.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provider-agnostic embedding client for autonomous memory.
|
|
8
|
+
*
|
|
9
|
+
* Factory pattern mirrors host's `getSharedSummaryBedrockClient()` in
|
|
10
|
+
* `api/server/controllers/agents/client.js` — lazy init, shared instance,
|
|
11
|
+
* testable reset. Same AWS credential chain as host's existing Bedrock
|
|
12
|
+
* calls, so no new IAM policy or secret.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Read embedder config from environment. Called once per factory invocation;
|
|
16
|
+
* fresh reads make testing override-friendly via {@link resetMemoryEmbedder}.
|
|
17
|
+
*/
|
|
18
|
+
function resolveConfig() {
|
|
19
|
+
const rawProvider = (process.env.MEMORY_EMBEDDINGS_PROVIDER ?? constants.DEFAULT_MEMORY_PROVIDER).toLowerCase();
|
|
20
|
+
if (rawProvider !== 'bedrock' && rawProvider !== 'openai') {
|
|
21
|
+
throw new Error(`Unsupported MEMORY_EMBEDDINGS_PROVIDER: "${rawProvider}". Supported: bedrock, openai.`);
|
|
22
|
+
}
|
|
23
|
+
const model = process.env.MEMORY_EMBEDDINGS_MODEL ?? constants.DEFAULT_MEMORY_MODEL;
|
|
24
|
+
const dimensions = Number(process.env.MEMORY_EMBEDDINGS_DIMENSIONS) ||
|
|
25
|
+
constants.DEFAULT_MEMORY_DIMENSIONS;
|
|
26
|
+
return { provider: rawProvider, model, dimensions };
|
|
27
|
+
}
|
|
28
|
+
class BedrockEmbedder {
|
|
29
|
+
kind = 'bedrock';
|
|
30
|
+
model;
|
|
31
|
+
dimensions;
|
|
32
|
+
client;
|
|
33
|
+
constructor(params) {
|
|
34
|
+
this.model = params.model;
|
|
35
|
+
this.dimensions = params.dimensions;
|
|
36
|
+
const region = process.env.BEDROCK_AWS_DEFAULT_REGION ??
|
|
37
|
+
process.env.AWS_REGION ??
|
|
38
|
+
'us-east-1';
|
|
39
|
+
// Default credential provider chain — identical to host's existing
|
|
40
|
+
// Bedrock client. In ECS this resolves to the task role.
|
|
41
|
+
this.client = new clientBedrockRuntime.BedrockRuntimeClient({ region });
|
|
42
|
+
}
|
|
43
|
+
async embed(text) {
|
|
44
|
+
const input = text.trim();
|
|
45
|
+
if (!input) {
|
|
46
|
+
throw new Error('Cannot embed empty text');
|
|
47
|
+
}
|
|
48
|
+
const body = {
|
|
49
|
+
inputText: input,
|
|
50
|
+
dimensions: this.dimensions,
|
|
51
|
+
normalize: true,
|
|
52
|
+
};
|
|
53
|
+
const command = new clientBedrockRuntime.InvokeModelCommand({
|
|
54
|
+
modelId: this.model,
|
|
55
|
+
contentType: 'application/json',
|
|
56
|
+
accept: 'application/json',
|
|
57
|
+
body: new TextEncoder().encode(JSON.stringify(body)),
|
|
58
|
+
});
|
|
59
|
+
const response = await this.client.send(command);
|
|
60
|
+
const decoded = JSON.parse(new TextDecoder().decode(response.body));
|
|
61
|
+
if (!Array.isArray(decoded.embedding)) {
|
|
62
|
+
throw new Error('Bedrock embedding response missing `embedding` field');
|
|
63
|
+
}
|
|
64
|
+
return decoded.embedding;
|
|
65
|
+
}
|
|
66
|
+
async embedBatch(texts) {
|
|
67
|
+
// Titan embed v2 has no native batch API — sequential calls are the
|
|
68
|
+
// documented pattern. Parallelism here can trip per-account TPS ceilings,
|
|
69
|
+
// so we keep it serial. For Phase 4 we can introduce a bounded queue.
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const text of texts) {
|
|
72
|
+
out.push(await this.embed(text));
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class OpenAIEmbedder {
|
|
78
|
+
kind = 'openai';
|
|
79
|
+
model;
|
|
80
|
+
dimensions;
|
|
81
|
+
apiKey;
|
|
82
|
+
baseUrl;
|
|
83
|
+
constructor(params) {
|
|
84
|
+
this.model = params.model;
|
|
85
|
+
this.dimensions = params.dimensions;
|
|
86
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
throw new Error('OPENAI_API_KEY is not set — required when MEMORY_EMBEDDINGS_PROVIDER=openai');
|
|
89
|
+
}
|
|
90
|
+
this.apiKey = apiKey;
|
|
91
|
+
this.baseUrl = process.env.OPENAI_BASEURL ?? 'https://api.openai.com/v1';
|
|
92
|
+
}
|
|
93
|
+
async call(texts) {
|
|
94
|
+
const res = await fetch(`${this.baseUrl}/embeddings`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({ model: this.model, input: texts }),
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const errText = await res.text();
|
|
104
|
+
throw new Error(`OpenAI embeddings ${res.status}: ${errText}`);
|
|
105
|
+
}
|
|
106
|
+
const json = (await res.json());
|
|
107
|
+
return json.data.map((d) => d.embedding);
|
|
108
|
+
}
|
|
109
|
+
async embed(text) {
|
|
110
|
+
const [vec] = await this.call([text]);
|
|
111
|
+
return vec;
|
|
112
|
+
}
|
|
113
|
+
async embedBatch(texts) {
|
|
114
|
+
if (texts.length === 0)
|
|
115
|
+
return [];
|
|
116
|
+
return this.call(texts);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let sharedEmbedder = null;
|
|
120
|
+
/**
|
|
121
|
+
* Lazy singleton accessor. Same idiom as host's
|
|
122
|
+
* `getSharedSummaryBedrockClient()`.
|
|
123
|
+
*/
|
|
124
|
+
function getMemoryEmbedder() {
|
|
125
|
+
if (sharedEmbedder)
|
|
126
|
+
return sharedEmbedder;
|
|
127
|
+
const cfg = resolveConfig();
|
|
128
|
+
switch (cfg.provider) {
|
|
129
|
+
case 'bedrock':
|
|
130
|
+
sharedEmbedder = new BedrockEmbedder({
|
|
131
|
+
model: cfg.model,
|
|
132
|
+
dimensions: cfg.dimensions,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
case 'openai':
|
|
136
|
+
sharedEmbedder = new OpenAIEmbedder({
|
|
137
|
+
model: cfg.model,
|
|
138
|
+
dimensions: cfg.dimensions,
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
return sharedEmbedder;
|
|
143
|
+
}
|
|
144
|
+
/** Test hook — drops the cached embedder so the next call re-reads env. */
|
|
145
|
+
function resetMemoryEmbedder() {
|
|
146
|
+
sharedEmbedder = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
exports.getMemoryEmbedder = getMemoryEmbedder;
|
|
150
|
+
exports.resetMemoryEmbedder = resetMemoryEmbedder;
|
|
151
|
+
//# sourceMappingURL=embeddings.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeddings.cjs","sources":["../../../src/memory/embeddings.ts"],"sourcesContent":["/**\n * Provider-agnostic embedding client for autonomous memory.\n *\n * Factory pattern mirrors host's `getSharedSummaryBedrockClient()` in\n * `api/server/controllers/agents/client.js` — lazy init, shared instance,\n * testable reset. Same AWS credential chain as host's existing Bedrock\n * calls, so no new IAM policy or secret.\n */\nimport {\n BedrockRuntimeClient,\n InvokeModelCommand,\n} from '@aws-sdk/client-bedrock-runtime';\nimport {\n DEFAULT_MEMORY_DIMENSIONS,\n DEFAULT_MEMORY_MODEL,\n DEFAULT_MEMORY_PROVIDER,\n} from './constants';\n\nexport type EmbeddingProviderKind = 'bedrock' | 'openai';\n\nexport interface EmbeddingProvider {\n readonly kind: EmbeddingProviderKind;\n readonly model: string;\n readonly dimensions: number;\n embed(text: string): Promise<number[]>;\n embedBatch(texts: string[]): Promise<number[][]>;\n}\n\ninterface ResolvedEmbedderConfig {\n provider: EmbeddingProviderKind;\n model: string;\n dimensions: number;\n}\n\n/**\n * Read embedder config from environment. Called once per factory invocation;\n * fresh reads make testing override-friendly via {@link resetMemoryEmbedder}.\n */\nfunction resolveConfig(): ResolvedEmbedderConfig {\n const rawProvider = (\n process.env.MEMORY_EMBEDDINGS_PROVIDER ?? DEFAULT_MEMORY_PROVIDER\n ).toLowerCase();\n if (rawProvider !== 'bedrock' && rawProvider !== 'openai') {\n throw new Error(\n `Unsupported MEMORY_EMBEDDINGS_PROVIDER: \"${rawProvider}\". Supported: bedrock, openai.`\n );\n }\n const model = process.env.MEMORY_EMBEDDINGS_MODEL ?? DEFAULT_MEMORY_MODEL;\n const dimensions =\n Number(process.env.MEMORY_EMBEDDINGS_DIMENSIONS) ||\n DEFAULT_MEMORY_DIMENSIONS;\n return { provider: rawProvider, model, dimensions };\n}\n\nclass BedrockEmbedder implements EmbeddingProvider {\n readonly kind = 'bedrock' as const;\n readonly model: string;\n readonly dimensions: number;\n private client: BedrockRuntimeClient;\n\n constructor(params: { model: string; dimensions: number }) {\n this.model = params.model;\n this.dimensions = params.dimensions;\n const region =\n process.env.BEDROCK_AWS_DEFAULT_REGION ??\n process.env.AWS_REGION ??\n 'us-east-1';\n // Default credential provider chain — identical to host's existing\n // Bedrock client. In ECS this resolves to the task role.\n this.client = new BedrockRuntimeClient({ region });\n }\n\n async embed(text: string): Promise<number[]> {\n const input = text.trim();\n if (!input) {\n throw new Error('Cannot embed empty text');\n }\n const body = {\n inputText: input,\n dimensions: this.dimensions,\n normalize: true,\n };\n const command = new InvokeModelCommand({\n modelId: this.model,\n contentType: 'application/json',\n accept: 'application/json',\n body: new TextEncoder().encode(JSON.stringify(body)),\n });\n const response = await this.client.send(command);\n const decoded = JSON.parse(new TextDecoder().decode(response.body)) as {\n embedding: number[];\n };\n if (!Array.isArray(decoded.embedding)) {\n throw new Error('Bedrock embedding response missing `embedding` field');\n }\n return decoded.embedding;\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n // Titan embed v2 has no native batch API — sequential calls are the\n // documented pattern. Parallelism here can trip per-account TPS ceilings,\n // so we keep it serial. For Phase 4 we can introduce a bounded queue.\n const out: number[][] = [];\n for (const text of texts) {\n out.push(await this.embed(text));\n }\n return out;\n }\n}\n\nclass OpenAIEmbedder implements EmbeddingProvider {\n readonly kind = 'openai' as const;\n readonly model: string;\n readonly dimensions: number;\n private apiKey: string;\n private baseUrl: string;\n\n constructor(params: { model: string; dimensions: number }) {\n this.model = params.model;\n this.dimensions = params.dimensions;\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPENAI_API_KEY is not set — required when MEMORY_EMBEDDINGS_PROVIDER=openai'\n );\n }\n this.apiKey = apiKey;\n this.baseUrl = process.env.OPENAI_BASEURL ?? 'https://api.openai.com/v1';\n }\n\n private async call(texts: string[]): Promise<number[][]> {\n const res = await fetch(`${this.baseUrl}/embeddings`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ model: this.model, input: texts }),\n });\n if (!res.ok) {\n const errText = await res.text();\n throw new Error(`OpenAI embeddings ${res.status}: ${errText}`);\n }\n const json = (await res.json()) as { data: Array<{ embedding: number[] }> };\n return json.data.map((d) => d.embedding);\n }\n\n async embed(text: string): Promise<number[]> {\n const [vec] = await this.call([text]);\n return vec;\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n if (texts.length === 0) return [];\n return this.call(texts);\n }\n}\n\nlet sharedEmbedder: EmbeddingProvider | null = null;\n\n/**\n * Lazy singleton accessor. Same idiom as host's\n * `getSharedSummaryBedrockClient()`.\n */\nexport function getMemoryEmbedder(): EmbeddingProvider {\n if (sharedEmbedder) return sharedEmbedder;\n const cfg = resolveConfig();\n switch (cfg.provider) {\n case 'bedrock':\n sharedEmbedder = new BedrockEmbedder({\n model: cfg.model,\n dimensions: cfg.dimensions,\n });\n break;\n case 'openai':\n sharedEmbedder = new OpenAIEmbedder({\n model: cfg.model,\n dimensions: cfg.dimensions,\n });\n break;\n }\n return sharedEmbedder;\n}\n\n/** Test hook — drops the cached embedder so the next call re-reads env. */\nexport function resetMemoryEmbedder(): void {\n sharedEmbedder = null;\n}\n"],"names":["DEFAULT_MEMORY_PROVIDER","DEFAULT_MEMORY_MODEL","DEFAULT_MEMORY_DIMENSIONS","BedrockRuntimeClient","InvokeModelCommand"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pg = require('pg');
|
|
4
|
+
var constants = require('./constants.cjs');
|
|
5
|
+
var pgvectorStore = require('./pgvectorStore.cjs');
|
|
6
|
+
var migrate = require('./migrate.cjs');
|
|
7
|
+
var recallTracking = require('./recallTracking.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Factory for building a memory backend from environment variables.
|
|
11
|
+
*
|
|
12
|
+
* Called once at startup by host's singleton (`api/server/services/memoryStore.js`).
|
|
13
|
+
* Returns `null` — never throws — when required env vars are missing, so
|
|
14
|
+
* agents with `memory_enabled=true` still run, just without memory. Host
|
|
15
|
+
* logs a single warning on boot instead of crashing.
|
|
16
|
+
*/
|
|
17
|
+
function readPgConfig() {
|
|
18
|
+
// Dev shortcut: single URL overrides discrete fields if present.
|
|
19
|
+
const url = process.env.MEMORY_DATABASE_URL;
|
|
20
|
+
if (url) {
|
|
21
|
+
try {
|
|
22
|
+
const u = new URL(url);
|
|
23
|
+
return {
|
|
24
|
+
host: u.hostname,
|
|
25
|
+
port: Number(u.port || 5432),
|
|
26
|
+
user: decodeURIComponent(u.username),
|
|
27
|
+
password: decodeURIComponent(u.password),
|
|
28
|
+
database: u.pathname.replace(/^\//, ''),
|
|
29
|
+
ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const host = process.env.MEMORY_POSTGRES_HOST;
|
|
37
|
+
const user = process.env.MEMORY_POSTGRES_USER;
|
|
38
|
+
const password = process.env.MEMORY_POSTGRES_PASSWORD;
|
|
39
|
+
const database = process.env.MEMORY_POSTGRES_DB;
|
|
40
|
+
if (!host || !user || !password || !database) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
host,
|
|
45
|
+
port: Number(process.env.MEMORY_POSTGRES_PORT ?? 5432),
|
|
46
|
+
user,
|
|
47
|
+
password,
|
|
48
|
+
database,
|
|
49
|
+
ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Try to build a configured memory backend. Returns null when the required
|
|
54
|
+
* env vars are absent — caller should log a single "memory disabled" warning
|
|
55
|
+
* and continue. Never throws on missing config.
|
|
56
|
+
*/
|
|
57
|
+
async function buildMemoryBackendFromEnv() {
|
|
58
|
+
const cfg = readPgConfig();
|
|
59
|
+
if (!cfg)
|
|
60
|
+
return null;
|
|
61
|
+
const pool = new pg.Pool({
|
|
62
|
+
host: cfg.host,
|
|
63
|
+
port: cfg.port,
|
|
64
|
+
user: cfg.user,
|
|
65
|
+
password: cfg.password,
|
|
66
|
+
database: cfg.database,
|
|
67
|
+
ssl: cfg.ssl ? { rejectUnauthorized: false } : undefined,
|
|
68
|
+
max: 10,
|
|
69
|
+
});
|
|
70
|
+
const table = process.env.MEMORY_TABLE_NAME ?? constants.DEFAULT_MEMORY_TABLE;
|
|
71
|
+
try {
|
|
72
|
+
await migrate.runMemoryMigration({ pool, table });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// Migration failures ARE fatal — they mean dimension mismatch or
|
|
76
|
+
// permissions wrong, and silently running past that would produce
|
|
77
|
+
// corrupt or missing memory reads.
|
|
78
|
+
await pool.end().catch(() => undefined);
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
const backend = new pgvectorStore.PgvectorMemoryStore({ pool, table });
|
|
82
|
+
// Phase 2 — recall tracker migration is best-effort at boot. If it fails,
|
|
83
|
+
// tool wiring continues with a null tracker rather than crashing host.
|
|
84
|
+
const recallTracker = new recallTracking.PgvectorRecallTracker(pool);
|
|
85
|
+
try {
|
|
86
|
+
await recallTracker.migrate();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// [phase2-recall-tracking] debug: migration skipped — non-fatal
|
|
90
|
+
}
|
|
91
|
+
return { backend, pool, recallTracker };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exports.buildMemoryBackendFromEnv = buildMemoryBackendFromEnv;
|
|
95
|
+
//# sourceMappingURL=factory.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.cjs","sources":["../../../src/memory/factory.ts"],"sourcesContent":["/**\n * Factory for building a memory backend from environment variables.\n *\n * Called once at startup by host's singleton (`api/server/services/memoryStore.js`).\n * Returns `null` — never throws — when required env vars are missing, so\n * agents with `memory_enabled=true` still run, just without memory. Host\n * logs a single warning on boot instead of crashing.\n */\nimport { Pool } from 'pg';\nimport { DEFAULT_MEMORY_TABLE } from './constants';\nimport { PgvectorMemoryStore } from './pgvectorStore';\nimport { runMemoryMigration } from './migrate';\nimport { PgvectorRecallTracker, type RecallTracker } from './recallTracking';\nimport type { MemoryBackend } from './types';\n\nexport interface BuildMemoryBackendResult {\n backend: MemoryBackend;\n pool: Pool;\n /** Phase 2 — shared recall tracker bound to the same pool/migration. */\n recallTracker: RecallTracker;\n}\n\ninterface PgConfig {\n host: string;\n port: number;\n user: string;\n password: string;\n database: string;\n ssl: boolean;\n}\n\nfunction readPgConfig(): PgConfig | null {\n // Dev shortcut: single URL overrides discrete fields if present.\n const url = process.env.MEMORY_DATABASE_URL;\n if (url) {\n try {\n const u = new URL(url);\n return {\n host: u.hostname,\n port: Number(u.port || 5432),\n user: decodeURIComponent(u.username),\n password: decodeURIComponent(u.password),\n database: u.pathname.replace(/^\\//, ''),\n ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require',\n };\n } catch {\n return null;\n }\n }\n\n const host = process.env.MEMORY_POSTGRES_HOST;\n const user = process.env.MEMORY_POSTGRES_USER;\n const password = process.env.MEMORY_POSTGRES_PASSWORD;\n const database = process.env.MEMORY_POSTGRES_DB;\n if (!host || !user || !password || !database) {\n return null;\n }\n return {\n host,\n port: Number(process.env.MEMORY_POSTGRES_PORT ?? 5432),\n user,\n password,\n database,\n ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require',\n };\n}\n\n/**\n * Try to build a configured memory backend. Returns null when the required\n * env vars are absent — caller should log a single \"memory disabled\" warning\n * and continue. Never throws on missing config.\n */\nexport async function buildMemoryBackendFromEnv(): Promise<BuildMemoryBackendResult | null> {\n const cfg = readPgConfig();\n if (!cfg) return null;\n\n const pool = new Pool({\n host: cfg.host,\n port: cfg.port,\n user: cfg.user,\n password: cfg.password,\n database: cfg.database,\n ssl: cfg.ssl ? { rejectUnauthorized: false } : undefined,\n max: 10,\n });\n\n const table = process.env.MEMORY_TABLE_NAME ?? DEFAULT_MEMORY_TABLE;\n\n try {\n await runMemoryMigration({ pool, table });\n } catch (err) {\n // Migration failures ARE fatal — they mean dimension mismatch or\n // permissions wrong, and silently running past that would produce\n // corrupt or missing memory reads.\n await pool.end().catch(() => undefined);\n throw err;\n }\n\n const backend = new PgvectorMemoryStore({ pool, table });\n\n // Phase 2 — recall tracker migration is best-effort at boot. If it fails,\n // tool wiring continues with a null tracker rather than crashing host.\n const recallTracker = new PgvectorRecallTracker(pool);\n try {\n await recallTracker.migrate();\n } catch {\n // [phase2-recall-tracking] debug: migration skipped — non-fatal\n }\n\n return { backend, pool, recallTracker };\n}\n"],"names":["Pool","DEFAULT_MEMORY_TABLE","runMemoryMigration","PgvectorMemoryStore","PgvectorRecallTracker"],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var url = require('url');
|
|
6
|
+
var constants = require('./constants.cjs');
|
|
7
|
+
var embeddings = require('./embeddings.cjs');
|
|
8
|
+
|
|
9
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
10
|
+
function resolveSchemaSqlPath() {
|
|
11
|
+
// Works under both ESM (import.meta.url) and compiled CJS (fall back to __dirname).
|
|
12
|
+
const meta = ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('memory/migrate.cjs', document.baseURI).href)) });
|
|
13
|
+
const metaUrl = typeof meta.url === 'string' ? meta.url : undefined;
|
|
14
|
+
if (metaUrl) {
|
|
15
|
+
return path.join(path.dirname(url.fileURLToPath(metaUrl)), 'schema.sql');
|
|
16
|
+
}
|
|
17
|
+
const globalDirname = globalThis
|
|
18
|
+
.__dirname;
|
|
19
|
+
return path.join(globalDirname ?? process.cwd(), 'schema.sql');
|
|
20
|
+
}
|
|
21
|
+
async function runMemoryMigration(opts) {
|
|
22
|
+
const table = opts.table ?? constants.DEFAULT_MEMORY_TABLE;
|
|
23
|
+
const schemaPath = resolveSchemaSqlPath();
|
|
24
|
+
const schemaSql = fs.readFileSync(schemaPath, 'utf8').replace(/agent_memories/g, table);
|
|
25
|
+
// Drop the legacy `(agent_id, path)` unique constraint BEFORE running
|
|
26
|
+
// the schema SQL. The schema creates the new
|
|
27
|
+
// `(agent_id, user_id, path)` NULLS-NOT-DISTINCT constraint, which
|
|
28
|
+
// must not collide with the old one. Dropping by the legacy name is a
|
|
29
|
+
// no-op on fresh installs where the constraint never existed, and on
|
|
30
|
+
// upgrades it cleanly frees the unique-index slot so the new
|
|
31
|
+
// constraint can be installed.
|
|
32
|
+
await opts.pool
|
|
33
|
+
.query(`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_agent_path_uq`)
|
|
34
|
+
.catch(() => undefined);
|
|
35
|
+
await opts.pool.query(schemaSql);
|
|
36
|
+
// Adopt legacy rows if the table pre-existed with an older shape.
|
|
37
|
+
// Idempotent — every statement is no-op-safe on a fresh schema.
|
|
38
|
+
await opts.pool
|
|
39
|
+
.query(`ALTER TABLE ${table} ALTER COLUMN user_id DROP NOT NULL`)
|
|
40
|
+
.catch(() => undefined);
|
|
41
|
+
await opts.pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`);
|
|
42
|
+
await opts.pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS last_user_id TEXT`);
|
|
43
|
+
// Belt-and-suspenders: if the schema CREATE was skipped because the
|
|
44
|
+
// table already existed AND the new constraint was absent, add it
|
|
45
|
+
// explicitly. Swallow duplicate-object errors on happy-path re-runs.
|
|
46
|
+
await opts.pool
|
|
47
|
+
.query(`ALTER TABLE ${table}
|
|
48
|
+
ADD CONSTRAINT ${table}_agent_user_path_uq
|
|
49
|
+
UNIQUE NULLS NOT DISTINCT (agent_id, user_id, path)`)
|
|
50
|
+
.catch(() => undefined);
|
|
51
|
+
if (opts.skipEmbedderProbe)
|
|
52
|
+
return;
|
|
53
|
+
// Vector dimension sanity check — refuses to serve if the column width
|
|
54
|
+
// disagrees with the live embedder and the table has data. See plan §3.
|
|
55
|
+
const embedder = embeddings.getMemoryEmbedder();
|
|
56
|
+
const liveDim = embedder.dimensions || constants.DEFAULT_MEMORY_DIMENSIONS;
|
|
57
|
+
// pgvector stores the declared width directly in atttypmod (unlike varchar
|
|
58
|
+
// which uses atttypmod = N + VARHDRSZ). Reading it raw gives the true dim.
|
|
59
|
+
const dimResult = await opts.pool.query(`
|
|
60
|
+
SELECT atttypmod AS dim
|
|
61
|
+
FROM pg_attribute
|
|
62
|
+
WHERE attrelid = to_regclass($1)
|
|
63
|
+
AND attname = 'embedding'
|
|
64
|
+
`, [table]);
|
|
65
|
+
const columnDim = dimResult.rows[0]?.dim;
|
|
66
|
+
if (columnDim && Number(columnDim) !== liveDim) {
|
|
67
|
+
const countRes = await opts.pool.query(`SELECT COUNT(*)::int AS n FROM ${table}`);
|
|
68
|
+
const rowCount = Number(countRes.rows[0]?.n ?? 0);
|
|
69
|
+
if (rowCount === 0) {
|
|
70
|
+
await opts.pool.query(`ALTER TABLE ${table} ALTER COLUMN embedding TYPE VECTOR(${liveDim})`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
throw new Error(`Memory vector dimension mismatch: column is VECTOR(${columnDim}) but ` +
|
|
74
|
+
`embedder "${embedder.model}" produces ${liveDim}-d vectors, and ${rowCount} ` +
|
|
75
|
+
`rows exist. Refusing to serve memory. Admin must run a reindex job.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
exports.runMemoryMigration = runMemoryMigration;
|
|
81
|
+
//# sourceMappingURL=migrate.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.cjs","sources":["../../../src/memory/migrate.ts"],"sourcesContent":["/**\n * Idempotent schema install + vector-dimension safety check.\n *\n * Called once at agents-library startup by host's `memoryStore.js` singleton\n * factory. Safe to call multiple times — every statement is `IF NOT EXISTS`.\n */\nimport type { Pool } from 'pg';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { DEFAULT_MEMORY_DIMENSIONS, DEFAULT_MEMORY_TABLE } from './constants';\nimport { getMemoryEmbedder } from './embeddings';\n\nexport interface MigrationOptions {\n pool: Pool;\n table?: string;\n /** If true, skip the live embedder probe (tests). */\n skipEmbedderProbe?: boolean;\n}\n\nfunction resolveSchemaSqlPath(): string {\n // Works under both ESM (import.meta.url) and compiled CJS (fall back to __dirname).\n const meta = import.meta as unknown as { url?: string };\n const metaUrl = typeof meta.url === 'string' ? meta.url : undefined;\n if (metaUrl) {\n return join(dirname(fileURLToPath(metaUrl)), 'schema.sql');\n }\n const globalDirname = (globalThis as unknown as { __dirname?: string })\n .__dirname;\n return join(globalDirname ?? process.cwd(), 'schema.sql');\n}\n\nexport async function runMemoryMigration(\n opts: MigrationOptions\n): Promise<void> {\n const table = opts.table ?? DEFAULT_MEMORY_TABLE;\n const schemaPath = resolveSchemaSqlPath();\n const schemaSql = readFileSync(schemaPath, 'utf8').replace(\n /agent_memories/g,\n table\n );\n\n // Drop the legacy `(agent_id, path)` unique constraint BEFORE running\n // the schema SQL. The schema creates the new\n // `(agent_id, user_id, path)` NULLS-NOT-DISTINCT constraint, which\n // must not collide with the old one. Dropping by the legacy name is a\n // no-op on fresh installs where the constraint never existed, and on\n // upgrades it cleanly frees the unique-index slot so the new\n // constraint can be installed.\n await opts.pool\n .query(\n `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_agent_path_uq`\n )\n .catch(() => undefined);\n\n await opts.pool.query(schemaSql);\n\n // Adopt legacy rows if the table pre-existed with an older shape.\n // Idempotent — every statement is no-op-safe on a fresh schema.\n await opts.pool\n .query(`ALTER TABLE ${table} ALTER COLUMN user_id DROP NOT NULL`)\n .catch(() => undefined);\n await opts.pool.query(\n `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`\n );\n await opts.pool.query(\n `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS last_user_id TEXT`\n );\n // Belt-and-suspenders: if the schema CREATE was skipped because the\n // table already existed AND the new constraint was absent, add it\n // explicitly. Swallow duplicate-object errors on happy-path re-runs.\n await opts.pool\n .query(\n `ALTER TABLE ${table}\n ADD CONSTRAINT ${table}_agent_user_path_uq\n UNIQUE NULLS NOT DISTINCT (agent_id, user_id, path)`\n )\n .catch(() => undefined);\n\n if (opts.skipEmbedderProbe) return;\n\n // Vector dimension sanity check — refuses to serve if the column width\n // disagrees with the live embedder and the table has data. See plan §3.\n const embedder = getMemoryEmbedder();\n const liveDim = embedder.dimensions || DEFAULT_MEMORY_DIMENSIONS;\n\n // pgvector stores the declared width directly in atttypmod (unlike varchar\n // which uses atttypmod = N + VARHDRSZ). Reading it raw gives the true dim.\n const dimResult = await opts.pool.query(\n `\n SELECT atttypmod AS dim\n FROM pg_attribute\n WHERE attrelid = to_regclass($1)\n AND attname = 'embedding'\n `,\n [table]\n );\n const columnDim = dimResult.rows[0]?.dim;\n if (columnDim && Number(columnDim) !== liveDim) {\n const countRes = await opts.pool.query(\n `SELECT COUNT(*)::int AS n FROM ${table}`\n );\n const rowCount = Number(countRes.rows[0]?.n ?? 0);\n if (rowCount === 0) {\n await opts.pool.query(\n `ALTER TABLE ${table} ALTER COLUMN embedding TYPE VECTOR(${liveDim})`\n );\n } else {\n throw new Error(\n `Memory vector dimension mismatch: column is VECTOR(${columnDim}) but ` +\n `embedder \"${embedder.model}\" produces ${liveDim}-d vectors, and ${rowCount} ` +\n `rows exist. Refusing to serve memory. Admin must run a reindex job.`\n );\n }\n }\n}\n"],"names":["join","dirname","fileURLToPath","DEFAULT_MEMORY_TABLE","readFileSync","getMemoryEmbedder","DEFAULT_MEMORY_DIMENSIONS"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maximal Marginal Relevance (MMR) re-ranking — Phase 2.
|
|
5
|
+
*
|
|
6
|
+
* Ported from upstream `extensions/memory-core/src/memory/mmr.ts` with
|
|
7
|
+
* minor adaptation for our `MemoryEntry` shape (content vs snippet, id vs
|
|
8
|
+
* path+startLine). Behavior is identical: normalize scores, iteratively
|
|
9
|
+
* pick the item that maximizes `λ * relevance - (1-λ) * max_similarity`
|
|
10
|
+
* using Jaccard on tokenized content.
|
|
11
|
+
*
|
|
12
|
+
* @see Carbonell & Goldstein, "The Use of MMR, Diversity-Based Reranking" (1998)
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_MMR_CONFIG = {
|
|
15
|
+
enabled: false,
|
|
16
|
+
lambda: 0.7,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* CJK Unified Ideographs + Extension A, Hiragana/Katakana, Hangul.
|
|
20
|
+
* These lack whitespace boundaries so we must tokenize them differently.
|
|
21
|
+
*/
|
|
22
|
+
const CJK_RE = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af\u1100-\u11ff]/;
|
|
23
|
+
/**
|
|
24
|
+
* Tokenize content into a set for Jaccard similarity.
|
|
25
|
+
*
|
|
26
|
+
* ASCII: alphanumeric + underscore runs, lowercased.
|
|
27
|
+
* CJK: each char becomes a unigram; consecutive pairs become a bigram.
|
|
28
|
+
* Non-adjacent CJK chars (e.g. `我a好`) do NOT form a bigram.
|
|
29
|
+
*/
|
|
30
|
+
function tokenize(text) {
|
|
31
|
+
const lower = (text ?? '').toLowerCase();
|
|
32
|
+
const ascii = lower.match(/[a-z0-9_]+/g) ?? [];
|
|
33
|
+
const chars = Array.from(lower);
|
|
34
|
+
const cjkData = [];
|
|
35
|
+
for (let i = 0; i < chars.length; i++) {
|
|
36
|
+
if (CJK_RE.test(chars[i]))
|
|
37
|
+
cjkData.push({ char: chars[i], index: i });
|
|
38
|
+
}
|
|
39
|
+
const bigrams = [];
|
|
40
|
+
for (let i = 0; i < cjkData.length - 1; i++) {
|
|
41
|
+
if (cjkData[i + 1].index === cjkData[i].index + 1) {
|
|
42
|
+
bigrams.push(cjkData[i].char + cjkData[i + 1].char);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return new Set([...ascii, ...bigrams, ...cjkData.map((d) => d.char)]);
|
|
46
|
+
}
|
|
47
|
+
function jaccardSimilarity(a, b) {
|
|
48
|
+
if (a.size === 0 && b.size === 0)
|
|
49
|
+
return 1;
|
|
50
|
+
if (a.size === 0 || b.size === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
|
|
53
|
+
let intersection = 0;
|
|
54
|
+
for (const t of smaller)
|
|
55
|
+
if (larger.has(t))
|
|
56
|
+
intersection++;
|
|
57
|
+
const union = a.size + b.size - intersection;
|
|
58
|
+
return union === 0 ? 0 : intersection / union;
|
|
59
|
+
}
|
|
60
|
+
function textSimilarity(a, b) {
|
|
61
|
+
return jaccardSimilarity(tokenize(a), tokenize(b));
|
|
62
|
+
}
|
|
63
|
+
function computeMMRScore(relevance, maxSimilarity, lambda) {
|
|
64
|
+
return lambda * relevance - (1 - lambda) * maxSimilarity;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Re-rank items using MMR. Returns a new array in MMR order.
|
|
68
|
+
*/
|
|
69
|
+
function mmrRerank(items, config = {}) {
|
|
70
|
+
const enabled = config.enabled ?? DEFAULT_MMR_CONFIG.enabled;
|
|
71
|
+
const rawLambda = config.lambda ?? DEFAULT_MMR_CONFIG.lambda;
|
|
72
|
+
if (!enabled || items.length <= 1)
|
|
73
|
+
return [...items];
|
|
74
|
+
const lambda = Math.max(0, Math.min(1, rawLambda));
|
|
75
|
+
if (lambda === 1)
|
|
76
|
+
return [...items].sort((a, b) => b.score - a.score);
|
|
77
|
+
const tokenCache = new Map();
|
|
78
|
+
for (const item of items)
|
|
79
|
+
tokenCache.set(item.id, tokenize(item.content));
|
|
80
|
+
const scores = items.map((i) => i.score);
|
|
81
|
+
const maxScore = Math.max(...scores);
|
|
82
|
+
const minScore = Math.min(...scores);
|
|
83
|
+
const range = maxScore - minScore;
|
|
84
|
+
const normalize = (s) => range === 0 ? 1 : (s - minScore) / range;
|
|
85
|
+
const selected = [];
|
|
86
|
+
const remaining = new Set(items);
|
|
87
|
+
while (remaining.size > 0) {
|
|
88
|
+
let best = null;
|
|
89
|
+
let bestMMR = -Infinity;
|
|
90
|
+
for (const cand of remaining) {
|
|
91
|
+
const rel = normalize(cand.score);
|
|
92
|
+
const candTokens = tokenCache.get(cand.id);
|
|
93
|
+
let maxSim = 0;
|
|
94
|
+
for (const sel of selected) {
|
|
95
|
+
const sim = jaccardSimilarity(candTokens, tokenCache.get(sel.id));
|
|
96
|
+
if (sim > maxSim)
|
|
97
|
+
maxSim = sim;
|
|
98
|
+
}
|
|
99
|
+
const mmr = computeMMRScore(rel, maxSim, lambda);
|
|
100
|
+
if (mmr > bestMMR ||
|
|
101
|
+
(mmr === bestMMR && cand.score > (best?.score ?? -Infinity))) {
|
|
102
|
+
bestMMR = mmr;
|
|
103
|
+
best = cand;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!best)
|
|
107
|
+
break;
|
|
108
|
+
selected.push(best);
|
|
109
|
+
remaining.delete(best);
|
|
110
|
+
}
|
|
111
|
+
return selected;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Adapter: apply MMR to an array of MemoryEntry-shaped hits.
|
|
115
|
+
*
|
|
116
|
+
* Uses (path|id|index) as the stable ID so two hits from the same file at
|
|
117
|
+
* different content still get distinct MMR identities.
|
|
118
|
+
*/
|
|
119
|
+
function applyMMRToMemoryHits(results, config = {}) {
|
|
120
|
+
if (results.length <= 1)
|
|
121
|
+
return results;
|
|
122
|
+
const byId = new Map();
|
|
123
|
+
const items = results.map((r, i) => {
|
|
124
|
+
const id = `${r.path}#${r.id}#${i}`;
|
|
125
|
+
byId.set(id, r);
|
|
126
|
+
return { id, score: r.score, content: r.content };
|
|
127
|
+
});
|
|
128
|
+
return mmrRerank(items, config).map((m) => byId.get(m.id));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
exports.DEFAULT_MMR_CONFIG = DEFAULT_MMR_CONFIG;
|
|
132
|
+
exports.applyMMRToMemoryHits = applyMMRToMemoryHits;
|
|
133
|
+
exports.computeMMRScore = computeMMRScore;
|
|
134
|
+
exports.jaccardSimilarity = jaccardSimilarity;
|
|
135
|
+
exports.mmrRerank = mmrRerank;
|
|
136
|
+
exports.textSimilarity = textSimilarity;
|
|
137
|
+
exports.tokenize = tokenize;
|
|
138
|
+
//# sourceMappingURL=mmr.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mmr.cjs","sources":["../../../src/memory/mmr.ts"],"sourcesContent":["/**\n * Maximal Marginal Relevance (MMR) re-ranking — Phase 2.\n *\n * Ported from upstream `extensions/memory-core/src/memory/mmr.ts` with\n * minor adaptation for our `MemoryEntry` shape (content vs snippet, id vs\n * path+startLine). Behavior is identical: normalize scores, iteratively\n * pick the item that maximizes `λ * relevance - (1-λ) * max_similarity`\n * using Jaccard on tokenized content.\n *\n * @see Carbonell & Goldstein, \"The Use of MMR, Diversity-Based Reranking\" (1998)\n */\n\nexport interface MMRConfig {\n /** Opt-in. Upstream default is false. */\n enabled: boolean;\n /** 0 = max diversity, 1 = max relevance. Upstream default 0.7. */\n lambda: number;\n}\n\nexport const DEFAULT_MMR_CONFIG: MMRConfig = {\n enabled: false,\n lambda: 0.7,\n};\n\n/**\n * CJK Unified Ideographs + Extension A, Hiragana/Katakana, Hangul.\n * These lack whitespace boundaries so we must tokenize them differently.\n */\nconst CJK_RE =\n /[\\u3040-\\u309f\\u30a0-\\u30ff\\u3400-\\u4dbf\\u4e00-\\u9fff\\uac00-\\ud7af\\u1100-\\u11ff]/;\n\n/**\n * Tokenize content into a set for Jaccard similarity.\n *\n * ASCII: alphanumeric + underscore runs, lowercased.\n * CJK: each char becomes a unigram; consecutive pairs become a bigram.\n * Non-adjacent CJK chars (e.g. `我a好`) do NOT form a bigram.\n */\nexport function tokenize(text: string): Set<string> {\n const lower = (text ?? '').toLowerCase();\n const ascii = lower.match(/[a-z0-9_]+/g) ?? [];\n\n const chars = Array.from(lower);\n const cjkData: Array<{ char: string; index: number }> = [];\n for (let i = 0; i < chars.length; i++) {\n if (CJK_RE.test(chars[i])) cjkData.push({ char: chars[i], index: i });\n }\n\n const bigrams: string[] = [];\n for (let i = 0; i < cjkData.length - 1; i++) {\n if (cjkData[i + 1].index === cjkData[i].index + 1) {\n bigrams.push(cjkData[i].char + cjkData[i + 1].char);\n }\n }\n\n return new Set([...ascii, ...bigrams, ...cjkData.map((d) => d.char)]);\n}\n\nexport function jaccardSimilarity(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 1;\n if (a.size === 0 || b.size === 0) return 0;\n\n const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];\n let intersection = 0;\n for (const t of smaller) if (larger.has(t)) intersection++;\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nexport function textSimilarity(a: string, b: string): number {\n return jaccardSimilarity(tokenize(a), tokenize(b));\n}\n\nexport function computeMMRScore(\n relevance: number,\n maxSimilarity: number,\n lambda: number\n): number {\n return lambda * relevance - (1 - lambda) * maxSimilarity;\n}\n\nexport interface MMRItem {\n id: string;\n score: number;\n content: string;\n}\n\n/**\n * Re-rank items using MMR. Returns a new array in MMR order.\n */\nexport function mmrRerank<T extends MMRItem>(\n items: T[],\n config: Partial<MMRConfig> = {}\n): T[] {\n const enabled = config.enabled ?? DEFAULT_MMR_CONFIG.enabled;\n const rawLambda = config.lambda ?? DEFAULT_MMR_CONFIG.lambda;\n\n if (!enabled || items.length <= 1) return [...items];\n\n const lambda = Math.max(0, Math.min(1, rawLambda));\n if (lambda === 1) return [...items].sort((a, b) => b.score - a.score);\n\n const tokenCache = new Map<string, Set<string>>();\n for (const item of items) tokenCache.set(item.id, tokenize(item.content));\n\n const scores = items.map((i) => i.score);\n const maxScore = Math.max(...scores);\n const minScore = Math.min(...scores);\n const range = maxScore - minScore;\n const normalize = (s: number): number =>\n range === 0 ? 1 : (s - minScore) / range;\n\n const selected: T[] = [];\n const remaining = new Set(items);\n\n while (remaining.size > 0) {\n let best: T | null = null;\n let bestMMR = -Infinity;\n for (const cand of remaining) {\n const rel = normalize(cand.score);\n const candTokens = tokenCache.get(cand.id)!;\n let maxSim = 0;\n for (const sel of selected) {\n const sim = jaccardSimilarity(candTokens, tokenCache.get(sel.id)!);\n if (sim > maxSim) maxSim = sim;\n }\n const mmr = computeMMRScore(rel, maxSim, lambda);\n if (\n mmr > bestMMR ||\n (mmr === bestMMR && cand.score > (best?.score ?? -Infinity))\n ) {\n bestMMR = mmr;\n best = cand;\n }\n }\n if (!best) break;\n selected.push(best);\n remaining.delete(best);\n }\n\n return selected;\n}\n\n/**\n * Adapter: apply MMR to an array of MemoryEntry-shaped hits.\n *\n * Uses (path|id|index) as the stable ID so two hits from the same file at\n * different content still get distinct MMR identities.\n */\nexport function applyMMRToMemoryHits<\n T extends { id: string; path: string; content: string; score: number },\n>(results: T[], config: Partial<MMRConfig> = {}): T[] {\n if (results.length <= 1) return results;\n const byId = new Map<string, T>();\n const items: MMRItem[] = results.map((r, i) => {\n const id = `${r.path}#${r.id}#${i}`;\n byId.set(id, r);\n return { id, score: r.score, content: r.content };\n });\n return mmrRerank(items, config).map((m) => byId.get(m.id)!);\n}\n"],"names":[],"mappings":"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"}
|