@usewhisper/sdk 3.10.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -52
- package/adapters/ai-sdk.d.mts +2462 -0
- package/adapters/ai-sdk.d.ts +2462 -0
- package/adapters/ai-sdk.js +3192 -0
- package/adapters/ai-sdk.mjs +3155 -0
- package/adapters/tools.d.mts +2 -0
- package/adapters/tools.d.ts +2 -0
- package/adapters/tools.js +7148 -0
- package/adapters/tools.mjs +7117 -0
- package/index.d.mts +2 -2265
- package/index.d.ts +2 -2265
- package/index.js +5045 -122
- package/index.mjs +5045 -120
- package/package.json +30 -3
- package/router/memory-router.d.mts +2 -0
- package/router/memory-router.d.ts +2 -0
- package/router/memory-router.js +3169 -0
- package/router/memory-router.mjs +3131 -0
|
@@ -0,0 +1,3131 @@
|
|
|
1
|
+
// ../src/sdk/errors.ts
|
|
2
|
+
var WhisperError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
retryable;
|
|
6
|
+
hint;
|
|
7
|
+
requestId;
|
|
8
|
+
details;
|
|
9
|
+
constructor(args) {
|
|
10
|
+
super(args.message, args.cause ? { cause: args.cause } : void 0);
|
|
11
|
+
this.name = "WhisperError";
|
|
12
|
+
this.code = args.code;
|
|
13
|
+
this.status = args.status;
|
|
14
|
+
this.retryable = args.retryable ?? false;
|
|
15
|
+
this.hint = args.hint;
|
|
16
|
+
this.requestId = args.requestId;
|
|
17
|
+
this.details = args.details;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ../src/sdk/core/telemetry.ts
|
|
22
|
+
var DiagnosticsStore = class {
|
|
23
|
+
maxEntries;
|
|
24
|
+
records = [];
|
|
25
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
26
|
+
constructor(maxEntries = 1e3) {
|
|
27
|
+
this.maxEntries = Math.max(1, maxEntries);
|
|
28
|
+
}
|
|
29
|
+
add(record) {
|
|
30
|
+
this.records.push(record);
|
|
31
|
+
if (this.records.length > this.maxEntries) {
|
|
32
|
+
this.records.splice(0, this.records.length - this.maxEntries);
|
|
33
|
+
}
|
|
34
|
+
for (const fn of this.subscribers) {
|
|
35
|
+
try {
|
|
36
|
+
fn(record);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
getLast(limit = 25) {
|
|
42
|
+
const count = Math.max(1, limit);
|
|
43
|
+
return this.records.slice(-count);
|
|
44
|
+
}
|
|
45
|
+
snapshot() {
|
|
46
|
+
const total = this.records.length;
|
|
47
|
+
const success = this.records.filter((r) => r.success).length;
|
|
48
|
+
const failure = total - success;
|
|
49
|
+
const duration = this.records.reduce((acc, item) => acc + item.durationMs, 0);
|
|
50
|
+
return {
|
|
51
|
+
total,
|
|
52
|
+
success,
|
|
53
|
+
failure,
|
|
54
|
+
avgDurationMs: total > 0 ? duration / total : 0,
|
|
55
|
+
lastTraceId: this.records[this.records.length - 1]?.traceId
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
subscribe(fn) {
|
|
59
|
+
this.subscribers.add(fn);
|
|
60
|
+
return () => {
|
|
61
|
+
this.subscribers.delete(fn);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ../src/sdk/core/utils.ts
|
|
67
|
+
function normalizeBaseUrl(url) {
|
|
68
|
+
let normalized = url.trim().replace(/\/+$/, "");
|
|
69
|
+
normalized = normalized.replace(/\/api\/v1$/i, "");
|
|
70
|
+
normalized = normalized.replace(/\/v1$/i, "");
|
|
71
|
+
normalized = normalized.replace(/\/api$/i, "");
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
function normalizeEndpoint(endpoint) {
|
|
75
|
+
const withLeadingSlash = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
76
|
+
if (/^\/api\/v1(\/|$)/i.test(withLeadingSlash)) {
|
|
77
|
+
return withLeadingSlash.replace(/^\/api/i, "");
|
|
78
|
+
}
|
|
79
|
+
return withLeadingSlash;
|
|
80
|
+
}
|
|
81
|
+
function nowIso() {
|
|
82
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
83
|
+
}
|
|
84
|
+
function stableHash(input) {
|
|
85
|
+
let hash = 2166136261;
|
|
86
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
87
|
+
hash ^= input.charCodeAt(i);
|
|
88
|
+
hash = Math.imul(hash, 16777619);
|
|
89
|
+
}
|
|
90
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
91
|
+
}
|
|
92
|
+
function normalizeQuery(query) {
|
|
93
|
+
return query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
94
|
+
}
|
|
95
|
+
function randomId(prefix = "id") {
|
|
96
|
+
return `${prefix}_${stableHash(`${Date.now()}_${Math.random()}`)}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ../src/sdk/core/client.ts
|
|
100
|
+
var DEFAULT_TIMEOUTS = {
|
|
101
|
+
searchMs: 3e3,
|
|
102
|
+
writeAckMs: 2e3,
|
|
103
|
+
bulkMs: 1e4,
|
|
104
|
+
profileMs: 2500,
|
|
105
|
+
sessionMs: 2500
|
|
106
|
+
};
|
|
107
|
+
var DEFAULT_RETRYABLE_STATUS = [408, 429, 500, 502, 503, 504];
|
|
108
|
+
var DEFAULT_API_KEY_ONLY_PREFIXES = ["/v1/memory", "/v1/context/query"];
|
|
109
|
+
var DEFAULT_RETRY_ATTEMPTS = {
|
|
110
|
+
search: 3,
|
|
111
|
+
writeAck: 2,
|
|
112
|
+
bulk: 2,
|
|
113
|
+
profile: 2,
|
|
114
|
+
session: 2,
|
|
115
|
+
query: 3,
|
|
116
|
+
get: 2
|
|
117
|
+
};
|
|
118
|
+
function isObject(value) {
|
|
119
|
+
return typeof value === "object" && value !== null;
|
|
120
|
+
}
|
|
121
|
+
function toMessage(payload, status, statusText) {
|
|
122
|
+
if (typeof payload === "string" && payload.trim()) return payload;
|
|
123
|
+
if (isObject(payload)) {
|
|
124
|
+
const maybeError = payload.error;
|
|
125
|
+
const maybeMessage = payload.message;
|
|
126
|
+
if (typeof maybeError === "string" && maybeError.trim()) return maybeError;
|
|
127
|
+
if (typeof maybeMessage === "string" && maybeMessage.trim()) return maybeMessage;
|
|
128
|
+
if (isObject(maybeError) && typeof maybeError.message === "string") return maybeError.message;
|
|
129
|
+
}
|
|
130
|
+
return `HTTP ${status}: ${statusText}`;
|
|
131
|
+
}
|
|
132
|
+
var RuntimeClientError = class extends Error {
|
|
133
|
+
status;
|
|
134
|
+
retryable;
|
|
135
|
+
code;
|
|
136
|
+
details;
|
|
137
|
+
traceId;
|
|
138
|
+
hint;
|
|
139
|
+
requestId;
|
|
140
|
+
constructor(args) {
|
|
141
|
+
super(args.message);
|
|
142
|
+
this.name = "RuntimeClientError";
|
|
143
|
+
this.status = args.status;
|
|
144
|
+
this.retryable = args.retryable;
|
|
145
|
+
this.code = args.code;
|
|
146
|
+
this.details = args.details;
|
|
147
|
+
this.traceId = args.traceId;
|
|
148
|
+
this.hint = args.hint;
|
|
149
|
+
this.requestId = args.requestId;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var RuntimeClient = class {
|
|
153
|
+
apiKey;
|
|
154
|
+
baseUrl;
|
|
155
|
+
sdkVersion;
|
|
156
|
+
compatMode;
|
|
157
|
+
retryPolicy;
|
|
158
|
+
timeouts;
|
|
159
|
+
diagnostics;
|
|
160
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
161
|
+
sendApiKeyHeader;
|
|
162
|
+
fetchImpl;
|
|
163
|
+
constructor(options, diagnostics) {
|
|
164
|
+
if (!options.apiKey) {
|
|
165
|
+
throw new RuntimeClientError({
|
|
166
|
+
code: "INVALID_API_KEY",
|
|
167
|
+
message: "API key is required",
|
|
168
|
+
retryable: false
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
this.apiKey = options.apiKey;
|
|
172
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl || "https://context.usewhisper.dev");
|
|
173
|
+
this.sdkVersion = options.sdkVersion || "2.x-runtime";
|
|
174
|
+
this.compatMode = options.compatMode || "fallback";
|
|
175
|
+
this.retryPolicy = {
|
|
176
|
+
retryableStatusCodes: options.retryPolicy?.retryableStatusCodes || DEFAULT_RETRYABLE_STATUS,
|
|
177
|
+
retryOnNetworkError: options.retryPolicy?.retryOnNetworkError ?? true,
|
|
178
|
+
maxBackoffMs: options.retryPolicy?.maxBackoffMs ?? 1200,
|
|
179
|
+
baseBackoffMs: options.retryPolicy?.baseBackoffMs ?? 250,
|
|
180
|
+
maxAttemptsByOperation: options.retryPolicy?.maxAttemptsByOperation || {}
|
|
181
|
+
};
|
|
182
|
+
this.timeouts = {
|
|
183
|
+
...DEFAULT_TIMEOUTS,
|
|
184
|
+
...options.timeouts || {}
|
|
185
|
+
};
|
|
186
|
+
this.sendApiKeyHeader = process.env.WHISPER_SEND_X_API_KEY === "1";
|
|
187
|
+
this.fetchImpl = options.fetchImpl || fetch;
|
|
188
|
+
this.diagnostics = diagnostics || new DiagnosticsStore(1e3);
|
|
189
|
+
}
|
|
190
|
+
getDiagnosticsStore() {
|
|
191
|
+
return this.diagnostics;
|
|
192
|
+
}
|
|
193
|
+
getCompatMode() {
|
|
194
|
+
return this.compatMode;
|
|
195
|
+
}
|
|
196
|
+
timeoutFor(operation) {
|
|
197
|
+
switch (operation) {
|
|
198
|
+
case "search":
|
|
199
|
+
return this.timeouts.searchMs;
|
|
200
|
+
case "writeAck":
|
|
201
|
+
return this.timeouts.writeAckMs;
|
|
202
|
+
case "bulk":
|
|
203
|
+
return this.timeouts.bulkMs;
|
|
204
|
+
case "profile":
|
|
205
|
+
return this.timeouts.profileMs;
|
|
206
|
+
case "session":
|
|
207
|
+
return this.timeouts.sessionMs;
|
|
208
|
+
case "query":
|
|
209
|
+
case "get":
|
|
210
|
+
default:
|
|
211
|
+
return this.timeouts.searchMs;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
maxAttemptsFor(operation) {
|
|
215
|
+
const override = this.retryPolicy.maxAttemptsByOperation?.[operation];
|
|
216
|
+
return Math.max(1, override ?? DEFAULT_RETRY_ATTEMPTS[operation]);
|
|
217
|
+
}
|
|
218
|
+
shouldRetryStatus(status) {
|
|
219
|
+
return status !== void 0 && this.retryPolicy.retryableStatusCodes?.includes(status) === true;
|
|
220
|
+
}
|
|
221
|
+
backoff(attempt) {
|
|
222
|
+
const base = this.retryPolicy.baseBackoffMs ?? 250;
|
|
223
|
+
const max = this.retryPolicy.maxBackoffMs ?? 1200;
|
|
224
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
225
|
+
return Math.min(max, Math.floor(base * Math.pow(2, attempt) * jitter));
|
|
226
|
+
}
|
|
227
|
+
runtimeName() {
|
|
228
|
+
const maybeWindow = globalThis.window;
|
|
229
|
+
return maybeWindow && typeof maybeWindow === "object" ? "browser" : "node";
|
|
230
|
+
}
|
|
231
|
+
apiKeyOnlyPrefixes() {
|
|
232
|
+
const raw = process.env.WHISPER_API_KEY_ONLY_PREFIXES;
|
|
233
|
+
if (!raw || !raw.trim()) return DEFAULT_API_KEY_ONLY_PREFIXES;
|
|
234
|
+
return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
235
|
+
}
|
|
236
|
+
shouldAttachApiKeyHeader(endpoint) {
|
|
237
|
+
if (this.sendApiKeyHeader) return true;
|
|
238
|
+
const prefixes = this.apiKeyOnlyPrefixes();
|
|
239
|
+
return prefixes.some((prefix) => endpoint === prefix || endpoint.startsWith(`${prefix}/`));
|
|
240
|
+
}
|
|
241
|
+
createRequestFingerprint(options) {
|
|
242
|
+
const normalizedEndpoint = normalizeEndpoint(options.endpoint);
|
|
243
|
+
const authFingerprint = stableHash(this.apiKey.replace(/^Bearer\s+/i, ""));
|
|
244
|
+
const payload = JSON.stringify({
|
|
245
|
+
method: options.method || "GET",
|
|
246
|
+
endpoint: normalizedEndpoint,
|
|
247
|
+
body: options.body || null,
|
|
248
|
+
extra: options.dedupeKeyExtra || "",
|
|
249
|
+
authFingerprint
|
|
250
|
+
});
|
|
251
|
+
return stableHash(payload);
|
|
252
|
+
}
|
|
253
|
+
async request(options) {
|
|
254
|
+
const dedupeKey = options.idempotent ? this.createRequestFingerprint(options) : null;
|
|
255
|
+
if (dedupeKey) {
|
|
256
|
+
const inFlight = this.inFlight.get(dedupeKey);
|
|
257
|
+
if (inFlight) {
|
|
258
|
+
const data = await inFlight;
|
|
259
|
+
this.diagnostics.add({
|
|
260
|
+
id: randomId("diag"),
|
|
261
|
+
startedAt: nowIso(),
|
|
262
|
+
endedAt: nowIso(),
|
|
263
|
+
traceId: data.traceId,
|
|
264
|
+
spanId: randomId("span"),
|
|
265
|
+
operation: options.operation,
|
|
266
|
+
method: options.method || "GET",
|
|
267
|
+
endpoint: normalizeEndpoint(options.endpoint),
|
|
268
|
+
status: data.status,
|
|
269
|
+
durationMs: 0,
|
|
270
|
+
success: true,
|
|
271
|
+
deduped: true
|
|
272
|
+
});
|
|
273
|
+
const cloned = {
|
|
274
|
+
data: data.data,
|
|
275
|
+
status: data.status,
|
|
276
|
+
traceId: data.traceId
|
|
277
|
+
};
|
|
278
|
+
return cloned;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const runner = this.performRequest(options).then((data) => {
|
|
282
|
+
if (dedupeKey) this.inFlight.delete(dedupeKey);
|
|
283
|
+
return data;
|
|
284
|
+
}).catch((error) => {
|
|
285
|
+
if (dedupeKey) this.inFlight.delete(dedupeKey);
|
|
286
|
+
throw error;
|
|
287
|
+
});
|
|
288
|
+
if (dedupeKey) {
|
|
289
|
+
this.inFlight.set(dedupeKey, runner);
|
|
290
|
+
}
|
|
291
|
+
return runner;
|
|
292
|
+
}
|
|
293
|
+
async performRequest(options) {
|
|
294
|
+
const method = options.method || "GET";
|
|
295
|
+
const normalizedEndpoint = normalizeEndpoint(options.endpoint);
|
|
296
|
+
const operation = options.operation;
|
|
297
|
+
const maxAttempts = this.maxAttemptsFor(operation);
|
|
298
|
+
const timeoutMs = this.timeoutFor(operation);
|
|
299
|
+
const traceId = options.traceId || randomId("trace");
|
|
300
|
+
let lastError = null;
|
|
301
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
302
|
+
const spanId = randomId("span");
|
|
303
|
+
const startedAt = Date.now();
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
306
|
+
try {
|
|
307
|
+
const attachApiKeyHeader = this.shouldAttachApiKeyHeader(normalizedEndpoint);
|
|
308
|
+
const response = await this.fetchImpl(`${this.baseUrl}${normalizedEndpoint}`, {
|
|
309
|
+
method,
|
|
310
|
+
signal: controller.signal,
|
|
311
|
+
keepalive: method !== "GET",
|
|
312
|
+
headers: {
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
Authorization: this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`,
|
|
315
|
+
...attachApiKeyHeader ? { "X-API-Key": this.apiKey.replace(/^Bearer\s+/i, "") } : {},
|
|
316
|
+
"x-trace-id": traceId,
|
|
317
|
+
"x-span-id": spanId,
|
|
318
|
+
"x-sdk-version": this.sdkVersion,
|
|
319
|
+
"x-sdk-runtime": this.runtimeName(),
|
|
320
|
+
...options.headers || {}
|
|
321
|
+
},
|
|
322
|
+
body: method === "GET" || method === "DELETE" ? void 0 : JSON.stringify(options.body || {})
|
|
323
|
+
});
|
|
324
|
+
clearTimeout(timeout);
|
|
325
|
+
let payload = null;
|
|
326
|
+
try {
|
|
327
|
+
payload = await response.json();
|
|
328
|
+
} catch {
|
|
329
|
+
payload = await response.text().catch(() => "");
|
|
330
|
+
}
|
|
331
|
+
const durationMs = Date.now() - startedAt;
|
|
332
|
+
const record = {
|
|
333
|
+
id: randomId("diag"),
|
|
334
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
335
|
+
endedAt: nowIso(),
|
|
336
|
+
traceId,
|
|
337
|
+
spanId,
|
|
338
|
+
operation,
|
|
339
|
+
method,
|
|
340
|
+
endpoint: normalizedEndpoint,
|
|
341
|
+
status: response.status,
|
|
342
|
+
durationMs,
|
|
343
|
+
success: response.ok
|
|
344
|
+
};
|
|
345
|
+
this.diagnostics.add(record);
|
|
346
|
+
if (response.ok) {
|
|
347
|
+
return {
|
|
348
|
+
data: payload,
|
|
349
|
+
status: response.status,
|
|
350
|
+
traceId
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const message = toMessage(payload, response.status, response.statusText);
|
|
354
|
+
const payloadObject = isObject(payload) ? payload : {};
|
|
355
|
+
const payloadCode = typeof payloadObject.code === "string" ? payloadObject.code : void 0;
|
|
356
|
+
const payloadHint = typeof payloadObject.hint === "string" ? payloadObject.hint : void 0;
|
|
357
|
+
const payloadRequestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : typeof payloadObject.request_id === "string" ? payloadObject.request_id : void 0;
|
|
358
|
+
const payloadRetryable = typeof payloadObject.retryable === "boolean" ? payloadObject.retryable : void 0;
|
|
359
|
+
const statusRetryable = this.shouldRetryStatus(response.status);
|
|
360
|
+
const retryable = payloadRetryable ?? statusRetryable;
|
|
361
|
+
const error = new RuntimeClientError({
|
|
362
|
+
message,
|
|
363
|
+
status: response.status,
|
|
364
|
+
retryable,
|
|
365
|
+
code: payloadCode || (response.status === 404 ? "NOT_FOUND" : "REQUEST_FAILED"),
|
|
366
|
+
details: payload,
|
|
367
|
+
traceId: payloadRequestId || traceId,
|
|
368
|
+
requestId: payloadRequestId || traceId,
|
|
369
|
+
hint: payloadHint
|
|
370
|
+
});
|
|
371
|
+
lastError = error;
|
|
372
|
+
if (!retryable || attempt === maxAttempts - 1) {
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
clearTimeout(timeout);
|
|
377
|
+
const durationMs = Date.now() - startedAt;
|
|
378
|
+
const isAbort = isObject(error) && error.name === "AbortError";
|
|
379
|
+
const mapped = error instanceof RuntimeClientError ? error : new RuntimeClientError({
|
|
380
|
+
message: isAbort ? "Request timed out" : error instanceof Error ? error.message : "Network error",
|
|
381
|
+
retryable: this.retryPolicy.retryOnNetworkError ?? true,
|
|
382
|
+
code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
|
|
383
|
+
traceId,
|
|
384
|
+
requestId: traceId
|
|
385
|
+
});
|
|
386
|
+
lastError = mapped;
|
|
387
|
+
this.diagnostics.add({
|
|
388
|
+
id: randomId("diag"),
|
|
389
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
390
|
+
endedAt: nowIso(),
|
|
391
|
+
traceId,
|
|
392
|
+
spanId,
|
|
393
|
+
operation,
|
|
394
|
+
method,
|
|
395
|
+
endpoint: normalizedEndpoint,
|
|
396
|
+
durationMs,
|
|
397
|
+
success: false,
|
|
398
|
+
errorCode: mapped.code,
|
|
399
|
+
errorMessage: mapped.message
|
|
400
|
+
});
|
|
401
|
+
if (!mapped.retryable || attempt === maxAttempts - 1) {
|
|
402
|
+
throw mapped;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
await new Promise((resolve) => setTimeout(resolve, this.backoff(attempt)));
|
|
406
|
+
}
|
|
407
|
+
throw lastError || new RuntimeClientError({
|
|
408
|
+
message: "Request failed",
|
|
409
|
+
retryable: false,
|
|
410
|
+
code: "REQUEST_FAILED"
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// ../src/sdk/core/cache.ts
|
|
416
|
+
var SearchResponseCache = class {
|
|
417
|
+
ttlMs;
|
|
418
|
+
capacity;
|
|
419
|
+
byKey = /* @__PURE__ */ new Map();
|
|
420
|
+
scopeIndex = /* @__PURE__ */ new Map();
|
|
421
|
+
constructor(ttlMs = 7e3, capacity = 500) {
|
|
422
|
+
this.ttlMs = Math.max(1e3, ttlMs);
|
|
423
|
+
this.capacity = Math.max(10, capacity);
|
|
424
|
+
}
|
|
425
|
+
makeScopeKey(project, userId, sessionId) {
|
|
426
|
+
return `${project}:${userId || "_"}:${sessionId || "_"}`;
|
|
427
|
+
}
|
|
428
|
+
makeKey(input) {
|
|
429
|
+
const normalized = {
|
|
430
|
+
project: input.project,
|
|
431
|
+
userId: input.userId || "",
|
|
432
|
+
sessionId: input.sessionId || "",
|
|
433
|
+
query: normalizeQuery(input.query),
|
|
434
|
+
topK: input.topK,
|
|
435
|
+
profile: input.profile,
|
|
436
|
+
includePending: input.includePending
|
|
437
|
+
};
|
|
438
|
+
return `search:${stableHash(JSON.stringify(normalized))}`;
|
|
439
|
+
}
|
|
440
|
+
get(key) {
|
|
441
|
+
const found = this.byKey.get(key);
|
|
442
|
+
if (!found) return null;
|
|
443
|
+
if (found.expiresAt <= Date.now()) {
|
|
444
|
+
this.deleteByKey(key);
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
found.touchedAt = Date.now();
|
|
448
|
+
return found.value;
|
|
449
|
+
}
|
|
450
|
+
set(key, scopeKey, value) {
|
|
451
|
+
this.byKey.set(key, {
|
|
452
|
+
value,
|
|
453
|
+
scopeKey,
|
|
454
|
+
touchedAt: Date.now(),
|
|
455
|
+
expiresAt: Date.now() + this.ttlMs
|
|
456
|
+
});
|
|
457
|
+
if (!this.scopeIndex.has(scopeKey)) {
|
|
458
|
+
this.scopeIndex.set(scopeKey, /* @__PURE__ */ new Set());
|
|
459
|
+
}
|
|
460
|
+
this.scopeIndex.get(scopeKey).add(key);
|
|
461
|
+
this.evictIfNeeded();
|
|
462
|
+
}
|
|
463
|
+
invalidateScope(scopeKey) {
|
|
464
|
+
const keys = this.scopeIndex.get(scopeKey);
|
|
465
|
+
if (!keys || keys.size === 0) {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
const toDelete = Array.from(keys);
|
|
469
|
+
for (const key of toDelete) {
|
|
470
|
+
this.deleteByKey(key);
|
|
471
|
+
}
|
|
472
|
+
this.scopeIndex.delete(scopeKey);
|
|
473
|
+
return toDelete.length;
|
|
474
|
+
}
|
|
475
|
+
evictIfNeeded() {
|
|
476
|
+
if (this.byKey.size <= this.capacity) return;
|
|
477
|
+
const ordered = Array.from(this.byKey.entries()).sort((a, b) => a[1].touchedAt - b[1].touchedAt);
|
|
478
|
+
const removeCount = this.byKey.size - this.capacity;
|
|
479
|
+
for (let i = 0; i < removeCount; i += 1) {
|
|
480
|
+
this.deleteByKey(ordered[i][0]);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
deleteByKey(key) {
|
|
484
|
+
const found = this.byKey.get(key);
|
|
485
|
+
if (!found) return;
|
|
486
|
+
this.byKey.delete(key);
|
|
487
|
+
const scopeKeys = this.scopeIndex.get(found.scopeKey);
|
|
488
|
+
if (!scopeKeys) return;
|
|
489
|
+
scopeKeys.delete(key);
|
|
490
|
+
if (scopeKeys.size === 0) {
|
|
491
|
+
this.scopeIndex.delete(found.scopeKey);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// ../src/sdk/core/queue.ts
|
|
497
|
+
var InMemoryQueueStore = class {
|
|
498
|
+
items = [];
|
|
499
|
+
async load() {
|
|
500
|
+
return [...this.items];
|
|
501
|
+
}
|
|
502
|
+
async save(items) {
|
|
503
|
+
this.items = [...items];
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var WriteQueue = class {
|
|
507
|
+
flushHandler;
|
|
508
|
+
store;
|
|
509
|
+
maxBatchSize;
|
|
510
|
+
flushIntervalMs;
|
|
511
|
+
maxAttempts;
|
|
512
|
+
queue = [];
|
|
513
|
+
flushTimer = null;
|
|
514
|
+
flushing = false;
|
|
515
|
+
lastFlushAt;
|
|
516
|
+
lastFlushCount = 0;
|
|
517
|
+
constructor(args) {
|
|
518
|
+
this.flushHandler = args.flushHandler;
|
|
519
|
+
this.store = args.store || new InMemoryQueueStore();
|
|
520
|
+
this.maxBatchSize = Math.max(1, args.maxBatchSize ?? 50);
|
|
521
|
+
this.flushIntervalMs = Math.max(10, args.flushIntervalMs ?? 100);
|
|
522
|
+
this.maxAttempts = Math.max(1, args.maxAttempts ?? 2);
|
|
523
|
+
}
|
|
524
|
+
async start() {
|
|
525
|
+
const pending = await this.store.load();
|
|
526
|
+
if (pending.length > 0) {
|
|
527
|
+
this.queue.push(...pending);
|
|
528
|
+
}
|
|
529
|
+
if (!this.flushTimer) {
|
|
530
|
+
this.flushTimer = setInterval(() => {
|
|
531
|
+
void this.flush();
|
|
532
|
+
}, this.flushIntervalMs);
|
|
533
|
+
const timer = this.flushTimer;
|
|
534
|
+
if (typeof timer.unref === "function") {
|
|
535
|
+
timer.unref();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
this.bindProcessHooks();
|
|
539
|
+
}
|
|
540
|
+
async stop() {
|
|
541
|
+
if (this.flushTimer) {
|
|
542
|
+
clearInterval(this.flushTimer);
|
|
543
|
+
this.flushTimer = null;
|
|
544
|
+
}
|
|
545
|
+
await this.flush();
|
|
546
|
+
}
|
|
547
|
+
status() {
|
|
548
|
+
return {
|
|
549
|
+
queued: this.queue.length,
|
|
550
|
+
flushing: this.flushing,
|
|
551
|
+
lastFlushAt: this.lastFlushAt,
|
|
552
|
+
lastFlushCount: this.lastFlushCount
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
async enqueue(input) {
|
|
556
|
+
const eventId = input.eventId || this.makeEventId(input);
|
|
557
|
+
const item = {
|
|
558
|
+
...input,
|
|
559
|
+
eventId,
|
|
560
|
+
createdAt: nowIso()
|
|
561
|
+
};
|
|
562
|
+
this.queue.push(item);
|
|
563
|
+
await this.store.save(this.queue);
|
|
564
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
565
|
+
void this.flush();
|
|
566
|
+
}
|
|
567
|
+
return item;
|
|
568
|
+
}
|
|
569
|
+
async flush() {
|
|
570
|
+
if (this.flushing || this.queue.length === 0) return;
|
|
571
|
+
this.flushing = true;
|
|
572
|
+
try {
|
|
573
|
+
while (this.queue.length > 0) {
|
|
574
|
+
const batch = this.queue.slice(0, this.maxBatchSize);
|
|
575
|
+
let done = false;
|
|
576
|
+
let error = null;
|
|
577
|
+
for (let attempt = 0; attempt < this.maxAttempts; attempt += 1) {
|
|
578
|
+
try {
|
|
579
|
+
await this.flushHandler(batch);
|
|
580
|
+
done = true;
|
|
581
|
+
break;
|
|
582
|
+
} catch (err) {
|
|
583
|
+
error = err;
|
|
584
|
+
await new Promise((resolve) => setTimeout(resolve, (attempt + 1) * 180));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (!done) {
|
|
588
|
+
throw error instanceof Error ? error : new Error("Queue flush failed");
|
|
589
|
+
}
|
|
590
|
+
this.queue.splice(0, batch.length);
|
|
591
|
+
this.lastFlushAt = nowIso();
|
|
592
|
+
this.lastFlushCount = batch.length;
|
|
593
|
+
await this.store.save(this.queue);
|
|
594
|
+
}
|
|
595
|
+
} finally {
|
|
596
|
+
this.flushing = false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
makeEventId(input) {
|
|
600
|
+
const source = JSON.stringify({
|
|
601
|
+
project: input.project,
|
|
602
|
+
userId: input.userId || "",
|
|
603
|
+
sessionId: input.sessionId || "",
|
|
604
|
+
payload: input.payload
|
|
605
|
+
});
|
|
606
|
+
return `evt_${stableHash(source)}`;
|
|
607
|
+
}
|
|
608
|
+
bindProcessHooks() {
|
|
609
|
+
if (typeof process === "undefined") return;
|
|
610
|
+
const proc = process;
|
|
611
|
+
const flushOnExit = () => {
|
|
612
|
+
void this.flush();
|
|
613
|
+
};
|
|
614
|
+
proc.once("beforeExit", flushOnExit);
|
|
615
|
+
proc.once("SIGINT", flushOnExit);
|
|
616
|
+
proc.once("SIGTERM", flushOnExit);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
function createStorageQueueStore(key = "whisper_sdk_queue") {
|
|
620
|
+
const getStorage = () => {
|
|
621
|
+
const maybeStorage = globalThis.localStorage;
|
|
622
|
+
if (!maybeStorage || typeof maybeStorage !== "object") return null;
|
|
623
|
+
const candidate = maybeStorage;
|
|
624
|
+
if (typeof candidate.getItem !== "function" || typeof candidate.setItem !== "function") {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
getItem: candidate.getItem,
|
|
629
|
+
setItem: candidate.setItem
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
async load() {
|
|
634
|
+
const storage = getStorage();
|
|
635
|
+
if (!storage) return [];
|
|
636
|
+
const raw = storage.getItem(key);
|
|
637
|
+
if (!raw) return [];
|
|
638
|
+
try {
|
|
639
|
+
const parsed = JSON.parse(raw);
|
|
640
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
641
|
+
} catch {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
async save(items) {
|
|
646
|
+
const storage = getStorage();
|
|
647
|
+
if (!storage) return;
|
|
648
|
+
storage.setItem(key, JSON.stringify(items));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function createFileQueueStore(filePath) {
|
|
653
|
+
return {
|
|
654
|
+
async load() {
|
|
655
|
+
if (typeof process === "undefined") return [];
|
|
656
|
+
const fs = await import("fs/promises");
|
|
657
|
+
try {
|
|
658
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
659
|
+
const parsed = JSON.parse(raw);
|
|
660
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
661
|
+
} catch (error) {
|
|
662
|
+
const nodeError = error;
|
|
663
|
+
if (nodeError?.code === "ENOENT") {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
async save(items) {
|
|
670
|
+
if (typeof process === "undefined") return;
|
|
671
|
+
const fs = await import("fs/promises");
|
|
672
|
+
const path = await import("path");
|
|
673
|
+
const dir = path.dirname(filePath);
|
|
674
|
+
await fs.mkdir(dir, { recursive: true });
|
|
675
|
+
await fs.writeFile(filePath, JSON.stringify(items), "utf8");
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ../src/sdk/modules/memory.ts
|
|
681
|
+
function isEndpointNotFound(error) {
|
|
682
|
+
return error instanceof RuntimeClientError && error.status === 404;
|
|
683
|
+
}
|
|
684
|
+
function toSotaType(memoryType) {
|
|
685
|
+
if (!memoryType) return void 0;
|
|
686
|
+
switch (memoryType) {
|
|
687
|
+
case "episodic":
|
|
688
|
+
return "event";
|
|
689
|
+
case "semantic":
|
|
690
|
+
return "factual";
|
|
691
|
+
case "procedural":
|
|
692
|
+
return "instruction";
|
|
693
|
+
default:
|
|
694
|
+
return memoryType;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function toLegacyType(memoryType) {
|
|
698
|
+
if (!memoryType) return void 0;
|
|
699
|
+
switch (memoryType) {
|
|
700
|
+
case "event":
|
|
701
|
+
return "episodic";
|
|
702
|
+
case "instruction":
|
|
703
|
+
return "procedural";
|
|
704
|
+
case "preference":
|
|
705
|
+
case "relationship":
|
|
706
|
+
case "opinion":
|
|
707
|
+
case "goal":
|
|
708
|
+
return "semantic";
|
|
709
|
+
default:
|
|
710
|
+
return memoryType;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
var MemoryModule = class {
|
|
714
|
+
constructor(client, cache, queue, options = {}) {
|
|
715
|
+
this.client = client;
|
|
716
|
+
this.cache = cache;
|
|
717
|
+
this.queue = queue;
|
|
718
|
+
this.options = options;
|
|
719
|
+
}
|
|
720
|
+
resolveProject(project) {
|
|
721
|
+
const value = project || this.options.defaultProject;
|
|
722
|
+
if (!value) {
|
|
723
|
+
throw new RuntimeClientError({
|
|
724
|
+
code: "MISSING_PROJECT",
|
|
725
|
+
message: "Project is required",
|
|
726
|
+
retryable: false
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return value;
|
|
730
|
+
}
|
|
731
|
+
invalidate(project, userId, sessionId) {
|
|
732
|
+
if (this.options.cacheEnabled === false) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const scope = this.cache.makeScopeKey(project, userId, sessionId);
|
|
736
|
+
this.cache.invalidateScope(scope);
|
|
737
|
+
}
|
|
738
|
+
async add(params) {
|
|
739
|
+
const project = this.resolveProject(params.project);
|
|
740
|
+
const queueEnabled = this.options.queueEnabled !== false;
|
|
741
|
+
const useQueue = queueEnabled && (params.write_mode === "async" || params.async === true);
|
|
742
|
+
if (useQueue) {
|
|
743
|
+
const queued = await this.queue.enqueue({
|
|
744
|
+
project,
|
|
745
|
+
userId: params.user_id,
|
|
746
|
+
sessionId: params.session_id,
|
|
747
|
+
payload: {
|
|
748
|
+
content: params.content,
|
|
749
|
+
memory_type: toSotaType(params.memory_type),
|
|
750
|
+
user_id: params.user_id,
|
|
751
|
+
session_id: params.session_id,
|
|
752
|
+
agent_id: params.agent_id,
|
|
753
|
+
importance: params.importance,
|
|
754
|
+
confidence: params.confidence,
|
|
755
|
+
metadata: params.metadata,
|
|
756
|
+
document_date: params.document_date,
|
|
757
|
+
event_date: params.event_date
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
this.invalidate(project, params.user_id, params.session_id);
|
|
761
|
+
return {
|
|
762
|
+
success: true,
|
|
763
|
+
mode: "async",
|
|
764
|
+
queued: true,
|
|
765
|
+
event_id: queued.eventId,
|
|
766
|
+
accepted_at: queued.createdAt
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const response = await this.client.request({
|
|
771
|
+
endpoint: "/v1/memory",
|
|
772
|
+
method: "POST",
|
|
773
|
+
operation: "writeAck",
|
|
774
|
+
body: {
|
|
775
|
+
project,
|
|
776
|
+
content: params.content,
|
|
777
|
+
memory_type: toSotaType(params.memory_type),
|
|
778
|
+
user_id: params.user_id,
|
|
779
|
+
session_id: params.session_id,
|
|
780
|
+
agent_id: params.agent_id,
|
|
781
|
+
importance: params.importance,
|
|
782
|
+
confidence: params.confidence,
|
|
783
|
+
metadata: params.metadata,
|
|
784
|
+
document_date: params.document_date,
|
|
785
|
+
event_date: params.event_date,
|
|
786
|
+
write_mode: params.write_mode === "async" || params.async === true ? "async" : "sync"
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
this.invalidate(project, params.user_id, params.session_id);
|
|
790
|
+
return {
|
|
791
|
+
success: true,
|
|
792
|
+
mode: "sync",
|
|
793
|
+
trace_id: response.trace_id || response.traceId,
|
|
794
|
+
memory_id: response.memory_id || response.memory?.id,
|
|
795
|
+
semantic_status: response.semantic_status || response.memory?.semantic_status,
|
|
796
|
+
pending_visibility: Boolean(response.pending_visibility),
|
|
797
|
+
visibility_sla_ms: response.visibility_sla_ms
|
|
798
|
+
};
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
await this.client.request({
|
|
804
|
+
endpoint: "/v1/memories",
|
|
805
|
+
method: "POST",
|
|
806
|
+
operation: "writeAck",
|
|
807
|
+
body: {
|
|
808
|
+
project,
|
|
809
|
+
content: params.content,
|
|
810
|
+
memory_type: toLegacyType(params.memory_type),
|
|
811
|
+
user_id: params.user_id,
|
|
812
|
+
session_id: params.session_id,
|
|
813
|
+
agent_id: params.agent_id,
|
|
814
|
+
importance: params.importance,
|
|
815
|
+
metadata: params.metadata
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
this.invalidate(project, params.user_id, params.session_id);
|
|
819
|
+
return {
|
|
820
|
+
success: true,
|
|
821
|
+
mode: "sync"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async addBulk(params) {
|
|
826
|
+
const project = this.resolveProject(params.project);
|
|
827
|
+
if (!Array.isArray(params.memories) || params.memories.length === 0) {
|
|
828
|
+
throw new RuntimeClientError({
|
|
829
|
+
code: "VALIDATION_ERROR",
|
|
830
|
+
message: "memories is required",
|
|
831
|
+
retryable: false
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const queueEnabled = this.options.queueEnabled !== false;
|
|
835
|
+
const useQueue = queueEnabled && (params.write_mode === "async" || params.async === true);
|
|
836
|
+
if (useQueue) {
|
|
837
|
+
const queued = await Promise.all(
|
|
838
|
+
params.memories.map(
|
|
839
|
+
(memory) => this.queue.enqueue({
|
|
840
|
+
project,
|
|
841
|
+
userId: memory.user_id,
|
|
842
|
+
sessionId: memory.session_id,
|
|
843
|
+
payload: {
|
|
844
|
+
content: memory.content,
|
|
845
|
+
memory_type: toSotaType(memory.memory_type),
|
|
846
|
+
user_id: memory.user_id,
|
|
847
|
+
session_id: memory.session_id,
|
|
848
|
+
agent_id: memory.agent_id,
|
|
849
|
+
importance: memory.importance,
|
|
850
|
+
confidence: memory.confidence,
|
|
851
|
+
metadata: memory.metadata,
|
|
852
|
+
document_date: memory.document_date,
|
|
853
|
+
event_date: memory.event_date
|
|
854
|
+
}
|
|
855
|
+
})
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
for (const memory of params.memories) {
|
|
859
|
+
this.invalidate(project, memory.user_id, memory.session_id);
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
success: true,
|
|
863
|
+
mode: "async",
|
|
864
|
+
queued: true,
|
|
865
|
+
created: queued.length
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
const response = await this.client.request({
|
|
870
|
+
endpoint: "/v1/memory/bulk",
|
|
871
|
+
method: "POST",
|
|
872
|
+
operation: "bulk",
|
|
873
|
+
body: {
|
|
874
|
+
project,
|
|
875
|
+
memories: params.memories.map((memory) => ({
|
|
876
|
+
...memory,
|
|
877
|
+
memory_type: toSotaType(memory.memory_type)
|
|
878
|
+
})),
|
|
879
|
+
write_mode: params.write_mode === "async" || params.async === true ? "async" : "sync"
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
for (const memory of params.memories) {
|
|
883
|
+
this.invalidate(project, memory.user_id, memory.session_id);
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
success: true,
|
|
887
|
+
mode: "sync",
|
|
888
|
+
trace_id: response.traceId
|
|
889
|
+
};
|
|
890
|
+
} catch (error) {
|
|
891
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
await Promise.all(
|
|
895
|
+
params.memories.map(
|
|
896
|
+
(memory) => this.add({
|
|
897
|
+
project,
|
|
898
|
+
...memory,
|
|
899
|
+
write_mode: "sync"
|
|
900
|
+
})
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
return {
|
|
904
|
+
success: true,
|
|
905
|
+
mode: "sync"
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async search(params) {
|
|
910
|
+
const project = this.resolveProject(params.project);
|
|
911
|
+
const topK = params.top_k || 10;
|
|
912
|
+
const profile = params.profile || "fast";
|
|
913
|
+
const includePending = params.include_pending !== false;
|
|
914
|
+
const cacheKey = this.cache.makeKey({
|
|
915
|
+
project,
|
|
916
|
+
userId: params.user_id,
|
|
917
|
+
sessionId: params.session_id,
|
|
918
|
+
query: params.query,
|
|
919
|
+
topK,
|
|
920
|
+
profile,
|
|
921
|
+
includePending
|
|
922
|
+
});
|
|
923
|
+
if (this.options.cacheEnabled !== false) {
|
|
924
|
+
const cached = this.cache.get(cacheKey);
|
|
925
|
+
if (cached) {
|
|
926
|
+
return {
|
|
927
|
+
...cached,
|
|
928
|
+
cache_hit: true
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
const response = await this.client.request({
|
|
934
|
+
endpoint: "/v1/memory/search",
|
|
935
|
+
method: "POST",
|
|
936
|
+
operation: "search",
|
|
937
|
+
idempotent: true,
|
|
938
|
+
body: {
|
|
939
|
+
project,
|
|
940
|
+
query: params.query,
|
|
941
|
+
user_id: params.user_id,
|
|
942
|
+
session_id: params.session_id,
|
|
943
|
+
top_k: topK,
|
|
944
|
+
profile,
|
|
945
|
+
include_pending: includePending,
|
|
946
|
+
memory_types: params.memory_type ? [toSotaType(params.memory_type)] : void 0
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
const data = {
|
|
950
|
+
...response.data || {},
|
|
951
|
+
cache_hit: false
|
|
952
|
+
};
|
|
953
|
+
if (this.options.cacheEnabled !== false) {
|
|
954
|
+
const scope = this.cache.makeScopeKey(project, params.user_id, params.session_id);
|
|
955
|
+
this.cache.set(cacheKey, scope, data);
|
|
956
|
+
}
|
|
957
|
+
return data;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
const legacy = await this.client.request({
|
|
963
|
+
endpoint: "/v1/memories/search",
|
|
964
|
+
method: "POST",
|
|
965
|
+
operation: "search",
|
|
966
|
+
idempotent: true,
|
|
967
|
+
body: {
|
|
968
|
+
project,
|
|
969
|
+
query: params.query,
|
|
970
|
+
user_id: params.user_id,
|
|
971
|
+
session_id: params.session_id,
|
|
972
|
+
top_k: topK,
|
|
973
|
+
memory_type: toLegacyType(params.memory_type)
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
const data = {
|
|
977
|
+
...legacy.data || {},
|
|
978
|
+
cache_hit: false
|
|
979
|
+
};
|
|
980
|
+
if (this.options.cacheEnabled !== false) {
|
|
981
|
+
const scope = this.cache.makeScopeKey(project, params.user_id, params.session_id);
|
|
982
|
+
this.cache.set(cacheKey, scope, data);
|
|
983
|
+
}
|
|
984
|
+
return data;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
async getUserProfile(params) {
|
|
988
|
+
const project = this.resolveProject(params.project);
|
|
989
|
+
const query = new URLSearchParams({
|
|
990
|
+
project,
|
|
991
|
+
...params.include_pending !== void 0 ? { include_pending: String(params.include_pending) } : {},
|
|
992
|
+
...params.memory_types ? { memory_types: params.memory_types } : {}
|
|
993
|
+
});
|
|
994
|
+
try {
|
|
995
|
+
const response = await this.client.request({
|
|
996
|
+
endpoint: `/v1/memory/profile/${params.user_id}?${query}`,
|
|
997
|
+
method: "GET",
|
|
998
|
+
operation: "profile",
|
|
999
|
+
idempotent: true
|
|
1000
|
+
});
|
|
1001
|
+
return response.data;
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
const legacyQuery = new URLSearchParams({
|
|
1007
|
+
project,
|
|
1008
|
+
user_id: params.user_id,
|
|
1009
|
+
limit: "200"
|
|
1010
|
+
});
|
|
1011
|
+
const legacy = await this.client.request({
|
|
1012
|
+
endpoint: `/v1/memories?${legacyQuery}`,
|
|
1013
|
+
method: "GET",
|
|
1014
|
+
operation: "profile",
|
|
1015
|
+
idempotent: true
|
|
1016
|
+
});
|
|
1017
|
+
const memories = Array.isArray(legacy.data?.memories) ? legacy.data.memories : [];
|
|
1018
|
+
return {
|
|
1019
|
+
user_id: params.user_id,
|
|
1020
|
+
memories,
|
|
1021
|
+
count: memories.length
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async getSessionMemories(params) {
|
|
1026
|
+
const project = this.resolveProject(params.project);
|
|
1027
|
+
const query = new URLSearchParams({
|
|
1028
|
+
project,
|
|
1029
|
+
...params.limit ? { limit: String(params.limit) } : {},
|
|
1030
|
+
...params.include_pending !== void 0 ? { include_pending: String(params.include_pending) } : {}
|
|
1031
|
+
});
|
|
1032
|
+
const response = await this.client.request({
|
|
1033
|
+
endpoint: `/v1/memory/session/${params.session_id}?${query}`,
|
|
1034
|
+
method: "GET",
|
|
1035
|
+
operation: "profile",
|
|
1036
|
+
idempotent: true
|
|
1037
|
+
});
|
|
1038
|
+
return response.data;
|
|
1039
|
+
}
|
|
1040
|
+
async get(memoryId) {
|
|
1041
|
+
try {
|
|
1042
|
+
const response = await this.client.request({
|
|
1043
|
+
endpoint: `/v1/memory/${memoryId}`,
|
|
1044
|
+
method: "GET",
|
|
1045
|
+
operation: "get",
|
|
1046
|
+
idempotent: true
|
|
1047
|
+
});
|
|
1048
|
+
return response.data;
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
const legacy = await this.client.request({
|
|
1054
|
+
endpoint: `/v1/memories/${memoryId}`,
|
|
1055
|
+
method: "GET",
|
|
1056
|
+
operation: "get",
|
|
1057
|
+
idempotent: true
|
|
1058
|
+
});
|
|
1059
|
+
return legacy.data;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async update(memoryId, params) {
|
|
1063
|
+
try {
|
|
1064
|
+
await this.client.request({
|
|
1065
|
+
endpoint: `/v1/memory/${memoryId}`,
|
|
1066
|
+
method: "PUT",
|
|
1067
|
+
operation: "writeAck",
|
|
1068
|
+
body: params
|
|
1069
|
+
});
|
|
1070
|
+
return { success: true };
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
1075
|
+
await this.client.request({
|
|
1076
|
+
endpoint: `/v1/memories/${memoryId}`,
|
|
1077
|
+
method: "PUT",
|
|
1078
|
+
operation: "writeAck",
|
|
1079
|
+
body: { content: params.content }
|
|
1080
|
+
});
|
|
1081
|
+
return { success: true };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
async delete(memoryId) {
|
|
1085
|
+
try {
|
|
1086
|
+
await this.client.request({
|
|
1087
|
+
endpoint: `/v1/memory/${memoryId}`,
|
|
1088
|
+
method: "DELETE",
|
|
1089
|
+
operation: "writeAck"
|
|
1090
|
+
});
|
|
1091
|
+
return { success: true, deleted: memoryId };
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
await this.client.request({
|
|
1097
|
+
endpoint: `/v1/memories/${memoryId}`,
|
|
1098
|
+
method: "DELETE",
|
|
1099
|
+
operation: "writeAck"
|
|
1100
|
+
});
|
|
1101
|
+
return { success: true, deleted: memoryId };
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
async flag(params) {
|
|
1105
|
+
try {
|
|
1106
|
+
await this.client.request({
|
|
1107
|
+
endpoint: `/v1/memory/${params.memoryId}/flag`,
|
|
1108
|
+
method: "POST",
|
|
1109
|
+
operation: "writeAck",
|
|
1110
|
+
body: {
|
|
1111
|
+
reason: params.reason,
|
|
1112
|
+
severity: params.severity || "medium"
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
return { success: true };
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
if (this.client.getCompatMode() !== "fallback" || !isEndpointNotFound(error)) {
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
1120
|
+
await this.client.request({
|
|
1121
|
+
endpoint: `/v1/memory/${params.memoryId}`,
|
|
1122
|
+
method: "PUT",
|
|
1123
|
+
operation: "writeAck",
|
|
1124
|
+
body: {
|
|
1125
|
+
content: `[FLAGGED:${params.severity || "medium"}] ${params.reason}`
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
return { success: true };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// ../src/sdk/modules/session.ts
|
|
1134
|
+
function randomSessionId() {
|
|
1135
|
+
return `sess_${stableHash(`${Date.now()}_${Math.random()}`)}`;
|
|
1136
|
+
}
|
|
1137
|
+
function assertTransition(current, next) {
|
|
1138
|
+
const allowed = {
|
|
1139
|
+
created: ["active", "ended", "archived"],
|
|
1140
|
+
active: ["suspended", "ended", "archived"],
|
|
1141
|
+
suspended: ["resumed", "ended", "archived"],
|
|
1142
|
+
resumed: ["suspended", "ended", "archived"],
|
|
1143
|
+
ended: ["archived"],
|
|
1144
|
+
archived: []
|
|
1145
|
+
};
|
|
1146
|
+
if (!allowed[current].includes(next)) {
|
|
1147
|
+
throw new RuntimeClientError({
|
|
1148
|
+
code: "INVALID_SESSION_STATE",
|
|
1149
|
+
message: `Invalid session transition ${current} -> ${next}`,
|
|
1150
|
+
retryable: false
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
var SessionModule = class {
|
|
1155
|
+
constructor(memory, defaultProject) {
|
|
1156
|
+
this.memory = memory;
|
|
1157
|
+
this.defaultProject = defaultProject;
|
|
1158
|
+
}
|
|
1159
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1160
|
+
resolveProject(project) {
|
|
1161
|
+
const value = project || this.defaultProject;
|
|
1162
|
+
if (!value) {
|
|
1163
|
+
throw new RuntimeClientError({
|
|
1164
|
+
code: "MISSING_PROJECT",
|
|
1165
|
+
message: "Project is required",
|
|
1166
|
+
retryable: false
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
return value;
|
|
1170
|
+
}
|
|
1171
|
+
ensure(sessionId) {
|
|
1172
|
+
const found = this.sessions.get(sessionId);
|
|
1173
|
+
if (!found) {
|
|
1174
|
+
throw new RuntimeClientError({
|
|
1175
|
+
code: "SESSION_NOT_FOUND",
|
|
1176
|
+
message: `Unknown session ${sessionId}`,
|
|
1177
|
+
retryable: false
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return found;
|
|
1181
|
+
}
|
|
1182
|
+
async start(params) {
|
|
1183
|
+
const project = this.resolveProject(params.project);
|
|
1184
|
+
const sessionId = params.sessionId || randomSessionId();
|
|
1185
|
+
const now = nowIso();
|
|
1186
|
+
const record = {
|
|
1187
|
+
sessionId,
|
|
1188
|
+
project,
|
|
1189
|
+
userId: params.userId,
|
|
1190
|
+
state: "active",
|
|
1191
|
+
sequence: 0,
|
|
1192
|
+
metadata: params.metadata,
|
|
1193
|
+
createdAt: now,
|
|
1194
|
+
updatedAt: now
|
|
1195
|
+
};
|
|
1196
|
+
this.sessions.set(sessionId, record);
|
|
1197
|
+
return {
|
|
1198
|
+
sessionId,
|
|
1199
|
+
state: record.state,
|
|
1200
|
+
createdAt: now
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
async event(params) {
|
|
1204
|
+
const session = this.ensure(params.sessionId);
|
|
1205
|
+
if (session.state !== "active" && session.state !== "resumed") {
|
|
1206
|
+
throw new RuntimeClientError({
|
|
1207
|
+
code: "INVALID_SESSION_STATE",
|
|
1208
|
+
message: `Cannot append event in ${session.state} state`,
|
|
1209
|
+
retryable: false
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
session.sequence += 1;
|
|
1213
|
+
session.updatedAt = nowIso();
|
|
1214
|
+
const eventId = `evt_${stableHash(JSON.stringify({
|
|
1215
|
+
sessionId: session.sessionId,
|
|
1216
|
+
seq: session.sequence,
|
|
1217
|
+
type: params.type,
|
|
1218
|
+
content: params.content,
|
|
1219
|
+
parent: params.parentEventId || ""
|
|
1220
|
+
}))}`;
|
|
1221
|
+
await this.memory.add({
|
|
1222
|
+
project: session.project,
|
|
1223
|
+
content: `${params.type}: ${params.content}`,
|
|
1224
|
+
memory_type: "event",
|
|
1225
|
+
user_id: session.userId,
|
|
1226
|
+
session_id: session.sessionId,
|
|
1227
|
+
metadata: {
|
|
1228
|
+
session_event: true,
|
|
1229
|
+
event_id: eventId,
|
|
1230
|
+
sequence: session.sequence,
|
|
1231
|
+
parent_event_id: params.parentEventId,
|
|
1232
|
+
...session.metadata,
|
|
1233
|
+
...params.metadata || {}
|
|
1234
|
+
},
|
|
1235
|
+
write_mode: "async"
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
success: true,
|
|
1239
|
+
eventId,
|
|
1240
|
+
sequence: session.sequence
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
async suspend(params) {
|
|
1244
|
+
const session = this.ensure(params.sessionId);
|
|
1245
|
+
assertTransition(session.state, "suspended");
|
|
1246
|
+
session.state = "suspended";
|
|
1247
|
+
session.updatedAt = nowIso();
|
|
1248
|
+
return { sessionId: session.sessionId, state: session.state };
|
|
1249
|
+
}
|
|
1250
|
+
async resume(params) {
|
|
1251
|
+
const session = this.ensure(params.sessionId);
|
|
1252
|
+
const target = session.state === "suspended" ? "resumed" : "active";
|
|
1253
|
+
assertTransition(session.state, target);
|
|
1254
|
+
session.state = target;
|
|
1255
|
+
session.updatedAt = nowIso();
|
|
1256
|
+
return { sessionId: session.sessionId, state: session.state };
|
|
1257
|
+
}
|
|
1258
|
+
async end(params) {
|
|
1259
|
+
const session = this.ensure(params.sessionId);
|
|
1260
|
+
assertTransition(session.state, "ended");
|
|
1261
|
+
session.state = "ended";
|
|
1262
|
+
session.updatedAt = nowIso();
|
|
1263
|
+
return { sessionId: session.sessionId, state: session.state };
|
|
1264
|
+
}
|
|
1265
|
+
async archive(params) {
|
|
1266
|
+
const session = this.ensure(params.sessionId);
|
|
1267
|
+
assertTransition(session.state, "archived");
|
|
1268
|
+
session.state = "archived";
|
|
1269
|
+
session.updatedAt = nowIso();
|
|
1270
|
+
return { sessionId: session.sessionId, state: session.state };
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// ../src/sdk/modules/profile.ts
|
|
1275
|
+
var ProfileModule = class {
|
|
1276
|
+
constructor(memory) {
|
|
1277
|
+
this.memory = memory;
|
|
1278
|
+
}
|
|
1279
|
+
async getUserProfile(params) {
|
|
1280
|
+
return this.memory.getUserProfile(params);
|
|
1281
|
+
}
|
|
1282
|
+
async getSessionMemories(params) {
|
|
1283
|
+
return this.memory.getSessionMemories(params);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// ../src/sdk/modules/analytics.ts
|
|
1288
|
+
var AnalyticsModule = class {
|
|
1289
|
+
constructor(diagnostics, queue) {
|
|
1290
|
+
this.diagnostics = diagnostics;
|
|
1291
|
+
this.queue = queue;
|
|
1292
|
+
}
|
|
1293
|
+
diagnosticsSnapshot() {
|
|
1294
|
+
return this.diagnostics.snapshot();
|
|
1295
|
+
}
|
|
1296
|
+
queueStatus() {
|
|
1297
|
+
return this.queue.status();
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
// ../src/sdk/agent-runtime.ts
|
|
1302
|
+
function detectBrowserStorage() {
|
|
1303
|
+
const maybeStorage = globalThis.localStorage;
|
|
1304
|
+
if (!maybeStorage || typeof maybeStorage !== "object") return null;
|
|
1305
|
+
const candidate = maybeStorage;
|
|
1306
|
+
if (typeof candidate.getItem !== "function" || typeof candidate.setItem !== "function") {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
getItem: candidate.getItem,
|
|
1311
|
+
setItem: candidate.setItem
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function createBindingStore(filePath) {
|
|
1315
|
+
const storage = detectBrowserStorage();
|
|
1316
|
+
if (storage) {
|
|
1317
|
+
const key = "whisper_agent_runtime_bindings";
|
|
1318
|
+
return {
|
|
1319
|
+
async load() {
|
|
1320
|
+
const raw = storage.getItem(key);
|
|
1321
|
+
if (!raw) return {};
|
|
1322
|
+
try {
|
|
1323
|
+
const parsed = JSON.parse(raw);
|
|
1324
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1325
|
+
} catch {
|
|
1326
|
+
return {};
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
async save(bindings) {
|
|
1330
|
+
storage.setItem(key, JSON.stringify(bindings));
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
async load() {
|
|
1336
|
+
if (typeof process === "undefined") return {};
|
|
1337
|
+
const fs = await import("fs/promises");
|
|
1338
|
+
const path = filePath || `${process.env.USERPROFILE || process.env.HOME || "."}/.whisper/sdk/agent-bindings.json`;
|
|
1339
|
+
try {
|
|
1340
|
+
const raw = await fs.readFile(path, "utf8");
|
|
1341
|
+
const parsed = JSON.parse(raw);
|
|
1342
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1343
|
+
} catch {
|
|
1344
|
+
return {};
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
async save(bindings) {
|
|
1348
|
+
if (typeof process === "undefined") return;
|
|
1349
|
+
const fs = await import("fs/promises");
|
|
1350
|
+
const pathMod = await import("path");
|
|
1351
|
+
const path = filePath || `${process.env.USERPROFILE || process.env.HOME || "."}/.whisper/sdk/agent-bindings.json`;
|
|
1352
|
+
await fs.mkdir(pathMod.dirname(path), { recursive: true });
|
|
1353
|
+
await fs.writeFile(path, JSON.stringify(bindings), "utf8");
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function normalizeWorkspacePath(value) {
|
|
1358
|
+
const trimmed = value?.trim();
|
|
1359
|
+
if (!trimmed) return null;
|
|
1360
|
+
return trimmed.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
1361
|
+
}
|
|
1362
|
+
function pathBase(value) {
|
|
1363
|
+
const normalized = value.replace(/\\/g, "/");
|
|
1364
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
1365
|
+
return parts[parts.length - 1] || normalized;
|
|
1366
|
+
}
|
|
1367
|
+
function defaultSalience(kind, success) {
|
|
1368
|
+
if (kind === "decision" || kind === "constraint" || kind === "failure") return "high";
|
|
1369
|
+
if (kind === "outcome" || kind === "task_update") return "medium";
|
|
1370
|
+
if (kind === "tool_result" && success === false) return "high";
|
|
1371
|
+
return "low";
|
|
1372
|
+
}
|
|
1373
|
+
function toMemoryType(kind) {
|
|
1374
|
+
if (kind === "decision" || kind === "constraint") return "instruction";
|
|
1375
|
+
return "event";
|
|
1376
|
+
}
|
|
1377
|
+
function summarizeLowSalience(events) {
|
|
1378
|
+
const lines = events.slice(-10).map((event) => {
|
|
1379
|
+
const fileSuffix = event.filePaths?.length ? ` [files: ${event.filePaths.join(", ")}]` : "";
|
|
1380
|
+
const toolSuffix = event.toolName ? ` [tool: ${event.toolName}]` : "";
|
|
1381
|
+
return `- ${event.kind}: ${event.summary}${fileSuffix}${toolSuffix}`;
|
|
1382
|
+
});
|
|
1383
|
+
return `Recent low-salience work:
|
|
1384
|
+
${lines.join("\n")}`;
|
|
1385
|
+
}
|
|
1386
|
+
function compactWhitespace(value) {
|
|
1387
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1388
|
+
}
|
|
1389
|
+
function normalizeSummary(value) {
|
|
1390
|
+
return compactWhitespace(String(value || "").toLowerCase());
|
|
1391
|
+
}
|
|
1392
|
+
function tokenize(value) {
|
|
1393
|
+
return normalizeSummary(value).split(/[^a-z0-9_./-]+/i).map((token) => token.trim()).filter(Boolean);
|
|
1394
|
+
}
|
|
1395
|
+
function jaccardOverlap(left, right) {
|
|
1396
|
+
const leftTokens = new Set(tokenize(left));
|
|
1397
|
+
const rightTokens = new Set(tokenize(right));
|
|
1398
|
+
if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
|
|
1399
|
+
let intersection = 0;
|
|
1400
|
+
for (const token of leftTokens) {
|
|
1401
|
+
if (rightTokens.has(token)) intersection += 1;
|
|
1402
|
+
}
|
|
1403
|
+
const union = (/* @__PURE__ */ new Set([...leftTokens, ...rightTokens])).size;
|
|
1404
|
+
return union > 0 ? intersection / union : 0;
|
|
1405
|
+
}
|
|
1406
|
+
function clamp01(value) {
|
|
1407
|
+
if (!Number.isFinite(value)) return 0;
|
|
1408
|
+
if (value < 0) return 0;
|
|
1409
|
+
if (value > 1) return 1;
|
|
1410
|
+
return value;
|
|
1411
|
+
}
|
|
1412
|
+
function withTimeout(promise, timeoutMs) {
|
|
1413
|
+
return new Promise((resolve, reject) => {
|
|
1414
|
+
const timeout = setTimeout(() => {
|
|
1415
|
+
reject(new Error("timeout"));
|
|
1416
|
+
}, timeoutMs);
|
|
1417
|
+
promise.then(
|
|
1418
|
+
(value) => {
|
|
1419
|
+
clearTimeout(timeout);
|
|
1420
|
+
resolve(value);
|
|
1421
|
+
},
|
|
1422
|
+
(error) => {
|
|
1423
|
+
clearTimeout(timeout);
|
|
1424
|
+
reject(error);
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
function extractTimestamp(metadata) {
|
|
1430
|
+
const candidates = [
|
|
1431
|
+
metadata?.updatedAt,
|
|
1432
|
+
metadata?.createdAt,
|
|
1433
|
+
metadata?.timestamp,
|
|
1434
|
+
metadata?.event_date,
|
|
1435
|
+
metadata?.eventDate
|
|
1436
|
+
];
|
|
1437
|
+
for (const value of candidates) {
|
|
1438
|
+
if (typeof value === "string") {
|
|
1439
|
+
const parsed = Date.parse(value);
|
|
1440
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return 0;
|
|
1444
|
+
}
|
|
1445
|
+
var DEFAULT_RANK_WEIGHTS = {
|
|
1446
|
+
focusedPassBonus: 0.2,
|
|
1447
|
+
sourceMatchBonus: 0.18,
|
|
1448
|
+
touchedFileBonus: 0.12,
|
|
1449
|
+
clientMatchBonus: 0.1,
|
|
1450
|
+
highSalienceBonus: 0.12,
|
|
1451
|
+
mediumSalienceBonus: 0.06,
|
|
1452
|
+
staleBroadPenalty: -0.1,
|
|
1453
|
+
unrelatedClientPenalty: -0.18,
|
|
1454
|
+
lowSaliencePenalty: -0.12
|
|
1455
|
+
};
|
|
1456
|
+
var DEFAULT_SOURCE_ACTIVITY = {
|
|
1457
|
+
maxTurns: 10,
|
|
1458
|
+
maxIdleMs: 30 * 60 * 1e3,
|
|
1459
|
+
decayAfterTurns: 5,
|
|
1460
|
+
decayAfterIdleMs: 15 * 60 * 1e3,
|
|
1461
|
+
evictOnTaskSwitch: true
|
|
1462
|
+
};
|
|
1463
|
+
var WhisperAgentRuntime = class {
|
|
1464
|
+
constructor(args) {
|
|
1465
|
+
this.args = args;
|
|
1466
|
+
this.bindingStore = createBindingStore(args.options.bindingStorePath);
|
|
1467
|
+
const retrieval = args.options.retrieval || {};
|
|
1468
|
+
this.focusedTopK = retrieval.focusedTopK ?? args.options.topK ?? 6;
|
|
1469
|
+
this.broadTopK = retrieval.broadTopK ?? Math.max(args.options.topK ?? 6, 10);
|
|
1470
|
+
this.maxTokens = args.options.maxTokens ?? 4e3;
|
|
1471
|
+
this.targetRetrievalMs = args.options.targetRetrievalMs ?? 2500;
|
|
1472
|
+
this.hardRetrievalTimeoutMs = args.options.hardRetrievalTimeoutMs ?? 4e3;
|
|
1473
|
+
this.recentWorkLimit = args.options.recentWorkLimit ?? 40;
|
|
1474
|
+
this.baseContext = args.baseContext;
|
|
1475
|
+
this.clientName = args.baseContext.clientName || "whisper-agent-runtime";
|
|
1476
|
+
this.minFocusedResults = retrieval.minFocusedResults ?? 3;
|
|
1477
|
+
this.minFocusedTopScore = retrieval.minFocusedTopScore ?? 0.55;
|
|
1478
|
+
this.minProjectScore = retrieval.minProjectScore ?? 0.5;
|
|
1479
|
+
this.minMemoryScore = retrieval.minMemoryScore ?? 0.6;
|
|
1480
|
+
this.rankWeights = { ...DEFAULT_RANK_WEIGHTS, ...retrieval.rankWeights || {} };
|
|
1481
|
+
this.sourceActivityOptions = { ...DEFAULT_SOURCE_ACTIVITY, ...retrieval.sourceActivity || {} };
|
|
1482
|
+
}
|
|
1483
|
+
bindingStore;
|
|
1484
|
+
focusedTopK;
|
|
1485
|
+
broadTopK;
|
|
1486
|
+
maxTokens;
|
|
1487
|
+
targetRetrievalMs;
|
|
1488
|
+
hardRetrievalTimeoutMs;
|
|
1489
|
+
recentWorkLimit;
|
|
1490
|
+
baseContext;
|
|
1491
|
+
clientName;
|
|
1492
|
+
minFocusedResults;
|
|
1493
|
+
minFocusedTopScore;
|
|
1494
|
+
minProjectScore;
|
|
1495
|
+
minMemoryScore;
|
|
1496
|
+
rankWeights;
|
|
1497
|
+
sourceActivityOptions;
|
|
1498
|
+
bindings = null;
|
|
1499
|
+
touchedFiles = [];
|
|
1500
|
+
recentWork = [];
|
|
1501
|
+
recentSourceActivity = [];
|
|
1502
|
+
bufferedLowSalience = [];
|
|
1503
|
+
lastPreparedTurn = null;
|
|
1504
|
+
mergedCount = 0;
|
|
1505
|
+
droppedCount = 0;
|
|
1506
|
+
focusedPassHits = 0;
|
|
1507
|
+
fallbackTriggers = 0;
|
|
1508
|
+
floorDroppedCount = 0;
|
|
1509
|
+
injectedItemCount = 0;
|
|
1510
|
+
sourceScopedTurns = 0;
|
|
1511
|
+
broadScopedTurns = 0;
|
|
1512
|
+
totalTurns = 0;
|
|
1513
|
+
currentTurn = 0;
|
|
1514
|
+
lastTaskSummary = "";
|
|
1515
|
+
lastScope = {};
|
|
1516
|
+
async getBindings() {
|
|
1517
|
+
if (!this.bindings) {
|
|
1518
|
+
this.bindings = await this.bindingStore.load();
|
|
1519
|
+
}
|
|
1520
|
+
return this.bindings;
|
|
1521
|
+
}
|
|
1522
|
+
pushTouchedFiles(paths) {
|
|
1523
|
+
if (!paths || paths.length === 0) return;
|
|
1524
|
+
for (const path of paths) {
|
|
1525
|
+
if (!path) continue;
|
|
1526
|
+
this.touchedFiles = [...this.touchedFiles.filter((entry) => entry !== path), path].slice(-20);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
pushWorkEvent(event) {
|
|
1530
|
+
this.recentWork = [...this.recentWork, event].slice(-this.recentWorkLimit);
|
|
1531
|
+
}
|
|
1532
|
+
noteSourceActivity(sourceIds) {
|
|
1533
|
+
const now = Date.now();
|
|
1534
|
+
for (const sourceId of [...new Set((sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))]) {
|
|
1535
|
+
this.recentSourceActivity = [
|
|
1536
|
+
...this.recentSourceActivity.filter((entry) => entry.sourceId !== sourceId),
|
|
1537
|
+
{ sourceId, turn: this.currentTurn, at: now }
|
|
1538
|
+
].slice(-24);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
refreshTaskSummary(taskSummary) {
|
|
1542
|
+
const next = normalizeSummary(taskSummary);
|
|
1543
|
+
if (!next) return;
|
|
1544
|
+
if (this.sourceActivityOptions.evictOnTaskSwitch && this.lastTaskSummary && this.lastTaskSummary !== next && jaccardOverlap(this.lastTaskSummary, next) < 0.6) {
|
|
1545
|
+
this.recentSourceActivity = [];
|
|
1546
|
+
}
|
|
1547
|
+
this.lastTaskSummary = next;
|
|
1548
|
+
}
|
|
1549
|
+
activeSourceIds() {
|
|
1550
|
+
const now = Date.now();
|
|
1551
|
+
const active = /* @__PURE__ */ new Map();
|
|
1552
|
+
const maxTurns = this.sourceActivityOptions.maxTurns;
|
|
1553
|
+
const maxIdleMs = this.sourceActivityOptions.maxIdleMs;
|
|
1554
|
+
const decayAfterTurns = this.sourceActivityOptions.decayAfterTurns;
|
|
1555
|
+
const decayAfterIdleMs = this.sourceActivityOptions.decayAfterIdleMs;
|
|
1556
|
+
const fresh = [];
|
|
1557
|
+
for (const entry of this.recentSourceActivity) {
|
|
1558
|
+
const turnDelta = this.currentTurn - entry.turn;
|
|
1559
|
+
const idleDelta = now - entry.at;
|
|
1560
|
+
if (turnDelta > maxTurns || idleDelta > maxIdleMs) continue;
|
|
1561
|
+
fresh.push(entry);
|
|
1562
|
+
let weight = 1;
|
|
1563
|
+
if (turnDelta > decayAfterTurns || idleDelta > decayAfterIdleMs) {
|
|
1564
|
+
weight = 0.5;
|
|
1565
|
+
}
|
|
1566
|
+
const current = active.get(entry.sourceId) || 0;
|
|
1567
|
+
active.set(entry.sourceId, Math.max(current, weight));
|
|
1568
|
+
}
|
|
1569
|
+
this.recentSourceActivity = fresh.slice(-24);
|
|
1570
|
+
return [...active.entries()].sort((left, right) => right[1] - left[1]).map(([sourceId]) => sourceId).slice(0, 4);
|
|
1571
|
+
}
|
|
1572
|
+
focusedScope(input) {
|
|
1573
|
+
const sourceIds = this.activeSourceIds();
|
|
1574
|
+
const fileHints = [...new Set([
|
|
1575
|
+
...input.touchedFiles || [],
|
|
1576
|
+
...this.touchedFiles,
|
|
1577
|
+
...this.recentWork.flatMap((event) => event.filePaths || [])
|
|
1578
|
+
].map((value) => String(value || "").trim()).filter(Boolean))].slice(-4);
|
|
1579
|
+
return {
|
|
1580
|
+
sourceIds,
|
|
1581
|
+
fileHints,
|
|
1582
|
+
clientName: this.clientName || void 0
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
exactFileMetadataFilter(fileHints) {
|
|
1586
|
+
const exact = fileHints.find((value) => /[\\/]/.test(value));
|
|
1587
|
+
if (!exact) return void 0;
|
|
1588
|
+
return { filePath: exact };
|
|
1589
|
+
}
|
|
1590
|
+
makeTaskFrameQuery(input) {
|
|
1591
|
+
const task = compactWhitespace(input.taskSummary || "");
|
|
1592
|
+
const salient = this.recentWork.filter((event) => event.salience === "high").slice(-3).map((event) => `${event.kind}: ${event.summary}`);
|
|
1593
|
+
const files = [...input.touchedFiles || [], ...this.touchedFiles].slice(-3).map((file) => pathBase(file));
|
|
1594
|
+
const parts = [
|
|
1595
|
+
task ? `task ${task}` : "",
|
|
1596
|
+
salient.length > 0 ? `recent ${salient.join(" ; ")}` : "",
|
|
1597
|
+
files.length > 0 ? `files ${files.join(" ")}` : "",
|
|
1598
|
+
input.toolContext ? `tool context ${compactWhitespace(input.toolContext)}` : ""
|
|
1599
|
+
].filter(Boolean);
|
|
1600
|
+
if (parts.length === 0) return null;
|
|
1601
|
+
return parts.join(" | ");
|
|
1602
|
+
}
|
|
1603
|
+
async resolveScope(overrides) {
|
|
1604
|
+
const merged = {
|
|
1605
|
+
...this.baseContext,
|
|
1606
|
+
...overrides
|
|
1607
|
+
};
|
|
1608
|
+
const normalizedWorkspace = normalizeWorkspacePath(merged.workspacePath);
|
|
1609
|
+
const bindings = await this.getBindings();
|
|
1610
|
+
const workspaceProject = normalizedWorkspace ? bindings[normalizedWorkspace] : void 0;
|
|
1611
|
+
const configuredProject = merged.project;
|
|
1612
|
+
let projectRef = configuredProject;
|
|
1613
|
+
let projectSource = overrides?.project ? "explicit" : "generated";
|
|
1614
|
+
let warning;
|
|
1615
|
+
if (workspaceProject) {
|
|
1616
|
+
projectRef = workspaceProject;
|
|
1617
|
+
projectSource = "workspace";
|
|
1618
|
+
if (configuredProject && workspaceProject !== configuredProject) {
|
|
1619
|
+
warning = `workspace mapping '${workspaceProject}' overrides configured project '${configuredProject}'`;
|
|
1620
|
+
}
|
|
1621
|
+
} else if (configuredProject) {
|
|
1622
|
+
projectRef = configuredProject;
|
|
1623
|
+
projectSource = overrides?.project ? "explicit" : "config";
|
|
1624
|
+
}
|
|
1625
|
+
const project = (await this.args.adapter.resolveProject(projectRef)).id;
|
|
1626
|
+
if (normalizedWorkspace) {
|
|
1627
|
+
bindings[normalizedWorkspace] = project;
|
|
1628
|
+
await this.bindingStore.save(bindings);
|
|
1629
|
+
}
|
|
1630
|
+
const scope = {
|
|
1631
|
+
...merged,
|
|
1632
|
+
project,
|
|
1633
|
+
userId: merged.userId || `${this.clientName}-user`,
|
|
1634
|
+
sessionId: merged.sessionId || `sess_${stableHash(`${this.clientName}_${normalizedWorkspace || "default"}`)}`
|
|
1635
|
+
};
|
|
1636
|
+
this.lastScope = {
|
|
1637
|
+
project: scope.project,
|
|
1638
|
+
userId: scope.userId,
|
|
1639
|
+
sessionId: scope.sessionId,
|
|
1640
|
+
source: projectSource,
|
|
1641
|
+
warning
|
|
1642
|
+
};
|
|
1643
|
+
return { scope, projectSource, warning };
|
|
1644
|
+
}
|
|
1645
|
+
async runBranch(name, task) {
|
|
1646
|
+
const startedAt = Date.now();
|
|
1647
|
+
try {
|
|
1648
|
+
const value = await withTimeout(task(), this.hardRetrievalTimeoutMs);
|
|
1649
|
+
return {
|
|
1650
|
+
name,
|
|
1651
|
+
status: "ok",
|
|
1652
|
+
durationMs: Date.now() - startedAt,
|
|
1653
|
+
value
|
|
1654
|
+
};
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
const durationMs = Date.now() - startedAt;
|
|
1657
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1658
|
+
return {
|
|
1659
|
+
name,
|
|
1660
|
+
status: reason === "timeout" ? "timeout" : "error",
|
|
1661
|
+
durationMs,
|
|
1662
|
+
reason
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
contextItems(result, sourceQuery, pass) {
|
|
1667
|
+
const sourceScope = result.meta?.source_scope;
|
|
1668
|
+
if (sourceScope?.mode === "auto" || sourceScope?.mode === "explicit") {
|
|
1669
|
+
this.noteSourceActivity(sourceScope.source_ids || []);
|
|
1670
|
+
}
|
|
1671
|
+
return (result.results || []).map((item) => ({
|
|
1672
|
+
id: item.id,
|
|
1673
|
+
content: item.content,
|
|
1674
|
+
type: "project",
|
|
1675
|
+
score: item.score ?? 0,
|
|
1676
|
+
sourceQuery,
|
|
1677
|
+
pass,
|
|
1678
|
+
metadata: item.metadata || {}
|
|
1679
|
+
}));
|
|
1680
|
+
}
|
|
1681
|
+
memoryItems(result, sourceQuery, pass) {
|
|
1682
|
+
return (result.results || []).map((item, index) => ({
|
|
1683
|
+
id: item.memory?.id || item.chunk?.id || `${sourceQuery}_memory_${index}`,
|
|
1684
|
+
content: item.chunk?.content || item.memory?.content || "",
|
|
1685
|
+
type: "memory",
|
|
1686
|
+
score: item.similarity ?? 0,
|
|
1687
|
+
sourceQuery,
|
|
1688
|
+
pass,
|
|
1689
|
+
metadata: {
|
|
1690
|
+
...item.chunk?.metadata || {},
|
|
1691
|
+
...item.memory?.temporal || {},
|
|
1692
|
+
confidence: item.memory?.confidence
|
|
1693
|
+
}
|
|
1694
|
+
})).filter((item) => item.content);
|
|
1695
|
+
}
|
|
1696
|
+
stableItemKey(item) {
|
|
1697
|
+
const metadata = item.metadata || {};
|
|
1698
|
+
const sourceId = String(metadata.source_id || "");
|
|
1699
|
+
const documentId = String(metadata.document_id || metadata.documentId || "");
|
|
1700
|
+
const chunkId = String(metadata.chunk_id || metadata.chunkId || item.id || "");
|
|
1701
|
+
return stableHash(`${sourceId}|${documentId}|${chunkId}|${item.content.slice(0, 256)}`);
|
|
1702
|
+
}
|
|
1703
|
+
metadataStrings(item) {
|
|
1704
|
+
const metadata = item.metadata || {};
|
|
1705
|
+
return [
|
|
1706
|
+
metadata.filePath,
|
|
1707
|
+
metadata.file_path,
|
|
1708
|
+
metadata.path,
|
|
1709
|
+
metadata.section_path,
|
|
1710
|
+
metadata.parent_section_path,
|
|
1711
|
+
metadata.web_url,
|
|
1712
|
+
metadata.url
|
|
1713
|
+
].map((value) => String(value || "").toLowerCase()).filter(Boolean);
|
|
1714
|
+
}
|
|
1715
|
+
hasSourceMatch(item, scope) {
|
|
1716
|
+
const sourceId = String(item.metadata?.source_id || "");
|
|
1717
|
+
return Boolean(sourceId && scope.sourceIds.includes(sourceId));
|
|
1718
|
+
}
|
|
1719
|
+
hasFileMatch(item, scope) {
|
|
1720
|
+
if (scope.fileHints.length === 0) return false;
|
|
1721
|
+
const metadata = this.metadataStrings(item);
|
|
1722
|
+
const lowerHints = scope.fileHints.map((hint) => hint.toLowerCase());
|
|
1723
|
+
return lowerHints.some((hint) => {
|
|
1724
|
+
const base = pathBase(hint).toLowerCase();
|
|
1725
|
+
return metadata.some((value) => value.includes(hint) || value.endsWith(base));
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
hasClientMatch(item, scope) {
|
|
1729
|
+
const itemClient = String(item.metadata?.client_name || "");
|
|
1730
|
+
return Boolean(scope.clientName && itemClient && itemClient === scope.clientName);
|
|
1731
|
+
}
|
|
1732
|
+
salienceAdjustment(item) {
|
|
1733
|
+
const salience = item.metadata?.salience;
|
|
1734
|
+
if (salience === "high") return this.rankWeights.highSalienceBonus;
|
|
1735
|
+
if (salience === "medium") return this.rankWeights.mediumSalienceBonus;
|
|
1736
|
+
if (salience === "low") return this.rankWeights.lowSaliencePenalty;
|
|
1737
|
+
return 0;
|
|
1738
|
+
}
|
|
1739
|
+
narrowFocusedMemories(items, scope) {
|
|
1740
|
+
const hasSignals = scope.sourceIds.length > 0 || scope.fileHints.length > 0 || Boolean(scope.clientName);
|
|
1741
|
+
if (!hasSignals) return items;
|
|
1742
|
+
const narrowed = items.filter((item) => {
|
|
1743
|
+
const matchesClient = this.hasClientMatch(item, scope);
|
|
1744
|
+
const matchesFile = this.hasFileMatch(item, scope);
|
|
1745
|
+
const matchesSource = this.hasSourceMatch(item, scope);
|
|
1746
|
+
const salience = item.metadata?.salience;
|
|
1747
|
+
if (scope.clientName && item.metadata?.client_name && !matchesClient) {
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
if (salience === "low" && !matchesFile && !matchesSource) {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
return matchesClient || matchesFile || matchesSource || !scope.clientName;
|
|
1754
|
+
});
|
|
1755
|
+
return narrowed.length > 0 ? narrowed : items;
|
|
1756
|
+
}
|
|
1757
|
+
applyRelevanceFloor(items) {
|
|
1758
|
+
const filtered = items.filter(
|
|
1759
|
+
(item) => item.type === "project" ? item.score >= this.minProjectScore : item.score >= this.minMemoryScore
|
|
1760
|
+
);
|
|
1761
|
+
return { items: filtered, dropped: Math.max(0, items.length - filtered.length) };
|
|
1762
|
+
}
|
|
1763
|
+
rerank(items, scope) {
|
|
1764
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
1765
|
+
for (const item of items) {
|
|
1766
|
+
const key = this.stableItemKey(item);
|
|
1767
|
+
const recency = extractTimestamp(item.metadata) > 0 ? 0.04 : 0;
|
|
1768
|
+
const queryBonus = item.sourceQuery === "primary" ? 0.08 : item.sourceQuery === "task_frame" ? 0.04 : 0.03;
|
|
1769
|
+
const sourceMatch = this.hasSourceMatch(item, scope);
|
|
1770
|
+
const fileMatch = this.hasFileMatch(item, scope);
|
|
1771
|
+
const clientMatch = this.hasClientMatch(item, scope);
|
|
1772
|
+
const broadPenalty = item.pass === "broad" && !sourceMatch && !fileMatch && !clientMatch ? this.rankWeights.staleBroadPenalty : 0;
|
|
1773
|
+
const clientPenalty = scope.clientName && item.metadata?.client_name && !clientMatch ? this.rankWeights.unrelatedClientPenalty : 0;
|
|
1774
|
+
const next = {
|
|
1775
|
+
...item,
|
|
1776
|
+
score: clamp01(
|
|
1777
|
+
item.score + queryBonus + recency + (item.pass === "focused" ? this.rankWeights.focusedPassBonus : 0) + (sourceMatch ? this.rankWeights.sourceMatchBonus : 0) + (fileMatch ? this.rankWeights.touchedFileBonus : 0) + (clientMatch ? this.rankWeights.clientMatchBonus : 0) + this.salienceAdjustment(item) + broadPenalty + clientPenalty
|
|
1778
|
+
)
|
|
1779
|
+
};
|
|
1780
|
+
const existing = deduped.get(key);
|
|
1781
|
+
if (!existing || next.score > existing.score) {
|
|
1782
|
+
deduped.set(key, next);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
items: [...deduped.values()].sort((left, right) => right.score - left.score),
|
|
1787
|
+
dedupedCount: Math.max(0, items.length - deduped.size)
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
buildContext(items) {
|
|
1791
|
+
const maxChars = this.maxTokens * 4;
|
|
1792
|
+
const lines = [];
|
|
1793
|
+
let used = 0;
|
|
1794
|
+
for (const item of items) {
|
|
1795
|
+
const label = item.type === "memory" ? "memory" : "context";
|
|
1796
|
+
const content = compactWhitespace(item.content);
|
|
1797
|
+
if (!content) continue;
|
|
1798
|
+
const line = `[${label}] ${content}`;
|
|
1799
|
+
if (used + line.length > maxChars) break;
|
|
1800
|
+
lines.push(line);
|
|
1801
|
+
used += line.length;
|
|
1802
|
+
}
|
|
1803
|
+
if (lines.length === 0) return "";
|
|
1804
|
+
return `Relevant context:
|
|
1805
|
+
${lines.join("\n")}`;
|
|
1806
|
+
}
|
|
1807
|
+
async bootstrap(context = {}) {
|
|
1808
|
+
const { scope, warning } = await this.resolveScope(context);
|
|
1809
|
+
const warnings = warning ? [warning] : [];
|
|
1810
|
+
const startedAt = Date.now();
|
|
1811
|
+
const branches = await Promise.all([
|
|
1812
|
+
this.runBranch("session_recent", () => this.args.adapter.getSessionMemories({
|
|
1813
|
+
project: scope.project,
|
|
1814
|
+
session_id: scope.sessionId,
|
|
1815
|
+
include_pending: true,
|
|
1816
|
+
limit: 12
|
|
1817
|
+
})),
|
|
1818
|
+
this.runBranch("user_profile", () => scope.userId ? this.args.adapter.getUserProfile({
|
|
1819
|
+
project: scope.project,
|
|
1820
|
+
user_id: scope.userId,
|
|
1821
|
+
include_pending: true,
|
|
1822
|
+
memory_types: "preference,instruction,goal"
|
|
1823
|
+
}) : Promise.resolve({ user_id: scope.userId, memories: [], count: 0 })),
|
|
1824
|
+
this.runBranch("project_rules", () => this.args.adapter.query({
|
|
1825
|
+
project: scope.project,
|
|
1826
|
+
query: "project rules instructions constraints conventions open threads",
|
|
1827
|
+
top_k: this.focusedTopK,
|
|
1828
|
+
include_memories: false,
|
|
1829
|
+
user_id: scope.userId,
|
|
1830
|
+
session_id: scope.sessionId,
|
|
1831
|
+
max_tokens: this.maxTokens,
|
|
1832
|
+
compress: true,
|
|
1833
|
+
compression_strategy: "adaptive"
|
|
1834
|
+
}))
|
|
1835
|
+
]);
|
|
1836
|
+
const items = [];
|
|
1837
|
+
const branchStatus = {};
|
|
1838
|
+
for (const branch of branches) {
|
|
1839
|
+
branchStatus[branch.name] = branch.status;
|
|
1840
|
+
if (branch.status !== "ok") {
|
|
1841
|
+
if (branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (branch.name === "project_rules") {
|
|
1845
|
+
items.push(...this.contextItems(branch.value, "bootstrap", "bootstrap"));
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
const records = branch.value.memories || [];
|
|
1849
|
+
items.push(...records.map((memory, index) => ({
|
|
1850
|
+
id: String(memory.id || `${branch.name}_${index}`),
|
|
1851
|
+
content: String(memory.content || ""),
|
|
1852
|
+
type: "memory",
|
|
1853
|
+
score: 0.4,
|
|
1854
|
+
sourceQuery: "bootstrap",
|
|
1855
|
+
pass: "bootstrap",
|
|
1856
|
+
metadata: memory
|
|
1857
|
+
})).filter((item) => item.content));
|
|
1858
|
+
}
|
|
1859
|
+
const reranked = this.rerank(items, { sourceIds: [], fileHints: [], clientName: this.clientName });
|
|
1860
|
+
const ranked = reranked.items.slice(0, this.broadTopK * 2);
|
|
1861
|
+
const prepared = {
|
|
1862
|
+
scope,
|
|
1863
|
+
retrieval: {
|
|
1864
|
+
primaryQuery: "bootstrap",
|
|
1865
|
+
taskFrameQuery: null,
|
|
1866
|
+
warnings,
|
|
1867
|
+
degraded: warnings.length > 0,
|
|
1868
|
+
degradedReason: warnings.length > 0 ? "partial_bootstrap" : void 0,
|
|
1869
|
+
durationMs: Date.now() - startedAt,
|
|
1870
|
+
targetBudgetMs: this.targetRetrievalMs,
|
|
1871
|
+
hardTimeoutMs: this.hardRetrievalTimeoutMs,
|
|
1872
|
+
branchStatus,
|
|
1873
|
+
focusedScopeApplied: false,
|
|
1874
|
+
focusedSourceIds: [],
|
|
1875
|
+
focusedFileHints: [],
|
|
1876
|
+
clientScoped: false,
|
|
1877
|
+
fallbackUsed: false,
|
|
1878
|
+
droppedBelowFloor: 0,
|
|
1879
|
+
dedupedCount: reranked.dedupedCount
|
|
1880
|
+
},
|
|
1881
|
+
context: this.buildContext(ranked),
|
|
1882
|
+
items: ranked
|
|
1883
|
+
};
|
|
1884
|
+
this.lastPreparedTurn = prepared.retrieval;
|
|
1885
|
+
return prepared;
|
|
1886
|
+
}
|
|
1887
|
+
async beforeTurn(input, context = {}) {
|
|
1888
|
+
this.currentTurn += 1;
|
|
1889
|
+
this.pushTouchedFiles(input.touchedFiles);
|
|
1890
|
+
this.refreshTaskSummary(input.taskSummary);
|
|
1891
|
+
const { scope, warning } = await this.resolveScope(context);
|
|
1892
|
+
const primaryQuery = compactWhitespace(input.userMessage);
|
|
1893
|
+
const taskFrameQuery = this.makeTaskFrameQuery(input);
|
|
1894
|
+
const focusedScope = this.focusedScope(input);
|
|
1895
|
+
const focusedMetadataFilter = this.exactFileMetadataFilter(focusedScope.fileHints);
|
|
1896
|
+
const focusedScopeApplied = focusedScope.sourceIds.length > 0 || focusedScope.fileHints.length > 0 || Boolean(focusedScope.clientName);
|
|
1897
|
+
const warnings = warning ? [warning] : [];
|
|
1898
|
+
const startedAt = Date.now();
|
|
1899
|
+
const branchStatus = {};
|
|
1900
|
+
const collectFromBranches = (branches, pass) => {
|
|
1901
|
+
const collected = [];
|
|
1902
|
+
let okCount = 0;
|
|
1903
|
+
for (const branch of branches) {
|
|
1904
|
+
branchStatus[branch.name] = branch.status;
|
|
1905
|
+
if (branch.status !== "ok") {
|
|
1906
|
+
if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
okCount += 1;
|
|
1910
|
+
if (branch.name.startsWith("context")) {
|
|
1911
|
+
collected.push(...this.contextItems(
|
|
1912
|
+
branch.value,
|
|
1913
|
+
branch.name.includes("task_frame") ? "task_frame" : "primary",
|
|
1914
|
+
pass
|
|
1915
|
+
));
|
|
1916
|
+
} else {
|
|
1917
|
+
const memoryItems = this.memoryItems(
|
|
1918
|
+
branch.value,
|
|
1919
|
+
branch.name.includes("task_frame") ? "task_frame" : "primary",
|
|
1920
|
+
pass
|
|
1921
|
+
);
|
|
1922
|
+
collected.push(...pass === "focused" ? this.narrowFocusedMemories(memoryItems, focusedScope) : memoryItems);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return { collected, okCount };
|
|
1926
|
+
};
|
|
1927
|
+
const focusedBranches = await Promise.all([
|
|
1928
|
+
this.runBranch("context_primary_focused", () => this.args.adapter.query({
|
|
1929
|
+
project: scope.project,
|
|
1930
|
+
query: primaryQuery,
|
|
1931
|
+
top_k: this.focusedTopK,
|
|
1932
|
+
include_memories: false,
|
|
1933
|
+
user_id: scope.userId,
|
|
1934
|
+
session_id: scope.sessionId,
|
|
1935
|
+
source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
|
|
1936
|
+
metadata_filter: focusedMetadataFilter,
|
|
1937
|
+
max_tokens: this.maxTokens,
|
|
1938
|
+
compress: true,
|
|
1939
|
+
compression_strategy: "adaptive"
|
|
1940
|
+
})),
|
|
1941
|
+
this.runBranch("memory_primary_focused", () => this.args.adapter.searchMemories({
|
|
1942
|
+
project: scope.project,
|
|
1943
|
+
query: primaryQuery,
|
|
1944
|
+
user_id: scope.userId,
|
|
1945
|
+
session_id: scope.sessionId,
|
|
1946
|
+
top_k: this.focusedTopK,
|
|
1947
|
+
include_pending: true,
|
|
1948
|
+
profile: "balanced"
|
|
1949
|
+
})),
|
|
1950
|
+
taskFrameQuery ? this.runBranch("context_task_frame_focused", () => this.args.adapter.query({
|
|
1951
|
+
project: scope.project,
|
|
1952
|
+
query: taskFrameQuery,
|
|
1953
|
+
top_k: this.focusedTopK,
|
|
1954
|
+
include_memories: false,
|
|
1955
|
+
user_id: scope.userId,
|
|
1956
|
+
session_id: scope.sessionId,
|
|
1957
|
+
source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
|
|
1958
|
+
metadata_filter: focusedMetadataFilter,
|
|
1959
|
+
max_tokens: this.maxTokens,
|
|
1960
|
+
compress: true,
|
|
1961
|
+
compression_strategy: "adaptive"
|
|
1962
|
+
})) : Promise.resolve({ name: "context_task_frame_focused", status: "skipped", durationMs: 0 }),
|
|
1963
|
+
taskFrameQuery ? this.runBranch("memory_task_frame_focused", () => this.args.adapter.searchMemories({
|
|
1964
|
+
project: scope.project,
|
|
1965
|
+
query: taskFrameQuery,
|
|
1966
|
+
user_id: scope.userId,
|
|
1967
|
+
session_id: scope.sessionId,
|
|
1968
|
+
top_k: this.focusedTopK,
|
|
1969
|
+
include_pending: true,
|
|
1970
|
+
profile: "balanced"
|
|
1971
|
+
})) : Promise.resolve({ name: "memory_task_frame_focused", status: "skipped", durationMs: 0 })
|
|
1972
|
+
]);
|
|
1973
|
+
const focusedCollected = collectFromBranches(focusedBranches, "focused");
|
|
1974
|
+
const focusedRanked = this.rerank(focusedCollected.collected, focusedScope);
|
|
1975
|
+
const focusedFloored = this.applyRelevanceFloor(focusedRanked.items);
|
|
1976
|
+
let allCollected = [...focusedFloored.items];
|
|
1977
|
+
let totalOkCount = focusedCollected.okCount;
|
|
1978
|
+
let dedupedCount = focusedRanked.dedupedCount;
|
|
1979
|
+
let droppedBelowFloor = focusedFloored.dropped;
|
|
1980
|
+
const focusedTopScore = focusedFloored.items[0]?.score ?? 0;
|
|
1981
|
+
const fallbackUsed = focusedFloored.items.length < this.minFocusedResults || focusedTopScore < this.minFocusedTopScore;
|
|
1982
|
+
if (focusedScopeApplied) {
|
|
1983
|
+
this.sourceScopedTurns += 1;
|
|
1984
|
+
}
|
|
1985
|
+
if (!fallbackUsed) {
|
|
1986
|
+
this.focusedPassHits += 1;
|
|
1987
|
+
}
|
|
1988
|
+
const broadBranches = fallbackUsed ? await Promise.all([
|
|
1989
|
+
this.runBranch("context_primary_broad", () => this.args.adapter.query({
|
|
1990
|
+
project: scope.project,
|
|
1991
|
+
query: primaryQuery,
|
|
1992
|
+
top_k: this.broadTopK,
|
|
1993
|
+
include_memories: false,
|
|
1994
|
+
user_id: scope.userId,
|
|
1995
|
+
session_id: scope.sessionId,
|
|
1996
|
+
max_tokens: this.maxTokens,
|
|
1997
|
+
compress: true,
|
|
1998
|
+
compression_strategy: "adaptive"
|
|
1999
|
+
})),
|
|
2000
|
+
this.runBranch("memory_primary_broad", () => this.args.adapter.searchMemories({
|
|
2001
|
+
project: scope.project,
|
|
2002
|
+
query: primaryQuery,
|
|
2003
|
+
user_id: scope.userId,
|
|
2004
|
+
session_id: scope.sessionId,
|
|
2005
|
+
top_k: this.broadTopK,
|
|
2006
|
+
include_pending: true,
|
|
2007
|
+
profile: "balanced"
|
|
2008
|
+
})),
|
|
2009
|
+
taskFrameQuery ? this.runBranch("context_task_frame_broad", () => this.args.adapter.query({
|
|
2010
|
+
project: scope.project,
|
|
2011
|
+
query: taskFrameQuery,
|
|
2012
|
+
top_k: this.broadTopK,
|
|
2013
|
+
include_memories: false,
|
|
2014
|
+
user_id: scope.userId,
|
|
2015
|
+
session_id: scope.sessionId,
|
|
2016
|
+
max_tokens: this.maxTokens,
|
|
2017
|
+
compress: true,
|
|
2018
|
+
compression_strategy: "adaptive"
|
|
2019
|
+
})) : Promise.resolve({ name: "context_task_frame_broad", status: "skipped", durationMs: 0 }),
|
|
2020
|
+
taskFrameQuery ? this.runBranch("memory_task_frame_broad", () => this.args.adapter.searchMemories({
|
|
2021
|
+
project: scope.project,
|
|
2022
|
+
query: taskFrameQuery,
|
|
2023
|
+
user_id: scope.userId,
|
|
2024
|
+
session_id: scope.sessionId,
|
|
2025
|
+
top_k: this.broadTopK,
|
|
2026
|
+
include_pending: true,
|
|
2027
|
+
profile: "balanced"
|
|
2028
|
+
})) : Promise.resolve({ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 })
|
|
2029
|
+
]) : [
|
|
2030
|
+
{ name: "context_primary_broad", status: "skipped", durationMs: 0 },
|
|
2031
|
+
{ name: "memory_primary_broad", status: "skipped", durationMs: 0 },
|
|
2032
|
+
{ name: "context_task_frame_broad", status: "skipped", durationMs: 0 },
|
|
2033
|
+
{ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 }
|
|
2034
|
+
];
|
|
2035
|
+
const broadCollected = collectFromBranches(broadBranches, "broad");
|
|
2036
|
+
totalOkCount += broadCollected.okCount;
|
|
2037
|
+
if (fallbackUsed) {
|
|
2038
|
+
this.fallbackTriggers += 1;
|
|
2039
|
+
this.broadScopedTurns += 1;
|
|
2040
|
+
allCollected = [...allCollected, ...broadCollected.collected];
|
|
2041
|
+
}
|
|
2042
|
+
const ranked = this.rerank(allCollected, focusedScope);
|
|
2043
|
+
dedupedCount += ranked.dedupedCount;
|
|
2044
|
+
const floored = this.applyRelevanceFloor(ranked.items);
|
|
2045
|
+
droppedBelowFloor += floored.dropped;
|
|
2046
|
+
this.floorDroppedCount += droppedBelowFloor;
|
|
2047
|
+
this.droppedCount += droppedBelowFloor;
|
|
2048
|
+
const finalItems = floored.items.slice(0, this.broadTopK);
|
|
2049
|
+
this.injectedItemCount += finalItems.length;
|
|
2050
|
+
this.totalTurns += 1;
|
|
2051
|
+
const executedBranches = [...focusedBranches, ...broadBranches].filter((branch) => branch.status !== "skipped");
|
|
2052
|
+
for (const branch of [...focusedBranches, ...broadBranches]) {
|
|
2053
|
+
branchStatus[branch.name] = branch.status;
|
|
2054
|
+
}
|
|
2055
|
+
const prepared = {
|
|
2056
|
+
scope,
|
|
2057
|
+
retrieval: {
|
|
2058
|
+
primaryQuery,
|
|
2059
|
+
taskFrameQuery,
|
|
2060
|
+
warnings,
|
|
2061
|
+
degraded: totalOkCount < executedBranches.length,
|
|
2062
|
+
degradedReason: totalOkCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
|
|
2063
|
+
durationMs: Date.now() - startedAt,
|
|
2064
|
+
targetBudgetMs: this.targetRetrievalMs,
|
|
2065
|
+
hardTimeoutMs: this.hardRetrievalTimeoutMs,
|
|
2066
|
+
branchStatus,
|
|
2067
|
+
focusedScopeApplied,
|
|
2068
|
+
focusedSourceIds: focusedScope.sourceIds,
|
|
2069
|
+
focusedFileHints: focusedScope.fileHints.map((value) => pathBase(value)),
|
|
2070
|
+
clientScoped: Boolean(focusedScope.clientName),
|
|
2071
|
+
fallbackUsed,
|
|
2072
|
+
droppedBelowFloor,
|
|
2073
|
+
dedupedCount
|
|
2074
|
+
},
|
|
2075
|
+
context: this.buildContext(finalItems),
|
|
2076
|
+
items: finalItems
|
|
2077
|
+
};
|
|
2078
|
+
this.lastPreparedTurn = prepared.retrieval;
|
|
2079
|
+
return prepared;
|
|
2080
|
+
}
|
|
2081
|
+
async recordWork(event, context = {}) {
|
|
2082
|
+
const normalized = {
|
|
2083
|
+
...event,
|
|
2084
|
+
salience: event.salience || defaultSalience(event.kind, event.success),
|
|
2085
|
+
timestamp: event.timestamp || nowIso()
|
|
2086
|
+
};
|
|
2087
|
+
this.pushTouchedFiles(normalized.filePaths);
|
|
2088
|
+
this.pushWorkEvent(normalized);
|
|
2089
|
+
if (normalized.salience === "low") {
|
|
2090
|
+
this.bufferedLowSalience = [...this.bufferedLowSalience, normalized].slice(-20);
|
|
2091
|
+
return { success: true, buffered: true };
|
|
2092
|
+
}
|
|
2093
|
+
const { scope } = await this.resolveScope(context);
|
|
2094
|
+
return this.args.adapter.addMemory({
|
|
2095
|
+
project: scope.project,
|
|
2096
|
+
user_id: scope.userId,
|
|
2097
|
+
session_id: scope.sessionId,
|
|
2098
|
+
content: `${normalized.kind}: ${normalized.summary}${normalized.details ? ` (${normalized.details})` : ""}`,
|
|
2099
|
+
memory_type: toMemoryType(normalized.kind),
|
|
2100
|
+
event_date: normalized.timestamp,
|
|
2101
|
+
write_mode: "async",
|
|
2102
|
+
metadata: {
|
|
2103
|
+
runtime_auto: true,
|
|
2104
|
+
client_name: this.clientName,
|
|
2105
|
+
work_event_kind: normalized.kind,
|
|
2106
|
+
salience: normalized.salience,
|
|
2107
|
+
file_paths: normalized.filePaths || [],
|
|
2108
|
+
tool_name: normalized.toolName,
|
|
2109
|
+
success: normalized.success
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
async resolveLearningScope(overrides) {
|
|
2114
|
+
const merged = {
|
|
2115
|
+
...this.baseContext,
|
|
2116
|
+
...overrides
|
|
2117
|
+
};
|
|
2118
|
+
const { scope } = await this.resolveScope(overrides);
|
|
2119
|
+
return {
|
|
2120
|
+
project: scope.project,
|
|
2121
|
+
sessionId: merged.sessionId || scope.sessionId,
|
|
2122
|
+
userId: merged.userId
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
async afterTurn(input, context = {}) {
|
|
2126
|
+
this.pushTouchedFiles(input.touchedFiles);
|
|
2127
|
+
if (input.auto_learn === false) {
|
|
2128
|
+
return {
|
|
2129
|
+
success: true,
|
|
2130
|
+
sessionIngested: false,
|
|
2131
|
+
memoriesCreated: 0,
|
|
2132
|
+
relationsCreated: 0,
|
|
2133
|
+
invalidatedCount: 0,
|
|
2134
|
+
mergedCount: 0,
|
|
2135
|
+
droppedCount: 0,
|
|
2136
|
+
warnings: []
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
const scope = await this.resolveLearningScope(context);
|
|
2140
|
+
const result = await this.args.adapter.ingestSession({
|
|
2141
|
+
project: scope.project,
|
|
2142
|
+
session_id: scope.sessionId,
|
|
2143
|
+
user_id: scope.userId,
|
|
2144
|
+
messages: [
|
|
2145
|
+
{ role: "user", content: input.userMessage, timestamp: nowIso() },
|
|
2146
|
+
{ role: "assistant", content: input.assistantMessage, timestamp: nowIso() }
|
|
2147
|
+
],
|
|
2148
|
+
write_mode: "async"
|
|
2149
|
+
});
|
|
2150
|
+
this.mergedCount += result.memories_invalidated || 0;
|
|
2151
|
+
return {
|
|
2152
|
+
success: Boolean(result.success),
|
|
2153
|
+
sessionIngested: true,
|
|
2154
|
+
memoriesCreated: result.memories_created || 0,
|
|
2155
|
+
relationsCreated: result.relations_created || 0,
|
|
2156
|
+
invalidatedCount: result.memories_invalidated || 0,
|
|
2157
|
+
mergedCount: result.memories_invalidated || 0,
|
|
2158
|
+
droppedCount: 0,
|
|
2159
|
+
warnings: result.errors || []
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
async flush(reason = "manual", context = {}) {
|
|
2163
|
+
if (this.bufferedLowSalience.length > 0) {
|
|
2164
|
+
const { scope } = await this.resolveScope(context);
|
|
2165
|
+
const summary = summarizeLowSalience(this.bufferedLowSalience);
|
|
2166
|
+
await this.args.adapter.addMemory({
|
|
2167
|
+
project: scope.project,
|
|
2168
|
+
user_id: scope.userId,
|
|
2169
|
+
session_id: scope.sessionId,
|
|
2170
|
+
content: summary,
|
|
2171
|
+
memory_type: "event",
|
|
2172
|
+
write_mode: "async",
|
|
2173
|
+
metadata: {
|
|
2174
|
+
runtime_auto: true,
|
|
2175
|
+
client_name: this.clientName,
|
|
2176
|
+
flush_reason: reason,
|
|
2177
|
+
salience: "low",
|
|
2178
|
+
summarized_count: this.bufferedLowSalience.length
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
this.bufferedLowSalience = [];
|
|
2182
|
+
}
|
|
2183
|
+
await this.args.adapter.flushQueue();
|
|
2184
|
+
return this.status();
|
|
2185
|
+
}
|
|
2186
|
+
status() {
|
|
2187
|
+
return {
|
|
2188
|
+
clientName: this.clientName,
|
|
2189
|
+
scope: this.lastScope,
|
|
2190
|
+
queue: this.args.adapter.queueStatus(),
|
|
2191
|
+
retrieval: this.lastPreparedTurn,
|
|
2192
|
+
counters: {
|
|
2193
|
+
mergedCount: this.mergedCount,
|
|
2194
|
+
droppedCount: this.droppedCount,
|
|
2195
|
+
bufferedLowSalience: this.bufferedLowSalience.length,
|
|
2196
|
+
focusedPassHits: this.focusedPassHits,
|
|
2197
|
+
fallbackTriggers: this.fallbackTriggers,
|
|
2198
|
+
floorDroppedCount: this.floorDroppedCount,
|
|
2199
|
+
injectedItemCount: this.injectedItemCount,
|
|
2200
|
+
sourceScopedTurns: this.sourceScopedTurns,
|
|
2201
|
+
broadScopedTurns: this.broadScopedTurns,
|
|
2202
|
+
totalTurns: this.totalTurns
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
// ../src/sdk/whisper.ts
|
|
2209
|
+
var PROJECT_CACHE_TTL_MS = 3e4;
|
|
2210
|
+
var IDENTITY_WARNINGS = /* @__PURE__ */ new Set();
|
|
2211
|
+
function isLikelyProjectId(projectRef) {
|
|
2212
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(projectRef);
|
|
2213
|
+
}
|
|
2214
|
+
function randomRequestId() {
|
|
2215
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2216
|
+
return crypto.randomUUID();
|
|
2217
|
+
}
|
|
2218
|
+
return `req_${Math.random().toString(36).slice(2, 11)}`;
|
|
2219
|
+
}
|
|
2220
|
+
function parseIdentityMode(value) {
|
|
2221
|
+
return value === "app-identity" ? "app-identity" : "demo-local";
|
|
2222
|
+
}
|
|
2223
|
+
function parseEnvironment(value) {
|
|
2224
|
+
if (value === "staging" || value === "production") return value;
|
|
2225
|
+
return "local";
|
|
2226
|
+
}
|
|
2227
|
+
function classifyRuntimeErrorCode(error) {
|
|
2228
|
+
if (error.code === "MISSING_PROJECT") return "MISSING_PROJECT";
|
|
2229
|
+
if (error.code === "TIMEOUT") return "TIMEOUT";
|
|
2230
|
+
if (error.code === "NETWORK_ERROR") return "NETWORK_ERROR";
|
|
2231
|
+
if (error.code === "VALIDATION_ERROR") return "VALIDATION_ERROR";
|
|
2232
|
+
if (error.status === 401 || error.status === 403) return "INVALID_API_KEY";
|
|
2233
|
+
if (error.status === 408) return "TIMEOUT";
|
|
2234
|
+
if (error.status === 429) return "RATE_LIMITED";
|
|
2235
|
+
if (error.status && error.status >= 500) return "TEMPORARY_UNAVAILABLE";
|
|
2236
|
+
if (error.code === "PROJECT_NOT_FOUND" || error.code === "NOT_FOUND") return "PROJECT_NOT_FOUND";
|
|
2237
|
+
return "REQUEST_FAILED";
|
|
2238
|
+
}
|
|
2239
|
+
function detectSourceType(url) {
|
|
2240
|
+
const ghMatch = url.match(/github\.com\/([^/?#]+)\/([^/?#]+)/);
|
|
2241
|
+
if (ghMatch) {
|
|
2242
|
+
const [, owner, repo] = ghMatch;
|
|
2243
|
+
return { type: "github", owner, repo: repo.replace(/\.git$/, "") };
|
|
2244
|
+
}
|
|
2245
|
+
if (/youtube\.com|youtu\.be|loom\.com/.test(url)) return { type: "video" };
|
|
2246
|
+
if (/\.pdf(\?|$)/.test(url)) return { type: "pdf" };
|
|
2247
|
+
return { type: "web" };
|
|
2248
|
+
}
|
|
2249
|
+
var WhisperClient = class _WhisperClient {
|
|
2250
|
+
constructor(config) {
|
|
2251
|
+
this.config = config;
|
|
2252
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
2253
|
+
this.identityMode = parseIdentityMode(config.identityMode || env.WHISPER_IDENTITY_MODE);
|
|
2254
|
+
this.environment = parseEnvironment(config.environment || env.WHISPER_ENV || (env.NODE_ENV === "production" ? "production" : "local"));
|
|
2255
|
+
this.strictIdentityMode = config.strictIdentityMode ?? env.WHISPER_DEMO_LOCAL_STRICT === "true";
|
|
2256
|
+
this.getIdentity = config.getIdentity;
|
|
2257
|
+
this.enforceIdentityModeGuardrail();
|
|
2258
|
+
this.diagnosticsStore = new DiagnosticsStore(config.telemetry?.maxEntries || 1e3);
|
|
2259
|
+
this.runtimeClient = new RuntimeClient(
|
|
2260
|
+
{
|
|
2261
|
+
apiKey: config.apiKey,
|
|
2262
|
+
baseUrl: config.baseUrl,
|
|
2263
|
+
compatMode: config.compatMode || "fallback",
|
|
2264
|
+
timeouts: config.timeouts,
|
|
2265
|
+
retryPolicy: config.retryPolicy,
|
|
2266
|
+
fetchImpl: config.fetch
|
|
2267
|
+
},
|
|
2268
|
+
this.diagnosticsStore
|
|
2269
|
+
);
|
|
2270
|
+
this.searchCache = new SearchResponseCache(
|
|
2271
|
+
config.cache?.ttlMs ?? 7e3,
|
|
2272
|
+
config.cache?.capacity ?? 500
|
|
2273
|
+
);
|
|
2274
|
+
const queueStore = this.createQueueStore(config);
|
|
2275
|
+
this.writeQueue = new WriteQueue({
|
|
2276
|
+
store: queueStore,
|
|
2277
|
+
maxBatchSize: config.queue?.maxBatchSize ?? 50,
|
|
2278
|
+
flushIntervalMs: config.queue?.flushIntervalMs ?? 100,
|
|
2279
|
+
maxAttempts: config.queue?.maxAttempts ?? 2,
|
|
2280
|
+
flushHandler: async (items) => {
|
|
2281
|
+
if (items.length === 0) return;
|
|
2282
|
+
const project = items[0].project;
|
|
2283
|
+
const memories = items.map((item) => ({
|
|
2284
|
+
...item.payload,
|
|
2285
|
+
user_id: item.payload.user_id ?? item.userId,
|
|
2286
|
+
session_id: item.payload.session_id ?? item.sessionId,
|
|
2287
|
+
metadata: {
|
|
2288
|
+
...item.payload.metadata || {},
|
|
2289
|
+
event_id: item.eventId,
|
|
2290
|
+
queued_at: item.createdAt
|
|
2291
|
+
}
|
|
2292
|
+
}));
|
|
2293
|
+
try {
|
|
2294
|
+
await this.runtimeClient.request({
|
|
2295
|
+
endpoint: "/v1/memory/bulk",
|
|
2296
|
+
method: "POST",
|
|
2297
|
+
operation: "bulk",
|
|
2298
|
+
body: {
|
|
2299
|
+
project,
|
|
2300
|
+
write_mode: "async",
|
|
2301
|
+
memories
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
if (this.runtimeClient.getCompatMode() !== "fallback" || !(error instanceof RuntimeClientError) || error.status !== 404) {
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
await Promise.all(
|
|
2309
|
+
memories.map(async (memory) => {
|
|
2310
|
+
try {
|
|
2311
|
+
await this.runtimeClient.request({
|
|
2312
|
+
endpoint: "/v1/memory",
|
|
2313
|
+
method: "POST",
|
|
2314
|
+
operation: "writeAck",
|
|
2315
|
+
body: {
|
|
2316
|
+
project,
|
|
2317
|
+
...memory,
|
|
2318
|
+
write_mode: "sync"
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
} catch (fallbackError) {
|
|
2322
|
+
if (this.runtimeClient.getCompatMode() !== "fallback" || !(fallbackError instanceof RuntimeClientError) || fallbackError.status !== 404) {
|
|
2323
|
+
throw fallbackError;
|
|
2324
|
+
}
|
|
2325
|
+
await this.runtimeClient.request({
|
|
2326
|
+
endpoint: "/v1/memories",
|
|
2327
|
+
method: "POST",
|
|
2328
|
+
operation: "writeAck",
|
|
2329
|
+
body: {
|
|
2330
|
+
project,
|
|
2331
|
+
...memory,
|
|
2332
|
+
memory_type: memory.memory_type === "event" ? "episodic" : memory.memory_type
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
})
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
if (config.queue?.enabled !== false) {
|
|
2342
|
+
void this.writeQueue.start();
|
|
2343
|
+
}
|
|
2344
|
+
this.memoryModule = new MemoryModule(
|
|
2345
|
+
this.runtimeClient,
|
|
2346
|
+
this.searchCache,
|
|
2347
|
+
this.writeQueue,
|
|
2348
|
+
{
|
|
2349
|
+
defaultProject: config.project,
|
|
2350
|
+
cacheEnabled: config.cache?.enabled !== false,
|
|
2351
|
+
queueEnabled: config.queue?.enabled !== false
|
|
2352
|
+
}
|
|
2353
|
+
);
|
|
2354
|
+
this.sessionModule = new SessionModule(this.memoryModule, config.project);
|
|
2355
|
+
this.profileModule = new ProfileModule(this.memoryModule);
|
|
2356
|
+
this.analyticsModule = new AnalyticsModule(this.diagnosticsStore, this.writeQueue);
|
|
2357
|
+
this.diagnostics = {
|
|
2358
|
+
getLast: (limit) => this.diagnosticsStore.getLast(limit),
|
|
2359
|
+
subscribe: (fn) => this.diagnosticsStore.subscribe(fn),
|
|
2360
|
+
snapshot: () => this.diagnosticsStore.snapshot()
|
|
2361
|
+
};
|
|
2362
|
+
this.queue = {
|
|
2363
|
+
flush: () => this.writeQueue.flush(),
|
|
2364
|
+
status: () => this.writeQueue.status()
|
|
2365
|
+
};
|
|
2366
|
+
this.memory = {
|
|
2367
|
+
add: (params) => this.runOrThrow(async () => this.memoryModule.add(await this.withIdentity(params))),
|
|
2368
|
+
addBulk: (params) => this.runOrThrow(async () => this.memoryModule.addBulk({
|
|
2369
|
+
...params,
|
|
2370
|
+
memories: await Promise.all(params.memories.map((memory) => this.withIdentity(memory)))
|
|
2371
|
+
})),
|
|
2372
|
+
search: (params) => this.runOrThrow(async () => this.memoryModule.search(await this.withIdentity(params))),
|
|
2373
|
+
get: (memoryId) => this.runOrThrow(async () => this.memoryModule.get(memoryId)),
|
|
2374
|
+
getUserProfile: (params) => this.runOrThrow(async () => this.profileModule.getUserProfile(await this.withIdentity(params, true))),
|
|
2375
|
+
getSessionMemories: (params) => this.runOrThrow(async () => this.profileModule.getSessionMemories(await this.withIdentity(params))),
|
|
2376
|
+
update: (memoryId, params) => this.runOrThrow(async () => this.memoryModule.update(memoryId, params)),
|
|
2377
|
+
delete: (memoryId) => this.runOrThrow(async () => this.memoryModule.delete(memoryId)),
|
|
2378
|
+
flag: (params) => this.runOrThrow(async () => this.memoryModule.flag(params))
|
|
2379
|
+
};
|
|
2380
|
+
this.session = {
|
|
2381
|
+
start: (params) => this.runOrThrow(async () => this.sessionModule.start(await this.withSessionIdentity(params))),
|
|
2382
|
+
event: (params) => this.runOrThrow(async () => this.sessionModule.event(params)),
|
|
2383
|
+
suspend: (params) => this.runOrThrow(async () => this.sessionModule.suspend(params)),
|
|
2384
|
+
resume: (params) => this.runOrThrow(async () => this.sessionModule.resume(params)),
|
|
2385
|
+
end: (params) => this.runOrThrow(async () => this.sessionModule.end(params))
|
|
2386
|
+
};
|
|
2387
|
+
this.profile = {
|
|
2388
|
+
getUserProfile: (params) => this.runOrThrow(async () => this.profileModule.getUserProfile(await this.withIdentity(params, true))),
|
|
2389
|
+
getSessionMemories: (params) => this.runOrThrow(async () => this.profileModule.getSessionMemories(await this.withIdentity(params)))
|
|
2390
|
+
};
|
|
2391
|
+
this.analytics = {
|
|
2392
|
+
diagnosticsSnapshot: () => this.analyticsModule.diagnosticsSnapshot(),
|
|
2393
|
+
queueStatus: () => this.analyticsModule.queueStatus()
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
diagnostics;
|
|
2397
|
+
queue;
|
|
2398
|
+
memory;
|
|
2399
|
+
session;
|
|
2400
|
+
profile;
|
|
2401
|
+
analytics;
|
|
2402
|
+
runtimeClient;
|
|
2403
|
+
diagnosticsStore;
|
|
2404
|
+
searchCache;
|
|
2405
|
+
writeQueue;
|
|
2406
|
+
memoryModule;
|
|
2407
|
+
sessionModule;
|
|
2408
|
+
profileModule;
|
|
2409
|
+
analyticsModule;
|
|
2410
|
+
projectRefToId = /* @__PURE__ */ new Map();
|
|
2411
|
+
identityMode;
|
|
2412
|
+
environment;
|
|
2413
|
+
strictIdentityMode;
|
|
2414
|
+
getIdentity;
|
|
2415
|
+
projectCache = [];
|
|
2416
|
+
projectCacheExpiresAt = 0;
|
|
2417
|
+
enforceIdentityModeGuardrail(requestId = randomRequestId()) {
|
|
2418
|
+
if (this.identityMode !== "demo-local") return;
|
|
2419
|
+
if (this.environment === "local") return;
|
|
2420
|
+
const message = "[Whisper SDK] WHISPER_IDENTITY_MODE=demo-local is intended only for local development. Switch to app-identity and provide getIdentity() or per-call user_id/session_id.";
|
|
2421
|
+
if (this.strictIdentityMode || this.environment === "production") {
|
|
2422
|
+
throw new WhisperError({
|
|
2423
|
+
code: "MISCONFIGURED_IDENTITY_MODE",
|
|
2424
|
+
message,
|
|
2425
|
+
retryable: false,
|
|
2426
|
+
hint: "Set identityMode: 'app-identity' and provide a getIdentity() function that returns { userId, sessionId? }. To override for testing, set environment: 'local' explicitly."
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
const warningKey = `${this.environment}:${this.identityMode}`;
|
|
2430
|
+
if (!IDENTITY_WARNINGS.has(warningKey)) {
|
|
2431
|
+
IDENTITY_WARNINGS.add(warningKey);
|
|
2432
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
2433
|
+
console.warn(`${message} requestId=${requestId}`);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
toWhisperError(error, hint) {
|
|
2438
|
+
if (error instanceof WhisperError) return error;
|
|
2439
|
+
if (error instanceof RuntimeClientError) {
|
|
2440
|
+
return new WhisperError({
|
|
2441
|
+
code: classifyRuntimeErrorCode(error),
|
|
2442
|
+
message: error.message,
|
|
2443
|
+
status: error.status,
|
|
2444
|
+
retryable: error.retryable,
|
|
2445
|
+
hint: error.hint || hint,
|
|
2446
|
+
requestId: error.requestId || error.traceId,
|
|
2447
|
+
details: error.details,
|
|
2448
|
+
cause: error
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
if (error instanceof Error) {
|
|
2452
|
+
return new WhisperError({
|
|
2453
|
+
code: "REQUEST_FAILED",
|
|
2454
|
+
message: error.message,
|
|
2455
|
+
retryable: false,
|
|
2456
|
+
hint,
|
|
2457
|
+
cause: error
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
return new WhisperError({
|
|
2461
|
+
code: "REQUEST_FAILED",
|
|
2462
|
+
message: "Unknown SDK error",
|
|
2463
|
+
retryable: false,
|
|
2464
|
+
hint,
|
|
2465
|
+
details: error
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
async runOrThrow(work, hint) {
|
|
2469
|
+
try {
|
|
2470
|
+
return await work();
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
throw this.toWhisperError(error, hint);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
async resolveIdentityOverride() {
|
|
2476
|
+
if (!this.getIdentity) return null;
|
|
2477
|
+
const resolved = await this.getIdentity();
|
|
2478
|
+
const userId = String(resolved?.userId || "").trim();
|
|
2479
|
+
const sessionId = resolved?.sessionId ? String(resolved.sessionId).trim() : void 0;
|
|
2480
|
+
if (!userId) {
|
|
2481
|
+
throw new WhisperError({
|
|
2482
|
+
code: "AUTH_IDENTITY_INVALID",
|
|
2483
|
+
message: "getIdentity() returned an invalid identity payload.",
|
|
2484
|
+
retryable: false,
|
|
2485
|
+
hint: "Return { userId, sessionId? } from getIdentity()."
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
return {
|
|
2489
|
+
userId,
|
|
2490
|
+
sessionId: sessionId || void 0
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
async withIdentity(params, requireUser = false) {
|
|
2494
|
+
const currentUser = params.user_id ? String(params.user_id).trim() : "";
|
|
2495
|
+
const currentSession = params.session_id ? String(params.session_id).trim() : "";
|
|
2496
|
+
if (currentUser) {
|
|
2497
|
+
return {
|
|
2498
|
+
...params,
|
|
2499
|
+
user_id: currentUser,
|
|
2500
|
+
session_id: currentSession || params.session_id
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
const resolved = await this.resolveIdentityOverride();
|
|
2504
|
+
if (resolved) {
|
|
2505
|
+
return {
|
|
2506
|
+
...params,
|
|
2507
|
+
user_id: resolved.userId,
|
|
2508
|
+
session_id: currentSession || resolved.sessionId
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
if (requireUser || this.identityMode === "app-identity") {
|
|
2512
|
+
throw new WhisperError({
|
|
2513
|
+
code: "AUTH_IDENTITY_REQUIRED",
|
|
2514
|
+
message: "A user identity is required in app-identity mode.",
|
|
2515
|
+
retryable: false,
|
|
2516
|
+
hint: "Provide user_id/session_id per call or configure getIdentity() in WhisperClient."
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
return params;
|
|
2520
|
+
}
|
|
2521
|
+
async withSessionIdentity(params) {
|
|
2522
|
+
const userId = params.userId ? String(params.userId).trim() : "";
|
|
2523
|
+
const sessionId = params.sessionId ? String(params.sessionId).trim() : "";
|
|
2524
|
+
if (userId) {
|
|
2525
|
+
return {
|
|
2526
|
+
...params,
|
|
2527
|
+
userId,
|
|
2528
|
+
sessionId: sessionId || params.sessionId
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
const resolved = await this.resolveIdentityOverride();
|
|
2532
|
+
if (resolved?.userId) {
|
|
2533
|
+
return {
|
|
2534
|
+
...params,
|
|
2535
|
+
userId: resolved.userId,
|
|
2536
|
+
sessionId: sessionId || resolved.sessionId
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
throw new WhisperError({
|
|
2540
|
+
code: "AUTH_IDENTITY_REQUIRED",
|
|
2541
|
+
message: "Session operations require a user identity.",
|
|
2542
|
+
retryable: false,
|
|
2543
|
+
hint: "Pass userId explicitly or configure getIdentity() in WhisperClient."
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
static fromEnv(overrides = {}) {
|
|
2547
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
2548
|
+
const apiKey = overrides.apiKey || env.WHISPER_API_KEY || env.USEWHISPER_API_KEY || env.API_KEY;
|
|
2549
|
+
if (!apiKey) {
|
|
2550
|
+
throw new WhisperError({
|
|
2551
|
+
code: "INVALID_API_KEY",
|
|
2552
|
+
message: "Missing API key. Set WHISPER_API_KEY / USEWHISPER_API_KEY / API_KEY.",
|
|
2553
|
+
retryable: false
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
return new _WhisperClient({
|
|
2557
|
+
apiKey,
|
|
2558
|
+
baseUrl: overrides.baseUrl || env.WHISPER_BASE_URL || env.API_BASE_URL || "https://context.usewhisper.dev",
|
|
2559
|
+
project: overrides.project || env.WHISPER_PROJECT || env.PROJECT,
|
|
2560
|
+
identityMode: overrides.identityMode || parseIdentityMode(env.WHISPER_IDENTITY_MODE),
|
|
2561
|
+
environment: overrides.environment || parseEnvironment(env.WHISPER_ENV || (env.NODE_ENV === "production" ? "production" : "local")),
|
|
2562
|
+
strictIdentityMode: overrides.strictIdentityMode ?? env.WHISPER_DEMO_LOCAL_STRICT === "true",
|
|
2563
|
+
...overrides
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
createQueueStore(config) {
|
|
2567
|
+
const persistence = config.queue?.persistence || this.defaultQueuePersistence();
|
|
2568
|
+
if (persistence === "storage") {
|
|
2569
|
+
return createStorageQueueStore();
|
|
2570
|
+
}
|
|
2571
|
+
if (persistence === "file") {
|
|
2572
|
+
const filePath = config.queue?.filePath || this.defaultQueueFilePath();
|
|
2573
|
+
if (filePath) {
|
|
2574
|
+
return createFileQueueStore(filePath);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return new InMemoryQueueStore();
|
|
2578
|
+
}
|
|
2579
|
+
defaultQueuePersistence() {
|
|
2580
|
+
const maybeWindow = globalThis.window;
|
|
2581
|
+
if (maybeWindow && typeof maybeWindow === "object") {
|
|
2582
|
+
const maybeStorage = globalThis.localStorage;
|
|
2583
|
+
return maybeStorage && typeof maybeStorage === "object" ? "storage" : "memory";
|
|
2584
|
+
}
|
|
2585
|
+
return "file";
|
|
2586
|
+
}
|
|
2587
|
+
defaultQueueFilePath() {
|
|
2588
|
+
if (typeof process === "undefined") return void 0;
|
|
2589
|
+
const path = process.env.WHISPER_QUEUE_FILE_PATH;
|
|
2590
|
+
if (path) return path;
|
|
2591
|
+
const home = process.env.USERPROFILE || process.env.HOME;
|
|
2592
|
+
if (!home) return void 0;
|
|
2593
|
+
const normalizedHome = home.replace(/[\\\/]+$/, "");
|
|
2594
|
+
return `${normalizedHome}/.whisper/sdk/queue.json`;
|
|
2595
|
+
}
|
|
2596
|
+
getRequiredProject(project) {
|
|
2597
|
+
const resolved = project || this.config.project;
|
|
2598
|
+
if (!resolved) {
|
|
2599
|
+
throw new WhisperError({
|
|
2600
|
+
code: "MISSING_PROJECT",
|
|
2601
|
+
message: "Project is required",
|
|
2602
|
+
retryable: false,
|
|
2603
|
+
hint: "Pass project in the call or configure a default project in WhisperClient."
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
return resolved;
|
|
2607
|
+
}
|
|
2608
|
+
async refreshProjectCache(force = false) {
|
|
2609
|
+
if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
|
|
2610
|
+
return this.projectCache;
|
|
2611
|
+
}
|
|
2612
|
+
const response = await this.runtimeClient.request({
|
|
2613
|
+
endpoint: "/v1/projects",
|
|
2614
|
+
method: "GET",
|
|
2615
|
+
operation: "get",
|
|
2616
|
+
idempotent: true
|
|
2617
|
+
});
|
|
2618
|
+
this.projectRefToId.clear();
|
|
2619
|
+
this.projectCache = response.data?.projects || [];
|
|
2620
|
+
for (const project of this.projectCache) {
|
|
2621
|
+
this.projectRefToId.set(project.id, project.id);
|
|
2622
|
+
this.projectRefToId.set(project.slug, project.id);
|
|
2623
|
+
this.projectRefToId.set(project.name, project.id);
|
|
2624
|
+
}
|
|
2625
|
+
this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
|
|
2626
|
+
return this.projectCache;
|
|
2627
|
+
}
|
|
2628
|
+
async fetchResolvedProject(projectRef) {
|
|
2629
|
+
try {
|
|
2630
|
+
const response = await this.runtimeClient.request({
|
|
2631
|
+
endpoint: `/v1/projects/resolve?project=${encodeURIComponent(projectRef)}`,
|
|
2632
|
+
method: "GET",
|
|
2633
|
+
operation: "get",
|
|
2634
|
+
idempotent: true
|
|
2635
|
+
});
|
|
2636
|
+
return response.data?.resolved || null;
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
if (error instanceof RuntimeClientError && error.status === 404) {
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
throw error;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
async resolveProject(projectRef) {
|
|
2645
|
+
return this.runOrThrow(async () => {
|
|
2646
|
+
const resolvedRef = this.getRequiredProject(projectRef);
|
|
2647
|
+
const cachedProjects = await this.refreshProjectCache(false);
|
|
2648
|
+
const cachedProject = cachedProjects.find(
|
|
2649
|
+
(project) => project.id === resolvedRef || project.slug === resolvedRef || project.name === resolvedRef
|
|
2650
|
+
);
|
|
2651
|
+
if (cachedProject) {
|
|
2652
|
+
return cachedProject;
|
|
2653
|
+
}
|
|
2654
|
+
const resolvedProject = await this.fetchResolvedProject(resolvedRef);
|
|
2655
|
+
if (resolvedProject) {
|
|
2656
|
+
this.projectRefToId.set(resolvedProject.id, resolvedProject.id);
|
|
2657
|
+
this.projectRefToId.set(resolvedProject.slug, resolvedProject.id);
|
|
2658
|
+
this.projectRefToId.set(resolvedProject.name, resolvedProject.id);
|
|
2659
|
+
this.projectCache = [
|
|
2660
|
+
...this.projectCache.filter((project) => project.id !== resolvedProject.id),
|
|
2661
|
+
resolvedProject
|
|
2662
|
+
];
|
|
2663
|
+
this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
|
|
2664
|
+
return resolvedProject;
|
|
2665
|
+
}
|
|
2666
|
+
if (isLikelyProjectId(resolvedRef)) {
|
|
2667
|
+
return {
|
|
2668
|
+
id: resolvedRef,
|
|
2669
|
+
orgId: "",
|
|
2670
|
+
name: resolvedRef,
|
|
2671
|
+
slug: resolvedRef,
|
|
2672
|
+
createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2673
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
throw new WhisperError({
|
|
2677
|
+
code: "PROJECT_NOT_FOUND",
|
|
2678
|
+
message: `Project '${resolvedRef}' not found`,
|
|
2679
|
+
retryable: false
|
|
2680
|
+
});
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
async preflight(options) {
|
|
2684
|
+
const requestId = randomRequestId();
|
|
2685
|
+
const checks = [];
|
|
2686
|
+
try {
|
|
2687
|
+
this.enforceIdentityModeGuardrail(requestId);
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
throw this.toWhisperError(error, "Update identity mode before running preflight.");
|
|
2690
|
+
}
|
|
2691
|
+
const apiKeyOk = typeof this.config.apiKey === "string" && this.config.apiKey.trim().length > 0;
|
|
2692
|
+
checks.push({
|
|
2693
|
+
check: "api_key",
|
|
2694
|
+
ok: apiKeyOk,
|
|
2695
|
+
message: apiKeyOk ? "API key is configured." : "Missing API key.",
|
|
2696
|
+
hint: apiKeyOk ? void 0 : "Set WHISPER_API_KEY or pass apiKey to WhisperClient."
|
|
2697
|
+
});
|
|
2698
|
+
try {
|
|
2699
|
+
await this.runtimeClient.request({
|
|
2700
|
+
endpoint: "/v1/projects",
|
|
2701
|
+
method: "GET",
|
|
2702
|
+
operation: "get",
|
|
2703
|
+
idempotent: true,
|
|
2704
|
+
traceId: requestId
|
|
2705
|
+
});
|
|
2706
|
+
checks.push({
|
|
2707
|
+
check: "api_connectivity",
|
|
2708
|
+
ok: true,
|
|
2709
|
+
message: "Connected to Whisper API."
|
|
2710
|
+
});
|
|
2711
|
+
} catch (error) {
|
|
2712
|
+
const mapped = this.toWhisperError(error, "Confirm WHISPER_BASE_URL and API key permissions.");
|
|
2713
|
+
checks.push({
|
|
2714
|
+
check: "api_connectivity",
|
|
2715
|
+
ok: false,
|
|
2716
|
+
message: mapped.message,
|
|
2717
|
+
hint: mapped.hint
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
const projectRef = options?.project || this.config.project;
|
|
2721
|
+
if (projectRef) {
|
|
2722
|
+
try {
|
|
2723
|
+
await this.resolveProject(projectRef);
|
|
2724
|
+
checks.push({
|
|
2725
|
+
check: "project_access",
|
|
2726
|
+
ok: true,
|
|
2727
|
+
message: `Project '${projectRef}' is reachable.`
|
|
2728
|
+
});
|
|
2729
|
+
} catch (error) {
|
|
2730
|
+
const mapped = this.toWhisperError(error, "Create or grant access to the configured project.");
|
|
2731
|
+
checks.push({
|
|
2732
|
+
check: "project_access",
|
|
2733
|
+
ok: false,
|
|
2734
|
+
message: mapped.message,
|
|
2735
|
+
hint: mapped.hint
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
} else {
|
|
2739
|
+
checks.push({
|
|
2740
|
+
check: "project_access",
|
|
2741
|
+
ok: true,
|
|
2742
|
+
message: "No default project configured (project will be required per call)."
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
if (options?.requireIdentity || this.identityMode === "app-identity") {
|
|
2746
|
+
try {
|
|
2747
|
+
const identity = await this.resolveIdentityOverride();
|
|
2748
|
+
const ok = Boolean(identity?.userId);
|
|
2749
|
+
checks.push({
|
|
2750
|
+
check: "identity_resolution",
|
|
2751
|
+
ok,
|
|
2752
|
+
message: ok ? "Identity resolver is configured." : "Identity resolver is missing.",
|
|
2753
|
+
hint: ok ? void 0 : "Provide getIdentity() or pass user_id/session_id per call."
|
|
2754
|
+
});
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
const mapped = this.toWhisperError(error, "Fix identity resolver output before production usage.");
|
|
2757
|
+
checks.push({
|
|
2758
|
+
check: "identity_resolution",
|
|
2759
|
+
ok: false,
|
|
2760
|
+
message: mapped.message,
|
|
2761
|
+
hint: mapped.hint
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return {
|
|
2766
|
+
ok: checks.every((check) => check.ok),
|
|
2767
|
+
checks,
|
|
2768
|
+
requestId,
|
|
2769
|
+
identityMode: this.identityMode,
|
|
2770
|
+
environment: this.environment
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
async query(params) {
|
|
2774
|
+
return this.runOrThrow(async () => {
|
|
2775
|
+
const { q, userId, ...rest } = params;
|
|
2776
|
+
const normalized = {
|
|
2777
|
+
...rest,
|
|
2778
|
+
query: rest.query ?? q ?? "",
|
|
2779
|
+
user_id: rest.user_id ?? userId,
|
|
2780
|
+
include_memories: rest.include_memories ?? true
|
|
2781
|
+
};
|
|
2782
|
+
const identityParams = await this.withIdentity(normalized);
|
|
2783
|
+
const project = (await this.resolveProject(identityParams.project)).id;
|
|
2784
|
+
const response = await this.runtimeClient.request({
|
|
2785
|
+
endpoint: "/v1/context/query",
|
|
2786
|
+
method: "POST",
|
|
2787
|
+
operation: "search",
|
|
2788
|
+
body: {
|
|
2789
|
+
...identityParams,
|
|
2790
|
+
project
|
|
2791
|
+
},
|
|
2792
|
+
idempotent: true
|
|
2793
|
+
});
|
|
2794
|
+
return response.data;
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
async remember(params) {
|
|
2798
|
+
if (!params.userId && !this.getIdentity) {
|
|
2799
|
+
throw new WhisperError({
|
|
2800
|
+
code: "AUTH_IDENTITY_REQUIRED",
|
|
2801
|
+
message: "remember() requires userId or a getIdentity() resolver on the client.",
|
|
2802
|
+
retryable: false,
|
|
2803
|
+
hint: "Pass userId in the call or configure getIdentity() in WhisperClient."
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
const sessionId = params.sessionId ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `session_${Math.random().toString(36).slice(2, 11)}`);
|
|
2807
|
+
return this.learn({
|
|
2808
|
+
mode: "conversation",
|
|
2809
|
+
project: params.project,
|
|
2810
|
+
user_id: params.userId,
|
|
2811
|
+
session_id: sessionId,
|
|
2812
|
+
messages: params.messages
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
async ingest(params) {
|
|
2816
|
+
const detected = detectSourceType(params.url);
|
|
2817
|
+
return this.learn({
|
|
2818
|
+
mode: "source",
|
|
2819
|
+
project: params.project,
|
|
2820
|
+
type: detected.type,
|
|
2821
|
+
url: params.url,
|
|
2822
|
+
name: params.name,
|
|
2823
|
+
token: params.token,
|
|
2824
|
+
...detected.owner ? { owner: detected.owner } : {},
|
|
2825
|
+
...detected.repo ? { repo: detected.repo } : {}
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
async userProfile(userId) {
|
|
2829
|
+
return this.memory.getUserProfile({ user_id: userId });
|
|
2830
|
+
}
|
|
2831
|
+
async ingestSession(params) {
|
|
2832
|
+
return this.runOrThrow(async () => {
|
|
2833
|
+
const identityParams = await this.withIdentity(params);
|
|
2834
|
+
const project = (await this.resolveProject(identityParams.project)).id;
|
|
2835
|
+
const response = await this.runtimeClient.request({
|
|
2836
|
+
endpoint: "/v1/memory/ingest/session",
|
|
2837
|
+
method: "POST",
|
|
2838
|
+
operation: "session",
|
|
2839
|
+
body: {
|
|
2840
|
+
...identityParams,
|
|
2841
|
+
project
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
return response.data;
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
async learn(params) {
|
|
2848
|
+
return this.runOrThrow(async () => {
|
|
2849
|
+
const identityParams = params.mode === "conversation" ? await this.withIdentity(params) : params;
|
|
2850
|
+
const project = (await this.resolveProject(identityParams.project)).id;
|
|
2851
|
+
const response = await this.runtimeClient.request({
|
|
2852
|
+
endpoint: "/v1/learn",
|
|
2853
|
+
method: "POST",
|
|
2854
|
+
operation: params.mode === "conversation" ? "session" : "bulk",
|
|
2855
|
+
body: {
|
|
2856
|
+
...identityParams,
|
|
2857
|
+
project
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
return response.data;
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
createAgentRuntime(options = {}) {
|
|
2864
|
+
const baseContext = {
|
|
2865
|
+
workspacePath: options.workspacePath,
|
|
2866
|
+
project: options.project || this.config.project,
|
|
2867
|
+
userId: options.userId,
|
|
2868
|
+
sessionId: options.sessionId,
|
|
2869
|
+
traceId: options.traceId,
|
|
2870
|
+
clientName: options.clientName
|
|
2871
|
+
};
|
|
2872
|
+
return new WhisperAgentRuntime({
|
|
2873
|
+
baseContext,
|
|
2874
|
+
options,
|
|
2875
|
+
adapter: {
|
|
2876
|
+
resolveProject: (project) => this.resolveProject(project),
|
|
2877
|
+
query: (params) => this.query(params),
|
|
2878
|
+
ingestSession: (params) => this.ingestSession(params),
|
|
2879
|
+
getSessionMemories: (params) => this.memory.getSessionMemories(params),
|
|
2880
|
+
getUserProfile: (params) => this.memory.getUserProfile(params),
|
|
2881
|
+
searchMemories: (params) => this.memory.search(params),
|
|
2882
|
+
addMemory: (params) => this.memory.add(params),
|
|
2883
|
+
queueStatus: () => this.queue.status(),
|
|
2884
|
+
flushQueue: () => this.queue.flush()
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
withRunContext(context) {
|
|
2889
|
+
const base = this;
|
|
2890
|
+
return {
|
|
2891
|
+
memory: {
|
|
2892
|
+
add: (params) => base.memory.add({
|
|
2893
|
+
...params,
|
|
2894
|
+
project: params.project || context.project || base.config.project,
|
|
2895
|
+
user_id: params.user_id || context.userId,
|
|
2896
|
+
session_id: params.session_id || context.sessionId
|
|
2897
|
+
}),
|
|
2898
|
+
search: (params) => base.memory.search({
|
|
2899
|
+
...params,
|
|
2900
|
+
project: params.project || context.project || base.config.project,
|
|
2901
|
+
user_id: params.user_id || context.userId,
|
|
2902
|
+
session_id: params.session_id || context.sessionId
|
|
2903
|
+
})
|
|
2904
|
+
},
|
|
2905
|
+
session: {
|
|
2906
|
+
event: (params) => base.session.event({
|
|
2907
|
+
...params,
|
|
2908
|
+
sessionId: params.sessionId || context.sessionId || ""
|
|
2909
|
+
})
|
|
2910
|
+
},
|
|
2911
|
+
learn: (params) => base.learn({
|
|
2912
|
+
...params,
|
|
2913
|
+
project: params.project || context.project || base.config.project,
|
|
2914
|
+
...params.mode === "conversation" ? {
|
|
2915
|
+
user_id: params.user_id ?? context.userId,
|
|
2916
|
+
session_id: params.session_id || context.sessionId || ""
|
|
2917
|
+
} : {}
|
|
2918
|
+
}),
|
|
2919
|
+
queue: base.queue,
|
|
2920
|
+
diagnostics: base.diagnostics
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
async deleteSource(sourceId) {
|
|
2924
|
+
return this.runOrThrow(async () => {
|
|
2925
|
+
const response = await this.runtimeClient.request({
|
|
2926
|
+
endpoint: `/v1/sources/${sourceId}`,
|
|
2927
|
+
method: "DELETE",
|
|
2928
|
+
operation: "writeAck"
|
|
2929
|
+
});
|
|
2930
|
+
return response.data;
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
async extractMemories(params) {
|
|
2934
|
+
return this.runOrThrow(async () => {
|
|
2935
|
+
const project = (await this.resolveProject(params.project)).id;
|
|
2936
|
+
const response = await this.runtimeClient.request({
|
|
2937
|
+
endpoint: "/v1/memory/extract",
|
|
2938
|
+
method: "POST",
|
|
2939
|
+
operation: "writeAck",
|
|
2940
|
+
body: { project, message: params.message }
|
|
2941
|
+
});
|
|
2942
|
+
return response.data;
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
async shutdown() {
|
|
2946
|
+
await this.writeQueue.stop();
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
|
|
2950
|
+
// ../src/sdk/router/memory-router.ts
|
|
2951
|
+
function randomRequestId2() {
|
|
2952
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2953
|
+
return crypto.randomUUID();
|
|
2954
|
+
}
|
|
2955
|
+
return `req_${Math.random().toString(36).slice(2, 11)}`;
|
|
2956
|
+
}
|
|
2957
|
+
function normalizeProviderUrl(baseUrl, routePath) {
|
|
2958
|
+
const trimmedBase = baseUrl.replace(/\/+$/, "");
|
|
2959
|
+
const normalizedPath = routePath.startsWith("/") ? routePath : `/${routePath}`;
|
|
2960
|
+
return `${trimmedBase}${normalizedPath}`;
|
|
2961
|
+
}
|
|
2962
|
+
function trimText(value) {
|
|
2963
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2964
|
+
}
|
|
2965
|
+
function extractUserPrompt(messages) {
|
|
2966
|
+
if (!Array.isArray(messages)) return "";
|
|
2967
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
2968
|
+
const item = messages[i];
|
|
2969
|
+
if (!item || typeof item !== "object") continue;
|
|
2970
|
+
if (trimText(item.role).toLowerCase() !== "user") continue;
|
|
2971
|
+
const content = item.content;
|
|
2972
|
+
if (typeof content === "string" && content.trim()) return content.trim();
|
|
2973
|
+
if (Array.isArray(content)) {
|
|
2974
|
+
const text = content.map((part) => {
|
|
2975
|
+
if (typeof part === "string") return part;
|
|
2976
|
+
if (!part || typeof part !== "object") return "";
|
|
2977
|
+
const record = part;
|
|
2978
|
+
if (trimText(record.type).toLowerCase() !== "text") return "";
|
|
2979
|
+
return trimText(record.text);
|
|
2980
|
+
}).filter((part) => part.length > 0).join("\n");
|
|
2981
|
+
if (text) return text;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return "";
|
|
2985
|
+
}
|
|
2986
|
+
function buildClient(config) {
|
|
2987
|
+
if (config.client) return config.client;
|
|
2988
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
2989
|
+
const apiKey = config.apiKey || env.WHISPER_API_KEY || env.USEWHISPER_API_KEY || env.API_KEY;
|
|
2990
|
+
if (!apiKey) {
|
|
2991
|
+
throw new WhisperError({
|
|
2992
|
+
code: "INVALID_API_KEY",
|
|
2993
|
+
message: "Missing API key. Pass apiKey to createMemoryRouter(...) or set WHISPER_API_KEY.",
|
|
2994
|
+
retryable: false
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
return new WhisperClient({
|
|
2998
|
+
apiKey,
|
|
2999
|
+
baseUrl: config.baseUrl,
|
|
3000
|
+
project: config.project,
|
|
3001
|
+
identityMode: config.identityMode,
|
|
3002
|
+
getIdentity: config.getIdentity,
|
|
3003
|
+
environment: config.environment,
|
|
3004
|
+
strictIdentityMode: config.strictIdentityMode,
|
|
3005
|
+
compatMode: config.compatMode,
|
|
3006
|
+
fetch: config.fetch,
|
|
3007
|
+
timeouts: config.timeouts,
|
|
3008
|
+
retryPolicy: config.retryPolicy,
|
|
3009
|
+
cache: config.cache,
|
|
3010
|
+
queue: config.queue,
|
|
3011
|
+
telemetry: config.telemetry
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
function ensureBetaEnabled(_explicitBeta) {
|
|
3015
|
+
}
|
|
3016
|
+
var WhisperMemoryRouter = class {
|
|
3017
|
+
config;
|
|
3018
|
+
providerUrl;
|
|
3019
|
+
fetchImpl;
|
|
3020
|
+
client = null;
|
|
3021
|
+
lastTrace = null;
|
|
3022
|
+
constructor(config) {
|
|
3023
|
+
ensureBetaEnabled(config.beta);
|
|
3024
|
+
this.config = config;
|
|
3025
|
+
this.providerUrl = normalizeProviderUrl(
|
|
3026
|
+
config.providerBaseUrl,
|
|
3027
|
+
config.routePath || "/v1/chat/completions"
|
|
3028
|
+
);
|
|
3029
|
+
this.fetchImpl = config.fetch || fetch;
|
|
3030
|
+
}
|
|
3031
|
+
getClient() {
|
|
3032
|
+
if (!this.client) this.client = buildClient(this.config);
|
|
3033
|
+
return this.client;
|
|
3034
|
+
}
|
|
3035
|
+
getLastTrace() {
|
|
3036
|
+
return this.lastTrace ? { ...this.lastTrace } : null;
|
|
3037
|
+
}
|
|
3038
|
+
async chatCompletions(payload) {
|
|
3039
|
+
const requestId = randomRequestId2();
|
|
3040
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
3041
|
+
const prompt = extractUserPrompt(messages);
|
|
3042
|
+
const contextPrefix = this.config.contextPrefix || "Relevant memory context:";
|
|
3043
|
+
let usedMemory = false;
|
|
3044
|
+
let fallbackReason = "none";
|
|
3045
|
+
let contextText = "";
|
|
3046
|
+
if (!prompt) {
|
|
3047
|
+
fallbackReason = "no_user_prompt";
|
|
3048
|
+
} else {
|
|
3049
|
+
try {
|
|
3050
|
+
const queryResult = await this.getClient().query({
|
|
3051
|
+
query: prompt,
|
|
3052
|
+
project: this.config.project,
|
|
3053
|
+
user_id: trimText(payload.user) || void 0
|
|
3054
|
+
});
|
|
3055
|
+
if (queryResult.context && queryResult.context.trim()) {
|
|
3056
|
+
contextText = queryResult.context.trim();
|
|
3057
|
+
usedMemory = true;
|
|
3058
|
+
}
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
if (this.config.bestEffort === false) {
|
|
3061
|
+
throw error;
|
|
3062
|
+
}
|
|
3063
|
+
fallbackReason = "memory_query_failed";
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const providerPayload = {
|
|
3067
|
+
...payload,
|
|
3068
|
+
messages: usedMemory ? [
|
|
3069
|
+
{
|
|
3070
|
+
role: "system",
|
|
3071
|
+
content: `${contextPrefix}
|
|
3072
|
+
${contextText}`
|
|
3073
|
+
},
|
|
3074
|
+
...messages
|
|
3075
|
+
] : messages
|
|
3076
|
+
};
|
|
3077
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
3078
|
+
const providerApiKey = this.config.providerApiKey || env.MEMORY_ROUTER_PROVIDER_API_KEY || "";
|
|
3079
|
+
const headers = {
|
|
3080
|
+
"content-type": "application/json",
|
|
3081
|
+
"x-whisper-memory-router": "beta",
|
|
3082
|
+
"x-whisper-request-id": requestId,
|
|
3083
|
+
...this.config.providerHeaders || {}
|
|
3084
|
+
};
|
|
3085
|
+
if (providerApiKey) {
|
|
3086
|
+
headers.Authorization = providerApiKey.startsWith("Bearer ") ? providerApiKey : `Bearer ${providerApiKey}`;
|
|
3087
|
+
}
|
|
3088
|
+
const response = await this.fetchImpl(this.providerUrl, {
|
|
3089
|
+
method: "POST",
|
|
3090
|
+
headers,
|
|
3091
|
+
body: JSON.stringify(providerPayload)
|
|
3092
|
+
});
|
|
3093
|
+
const parsed = await response.json().catch(async () => await response.text().catch(() => null));
|
|
3094
|
+
const trace = {
|
|
3095
|
+
providerUrl: this.providerUrl,
|
|
3096
|
+
requestId,
|
|
3097
|
+
usedMemory,
|
|
3098
|
+
fallbackReason,
|
|
3099
|
+
providerStatus: response.status,
|
|
3100
|
+
model: trimText(payload.model) || void 0
|
|
3101
|
+
};
|
|
3102
|
+
this.lastTrace = trace;
|
|
3103
|
+
if (this.config.logger) this.config.logger(trace);
|
|
3104
|
+
if (!response.ok) {
|
|
3105
|
+
throw new WhisperError({
|
|
3106
|
+
code: response.status >= 500 ? "TEMPORARY_UNAVAILABLE" : "REQUEST_FAILED",
|
|
3107
|
+
status: response.status,
|
|
3108
|
+
message: typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error?.message || "Provider request failed") : "Provider request failed",
|
|
3109
|
+
retryable: response.status >= 500 || response.status === 429,
|
|
3110
|
+
requestId,
|
|
3111
|
+
details: {
|
|
3112
|
+
providerUrl: this.providerUrl,
|
|
3113
|
+
body: parsed,
|
|
3114
|
+
trace
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
return {
|
|
3119
|
+
status: response.status,
|
|
3120
|
+
data: parsed,
|
|
3121
|
+
trace
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
function createMemoryRouter(config) {
|
|
3126
|
+
return new WhisperMemoryRouter(config);
|
|
3127
|
+
}
|
|
3128
|
+
export {
|
|
3129
|
+
WhisperMemoryRouter,
|
|
3130
|
+
createMemoryRouter
|
|
3131
|
+
};
|