@seanhogg/builderforce-memory 2026.9.28 → 2026.9.30

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 (104) hide show
  1. package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
  2. package/dist/bridges/AnthropicBridge.js +24 -65
  3. package/dist/bridges/AnthropicBridge.js.map +1 -1
  4. package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
  5. package/dist/bridges/OpenAIBridge.js +12 -46
  6. package/dist/bridges/OpenAIBridge.js.map +1 -1
  7. package/dist/bridges/VertexAIBridge.d.ts.map +1 -1
  8. package/dist/bridges/VertexAIBridge.js +3 -34
  9. package/dist/bridges/VertexAIBridge.js.map +1 -1
  10. package/dist/distillation/DistillationEngine.d.ts +71 -54
  11. package/dist/distillation/DistillationEngine.d.ts.map +1 -1
  12. package/dist/distillation/DistillationEngine.js +112 -95
  13. package/dist/distillation/DistillationEngine.js.map +1 -1
  14. package/dist/distillation/index.d.ts +7 -1
  15. package/dist/distillation/index.d.ts.map +1 -1
  16. package/dist/distillation/index.js +5 -0
  17. package/dist/distillation/index.js.map +1 -1
  18. package/dist/distillation/ports.d.ts +43 -0
  19. package/dist/distillation/ports.d.ts.map +1 -0
  20. package/dist/distillation/ports.js +29 -0
  21. package/dist/distillation/ports.js.map +1 -0
  22. package/dist/eval/EvalHarness.d.ts +2 -1
  23. package/dist/eval/EvalHarness.d.ts.map +1 -1
  24. package/dist/eval/EvalHarness.js +13 -1
  25. package/dist/eval/EvalHarness.js.map +1 -1
  26. package/dist/eval/baseline.d.ts +33 -0
  27. package/dist/eval/baseline.d.ts.map +1 -0
  28. package/dist/eval/baseline.js +41 -0
  29. package/dist/eval/baseline.js.map +1 -0
  30. package/dist/eval/index.d.ts +1 -0
  31. package/dist/eval/index.d.ts.map +1 -1
  32. package/dist/eval/index.js +1 -0
  33. package/dist/eval/index.js.map +1 -1
  34. package/dist/eval/types.d.ts +7 -0
  35. package/dist/eval/types.d.ts.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/rag/EnterpriseRetriever.d.ts.map +1 -1
  41. package/dist/rag/EnterpriseRetriever.js +2 -1
  42. package/dist/rag/EnterpriseRetriever.js.map +1 -1
  43. package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
  44. package/dist/retrieval/HybridRetriever.js +2 -1
  45. package/dist/retrieval/HybridRetriever.js.map +1 -1
  46. package/dist/retrieval/decay.d.ts +24 -0
  47. package/dist/retrieval/decay.d.ts.map +1 -0
  48. package/dist/retrieval/decay.js +29 -0
  49. package/dist/retrieval/decay.js.map +1 -0
  50. package/dist/retrieval/fusion.d.ts +13 -19
  51. package/dist/retrieval/fusion.d.ts.map +1 -1
  52. package/dist/retrieval/fusion.js +17 -44
  53. package/dist/retrieval/fusion.js.map +1 -1
  54. package/dist/retrieval/index.d.ts +13 -6
  55. package/dist/retrieval/index.d.ts.map +1 -1
  56. package/dist/retrieval/index.js +10 -5
  57. package/dist/retrieval/index.js.map +1 -1
  58. package/dist/retrieval/mmr.d.ts +60 -0
  59. package/dist/retrieval/mmr.d.ts.map +1 -0
  60. package/dist/retrieval/mmr.js +94 -0
  61. package/dist/retrieval/mmr.js.map +1 -0
  62. package/dist/retrieval/weightedFusion.d.ts +64 -0
  63. package/dist/retrieval/weightedFusion.d.ts.map +1 -0
  64. package/dist/retrieval/weightedFusion.js +80 -0
  65. package/dist/retrieval/weightedFusion.js.map +1 -0
  66. package/dist/similarity/index.d.ts +6 -4
  67. package/dist/similarity/index.d.ts.map +1 -1
  68. package/dist/similarity/index.js +5 -3
  69. package/dist/similarity/index.js.map +1 -1
  70. package/dist/wire/index.d.ts +18 -0
  71. package/dist/wire/index.d.ts.map +1 -0
  72. package/dist/wire/index.js +17 -0
  73. package/dist/wire/index.js.map +1 -0
  74. package/dist/wire/sse.d.ts +29 -0
  75. package/dist/wire/sse.d.ts.map +1 -0
  76. package/dist/wire/sse.js +81 -0
  77. package/dist/wire/sse.js.map +1 -0
  78. package/dist/wire/usage.d.ts +46 -0
  79. package/dist/wire/usage.d.ts.map +1 -0
  80. package/dist/wire/usage.js +78 -0
  81. package/dist/wire/usage.js.map +1 -0
  82. package/package.json +11 -3
  83. package/src/bridges/AnthropicBridge.ts +24 -68
  84. package/src/bridges/OpenAIBridge.ts +11 -50
  85. package/src/bridges/VertexAIBridge.ts +3 -32
  86. package/src/distillation/DistillationEngine.ts +176 -141
  87. package/src/distillation/index.ts +15 -1
  88. package/src/distillation/ports.ts +55 -0
  89. package/src/eval/EvalHarness.ts +12 -1
  90. package/src/eval/baseline.ts +51 -0
  91. package/src/eval/index.ts +1 -0
  92. package/src/eval/types.ts +7 -0
  93. package/src/index.ts +5 -2
  94. package/src/rag/EnterpriseRetriever.ts +2 -1
  95. package/src/retrieval/HybridRetriever.ts +2 -1
  96. package/src/retrieval/decay.ts +29 -0
  97. package/src/retrieval/fusion.ts +19 -52
  98. package/src/retrieval/index.ts +24 -6
  99. package/src/retrieval/mmr.ts +126 -0
  100. package/src/retrieval/weightedFusion.ts +101 -0
  101. package/src/similarity/index.ts +6 -4
  102. package/src/wire/index.ts +18 -0
  103. package/src/wire/sse.ts +74 -0
  104. package/src/wire/usage.ts +95 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-memory",
3
- "version": "2026.9.28",
3
+ "version": "2026.9.30",
4
4
  "description": "BuilderForce Agent Memory — runtime layer. SSM execution, Transformer orchestration, online distillation, enterprise RAG (ingestion pipelines, vector-store adapters, tenant/ACL-scoped hybrid retrieval), multi-agent graph orchestration, LLM telemetry and cost accounting, and persistent agent memory for BuilderForce.ai agents.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -14,6 +14,14 @@
14
14
  "types": "./dist/retrieval/index.d.ts",
15
15
  "import": "./dist/retrieval/index.js"
16
16
  },
17
+ "./wire": {
18
+ "types": "./dist/wire/index.d.ts",
19
+ "import": "./dist/wire/index.js"
20
+ },
21
+ "./distillation": {
22
+ "types": "./dist/distillation/index.d.ts",
23
+ "import": "./dist/distillation/index.js"
24
+ },
17
25
  "./telemetry": {
18
26
  "types": "./dist/telemetry/index.d.ts",
19
27
  "import": "./dist/telemetry/index.js"
@@ -87,7 +95,7 @@
87
95
  },
88
96
  "homepage": "https://github.com/SeanHogg/builderforce-memory/tree/main/packages/memory#readme",
89
97
  "peerDependencies": {
90
- "@seanhogg/builderforce-memory-engine": "^2026.9.28"
98
+ "@seanhogg/builderforce-memory-engine": "^2026.9.30"
91
99
  },
92
100
  "devDependencies": {
93
101
  "@jest/globals": "^29.7.0",
@@ -101,7 +109,7 @@
101
109
  "ts-jest": "^29.2.0",
102
110
  "typescript": "^6.0.3",
103
111
  "@typescript/native-preview": "7.0.0-dev.20260627.2",
104
- "@seanhogg/builderforce-memory-engine": "2026.9.28"
112
+ "@seanhogg/builderforce-memory-engine": "2026.9.30"
105
113
  },
106
114
  "jest": {
107
115
  "preset": "ts-jest/presets/default-esm",
@@ -8,6 +8,7 @@
8
8
  import { SSMError } from '../errors/SSMError.js';
9
9
  import type { LlmUsage } from '../telemetry/types.js';
10
10
  import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
11
+ import { readSseDataFrames, readUsageFields, AnthropicStreamUsage } from '../wire/index.js';
11
12
 
12
13
  export interface AnthropicBridgeOptions {
13
14
  /** Anthropic API key. */
@@ -110,30 +111,26 @@ export class AnthropicBridge implements TransformerBridge {
110
111
  throw new SSMError('BRIDGE_RESPONSE_INVALID', 'Anthropic streaming response has no body.');
111
112
  }
112
113
 
113
- const model = opts.model ?? this._model;
114
- let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, reportedModel = model;
115
-
116
- yield* parseAnthropicStream(res.body, (event) => {
117
- // `message_start` carries the input split; `message_delta` the running
118
- // output count. Both are folded so the final usage is provider-measured.
119
- const start = (event as any).message?.usage;
120
- const delta = (event as any).usage;
121
- const usage = start ?? delta;
122
- if (!usage) return;
123
- if (typeof (event as any).message?.model === 'string') reportedModel = (event as any).message.model;
124
- input = Math.max(input, Number(usage.input_tokens) || 0);
125
- output = Math.max(output, Number(usage.output_tokens) || 0);
126
- cacheRead = Math.max(cacheRead, Number(usage.cache_read_input_tokens) || 0);
127
- cacheWrite = Math.max(cacheWrite, Number(usage.cache_creation_input_tokens) || 0);
128
- });
114
+ // `message_start` carries the input split; `message_delta` the running
115
+ // output count. Both are folded so the final usage is provider-measured.
116
+ const usage = new AnthropicStreamUsage();
117
+ for await (const frame of readSseDataFrames(res.body)) {
118
+ usage.observe(frame);
119
+ const ev = frame as { type?: unknown; delta?: { text?: unknown } };
120
+ // content_block_delta events carry the streamed text
121
+ if (ev.type === 'content_block_delta' && typeof ev.delta?.text === 'string' && ev.delta.text.length > 0) {
122
+ yield ev.delta.text;
123
+ }
124
+ }
129
125
 
126
+ const totals = usage.result();
130
127
  this._lastCall = {
131
128
  usage: {
132
- model: reportedModel,
133
- inputTokens: input,
134
- outputTokens: output,
135
- cachedInputTokens: cacheRead,
136
- cacheWriteTokens: cacheWrite,
129
+ model: totals.model ?? opts.model ?? this._model,
130
+ inputTokens: totals.inputTokens,
131
+ outputTokens: totals.outputTokens,
132
+ cachedInputTokens: totals.cacheReadTokens,
133
+ cacheWriteTokens: totals.cacheCreationTokens,
137
134
  },
138
135
  };
139
136
  }
@@ -171,48 +168,6 @@ export class AnthropicBridge implements TransformerBridge {
171
168
  }
172
169
  }
173
170
 
174
- // ── SSE parser (Anthropic event format) ──────────────────────────────────────
175
-
176
- async function* parseAnthropicStream(
177
- body: ReadableStream<Uint8Array>,
178
- onEvent?: (event: Record<string, unknown>) => void,
179
- ): AsyncIterable<string> {
180
- const reader = body.getReader();
181
- const decoder = new TextDecoder();
182
- let buffer = '';
183
-
184
- try {
185
- while (true) {
186
- const { done, value } = await reader.read();
187
- if (done) break;
188
-
189
- buffer += decoder.decode(value, { stream: true });
190
- const lines = buffer.split('\n');
191
- buffer = lines.pop() as string; // split() always yields ≥1 element → never undefined
192
-
193
- for (const line of lines) {
194
- const trimmed = line.trim();
195
- if (!trimmed.startsWith('data: ')) continue;
196
-
197
- const data = trimmed.slice(6);
198
- try {
199
- const event = JSON.parse(data) as Record<string, unknown>;
200
- onEvent?.(event);
201
- // content_block_delta events carry the streamed text
202
- if (event['type'] === 'content_block_delta') {
203
- const text = (event as any).delta?.text;
204
- if (typeof text === 'string' && text.length > 0) yield text;
205
- }
206
- } catch {
207
- // Skip malformed SSE lines
208
- }
209
- }
210
- }
211
- } finally {
212
- reader.releaseLock();
213
- }
214
- }
215
-
216
171
  /**
217
172
  * Maps an Anthropic `usage` object onto the canonical {@link LlmUsage} shape.
218
173
  * Absent when the response omits usage (an older gateway shim), in which case
@@ -220,12 +175,13 @@ async function* parseAnthropicStream(
220
175
  */
221
176
  function readAnthropicUsage(usage: unknown, model: string): LlmUsage | undefined {
222
177
  if (!usage || typeof usage !== 'object') return undefined;
223
- const u = usage as Record<string, unknown>;
178
+ // Anthropic's `input_tokens` already excludes both cache counts.
179
+ const u = readUsageFields(usage);
224
180
  return {
225
181
  model,
226
- inputTokens: Number(u['input_tokens']) || 0,
227
- outputTokens: Number(u['output_tokens']) || 0,
228
- cachedInputTokens: Number(u['cache_read_input_tokens']) || 0,
229
- cacheWriteTokens: Number(u['cache_creation_input_tokens']) || 0,
182
+ inputTokens: u.promptTokens ?? 0,
183
+ outputTokens: u.completionTokens ?? 0,
184
+ cachedInputTokens: u.cacheReadTokens ?? 0,
185
+ cacheWriteTokens: u.cacheCreationTokens ?? 0,
230
186
  };
231
187
  }
@@ -8,6 +8,7 @@
8
8
  import { SSMError } from '../errors/SSMError.js';
9
9
  import type { LlmUsage } from '../telemetry/types.js';
10
10
  import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
11
+ import { readSseDataFrames, readUsageFields } from '../wire/index.js';
11
12
 
12
13
  export interface OpenAIBridgeOptions {
13
14
  /** OpenAI API key (or compatible service key). */
@@ -88,12 +89,14 @@ export class OpenAIBridge implements TransformerBridge {
88
89
  const model = opts.model ?? this._model;
89
90
  let usage: LlmUsage | undefined;
90
91
 
91
- yield* parseOpenAIStream(res.body, (event) => {
92
- // Only emitted because `_buildBody` sets `stream_options.include_usage`;
92
+ for await (const chunk of readSseDataFrames(res.body)) {
93
+ // Usage is only emitted because `_buildBody` sets `stream_options.include_usage`;
93
94
  // without it OpenAI streams no usage at all and cost would be a guess.
94
- const reported = readOpenAIUsage((event as any).usage, (event as any).model ?? model);
95
+ const reported = readOpenAIUsage((chunk as any).usage, (chunk as any).model ?? model);
95
96
  if (reported) usage = reported;
96
- });
97
+ const delta = (chunk as any).choices?.[0]?.delta?.content;
98
+ if (typeof delta === 'string' && delta.length > 0) yield delta;
99
+ }
97
100
 
98
101
  this._lastCall = usage ? { usage } : {};
99
102
  }
@@ -130,59 +133,17 @@ export class OpenAIBridge implements TransformerBridge {
130
133
  }
131
134
  }
132
135
 
133
- // ── SSE parser ────────────────────────────────────────────────────────────────
134
-
135
- async function* parseOpenAIStream(
136
- body: ReadableStream<Uint8Array>,
137
- onEvent?: (event: Record<string, unknown>) => void,
138
- ): AsyncIterable<string> {
139
- const reader = body.getReader();
140
- const decoder = new TextDecoder();
141
- let buffer = '';
142
-
143
- try {
144
- while (true) {
145
- const { done, value } = await reader.read();
146
- if (done) break;
147
-
148
- buffer += decoder.decode(value, { stream: true });
149
- const lines = buffer.split('\n');
150
- buffer = lines.pop() as string; // keep the last (possibly partial) line; split() always yields ≥1 element
151
-
152
- for (const line of lines) {
153
- const trimmed = line.trim();
154
- if (!trimmed.startsWith('data: ')) continue;
155
-
156
- const data = trimmed.slice(6);
157
- if (data === '[DONE]') return;
158
-
159
- try {
160
- const chunk = JSON.parse(data) as Record<string, unknown>;
161
- onEvent?.(chunk);
162
- const delta = (chunk as any).choices?.[0]?.delta?.content;
163
- if (typeof delta === 'string' && delta.length > 0) yield delta;
164
- } catch {
165
- // Malformed JSON in stream — skip silently
166
- }
167
- }
168
- }
169
- } finally {
170
- reader.releaseLock();
171
- }
172
- }
173
-
174
136
  /** Maps an OpenAI `usage` object onto the canonical {@link LlmUsage} shape. */
175
137
  function readOpenAIUsage(usage: unknown, model: string): LlmUsage | undefined {
176
138
  if (!usage || typeof usage !== 'object') return undefined;
177
- const u = usage as Record<string, unknown>;
178
- const cached = Number((u['prompt_tokens_details'] as Record<string, unknown> | undefined)?.['cached_tokens']) || 0;
179
- const prompt = Number(u['prompt_tokens']) || 0;
139
+ const u = readUsageFields(usage);
140
+ const cached = u.cacheReadTokens ?? 0;
180
141
  return {
181
142
  model,
182
143
  // OpenAI reports `prompt_tokens` INCLUSIVE of cached tokens; the canonical
183
144
  // shape keeps them disjoint so the two rates are applied exactly once each.
184
- inputTokens: Math.max(0, prompt - cached),
185
- outputTokens: Number(u['completion_tokens']) || 0,
145
+ inputTokens: Math.max(0, (u.promptTokens ?? 0) - cached),
146
+ outputTokens: u.completionTokens ?? 0,
186
147
  cachedInputTokens: cached,
187
148
  };
188
149
  }
@@ -25,6 +25,7 @@
25
25
  import { SSMError } from '../errors/SSMError.js';
26
26
  import type { LlmUsage } from '../telemetry/types.js';
27
27
  import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
28
+ import { readSseDataFrames } from '../wire/index.js';
28
29
 
29
30
  export interface VertexAIBridgeOptions {
30
31
  /** GCP project id. */
@@ -101,7 +102,8 @@ export class VertexAIBridge implements TransformerBridge {
101
102
  }
102
103
 
103
104
  let usage: LlmUsage | undefined;
104
- for await (const event of parseSseJson(res.body)) {
105
+ for await (const frame of readSseDataFrames(res.body)) {
106
+ const event = frame as Record<string, unknown>;
105
107
  const text = readCandidateText(event);
106
108
  if (text) yield text;
107
109
  // Vertex repeats cumulative usage on each chunk; the last one wins.
@@ -264,34 +266,3 @@ function readVertexUsage(usage: unknown, model: string): LlmUsage | undefined {
264
266
  cachedInputTokens: cached,
265
267
  };
266
268
  }
267
-
268
- /** Parses an `alt=sse` stream into JSON events. */
269
- async function* parseSseJson(body: ReadableStream<Uint8Array>): AsyncIterable<Record<string, unknown>> {
270
- const reader = body.getReader();
271
- const decoder = new TextDecoder();
272
- let buffer = '';
273
-
274
- try {
275
- while (true) {
276
- const { done, value } = await reader.read();
277
- if (done) break;
278
- buffer += decoder.decode(value, { stream: true });
279
-
280
- const lines = buffer.split('\n');
281
- buffer = lines.pop() as string;
282
- for (const line of lines) {
283
- const trimmed = line.trim();
284
- if (!trimmed.startsWith('data:')) continue;
285
- const payload = trimmed.slice(5).trim();
286
- if (!payload || payload === '[DONE]') continue;
287
- try {
288
- yield JSON.parse(payload) as Record<string, unknown>;
289
- } catch {
290
- // Skip malformed SSE frames rather than failing the stream.
291
- }
292
- }
293
- }
294
- } finally {
295
- reader.releaseLock();
296
- }
297
- }