@seanhogg/builderforce-memory 2026.6.18

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +582 -0
  3. package/dist/agent/SSMAgent.d.ts +146 -0
  4. package/dist/agent/SSMAgent.d.ts.map +1 -0
  5. package/dist/agent/SSMAgent.js +231 -0
  6. package/dist/agent/SSMAgent.js.map +1 -0
  7. package/dist/agent/index.d.ts +3 -0
  8. package/dist/agent/index.d.ts.map +1 -0
  9. package/dist/agent/index.js +2 -0
  10. package/dist/agent/index.js.map +1 -0
  11. package/dist/bridges/AnthropicBridge.d.ts +47 -0
  12. package/dist/bridges/AnthropicBridge.d.ts.map +1 -0
  13. package/dist/bridges/AnthropicBridge.js +120 -0
  14. package/dist/bridges/AnthropicBridge.js.map +1 -0
  15. package/dist/bridges/CachingBridge.d.ts +44 -0
  16. package/dist/bridges/CachingBridge.d.ts.map +1 -0
  17. package/dist/bridges/CachingBridge.js +62 -0
  18. package/dist/bridges/CachingBridge.js.map +1 -0
  19. package/dist/bridges/FetchBridge.d.ts +30 -0
  20. package/dist/bridges/FetchBridge.d.ts.map +1 -0
  21. package/dist/bridges/FetchBridge.js +24 -0
  22. package/dist/bridges/FetchBridge.js.map +1 -0
  23. package/dist/bridges/OpenAIBridge.d.ts +33 -0
  24. package/dist/bridges/OpenAIBridge.d.ts.map +1 -0
  25. package/dist/bridges/OpenAIBridge.js +110 -0
  26. package/dist/bridges/OpenAIBridge.js.map +1 -0
  27. package/dist/bridges/ResponseCache.d.ts +65 -0
  28. package/dist/bridges/ResponseCache.d.ts.map +1 -0
  29. package/dist/bridges/ResponseCache.js +97 -0
  30. package/dist/bridges/ResponseCache.js.map +1 -0
  31. package/dist/bridges/SemanticCachingBridge.d.ts +31 -0
  32. package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -0
  33. package/dist/bridges/SemanticCachingBridge.js +44 -0
  34. package/dist/bridges/SemanticCachingBridge.js.map +1 -0
  35. package/dist/bridges/TransformerBridge.d.ts +35 -0
  36. package/dist/bridges/TransformerBridge.d.ts.map +1 -0
  37. package/dist/bridges/TransformerBridge.js +10 -0
  38. package/dist/bridges/TransformerBridge.js.map +1 -0
  39. package/dist/bridges/index.d.ts +14 -0
  40. package/dist/bridges/index.d.ts.map +1 -0
  41. package/dist/bridges/index.js +7 -0
  42. package/dist/bridges/index.js.map +1 -0
  43. package/dist/cache/FetchSemanticCacheBackend.d.ts +40 -0
  44. package/dist/cache/FetchSemanticCacheBackend.d.ts.map +1 -0
  45. package/dist/cache/FetchSemanticCacheBackend.js +61 -0
  46. package/dist/cache/FetchSemanticCacheBackend.js.map +1 -0
  47. package/dist/cache/SemanticCache.d.ts +105 -0
  48. package/dist/cache/SemanticCache.d.ts.map +1 -0
  49. package/dist/cache/SemanticCache.js +130 -0
  50. package/dist/cache/SemanticCache.js.map +1 -0
  51. package/dist/cache/index.d.ts +5 -0
  52. package/dist/cache/index.d.ts.map +1 -0
  53. package/dist/cache/index.js +3 -0
  54. package/dist/cache/index.js.map +1 -0
  55. package/dist/distillation/DistillationEngine.d.ts +107 -0
  56. package/dist/distillation/DistillationEngine.d.ts.map +1 -0
  57. package/dist/distillation/DistillationEngine.js +152 -0
  58. package/dist/distillation/DistillationEngine.js.map +1 -0
  59. package/dist/distillation/index.d.ts +3 -0
  60. package/dist/distillation/index.d.ts.map +1 -0
  61. package/dist/distillation/index.js +2 -0
  62. package/dist/distillation/index.js.map +1 -0
  63. package/dist/errors/SSMError.d.ts +14 -0
  64. package/dist/errors/SSMError.d.ts.map +1 -0
  65. package/dist/errors/SSMError.js +18 -0
  66. package/dist/errors/SSMError.js.map +1 -0
  67. package/dist/errors/index.d.ts +3 -0
  68. package/dist/errors/index.d.ts.map +1 -0
  69. package/dist/errors/index.js +2 -0
  70. package/dist/errors/index.js.map +1 -0
  71. package/dist/index.d.ts +65 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +59 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/memory/MemoryStore.d.ts +152 -0
  76. package/dist/memory/MemoryStore.d.ts.map +1 -0
  77. package/dist/memory/MemoryStore.js +290 -0
  78. package/dist/memory/MemoryStore.js.map +1 -0
  79. package/dist/memory/index.d.ts +3 -0
  80. package/dist/memory/index.d.ts.map +1 -0
  81. package/dist/memory/index.js +2 -0
  82. package/dist/memory/index.js.map +1 -0
  83. package/dist/router/InferenceRouter.d.ts +92 -0
  84. package/dist/router/InferenceRouter.d.ts.map +1 -0
  85. package/dist/router/InferenceRouter.js +113 -0
  86. package/dist/router/InferenceRouter.js.map +1 -0
  87. package/dist/router/index.d.ts +3 -0
  88. package/dist/router/index.d.ts.map +1 -0
  89. package/dist/router/index.js +2 -0
  90. package/dist/router/index.js.map +1 -0
  91. package/dist/runtime/SSMRuntime.d.ts +167 -0
  92. package/dist/runtime/SSMRuntime.d.ts.map +1 -0
  93. package/dist/runtime/SSMRuntime.js +199 -0
  94. package/dist/runtime/SSMRuntime.js.map +1 -0
  95. package/dist/runtime/index.d.ts +3 -0
  96. package/dist/runtime/index.d.ts.map +1 -0
  97. package/dist/runtime/index.js +2 -0
  98. package/dist/runtime/index.js.map +1 -0
  99. package/dist/session/errors.d.ts +10 -0
  100. package/dist/session/errors.d.ts.map +1 -0
  101. package/dist/session/errors.js +14 -0
  102. package/dist/session/errors.js.map +1 -0
  103. package/dist/session/index.d.ts +11 -0
  104. package/dist/session/index.d.ts.map +1 -0
  105. package/dist/session/index.js +7 -0
  106. package/dist/session/index.js.map +1 -0
  107. package/dist/session/persistence.d.ts +14 -0
  108. package/dist/session/persistence.d.ts.map +1 -0
  109. package/dist/session/persistence.js +100 -0
  110. package/dist/session/persistence.js.map +1 -0
  111. package/dist/session/presets.d.ts +31 -0
  112. package/dist/session/presets.d.ts.map +1 -0
  113. package/dist/session/presets.js +91 -0
  114. package/dist/session/presets.js.map +1 -0
  115. package/dist/session/session.d.ts +186 -0
  116. package/dist/session/session.d.ts.map +1 -0
  117. package/dist/session/session.js +358 -0
  118. package/dist/session/session.js.map +1 -0
  119. package/dist/session/streaming.d.ts +13 -0
  120. package/dist/session/streaming.d.ts.map +1 -0
  121. package/dist/session/streaming.js +74 -0
  122. package/dist/session/streaming.js.map +1 -0
  123. package/dist/session/tokenizer.d.ts +18 -0
  124. package/dist/session/tokenizer.d.ts.map +1 -0
  125. package/dist/session/tokenizer.js +11 -0
  126. package/dist/session/tokenizer.js.map +1 -0
  127. package/dist/similarity/index.d.ts +19 -0
  128. package/dist/similarity/index.d.ts.map +1 -0
  129. package/dist/similarity/index.js +42 -0
  130. package/dist/similarity/index.js.map +1 -0
  131. package/package.json +120 -0
  132. package/src/agent/SSMAgent.ts +327 -0
  133. package/src/agent/index.ts +2 -0
  134. package/src/bridges/AnthropicBridge.ts +166 -0
  135. package/src/bridges/CachingBridge.ts +79 -0
  136. package/src/bridges/FetchBridge.ts +41 -0
  137. package/src/bridges/OpenAIBridge.ts +143 -0
  138. package/src/bridges/ResponseCache.ts +131 -0
  139. package/src/bridges/SemanticCachingBridge.ts +60 -0
  140. package/src/bridges/TransformerBridge.ts +38 -0
  141. package/src/bridges/index.ts +13 -0
  142. package/src/cache/FetchSemanticCacheBackend.ts +79 -0
  143. package/src/cache/SemanticCache.ts +196 -0
  144. package/src/cache/index.ts +9 -0
  145. package/src/distillation/DistillationEngine.ts +248 -0
  146. package/src/distillation/index.ts +2 -0
  147. package/src/errors/SSMError.ts +26 -0
  148. package/src/errors/index.ts +2 -0
  149. package/src/index.ts +128 -0
  150. package/src/memory/MemoryStore.ts +408 -0
  151. package/src/memory/index.ts +2 -0
  152. package/src/router/InferenceRouter.ts +201 -0
  153. package/src/router/index.ts +2 -0
  154. package/src/runtime/SSMRuntime.ts +309 -0
  155. package/src/runtime/index.ts +2 -0
  156. package/src/session/errors.ts +24 -0
  157. package/src/session/index.ts +25 -0
  158. package/src/session/persistence.ts +142 -0
  159. package/src/session/presets.ts +122 -0
  160. package/src/session/session.ts +657 -0
  161. package/src/session/streaming.ts +97 -0
  162. package/src/session/tokenizer.ts +18 -0
  163. package/src/similarity/index.ts +42 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * OpenAIBridge – TransformerBridge implementation for the OpenAI Chat API.
3
+ *
4
+ * Supports both non-streaming and streaming (SSE) completions.
5
+ * Compatible with any OpenAI-compatible endpoint via the `baseUrl` option.
6
+ */
7
+
8
+ import { SSMError } from '../errors/SSMError.js';
9
+ import type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
10
+
11
+ export interface OpenAIBridgeOptions {
12
+ /** OpenAI API key (or compatible service key). */
13
+ apiKey : string;
14
+ /** Model to use. Default: 'gpt-4o-mini'. */
15
+ model? : string;
16
+ /** API base URL. Default: 'https://api.openai.com/v1'. */
17
+ baseUrl? : string;
18
+ /** Default system prompt sent with every request. */
19
+ systemPrompt? : string;
20
+ /** Default max tokens. Default: 512. */
21
+ maxTokens? : number;
22
+ }
23
+
24
+ export class OpenAIBridge implements TransformerBridge {
25
+ readonly supportsStreaming = true as const;
26
+
27
+ private readonly _apiKey : string;
28
+ private readonly _model : string;
29
+ private readonly _baseUrl : string;
30
+ private readonly _systemPrompt: string;
31
+ private readonly _maxTokens : number;
32
+
33
+ constructor(opts: OpenAIBridgeOptions) {
34
+ this._apiKey = opts.apiKey;
35
+ this._model = opts.model ?? 'gpt-4o-mini';
36
+ this._baseUrl = (opts.baseUrl ?? 'https://api.openai.com/v1').replace(/\/$/, '');
37
+ this._systemPrompt = opts.systemPrompt ?? '';
38
+ this._maxTokens = opts.maxTokens ?? 512;
39
+ }
40
+
41
+ async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
42
+ const body = this._buildBody(prompt, opts, false);
43
+ const res = await this._fetch(body);
44
+
45
+ if (!res.ok) {
46
+ const text = await res.text().catch(() => '');
47
+ throw new SSMError(
48
+ 'BRIDGE_REQUEST_FAILED',
49
+ `OpenAI API returned ${res.status}: ${text}`,
50
+ );
51
+ }
52
+
53
+ const json = await res.json() as Record<string, unknown>;
54
+ const content = (json as any).choices?.[0]?.message?.content;
55
+ if (typeof content !== 'string') {
56
+ throw new SSMError('BRIDGE_RESPONSE_INVALID', 'Unexpected OpenAI response shape.');
57
+ }
58
+ return content;
59
+ }
60
+
61
+ async *stream(prompt: string, opts: BridgeGenerateOptions = {}): AsyncIterable<string> {
62
+ const body = this._buildBody(prompt, opts, true);
63
+ const res = await this._fetch(body);
64
+
65
+ if (!res.ok) {
66
+ const text = await res.text().catch(() => '');
67
+ throw new SSMError(
68
+ 'BRIDGE_REQUEST_FAILED',
69
+ `OpenAI streaming API returned ${res.status}: ${text}`,
70
+ );
71
+ }
72
+
73
+ if (!res.body) {
74
+ throw new SSMError('BRIDGE_RESPONSE_INVALID', 'OpenAI streaming response has no body.');
75
+ }
76
+
77
+ yield* parseOpenAIStream(res.body);
78
+ }
79
+
80
+ private _buildBody(prompt: string, opts: BridgeGenerateOptions, stream: boolean): string {
81
+ const sys = opts.systemPrompt ?? this._systemPrompt;
82
+ const messages: { role: string; content: string }[] = [];
83
+ if (sys) messages.push({ role: 'system', content: sys });
84
+ messages.push({ role: 'user', content: prompt });
85
+
86
+ return JSON.stringify({
87
+ model : opts.model ?? this._model,
88
+ messages,
89
+ max_tokens : opts.maxTokens ?? this._maxTokens,
90
+ temperature: opts.temperature ?? 0.7,
91
+ top_p : opts.topP ?? 0.9,
92
+ stream,
93
+ });
94
+ }
95
+
96
+ private _fetch(body: string): Promise<Response> {
97
+ return fetch(`${this._baseUrl}/chat/completions`, {
98
+ method : 'POST',
99
+ headers: {
100
+ 'Content-Type' : 'application/json',
101
+ 'Authorization': `Bearer ${this._apiKey}`,
102
+ },
103
+ body,
104
+ });
105
+ }
106
+ }
107
+
108
+ // ── SSE parser ────────────────────────────────────────────────────────────────
109
+
110
+ async function* parseOpenAIStream(body: ReadableStream<Uint8Array>): AsyncIterable<string> {
111
+ const reader = body.getReader();
112
+ const decoder = new TextDecoder();
113
+ let buffer = '';
114
+
115
+ try {
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done) break;
119
+
120
+ buffer += decoder.decode(value, { stream: true });
121
+ const lines = buffer.split('\n');
122
+ buffer = lines.pop() as string; // keep the last (possibly partial) line; split() always yields ≥1 element
123
+
124
+ for (const line of lines) {
125
+ const trimmed = line.trim();
126
+ if (!trimmed.startsWith('data: ')) continue;
127
+
128
+ const data = trimmed.slice(6);
129
+ if (data === '[DONE]') return;
130
+
131
+ try {
132
+ const chunk = JSON.parse(data) as Record<string, unknown>;
133
+ const delta = (chunk as any).choices?.[0]?.delta?.content;
134
+ if (typeof delta === 'string' && delta.length > 0) yield delta;
135
+ } catch {
136
+ // Malformed JSON in stream — skip silently
137
+ }
138
+ }
139
+ }
140
+ } finally {
141
+ reader.releaseLock();
142
+ }
143
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ResponseCache – a small, dependency-free read-through cache for transformer
3
+ * bridge completions.
4
+ *
5
+ * This is the canonical cache for this library: a single bounded LRU with an
6
+ * optional TTL, not an ad-hoc Map inlined at a call site. It exists because an
7
+ * external LLM call is the most expensive thing the runtime does — identical
8
+ * (model, system, prompt, sampling) requests should never be billed twice.
9
+ *
10
+ * Scope is in-process by design: this package targets the browser and Node, so
11
+ * there is no shared KV / cross-isolate tier to propagate to (unlike the
12
+ * BuilderForce.ai gateway, whose read-through cache is L1 Map + L2 KV). A
13
+ * consumer that needs cross-process sharing can wrap a bridge with its own
14
+ * distributed cache using the same `CachingBridge` shape.
15
+ */
16
+
17
+ export interface ResponseCacheOptions {
18
+ /**
19
+ * Maximum number of entries retained. Oldest-accessed entries are evicted
20
+ * first once the bound is reached. Default: 500.
21
+ */
22
+ maxEntries? : number;
23
+ /**
24
+ * Optional time-to-live in milliseconds. Entries older than this are treated
25
+ * as misses and dropped on access. Omit for no expiry (cache until evicted).
26
+ */
27
+ ttlMs? : number;
28
+ }
29
+
30
+ interface CacheRecord {
31
+ value : string;
32
+ timestamp : number;
33
+ }
34
+
35
+ const DEFAULT_MAX_ENTRIES = 500;
36
+
37
+ export class ResponseCache {
38
+ private readonly _maxEntries : number;
39
+ private readonly _ttlMs : number | undefined;
40
+ // Map preserves insertion order; re-insertion on hit gives us LRU ordering.
41
+ private readonly _store = new Map<string, CacheRecord>();
42
+
43
+ private _hits = 0;
44
+ private _misses = 0;
45
+
46
+ constructor(opts: ResponseCacheOptions = {}) {
47
+ this._maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
48
+ this._ttlMs = opts.ttlMs;
49
+ }
50
+
51
+ /**
52
+ * Returns the cached value for `key`, or `undefined` on a miss (including an
53
+ * expired entry, which is also evicted). A hit refreshes recency.
54
+ */
55
+ get(key: string): string | undefined {
56
+ const record = this._store.get(key);
57
+ if (!record) {
58
+ this._misses++;
59
+ return undefined;
60
+ }
61
+ if (this._isExpired(record)) {
62
+ this._store.delete(key);
63
+ this._misses++;
64
+ return undefined;
65
+ }
66
+ // Refresh recency: delete + re-insert moves the key to the newest slot.
67
+ this._store.delete(key);
68
+ this._store.set(key, record);
69
+ this._hits++;
70
+ return record.value;
71
+ }
72
+
73
+ /** Stores `value` under `key`, evicting the least-recently-used entry if full. */
74
+ set(key: string, value: string, now: number): void {
75
+ if (this._store.has(key)) this._store.delete(key);
76
+ this._store.set(key, { value, timestamp: now });
77
+
78
+ while (this._store.size > this._maxEntries) {
79
+ // size > maxEntries (≥ 0) guarantees the map is non-empty, so the
80
+ // oldest key always exists — the non-null assertion is safe.
81
+ const oldest = this._store.keys().next().value as string;
82
+ this._store.delete(oldest);
83
+ }
84
+ }
85
+
86
+ /** Drops all cached entries. */
87
+ clear(): void {
88
+ this._store.clear();
89
+ }
90
+
91
+ /** Current entry count (including not-yet-evicted expired entries). */
92
+ get size(): number {
93
+ return this._store.size;
94
+ }
95
+
96
+ /** Cumulative hit / miss counters, for observability and cache-tuning. */
97
+ get stats(): { hits: number; misses: number } {
98
+ return { hits: this._hits, misses: this._misses };
99
+ }
100
+
101
+ private _isExpired(record: CacheRecord): boolean {
102
+ if (this._ttlMs == null) return false;
103
+ // `now` is read at access time so a single import of Date is enough; the
104
+ // caller-supplied `now` on set() keeps insertion timestamps consistent.
105
+ return Date.now() > record.timestamp + this._ttlMs;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Builds a stable, collision-resistant cache key from the request shape. Any
111
+ * field that changes the model's output must be part of the key.
112
+ */
113
+ export function buildCacheKey(parts: {
114
+ prompt : string;
115
+ model? : string;
116
+ systemPrompt? : string;
117
+ maxTokens? : number;
118
+ temperature? : number;
119
+ topP? : number;
120
+ }): string {
121
+ // JSON of a fixed-order tuple — deterministic and unambiguous (a delimiter
122
+ // string could collide across fields; positional JSON cannot).
123
+ return JSON.stringify([
124
+ parts.model ?? '',
125
+ parts.systemPrompt ?? '',
126
+ parts.maxTokens ?? '',
127
+ parts.temperature ?? '',
128
+ parts.topP ?? '',
129
+ parts.prompt,
130
+ ]);
131
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * SemanticCachingBridge – a read-through *semantic* caching decorator for any
3
+ * TransformerBridge. The semantic sibling of CachingBridge: where CachingBridge
4
+ * only reuses byte-identical prompts, this reuses a prior answer when the new
5
+ * prompt is within `threshold` cosine similarity of one already answered.
6
+ *
7
+ * const bridge = new SemanticCachingBridge(new AnthropicBridge({ apiKey }), {
8
+ * embed: (t) => runtime.embed(t), // on-device SSM, free
9
+ * l2: new FetchSemanticCacheBackend({ baseUrl, apiKey }), // shared via gateway
10
+ * });
11
+ *
12
+ * Streaming is delegated straight through and never cached.
13
+ */
14
+
15
+ import type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
16
+ import { SemanticCache, type SemanticCacheOptions } from '../cache/SemanticCache.js';
17
+
18
+ export interface SemanticCachingBridgeOptions extends Omit<SemanticCacheOptions, never> {
19
+ /** Provide a shared SemanticCache instance instead of constructing one. */
20
+ cache? : SemanticCache;
21
+ }
22
+
23
+ export class SemanticCachingBridge implements TransformerBridge {
24
+ private readonly _inner : TransformerBridge;
25
+ private readonly _cache : SemanticCache;
26
+
27
+ constructor(inner: TransformerBridge, opts: SemanticCachingBridgeOptions) {
28
+ this._inner = inner;
29
+ this._cache = opts.cache ?? new SemanticCache(opts);
30
+ }
31
+
32
+ get supportsStreaming(): boolean {
33
+ return this._inner.supportsStreaming;
34
+ }
35
+
36
+ /** The underlying SemanticCache — exposed for stats inspection. */
37
+ get cache(): SemanticCache {
38
+ return this._cache;
39
+ }
40
+
41
+ async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
42
+ // Match on system + prompt meaning so different system contexts don't
43
+ // cross-hit; partition further by model via the stored meta.
44
+ const queryText = opts.systemPrompt ? `${opts.systemPrompt}\n${prompt}` : prompt;
45
+ const { response } = await this._cache.getOrGenerate(
46
+ queryText,
47
+ () => this._inner.generate(prompt, opts),
48
+ opts.model ? { model: opts.model } : undefined,
49
+ );
50
+ return response;
51
+ }
52
+
53
+ /** Streaming is delegated unchanged and never cached. */
54
+ stream(prompt: string, opts?: BridgeGenerateOptions): AsyncIterable<string> {
55
+ if (!this._inner.stream) {
56
+ throw new Error('Wrapped bridge does not support streaming.');
57
+ }
58
+ return this._inner.stream(prompt, opts);
59
+ }
60
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * TransformerBridge – pluggable interface for any transformer LLM backend.
3
+ *
4
+ * Implementations (OpenAIBridge, AnthropicBridge, FetchBridge) satisfy this
5
+ * interface and are passed to SSMRuntime to enable hybrid inference and
6
+ * distillation. The interface is structural — any object with the right
7
+ * shape works, no base class required.
8
+ */
9
+
10
+ export interface BridgeGenerateOptions {
11
+ /** Max tokens to generate. Default per-adapter (typically 512). */
12
+ maxTokens? : number;
13
+ /** Sampling temperature. Default per-adapter (typically 0.7). */
14
+ temperature? : number;
15
+ /** Nucleus sampling p. Default per-adapter (typically 0.9). */
16
+ topP? : number;
17
+ /** System prompt for this request, overriding the adapter's default. */
18
+ systemPrompt? : string;
19
+ /** Model string, overriding the adapter's default. */
20
+ model? : string;
21
+ }
22
+
23
+ export interface TransformerBridge {
24
+ /**
25
+ * Generates a completion for the given prompt.
26
+ * Must resolve to the assistant's reply text only (not including the prompt).
27
+ */
28
+ generate(prompt: string, opts?: BridgeGenerateOptions): Promise<string>;
29
+
30
+ /**
31
+ * Streaming variant — yields tokens incrementally.
32
+ * Check `supportsStreaming` before calling.
33
+ */
34
+ stream?(prompt: string, opts?: BridgeGenerateOptions): AsyncIterable<string>;
35
+
36
+ /** True when this bridge supports the `stream()` method. */
37
+ readonly supportsStreaming: boolean;
38
+ }
@@ -0,0 +1,13 @@
1
+ export type { TransformerBridge, BridgeGenerateOptions } from './TransformerBridge.js';
2
+ export { OpenAIBridge } from './OpenAIBridge.js';
3
+ export { AnthropicBridge } from './AnthropicBridge.js';
4
+ export { FetchBridge } from './FetchBridge.js';
5
+ export { CachingBridge } from './CachingBridge.js';
6
+ export { SemanticCachingBridge } from './SemanticCachingBridge.js';
7
+ export { ResponseCache, buildCacheKey } from './ResponseCache.js';
8
+ export type { OpenAIBridgeOptions } from './OpenAIBridge.js';
9
+ export type { AnthropicBridgeOptions } from './AnthropicBridge.js';
10
+ export type { FetchBridgeOptions } from './FetchBridge.js';
11
+ export type { CachingBridgeOptions } from './CachingBridge.js';
12
+ export type { SemanticCachingBridgeOptions } from './SemanticCachingBridge.js';
13
+ export type { ResponseCacheOptions } from './ResponseCache.js';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * FetchSemanticCacheBackend – the shared (L2) tier of the SemanticCache, backed
3
+ * by the BuilderForce.ai gateway's vector store over HTTP.
4
+ *
5
+ * One client used by both consumers (browser + agent) so a semantic hit on one
6
+ * surface is reusable by the other. Pure `fetch` — no environment-specific deps;
7
+ * inject `fetchImpl` in tests.
8
+ *
9
+ * Wire protocol (gateway `/v1/semantic-cache`):
10
+ * POST /lookup { embedding: number[], threshold, namespace? } → { hit?: { response, score } }
11
+ * POST /store { embedding: number[], response, namespace?, meta? } → 2xx
12
+ */
13
+
14
+ import type { SemanticCacheBackend } from './SemanticCache.js';
15
+
16
+ export interface FetchSemanticCacheBackendOptions {
17
+ /** Gateway base URL, e.g. 'https://api.builderforce.ai'. Trailing slash trimmed. */
18
+ baseUrl : string;
19
+ /** Tenant API key (sent as a bearer token). */
20
+ apiKey : string;
21
+ /**
22
+ * Optional cache partition. Scope hits to a tenant/model/agent so unrelated
23
+ * traffic can't cross-hit. Defaults to the gateway's per-tenant default.
24
+ */
25
+ namespace? : string;
26
+ /** Injectable fetch (defaults to global fetch). */
27
+ fetchImpl? : typeof fetch;
28
+ }
29
+
30
+ export class FetchSemanticCacheBackend implements SemanticCacheBackend {
31
+ private readonly _base : string;
32
+ private readonly _apiKey : string;
33
+ private readonly _namespace : string | undefined;
34
+ private readonly _fetch : typeof fetch;
35
+
36
+ constructor(opts: FetchSemanticCacheBackendOptions) {
37
+ this._base = opts.baseUrl.replace(/\/$/, '');
38
+ this._apiKey = opts.apiKey;
39
+ this._namespace = opts.namespace;
40
+ this._fetch = opts.fetchImpl ?? fetch;
41
+ }
42
+
43
+ async lookup(embedding: Float32Array, threshold: number): Promise<{ response: string; score: number } | undefined> {
44
+ const res = await this._fetch(`${this._base}/v1/semantic-cache/lookup`, {
45
+ method : 'POST',
46
+ headers: this._headers(),
47
+ body : JSON.stringify({
48
+ embedding: Array.from(embedding),
49
+ threshold,
50
+ ...(this._namespace ? { namespace: this._namespace } : {}),
51
+ }),
52
+ });
53
+ if (!res.ok) return undefined;
54
+ const json = await res.json().catch(() => null) as { hit?: { response?: unknown; score?: unknown } } | null;
55
+ const hit = json?.hit;
56
+ if (!hit || typeof hit.response !== 'string' || typeof hit.score !== 'number') return undefined;
57
+ return { response: hit.response, score: hit.score };
58
+ }
59
+
60
+ async store(embedding: Float32Array, response: string, meta?: Record<string, unknown>): Promise<void> {
61
+ await this._fetch(`${this._base}/v1/semantic-cache/store`, {
62
+ method : 'POST',
63
+ headers: this._headers(),
64
+ body : JSON.stringify({
65
+ embedding: Array.from(embedding),
66
+ response,
67
+ ...(this._namespace ? { namespace: this._namespace } : {}),
68
+ ...(meta ? { meta } : {}),
69
+ }),
70
+ });
71
+ }
72
+
73
+ private _headers(): Record<string, string> {
74
+ return {
75
+ 'Content-Type' : 'application/json',
76
+ Authorization : `Bearer ${this._apiKey}`,
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * SemanticCache – an embedding-keyed read-through cache for LLM completions.
3
+ *
4
+ * Unlike the exact-match ResponseCache (which keys on the byte-identical prompt),
5
+ * this keys on the *meaning* of the query: it embeds the query and serves a
6
+ * cached answer when a stored entry is within `threshold` cosine similarity.
7
+ * That catches paraphrases — "fix the auth bug" ≈ "login is broken" — which is
8
+ * where real frontier-call avoidance (and token savings) comes from.
9
+ *
10
+ * Two tiers, mirroring the project's L1-in-process / L2-shared read-through
11
+ * pattern:
12
+ * - L1: an in-process vector list, scanned locally (fast, offline-capable).
13
+ * - L2: an optional shared backend (e.g. the BuilderForce.ai gateway vector
14
+ * store) so a hit on one surface — web or agent — benefits the other.
15
+ *
16
+ * Fully portable: the embedder and the L2 backend are injected, so the same
17
+ * class runs in the browser (WebGPU SSM + native fetch) and in Node (the agent's
18
+ * `@webgpu/node` SSM + fetch) with no environment-specific forks.
19
+ */
20
+
21
+ import { cosineSimilarity } from '../similarity/index.js';
22
+
23
+ /** Produces an embedding vector for a piece of text (the on-device SSM, typically). */
24
+ export type Embedder = (text: string) => Promise<Float32Array>;
25
+
26
+ /**
27
+ * The shared (L2) cache tier. Implemented by `FetchSemanticCacheBackend` against
28
+ * the gateway, but any store satisfying this shape can be injected.
29
+ */
30
+ export interface SemanticCacheBackend {
31
+ /** Returns the best stored entry at/above `threshold` cosine similarity, or undefined. */
32
+ lookup(embedding: Float32Array, threshold: number): Promise<{ response: string; score: number } | undefined>;
33
+ /** Persists an embedding → response association. */
34
+ store(embedding: Float32Array, response: string, meta?: Record<string, unknown>): Promise<void>;
35
+ }
36
+
37
+ export interface SemanticCacheHit {
38
+ response: string;
39
+ /** Cosine similarity of the matched entry to the query. */
40
+ score: number;
41
+ /** Which tier served the hit. */
42
+ tier: 'l1' | 'l2';
43
+ }
44
+
45
+ export interface SemanticCacheOptions {
46
+ /** Embeds queries. Required — this is what makes the cache semantic. */
47
+ embed: Embedder;
48
+ /**
49
+ * Cosine similarity at/above which a stored entry counts as a hit.
50
+ * Higher = stricter (fewer false hits, lower hit rate). Default: 0.92.
51
+ */
52
+ threshold?: number;
53
+ /** Max L1 entries retained (oldest evicted first). Default: 500. */
54
+ maxEntries?: number;
55
+ /** Optional TTL (ms) for L1 entries. Omit for no expiry. */
56
+ ttlMs?: number;
57
+ /** Optional shared L2 backend (e.g. the gateway). */
58
+ l2?: SemanticCacheBackend;
59
+ /**
60
+ * When true (default), an answer served by L2 is also written into L1 so the
61
+ * next local lookup is a fast hit — read-through cache warming.
62
+ */
63
+ warmL1FromL2?: boolean;
64
+ }
65
+
66
+ interface L1Entry { embedding: Float32Array; response: string; timestamp: number; }
67
+
68
+ const DEFAULT_THRESHOLD = 0.92;
69
+ const DEFAULT_MAX_ENTRIES = 500;
70
+
71
+ export class SemanticCache {
72
+ private readonly _embed : Embedder;
73
+ private readonly _threshold : number;
74
+ private readonly _maxEntries : number;
75
+ private readonly _ttlMs : number | undefined;
76
+ private readonly _l2 : SemanticCacheBackend | undefined;
77
+ private readonly _warmL1 : boolean;
78
+ private readonly _l1 : L1Entry[] = [];
79
+
80
+ private _l1Hits = 0;
81
+ private _l2Hits = 0;
82
+ private _misses = 0;
83
+
84
+ constructor(opts: SemanticCacheOptions) {
85
+ this._embed = opts.embed;
86
+ this._threshold = opts.threshold ?? DEFAULT_THRESHOLD;
87
+ this._maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
88
+ this._ttlMs = opts.ttlMs;
89
+ this._l2 = opts.l2;
90
+ this._warmL1 = opts.warmL1FromL2 ?? true;
91
+ }
92
+
93
+ /**
94
+ * Read-through entry point: returns a cached answer for a semantically-similar
95
+ * prior query, otherwise runs `generate()`, stores the result in both tiers,
96
+ * and returns it. Embeds the query exactly once (lookup + store share it).
97
+ */
98
+ async getOrGenerate(
99
+ query: string,
100
+ generate: () => Promise<string>,
101
+ meta?: Record<string, unknown>,
102
+ ): Promise<{ response: string; cached: boolean; tier?: 'l1' | 'l2'; score?: number }> {
103
+ const qv = await this._embed(query);
104
+ const hit = await this._lookupVec(qv);
105
+ if (hit) return { response: hit.response, cached: true, tier: hit.tier, score: hit.score };
106
+
107
+ const response = await generate();
108
+ await this._storeVec(qv, response, meta);
109
+ return { response, cached: false };
110
+ }
111
+
112
+ /** Looks up a semantically-similar cached answer without generating on a miss. */
113
+ async lookup(query: string): Promise<SemanticCacheHit | undefined> {
114
+ return this._lookupVec(await this._embed(query));
115
+ }
116
+
117
+ /** Stores a query → response association in both tiers. */
118
+ async store(query: string, response: string, meta?: Record<string, unknown>): Promise<void> {
119
+ await this._storeVec(await this._embed(query), response, meta);
120
+ }
121
+
122
+ /** Drops all L1 entries. Does not touch the shared L2 backend. */
123
+ clear(): void {
124
+ this._l1.length = 0;
125
+ }
126
+
127
+ /** Current L1 entry count. */
128
+ get size(): number {
129
+ return this._l1.length;
130
+ }
131
+
132
+ /** Cumulative hit/miss counters across both tiers — for measuring savings. */
133
+ get stats(): { l1Hits: number; l2Hits: number; misses: number } {
134
+ return { l1Hits: this._l1Hits, l2Hits: this._l2Hits, misses: this._misses };
135
+ }
136
+
137
+ // ── Internals (operate on a precomputed embedding) ────────────────────────
138
+
139
+ private async _lookupVec(qv: Float32Array): Promise<SemanticCacheHit | undefined> {
140
+ const local = this._searchL1(qv);
141
+ if (local) {
142
+ this._l1Hits++;
143
+ return { response: local.response, score: local.score, tier: 'l1' };
144
+ }
145
+
146
+ if (this._l2) {
147
+ // L2 is best-effort: a gateway error degrades to local-only, never throws.
148
+ const remote = await this._l2.lookup(qv, this._threshold).catch(() => undefined);
149
+ if (remote && remote.score >= this._threshold) {
150
+ if (this._warmL1) this._addL1(qv, remote.response);
151
+ this._l2Hits++;
152
+ return { response: remote.response, score: remote.score, tier: 'l2' };
153
+ }
154
+ }
155
+
156
+ this._misses++;
157
+ return undefined;
158
+ }
159
+
160
+ private async _storeVec(qv: Float32Array, response: string, meta?: Record<string, unknown>): Promise<void> {
161
+ this._addL1(qv, response);
162
+ if (this._l2) {
163
+ // Best-effort: failing to share to L2 must not fail the caller's request.
164
+ await this._l2.store(qv, response, meta).catch(() => { /* swallow — local copy still cached */ });
165
+ }
166
+ }
167
+
168
+ /** Linear cosine scan over L1, dropping expired entries en route. */
169
+ private _searchL1(qv: Float32Array): { response: string; score: number } | undefined {
170
+ const now = Date.now();
171
+ let best: L1Entry | undefined;
172
+ let bestScore = -Infinity;
173
+
174
+ for (let i = this._l1.length - 1; i >= 0; i--) {
175
+ const entry = this._l1[i]!;
176
+ if (this._ttlMs != null && now > entry.timestamp + this._ttlMs) {
177
+ this._l1.splice(i, 1);
178
+ continue;
179
+ }
180
+ const score = cosineSimilarity(qv, entry.embedding);
181
+ if (score > bestScore) {
182
+ bestScore = score;
183
+ best = entry;
184
+ }
185
+ }
186
+
187
+ return best && bestScore >= this._threshold
188
+ ? { response: best.response, score: bestScore }
189
+ : undefined;
190
+ }
191
+
192
+ private _addL1(qv: Float32Array, response: string): void {
193
+ this._l1.push({ embedding: qv, response, timestamp: Date.now() });
194
+ while (this._l1.length > this._maxEntries) this._l1.shift();
195
+ }
196
+ }
@@ -0,0 +1,9 @@
1
+ export { SemanticCache } from './SemanticCache.js';
2
+ export type {
3
+ Embedder,
4
+ SemanticCacheBackend,
5
+ SemanticCacheHit,
6
+ SemanticCacheOptions,
7
+ } from './SemanticCache.js';
8
+ export { FetchSemanticCacheBackend } from './FetchSemanticCacheBackend.js';
9
+ export type { FetchSemanticCacheBackendOptions } from './FetchSemanticCacheBackend.js';