@fgv/ts-agent-memory 5.1.0-45 → 5.1.0-46

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 (96) hide show
  1. package/.rush/temp/{bc5b0f2098f7df2a5c498ea77b0de13316349b96.tar.log → 6e1299d1cebb080772163a532c221cd181b54f9f.tar.log} +20 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/README.md +79 -4
  7. package/dist/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
  8. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  9. package/dist/packlets/store/fileTreeMemoryStore.js +20 -4
  10. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  11. package/dist/packlets/types/writePolicy.js +10 -2
  12. package/dist/packlets/types/writePolicy.js.map +1 -1
  13. package/dist/packlets/vector/fragmentConverters.js +41 -0
  14. package/dist/packlets/vector/fragmentConverters.js.map +1 -0
  15. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
  16. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  17. package/dist/packlets/vector/index.js +1 -0
  18. package/dist/packlets/vector/index.js.map +1 -1
  19. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  20. package/dist/test/unit/store/fileTreeMemoryStore.test.js +64 -0
  21. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  22. package/dist/test/unit/store/indexInjection.test.js +432 -0
  23. package/dist/test/unit/store/indexInjection.test.js.map +1 -0
  24. package/dist/test/unit/types/temporalPolicy.test.js +21 -0
  25. package/dist/test/unit/types/temporalPolicy.test.js.map +1 -1
  26. package/dist/test/unit/types/writePolicy.test.js +83 -0
  27. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  28. package/dist/test/unit/vector/fragmentConverters.test.js +58 -0
  29. package/dist/test/unit/vector/fragmentConverters.test.js.map +1 -0
  30. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
  31. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
  32. package/dist/ts-agent-memory.d.ts +240 -37
  33. package/etc/ts-agent-memory.api.md +10 -1
  34. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +10 -7
  35. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  36. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
  37. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  38. package/lib/packlets/store/fileTreeMemoryStore.d.ts +65 -3
  39. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  40. package/lib/packlets/store/fileTreeMemoryStore.js +20 -4
  41. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  42. package/lib/packlets/types/writePolicy.d.ts +40 -2
  43. package/lib/packlets/types/writePolicy.d.ts.map +1 -1
  44. package/lib/packlets/types/writePolicy.js +10 -2
  45. package/lib/packlets/types/writePolicy.js.map +1 -1
  46. package/lib/packlets/vector/fragmentConverters.d.ts +25 -0
  47. package/lib/packlets/vector/fragmentConverters.d.ts.map +1 -0
  48. package/lib/packlets/vector/fragmentConverters.js +44 -0
  49. package/lib/packlets/vector/fragmentConverters.js.map +1 -0
  50. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +5 -3
  51. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  52. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
  53. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  54. package/lib/packlets/vector/index.d.ts +1 -0
  55. package/lib/packlets/vector/index.d.ts.map +1 -1
  56. package/lib/packlets/vector/index.js +1 -0
  57. package/lib/packlets/vector/index.js.map +1 -1
  58. package/lib/packlets/vector/vectorIndex.d.ts +97 -22
  59. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  60. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  61. package/lib/test/unit/store/fileTreeMemoryStore.test.js +64 -0
  62. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  63. package/lib/test/unit/store/indexInjection.test.d.ts +2 -0
  64. package/lib/test/unit/store/indexInjection.test.d.ts.map +1 -0
  65. package/lib/test/unit/store/indexInjection.test.js +434 -0
  66. package/lib/test/unit/store/indexInjection.test.js.map +1 -0
  67. package/lib/test/unit/types/temporalPolicy.test.js +21 -0
  68. package/lib/test/unit/types/temporalPolicy.test.js.map +1 -1
  69. package/lib/test/unit/types/writePolicy.test.js +83 -0
  70. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  71. package/lib/test/unit/vector/fragmentConverters.test.d.ts +2 -0
  72. package/lib/test/unit/vector/fragmentConverters.test.d.ts.map +1 -0
  73. package/lib/test/unit/vector/fragmentConverters.test.js +60 -0
  74. package/lib/test/unit/vector/fragmentConverters.test.js.map +1 -0
  75. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
  76. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
  77. package/package.json +7 -7
  78. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  79. package/rush-logs/ts-agent-memory.build.log +2 -2
  80. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +10 -7
  81. package/src/packlets/store/fileTreeMemoryStore.ts +74 -4
  82. package/src/packlets/types/writePolicy.ts +40 -2
  83. package/src/packlets/vector/fragmentConverters.ts +55 -0
  84. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +35 -8
  85. package/src/packlets/vector/index.ts +1 -0
  86. package/src/packlets/vector/vectorIndex.ts +97 -22
  87. package/src/test/unit/store/fileTreeMemoryStore.test.ts +75 -0
  88. package/src/test/unit/store/indexInjection.test.ts +550 -0
  89. package/src/test/unit/types/temporalPolicy.test.ts +28 -0
  90. package/src/test/unit/types/writePolicy.test.ts +112 -0
  91. package/src/test/unit/vector/fragmentConverters.test.ts +80 -0
  92. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +88 -0
  93. package/temp/build/lint/_eslint-5eVG3S6w.json +23 -11
  94. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  95. package/temp/ts-agent-memory.api.json +177 -21
  96. package/temp/ts-agent-memory.api.md +10 -1
@@ -8,10 +8,24 @@ import { IEdgeTarget, IMemoryRecord } from '../types';
8
8
 
9
9
  /**
10
10
  * A half-open `[start, end)` span into a record's body — the in-record locator a
11
- * {@link IFragmentVectorIndex} carries on each fragment hit. `start` is inclusive,
11
+ * {@link IFragmentVectorIndex} may carry on a fragment. `start` is inclusive,
12
12
  * `end` exclusive. The unit (character / byte / token offsets) is the consumer's
13
13
  * choice: the index stores the two integers opaquely and never interprets them,
14
14
  * so they line up with whatever locator the consumer's own read side uses.
15
+ *
16
+ * @remarks
17
+ * **The span is advisory.** It names the region of the body a fragment was
18
+ * *derived from*; it is NOT a slice guaranteed to reproduce the fragment's text.
19
+ * `body.slice(start, end)` round-trips only under a segmenter that merely chooses
20
+ * boundaries. Under a **rewriting** segmenter — one that turns a span into a
21
+ * curated block, an increasingly common ingestion shape when a model both selects
22
+ * and rewrites — the fragment text is not a substring of the body at all, and the
23
+ * fragmentation is not re-derivable from the body. Treat the span as a pointer for
24
+ * locating context, never as an extraction recipe.
25
+ *
26
+ * A fragment whose provenance cannot honestly be expressed as a body span should
27
+ * omit the locator entirely and carry an {@link IEmbeddedFragment.fragmentId}
28
+ * instead.
15
29
  * @public
16
30
  */
17
31
  export interface IFragmentLocator {
@@ -35,9 +49,27 @@ export interface IFragmentLocator {
35
49
  * that share a stem. The caller re-resolves the hit against the record index by
36
50
  * the same scoped address.
37
51
  *
38
- * `locator` is present only on hits from a {@link IFragmentVectorIndex} — it
39
- * identifies WHICH fragment of the record matched. Record-granular
40
- * {@link IVectorIndex} hits omit it.
52
+ * **No single field discriminates a fragment hit from a record-granular hit.** A
53
+ * record hit carries neither `locator` nor `fragmentId`; a fragment hit carries at
54
+ * least one of the two, but not necessarily any particular one — a fragment with a
55
+ * body span but no consumer-minted id, and a fragment with an id but no honest span,
56
+ * are both legal. Testing one field for presence therefore cannot tell you which
57
+ * kind of hit you hold.
58
+ *
59
+ * That "at least one" requirement is enforced on the upsert side by
60
+ * {@link embeddedFragmentConverter} — a different boundary from this type — and is
61
+ * deliberately NOT offered here as a discriminator either. A caller keyed off it
62
+ * would be coupled to an invariant this type does not own, and would fail silently
63
+ * if the invariant were ever relaxed.
64
+ *
65
+ * **The robust rule is that fragment-ness is determined by which index produced the
66
+ * hit**: {@link IFragmentVectorIndex.query} returns fragment hits and
67
+ * {@link IVectorIndex.query} returns record hits. The caller chose the index it
68
+ * queried, so it already knows which kind it is holding.
69
+ *
70
+ * Note in particular that an absent `locator` now carries **two** distinct meanings
71
+ * — a record-granular hit, or a fragment with no honest body span (see
72
+ * {@link IFragmentLocator}) — which is precisely why presence-branching is unsafe.
41
73
  * @public
42
74
  */
43
75
  export interface IVectorQueryHit {
@@ -45,8 +77,18 @@ export interface IVectorQueryHit {
45
77
  readonly target: IEdgeTarget;
46
78
  /** Backend similarity score; higher is more similar. */
47
79
  readonly score: number;
48
- /** The matched fragment's in-record span; present only for fragment-index hits. */
80
+ /**
81
+ * The advisory in-record span the matched fragment was derived from, when the
82
+ * producing fragment carried one. Absent on record-granular hits AND on fragment
83
+ * hits with no honest span — see the remarks above; do not branch on its presence.
84
+ */
49
85
  readonly locator?: IFragmentLocator;
86
+ /**
87
+ * The opaque identity the producing fragment was stored with, carried back
88
+ * verbatim. Absent on record-granular hits AND on fragment hits stored without
89
+ * one — see the remarks above; do not branch on its presence.
90
+ */
91
+ readonly fragmentId?: string;
50
92
  }
51
93
 
52
94
  /**
@@ -88,32 +130,64 @@ export interface IVectorIndex {
88
130
  }
89
131
 
90
132
  /**
91
- * One embedded fragment of a record: its in-record {@link IFragmentLocator | span}
92
- * and the vector for that span. Produced by a {@link FragmentEmbedder} and stored
93
- * via {@link IFragmentVectorIndex.addFragments}.
133
+ * One embedded fragment of a record: the fragment's vector, plus at least one of the
134
+ * two ways to identify it its advisory in-record {@link IFragmentLocator | span}
135
+ * and/or an opaque consumer-minted {@link IEmbeddedFragment.fragmentId | fragmentId}.
136
+ * Produced by a {@link FragmentEmbedder} and stored via
137
+ * {@link IFragmentVectorIndex.addFragments}.
138
+ *
139
+ * @remarks
140
+ * Both identity fields are optional **in the type**, but the "at least one"
141
+ * requirement is real — a fragment carrying neither is unidentifiable at the read
142
+ * side. It is enforced by {@link embeddedFragmentConverter} (and re-checked by the
143
+ * in-package index implementations) rather than by a conditional-required union
144
+ * (`{ locator; fragmentId? } | { locator?; fragmentId }`), which was considered and
145
+ * declined: the union costs at every construction site and buys nothing at the read
146
+ * site, where each field reads as `… | undefined` either way.
94
147
  * @public
95
148
  */
96
149
  export interface IEmbeddedFragment {
97
- /** The fragment's in-record span. */
98
- readonly locator: IFragmentLocator;
99
- /** The embedding vector for that span. */
150
+ /**
151
+ * The region of the record body this fragment was derived from, when one can be
152
+ * stated honestly. Advisory see {@link IFragmentLocator}; it is NOT a slice that
153
+ * reproduces the fragment text. Omit it for a fragment with no honest body span (a
154
+ * rewriting segmenter), in which case `fragmentId` must be supplied.
155
+ */
156
+ readonly locator?: IFragmentLocator;
157
+ /**
158
+ * An opaque, consumer-minted identity for this fragment, carried verbatim through
159
+ * the index and returned on the corresponding {@link IVectorQueryHit}. The index
160
+ * **never parses it, never filters on it, and never assigns meaning to it** — it is
161
+ * a bytestring, not part of the query path. It exists so a fragment stays
162
+ * identifiable when its text is not re-derivable from the record body.
163
+ *
164
+ * The guarantee is "we never parse it", NOT "we keep it stable". Because
165
+ * `addFragments` is whole-record-replace, an updated record re-emits its entire
166
+ * fragment set, so **any stability of a fragment id across re-embeds is the
167
+ * consumer's responsibility**, not the index's.
168
+ */
169
+ readonly fragmentId?: string;
170
+ /** The embedding vector for this fragment. */
100
171
  readonly vector: Float32Array;
101
172
  }
102
173
 
103
174
  /**
104
175
  * The fragment-granular sibling of {@link IVectorIndex}: instead of one vector per
105
- * record it holds many vectors per record, each tagged with an in-record
106
- * {@link IFragmentLocator}, and its `query` returns per-fragment hits carrying that
107
- * locator. This is the seam behind sub-document semantic search — the "discovery"
108
- * half of a search-then-read contract, where a hit's `(target, locator)` tells the
109
- * consumer which record AND which span to read.
176
+ * record it holds many vectors per record, each tagged with the identity its
177
+ * {@link IEmbeddedFragment} carried, and its `query` returns per-fragment hits
178
+ * carrying that identity back. This is the seam behind sub-document semantic search
179
+ * — the "discovery" half of a search-then-read contract, where a hit tells the
180
+ * consumer which record AND which fragment of it to read.
110
181
  *
111
182
  * @remarks
112
- * Deliberately NOT `extends IVectorIndex`: an index keyed by `(target, locator)`
113
- * has no well-defined single-vector `add(target, vector)`. It is a parallel
114
- * contract with three operations — `addFragments`, `remove`, `query` — reusing
115
- * {@link IVectorQueryHit} (whose `locator` is always populated here). Kept distinct
116
- * from the record-granular index per the consumer contract: memory recall stays
183
+ * Deliberately NOT `extends IVectorIndex`: an index holding many vectors per record
184
+ * has no well-defined single-vector `add(target, vector)`. It is a parallel contract
185
+ * with three operations — `addFragments`, `remove`, `query` — reusing
186
+ * {@link IVectorQueryHit}, on which both `locator` and `fragmentId` are optional. A
187
+ * fragment hit populates whichever of the two its stored fragment carried; see
188
+ * {@link IVectorQueryHit} for why that is not a discriminator and why fragment-ness
189
+ * is determined by the index queried, not by field presence. Kept distinct from the
190
+ * record-granular index per the consumer contract: memory recall stays
117
191
  * record-granular; sub-document knowledge uses a separate fragment index.
118
192
  * @public
119
193
  */
@@ -134,7 +208,8 @@ export interface IFragmentVectorIndex {
134
208
 
135
209
  /**
136
210
  * Return the `topK` nearest fragments to `vector`, in descending score order,
137
- * each hit carrying its record `target` and fragment `locator`. When
211
+ * each hit carrying its record `target` plus whichever of `locator` /
212
+ * `fragmentId` the stored fragment was added with. When
138
213
  * `maxPerRecord` is supplied, no more than that many fragments of any single
139
214
  * record appear in the result — the cap is applied during selection (before the
140
215
  * `topK` cut) so one long document cannot crowd out others.
@@ -963,4 +963,79 @@ describe('FileTreeMemoryStore', () => {
963
963
  });
964
964
  });
965
965
  });
966
+
967
+ /**
968
+ * Pins the store-layer half of the provenance merge contract documented in the
969
+ * package README (§ "Record updates — the merge contract"). The policy-level
970
+ * behavior is pinned in `types/writePolicy.test.ts`; these tests pin the claims
971
+ * the README makes about reaching that behavior through `put` — namely that the
972
+ * patch is projected from the incoming record's provenance, so an omitted key is
973
+ * preserved while an explicit `null` clears, and that the cleared key stays gone
974
+ * across a reload rather than resurfacing from the persisted frontmatter.
975
+ */
976
+ describe('provenance merge contract through put (README-pinned)', () => {
977
+ /** A knowledge record carrying an explicit provenance block. */
978
+ function provenanceRecord(provenance: unknown, body: string): IMemoryRecord<unknown> {
979
+ return {
980
+ envelope: envelopeConverter
981
+ .convert({
982
+ id: 'doc-p',
983
+ entityId: 'doc-p',
984
+ kind: 'knowledge',
985
+ tags: [],
986
+ links: [],
987
+ created: 0,
988
+ updated: 0,
989
+ seq: 0,
990
+ contentHash: '',
991
+ provenance
992
+ })
993
+ .orThrow(),
994
+ body
995
+ };
996
+ }
997
+
998
+ test('an omitted key is preserved and an explicit null clears, surviving a reload', async () => {
999
+ const root = mutableRoot();
1000
+ const store = createStore({ root }).orThrow();
1001
+ (
1002
+ await store.put(provenanceRecord({ source: 'agent', confidence: 0.9, note: 'stale' }, 'first body'))
1003
+ ).orThrow();
1004
+
1005
+ // The second put omits `confidence` (must be preserved) and nulls `note`
1006
+ // (must be cleared) — the per-key merge, driven through the store.
1007
+ expect(
1008
+ await store.put(provenanceRecord({ source: 'agent', note: null }, 'second body'))
1009
+ ).toSucceedAndSatisfy((updated: IMemoryRecord<unknown>) => {
1010
+ expect(updated.envelope.provenance).toEqual({ source: 'agent', confidence: 0.9 });
1011
+ });
1012
+
1013
+ // A fresh store over the same root re-reads the persisted frontmatter: the
1014
+ // cleared key must not resurface.
1015
+ const reopened = createStore({ root }).orThrow();
1016
+ expect(await reopened.get(knowledgeKind, 'doc-p' as EntityId)).toSucceedAndSatisfy((r) => {
1017
+ expect(r?.envelope.provenance).toEqual({ source: 'agent', confidence: 0.9 });
1018
+ });
1019
+ });
1020
+
1021
+ test('a whole-block null provenance is not expressible — the converter rejects it', () => {
1022
+ // The README says the whole-block delete fails at the converter rather than
1023
+ // at the policy on the store path, because `IMemoryEnvelope.provenance` is
1024
+ // non-nullable. Pin that rejection site.
1025
+ expect(
1026
+ envelopeConverter.convert({
1027
+ id: 'doc-p',
1028
+ entityId: 'doc-p',
1029
+ kind: 'knowledge',
1030
+ tags: [],
1031
+ links: [],
1032
+ created: 0,
1033
+ updated: 0,
1034
+ seq: 0,
1035
+ contentHash: '',
1036
+ provenance: null
1037
+ })
1038
+ ).toFailWith(/provenance/i);
1039
+ });
1040
+ });
966
1041
  });