@seanhogg/builderforce-memory 2026.7.14 → 2026.9.28

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 (225) hide show
  1. package/README.md +10 -0
  2. package/dist/bridges/AnthropicBridge.d.ts +9 -1
  3. package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
  4. package/dist/bridges/AnthropicBridge.js +58 -2
  5. package/dist/bridges/AnthropicBridge.js.map +1 -1
  6. package/dist/bridges/CachingBridge.d.ts +9 -1
  7. package/dist/bridges/CachingBridge.d.ts.map +1 -1
  8. package/dist/bridges/CachingBridge.js +24 -1
  9. package/dist/bridges/CachingBridge.js.map +1 -1
  10. package/dist/bridges/OpenAIBridge.d.ts +4 -1
  11. package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
  12. package/dist/bridges/OpenAIBridge.js +40 -2
  13. package/dist/bridges/OpenAIBridge.js.map +1 -1
  14. package/dist/bridges/SemanticCachingBridge.d.ts +8 -1
  15. package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -1
  16. package/dist/bridges/SemanticCachingBridge.js +26 -1
  17. package/dist/bridges/SemanticCachingBridge.js.map +1 -1
  18. package/dist/bridges/TransformerBridge.d.ts +13 -0
  19. package/dist/bridges/TransformerBridge.d.ts.map +1 -1
  20. package/dist/bridges/VertexAIBridge.d.ts +102 -0
  21. package/dist/bridges/VertexAIBridge.d.ts.map +1 -0
  22. package/dist/bridges/VertexAIBridge.js +224 -0
  23. package/dist/bridges/VertexAIBridge.js.map +1 -0
  24. package/dist/bridges/index.d.ts +3 -1
  25. package/dist/bridges/index.d.ts.map +1 -1
  26. package/dist/bridges/index.js +1 -0
  27. package/dist/bridges/index.js.map +1 -1
  28. package/dist/eval/EvalHarness.d.ts +37 -0
  29. package/dist/eval/EvalHarness.d.ts.map +1 -0
  30. package/dist/eval/EvalHarness.js +178 -0
  31. package/dist/eval/EvalHarness.js.map +1 -0
  32. package/dist/eval/graders.d.ts +76 -0
  33. package/dist/eval/graders.d.ts.map +1 -0
  34. package/dist/eval/graders.js +168 -0
  35. package/dist/eval/graders.js.map +1 -0
  36. package/dist/eval/index.d.ts +26 -0
  37. package/dist/eval/index.d.ts.map +1 -0
  38. package/dist/eval/index.js +26 -0
  39. package/dist/eval/index.js.map +1 -0
  40. package/dist/eval/types.d.ts +97 -0
  41. package/dist/eval/types.d.ts.map +1 -0
  42. package/dist/eval/types.js +5 -0
  43. package/dist/eval/types.js.map +1 -0
  44. package/dist/evermind/contract.d.ts +40 -0
  45. package/dist/evermind/contract.d.ts.map +1 -0
  46. package/dist/evermind/contract.js +13 -0
  47. package/dist/evermind/contract.js.map +1 -0
  48. package/dist/evermind/index.d.ts +15 -0
  49. package/dist/evermind/index.d.ts.map +1 -0
  50. package/dist/evermind/index.js +4 -0
  51. package/dist/evermind/index.js.map +1 -0
  52. package/dist/evermind/memoryBlock.d.ts +7 -0
  53. package/dist/evermind/memoryBlock.d.ts.map +1 -0
  54. package/dist/evermind/memoryBlock.js +37 -0
  55. package/dist/evermind/memoryBlock.js.map +1 -0
  56. package/dist/evermind/recall.d.ts +48 -0
  57. package/dist/evermind/recall.d.ts.map +1 -0
  58. package/dist/evermind/recall.js +105 -0
  59. package/dist/evermind/recall.js.map +1 -0
  60. package/dist/index.d.ts +18 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +14 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/ingest/IngestionPipeline.d.ts +95 -0
  65. package/dist/ingest/IngestionPipeline.d.ts.map +1 -0
  66. package/dist/ingest/IngestionPipeline.js +295 -0
  67. package/dist/ingest/IngestionPipeline.js.map +1 -0
  68. package/dist/ingest/index.d.ts +10 -0
  69. package/dist/ingest/index.d.ts.map +1 -0
  70. package/dist/ingest/index.js +8 -0
  71. package/dist/ingest/index.js.map +1 -0
  72. package/dist/ingest/parsers.d.ts +53 -0
  73. package/dist/ingest/parsers.d.ts.map +1 -0
  74. package/dist/ingest/parsers.js +306 -0
  75. package/dist/ingest/parsers.js.map +1 -0
  76. package/dist/ingest/types.d.ts +114 -0
  77. package/dist/ingest/types.d.ts.map +1 -0
  78. package/dist/ingest/types.js +19 -0
  79. package/dist/ingest/types.js.map +1 -0
  80. package/dist/memory/MemoryStore.d.ts +33 -0
  81. package/dist/memory/MemoryStore.d.ts.map +1 -1
  82. package/dist/memory/MemoryStore.js +29 -3
  83. package/dist/memory/MemoryStore.js.map +1 -1
  84. package/dist/memory/index.d.ts +1 -1
  85. package/dist/memory/index.d.ts.map +1 -1
  86. package/dist/orchestration/AgentGraph.d.ts +104 -0
  87. package/dist/orchestration/AgentGraph.d.ts.map +1 -0
  88. package/dist/orchestration/AgentGraph.js +364 -0
  89. package/dist/orchestration/AgentGraph.js.map +1 -0
  90. package/dist/orchestration/checkpoint.d.ts +58 -0
  91. package/dist/orchestration/checkpoint.d.ts.map +1 -0
  92. package/dist/orchestration/checkpoint.js +92 -0
  93. package/dist/orchestration/checkpoint.js.map +1 -0
  94. package/dist/orchestration/index.d.ts +14 -0
  95. package/dist/orchestration/index.d.ts.map +1 -0
  96. package/dist/orchestration/index.js +10 -0
  97. package/dist/orchestration/index.js.map +1 -0
  98. package/dist/orchestration/patterns.d.ts +159 -0
  99. package/dist/orchestration/patterns.d.ts.map +1 -0
  100. package/dist/orchestration/patterns.js +371 -0
  101. package/dist/orchestration/patterns.js.map +1 -0
  102. package/dist/orchestration/types.d.ts +146 -0
  103. package/dist/orchestration/types.d.ts.map +1 -0
  104. package/dist/orchestration/types.js +20 -0
  105. package/dist/orchestration/types.js.map +1 -0
  106. package/dist/rag/EnterpriseRetriever.d.ts +114 -0
  107. package/dist/rag/EnterpriseRetriever.d.ts.map +1 -0
  108. package/dist/rag/EnterpriseRetriever.js +240 -0
  109. package/dist/rag/EnterpriseRetriever.js.map +1 -0
  110. package/dist/rag/index.d.ts +6 -0
  111. package/dist/rag/index.d.ts.map +1 -0
  112. package/dist/rag/index.js +5 -0
  113. package/dist/rag/index.js.map +1 -0
  114. package/dist/retrieval/bm25.d.ts +48 -1
  115. package/dist/retrieval/bm25.d.ts.map +1 -1
  116. package/dist/retrieval/bm25.js +59 -13
  117. package/dist/retrieval/bm25.js.map +1 -1
  118. package/dist/retrieval/index.d.ts +2 -2
  119. package/dist/retrieval/index.d.ts.map +1 -1
  120. package/dist/retrieval/index.js +1 -1
  121. package/dist/retrieval/index.js.map +1 -1
  122. package/dist/runtime/SSMRuntime.d.ts +1 -1
  123. package/dist/runtime/SSMRuntime.d.ts.map +1 -1
  124. package/dist/telemetry/InstrumentedBridge.d.ts +62 -0
  125. package/dist/telemetry/InstrumentedBridge.d.ts.map +1 -0
  126. package/dist/telemetry/InstrumentedBridge.js +120 -0
  127. package/dist/telemetry/InstrumentedBridge.js.map +1 -0
  128. package/dist/telemetry/MetricsRegistry.d.ts +88 -0
  129. package/dist/telemetry/MetricsRegistry.d.ts.map +1 -0
  130. package/dist/telemetry/MetricsRegistry.js +167 -0
  131. package/dist/telemetry/MetricsRegistry.js.map +1 -0
  132. package/dist/telemetry/Tracer.d.ts +137 -0
  133. package/dist/telemetry/Tracer.d.ts.map +1 -0
  134. package/dist/telemetry/Tracer.js +244 -0
  135. package/dist/telemetry/Tracer.js.map +1 -0
  136. package/dist/telemetry/exporters.d.ts +68 -0
  137. package/dist/telemetry/exporters.d.ts.map +1 -0
  138. package/dist/telemetry/exporters.js +145 -0
  139. package/dist/telemetry/exporters.js.map +1 -0
  140. package/dist/telemetry/index.d.ts +19 -0
  141. package/dist/telemetry/index.d.ts.map +1 -0
  142. package/dist/telemetry/index.js +12 -0
  143. package/dist/telemetry/index.js.map +1 -0
  144. package/dist/telemetry/pricing.d.ts +74 -0
  145. package/dist/telemetry/pricing.d.ts.map +1 -0
  146. package/dist/telemetry/pricing.js +98 -0
  147. package/dist/telemetry/pricing.js.map +1 -0
  148. package/dist/telemetry/traceparent.d.ts +50 -0
  149. package/dist/telemetry/traceparent.d.ts.map +1 -0
  150. package/dist/telemetry/traceparent.js +77 -0
  151. package/dist/telemetry/traceparent.js.map +1 -0
  152. package/dist/telemetry/types.d.ts +90 -0
  153. package/dist/telemetry/types.d.ts.map +1 -0
  154. package/dist/telemetry/types.js +17 -0
  155. package/dist/telemetry/types.js.map +1 -0
  156. package/dist/vectorstore/MemoryVectorStore.d.ts +72 -0
  157. package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -0
  158. package/dist/vectorstore/MemoryVectorStore.js +205 -0
  159. package/dist/vectorstore/MemoryVectorStore.js.map +1 -0
  160. package/dist/vectorstore/RestVectorStore.d.ts +79 -0
  161. package/dist/vectorstore/RestVectorStore.d.ts.map +1 -0
  162. package/dist/vectorstore/RestVectorStore.js +170 -0
  163. package/dist/vectorstore/RestVectorStore.js.map +1 -0
  164. package/dist/vectorstore/dialects.d.ts +101 -0
  165. package/dist/vectorstore/dialects.d.ts.map +1 -0
  166. package/dist/vectorstore/dialects.js +618 -0
  167. package/dist/vectorstore/dialects.js.map +1 -0
  168. package/dist/vectorstore/filter.d.ts +42 -0
  169. package/dist/vectorstore/filter.d.ts.map +1 -0
  170. package/dist/vectorstore/filter.js +132 -0
  171. package/dist/vectorstore/filter.js.map +1 -0
  172. package/dist/vectorstore/index.d.ts +15 -0
  173. package/dist/vectorstore/index.d.ts.map +1 -0
  174. package/dist/vectorstore/index.js +11 -0
  175. package/dist/vectorstore/index.js.map +1 -0
  176. package/dist/vectorstore/types.d.ts +151 -0
  177. package/dist/vectorstore/types.d.ts.map +1 -0
  178. package/dist/vectorstore/types.js +30 -0
  179. package/dist/vectorstore/types.js.map +1 -0
  180. package/package.json +39 -5
  181. package/src/bridges/AnthropicBridge.ts +68 -3
  182. package/src/bridges/CachingBridge.ts +28 -2
  183. package/src/bridges/OpenAIBridge.ts +48 -3
  184. package/src/bridges/SemanticCachingBridge.ts +29 -2
  185. package/src/bridges/TransformerBridge.ts +16 -0
  186. package/src/bridges/VertexAIBridge.ts +297 -0
  187. package/src/bridges/index.ts +3 -1
  188. package/src/eval/EvalHarness.ts +214 -0
  189. package/src/eval/graders.ts +218 -0
  190. package/src/eval/index.ts +26 -0
  191. package/src/eval/types.ts +107 -0
  192. package/src/evermind/contract.ts +39 -0
  193. package/src/evermind/index.ts +18 -0
  194. package/src/evermind/memoryBlock.ts +38 -0
  195. package/src/evermind/recall.ts +122 -0
  196. package/src/index.ts +232 -0
  197. package/src/ingest/IngestionPipeline.ts +378 -0
  198. package/src/ingest/index.ts +46 -0
  199. package/src/ingest/parsers.ts +318 -0
  200. package/src/ingest/types.ts +116 -0
  201. package/src/memory/MemoryStore.ts +55 -3
  202. package/src/memory/index.ts +1 -1
  203. package/src/orchestration/AgentGraph.ts +462 -0
  204. package/src/orchestration/checkpoint.ts +129 -0
  205. package/src/orchestration/index.ts +63 -0
  206. package/src/orchestration/patterns.ts +532 -0
  207. package/src/orchestration/types.ts +128 -0
  208. package/src/rag/EnterpriseRetriever.ts +348 -0
  209. package/src/rag/index.ts +18 -0
  210. package/src/retrieval/bm25.ts +71 -13
  211. package/src/retrieval/index.ts +2 -2
  212. package/src/telemetry/InstrumentedBridge.ts +152 -0
  213. package/src/telemetry/MetricsRegistry.ts +263 -0
  214. package/src/telemetry/Tracer.ts +310 -0
  215. package/src/telemetry/exporters.ts +187 -0
  216. package/src/telemetry/index.ts +56 -0
  217. package/src/telemetry/pricing.ts +129 -0
  218. package/src/telemetry/traceparent.ts +89 -0
  219. package/src/telemetry/types.ts +105 -0
  220. package/src/vectorstore/MemoryVectorStore.ts +250 -0
  221. package/src/vectorstore/RestVectorStore.ts +236 -0
  222. package/src/vectorstore/dialects.ts +702 -0
  223. package/src/vectorstore/filter.ts +148 -0
  224. package/src/vectorstore/index.ts +53 -0
  225. package/src/vectorstore/types.ts +149 -0
@@ -16,7 +16,7 @@
16
16
  * consumed once and caching it would defeat its purpose.
17
17
  */
18
18
 
19
- import type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
19
+ import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
20
20
  import { ResponseCache, buildCacheKey, type ResponseCacheOptions } from './ResponseCache.js';
21
21
 
22
22
  export interface CachingBridgeOptions extends ResponseCacheOptions {
@@ -32,6 +32,8 @@ export class CachingBridge implements TransformerBridge {
32
32
  private readonly _inner : TransformerBridge;
33
33
  private readonly _cache : ResponseCache;
34
34
 
35
+ private _lastCall: BridgeCallInfo | undefined;
36
+
35
37
  constructor(inner: TransformerBridge, opts: CachingBridgeOptions = {}) {
36
38
  this._inner = inner;
37
39
  this._cache = opts.cache ?? new ResponseCache(opts);
@@ -47,6 +49,16 @@ export class CachingBridge implements TransformerBridge {
47
49
  return this._cache;
48
50
  }
49
51
 
52
+ /**
53
+ * A hit is declared here rather than inferred downstream, which is what turns
54
+ * the cache-hit rate into a measured metric. On a miss the inner bridge's own
55
+ * provider-reported usage is passed through unchanged, so decorating a bridge
56
+ * never downgrades its cost accounting to an estimate.
57
+ */
58
+ get lastCall(): BridgeCallInfo | undefined {
59
+ return this._lastCall;
60
+ }
61
+
50
62
  async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
51
63
  const key = buildCacheKey({
52
64
  prompt,
@@ -58,10 +70,24 @@ export class CachingBridge implements TransformerBridge {
58
70
  });
59
71
 
60
72
  const cached = this._cache.get(key);
61
- if (cached !== undefined) return cached;
73
+ if (cached !== undefined) {
74
+ this._lastCall = {
75
+ cacheHit : true,
76
+ cacheTier: 'exact',
77
+ usage : {
78
+ model : opts.model ?? 'unknown',
79
+ inputTokens : 0,
80
+ outputTokens : 0,
81
+ localCacheHit : true,
82
+ },
83
+ };
84
+ return cached;
85
+ }
62
86
 
63
87
  const value = await this._inner.generate(prompt, opts);
64
88
  this._cache.set(key, value, Date.now());
89
+ const inner = this._inner.lastCall;
90
+ this._lastCall = { cacheHit: false, ...(inner?.usage ? { usage: inner.usage } : {}) };
65
91
  return value;
66
92
  }
67
93
 
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import { SSMError } from '../errors/SSMError.js';
9
- import type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
9
+ import type { LlmUsage } from '../telemetry/types.js';
10
+ import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
10
11
 
11
12
  export interface OpenAIBridgeOptions {
12
13
  /** OpenAI API key (or compatible service key). */
@@ -22,6 +23,8 @@ export interface OpenAIBridgeOptions {
22
23
  }
23
24
 
24
25
  export class OpenAIBridge implements TransformerBridge {
26
+ private _lastCall: BridgeCallInfo | undefined;
27
+
25
28
  readonly supportsStreaming = true as const;
26
29
 
27
30
  private readonly _apiKey : string;
@@ -38,6 +41,11 @@ export class OpenAIBridge implements TransformerBridge {
38
41
  this._maxTokens = opts.maxTokens ?? 512;
39
42
  }
40
43
 
44
+ /** Provider-reported usage for the last call — see {@link BridgeCallInfo}. */
45
+ get lastCall(): BridgeCallInfo | undefined {
46
+ return this._lastCall;
47
+ }
48
+
41
49
  async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
42
50
  const body = this._buildBody(prompt, opts, false);
43
51
  const res = await this._fetch(body);
@@ -55,6 +63,9 @@ export class OpenAIBridge implements TransformerBridge {
55
63
  if (typeof content !== 'string') {
56
64
  throw new SSMError('BRIDGE_RESPONSE_INVALID', 'Unexpected OpenAI response shape.');
57
65
  }
66
+ this._lastCall = {
67
+ usage: readOpenAIUsage((json as any).usage, (json as any).model ?? opts.model ?? this._model),
68
+ };
58
69
  return content;
59
70
  }
60
71
 
@@ -74,7 +85,17 @@ export class OpenAIBridge implements TransformerBridge {
74
85
  throw new SSMError('BRIDGE_RESPONSE_INVALID', 'OpenAI streaming response has no body.');
75
86
  }
76
87
 
77
- yield* parseOpenAIStream(res.body);
88
+ const model = opts.model ?? this._model;
89
+ let usage: LlmUsage | undefined;
90
+
91
+ yield* parseOpenAIStream(res.body, (event) => {
92
+ // Only emitted because `_buildBody` sets `stream_options.include_usage`;
93
+ // 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
+ if (reported) usage = reported;
96
+ });
97
+
98
+ this._lastCall = usage ? { usage } : {};
78
99
  }
79
100
 
80
101
  private _buildBody(prompt: string, opts: BridgeGenerateOptions, stream: boolean): string {
@@ -90,6 +111,10 @@ export class OpenAIBridge implements TransformerBridge {
90
111
  temperature: opts.temperature ?? 0.7,
91
112
  top_p : opts.topP ?? 0.9,
92
113
  stream,
114
+ // Without this the streaming response carries no usage block, so
115
+ // cost-per-request on streamed calls would silently fall back to an
116
+ // estimate. Non-streaming requests reject the field, hence the guard.
117
+ ...(stream ? { stream_options: { include_usage: true } } : {}),
93
118
  });
94
119
  }
95
120
 
@@ -107,7 +132,10 @@ export class OpenAIBridge implements TransformerBridge {
107
132
 
108
133
  // ── SSE parser ────────────────────────────────────────────────────────────────
109
134
 
110
- async function* parseOpenAIStream(body: ReadableStream<Uint8Array>): AsyncIterable<string> {
135
+ async function* parseOpenAIStream(
136
+ body: ReadableStream<Uint8Array>,
137
+ onEvent?: (event: Record<string, unknown>) => void,
138
+ ): AsyncIterable<string> {
111
139
  const reader = body.getReader();
112
140
  const decoder = new TextDecoder();
113
141
  let buffer = '';
@@ -130,6 +158,7 @@ async function* parseOpenAIStream(body: ReadableStream<Uint8Array>): AsyncIterab
130
158
 
131
159
  try {
132
160
  const chunk = JSON.parse(data) as Record<string, unknown>;
161
+ onEvent?.(chunk);
133
162
  const delta = (chunk as any).choices?.[0]?.delta?.content;
134
163
  if (typeof delta === 'string' && delta.length > 0) yield delta;
135
164
  } catch {
@@ -141,3 +170,19 @@ async function* parseOpenAIStream(body: ReadableStream<Uint8Array>): AsyncIterab
141
170
  reader.releaseLock();
142
171
  }
143
172
  }
173
+
174
+ /** Maps an OpenAI `usage` object onto the canonical {@link LlmUsage} shape. */
175
+ function readOpenAIUsage(usage: unknown, model: string): LlmUsage | undefined {
176
+ 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;
180
+ return {
181
+ model,
182
+ // OpenAI reports `prompt_tokens` INCLUSIVE of cached tokens; the canonical
183
+ // 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,
186
+ cachedInputTokens: cached,
187
+ };
188
+ }
@@ -12,7 +12,7 @@
12
12
  * Streaming is delegated straight through and never cached.
13
13
  */
14
14
 
15
- import type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
15
+ import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
16
16
  import { SemanticCache, type SemanticCacheOptions } from '../cache/SemanticCache.js';
17
17
 
18
18
  export interface SemanticCachingBridgeOptions extends Omit<SemanticCacheOptions, never> {
@@ -24,6 +24,8 @@ export class SemanticCachingBridge implements TransformerBridge {
24
24
  private readonly _inner : TransformerBridge;
25
25
  private readonly _cache : SemanticCache;
26
26
 
27
+ private _lastCall: BridgeCallInfo | undefined;
28
+
27
29
  constructor(inner: TransformerBridge, opts: SemanticCachingBridgeOptions) {
28
30
  this._inner = inner;
29
31
  this._cache = opts.cache ?? new SemanticCache(opts);
@@ -38,15 +40,40 @@ export class SemanticCachingBridge implements TransformerBridge {
38
40
  return this._cache;
39
41
  }
40
42
 
43
+ /**
44
+ * Reports which tier answered (`l1` local vector scan, `l2` shared backend, or
45
+ * a miss), so the saving from paraphrase reuse is attributable per tier rather
46
+ * than lumped into one "cache" number.
47
+ */
48
+ get lastCall(): BridgeCallInfo | undefined {
49
+ return this._lastCall;
50
+ }
51
+
41
52
  async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
42
53
  // Match on system + prompt meaning so different system contexts don't
43
54
  // cross-hit; partition further by model via the stored meta.
44
55
  const queryText = opts.systemPrompt ? `${opts.systemPrompt}\n${prompt}` : prompt;
45
- const { response } = await this._cache.getOrGenerate(
56
+ const { response, cached, tier } = await this._cache.getOrGenerate(
46
57
  queryText,
47
58
  () => this._inner.generate(prompt, opts),
48
59
  opts.model ? { model: opts.model } : undefined,
49
60
  );
61
+
62
+ if (cached) {
63
+ this._lastCall = {
64
+ cacheHit : true,
65
+ ...(tier ? { cacheTier: tier } : {}),
66
+ usage : {
67
+ model : opts.model ?? 'unknown',
68
+ inputTokens : 0,
69
+ outputTokens : 0,
70
+ localCacheHit: true,
71
+ },
72
+ };
73
+ } else {
74
+ const inner = this._inner.lastCall;
75
+ this._lastCall = { cacheHit: false, ...(inner?.usage ? { usage: inner.usage } : {}) };
76
+ }
50
77
  return response;
51
78
  }
52
79
 
@@ -7,6 +7,10 @@
7
7
  * shape works, no base class required.
8
8
  */
9
9
 
10
+ import type { BridgeCallInfo } from '../telemetry/types.js';
11
+
12
+ export type { BridgeCallInfo };
13
+
10
14
  export interface BridgeGenerateOptions {
11
15
  /** Max tokens to generate. Default per-adapter (typically 512). */
12
16
  maxTokens? : number;
@@ -27,6 +31,18 @@ export interface TransformerBridge {
27
31
  */
28
32
  generate(prompt: string, opts?: BridgeGenerateOptions): Promise<string>;
29
33
 
34
+ /**
35
+ * Usage and cache outcome of the most recent `generate()`/`stream()`.
36
+ *
37
+ * Optional so third-party bridges stay valid, but every bridge in this package
38
+ * populates it: it is what lets `InstrumentedBridge` report *provider-measured*
39
+ * tokens and cost instead of a character-count estimate, and what lets the
40
+ * caching decorators declare a hit so the cache-hit rate is a fact rather than
41
+ * an inference. Not concurrency-safe by design — read it immediately after the
42
+ * call, or use `InstrumentedBridge`, which does.
43
+ */
44
+ readonly lastCall?: BridgeCallInfo;
45
+
30
46
  /**
31
47
  * Streaming variant — yields tokens incrementally.
32
48
  * Check `supportsStreaming` before calling.
@@ -0,0 +1,297 @@
1
+ /**
2
+ * VertexAIBridge — TransformerBridge for Google Cloud Vertex AI.
3
+ *
4
+ * GCP is where a large share of enterprise AI actually runs, usually because the
5
+ * customer's data already lives in BigQuery and Cloud Storage and moving it is a
6
+ * procurement problem, not a technical one. This bridge lets Evermind serve those
7
+ * deployments without changing anything above the port.
8
+ *
9
+ * Two deliberate choices:
10
+ *
11
+ * • **Auth is injected, never owned.** `getAccessToken` is supplied by the
12
+ * caller, which is what lets the same code run under Application Default
13
+ * Credentials locally, a Workload Identity service account on GKE, and an
14
+ * impersonated token in CI — and keeps this package free of the Google auth
15
+ * library (and of any credential lifecycle it has no business managing).
16
+ * • **Raw HTTP, like every other bridge here.** The package is zero-dependency
17
+ * and runs in a browser, a Worker and Node; adding a Node-only vendor SDK to
18
+ * one bridge would cost that portability for the whole package.
19
+ *
20
+ * Usage is reported from `usageMetadata`, so cost accounting on Vertex is measured
21
+ * rather than estimated — with the caveat that Vertex is partner-billed at rates
22
+ * that differ from the first-party API, so supply a matching `PriceBook`.
23
+ */
24
+
25
+ import { SSMError } from '../errors/SSMError.js';
26
+ import type { LlmUsage } from '../telemetry/types.js';
27
+ import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
28
+
29
+ export interface VertexAIBridgeOptions {
30
+ /** GCP project id. */
31
+ projectId: string;
32
+ /** Region, or `'global'`. Default `'us-central1'`. */
33
+ location?: string;
34
+ /**
35
+ * Publisher model id, e.g. `'gemini-2.5-pro'`, or a Claude model served through
36
+ * Vertex. Default `'gemini-2.5-flash'`.
37
+ */
38
+ model?: string;
39
+ /** Publisher owning the model. Default `'google'`; use `'anthropic'` for Claude. */
40
+ publisher?: string;
41
+ /**
42
+ * Returns a current OAuth access token. Called per request, so the caller's
43
+ * token source owns caching and refresh.
44
+ */
45
+ getAccessToken: () => Promise<string> | string;
46
+ systemPrompt?: string;
47
+ maxTokens?: number;
48
+ /** Override the API host — for a private endpoint or a test double. */
49
+ baseUrl?: string;
50
+ fetchImpl?: typeof fetch;
51
+ }
52
+
53
+ export class VertexAIBridge implements TransformerBridge {
54
+ readonly supportsStreaming = true as const;
55
+
56
+ private readonly _projectId: string;
57
+ private readonly _location: string;
58
+ private readonly _model: string;
59
+ private readonly _publisher: string;
60
+ private readonly _getToken: () => Promise<string> | string;
61
+ private readonly _systemPrompt: string;
62
+ private readonly _maxTokens: number;
63
+ private readonly _baseUrl: string;
64
+ private readonly _fetch: typeof fetch;
65
+
66
+ private _lastCall: BridgeCallInfo | undefined;
67
+
68
+ constructor(opts: VertexAIBridgeOptions) {
69
+ this._projectId = opts.projectId;
70
+ this._location = opts.location ?? 'us-central1';
71
+ this._model = opts.model ?? 'gemini-2.5-flash';
72
+ this._publisher = opts.publisher ?? 'google';
73
+ this._getToken = opts.getAccessToken;
74
+ this._systemPrompt = opts.systemPrompt ?? '';
75
+ this._maxTokens = opts.maxTokens ?? 1024;
76
+ this._baseUrl = opts.baseUrl ?? defaultHost(this._location);
77
+ this._fetch = opts.fetchImpl ?? ((...args) => fetch(...args));
78
+ }
79
+
80
+ get lastCall(): BridgeCallInfo | undefined {
81
+ return this._lastCall;
82
+ }
83
+
84
+ async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
85
+ const res = await this._send(prompt, opts, false);
86
+ const json = await res.json() as Record<string, unknown>;
87
+
88
+ const text = readCandidateText(json);
89
+ if (text === undefined) {
90
+ throw new SSMError('BRIDGE_RESPONSE_INVALID', 'Unexpected Vertex AI response shape.');
91
+ }
92
+
93
+ this._lastCall = { usage: readVertexUsage(json['usageMetadata'], opts.model ?? this._model) };
94
+ return text;
95
+ }
96
+
97
+ async *stream(prompt: string, opts: BridgeGenerateOptions = {}): AsyncIterable<string> {
98
+ const res = await this._send(prompt, opts, true);
99
+ if (!res.body) {
100
+ throw new SSMError('BRIDGE_RESPONSE_INVALID', 'Vertex AI streaming response has no body.');
101
+ }
102
+
103
+ let usage: LlmUsage | undefined;
104
+ for await (const event of parseSseJson(res.body)) {
105
+ const text = readCandidateText(event);
106
+ if (text) yield text;
107
+ // Vertex repeats cumulative usage on each chunk; the last one wins.
108
+ const reported = readVertexUsage(event['usageMetadata'], opts.model ?? this._model);
109
+ if (reported) usage = reported;
110
+ }
111
+ this._lastCall = usage ? { usage } : {};
112
+ }
113
+
114
+ private async _send(prompt: string, opts: BridgeGenerateOptions, stream: boolean): Promise<Response> {
115
+ const model = opts.model ?? this._model;
116
+ const method = stream ? 'streamGenerateContent?alt=sse' : 'generateContent';
117
+ const url =
118
+ `${this._baseUrl}/v1/projects/${this._projectId}/locations/${this._location}` +
119
+ `/publishers/${this._publisher}/models/${model}:${method}`;
120
+
121
+ const system = opts.systemPrompt ?? this._systemPrompt;
122
+ const body: Record<string, unknown> = {
123
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
124
+ generationConfig: {
125
+ maxOutputTokens: opts.maxTokens ?? this._maxTokens,
126
+ temperature: opts.temperature ?? 0.7,
127
+ topP: opts.topP ?? 0.9,
128
+ },
129
+ };
130
+ if (system) body['systemInstruction'] = { parts: [{ text: system }] };
131
+
132
+ const token = await this._getToken();
133
+ const res = await this._fetch(url, {
134
+ method: 'POST',
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ 'Authorization': `Bearer ${token}`,
138
+ },
139
+ body: JSON.stringify(body),
140
+ });
141
+
142
+ if (!res.ok) {
143
+ const detail = await res.text().catch(() => '');
144
+ throw new SSMError(
145
+ 'BRIDGE_REQUEST_FAILED',
146
+ `Vertex AI returned ${res.status}: ${detail}`,
147
+ );
148
+ }
149
+ return res;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Vertex AI text embeddings — the `:predict` surface on a publisher model.
155
+ *
156
+ * Exposed as a {@link BatchEmbedder}-shaped function so it drops straight into the
157
+ * ingestion pipeline: on GCP the embedder, the vector index and the generator can
158
+ * all be Vertex, and nothing above the ports changes.
159
+ */
160
+ export interface VertexAIEmbedderOptions {
161
+ projectId: string;
162
+ location?: string;
163
+ /** Embedding model. Default `'text-embedding-005'`. */
164
+ model?: string;
165
+ getAccessToken: () => Promise<string> | string;
166
+ /**
167
+ * Task type hint. `RETRIEVAL_DOCUMENT` when indexing, `RETRIEVAL_QUERY` when
168
+ * searching — using the same hint for both measurably degrades recall, because
169
+ * the model embeds questions and passages into deliberately different regions.
170
+ */
171
+ taskType?: 'RETRIEVAL_DOCUMENT' | 'RETRIEVAL_QUERY' | 'SEMANTIC_SIMILARITY' | 'CLASSIFICATION';
172
+ baseUrl?: string;
173
+ fetchImpl?: typeof fetch;
174
+ }
175
+
176
+ export class VertexAIEmbedder {
177
+ private readonly _projectId: string;
178
+ private readonly _location: string;
179
+ private readonly _model: string;
180
+ private readonly _getToken: () => Promise<string> | string;
181
+ private readonly _taskType: string | undefined;
182
+ private readonly _baseUrl: string;
183
+ private readonly _fetch: typeof fetch;
184
+
185
+ constructor(opts: VertexAIEmbedderOptions) {
186
+ this._projectId = opts.projectId;
187
+ this._location = opts.location ?? 'us-central1';
188
+ this._model = opts.model ?? 'text-embedding-005';
189
+ this._getToken = opts.getAccessToken;
190
+ this._taskType = opts.taskType;
191
+ this._baseUrl = opts.baseUrl ?? defaultHost(this._location);
192
+ this._fetch = opts.fetchImpl ?? ((...args) => fetch(...args));
193
+ }
194
+
195
+ /** Embeds a batch. Shape matches `BatchEmbedder`, for the ingestion pipeline. */
196
+ embedBatch = async (texts: string[]): Promise<Float32Array[]> => {
197
+ if (texts.length === 0) return [];
198
+
199
+ const url =
200
+ `${this._baseUrl}/v1/projects/${this._projectId}/locations/${this._location}` +
201
+ `/publishers/google/models/${this._model}:predict`;
202
+
203
+ const token = await this._getToken();
204
+ const res = await this._fetch(url, {
205
+ method: 'POST',
206
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
207
+ body: JSON.stringify({
208
+ instances: texts.map((content) => ({
209
+ content,
210
+ ...(this._taskType ? { task_type: this._taskType } : {}),
211
+ })),
212
+ }),
213
+ });
214
+
215
+ if (!res.ok) {
216
+ const detail = await res.text().catch(() => '');
217
+ throw new SSMError('BRIDGE_REQUEST_FAILED', `Vertex AI embeddings returned ${res.status}: ${detail}`);
218
+ }
219
+
220
+ const json = await res.json() as { predictions?: Array<{ embeddings?: { values?: number[] } }> };
221
+ const predictions = json.predictions ?? [];
222
+ if (predictions.length !== texts.length) {
223
+ // A short batch would silently misalign vectors with their chunks — the
224
+ // worst kind of RAG bug, because retrieval still "works" and is wrong.
225
+ throw new SSMError(
226
+ 'BRIDGE_RESPONSE_INVALID',
227
+ `Vertex AI returned ${predictions.length} embeddings for ${texts.length} inputs.`,
228
+ );
229
+ }
230
+ return predictions.map((p) => Float32Array.from(p.embeddings?.values ?? []));
231
+ };
232
+
233
+ /** Single-text convenience, for the retriever's query embedding. */
234
+ embed = async (text: string): Promise<Float32Array> => {
235
+ const [vector] = await this.embedBatch([text]);
236
+ return vector ?? new Float32Array();
237
+ };
238
+ }
239
+
240
+ function defaultHost(location: string): string {
241
+ return location === 'global'
242
+ ? 'https://aiplatform.googleapis.com'
243
+ : `https://${location}-aiplatform.googleapis.com`;
244
+ }
245
+
246
+ function readCandidateText(json: Record<string, unknown>): string | undefined {
247
+ const candidates = json['candidates'];
248
+ if (!Array.isArray(candidates) || candidates.length === 0) return undefined;
249
+ const parts = (candidates[0] as { content?: { parts?: Array<{ text?: string }> } }).content?.parts;
250
+ if (!Array.isArray(parts)) return undefined;
251
+ // Multi-part responses are concatenated; taking only parts[0] silently truncates.
252
+ return parts.map((p) => p.text ?? '').join('');
253
+ }
254
+
255
+ function readVertexUsage(usage: unknown, model: string): LlmUsage | undefined {
256
+ if (!usage || typeof usage !== 'object') return undefined;
257
+ const u = usage as Record<string, unknown>;
258
+ const cached = Number(u['cachedContentTokenCount']) || 0;
259
+ const prompt = Number(u['promptTokenCount']) || 0;
260
+ return {
261
+ model,
262
+ inputTokens: Math.max(0, prompt - cached),
263
+ outputTokens: Number(u['candidatesTokenCount']) || 0,
264
+ cachedInputTokens: cached,
265
+ };
266
+ }
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
+ }
@@ -1,13 +1,15 @@
1
- export type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
1
+ export type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
2
2
  export { OpenAIBridge } from './OpenAIBridge.js';
3
3
  export { AnthropicBridge } from './AnthropicBridge.js';
4
4
  export { FetchBridge } from './FetchBridge.js';
5
5
  export { CachingBridge } from './CachingBridge.js';
6
6
  export { SemanticCachingBridge } from './SemanticCachingBridge.js';
7
+ export { VertexAIBridge, VertexAIEmbedder } from './VertexAIBridge.js';
7
8
  export { ResponseCache, buildCacheKey } from './ResponseCache.js';
8
9
  export type { OpenAIBridgeOptions } from './OpenAIBridge.js';
9
10
  export type { AnthropicBridgeOptions } from './AnthropicBridge.js';
10
11
  export type { FetchBridgeOptions } from './FetchBridge.js';
11
12
  export type { CachingBridgeOptions } from './CachingBridge.js';
12
13
  export type { SemanticCachingBridgeOptions } from './SemanticCachingBridge.js';
14
+ export type { VertexAIBridgeOptions, VertexAIEmbedderOptions } from './VertexAIBridge.js';
13
15
  export type { ResponseCacheOptions } from './ResponseCache.js';