@ai-agent-forge/plugin-memory 0.85.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/README.md +65 -0
- package/agent-forge.json +11 -0
- package/dist/capability.d.ts +182 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +2565 -0
- package/dist/capability.js.map +1 -0
- package/dist/entry.d.ts +36 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +154 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/assistant-card.d.ts +31 -0
- package/dist/memory/assistant-card.d.ts.map +1 -0
- package/dist/memory/assistant-card.js +108 -0
- package/dist/memory/assistant-card.js.map +1 -0
- package/dist/memory/candidates.d.ts +65 -0
- package/dist/memory/candidates.d.ts.map +1 -0
- package/dist/memory/candidates.js +100 -0
- package/dist/memory/candidates.js.map +1 -0
- package/dist/memory/code-memory.d.ts +89 -0
- package/dist/memory/code-memory.d.ts.map +1 -0
- package/dist/memory/code-memory.js +104 -0
- package/dist/memory/code-memory.js.map +1 -0
- package/dist/memory/compaction-sequencer.d.ts +63 -0
- package/dist/memory/compaction-sequencer.d.ts.map +1 -0
- package/dist/memory/compaction-sequencer.js +129 -0
- package/dist/memory/compaction-sequencer.js.map +1 -0
- package/dist/memory/continuation.d.ts +44 -0
- package/dist/memory/continuation.d.ts.map +1 -0
- package/dist/memory/continuation.js +49 -0
- package/dist/memory/continuation.js.map +1 -0
- package/dist/memory/curation.d.ts +58 -0
- package/dist/memory/curation.d.ts.map +1 -0
- package/dist/memory/curation.js +68 -0
- package/dist/memory/curation.js.map +1 -0
- package/dist/memory/egress-policy.d.ts +50 -0
- package/dist/memory/egress-policy.d.ts.map +1 -0
- package/dist/memory/egress-policy.js +71 -0
- package/dist/memory/egress-policy.js.map +1 -0
- package/dist/memory/embedding-provider.d.ts +70 -0
- package/dist/memory/embedding-provider.d.ts.map +1 -0
- package/dist/memory/embedding-provider.js +164 -0
- package/dist/memory/embedding-provider.js.map +1 -0
- package/dist/memory/embedding-reranker.d.ts +56 -0
- package/dist/memory/embedding-reranker.d.ts.map +1 -0
- package/dist/memory/embedding-reranker.js +109 -0
- package/dist/memory/embedding-reranker.js.map +1 -0
- package/dist/memory/foundation.d.ts +168 -0
- package/dist/memory/foundation.d.ts.map +1 -0
- package/dist/memory/foundation.js +487 -0
- package/dist/memory/foundation.js.map +1 -0
- package/dist/memory/host-module-import.d.ts +25 -0
- package/dist/memory/host-module-import.d.ts.map +1 -0
- package/dist/memory/host-module-import.js +41 -0
- package/dist/memory/host-module-import.js.map +1 -0
- package/dist/memory/ledger.d.ts +58 -0
- package/dist/memory/ledger.d.ts.map +1 -0
- package/dist/memory/ledger.js +315 -0
- package/dist/memory/ledger.js.map +1 -0
- package/dist/memory/lifecycle.d.ts +124 -0
- package/dist/memory/lifecycle.d.ts.map +1 -0
- package/dist/memory/lifecycle.js +201 -0
- package/dist/memory/lifecycle.js.map +1 -0
- package/dist/memory/memory-network.d.ts +55 -0
- package/dist/memory/memory-network.d.ts.map +1 -0
- package/dist/memory/memory-network.js +70 -0
- package/dist/memory/memory-network.js.map +1 -0
- package/dist/memory/model-cache-hygiene.d.ts +18 -0
- package/dist/memory/model-cache-hygiene.d.ts.map +1 -0
- package/dist/memory/model-cache-hygiene.js +38 -0
- package/dist/memory/model-cache-hygiene.js.map +1 -0
- package/dist/memory/preference-disambiguator.d.ts +43 -0
- package/dist/memory/preference-disambiguator.d.ts.map +1 -0
- package/dist/memory/preference-disambiguator.js +81 -0
- package/dist/memory/preference-disambiguator.js.map +1 -0
- package/dist/memory/preference-lifecycle.d.ts +66 -0
- package/dist/memory/preference-lifecycle.d.ts.map +1 -0
- package/dist/memory/preference-lifecycle.js +129 -0
- package/dist/memory/preference-lifecycle.js.map +1 -0
- package/dist/memory/preference-promotion.d.ts +87 -0
- package/dist/memory/preference-promotion.d.ts.map +1 -0
- package/dist/memory/preference-promotion.js +102 -0
- package/dist/memory/preference-promotion.js.map +1 -0
- package/dist/memory/preference-resolver.d.ts +44 -0
- package/dist/memory/preference-resolver.d.ts.map +1 -0
- package/dist/memory/preference-resolver.js +107 -0
- package/dist/memory/preference-resolver.js.map +1 -0
- package/dist/memory/purge-journal.d.ts +76 -0
- package/dist/memory/purge-journal.d.ts.map +1 -0
- package/dist/memory/purge-journal.js +130 -0
- package/dist/memory/purge-journal.js.map +1 -0
- package/dist/memory/purge.d.ts +90 -0
- package/dist/memory/purge.d.ts.map +1 -0
- package/dist/memory/purge.js +138 -0
- package/dist/memory/purge.js.map +1 -0
- package/dist/memory/recall-agent.d.ts +84 -0
- package/dist/memory/recall-agent.d.ts.map +1 -0
- package/dist/memory/recall-agent.js +199 -0
- package/dist/memory/recall-agent.js.map +1 -0
- package/dist/memory/recall-index.d.ts +87 -0
- package/dist/memory/recall-index.d.ts.map +1 -0
- package/dist/memory/recall-index.js +222 -0
- package/dist/memory/recall-index.js.map +1 -0
- package/dist/memory/recall-packet.d.ts +121 -0
- package/dist/memory/recall-packet.d.ts.map +1 -0
- package/dist/memory/recall-packet.js +156 -0
- package/dist/memory/recall-packet.js.map +1 -0
- package/dist/memory/scheduler-api.d.ts +99 -0
- package/dist/memory/scheduler-api.d.ts.map +1 -0
- package/dist/memory/scheduler-api.js +93 -0
- package/dist/memory/scheduler-api.js.map +1 -0
- package/dist/memory/scheduler.d.ts +55 -0
- package/dist/memory/scheduler.d.ts.map +1 -0
- package/dist/memory/scheduler.js +91 -0
- package/dist/memory/scheduler.js.map +1 -0
- package/dist/memory/store.d.ts +107 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +208 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/suite-memory.d.ts +208 -0
- package/dist/memory/suite-memory.d.ts.map +1 -0
- package/dist/memory/suite-memory.js +288 -0
- package/dist/memory/suite-memory.js.map +1 -0
- package/dist/memory/transfer.d.ts +142 -0
- package/dist/memory/transfer.d.ts.map +1 -0
- package/dist/memory/transfer.js +210 -0
- package/dist/memory/transfer.js.map +1 -0
- package/dist/memory/vector-index.d.ts +39 -0
- package/dist/memory/vector-index.d.ts.map +1 -0
- package/dist/memory/vector-index.js +136 -0
- package/dist/memory/vector-index.js.map +1 -0
- package/dist/memory/write-budget.d.ts +33 -0
- package/dist/memory/write-budget.d.ts.map +1 -0
- package/dist/memory/write-budget.js +45 -0
- package/dist/memory/write-budget.js.map +1 -0
- package/dist/testing/memory-testkit.d.ts +149 -0
- package/dist/testing/memory-testkit.d.ts.map +1 -0
- package/dist/testing/memory-testkit.js +438 -0
- package/dist/testing/memory-testkit.js.map +1 -0
- package/dist/utils/sync-sleep.d.ts +2 -0
- package/dist/utils/sync-sleep.d.ts.map +1 -0
- package/dist/utils/sync-sleep.js +11 -0
- package/dist/utils/sync-sleep.js.map +1 -0
- package/package.json +56 -0
- package/plugin.json +10 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { validateMemoryAtomV1 } from "./foundation.js";
|
|
2
|
+
/** The single contract version the first-party V1 Foundation reads natively. */
|
|
3
|
+
export const MEMORY_CONTRACT_VERSION_V1 = "agent-forge/memory@1";
|
|
4
|
+
/**
|
|
5
|
+
* The first-party write contract since M5 (方案系统设计 §11 契约版本随 M5
|
|
6
|
+
* 升级): introduces the optional atom `suiteId` dimension. Readers accept both
|
|
7
|
+
* @1 and @2; @1 atoms (no suiteId) read as legacy — invisible to every
|
|
8
|
+
* suite-scoped query and counted in the suite filter diagnostics.
|
|
9
|
+
*/
|
|
10
|
+
export const MEMORY_CONTRACT_VERSION_V2 = "agent-forge/memory@2";
|
|
11
|
+
/** Every contract version the first-party Foundation reads. */
|
|
12
|
+
export const MEMORY_CONTRACT_VERSIONS = Object.freeze([
|
|
13
|
+
MEMORY_CONTRACT_VERSION_V1,
|
|
14
|
+
MEMORY_CONTRACT_VERSION_V2,
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether every observed contract version is covered by the reader's
|
|
18
|
+
* supported set. A single foreign version makes the whole input explicitly
|
|
19
|
+
* unsupported — mixed-version data is never silently interpreted with the
|
|
20
|
+
* default schema (D-035).
|
|
21
|
+
*/
|
|
22
|
+
export function checkMemoryContractCompatibility(input) {
|
|
23
|
+
const supported = Object.freeze([...input.supportedContractVersions]);
|
|
24
|
+
const unsupportedVersions = Object.freeze([...new Set(input.contractVersions)].filter((version) => !supported.includes(version)));
|
|
25
|
+
if (unsupportedVersions.length > 0) {
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
status: "unsupported",
|
|
28
|
+
supportedVersions: supported,
|
|
29
|
+
unsupportedVersions,
|
|
30
|
+
reason: "D-035: mixed or foreign memory contract versions require the versioned migration boundary, " +
|
|
31
|
+
`which the single-replica V1 does not enable. Unsupported: ${unsupportedVersions.join(", ")}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze({ status: "compatible", supportedVersions: supported, unsupportedVersions: [] });
|
|
35
|
+
}
|
|
36
|
+
export function createMemoryUpcasterChain(initialUpcasters = []) {
|
|
37
|
+
const edges = new Map();
|
|
38
|
+
const register = (upcaster) => {
|
|
39
|
+
if (upcaster.fromContractVersion === upcaster.toContractVersion) {
|
|
40
|
+
throw new Error("Upcaster must change the contract version");
|
|
41
|
+
}
|
|
42
|
+
let targets = edges.get(upcaster.fromContractVersion);
|
|
43
|
+
if (!targets) {
|
|
44
|
+
targets = new Map();
|
|
45
|
+
edges.set(upcaster.fromContractVersion, targets);
|
|
46
|
+
}
|
|
47
|
+
if (targets.has(upcaster.toContractVersion)) {
|
|
48
|
+
throw new Error(`Upcaster already registered: ${upcaster.fromContractVersion} -> ${upcaster.toContractVersion}`);
|
|
49
|
+
}
|
|
50
|
+
targets.set(upcaster.toContractVersion, upcaster);
|
|
51
|
+
};
|
|
52
|
+
for (const upcaster of initialUpcasters)
|
|
53
|
+
register(upcaster);
|
|
54
|
+
return {
|
|
55
|
+
register,
|
|
56
|
+
get registeredPaths() {
|
|
57
|
+
return Object.freeze([...edges].flatMap(([from, targets]) => [...targets.keys()].map((to) => `${from}->${to}`)));
|
|
58
|
+
},
|
|
59
|
+
upcast(atom, targetContractVersion) {
|
|
60
|
+
if (atom.contractVersion === targetContractVersion) {
|
|
61
|
+
return { status: "already-current", atom };
|
|
62
|
+
}
|
|
63
|
+
// Walk the chain one hop at a time; every hop produces a validated,
|
|
64
|
+
// frozen atom while the original stays untouched.
|
|
65
|
+
let current = atom;
|
|
66
|
+
const viaVersions = [atom.contractVersion];
|
|
67
|
+
let version = atom.contractVersion;
|
|
68
|
+
const visited = new Set([version]);
|
|
69
|
+
while (version !== targetContractVersion) {
|
|
70
|
+
const targets = edges.get(version);
|
|
71
|
+
const nextEdge = targets ? [...targets.keys()][0] : undefined;
|
|
72
|
+
if (!targets || nextEdge === undefined) {
|
|
73
|
+
return {
|
|
74
|
+
status: "unsupported",
|
|
75
|
+
reason: `No upcaster path from ${version} to ${targetContractVersion} (D-035: V1 ships without migration adapters)`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (visited.has(nextEdge)) {
|
|
79
|
+
return { status: "unsupported", reason: `Upcaster path cycle at ${nextEdge}` };
|
|
80
|
+
}
|
|
81
|
+
visited.add(nextEdge);
|
|
82
|
+
const upcaster = targets.get(nextEdge);
|
|
83
|
+
let converted;
|
|
84
|
+
try {
|
|
85
|
+
converted = upcaster.upcast(current);
|
|
86
|
+
converted = validateMemoryAtomV1({
|
|
87
|
+
...converted,
|
|
88
|
+
contractVersion: nextEdge,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
status: "failed",
|
|
94
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
95
|
+
originalAtom: atom,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
current = converted;
|
|
99
|
+
version = nextEdge;
|
|
100
|
+
viaVersions.push(nextEdge);
|
|
101
|
+
}
|
|
102
|
+
return { status: "upcast", atom: current, viaVersions };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Export / import boundary
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
const MEMORY_BUNDLE_VERSION = 1;
|
|
110
|
+
/**
|
|
111
|
+
* Exports one owner's memories as a self-describing bundle. Mixed contract
|
|
112
|
+
* versions across the input are an explicit `unsupported` (D-035); a bundle
|
|
113
|
+
* never contains a payload summary in place of the atom.
|
|
114
|
+
*/
|
|
115
|
+
export function exportMemoryBundle(input) {
|
|
116
|
+
if (input.owner.trim() === "")
|
|
117
|
+
throw new Error("Export requires a non-empty owner");
|
|
118
|
+
const versions = [...new Set(input.atoms.map((atom) => atom.contractVersion))];
|
|
119
|
+
const compatibility = checkMemoryContractCompatibility({
|
|
120
|
+
supportedContractVersions: MEMORY_CONTRACT_VERSIONS,
|
|
121
|
+
contractVersions: versions,
|
|
122
|
+
});
|
|
123
|
+
if (compatibility.status === "unsupported") {
|
|
124
|
+
// v8 ignore next -- 兼容性检查的 unsupported 结果恒带 reason
|
|
125
|
+
return { status: "unsupported", reason: compatibility.reason ?? "unsupported contract versions" };
|
|
126
|
+
}
|
|
127
|
+
for (const atom of input.atoms) {
|
|
128
|
+
if (atom.owner !== input.owner) {
|
|
129
|
+
return {
|
|
130
|
+
status: "rejected",
|
|
131
|
+
reason: `Atom ${atom.memoryId} belongs to ${atom.owner}, not the export owner ${input.owner}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const foreignPurged = (input.purgedAudit ?? []).filter((stub) => !MEMORY_CONTRACT_VERSIONS.includes(stub.contractVersion));
|
|
136
|
+
if (foreignPurged.length > 0) {
|
|
137
|
+
return { status: "unsupported", reason: "Purged audit stubs carry foreign contract versions" };
|
|
138
|
+
}
|
|
139
|
+
const retentionPolicyVersions = [...new Set(input.atoms.map((atom) => atom.retentionPolicyVersion))];
|
|
140
|
+
return {
|
|
141
|
+
status: "exported",
|
|
142
|
+
bundle: Object.freeze({
|
|
143
|
+
bundleVersion: MEMORY_BUNDLE_VERSION,
|
|
144
|
+
owner: input.owner,
|
|
145
|
+
// The envelope declares the exporter's current write contract; atoms
|
|
146
|
+
// carry their own contractVersion (first-party @1 and @2 mix freely).
|
|
147
|
+
contractVersion: MEMORY_CONTRACT_VERSION_V2,
|
|
148
|
+
retentionPolicyVersions: Object.freeze(retentionPolicyVersions),
|
|
149
|
+
atoms: Object.freeze(input.atoms.map((atom) => validateMemoryAtomV1(atom))),
|
|
150
|
+
lifecycleEvents: Object.freeze([...(input.lifecycleEvents ?? [])]),
|
|
151
|
+
purgedAudit: Object.freeze([...(input.purgedAudit ?? [])]),
|
|
152
|
+
exportedAt: input.exportedAt ?? new Date().toISOString(),
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Imports a bundle after full re-validation. Purged audit identities can never
|
|
158
|
+
* come back as atoms: a bundle whose atoms overlap its purged audit list (or
|
|
159
|
+
* that carries a purged stub with payload) is rejected as tampered.
|
|
160
|
+
*/
|
|
161
|
+
export function importMemoryBundle(bundle, options) {
|
|
162
|
+
if (bundle.bundleVersion !== 1) {
|
|
163
|
+
return { status: "unsupported", reason: `Unsupported memory bundle version: ${String(bundle.bundleVersion)}` };
|
|
164
|
+
}
|
|
165
|
+
if (bundle.owner !== options.owner) {
|
|
166
|
+
return {
|
|
167
|
+
status: "rejected",
|
|
168
|
+
reason: `Bundle owner ${bundle.owner} does not match the importing owner ${options.owner}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const compatibility = checkMemoryContractCompatibility({
|
|
172
|
+
supportedContractVersions: MEMORY_CONTRACT_VERSIONS,
|
|
173
|
+
contractVersions: [bundle.contractVersion, ...bundle.atoms.map((atom) => atom.contractVersion)],
|
|
174
|
+
});
|
|
175
|
+
if (compatibility.status === "unsupported") {
|
|
176
|
+
return { status: "unsupported", reason: compatibility.reason ?? "unsupported contract versions" };
|
|
177
|
+
}
|
|
178
|
+
const purgedIds = new Set(bundle.purgedAudit.map((stub) => stub.memoryId));
|
|
179
|
+
const overlapping = bundle.atoms.filter((atom) => purgedIds.has(atom.memoryId));
|
|
180
|
+
if (overlapping.length > 0) {
|
|
181
|
+
return {
|
|
182
|
+
status: "rejected",
|
|
183
|
+
reason: `Bundle attempts to resurrect purged memories: ${overlapping.map((atom) => atom.memoryId).join(", ")}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const atoms = [];
|
|
187
|
+
for (const atom of bundle.atoms) {
|
|
188
|
+
try {
|
|
189
|
+
atoms.push(validateMemoryAtomV1(atom));
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
status: "rejected",
|
|
194
|
+
// v8 ignore next -- 校验器只抛 Error 对象
|
|
195
|
+
reason: `Atom ${atom.memoryId} failed validation: ${error instanceof Error ? error.message : String(error)}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const stub of bundle.purgedAudit) {
|
|
200
|
+
if ("payload" in stub || "atom" in stub) {
|
|
201
|
+
return { status: "rejected", reason: `Purged audit entry ${stub.memoryId} carries forbidden payload` };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
status: "imported",
|
|
206
|
+
atoms: Object.freeze(atoms),
|
|
207
|
+
skippedPurgedAudit: bundle.purgedAudit.length,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=transfer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../src/memory/transfer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory Foundation — contract compatibility, upcaster chain, and import/export\n * boundaries (1C.1c).\n *\n * Semantics frozen here (记忆系统设计.md §4 + D-035):\n * - Canonical atoms are never rewritten by an upgrade; old atoms are converted\n * on read by versioned upcasters. A failed upcaster preserves the original\n * data and reports the adapter as unavailable — never a silent reinterpretation.\n * - A reader must declare which contract versions it supports; mixed-version\n * input is only processed when every version is supported, otherwise the\n * result is an explicit `unsupported`.\n * - Export/import bundles carry atoms, lifecycle events, source revisions,\n * owner, contractVersion, and policy version. Purged memories export as\n * audit stubs without payload and can never be re-imported as recallable\n * atoms.\n * - D-035: the first-party V1 ships a single replica and a single contract\n * version. Until the D-035 trigger fires (a second managed replica or a\n * second contract version), migration/export capabilities return explicit\n * `unsupported` instead of pretending the distributed protocol exists.\n */\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\n/** The single contract version the first-party V1 Foundation reads natively. */\nexport const MEMORY_CONTRACT_VERSION_V1 = \"agent-forge/memory@1\";\n\n/**\n * The first-party write contract since M5 (方案系统设计 §11 契约版本随 M5\n * 升级): introduces the optional atom `suiteId` dimension. Readers accept both\n * @1 and @2; @1 atoms (no suiteId) read as legacy — invisible to every\n * suite-scoped query and counted in the suite filter diagnostics.\n */\nexport const MEMORY_CONTRACT_VERSION_V2 = \"agent-forge/memory@2\";\n\n/** Every contract version the first-party Foundation reads. */\nexport const MEMORY_CONTRACT_VERSIONS: readonly string[] = Object.freeze([\n\tMEMORY_CONTRACT_VERSION_V1,\n\tMEMORY_CONTRACT_VERSION_V2,\n]);\n\nexport type MemoryContractCompatibilityStatus = \"compatible\" | \"unsupported\";\n\nexport interface MemoryContractCompatibilityResult {\n\treadonly status: MemoryContractCompatibilityStatus;\n\treadonly supportedVersions: readonly string[];\n\treadonly unsupportedVersions: readonly string[];\n\treadonly reason?: string;\n}\n\n/**\n * Checks whether every observed contract version is covered by the reader's\n * supported set. A single foreign version makes the whole input explicitly\n * unsupported — mixed-version data is never silently interpreted with the\n * default schema (D-035).\n */\nexport function checkMemoryContractCompatibility(input: {\n\treadonly supportedContractVersions: readonly string[];\n\treadonly contractVersions: readonly string[];\n}): MemoryContractCompatibilityResult {\n\tconst supported = Object.freeze([...input.supportedContractVersions]);\n\tconst unsupportedVersions = Object.freeze(\n\t\t[...new Set(input.contractVersions)].filter((version) => !supported.includes(version)),\n\t);\n\tif (unsupportedVersions.length > 0) {\n\t\treturn Object.freeze({\n\t\t\tstatus: \"unsupported\",\n\t\t\tsupportedVersions: supported,\n\t\t\tunsupportedVersions,\n\t\t\treason:\n\t\t\t\t\"D-035: mixed or foreign memory contract versions require the versioned migration boundary, \" +\n\t\t\t\t`which the single-replica V1 does not enable. Unsupported: ${unsupportedVersions.join(\", \")}`,\n\t\t});\n\t}\n\treturn Object.freeze({ status: \"compatible\", supportedVersions: supported, unsupportedVersions: [] });\n}\n\n/** One versioned upcaster: converts an atom from `fromContractVersion` to `toContractVersion`. */\nexport interface MemoryUpcaster {\n\treadonly fromContractVersion: string;\n\treadonly toContractVersion: string;\n\tupcast(atom: MemoryAtomV1): MemoryAtomV1;\n}\n\nexport type MemoryUpcastResult =\n\t| { readonly status: \"upcast\"; readonly atom: MemoryAtomV1; readonly viaVersions: readonly string[] }\n\t| { readonly status: \"already-current\"; readonly atom: MemoryAtomV1 }\n\t| { readonly status: \"unsupported\"; readonly reason: string }\n\t| { readonly status: \"failed\"; readonly error: Error; readonly originalAtom: MemoryAtomV1 };\n\n/**\n * A chain of registered upcasters. A missing path reports `unsupported`; a\n * throwing upcaster reports `failed` and preserves the original atom — the\n * canonical data is never partially rewritten.\n */\nexport interface MemoryUpcasterChain {\n\tregister(upcaster: MemoryUpcaster): void;\n\tupcast(atom: MemoryAtomV1, targetContractVersion: string): MemoryUpcastResult;\n\treadonly registeredPaths: readonly string[];\n}\n\nexport function createMemoryUpcasterChain(initialUpcasters: readonly MemoryUpcaster[] = []): MemoryUpcasterChain {\n\tconst edges = new Map<string, Map<string, MemoryUpcaster>>();\n\tconst register = (upcaster: MemoryUpcaster): void => {\n\t\tif (upcaster.fromContractVersion === upcaster.toContractVersion) {\n\t\t\tthrow new Error(\"Upcaster must change the contract version\");\n\t\t}\n\t\tlet targets = edges.get(upcaster.fromContractVersion);\n\t\tif (!targets) {\n\t\t\ttargets = new Map();\n\t\t\tedges.set(upcaster.fromContractVersion, targets);\n\t\t}\n\t\tif (targets.has(upcaster.toContractVersion)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Upcaster already registered: ${upcaster.fromContractVersion} -> ${upcaster.toContractVersion}`,\n\t\t\t);\n\t\t}\n\t\ttargets.set(upcaster.toContractVersion, upcaster);\n\t};\n\tfor (const upcaster of initialUpcasters) register(upcaster);\n\n\treturn {\n\t\tregister,\n\t\tget registeredPaths() {\n\t\t\treturn Object.freeze(\n\t\t\t\t[...edges].flatMap(([from, targets]) => [...targets.keys()].map((to) => `${from}->${to}`)),\n\t\t\t);\n\t\t},\n\t\tupcast(atom, targetContractVersion) {\n\t\t\tif (atom.contractVersion === targetContractVersion) {\n\t\t\t\treturn { status: \"already-current\", atom };\n\t\t\t}\n\t\t\t// Walk the chain one hop at a time; every hop produces a validated,\n\t\t\t// frozen atom while the original stays untouched.\n\t\t\tlet current = atom;\n\t\t\tconst viaVersions: string[] = [atom.contractVersion];\n\t\t\tlet version = atom.contractVersion;\n\t\t\tconst visited = new Set<string>([version]);\n\t\t\twhile (version !== targetContractVersion) {\n\t\t\t\tconst targets = edges.get(version);\n\t\t\t\tconst nextEdge = targets ? [...targets.keys()][0] : undefined;\n\t\t\t\tif (!targets || nextEdge === undefined) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"unsupported\",\n\t\t\t\t\t\treason: `No upcaster path from ${version} to ${targetContractVersion} (D-035: V1 ships without migration adapters)`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (visited.has(nextEdge)) {\n\t\t\t\t\treturn { status: \"unsupported\", reason: `Upcaster path cycle at ${nextEdge}` };\n\t\t\t\t}\n\t\t\t\tvisited.add(nextEdge);\n\t\t\t\tconst upcaster = targets.get(nextEdge)!;\n\t\t\t\tlet converted: MemoryAtomV1;\n\t\t\t\ttry {\n\t\t\t\t\tconverted = upcaster.upcast(current);\n\t\t\t\t\tconverted = validateMemoryAtomV1({\n\t\t\t\t\t\t...converted,\n\t\t\t\t\t\tcontractVersion: nextEdge,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\t\toriginalAtom: atom,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tcurrent = converted;\n\t\t\t\tversion = nextEdge;\n\t\t\t\tviaVersions.push(nextEdge);\n\t\t\t}\n\t\t\treturn { status: \"upcast\", atom: current, viaVersions };\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Export / import boundary\n// ---------------------------------------------------------------------------\n\nconst MEMORY_BUNDLE_VERSION = 1;\n\n/** Audit stub for a purged memory: identity only, never payload. */\nexport interface PurgedMemoryAuditStubV1 {\n\treadonly memoryId: string;\n\treadonly purgeGroupId: string;\n\treadonly contractVersion: string;\n}\n\nexport interface MemoryExportBundleV1 {\n\treadonly bundleVersion: 1;\n\treadonly owner: string;\n\treadonly contractVersion: string;\n\treadonly retentionPolicyVersions: readonly string[];\n\treadonly atoms: readonly MemoryAtomV1[];\n\t/** Versioned lifecycle event payloads; typed and populated by 1C.2. */\n\treadonly lifecycleEvents: readonly unknown[];\n\t/** Audit identities of purged memories — payload is intentionally absent. */\n\treadonly purgedAudit: readonly PurgedMemoryAuditStubV1[];\n\treadonly exportedAt: string;\n}\n\nexport type MemoryExportResult =\n\t| { readonly status: \"exported\"; readonly bundle: MemoryExportBundleV1 }\n\t| { readonly status: \"unsupported\"; readonly reason: string }\n\t| { readonly status: \"rejected\"; readonly reason: string };\n\nexport interface MemoryExportInput {\n\treadonly owner: string;\n\treadonly atoms: readonly MemoryAtomV1[];\n\treadonly lifecycleEvents?: readonly unknown[];\n\treadonly purgedAudit?: readonly PurgedMemoryAuditStubV1[];\n\treadonly exportedAt?: string;\n}\n\n/**\n * Exports one owner's memories as a self-describing bundle. Mixed contract\n * versions across the input are an explicit `unsupported` (D-035); a bundle\n * never contains a payload summary in place of the atom.\n */\nexport function exportMemoryBundle(input: MemoryExportInput): MemoryExportResult {\n\tif (input.owner.trim() === \"\") throw new Error(\"Export requires a non-empty owner\");\n\tconst versions = [...new Set(input.atoms.map((atom) => atom.contractVersion))];\n\tconst compatibility = checkMemoryContractCompatibility({\n\t\tsupportedContractVersions: MEMORY_CONTRACT_VERSIONS,\n\t\tcontractVersions: versions,\n\t});\n\tif (compatibility.status === \"unsupported\") {\n\t\t// v8 ignore next -- 兼容性检查的 unsupported 结果恒带 reason\n\t\treturn { status: \"unsupported\", reason: compatibility.reason ?? \"unsupported contract versions\" };\n\t}\n\tfor (const atom of input.atoms) {\n\t\tif (atom.owner !== input.owner) {\n\t\t\treturn {\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\treason: `Atom ${atom.memoryId} belongs to ${atom.owner}, not the export owner ${input.owner}`,\n\t\t\t};\n\t\t}\n\t}\n\tconst foreignPurged = (input.purgedAudit ?? []).filter(\n\t\t(stub) => !MEMORY_CONTRACT_VERSIONS.includes(stub.contractVersion),\n\t);\n\tif (foreignPurged.length > 0) {\n\t\treturn { status: \"unsupported\", reason: \"Purged audit stubs carry foreign contract versions\" };\n\t}\n\tconst retentionPolicyVersions = [...new Set(input.atoms.map((atom) => atom.retentionPolicyVersion))];\n\treturn {\n\t\tstatus: \"exported\",\n\t\tbundle: Object.freeze({\n\t\t\tbundleVersion: MEMORY_BUNDLE_VERSION,\n\t\t\towner: input.owner,\n\t\t\t// The envelope declares the exporter's current write contract; atoms\n\t\t\t// carry their own contractVersion (first-party @1 and @2 mix freely).\n\t\t\tcontractVersion: MEMORY_CONTRACT_VERSION_V2,\n\t\t\tretentionPolicyVersions: Object.freeze(retentionPolicyVersions),\n\t\t\tatoms: Object.freeze(input.atoms.map((atom) => validateMemoryAtomV1(atom))),\n\t\t\tlifecycleEvents: Object.freeze([...(input.lifecycleEvents ?? [])]),\n\t\t\tpurgedAudit: Object.freeze([...(input.purgedAudit ?? [])]),\n\t\t\texportedAt: input.exportedAt ?? new Date().toISOString(),\n\t\t}),\n\t};\n}\n\nexport type MemoryImportResult =\n\t| { readonly status: \"imported\"; readonly atoms: readonly MemoryAtomV1[]; readonly skippedPurgedAudit: number }\n\t| { readonly status: \"unsupported\"; readonly reason: string }\n\t| { readonly status: \"rejected\"; readonly reason: string };\n\n/**\n * Imports a bundle after full re-validation. Purged audit identities can never\n * come back as atoms: a bundle whose atoms overlap its purged audit list (or\n * that carries a purged stub with payload) is rejected as tampered.\n */\nexport function importMemoryBundle(\n\tbundle: MemoryExportBundleV1,\n\toptions: { readonly owner: string },\n): MemoryImportResult {\n\tif (bundle.bundleVersion !== 1) {\n\t\treturn { status: \"unsupported\", reason: `Unsupported memory bundle version: ${String(bundle.bundleVersion)}` };\n\t}\n\tif (bundle.owner !== options.owner) {\n\t\treturn {\n\t\t\tstatus: \"rejected\",\n\t\t\treason: `Bundle owner ${bundle.owner} does not match the importing owner ${options.owner}`,\n\t\t};\n\t}\n\tconst compatibility = checkMemoryContractCompatibility({\n\t\tsupportedContractVersions: MEMORY_CONTRACT_VERSIONS,\n\t\tcontractVersions: [bundle.contractVersion, ...bundle.atoms.map((atom) => atom.contractVersion)],\n\t});\n\tif (compatibility.status === \"unsupported\") {\n\t\treturn { status: \"unsupported\", reason: compatibility.reason ?? \"unsupported contract versions\" };\n\t}\n\tconst purgedIds = new Set(bundle.purgedAudit.map((stub) => stub.memoryId));\n\tconst overlapping = bundle.atoms.filter((atom) => purgedIds.has(atom.memoryId));\n\tif (overlapping.length > 0) {\n\t\treturn {\n\t\t\tstatus: \"rejected\",\n\t\t\treason: `Bundle attempts to resurrect purged memories: ${overlapping.map((atom) => atom.memoryId).join(\", \")}`,\n\t\t};\n\t}\n\tconst atoms: MemoryAtomV1[] = [];\n\tfor (const atom of bundle.atoms) {\n\t\ttry {\n\t\t\tatoms.push(validateMemoryAtomV1(atom));\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\t// v8 ignore next -- 校验器只抛 Error 对象\n\t\t\t\treason: `Atom ${atom.memoryId} failed validation: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t};\n\t\t}\n\t}\n\tfor (const stub of bundle.purgedAudit) {\n\t\tif (\"payload\" in stub || \"atom\" in stub) {\n\t\t\treturn { status: \"rejected\", reason: `Purged audit entry ${stub.memoryId} carries forbidden payload` };\n\t\t}\n\t}\n\treturn {\n\t\tstatus: \"imported\",\n\t\tatoms: Object.freeze(atoms),\n\t\tskippedPurgedAudit: bundle.purgedAudit.length,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface MemoryVectorIndexEntryV1 {
|
|
2
|
+
readonly memoryId: string;
|
|
3
|
+
readonly owner: string;
|
|
4
|
+
readonly modelId: string;
|
|
5
|
+
readonly statement: string;
|
|
6
|
+
readonly tags: readonly string[];
|
|
7
|
+
readonly vector: readonly number[];
|
|
8
|
+
}
|
|
9
|
+
export interface MemoryVectorIndexQueryV1 {
|
|
10
|
+
readonly owner: string;
|
|
11
|
+
readonly limit: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MemoryVectorHitV1 {
|
|
14
|
+
readonly memoryId: string;
|
|
15
|
+
readonly score: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MemoryVectorIndexV1 {
|
|
18
|
+
readonly replicaId: "memory-vector-index";
|
|
19
|
+
upsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;
|
|
20
|
+
remove(memoryIds: readonly string[]): Promise<void>;
|
|
21
|
+
queryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;
|
|
22
|
+
queryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;
|
|
23
|
+
listMemoryIds(): Promise<ReadonlySet<string>>;
|
|
24
|
+
count(): Promise<number>;
|
|
25
|
+
getStoredModelId(): Promise<string | undefined>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.
|
|
30
|
+
* Table creation is deferred to the first upsert; queries against a
|
|
31
|
+
* not-yet-created table return empty results instead of throwing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createMemoryVectorIndex(options: {
|
|
34
|
+
readonly dbPath: string;
|
|
35
|
+
readonly dimensions: number;
|
|
36
|
+
/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */
|
|
37
|
+
readonly moduleEntryPath?: string;
|
|
38
|
+
}): Promise<MemoryVectorIndexV1>;
|
|
39
|
+
//# sourceMappingURL=vector-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector-index.d.ts","sourceRoot":"","sources":["../../src/memory/vector-index.ts"],"names":[],"mappings":"AA+CA,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,MAAM,CAAC,OAAO,EAAE,SAAS,wBAAwB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,MAAM,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IAC5G,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IACpG,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAChD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4FAA4F;IAC5F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwI/B","sourcesContent":["/**\n * Memory vector index for hybrid retrieval (混合检索 · LanceDB 适配层).\n *\n * POC-verified (2026-09) against @lancedb/lancedb 0.39.0 on a 100k corpus:\n * vector column must be named `vector` (number[] auto-maps to\n * fixed-size-list<float32>), FTS with `Index.fts({ baseTokenizer: \"icu\" })`\n * covers Chinese text, flat-scan KNN on 100k rows takes ~600ms (预期内).\n *\n * Deliberately NOT implemented (POC 结论, 留二期): IVF_PQ ANN index — flat\n * scan is acceptable at current scale; fused hybrid query via\n * `fullTextQuery()` — that chain does not exist in 0.39.0, the caller composes\n * KNN + FTS itself and fuses with RRF (k=60), which is why this adapter's FTS\n * scores use `1/(60 + rank + 1)`.\n *\n * 边界行为 (POC 观察): 空 table 上建 FTS 索引/查询可能抛错 — 空表查询返回\n * 空数组; 建索引包 try/catch 忽略 (注释见 createBodyFtsIndex)。LanceDB 其他\n * 失败原样抛出, 不吞错、不静默降级。\n *\n * Runtime dep @lancedb/lancedb is lazy-loaded via `await import` (惯例参考\n * packages/agent-forge/src/utils/photon.ts); type positions use type-only\n * imports.\n */\nimport type { Connection, Table as LanceDbTable, Query, VectorQuery } from \"@lancedb/lancedb\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\n\n/** Stored row shape: 5 columns; `body` is the FTS text (statement + tags). */\ntype MemoryVectorRecord = {\n\tmemoryId: string;\n\towner: string;\n\tmodelId: string;\n\tbody: string;\n\tvector: number[];\n};\n\ninterface MemoryVectorHitRow {\n\tmemoryId: string;\n\t_distance: number;\n}\n\nconst TABLE_NAME = \"memories\";\n/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */\nconst RRF_K = 60;\n\nfunction escapeSqlLiteral(value: string): string {\n\treturn value.replace(/'/g, \"''\");\n}\n\nexport interface MemoryVectorIndexEntryV1 {\n\treadonly memoryId: string;\n\treadonly owner: string;\n\treadonly modelId: string;\n\treadonly statement: string;\n\treadonly tags: readonly string[];\n\treadonly vector: readonly number[];\n}\n\nexport interface MemoryVectorIndexQueryV1 {\n\treadonly owner: string;\n\treadonly limit: number;\n}\n\nexport interface MemoryVectorHitV1 {\n\treadonly memoryId: string;\n\treadonly score: number;\n}\n\nexport interface MemoryVectorIndexV1 {\n\treadonly replicaId: \"memory-vector-index\";\n\tupsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;\n\tremove(memoryIds: readonly string[]): Promise<void>;\n\tqueryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tqueryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tlistMemoryIds(): Promise<ReadonlySet<string>>;\n\tcount(): Promise<number>;\n\tgetStoredModelId(): Promise<string | undefined>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.\n * Table creation is deferred to the first upsert; queries against a\n * not-yet-created table return empty results instead of throwing.\n */\nexport async function createMemoryVectorIndex(options: {\n\treadonly dbPath: string;\n\treadonly dimensions: number;\n\t/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): Promise<MemoryVectorIndexV1> {\n\tconst dimensions = options.dimensions;\n\tconst lancedb = await importHostOrBareModule<typeof import(\"@lancedb/lancedb\")>(\n\t\toptions.moduleEntryPath ?? \"@lancedb/lancedb\",\n\t);\n\tconst db: Connection = await lancedb.connect(options.dbPath);\n\tlet table: LanceDbTable | null = null;\n\n\tconst ensureTable = async (): Promise<LanceDbTable | null> => {\n\t\tif (table) {\n\t\t\treturn table;\n\t\t}\n\t\tconst names = await db.tableNames();\n\t\tif (!names.includes(TABLE_NAME)) {\n\t\t\treturn null;\n\t\t}\n\t\ttable = await db.openTable(TABLE_NAME);\n\t\treturn table;\n\t};\n\n\t// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表\n\t// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着\n\t// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。\n\tconst createBodyFtsIndex = async (target: LanceDbTable): Promise<void> => {\n\t\ttry {\n\t\t\tawait target.createIndex(\"body\", { config: lancedb.Index.fts({ baseTokenizer: \"icu\" }), replace: true });\n\t\t} catch {\n\t\t\t// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。\n\t\t}\n\t};\n\n\treturn {\n\t\treplicaId: \"memory-vector-index\",\n\n\t\tupsert: async (entries) => {\n\t\t\tif (entries.length === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (entry.vector.length !== dimensions) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst rows: MemoryVectorRecord[] = entries.map((entry) => ({\n\t\t\t\tmemoryId: entry.memoryId,\n\t\t\t\towner: entry.owner,\n\t\t\t\tmodelId: entry.modelId,\n\t\t\t\tbody: `${entry.statement} ${entry.tags.join(\" \")}`,\n\t\t\t\tvector: Array.from(entry.vector),\n\t\t\t}));\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\t// mode \"create\" (非 POC 的 \"overwrite\"): 表不存在才建; 并发竞态下\n\t\t\t\t// 若表已被他人创建则显式抛错, 而不是静默清空重写。\n\t\t\t\ttable = await db.createTable(TABLE_NAME, rows, { mode: \"create\" });\n\t\t\t\tawait createBodyFtsIndex(table);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// mergeInsert upsert: 同 memoryId 幂等更新, 新 id 插入。\n\t\t\tawait current.mergeInsert(\"memoryId\").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);\n\t\t},\n\n\t\tremove: async (memoryIds) => {\n\t\t\tif (memoryIds.length === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\t\tawait current.delete(predicate);\n\t\t},\n\n\t\tqueryKnn: async (vector, query) => {\n\t\t\tif (vector.length !== dimensions) {\n\t\t\t\tthrow new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);\n\t\t\t}\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(Array.from(vector)) as VectorQuery)\n\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t.limit(query.limit)\n\t\t\t\t.toArray();\n\t\t\t// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。\n\t\t\treturn rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));\n\t\t},\n\n\t\tqueryFts: async (queryText, query) => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(queryText) as Query)\n\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t.limit(query.limit)\n\t\t\t\t.toArray();\n\t\t\t// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方\n\t\t\t// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。\n\t\t\treturn rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));\n\t\t},\n\n\t\tlistMemoryIds: async () => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn new Set<string>();\n\t\t\t}\n\t\t\tconst rows: { memoryId: string }[] = await current.query().select([\"memoryId\"]).toArray();\n\t\t\treturn new Set(rows.map((row) => row.memoryId));\n\t\t},\n\n\t\tcount: async () => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\treturn current.countRows();\n\t\t},\n\n\t\tgetStoredModelId: async () => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tconst rows: { modelId: string }[] = await current.query().select([\"modelId\"]).limit(1).toArray();\n\t\t\treturn rows[0]?.modelId;\n\t\t},\n\n\t\tclose: async () => {\n\t\t\tdb.close();\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { importHostOrBareModule } from "./host-module-import.js";
|
|
2
|
+
const TABLE_NAME = "memories";
|
|
3
|
+
/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */
|
|
4
|
+
const RRF_K = 60;
|
|
5
|
+
function escapeSqlLiteral(value) {
|
|
6
|
+
return value.replace(/'/g, "''");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.
|
|
10
|
+
* Table creation is deferred to the first upsert; queries against a
|
|
11
|
+
* not-yet-created table return empty results instead of throwing.
|
|
12
|
+
*/
|
|
13
|
+
export async function createMemoryVectorIndex(options) {
|
|
14
|
+
const dimensions = options.dimensions;
|
|
15
|
+
const lancedb = await importHostOrBareModule(options.moduleEntryPath ?? "@lancedb/lancedb");
|
|
16
|
+
const db = await lancedb.connect(options.dbPath);
|
|
17
|
+
let table = null;
|
|
18
|
+
const ensureTable = async () => {
|
|
19
|
+
if (table) {
|
|
20
|
+
return table;
|
|
21
|
+
}
|
|
22
|
+
const names = await db.tableNames();
|
|
23
|
+
if (!names.includes(TABLE_NAME)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
table = await db.openTable(TABLE_NAME);
|
|
27
|
+
return table;
|
|
28
|
+
};
|
|
29
|
+
// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表
|
|
30
|
+
// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着
|
|
31
|
+
// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。
|
|
32
|
+
const createBodyFtsIndex = async (target) => {
|
|
33
|
+
try {
|
|
34
|
+
await target.createIndex("body", { config: lancedb.Index.fts({ baseTokenizer: "icu" }), replace: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
replicaId: "memory-vector-index",
|
|
42
|
+
upsert: async (entries) => {
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.vector.length !== dimensions) {
|
|
48
|
+
throw new Error(`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const rows = entries.map((entry) => ({
|
|
52
|
+
memoryId: entry.memoryId,
|
|
53
|
+
owner: entry.owner,
|
|
54
|
+
modelId: entry.modelId,
|
|
55
|
+
body: `${entry.statement} ${entry.tags.join(" ")}`,
|
|
56
|
+
vector: Array.from(entry.vector),
|
|
57
|
+
}));
|
|
58
|
+
const current = await ensureTable();
|
|
59
|
+
if (!current) {
|
|
60
|
+
// mode "create" (非 POC 的 "overwrite"): 表不存在才建; 并发竞态下
|
|
61
|
+
// 若表已被他人创建则显式抛错, 而不是静默清空重写。
|
|
62
|
+
table = await db.createTable(TABLE_NAME, rows, { mode: "create" });
|
|
63
|
+
await createBodyFtsIndex(table);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// mergeInsert upsert: 同 memoryId 幂等更新, 新 id 插入。
|
|
67
|
+
await current.mergeInsert("memoryId").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);
|
|
68
|
+
},
|
|
69
|
+
remove: async (memoryIds) => {
|
|
70
|
+
if (memoryIds.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const current = await ensureTable();
|
|
74
|
+
if (!current) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(",")})`;
|
|
78
|
+
await current.delete(predicate);
|
|
79
|
+
},
|
|
80
|
+
queryKnn: async (vector, query) => {
|
|
81
|
+
if (vector.length !== dimensions) {
|
|
82
|
+
throw new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);
|
|
83
|
+
}
|
|
84
|
+
const current = await ensureTable();
|
|
85
|
+
if (!current) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const rows = await current.search(Array.from(vector))
|
|
89
|
+
.where(`owner = '${escapeSqlLiteral(query.owner)}'`)
|
|
90
|
+
.limit(query.limit)
|
|
91
|
+
.toArray();
|
|
92
|
+
// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。
|
|
93
|
+
return rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));
|
|
94
|
+
},
|
|
95
|
+
queryFts: async (queryText, query) => {
|
|
96
|
+
const current = await ensureTable();
|
|
97
|
+
if (!current) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const rows = await current.search(queryText)
|
|
101
|
+
.where(`owner = '${escapeSqlLiteral(query.owner)}'`)
|
|
102
|
+
.limit(query.limit)
|
|
103
|
+
.toArray();
|
|
104
|
+
// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方
|
|
105
|
+
// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。
|
|
106
|
+
return rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));
|
|
107
|
+
},
|
|
108
|
+
listMemoryIds: async () => {
|
|
109
|
+
const current = await ensureTable();
|
|
110
|
+
if (!current) {
|
|
111
|
+
return new Set();
|
|
112
|
+
}
|
|
113
|
+
const rows = await current.query().select(["memoryId"]).toArray();
|
|
114
|
+
return new Set(rows.map((row) => row.memoryId));
|
|
115
|
+
},
|
|
116
|
+
count: async () => {
|
|
117
|
+
const current = await ensureTable();
|
|
118
|
+
if (!current) {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
return current.countRows();
|
|
122
|
+
},
|
|
123
|
+
getStoredModelId: async () => {
|
|
124
|
+
const current = await ensureTable();
|
|
125
|
+
if (!current) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const rows = await current.query().select(["modelId"]).limit(1).toArray();
|
|
129
|
+
return rows[0]?.modelId;
|
|
130
|
+
},
|
|
131
|
+
close: async () => {
|
|
132
|
+
db.close();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=vector-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector-index.js","sourceRoot":"","sources":["../../src/memory/vector-index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory vector index for hybrid retrieval (混合检索 · LanceDB 适配层).\n *\n * POC-verified (2026-09) against @lancedb/lancedb 0.39.0 on a 100k corpus:\n * vector column must be named `vector` (number[] auto-maps to\n * fixed-size-list<float32>), FTS with `Index.fts({ baseTokenizer: \"icu\" })`\n * covers Chinese text, flat-scan KNN on 100k rows takes ~600ms (预期内).\n *\n * Deliberately NOT implemented (POC 结论, 留二期): IVF_PQ ANN index — flat\n * scan is acceptable at current scale; fused hybrid query via\n * `fullTextQuery()` — that chain does not exist in 0.39.0, the caller composes\n * KNN + FTS itself and fuses with RRF (k=60), which is why this adapter's FTS\n * scores use `1/(60 + rank + 1)`.\n *\n * 边界行为 (POC 观察): 空 table 上建 FTS 索引/查询可能抛错 — 空表查询返回\n * 空数组; 建索引包 try/catch 忽略 (注释见 createBodyFtsIndex)。LanceDB 其他\n * 失败原样抛出, 不吞错、不静默降级。\n *\n * Runtime dep @lancedb/lancedb is lazy-loaded via `await import` (惯例参考\n * packages/agent-forge/src/utils/photon.ts); type positions use type-only\n * imports.\n */\nimport type { Connection, Table as LanceDbTable, Query, VectorQuery } from \"@lancedb/lancedb\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\n\n/** Stored row shape: 5 columns; `body` is the FTS text (statement + tags). */\ntype MemoryVectorRecord = {\n\tmemoryId: string;\n\towner: string;\n\tmodelId: string;\n\tbody: string;\n\tvector: number[];\n};\n\ninterface MemoryVectorHitRow {\n\tmemoryId: string;\n\t_distance: number;\n}\n\nconst TABLE_NAME = \"memories\";\n/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */\nconst RRF_K = 60;\n\nfunction escapeSqlLiteral(value: string): string {\n\treturn value.replace(/'/g, \"''\");\n}\n\nexport interface MemoryVectorIndexEntryV1 {\n\treadonly memoryId: string;\n\treadonly owner: string;\n\treadonly modelId: string;\n\treadonly statement: string;\n\treadonly tags: readonly string[];\n\treadonly vector: readonly number[];\n}\n\nexport interface MemoryVectorIndexQueryV1 {\n\treadonly owner: string;\n\treadonly limit: number;\n}\n\nexport interface MemoryVectorHitV1 {\n\treadonly memoryId: string;\n\treadonly score: number;\n}\n\nexport interface MemoryVectorIndexV1 {\n\treadonly replicaId: \"memory-vector-index\";\n\tupsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;\n\tremove(memoryIds: readonly string[]): Promise<void>;\n\tqueryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tqueryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tlistMemoryIds(): Promise<ReadonlySet<string>>;\n\tcount(): Promise<number>;\n\tgetStoredModelId(): Promise<string | undefined>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.\n * Table creation is deferred to the first upsert; queries against a\n * not-yet-created table return empty results instead of throwing.\n */\nexport async function createMemoryVectorIndex(options: {\n\treadonly dbPath: string;\n\treadonly dimensions: number;\n\t/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): Promise<MemoryVectorIndexV1> {\n\tconst dimensions = options.dimensions;\n\tconst lancedb = await importHostOrBareModule<typeof import(\"@lancedb/lancedb\")>(\n\t\toptions.moduleEntryPath ?? \"@lancedb/lancedb\",\n\t);\n\tconst db: Connection = await lancedb.connect(options.dbPath);\n\tlet table: LanceDbTable | null = null;\n\n\tconst ensureTable = async (): Promise<LanceDbTable | null> => {\n\t\tif (table) {\n\t\t\treturn table;\n\t\t}\n\t\tconst names = await db.tableNames();\n\t\tif (!names.includes(TABLE_NAME)) {\n\t\t\treturn null;\n\t\t}\n\t\ttable = await db.openTable(TABLE_NAME);\n\t\treturn table;\n\t};\n\n\t// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表\n\t// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着\n\t// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。\n\tconst createBodyFtsIndex = async (target: LanceDbTable): Promise<void> => {\n\t\ttry {\n\t\t\tawait target.createIndex(\"body\", { config: lancedb.Index.fts({ baseTokenizer: \"icu\" }), replace: true });\n\t\t} catch {\n\t\t\t// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。\n\t\t}\n\t};\n\n\treturn {\n\t\treplicaId: \"memory-vector-index\",\n\n\t\tupsert: async (entries) => {\n\t\t\tif (entries.length === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (entry.vector.length !== dimensions) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst rows: MemoryVectorRecord[] = entries.map((entry) => ({\n\t\t\t\tmemoryId: entry.memoryId,\n\t\t\t\towner: entry.owner,\n\t\t\t\tmodelId: entry.modelId,\n\t\t\t\tbody: `${entry.statement} ${entry.tags.join(\" \")}`,\n\t\t\t\tvector: Array.from(entry.vector),\n\t\t\t}));\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\t// mode \"create\" (非 POC 的 \"overwrite\"): 表不存在才建; 并发竞态下\n\t\t\t\t// 若表已被他人创建则显式抛错, 而不是静默清空重写。\n\t\t\t\ttable = await db.createTable(TABLE_NAME, rows, { mode: \"create\" });\n\t\t\t\tawait createBodyFtsIndex(table);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// mergeInsert upsert: 同 memoryId 幂等更新, 新 id 插入。\n\t\t\tawait current.mergeInsert(\"memoryId\").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);\n\t\t},\n\n\t\tremove: async (memoryIds) => {\n\t\t\tif (memoryIds.length === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\t\tawait current.delete(predicate);\n\t\t},\n\n\t\tqueryKnn: async (vector, query) => {\n\t\t\tif (vector.length !== dimensions) {\n\t\t\t\tthrow new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);\n\t\t\t}\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(Array.from(vector)) as VectorQuery)\n\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t.limit(query.limit)\n\t\t\t\t.toArray();\n\t\t\t// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。\n\t\t\treturn rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));\n\t\t},\n\n\t\tqueryFts: async (queryText, query) => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(queryText) as Query)\n\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t.limit(query.limit)\n\t\t\t\t.toArray();\n\t\t\t// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方\n\t\t\t// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。\n\t\t\treturn rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));\n\t\t},\n\n\t\tlistMemoryIds: async () => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn new Set<string>();\n\t\t\t}\n\t\t\tconst rows: { memoryId: string }[] = await current.query().select([\"memoryId\"]).toArray();\n\t\t\treturn new Set(rows.map((row) => row.memoryId));\n\t\t},\n\n\t\tcount: async () => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\treturn current.countRows();\n\t\t},\n\n\t\tgetStoredModelId: async () => {\n\t\t\tconst current = await ensureTable();\n\t\t\tif (!current) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tconst rows: { modelId: string }[] = await current.query().select([\"modelId\"]).limit(1).toArray();\n\t\t\treturn rows[0]?.modelId;\n\t\t},\n\n\t\tclose: async () => {\n\t\t\tdb.close();\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory write budget (记忆系统设计 §6.4) — the versioned write-side budget
|
|
3
|
+
* every memory write operation must carry (mirror of the recall-side
|
|
4
|
+
* budgets). Resolution rules mirror 2A/1C budget discipline: caller values
|
|
5
|
+
* may only tighten policy defaults; policy defaults clamp down to host hard
|
|
6
|
+
* limits with recorded adjustments; non-positive values are invalid.
|
|
7
|
+
*/
|
|
8
|
+
export interface MemoryWriteBudgetsV1 {
|
|
9
|
+
readonly maxOperationsPerMinute: number;
|
|
10
|
+
readonly maxCandidatesPerOperation: number;
|
|
11
|
+
readonly maxModelCalls: number;
|
|
12
|
+
readonly maxBytes: number;
|
|
13
|
+
readonly maxConcurrentWrites: number;
|
|
14
|
+
}
|
|
15
|
+
export type MemoryWriteBudgetField = keyof MemoryWriteBudgetsV1;
|
|
16
|
+
export declare const MEMORY_WRITE_BUDGET_FIELDS: readonly MemoryWriteBudgetField[];
|
|
17
|
+
export interface MemoryWriteBudgetPolicyV1 {
|
|
18
|
+
/** Version of the write-budget policy governing a write (迁移/审计锚). */
|
|
19
|
+
readonly budgetVersion: string;
|
|
20
|
+
readonly defaults: MemoryWriteBudgetsV1;
|
|
21
|
+
/** Host hard limits — policy defaults clamp down to these. */
|
|
22
|
+
readonly hostLimits: MemoryWriteBudgetsV1;
|
|
23
|
+
/** Caller contributions may only tighten the defaults. */
|
|
24
|
+
readonly requested?: Partial<MemoryWriteBudgetsV1>;
|
|
25
|
+
}
|
|
26
|
+
export interface MemoryWriteBudgetResolutionV1 {
|
|
27
|
+
readonly budgetVersion: string;
|
|
28
|
+
readonly budgets: MemoryWriteBudgetsV1;
|
|
29
|
+
readonly adjustments: readonly string[];
|
|
30
|
+
readonly callerTightened: readonly MemoryWriteBudgetField[];
|
|
31
|
+
}
|
|
32
|
+
export declare function resolveMemoryWriteBudgets(policy: MemoryWriteBudgetPolicyV1): MemoryWriteBudgetResolutionV1;
|
|
33
|
+
//# sourceMappingURL=write-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-budget.d.ts","sourceRoot":"","sources":["../../src/memory/write-budget.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,oBAAoB,CAAC;AAEhE,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAMvE,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACzC,+EAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,gEAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAC1C,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,6BAA6B;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC5D;AAQD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,yBAAyB,GAAG,6BAA6B,CAqC1G","sourcesContent":["/**\n * Memory write budget (记忆系统设计 §6.4) — the versioned write-side budget\n * every memory write operation must carry (mirror of the recall-side\n * budgets). Resolution rules mirror 2A/1C budget discipline: caller values\n * may only tighten policy defaults; policy defaults clamp down to host hard\n * limits with recorded adjustments; non-positive values are invalid.\n */\nexport interface MemoryWriteBudgetsV1 {\n\treadonly maxOperationsPerMinute: number;\n\treadonly maxCandidatesPerOperation: number;\n\treadonly maxModelCalls: number;\n\treadonly maxBytes: number;\n\treadonly maxConcurrentWrites: number;\n}\n\nexport type MemoryWriteBudgetField = keyof MemoryWriteBudgetsV1;\n\nexport const MEMORY_WRITE_BUDGET_FIELDS: readonly MemoryWriteBudgetField[] = [\n\t\"maxOperationsPerMinute\",\n\t\"maxCandidatesPerOperation\",\n\t\"maxModelCalls\",\n\t\"maxBytes\",\n\t\"maxConcurrentWrites\",\n];\n\nexport interface MemoryWriteBudgetPolicyV1 {\n\t/** Version of the write-budget policy governing a write (迁移/审计锚). */\n\treadonly budgetVersion: string;\n\treadonly defaults: MemoryWriteBudgetsV1;\n\t/** Host hard limits — policy defaults clamp down to these. */\n\treadonly hostLimits: MemoryWriteBudgetsV1;\n\t/** Caller contributions may only tighten the defaults. */\n\treadonly requested?: Partial<MemoryWriteBudgetsV1>;\n}\n\nexport interface MemoryWriteBudgetResolutionV1 {\n\treadonly budgetVersion: string;\n\treadonly budgets: MemoryWriteBudgetsV1;\n\treadonly adjustments: readonly string[];\n\treadonly callerTightened: readonly MemoryWriteBudgetField[];\n}\n\nfunction assertBudgetValue(value: unknown, field: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 1) {\n\t\tthrow new Error(`Write budget ${field} must be a positive safe integer`);\n\t}\n}\n\nexport function resolveMemoryWriteBudgets(policy: MemoryWriteBudgetPolicyV1): MemoryWriteBudgetResolutionV1 {\n\tconst adjustments: string[] = [];\n\tconst callerTightened: MemoryWriteBudgetField[] = [];\n\tconst budgets = {} as Record<MemoryWriteBudgetField, number>;\n\n\tfor (const field of MEMORY_WRITE_BUDGET_FIELDS) {\n\t\tassertBudgetValue(policy.defaults[field], `defaults.${field}`);\n\t\tassertBudgetValue(policy.hostLimits[field], `hostLimits.${field}`);\n\t\tconst requested = policy.requested?.[field];\n\t\tif (requested !== undefined) {\n\t\t\tassertBudgetValue(requested, `requested.${field}`);\n\t\t\tif (requested > policy.defaults[field]) {\n\t\t\t\tadjustments.push(\n\t\t\t\t\t`Requested ${field}=${requested} ignored: callers may only tighten (default ${policy.defaults[field]})`,\n\t\t\t\t);\n\t\t\t\tbudgets[field] = policy.defaults[field];\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgets[field] = requested;\n\t\t\tcallerTightened.push(field);\n\t\t\tcontinue;\n\t\t}\n\t\tbudgets[field] = policy.defaults[field];\n\t\tif (policy.defaults[field] > policy.hostLimits[field]) {\n\t\t\tadjustments.push(\n\t\t\t\t`Default ${field}=${policy.defaults[field]} clamped to host limit ${policy.hostLimits[field]}`,\n\t\t\t);\n\t\t\tbudgets[field] = policy.hostLimits[field];\n\t\t}\n\t}\n\n\treturn {\n\t\tbudgetVersion: policy.budgetVersion,\n\t\tbudgets: Object.freeze(budgets) as MemoryWriteBudgetsV1,\n\t\tadjustments: Object.freeze(adjustments),\n\t\tcallerTightened: Object.freeze(callerTightened),\n\t};\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const MEMORY_WRITE_BUDGET_FIELDS = [
|
|
2
|
+
"maxOperationsPerMinute",
|
|
3
|
+
"maxCandidatesPerOperation",
|
|
4
|
+
"maxModelCalls",
|
|
5
|
+
"maxBytes",
|
|
6
|
+
"maxConcurrentWrites",
|
|
7
|
+
];
|
|
8
|
+
function assertBudgetValue(value, field) {
|
|
9
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
10
|
+
throw new Error(`Write budget ${field} must be a positive safe integer`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function resolveMemoryWriteBudgets(policy) {
|
|
14
|
+
const adjustments = [];
|
|
15
|
+
const callerTightened = [];
|
|
16
|
+
const budgets = {};
|
|
17
|
+
for (const field of MEMORY_WRITE_BUDGET_FIELDS) {
|
|
18
|
+
assertBudgetValue(policy.defaults[field], `defaults.${field}`);
|
|
19
|
+
assertBudgetValue(policy.hostLimits[field], `hostLimits.${field}`);
|
|
20
|
+
const requested = policy.requested?.[field];
|
|
21
|
+
if (requested !== undefined) {
|
|
22
|
+
assertBudgetValue(requested, `requested.${field}`);
|
|
23
|
+
if (requested > policy.defaults[field]) {
|
|
24
|
+
adjustments.push(`Requested ${field}=${requested} ignored: callers may only tighten (default ${policy.defaults[field]})`);
|
|
25
|
+
budgets[field] = policy.defaults[field];
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
budgets[field] = requested;
|
|
29
|
+
callerTightened.push(field);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
budgets[field] = policy.defaults[field];
|
|
33
|
+
if (policy.defaults[field] > policy.hostLimits[field]) {
|
|
34
|
+
adjustments.push(`Default ${field}=${policy.defaults[field]} clamped to host limit ${policy.hostLimits[field]}`);
|
|
35
|
+
budgets[field] = policy.hostLimits[field];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
budgetVersion: policy.budgetVersion,
|
|
40
|
+
budgets: Object.freeze(budgets),
|
|
41
|
+
adjustments: Object.freeze(adjustments),
|
|
42
|
+
callerTightened: Object.freeze(callerTightened),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=write-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-budget.js","sourceRoot":"","sources":["../../src/memory/write-budget.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,0BAA0B,GAAsC;IAC5E,wBAAwB;IACxB,2BAA2B;IAC3B,eAAe;IACf,UAAU;IACV,qBAAqB;CACrB,CAAC;AAmBF,SAAS,iBAAiB,CAAC,KAAc,EAAE,KAAa,EAA2B;IAClF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,kCAAkC,CAAC,CAAC;IAC1E,CAAC;AAAA,CACD;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAiC,EAAiC;IAC3G,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,eAAe,GAA6B,EAAE,CAAC;IACrD,MAAM,OAAO,GAAG,EAA4C,CAAC;IAE7D,KAAK,MAAM,KAAK,IAAI,0BAA0B,EAAE,CAAC;QAChD,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,KAAK,EAAE,CAAC,CAAC;QAC/D,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,cAAc,KAAK,EAAE,CAAC,CAAC;QACnE,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC7B,iBAAiB,CAAC,SAAS,EAAE,aAAa,KAAK,EAAE,CAAC,CAAC;YACnD,IAAI,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxC,WAAW,CAAC,IAAI,CACf,aAAa,KAAK,IAAI,SAAS,+CAA+C,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CACvG,CAAC;gBACF,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;gBACxC,SAAS;YACV,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;YAC3B,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5B,SAAS;QACV,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACvD,WAAW,CAAC,IAAI,CACf,WAAW,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,0BAA0B,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAC9F,CAAC;YACF,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC3C,CAAC;IACF,CAAC;IAED,OAAO;QACN,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAyB;QACvD,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;QACvC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC;KAC/C,CAAC;AAAA,CACF","sourcesContent":["/**\n * Memory write budget (记忆系统设计 §6.4) — the versioned write-side budget\n * every memory write operation must carry (mirror of the recall-side\n * budgets). Resolution rules mirror 2A/1C budget discipline: caller values\n * may only tighten policy defaults; policy defaults clamp down to host hard\n * limits with recorded adjustments; non-positive values are invalid.\n */\nexport interface MemoryWriteBudgetsV1 {\n\treadonly maxOperationsPerMinute: number;\n\treadonly maxCandidatesPerOperation: number;\n\treadonly maxModelCalls: number;\n\treadonly maxBytes: number;\n\treadonly maxConcurrentWrites: number;\n}\n\nexport type MemoryWriteBudgetField = keyof MemoryWriteBudgetsV1;\n\nexport const MEMORY_WRITE_BUDGET_FIELDS: readonly MemoryWriteBudgetField[] = [\n\t\"maxOperationsPerMinute\",\n\t\"maxCandidatesPerOperation\",\n\t\"maxModelCalls\",\n\t\"maxBytes\",\n\t\"maxConcurrentWrites\",\n];\n\nexport interface MemoryWriteBudgetPolicyV1 {\n\t/** Version of the write-budget policy governing a write (迁移/审计锚). */\n\treadonly budgetVersion: string;\n\treadonly defaults: MemoryWriteBudgetsV1;\n\t/** Host hard limits — policy defaults clamp down to these. */\n\treadonly hostLimits: MemoryWriteBudgetsV1;\n\t/** Caller contributions may only tighten the defaults. */\n\treadonly requested?: Partial<MemoryWriteBudgetsV1>;\n}\n\nexport interface MemoryWriteBudgetResolutionV1 {\n\treadonly budgetVersion: string;\n\treadonly budgets: MemoryWriteBudgetsV1;\n\treadonly adjustments: readonly string[];\n\treadonly callerTightened: readonly MemoryWriteBudgetField[];\n}\n\nfunction assertBudgetValue(value: unknown, field: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 1) {\n\t\tthrow new Error(`Write budget ${field} must be a positive safe integer`);\n\t}\n}\n\nexport function resolveMemoryWriteBudgets(policy: MemoryWriteBudgetPolicyV1): MemoryWriteBudgetResolutionV1 {\n\tconst adjustments: string[] = [];\n\tconst callerTightened: MemoryWriteBudgetField[] = [];\n\tconst budgets = {} as Record<MemoryWriteBudgetField, number>;\n\n\tfor (const field of MEMORY_WRITE_BUDGET_FIELDS) {\n\t\tassertBudgetValue(policy.defaults[field], `defaults.${field}`);\n\t\tassertBudgetValue(policy.hostLimits[field], `hostLimits.${field}`);\n\t\tconst requested = policy.requested?.[field];\n\t\tif (requested !== undefined) {\n\t\t\tassertBudgetValue(requested, `requested.${field}`);\n\t\t\tif (requested > policy.defaults[field]) {\n\t\t\t\tadjustments.push(\n\t\t\t\t\t`Requested ${field}=${requested} ignored: callers may only tighten (default ${policy.defaults[field]})`,\n\t\t\t\t);\n\t\t\t\tbudgets[field] = policy.defaults[field];\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgets[field] = requested;\n\t\t\tcallerTightened.push(field);\n\t\t\tcontinue;\n\t\t}\n\t\tbudgets[field] = policy.defaults[field];\n\t\tif (policy.defaults[field] > policy.hostLimits[field]) {\n\t\t\tadjustments.push(\n\t\t\t\t`Default ${field}=${policy.defaults[field]} clamped to host limit ${policy.hostLimits[field]}`,\n\t\t\t);\n\t\t\tbudgets[field] = policy.hostLimits[field];\n\t\t}\n\t}\n\n\treturn {\n\t\tbudgetVersion: policy.budgetVersion,\n\t\tbudgets: Object.freeze(budgets) as MemoryWriteBudgetsV1,\n\t\tadjustments: Object.freeze(adjustments),\n\t\tcallerTightened: Object.freeze(callerTightened),\n\t};\n}\n"]}
|