@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.
Files changed (148) hide show
  1. package/README.md +65 -0
  2. package/agent-forge.json +11 -0
  3. package/dist/capability.d.ts +182 -0
  4. package/dist/capability.d.ts.map +1 -0
  5. package/dist/capability.js +2565 -0
  6. package/dist/capability.js.map +1 -0
  7. package/dist/entry.d.ts +36 -0
  8. package/dist/entry.d.ts.map +1 -0
  9. package/dist/entry.js +154 -0
  10. package/dist/entry.js.map +1 -0
  11. package/dist/index.d.ts +49 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +49 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/memory/assistant-card.d.ts +31 -0
  16. package/dist/memory/assistant-card.d.ts.map +1 -0
  17. package/dist/memory/assistant-card.js +108 -0
  18. package/dist/memory/assistant-card.js.map +1 -0
  19. package/dist/memory/candidates.d.ts +65 -0
  20. package/dist/memory/candidates.d.ts.map +1 -0
  21. package/dist/memory/candidates.js +100 -0
  22. package/dist/memory/candidates.js.map +1 -0
  23. package/dist/memory/code-memory.d.ts +89 -0
  24. package/dist/memory/code-memory.d.ts.map +1 -0
  25. package/dist/memory/code-memory.js +104 -0
  26. package/dist/memory/code-memory.js.map +1 -0
  27. package/dist/memory/compaction-sequencer.d.ts +63 -0
  28. package/dist/memory/compaction-sequencer.d.ts.map +1 -0
  29. package/dist/memory/compaction-sequencer.js +129 -0
  30. package/dist/memory/compaction-sequencer.js.map +1 -0
  31. package/dist/memory/continuation.d.ts +44 -0
  32. package/dist/memory/continuation.d.ts.map +1 -0
  33. package/dist/memory/continuation.js +49 -0
  34. package/dist/memory/continuation.js.map +1 -0
  35. package/dist/memory/curation.d.ts +58 -0
  36. package/dist/memory/curation.d.ts.map +1 -0
  37. package/dist/memory/curation.js +68 -0
  38. package/dist/memory/curation.js.map +1 -0
  39. package/dist/memory/egress-policy.d.ts +50 -0
  40. package/dist/memory/egress-policy.d.ts.map +1 -0
  41. package/dist/memory/egress-policy.js +71 -0
  42. package/dist/memory/egress-policy.js.map +1 -0
  43. package/dist/memory/embedding-provider.d.ts +70 -0
  44. package/dist/memory/embedding-provider.d.ts.map +1 -0
  45. package/dist/memory/embedding-provider.js +164 -0
  46. package/dist/memory/embedding-provider.js.map +1 -0
  47. package/dist/memory/embedding-reranker.d.ts +56 -0
  48. package/dist/memory/embedding-reranker.d.ts.map +1 -0
  49. package/dist/memory/embedding-reranker.js +109 -0
  50. package/dist/memory/embedding-reranker.js.map +1 -0
  51. package/dist/memory/foundation.d.ts +168 -0
  52. package/dist/memory/foundation.d.ts.map +1 -0
  53. package/dist/memory/foundation.js +487 -0
  54. package/dist/memory/foundation.js.map +1 -0
  55. package/dist/memory/host-module-import.d.ts +25 -0
  56. package/dist/memory/host-module-import.d.ts.map +1 -0
  57. package/dist/memory/host-module-import.js +41 -0
  58. package/dist/memory/host-module-import.js.map +1 -0
  59. package/dist/memory/ledger.d.ts +58 -0
  60. package/dist/memory/ledger.d.ts.map +1 -0
  61. package/dist/memory/ledger.js +315 -0
  62. package/dist/memory/ledger.js.map +1 -0
  63. package/dist/memory/lifecycle.d.ts +124 -0
  64. package/dist/memory/lifecycle.d.ts.map +1 -0
  65. package/dist/memory/lifecycle.js +201 -0
  66. package/dist/memory/lifecycle.js.map +1 -0
  67. package/dist/memory/memory-network.d.ts +55 -0
  68. package/dist/memory/memory-network.d.ts.map +1 -0
  69. package/dist/memory/memory-network.js +70 -0
  70. package/dist/memory/memory-network.js.map +1 -0
  71. package/dist/memory/model-cache-hygiene.d.ts +18 -0
  72. package/dist/memory/model-cache-hygiene.d.ts.map +1 -0
  73. package/dist/memory/model-cache-hygiene.js +38 -0
  74. package/dist/memory/model-cache-hygiene.js.map +1 -0
  75. package/dist/memory/preference-disambiguator.d.ts +43 -0
  76. package/dist/memory/preference-disambiguator.d.ts.map +1 -0
  77. package/dist/memory/preference-disambiguator.js +81 -0
  78. package/dist/memory/preference-disambiguator.js.map +1 -0
  79. package/dist/memory/preference-lifecycle.d.ts +66 -0
  80. package/dist/memory/preference-lifecycle.d.ts.map +1 -0
  81. package/dist/memory/preference-lifecycle.js +129 -0
  82. package/dist/memory/preference-lifecycle.js.map +1 -0
  83. package/dist/memory/preference-promotion.d.ts +87 -0
  84. package/dist/memory/preference-promotion.d.ts.map +1 -0
  85. package/dist/memory/preference-promotion.js +102 -0
  86. package/dist/memory/preference-promotion.js.map +1 -0
  87. package/dist/memory/preference-resolver.d.ts +44 -0
  88. package/dist/memory/preference-resolver.d.ts.map +1 -0
  89. package/dist/memory/preference-resolver.js +107 -0
  90. package/dist/memory/preference-resolver.js.map +1 -0
  91. package/dist/memory/purge-journal.d.ts +76 -0
  92. package/dist/memory/purge-journal.d.ts.map +1 -0
  93. package/dist/memory/purge-journal.js +130 -0
  94. package/dist/memory/purge-journal.js.map +1 -0
  95. package/dist/memory/purge.d.ts +90 -0
  96. package/dist/memory/purge.d.ts.map +1 -0
  97. package/dist/memory/purge.js +138 -0
  98. package/dist/memory/purge.js.map +1 -0
  99. package/dist/memory/recall-agent.d.ts +84 -0
  100. package/dist/memory/recall-agent.d.ts.map +1 -0
  101. package/dist/memory/recall-agent.js +199 -0
  102. package/dist/memory/recall-agent.js.map +1 -0
  103. package/dist/memory/recall-index.d.ts +87 -0
  104. package/dist/memory/recall-index.d.ts.map +1 -0
  105. package/dist/memory/recall-index.js +222 -0
  106. package/dist/memory/recall-index.js.map +1 -0
  107. package/dist/memory/recall-packet.d.ts +121 -0
  108. package/dist/memory/recall-packet.d.ts.map +1 -0
  109. package/dist/memory/recall-packet.js +156 -0
  110. package/dist/memory/recall-packet.js.map +1 -0
  111. package/dist/memory/scheduler-api.d.ts +99 -0
  112. package/dist/memory/scheduler-api.d.ts.map +1 -0
  113. package/dist/memory/scheduler-api.js +93 -0
  114. package/dist/memory/scheduler-api.js.map +1 -0
  115. package/dist/memory/scheduler.d.ts +55 -0
  116. package/dist/memory/scheduler.d.ts.map +1 -0
  117. package/dist/memory/scheduler.js +91 -0
  118. package/dist/memory/scheduler.js.map +1 -0
  119. package/dist/memory/store.d.ts +107 -0
  120. package/dist/memory/store.d.ts.map +1 -0
  121. package/dist/memory/store.js +208 -0
  122. package/dist/memory/store.js.map +1 -0
  123. package/dist/memory/suite-memory.d.ts +208 -0
  124. package/dist/memory/suite-memory.d.ts.map +1 -0
  125. package/dist/memory/suite-memory.js +288 -0
  126. package/dist/memory/suite-memory.js.map +1 -0
  127. package/dist/memory/transfer.d.ts +142 -0
  128. package/dist/memory/transfer.d.ts.map +1 -0
  129. package/dist/memory/transfer.js +210 -0
  130. package/dist/memory/transfer.js.map +1 -0
  131. package/dist/memory/vector-index.d.ts +39 -0
  132. package/dist/memory/vector-index.d.ts.map +1 -0
  133. package/dist/memory/vector-index.js +136 -0
  134. package/dist/memory/vector-index.js.map +1 -0
  135. package/dist/memory/write-budget.d.ts +33 -0
  136. package/dist/memory/write-budget.d.ts.map +1 -0
  137. package/dist/memory/write-budget.js +45 -0
  138. package/dist/memory/write-budget.js.map +1 -0
  139. package/dist/testing/memory-testkit.d.ts +149 -0
  140. package/dist/testing/memory-testkit.d.ts.map +1 -0
  141. package/dist/testing/memory-testkit.js +438 -0
  142. package/dist/testing/memory-testkit.js.map +1 -0
  143. package/dist/utils/sync-sleep.d.ts +2 -0
  144. package/dist/utils/sync-sleep.d.ts.map +1 -0
  145. package/dist/utils/sync-sleep.js +11 -0
  146. package/dist/utils/sync-sleep.js.map +1 -0
  147. package/package.json +56 -0
  148. package/plugin.json +10 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Memory Foundation — opaque continuation token (1C.5c).
3
+ *
4
+ * 后续召回轮次使用服务端签发的 opaque continuation token(记忆系统设计.md
5
+ * §6.4):token 本身不含任何可读数据,绑定状态(operation、lease、instance、
6
+ * owner/profile、scope、剩余预算、轮次)保存在服务端。resolve 时逐项
7
+ * 校验绑定关系——owner、lease、instance 或 operation 不匹配即显式拒绝,调用方
8
+ * 不能改写 operationId、scope 或任何上限。
9
+ */
10
+ import type { MemoryScopeV1 } from "./foundation.ts";
11
+ import type { MemoryRecallBudgetsV1 } from "./recall-packet.ts";
12
+ export interface MemoryContinuationStateV1 {
13
+ readonly operationId: string;
14
+ readonly leaseId: string;
15
+ readonly instanceId: string;
16
+ readonly owner: string;
17
+ readonly profileId?: string;
18
+ readonly scope?: MemoryScopeV1;
19
+ /** Remaining budgets for the continuation rounds (never expandable). */
20
+ readonly remainingBudgets: MemoryRecallBudgetsV1;
21
+ /** Next attempt number (monotonic per token). */
22
+ readonly nextAttemptNo: number;
23
+ }
24
+ export interface MemoryContinuationResolveInput {
25
+ readonly token: string;
26
+ readonly expectedLeaseId?: string;
27
+ readonly expectedInstanceId?: string;
28
+ readonly expectedOperationId?: string;
29
+ }
30
+ export interface MemoryContinuationIssuerV1 {
31
+ /** Issues an opaque token for the given bound state. */
32
+ issue(state: MemoryContinuationStateV1): string;
33
+ /** Resolves a token, enforcing every binding; mismatches are explicit errors. */
34
+ resolve(input: MemoryContinuationResolveInput): MemoryContinuationStateV1;
35
+ /** Marks the token consumed after a round, storing the advanced state. */
36
+ advance(token: string, nextState: MemoryContinuationStateV1): void;
37
+ inspect(): {
38
+ readonly outstandingTokens: number;
39
+ };
40
+ }
41
+ export declare function createMemoryContinuationIssuer(options?: {
42
+ readonly tokenIdFactory?: (state: MemoryContinuationStateV1) => string;
43
+ }): MemoryContinuationIssuerV1;
44
+ //# sourceMappingURL=continuation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../../src/memory/continuation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhE,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC;IACjD,iDAAiD;IACjD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,0BAA0B;IAC1C,wDAAwD;IACxD,KAAK,CAAC,KAAK,EAAE,yBAAyB,GAAG,MAAM,CAAC;IAChD,iFAAiF;IACjF,OAAO,CAAC,KAAK,EAAE,8BAA8B,GAAG,yBAAyB,CAAC;IAC1E,0EAA0E;IAC1E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,yBAAyB,GAAG,IAAI,CAAC;IACnE,OAAO,IAAI;QAAE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD;AAMD,wBAAgB,8BAA8B,CAC7C,OAAO,GAAE;IAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,MAAM,CAAA;CAAO,GACtF,0BAA0B,CA2C5B","sourcesContent":["/**\n * Memory Foundation — opaque continuation token (1C.5c).\n *\n * 后续召回轮次使用服务端签发的 opaque continuation token(记忆系统设计.md\n * §6.4):token 本身不含任何可读数据,绑定状态(operation、lease、instance、\n * owner/profile、scope、剩余预算、轮次)保存在服务端。resolve 时逐项\n * 校验绑定关系——owner、lease、instance 或 operation 不匹配即显式拒绝,调用方\n * 不能改写 operationId、scope 或任何上限。\n */\nimport type { MemoryScopeV1 } from \"./foundation.ts\";\nimport type { MemoryRecallBudgetsV1 } from \"./recall-packet.ts\";\n\nexport interface MemoryContinuationStateV1 {\n\treadonly operationId: string;\n\treadonly leaseId: string;\n\treadonly instanceId: string;\n\treadonly owner: string;\n\treadonly profileId?: string;\n\treadonly scope?: MemoryScopeV1;\n\t/** Remaining budgets for the continuation rounds (never expandable). */\n\treadonly remainingBudgets: MemoryRecallBudgetsV1;\n\t/** Next attempt number (monotonic per token). */\n\treadonly nextAttemptNo: number;\n}\n\nexport interface MemoryContinuationResolveInput {\n\treadonly token: string;\n\treadonly expectedLeaseId?: string;\n\treadonly expectedInstanceId?: string;\n\treadonly expectedOperationId?: string;\n}\n\nexport interface MemoryContinuationIssuerV1 {\n\t/** Issues an opaque token for the given bound state. */\n\tissue(state: MemoryContinuationStateV1): string;\n\t/** Resolves a token, enforcing every binding; mismatches are explicit errors. */\n\tresolve(input: MemoryContinuationResolveInput): MemoryContinuationStateV1;\n\t/** Marks the token consumed after a round, storing the advanced state. */\n\tadvance(token: string, nextState: MemoryContinuationStateV1): void;\n\tinspect(): { readonly outstandingTokens: number };\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\nexport function createMemoryContinuationIssuer(\n\toptions: { readonly tokenIdFactory?: (state: MemoryContinuationStateV1) => string } = {},\n): MemoryContinuationIssuerV1 {\n\tconst states = new Map<string, MemoryContinuationStateV1>();\n\tlet sequence = 0;\n\tconst tokenIdFactory =\n\t\toptions.tokenIdFactory ??\n\t\t(() => {\n\t\t\tsequence += 1;\n\t\t\treturn `mcont-${Date.now().toString(36)}-${sequence}-${Math.random().toString(36).slice(2, 10)}`;\n\t\t});\n\n\treturn {\n\t\tissue(state) {\n\t\t\tassertNonEmptyString(state.operationId, \"continuation state.operationId\");\n\t\t\tassertNonEmptyString(state.leaseId, \"continuation state.leaseId\");\n\t\t\tassertNonEmptyString(state.instanceId, \"continuation state.instanceId\");\n\t\t\tassertNonEmptyString(state.owner, \"continuation state.owner\");\n\t\t\tconst token = tokenIdFactory(state);\n\t\t\tstates.set(token, Object.freeze({ ...state }));\n\t\t\treturn token;\n\t\t},\n\t\tresolve({ token, expectedLeaseId, expectedInstanceId, expectedOperationId }) {\n\t\t\tassertNonEmptyString(token, \"continuation token\");\n\t\t\tconst state = states.get(token);\n\t\t\tif (!state) throw new Error(`Unknown continuation token`);\n\t\t\tif (expectedOperationId !== undefined && expectedOperationId !== state.operationId) {\n\t\t\t\tthrow new Error(`Continuation token is bound to operation ${state.operationId}`);\n\t\t\t}\n\t\t\tif (expectedLeaseId !== undefined && expectedLeaseId !== state.leaseId) {\n\t\t\t\tthrow new Error(`Continuation token is bound to lease ${state.leaseId}`);\n\t\t\t}\n\t\t\tif (expectedInstanceId !== undefined && expectedInstanceId !== state.instanceId) {\n\t\t\t\tthrow new Error(`Continuation token is bound to instance ${state.instanceId}`);\n\t\t\t}\n\t\t\treturn state;\n\t\t},\n\t\tadvance(token, nextState) {\n\t\t\tif (!states.has(token)) throw new Error(\"Unknown continuation token\");\n\t\t\tstates.set(token, Object.freeze({ ...nextState }));\n\t\t},\n\t\tinspect() {\n\t\t\treturn { outstandingTokens: states.size };\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,49 @@
1
+ function assertNonEmptyString(value, label) {
2
+ if (typeof value !== "string" || value.trim().length === 0)
3
+ throw new Error(`${label} must be a non-empty string`);
4
+ }
5
+ export function createMemoryContinuationIssuer(options = {}) {
6
+ const states = new Map();
7
+ let sequence = 0;
8
+ const tokenIdFactory = options.tokenIdFactory ??
9
+ (() => {
10
+ sequence += 1;
11
+ return `mcont-${Date.now().toString(36)}-${sequence}-${Math.random().toString(36).slice(2, 10)}`;
12
+ });
13
+ return {
14
+ issue(state) {
15
+ assertNonEmptyString(state.operationId, "continuation state.operationId");
16
+ assertNonEmptyString(state.leaseId, "continuation state.leaseId");
17
+ assertNonEmptyString(state.instanceId, "continuation state.instanceId");
18
+ assertNonEmptyString(state.owner, "continuation state.owner");
19
+ const token = tokenIdFactory(state);
20
+ states.set(token, Object.freeze({ ...state }));
21
+ return token;
22
+ },
23
+ resolve({ token, expectedLeaseId, expectedInstanceId, expectedOperationId }) {
24
+ assertNonEmptyString(token, "continuation token");
25
+ const state = states.get(token);
26
+ if (!state)
27
+ throw new Error(`Unknown continuation token`);
28
+ if (expectedOperationId !== undefined && expectedOperationId !== state.operationId) {
29
+ throw new Error(`Continuation token is bound to operation ${state.operationId}`);
30
+ }
31
+ if (expectedLeaseId !== undefined && expectedLeaseId !== state.leaseId) {
32
+ throw new Error(`Continuation token is bound to lease ${state.leaseId}`);
33
+ }
34
+ if (expectedInstanceId !== undefined && expectedInstanceId !== state.instanceId) {
35
+ throw new Error(`Continuation token is bound to instance ${state.instanceId}`);
36
+ }
37
+ return state;
38
+ },
39
+ advance(token, nextState) {
40
+ if (!states.has(token))
41
+ throw new Error("Unknown continuation token");
42
+ states.set(token, Object.freeze({ ...nextState }));
43
+ },
44
+ inspect() {
45
+ return { outstandingTokens: states.size };
46
+ },
47
+ };
48
+ }
49
+ //# sourceMappingURL=continuation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation.js","sourceRoot":"","sources":["../../src/memory/continuation.ts"],"names":[],"mappings":"AA0CA,SAAS,oBAAoB,CAAC,KAAc,EAAE,KAAa,EAA2B;IACrF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,6BAA6B,CAAC,CAAC;AAAA,CACnH;AAED,MAAM,UAAU,8BAA8B,CAC7C,OAAO,GAA+E,EAAE,EAC3D;IAC7B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqC,CAAC;IAC5D,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,MAAM,cAAc,GACnB,OAAO,CAAC,cAAc;QACtB,CAAC,GAAG,EAAE,CAAC;YACN,QAAQ,IAAI,CAAC,CAAC;YACd,OAAO,SAAS,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAAA,CACjG,CAAC,CAAC;IAEJ,OAAO;QACN,KAAK,CAAC,KAAK,EAAE;YACZ,oBAAoB,CAAC,KAAK,CAAC,WAAW,EAAE,gCAAgC,CAAC,CAAC;YAC1E,oBAAoB,CAAC,KAAK,CAAC,OAAO,EAAE,4BAA4B,CAAC,CAAC;YAClE,oBAAoB,CAAC,KAAK,CAAC,UAAU,EAAE,+BAA+B,CAAC,CAAC;YACxE,oBAAoB,CAAC,KAAK,CAAC,KAAK,EAAE,0BAA0B,CAAC,CAAC;YAC9D,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;YACpC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;YAC/C,OAAO,KAAK,CAAC;QAAA,CACb;QACD,OAAO,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,EAAE;YAC5E,oBAAoB,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC;YAClD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAChC,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;YAC1D,IAAI,mBAAmB,KAAK,SAAS,IAAI,mBAAmB,KAAK,KAAK,CAAC,WAAW,EAAE,CAAC;gBACpF,MAAM,IAAI,KAAK,CAAC,4CAA4C,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAClF,CAAC;YACD,IAAI,eAAe,KAAK,SAAS,IAAI,eAAe,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;gBACxE,MAAM,IAAI,KAAK,CAAC,wCAAwC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,kBAAkB,KAAK,SAAS,IAAI,kBAAkB,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC;gBACjF,MAAM,IAAI,KAAK,CAAC,2CAA2C,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;YAChF,CAAC;YACD,OAAO,KAAK,CAAC;QAAA,CACb;QACD,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE;YACzB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;YACtE,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC;QAAA,CACnD;QACD,OAAO,GAAG;YACT,OAAO,EAAE,iBAAiB,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QAAA,CAC1C;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * Memory Foundation — opaque continuation token (1C.5c).\n *\n * 后续召回轮次使用服务端签发的 opaque continuation token(记忆系统设计.md\n * §6.4):token 本身不含任何可读数据,绑定状态(operation、lease、instance、\n * owner/profile、scope、剩余预算、轮次)保存在服务端。resolve 时逐项\n * 校验绑定关系——owner、lease、instance 或 operation 不匹配即显式拒绝,调用方\n * 不能改写 operationId、scope 或任何上限。\n */\nimport type { MemoryScopeV1 } from \"./foundation.ts\";\nimport type { MemoryRecallBudgetsV1 } from \"./recall-packet.ts\";\n\nexport interface MemoryContinuationStateV1 {\n\treadonly operationId: string;\n\treadonly leaseId: string;\n\treadonly instanceId: string;\n\treadonly owner: string;\n\treadonly profileId?: string;\n\treadonly scope?: MemoryScopeV1;\n\t/** Remaining budgets for the continuation rounds (never expandable). */\n\treadonly remainingBudgets: MemoryRecallBudgetsV1;\n\t/** Next attempt number (monotonic per token). */\n\treadonly nextAttemptNo: number;\n}\n\nexport interface MemoryContinuationResolveInput {\n\treadonly token: string;\n\treadonly expectedLeaseId?: string;\n\treadonly expectedInstanceId?: string;\n\treadonly expectedOperationId?: string;\n}\n\nexport interface MemoryContinuationIssuerV1 {\n\t/** Issues an opaque token for the given bound state. */\n\tissue(state: MemoryContinuationStateV1): string;\n\t/** Resolves a token, enforcing every binding; mismatches are explicit errors. */\n\tresolve(input: MemoryContinuationResolveInput): MemoryContinuationStateV1;\n\t/** Marks the token consumed after a round, storing the advanced state. */\n\tadvance(token: string, nextState: MemoryContinuationStateV1): void;\n\tinspect(): { readonly outstandingTokens: number };\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\nexport function createMemoryContinuationIssuer(\n\toptions: { readonly tokenIdFactory?: (state: MemoryContinuationStateV1) => string } = {},\n): MemoryContinuationIssuerV1 {\n\tconst states = new Map<string, MemoryContinuationStateV1>();\n\tlet sequence = 0;\n\tconst tokenIdFactory =\n\t\toptions.tokenIdFactory ??\n\t\t(() => {\n\t\t\tsequence += 1;\n\t\t\treturn `mcont-${Date.now().toString(36)}-${sequence}-${Math.random().toString(36).slice(2, 10)}`;\n\t\t});\n\n\treturn {\n\t\tissue(state) {\n\t\t\tassertNonEmptyString(state.operationId, \"continuation state.operationId\");\n\t\t\tassertNonEmptyString(state.leaseId, \"continuation state.leaseId\");\n\t\t\tassertNonEmptyString(state.instanceId, \"continuation state.instanceId\");\n\t\t\tassertNonEmptyString(state.owner, \"continuation state.owner\");\n\t\t\tconst token = tokenIdFactory(state);\n\t\t\tstates.set(token, Object.freeze({ ...state }));\n\t\t\treturn token;\n\t\t},\n\t\tresolve({ token, expectedLeaseId, expectedInstanceId, expectedOperationId }) {\n\t\t\tassertNonEmptyString(token, \"continuation token\");\n\t\t\tconst state = states.get(token);\n\t\t\tif (!state) throw new Error(`Unknown continuation token`);\n\t\t\tif (expectedOperationId !== undefined && expectedOperationId !== state.operationId) {\n\t\t\t\tthrow new Error(`Continuation token is bound to operation ${state.operationId}`);\n\t\t\t}\n\t\t\tif (expectedLeaseId !== undefined && expectedLeaseId !== state.leaseId) {\n\t\t\t\tthrow new Error(`Continuation token is bound to lease ${state.leaseId}`);\n\t\t\t}\n\t\t\tif (expectedInstanceId !== undefined && expectedInstanceId !== state.instanceId) {\n\t\t\t\tthrow new Error(`Continuation token is bound to instance ${state.instanceId}`);\n\t\t\t}\n\t\t\treturn state;\n\t\t},\n\t\tadvance(token, nextState) {\n\t\t\tif (!states.has(token)) throw new Error(\"Unknown continuation token\");\n\t\t\tstates.set(token, Object.freeze({ ...nextState }));\n\t\t},\n\t\tinspect() {\n\t\t\treturn { outstandingTokens: states.size };\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Memory Foundation — memory-curation-agent candidate normalization, dedupe,
3
+ * and commit (1C.6a).
4
+ *
5
+ * Deterministic curation core (记忆系统设计.md §4/§5.1):
6
+ * - Normalization maps the preference envelope evidence class onto the atom's
7
+ * evidence class without losing the original category: `explicit` and
8
+ * `repeated_behavior` are preserved as-is, `inferred` maps to `derived`.
9
+ * - Dedupe eliminates only retries of the SAME observation
10
+ * (`(owner, observationId)` identity). Same payload under a different
11
+ * observation id is a different experience and is never merged by text
12
+ * similarity.
13
+ * - A memoryId that already exists under a different observation identity is
14
+ * an explicit conflict rejection; the original record stays untouched.
15
+ */
16
+ import type { MemoryAtomV1 } from "./foundation.ts";
17
+ import type { CommittedMemoryV1, MemoryStoreV1 } from "./store.ts";
18
+ export interface MemoryEvidenceMappingV1 {
19
+ readonly from: MemoryAtomV1["preference"] extends undefined ? never : NonNullable<MemoryAtomV1["preference"]>["evidence"]["class"];
20
+ readonly to: MemoryAtomV1["evidenceClass"];
21
+ }
22
+ export type MemoryCurationResultV1 = {
23
+ readonly status: "committed";
24
+ readonly committed: CommittedMemoryV1;
25
+ readonly evidenceMapping?: {
26
+ readonly from: string;
27
+ readonly to: string;
28
+ };
29
+ } | {
30
+ readonly status: "deduped";
31
+ readonly committed: CommittedMemoryV1;
32
+ } | {
33
+ readonly status: "conflict";
34
+ readonly reason: string;
35
+ } | {
36
+ readonly status: "rejected";
37
+ readonly reason: string;
38
+ };
39
+ export interface MemoryCurationAgentV1 {
40
+ /** Validates and normalizes a draft atom without committing it. */
41
+ normalize(draft: MemoryAtomV1): {
42
+ readonly status: "normalized";
43
+ readonly atom: MemoryAtomV1;
44
+ readonly evidenceMapping?: {
45
+ readonly from: string;
46
+ readonly to: string;
47
+ };
48
+ } | {
49
+ readonly status: "rejected";
50
+ readonly reason: string;
51
+ };
52
+ /** Normalizes, dedupes by observation identity, and commits into the store. */
53
+ commit(draft: MemoryAtomV1): MemoryCurationResultV1;
54
+ }
55
+ export declare function createMemoryCurationAgent(options: {
56
+ readonly store: MemoryStoreV1;
57
+ }): MemoryCurationAgentV1;
58
+ //# sourceMappingURL=curation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"curation.d.ts","sourceRoot":"","sources":["../../src/memory/curation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,YAAY,CAAC,SAAS,SAAS,GACxD,KAAK,GACL,WAAW,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC;IAChE,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,sBAAsB,GAC/B;IACA,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,eAAe,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAA;CAAE,GACrE;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACrC,mEAAmE;IACnE,SAAS,CAAC,KAAK,EAAE,YAAY,GAC1B;QACA,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;QAC9B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;QAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;KACzE,GACD;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,+EAA+E;IAC/E,MAAM,CAAC,KAAK,EAAE,YAAY,GAAG,sBAAsB,CAAC;CACpD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAAG,qBAAqB,CAgE3G","sourcesContent":["/**\n * Memory Foundation — memory-curation-agent candidate normalization, dedupe,\n * and commit (1C.6a).\n *\n * Deterministic curation core (记忆系统设计.md §4/§5.1):\n * - Normalization maps the preference envelope evidence class onto the atom's\n * evidence class without losing the original category: `explicit` and\n * `repeated_behavior` are preserved as-is, `inferred` maps to `derived`.\n * - Dedupe eliminates only retries of the SAME observation\n * (`(owner, observationId)` identity). Same payload under a different\n * observation id is a different experience and is never merged by text\n * similarity.\n * - A memoryId that already exists under a different observation identity is\n * an explicit conflict rejection; the original record stays untouched.\n */\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { buildMemoryAtomV1, memoryObservationKey } from \"./foundation.ts\";\nimport type { CommittedMemoryV1, MemoryStoreV1 } from \"./store.ts\";\n\nexport interface MemoryEvidenceMappingV1 {\n\treadonly from: MemoryAtomV1[\"preference\"] extends undefined\n\t\t? never\n\t\t: NonNullable<MemoryAtomV1[\"preference\"]>[\"evidence\"][\"class\"];\n\treadonly to: MemoryAtomV1[\"evidenceClass\"];\n}\n\nexport type MemoryCurationResultV1 =\n\t| {\n\t\t\treadonly status: \"committed\";\n\t\t\treadonly committed: CommittedMemoryV1;\n\t\t\treadonly evidenceMapping?: { readonly from: string; readonly to: string };\n\t }\n\t| { readonly status: \"deduped\"; readonly committed: CommittedMemoryV1 }\n\t| { readonly status: \"conflict\"; readonly reason: string }\n\t| { readonly status: \"rejected\"; readonly reason: string };\n\nexport interface MemoryCurationAgentV1 {\n\t/** Validates and normalizes a draft atom without committing it. */\n\tnormalize(draft: MemoryAtomV1):\n\t\t| {\n\t\t\t\treadonly status: \"normalized\";\n\t\t\t\treadonly atom: MemoryAtomV1;\n\t\t\t\treadonly evidenceMapping?: { readonly from: string; readonly to: string };\n\t\t }\n\t\t| { readonly status: \"rejected\"; readonly reason: string };\n\t/** Normalizes, dedupes by observation identity, and commits into the store. */\n\tcommit(draft: MemoryAtomV1): MemoryCurationResultV1;\n}\n\nexport function createMemoryCurationAgent(options: { readonly store: MemoryStoreV1 }): MemoryCurationAgentV1 {\n\tconst store = options.store;\n\tconst byObservation = new Map<string, CommittedMemoryV1>();\n\tconst observationByMemoryId = new Map<string, string>();\n\n\t/** Deterministic evidence-class mapping for preference atoms. */\n\tconst normalizeEvidence = (atom: MemoryAtomV1): { atom: MemoryAtomV1; mapping?: { from: string; to: string } } => {\n\t\tif (atom.memoryKind !== \"preference\" || !atom.preference) return { atom };\n\t\tconst from = atom.preference.evidence.class;\n\t\tconst to = from === \"inferred\" ? \"derived\" : from;\n\t\tif (atom.evidenceClass === to) return { atom };\n\t\tconst normalized = buildMemoryAtomV1({ ...atom, evidenceClass: to });\n\t\treturn { atom: normalized, mapping: { from, to } };\n\t};\n\n\treturn {\n\t\tnormalize(draft) {\n\t\t\tlet atom: MemoryAtomV1;\n\t\t\ttry {\n\t\t\t\tatom = buildMemoryAtomV1(draft);\n\t\t\t} catch (error) {\n\t\t\t\t// v8 ignore next -- 校验器只抛 Error 对象\n\t\t\t\treturn { status: \"rejected\", reason: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t\tconst { atom: normalized, mapping } = normalizeEvidence(atom);\n\t\t\treturn {\n\t\t\t\tstatus: \"normalized\",\n\t\t\t\tatom: normalized,\n\t\t\t\t...(mapping === undefined ? {} : { evidenceMapping: mapping }),\n\t\t\t};\n\t\t},\n\t\tcommit(draft) {\n\t\t\tconst normalized = this.normalize(draft);\n\t\t\tif (normalized.status === \"rejected\") {\n\t\t\t\treturn { status: \"rejected\", reason: normalized.reason };\n\t\t\t}\n\t\t\tconst atom = normalized.atom;\n\t\t\tconst observationKey = memoryObservationKey(atom.owner, atom.observationId);\n\t\t\tconst existingByObservation = byObservation.get(observationKey);\n\t\t\tif (existingByObservation) {\n\t\t\t\t// Retry/duplicate delivery of the same observation: return the\n\t\t\t\t// existing commit instead of writing a second record.\n\t\t\t\treturn { status: \"deduped\", committed: existingByObservation };\n\t\t\t}\n\t\t\tif (observationByMemoryId.has(atom.memoryId)) {\n\t\t\t\tconst existingKey = observationByMemoryId.get(atom.memoryId)!;\n\t\t\t\t// v8 ignore next -- 两个索引同步写入,memoryId 对应的 observationKey 必相同\n\t\t\t\tif (existingKey !== observationKey) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"conflict\",\n\t\t\t\t\t\treason: `memoryId ${atom.memoryId} is already committed under a different observation identity`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst committed = store.commit(atom);\n\t\t\tbyObservation.set(observationKey, committed);\n\t\t\tobservationByMemoryId.set(atom.memoryId, observationKey);\n\t\t\treturn {\n\t\t\t\tstatus: \"committed\",\n\t\t\t\tcommitted,\n\t\t\t\t...(normalized.evidenceMapping === undefined ? {} : { evidenceMapping: normalized.evidenceMapping }),\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,68 @@
1
+ import { buildMemoryAtomV1, memoryObservationKey } from "./foundation.js";
2
+ export function createMemoryCurationAgent(options) {
3
+ const store = options.store;
4
+ const byObservation = new Map();
5
+ const observationByMemoryId = new Map();
6
+ /** Deterministic evidence-class mapping for preference atoms. */
7
+ const normalizeEvidence = (atom) => {
8
+ if (atom.memoryKind !== "preference" || !atom.preference)
9
+ return { atom };
10
+ const from = atom.preference.evidence.class;
11
+ const to = from === "inferred" ? "derived" : from;
12
+ if (atom.evidenceClass === to)
13
+ return { atom };
14
+ const normalized = buildMemoryAtomV1({ ...atom, evidenceClass: to });
15
+ return { atom: normalized, mapping: { from, to } };
16
+ };
17
+ return {
18
+ normalize(draft) {
19
+ let atom;
20
+ try {
21
+ atom = buildMemoryAtomV1(draft);
22
+ }
23
+ catch (error) {
24
+ // v8 ignore next -- 校验器只抛 Error 对象
25
+ return { status: "rejected", reason: error instanceof Error ? error.message : String(error) };
26
+ }
27
+ const { atom: normalized, mapping } = normalizeEvidence(atom);
28
+ return {
29
+ status: "normalized",
30
+ atom: normalized,
31
+ ...(mapping === undefined ? {} : { evidenceMapping: mapping }),
32
+ };
33
+ },
34
+ commit(draft) {
35
+ const normalized = this.normalize(draft);
36
+ if (normalized.status === "rejected") {
37
+ return { status: "rejected", reason: normalized.reason };
38
+ }
39
+ const atom = normalized.atom;
40
+ const observationKey = memoryObservationKey(atom.owner, atom.observationId);
41
+ const existingByObservation = byObservation.get(observationKey);
42
+ if (existingByObservation) {
43
+ // Retry/duplicate delivery of the same observation: return the
44
+ // existing commit instead of writing a second record.
45
+ return { status: "deduped", committed: existingByObservation };
46
+ }
47
+ if (observationByMemoryId.has(atom.memoryId)) {
48
+ const existingKey = observationByMemoryId.get(atom.memoryId);
49
+ // v8 ignore next -- 两个索引同步写入,memoryId 对应的 observationKey 必相同
50
+ if (existingKey !== observationKey) {
51
+ return {
52
+ status: "conflict",
53
+ reason: `memoryId ${atom.memoryId} is already committed under a different observation identity`,
54
+ };
55
+ }
56
+ }
57
+ const committed = store.commit(atom);
58
+ byObservation.set(observationKey, committed);
59
+ observationByMemoryId.set(atom.memoryId, observationKey);
60
+ return {
61
+ status: "committed",
62
+ committed,
63
+ ...(normalized.evidenceMapping === undefined ? {} : { evidenceMapping: normalized.evidenceMapping }),
64
+ };
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=curation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"curation.js","sourceRoot":"","sources":["../../src/memory/curation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory Foundation — memory-curation-agent candidate normalization, dedupe,\n * and commit (1C.6a).\n *\n * Deterministic curation core (记忆系统设计.md §4/§5.1):\n * - Normalization maps the preference envelope evidence class onto the atom's\n * evidence class without losing the original category: `explicit` and\n * `repeated_behavior` are preserved as-is, `inferred` maps to `derived`.\n * - Dedupe eliminates only retries of the SAME observation\n * (`(owner, observationId)` identity). Same payload under a different\n * observation id is a different experience and is never merged by text\n * similarity.\n * - A memoryId that already exists under a different observation identity is\n * an explicit conflict rejection; the original record stays untouched.\n */\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { buildMemoryAtomV1, memoryObservationKey } from \"./foundation.ts\";\nimport type { CommittedMemoryV1, MemoryStoreV1 } from \"./store.ts\";\n\nexport interface MemoryEvidenceMappingV1 {\n\treadonly from: MemoryAtomV1[\"preference\"] extends undefined\n\t\t? never\n\t\t: NonNullable<MemoryAtomV1[\"preference\"]>[\"evidence\"][\"class\"];\n\treadonly to: MemoryAtomV1[\"evidenceClass\"];\n}\n\nexport type MemoryCurationResultV1 =\n\t| {\n\t\t\treadonly status: \"committed\";\n\t\t\treadonly committed: CommittedMemoryV1;\n\t\t\treadonly evidenceMapping?: { readonly from: string; readonly to: string };\n\t }\n\t| { readonly status: \"deduped\"; readonly committed: CommittedMemoryV1 }\n\t| { readonly status: \"conflict\"; readonly reason: string }\n\t| { readonly status: \"rejected\"; readonly reason: string };\n\nexport interface MemoryCurationAgentV1 {\n\t/** Validates and normalizes a draft atom without committing it. */\n\tnormalize(draft: MemoryAtomV1):\n\t\t| {\n\t\t\t\treadonly status: \"normalized\";\n\t\t\t\treadonly atom: MemoryAtomV1;\n\t\t\t\treadonly evidenceMapping?: { readonly from: string; readonly to: string };\n\t\t }\n\t\t| { readonly status: \"rejected\"; readonly reason: string };\n\t/** Normalizes, dedupes by observation identity, and commits into the store. */\n\tcommit(draft: MemoryAtomV1): MemoryCurationResultV1;\n}\n\nexport function createMemoryCurationAgent(options: { readonly store: MemoryStoreV1 }): MemoryCurationAgentV1 {\n\tconst store = options.store;\n\tconst byObservation = new Map<string, CommittedMemoryV1>();\n\tconst observationByMemoryId = new Map<string, string>();\n\n\t/** Deterministic evidence-class mapping for preference atoms. */\n\tconst normalizeEvidence = (atom: MemoryAtomV1): { atom: MemoryAtomV1; mapping?: { from: string; to: string } } => {\n\t\tif (atom.memoryKind !== \"preference\" || !atom.preference) return { atom };\n\t\tconst from = atom.preference.evidence.class;\n\t\tconst to = from === \"inferred\" ? \"derived\" : from;\n\t\tif (atom.evidenceClass === to) return { atom };\n\t\tconst normalized = buildMemoryAtomV1({ ...atom, evidenceClass: to });\n\t\treturn { atom: normalized, mapping: { from, to } };\n\t};\n\n\treturn {\n\t\tnormalize(draft) {\n\t\t\tlet atom: MemoryAtomV1;\n\t\t\ttry {\n\t\t\t\tatom = buildMemoryAtomV1(draft);\n\t\t\t} catch (error) {\n\t\t\t\t// v8 ignore next -- 校验器只抛 Error 对象\n\t\t\t\treturn { status: \"rejected\", reason: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t\tconst { atom: normalized, mapping } = normalizeEvidence(atom);\n\t\t\treturn {\n\t\t\t\tstatus: \"normalized\",\n\t\t\t\tatom: normalized,\n\t\t\t\t...(mapping === undefined ? {} : { evidenceMapping: mapping }),\n\t\t\t};\n\t\t},\n\t\tcommit(draft) {\n\t\t\tconst normalized = this.normalize(draft);\n\t\t\tif (normalized.status === \"rejected\") {\n\t\t\t\treturn { status: \"rejected\", reason: normalized.reason };\n\t\t\t}\n\t\t\tconst atom = normalized.atom;\n\t\t\tconst observationKey = memoryObservationKey(atom.owner, atom.observationId);\n\t\t\tconst existingByObservation = byObservation.get(observationKey);\n\t\t\tif (existingByObservation) {\n\t\t\t\t// Retry/duplicate delivery of the same observation: return the\n\t\t\t\t// existing commit instead of writing a second record.\n\t\t\t\treturn { status: \"deduped\", committed: existingByObservation };\n\t\t\t}\n\t\t\tif (observationByMemoryId.has(atom.memoryId)) {\n\t\t\t\tconst existingKey = observationByMemoryId.get(atom.memoryId)!;\n\t\t\t\t// v8 ignore next -- 两个索引同步写入,memoryId 对应的 observationKey 必相同\n\t\t\t\tif (existingKey !== observationKey) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"conflict\",\n\t\t\t\t\t\treason: `memoryId ${atom.memoryId} is already committed under a different observation identity`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst committed = store.commit(atom);\n\t\t\tbyObservation.set(observationKey, committed);\n\t\t\tobservationByMemoryId.set(atom.memoryId, observationKey);\n\t\t\treturn {\n\t\t\t\tstatus: \"committed\",\n\t\t\t\tcommitted,\n\t\t\t\t...(normalized.evidenceMapping === undefined ? {} : { evidenceMapping: normalized.evidenceMapping }),\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Memory egress policy (P0-2, 记忆系统设计 §11) — the check that MUST run
3
+ * before memory facts reach a model. A policy declares the allowed source
4
+ * kinds and payload fields that may not leave the memory subsystem.
5
+ *
6
+ * Fail-closed rules:
7
+ * - A fact without source information CANNOT be judged, so it is blocked —
8
+ * an unknown-provenance fact never reaches the model.
9
+ * - A fact whose source kinds are not fully within the allowlist is blocked
10
+ * with the offending memory ids named.
11
+ * Value-level redaction: declared payload fields are REMOVED from the
12
+ * fact's payload and the statement is re-derived from the redacted payload
13
+ * (matching the deterministic default statement format), so the sensitive
14
+ * VALUE itself never crosses egress — redacting the field NAME would leave
15
+ * the value exposed and is therefore not sufficient.
16
+ */
17
+ import type { MemorySourceRefV1 } from "./foundation.ts";
18
+ import type { MemoryRecallPacketV1 } from "./recall-packet.ts";
19
+ export interface MemoryEgressPolicyV1 {
20
+ readonly policyVersion: string;
21
+ /** Source kinds allowed to reach the model; anything else is blocked. */
22
+ readonly allowedSourceKinds: readonly string[];
23
+ /** Payload fields stripped from every fact before egress. */
24
+ readonly redactedPayloadFields: readonly string[];
25
+ }
26
+ /** Per-fact provenance and payload supplied by the caller (fail-closed: missing entries block). */
27
+ export interface MemoryEgressFactInfoV1 {
28
+ readonly sourceRefs: readonly MemorySourceRefV1[];
29
+ readonly payload: Record<string, unknown>;
30
+ }
31
+ export type MemoryEgressOutcomeV1 = {
32
+ readonly status: "allowed";
33
+ readonly policyVersion: string;
34
+ readonly redactions: readonly string[];
35
+ readonly packet: MemoryRecallPacketV1;
36
+ } | {
37
+ readonly status: "blocked";
38
+ readonly policyVersion: string;
39
+ readonly reason: string;
40
+ readonly offendingMemoryIds: readonly string[];
41
+ };
42
+ export interface MemoryEgressPolicyGateV1 {
43
+ apply(packet: MemoryRecallPacketV1, factInfo: Readonly<Record<string, MemoryEgressFactInfoV1 | undefined>>): MemoryEgressOutcomeV1;
44
+ }
45
+ export declare function createMemoryEgressPolicy(policy: {
46
+ readonly policyVersion: string;
47
+ readonly allowedSourceKinds: readonly string[];
48
+ readonly redactedPayloadFields: readonly string[];
49
+ }): MemoryEgressPolicyGateV1;
50
+ //# sourceMappingURL=egress-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"egress-policy.d.ts","sourceRoot":"","sources":["../../src/memory/egress-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;CAClD;AAED,mGAAmG;AACnG,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,MAAM,qBAAqB,GAC9B;IACA,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACrC,GACD;IACA,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C,CAAC;AAEL,MAAM,WAAW,wBAAwB;IACxC,KAAK,CACJ,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,GAAG,SAAS,CAAC,CAAC,GACpE,qBAAqB,CAAC;CACzB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;CAClD,GAAG,wBAAwB,CAsD3B","sourcesContent":["/**\n * Memory egress policy (P0-2, 记忆系统设计 §11) — the check that MUST run\n * before memory facts reach a model. A policy declares the allowed source\n * kinds and payload fields that may not leave the memory subsystem.\n *\n * Fail-closed rules:\n * - A fact without source information CANNOT be judged, so it is blocked —\n * an unknown-provenance fact never reaches the model.\n * - A fact whose source kinds are not fully within the allowlist is blocked\n * with the offending memory ids named.\n * Value-level redaction: declared payload fields are REMOVED from the\n * fact's payload and the statement is re-derived from the redacted payload\n * (matching the deterministic default statement format), so the sensitive\n * VALUE itself never crosses egress — redacting the field NAME would leave\n * the value exposed and is therefore not sufficient.\n */\n\nimport type { MemorySourceRefV1 } from \"./foundation.ts\";\nimport type { MemoryRecallPacketV1 } from \"./recall-packet.ts\";\n\nexport interface MemoryEgressPolicyV1 {\n\treadonly policyVersion: string;\n\t/** Source kinds allowed to reach the model; anything else is blocked. */\n\treadonly allowedSourceKinds: readonly string[];\n\t/** Payload fields stripped from every fact before egress. */\n\treadonly redactedPayloadFields: readonly string[];\n}\n\n/** Per-fact provenance and payload supplied by the caller (fail-closed: missing entries block). */\nexport interface MemoryEgressFactInfoV1 {\n\treadonly sourceRefs: readonly MemorySourceRefV1[];\n\treadonly payload: Record<string, unknown>;\n}\n\nexport type MemoryEgressOutcomeV1 =\n\t| {\n\t\t\treadonly status: \"allowed\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly redactions: readonly string[];\n\t\t\treadonly packet: MemoryRecallPacketV1;\n\t }\n\t| {\n\t\t\treadonly status: \"blocked\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly reason: string;\n\t\t\treadonly offendingMemoryIds: readonly string[];\n\t };\n\nexport interface MemoryEgressPolicyGateV1 {\n\tapply(\n\t\tpacket: MemoryRecallPacketV1,\n\t\tfactInfo: Readonly<Record<string, MemoryEgressFactInfoV1 | undefined>>,\n\t): MemoryEgressOutcomeV1;\n}\n\nexport function createMemoryEgressPolicy(policy: {\n\treadonly policyVersion: string;\n\treadonly allowedSourceKinds: readonly string[];\n\treadonly redactedPayloadFields: readonly string[];\n}): MemoryEgressPolicyGateV1 {\n\tif (policy.policyVersion.trim().length === 0) throw new Error(\"Egress policyVersion must be a non-empty string\");\n\tif (policy.allowedSourceKinds.length === 0) throw new Error(\"Egress policy must allow at least one source kind\");\n\n\treturn {\n\t\tapply(packet, factInfo) {\n\t\t\tconst offending: string[] = [];\n\t\t\tfor (const fact of packet.facts) {\n\t\t\t\tconst info = factInfo[fact.memoryId];\n\t\t\t\tif (!info) {\n\t\t\t\t\t// Fail-closed: a fact without provenance cannot be judged.\n\t\t\t\t\toffending.push(fact.memoryId);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst kinds = info.sourceRefs.map((ref) => ref.kind);\n\t\t\t\tif (kinds.some((kind) => !policy.allowedSourceKinds.includes(kind))) {\n\t\t\t\t\toffending.push(fact.memoryId);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (offending.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"blocked\",\n\t\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\t\treason: `Facts with unknown provenance or disallowed source kinds are blocked from egress: ${offending.join(\", \")}`,\n\t\t\t\t\toffendingMemoryIds: Object.freeze([...offending]),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst redactions: string[] = [];\n\t\t\tconst facts = packet.facts.map((fact) => {\n\t\t\t\tconst info = factInfo[fact.memoryId]!;\n\t\t\t\tlet payload: Record<string, unknown> = info.payload;\n\t\t\t\tfor (const field of policy.redactedPayloadFields) {\n\t\t\t\t\tif (field in payload) {\n\t\t\t\t\t\tconst { [field]: _removed, ...rest } = payload;\n\t\t\t\t\t\tpayload = rest;\n\t\t\t\t\t\tredactions.push(`${fact.memoryId}.${field}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Statement re-derived from the redacted payload in the same\n\t\t\t\t// deterministic JSON form the default statement builder uses —\n\t\t\t\t// the sensitive VALUE itself never crosses egress.\n\t\t\t\tconst statement = JSON.stringify(payload);\n\t\t\t\treturn { ...fact, statement };\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tstatus: \"allowed\",\n\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\tredactions: Object.freeze(redactions),\n\t\t\t\tpacket: Object.freeze({ ...packet, facts: Object.freeze(facts) }),\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Memory egress policy (P0-2, 记忆系统设计 §11) — the check that MUST run
3
+ * before memory facts reach a model. A policy declares the allowed source
4
+ * kinds and payload fields that may not leave the memory subsystem.
5
+ *
6
+ * Fail-closed rules:
7
+ * - A fact without source information CANNOT be judged, so it is blocked —
8
+ * an unknown-provenance fact never reaches the model.
9
+ * - A fact whose source kinds are not fully within the allowlist is blocked
10
+ * with the offending memory ids named.
11
+ * Value-level redaction: declared payload fields are REMOVED from the
12
+ * fact's payload and the statement is re-derived from the redacted payload
13
+ * (matching the deterministic default statement format), so the sensitive
14
+ * VALUE itself never crosses egress — redacting the field NAME would leave
15
+ * the value exposed and is therefore not sufficient.
16
+ */
17
+ export function createMemoryEgressPolicy(policy) {
18
+ if (policy.policyVersion.trim().length === 0)
19
+ throw new Error("Egress policyVersion must be a non-empty string");
20
+ if (policy.allowedSourceKinds.length === 0)
21
+ throw new Error("Egress policy must allow at least one source kind");
22
+ return {
23
+ apply(packet, factInfo) {
24
+ const offending = [];
25
+ for (const fact of packet.facts) {
26
+ const info = factInfo[fact.memoryId];
27
+ if (!info) {
28
+ // Fail-closed: a fact without provenance cannot be judged.
29
+ offending.push(fact.memoryId);
30
+ continue;
31
+ }
32
+ const kinds = info.sourceRefs.map((ref) => ref.kind);
33
+ if (kinds.some((kind) => !policy.allowedSourceKinds.includes(kind))) {
34
+ offending.push(fact.memoryId);
35
+ }
36
+ }
37
+ if (offending.length > 0) {
38
+ return {
39
+ status: "blocked",
40
+ policyVersion: policy.policyVersion,
41
+ reason: `Facts with unknown provenance or disallowed source kinds are blocked from egress: ${offending.join(", ")}`,
42
+ offendingMemoryIds: Object.freeze([...offending]),
43
+ };
44
+ }
45
+ const redactions = [];
46
+ const facts = packet.facts.map((fact) => {
47
+ const info = factInfo[fact.memoryId];
48
+ let payload = info.payload;
49
+ for (const field of policy.redactedPayloadFields) {
50
+ if (field in payload) {
51
+ const { [field]: _removed, ...rest } = payload;
52
+ payload = rest;
53
+ redactions.push(`${fact.memoryId}.${field}`);
54
+ }
55
+ }
56
+ // Statement re-derived from the redacted payload in the same
57
+ // deterministic JSON form the default statement builder uses —
58
+ // the sensitive VALUE itself never crosses egress.
59
+ const statement = JSON.stringify(payload);
60
+ return { ...fact, statement };
61
+ });
62
+ return {
63
+ status: "allowed",
64
+ policyVersion: policy.policyVersion,
65
+ redactions: Object.freeze(redactions),
66
+ packet: Object.freeze({ ...packet, facts: Object.freeze(facts) }),
67
+ };
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=egress-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"egress-policy.js","sourceRoot":"","sources":["../../src/memory/egress-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory egress policy (P0-2, 记忆系统设计 §11) — the check that MUST run\n * before memory facts reach a model. A policy declares the allowed source\n * kinds and payload fields that may not leave the memory subsystem.\n *\n * Fail-closed rules:\n * - A fact without source information CANNOT be judged, so it is blocked —\n * an unknown-provenance fact never reaches the model.\n * - A fact whose source kinds are not fully within the allowlist is blocked\n * with the offending memory ids named.\n * Value-level redaction: declared payload fields are REMOVED from the\n * fact's payload and the statement is re-derived from the redacted payload\n * (matching the deterministic default statement format), so the sensitive\n * VALUE itself never crosses egress — redacting the field NAME would leave\n * the value exposed and is therefore not sufficient.\n */\n\nimport type { MemorySourceRefV1 } from \"./foundation.ts\";\nimport type { MemoryRecallPacketV1 } from \"./recall-packet.ts\";\n\nexport interface MemoryEgressPolicyV1 {\n\treadonly policyVersion: string;\n\t/** Source kinds allowed to reach the model; anything else is blocked. */\n\treadonly allowedSourceKinds: readonly string[];\n\t/** Payload fields stripped from every fact before egress. */\n\treadonly redactedPayloadFields: readonly string[];\n}\n\n/** Per-fact provenance and payload supplied by the caller (fail-closed: missing entries block). */\nexport interface MemoryEgressFactInfoV1 {\n\treadonly sourceRefs: readonly MemorySourceRefV1[];\n\treadonly payload: Record<string, unknown>;\n}\n\nexport type MemoryEgressOutcomeV1 =\n\t| {\n\t\t\treadonly status: \"allowed\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly redactions: readonly string[];\n\t\t\treadonly packet: MemoryRecallPacketV1;\n\t }\n\t| {\n\t\t\treadonly status: \"blocked\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly reason: string;\n\t\t\treadonly offendingMemoryIds: readonly string[];\n\t };\n\nexport interface MemoryEgressPolicyGateV1 {\n\tapply(\n\t\tpacket: MemoryRecallPacketV1,\n\t\tfactInfo: Readonly<Record<string, MemoryEgressFactInfoV1 | undefined>>,\n\t): MemoryEgressOutcomeV1;\n}\n\nexport function createMemoryEgressPolicy(policy: {\n\treadonly policyVersion: string;\n\treadonly allowedSourceKinds: readonly string[];\n\treadonly redactedPayloadFields: readonly string[];\n}): MemoryEgressPolicyGateV1 {\n\tif (policy.policyVersion.trim().length === 0) throw new Error(\"Egress policyVersion must be a non-empty string\");\n\tif (policy.allowedSourceKinds.length === 0) throw new Error(\"Egress policy must allow at least one source kind\");\n\n\treturn {\n\t\tapply(packet, factInfo) {\n\t\t\tconst offending: string[] = [];\n\t\t\tfor (const fact of packet.facts) {\n\t\t\t\tconst info = factInfo[fact.memoryId];\n\t\t\t\tif (!info) {\n\t\t\t\t\t// Fail-closed: a fact without provenance cannot be judged.\n\t\t\t\t\toffending.push(fact.memoryId);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst kinds = info.sourceRefs.map((ref) => ref.kind);\n\t\t\t\tif (kinds.some((kind) => !policy.allowedSourceKinds.includes(kind))) {\n\t\t\t\t\toffending.push(fact.memoryId);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (offending.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"blocked\",\n\t\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\t\treason: `Facts with unknown provenance or disallowed source kinds are blocked from egress: ${offending.join(\", \")}`,\n\t\t\t\t\toffendingMemoryIds: Object.freeze([...offending]),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst redactions: string[] = [];\n\t\t\tconst facts = packet.facts.map((fact) => {\n\t\t\t\tconst info = factInfo[fact.memoryId]!;\n\t\t\t\tlet payload: Record<string, unknown> = info.payload;\n\t\t\t\tfor (const field of policy.redactedPayloadFields) {\n\t\t\t\t\tif (field in payload) {\n\t\t\t\t\t\tconst { [field]: _removed, ...rest } = payload;\n\t\t\t\t\t\tpayload = rest;\n\t\t\t\t\t\tredactions.push(`${fact.memoryId}.${field}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Statement re-derived from the redacted payload in the same\n\t\t\t\t// deterministic JSON form the default statement builder uses —\n\t\t\t\t// the sensitive VALUE itself never crosses egress.\n\t\t\t\tconst statement = JSON.stringify(payload);\n\t\t\t\treturn { ...fact, statement };\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tstatus: \"allowed\",\n\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\tredactions: Object.freeze(redactions),\n\t\t\t\tpacket: Object.freeze({ ...packet, facts: Object.freeze(facts) }),\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Memory embedding providers for hybrid retrieval (混合检索 · 向量侧).
3
+ *
4
+ * POC-verified (2026-09, 100k corpus): high tier (Xenova/bge-m3, 1024d) and
5
+ * low tier (Xenova/bge-small-zh-v1.5, 512d) inference is deterministic
6
+ * bit-exact across runs; high-tier drift queries reach re@10 100% after
7
+ * reranking, low tier 83%. Tier presets are caller configuration — this module
8
+ * only exposes a modelId-parameterized interface (宪法: 不绑死单一 provider).
9
+ *
10
+ * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at
11
+ * first embed (惯例参考 packages/agent-forge/src/utils/photon.ts); type
12
+ * positions use type-only imports. The deterministic mock provider keeps tests
13
+ * offline (门禁: deterministic mock AI; 真实模型不进 CI).
14
+ */
15
+ export interface MemoryEmbeddingProviderV1 {
16
+ readonly modelId: string;
17
+ readonly dimensions: number;
18
+ /**
19
+ * Batch embed. Vectors are L2-normalized float arrays. Deterministic for
20
+ * identical input. Plain text mapping — no instruction prefix is added
21
+ * here; query-side instructions for bge-small-zh family are the caller's
22
+ * responsibility (低档 bge-small-zh 系查询侧指令由调用方负责添加).
23
+ */
24
+ embed(texts: readonly string[]): Promise<readonly (readonly number[])[]>;
25
+ }
26
+ /**
27
+ * Minimal structural seam around a transformers.js feature-extraction
28
+ * pipeline, so tests can inject a fake without touching the real loader.
29
+ */
30
+ export interface TransformersEmbeddingBackendV1 {
31
+ /** Extract features for a batch of texts; one vector per input, input order. */
32
+ extract: (texts: string[]) => Promise<{
33
+ toList: () => number[][];
34
+ }>;
35
+ }
36
+ export type TransformersEmbeddingLoadImpl = (modelId: string, cacheDir: string, remoteHost: string) => Promise<TransformersEmbeddingBackendV1>;
37
+ /**
38
+ * Deterministic mock embedding provider for offline tests: tokenizes by
39
+ * `[^\p{L}\p{N}]+`, hashes each token with FNV-1a plus multi-round mixing into
40
+ * dimension indices, then L2-normalizes. Same text → same vector, no random,
41
+ * no clock. POC 已用真实模型验证 bit-exact; mock 只承担确定性测试门禁。
42
+ */
43
+ export declare function createDeterministicMockEmbeddingProvider(options?: {
44
+ readonly dimensions?: number;
45
+ }): MemoryEmbeddingProviderV1;
46
+ /**
47
+ * Transformers.js-backed embedding provider (POC 档位: 高档默认
48
+ * Xenova/bge-m3/1024d; 低档由调用方传 modelId "Xenova/bge-small-zh-v1.5" +
49
+ * dimensions 512)。
50
+ *
51
+ * 镜像序列: 首次 embed 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传
52
+ * ["https://huggingface.co", "https://hf-mirror.com"]); 某 host 加载抛错则
53
+ * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次
54
+ * (并发 embed 共享同一 in-flight promise); 加载失败不缓存, 下次 embed 重新
55
+ * 走完整序列, 失败原因原样保留, 不静默降级。
56
+ *
57
+ * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名
58
+ * (modelId, cacheDir, remoteHost) => backend。
59
+ */
60
+ export declare function createTransformersEmbeddingProvider(options: {
61
+ readonly modelId?: string;
62
+ readonly dimensions?: number;
63
+ readonly cacheDir: string;
64
+ readonly remoteHosts?: readonly string[];
65
+ readonly batchSize?: number;
66
+ readonly loadImpl?: TransformersEmbeddingLoadImpl;
67
+ /** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */
68
+ readonly moduleEntryPath?: string;
69
+ }): MemoryEmbeddingProviderV1;
70
+ //# sourceMappingURL=embedding-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-provider.d.ts","sourceRoot":"","sources":["../../src/memory/embedding-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;CACzE;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC9C,gFAAgF;IAChF,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,MAAM,EAAE,EAAE,CAAA;KAAE,CAAC,CAAC;CACpE;AAED,MAAM,MAAM,6BAA6B,GAAG,CAC3C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,KACd,OAAO,CAAC,8BAA8B,CAAC,CAAC;AA2C7C;;;;;GAKG;AACH,wBAAgB,wCAAwC,CAAC,OAAO,CAAC,EAAE;IAClE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,yBAAyB,CAO5B;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,6BAA6B,CAAC;IAClD,iGAAiG;IACjG,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,yBAAyB,CA+D5B","sourcesContent":["/**\n * Memory embedding providers for hybrid retrieval (混合检索 · 向量侧).\n *\n * POC-verified (2026-09, 100k corpus): high tier (Xenova/bge-m3, 1024d) and\n * low tier (Xenova/bge-small-zh-v1.5, 512d) inference is deterministic\n * bit-exact across runs; high-tier drift queries reach re@10 100% after\n * reranking, low tier 83%. Tier presets are caller configuration — this module\n * only exposes a modelId-parameterized interface (宪法: 不绑死单一 provider).\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first embed (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. The deterministic mock provider keeps tests\n * offline (门禁: deterministic mock AI; 真实模型不进 CI).\n */\n\n/**\n * Batch embedding provider. Implementations must be deterministic for\n * identical input and must not silently degrade: any failure propagates.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryEmbeddingProviderV1 {\n\treadonly modelId: string;\n\treadonly dimensions: number;\n\t/**\n\t * Batch embed. Vectors are L2-normalized float arrays. Deterministic for\n\t * identical input. Plain text mapping — no instruction prefix is added\n\t * here; query-side instructions for bge-small-zh family are the caller's\n\t * responsibility (低档 bge-small-zh 系查询侧指令由调用方负责添加).\n\t */\n\tembed(texts: readonly string[]): Promise<readonly (readonly number[])[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js feature-extraction\n * pipeline, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersEmbeddingBackendV1 {\n\t/** Extract features for a batch of texts; one vector per input, input order. */\n\textract: (texts: string[]) => Promise<{ toList: () => number[][] }>;\n}\n\nexport type TransformersEmbeddingLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersEmbeddingBackendV1>;\n\nconst DEFAULT_TRANSFORMERS_MODEL_ID = \"Xenova/bge-m3\";\nconst DEFAULT_TRANSFORMERS_DIMENSIONS = 1024;\nconst DEFAULT_TRANSFORMERS_BATCH_SIZE = 16;\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\nfunction fnv1a32(input: string): number {\n\tlet hash = 0x811c9dc5;\n\tfor (let offset = 0; offset < input.length; offset += 1) {\n\t\thash ^= input.charCodeAt(offset);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn hash >>> 0;\n}\n\nfunction embedDeterministicMock(text: string, dimensions: number): number[] {\n\tconst values = new Array<number>(dimensions).fill(0);\n\tconst tokens = text.toLowerCase().split(/[^\\p{L}\\p{N}]+/u);\n\tfor (const token of tokens) {\n\t\tif (token.length === 0) {\n\t\t\tcontinue;\n\t\t}\n\t\t// 多轮混淆累加: 每个 token 经 FNV-1a 后再派生 3 个维度沉点,\n\t\t// 放大不同文本间的向量差异; 全程无随机、无时钟。\n\t\tlet mixed = fnv1a32(token);\n\t\tfor (let round = 0; round < 3; round += 1) {\n\t\t\tmixed = Math.imul(mixed ^ 0x9e3779b9, 0x85ebca6b) >>> 0;\n\t\t\tvalues[mixed % dimensions] += 1;\n\t\t}\n\t}\n\tlet sumSquares = 0;\n\tfor (const value of values) {\n\t\tsumSquares += value * value;\n\t}\n\tif (sumSquares === 0) {\n\t\t// 空文本/全标点: 确定性零向量; L2 范数为 0, 不归一化 (避免除零 NaN)。\n\t\treturn values;\n\t}\n\tconst norm = Math.sqrt(sumSquares);\n\treturn values.map((value) => value / norm);\n}\n\n/**\n * Deterministic mock embedding provider for offline tests: tokenizes by\n * `[^\\p{L}\\p{N}]+`, hashes each token with FNV-1a plus multi-round mixing into\n * dimension indices, then L2-normalizes. Same text → same vector, no random,\n * no clock. POC 已用真实模型验证 bit-exact; mock 只承担确定性测试门禁。\n */\nexport function createDeterministicMockEmbeddingProvider(options?: {\n\treadonly dimensions?: number;\n}): MemoryEmbeddingProviderV1 {\n\tconst dimensions = options?.dimensions ?? 64;\n\treturn {\n\t\tmodelId: \"mock-embedding@1\",\n\t\tdimensions,\n\t\tembed: async (texts) => texts.map((text) => embedDeterministicMock(text, dimensions)),\n\t};\n}\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersEmbeddingLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst extractor = await transformers.pipeline(\"feature-extraction\", modelId, { dtype: \"q8\" });\n\t\treturn {\n\t\t\textract: async (texts) => {\n\t\t\t\tconst output = await extractor(texts, { pooling: \"cls\", normalize: true });\n\t\t\t\treturn { toList: () => output.tolist() as number[][] };\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed embedding provider (POC 档位: 高档默认\n * Xenova/bge-m3/1024d; 低档由调用方传 modelId \"Xenova/bge-small-zh-v1.5\" +\n * dimensions 512)。\n *\n * 镜像序列: 首次 embed 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 embed 共享同一 in-flight promise); 加载失败不缓存, 下次 embed 重新\n * 走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersEmbeddingProvider(options: {\n\treadonly modelId?: string;\n\treadonly dimensions?: number;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly batchSize?: number;\n\treadonly loadImpl?: TransformersEmbeddingLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryEmbeddingProviderV1 {\n\tconst modelId = options.modelId ?? DEFAULT_TRANSFORMERS_MODEL_ID;\n\tconst dimensions = options.dimensions ?? DEFAULT_TRANSFORMERS_DIMENSIONS;\n\tconst batchSize = options.batchSize ?? DEFAULT_TRANSFORMERS_BATCH_SIZE;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersEmbeddingBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersEmbeddingBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸: 失败下载留下的空文件会被 transformers\n\t\t\t\t\t// 当作缓存命中, 毒化下一个 host 的重试 (model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow lastError;\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 embed 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\tdimensions,\n\t\tembed: async (texts) => {\n\t\t\tif (texts.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\tconst vectors: number[][] = [];\n\t\t\tfor (let offset = 0; offset < texts.length; offset += batchSize) {\n\t\t\t\tconst batchTexts = texts.slice(offset, offset + batchSize);\n\t\t\t\tconst rows = (await backend.extract(batchTexts)).toList();\n\t\t\t\tfor (const row of rows) {\n\t\t\t\t\tif (row.length !== dimensions) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`embedding dimension mismatch: model ${modelId} returned ${row.length} dims, provider declares ${dimensions}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tvectors.push(row);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn vectors;\n\t\t},\n\t};\n}\n"]}