@usewhisper/sdk 3.5.0 → 3.7.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/index.mjs CHANGED
@@ -379,368 +379,83 @@ var RuntimeClient = class {
379
379
  }
380
380
  };
381
381
 
382
- // ../src/sdk/whisper-agent.ts
383
- var DEPRECATION_WARNINGS = /* @__PURE__ */ new Set();
384
- function warnDeprecatedOnce(key, message) {
385
- if (DEPRECATION_WARNINGS.has(key)) return;
386
- DEPRECATION_WARNINGS.add(key);
387
- if (typeof console !== "undefined" && typeof console.warn === "function") {
388
- console.warn(message);
382
+ // ../src/sdk/core/cache.ts
383
+ var SearchResponseCache = class {
384
+ ttlMs;
385
+ capacity;
386
+ byKey = /* @__PURE__ */ new Map();
387
+ scopeIndex = /* @__PURE__ */ new Map();
388
+ constructor(ttlMs = 7e3, capacity = 500) {
389
+ this.ttlMs = Math.max(1e3, ttlMs);
390
+ this.capacity = Math.max(10, capacity);
389
391
  }
390
- }
391
- var Whisper = class {
392
- client;
393
- options;
394
- sessionId;
395
- userId;
396
- constructor(options) {
397
- if (!options.apiKey) {
398
- throw new Error("API key is required");
399
- }
400
- const clientConfig = {
401
- apiKey: options.apiKey,
402
- baseUrl: options.baseUrl,
403
- project: options.project || "default"
404
- };
405
- if (options.timeoutMs) clientConfig.timeoutMs = options.timeoutMs;
406
- if (options.retry) clientConfig.retry = options.retry;
407
- this.client = new WhisperContext(clientConfig);
408
- warnDeprecatedOnce(
409
- "whisper_agent_wrapper",
410
- "[Whisper SDK] Whisper wrapper is supported for v2 compatibility. Prefer WhisperClient for new integrations."
411
- );
412
- const finalRetry = options.retry || { maxAttempts: 3, baseDelayMs: 250, maxDelayMs: 2e3 };
413
- this.options = {
414
- apiKey: options.apiKey,
415
- baseUrl: options.baseUrl || "https://context.usewhisper.dev",
416
- project: options.project || "default",
417
- timeoutMs: options.timeoutMs || 15e3,
418
- retry: finalRetry,
419
- contextLimit: options.contextLimit ?? 10,
420
- memoryTypes: options.memoryTypes ?? ["factual", "preference", "event", "goal", "relationship", "opinion", "instruction"],
421
- contextPrefix: options.contextPrefix ?? "Relevant context:",
422
- autoExtract: options.autoExtract ?? true,
423
- autoExtractMinConfidence: options.autoExtractMinConfidence ?? 0.65,
424
- maxMemoriesPerCapture: options.maxMemoriesPerCapture ?? 5
425
- };
392
+ makeScopeKey(project, userId, sessionId) {
393
+ return `${project}:${userId || "_"}:${sessionId || "_"}`;
426
394
  }
427
- /**
428
- * Set session ID for conversation tracking
429
- */
430
- session(sessionId) {
431
- this.sessionId = sessionId;
432
- return this;
395
+ makeKey(input) {
396
+ const normalized = {
397
+ project: input.project,
398
+ userId: input.userId || "",
399
+ sessionId: input.sessionId || "",
400
+ query: normalizeQuery(input.query),
401
+ topK: input.topK,
402
+ profile: input.profile,
403
+ includePending: input.includePending
404
+ };
405
+ return `search:${stableHash(JSON.stringify(normalized))}`;
433
406
  }
434
- /**
435
- * Set user ID for user-specific memories
436
- */
437
- user(userId) {
438
- this.userId = userId;
439
- return this;
407
+ get(key) {
408
+ const found = this.byKey.get(key);
409
+ if (!found) return null;
410
+ if (found.expiresAt <= Date.now()) {
411
+ this.deleteByKey(key);
412
+ return null;
413
+ }
414
+ found.touchedAt = Date.now();
415
+ return found.value;
440
416
  }
441
- /**
442
- * Get relevant context BEFORE your LLM call
443
- *
444
- * @param query - What you want to know / user question
445
- * @returns Context string and raw results
446
- *
447
- * @example
448
- * ```typescript
449
- * const { context, results, count } = await whisper.getContext(
450
- * "What are user's preferences?",
451
- * { userId: "user-123" }
452
- * );
453
- *
454
- * // Results: [
455
- * // { content: "User prefers dark mode", type: "preference", score: 0.95 },
456
- * // { content: "Allergic to nuts", type: "factual", score: 0.89 }
457
- * // ]
458
- * ```
459
- */
460
- async getContext(query, options) {
461
- const result = await this.client.query({
462
- project: options?.project ?? this.options.project,
463
- query,
464
- top_k: options?.limit ?? this.options.contextLimit,
465
- include_memories: true,
466
- user_id: options?.userId ?? this.userId,
467
- session_id: options?.sessionId ?? this.sessionId
417
+ set(key, scopeKey, value) {
418
+ this.byKey.set(key, {
419
+ value,
420
+ scopeKey,
421
+ touchedAt: Date.now(),
422
+ expiresAt: Date.now() + this.ttlMs
468
423
  });
469
- const context = result.results.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
470
- return {
471
- context: context ? `${this.options.contextPrefix}
472
- ${context}` : "",
473
- results: result.results,
474
- count: result.meta.total
475
- };
424
+ if (!this.scopeIndex.has(scopeKey)) {
425
+ this.scopeIndex.set(scopeKey, /* @__PURE__ */ new Set());
426
+ }
427
+ this.scopeIndex.get(scopeKey).add(key);
428
+ this.evictIfNeeded();
476
429
  }
477
- /**
478
- * Remember what happened AFTER your LLM response
479
- *
480
- * Fire-and-forget - doesn't block your response
481
- *
482
- * @param content - What your LLM responded with
483
- * @returns Promise that resolves when stored (or fails silently)
484
- *
485
- * @example
486
- * ```typescript
487
- * const llmResponse = "I've set your theme to dark mode and removed nuts from recommendations.";
488
- *
489
- * await whisper.remember(llmResponse, { userId: "user-123" });
490
- * // → Auto-extracts: "theme set to dark mode", "nut allergy"
491
- * // → Stored as preferences
492
- * ```
493
- */
494
- async remember(content, options) {
495
- if (!content || content.length < 5) {
496
- return { success: false };
430
+ invalidateScope(scopeKey) {
431
+ const keys = this.scopeIndex.get(scopeKey);
432
+ if (!keys || keys.size === 0) {
433
+ return 0;
497
434
  }
498
- try {
499
- if (this.options.autoExtract) {
500
- const extraction = await this.client.extractMemories({
501
- project: options?.project ?? this.options.project,
502
- message: content,
503
- user_id: options?.userId ?? this.userId,
504
- session_id: options?.sessionId ?? this.sessionId,
505
- enable_pattern: true,
506
- enable_inference: true,
507
- min_confidence: this.options.autoExtractMinConfidence
508
- });
509
- const extractedMemories = (extraction.all || []).filter((m) => (m.confidence || 0) >= this.options.autoExtractMinConfidence).slice(0, this.options.maxMemoriesPerCapture);
510
- if (extractedMemories.length > 0) {
511
- const bulk = await this.client.addMemoriesBulk({
512
- project: options?.project ?? this.options.project,
513
- write_mode: "async",
514
- memories: extractedMemories.map((m) => ({
515
- content: m.content,
516
- memory_type: m.memoryType,
517
- user_id: options?.userId ?? this.userId,
518
- session_id: options?.sessionId ?? this.sessionId,
519
- importance: Math.max(0.5, Math.min(1, m.confidence || 0.7)),
520
- confidence: m.confidence || 0.7,
521
- entity_mentions: m.entityMentions || [],
522
- event_date: m.eventDate || void 0,
523
- metadata: {
524
- extracted: true,
525
- extraction_method: extraction.extractionMethod,
526
- extraction_reasoning: m.reasoning,
527
- inferred: Boolean(m.inferred)
528
- }
529
- }))
530
- });
531
- const memoryIds = this.extractMemoryIdsFromBulkResponse(bulk);
532
- return {
533
- success: true,
534
- memoryId: memoryIds[0],
535
- memoryIds: memoryIds.length > 0 ? memoryIds : void 0,
536
- extracted: extractedMemories.length
537
- };
538
- }
539
- }
540
- const result = await this.client.addMemory({
541
- project: options?.project ?? this.options.project,
542
- content,
543
- user_id: options?.userId ?? this.userId,
544
- session_id: options?.sessionId ?? this.sessionId
545
- });
546
- return {
547
- success: true,
548
- memoryId: result?.id
549
- };
550
- } catch (error) {
551
- console.error("[Whisper] Remember failed:", error);
552
- return { success: false };
435
+ const toDelete = Array.from(keys);
436
+ for (const key of toDelete) {
437
+ this.deleteByKey(key);
553
438
  }
439
+ this.scopeIndex.delete(scopeKey);
440
+ return toDelete.length;
554
441
  }
555
- /**
556
- * Alias for remember() - same thing
557
- */
558
- async capture(content, options) {
559
- return this.remember(content, options);
442
+ evictIfNeeded() {
443
+ if (this.byKey.size <= this.capacity) return;
444
+ const ordered = Array.from(this.byKey.entries()).sort((a, b) => a[1].touchedAt - b[1].touchedAt);
445
+ const removeCount = this.byKey.size - this.capacity;
446
+ for (let i = 0; i < removeCount; i += 1) {
447
+ this.deleteByKey(ordered[i][0]);
448
+ }
560
449
  }
561
- /**
562
- * Capture from multiple messages (e.g., full conversation)
563
- */
564
- async captureSession(messages, options) {
565
- try {
566
- const result = await this.client.ingestSession({
567
- project: options?.project ?? this.options.project,
568
- session_id: options?.sessionId ?? this.sessionId ?? "default",
569
- user_id: options?.userId ?? this.userId,
570
- messages: messages.filter((m) => m.role !== "system").map((m) => ({
571
- role: m.role,
572
- content: m.content,
573
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
574
- }))
575
- });
576
- return {
577
- success: true,
578
- extracted: result?.memories_created ?? 0
579
- };
580
- } catch (error) {
581
- const fallback = await this.fallbackCaptureViaAddMemory(messages, options);
582
- if (fallback.success) {
583
- return fallback;
584
- }
585
- console.error("[Whisper] Session capture failed:", error);
586
- return { success: false, extracted: 0 };
587
- }
588
- }
589
- /**
590
- * Run a full agent turn with automatic memory read (before) + write (after).
591
- */
592
- async runTurn(params) {
593
- const contextResult = await this.getContext(params.userMessage, {
594
- userId: params.userId,
595
- sessionId: params.sessionId,
596
- project: params.project,
597
- limit: params.limit
598
- });
599
- const prompt = contextResult.context ? `${contextResult.context}
600
-
601
- User: ${params.userMessage}` : params.userMessage;
602
- const response = await params.generate(prompt);
603
- const captureResult = await this.captureSession(
604
- [
605
- { role: "user", content: params.userMessage },
606
- { role: "assistant", content: response }
607
- ],
608
- {
609
- userId: params.userId,
610
- sessionId: params.sessionId,
611
- project: params.project
612
- }
613
- );
614
- return {
615
- response,
616
- context: contextResult.context,
617
- count: contextResult.count,
618
- extracted: captureResult.extracted
619
- };
620
- }
621
- /**
622
- * Direct access to WhisperContext for advanced usage
623
- */
624
- raw() {
625
- return this.client;
626
- }
627
- extractMemoryIdsFromBulkResponse(bulkResponse) {
628
- const ids = [];
629
- if (Array.isArray(bulkResponse?.memories)) {
630
- for (const memory of bulkResponse.memories) {
631
- if (memory?.id) ids.push(memory.id);
632
- }
633
- }
634
- if (bulkResponse?.memory?.id) {
635
- ids.push(bulkResponse.memory.id);
636
- }
637
- if (bulkResponse?.id) {
638
- ids.push(bulkResponse.id);
639
- }
640
- return Array.from(new Set(ids));
641
- }
642
- async fallbackCaptureViaAddMemory(messages, options) {
643
- const userMessages = messages.filter((m) => m.role === "user").map((m) => (m.content || "").trim()).filter((content) => content.length >= 5).slice(-2);
644
- if (userMessages.length === 0) {
645
- return { success: false, extracted: 0 };
646
- }
647
- let extracted = 0;
648
- for (const content of userMessages) {
649
- try {
650
- await this.client.addMemory({
651
- project: options?.project ?? this.options.project,
652
- content,
653
- memory_type: "factual",
654
- user_id: options?.userId ?? this.userId,
655
- session_id: options?.sessionId ?? this.sessionId,
656
- allow_legacy_fallback: true
657
- });
658
- extracted += 1;
659
- } catch {
660
- }
661
- }
662
- return { success: extracted > 0, extracted };
663
- }
664
- };
665
- var whisper_agent_default = Whisper;
666
-
667
- // ../src/sdk/core/cache.ts
668
- var SearchResponseCache = class {
669
- ttlMs;
670
- capacity;
671
- byKey = /* @__PURE__ */ new Map();
672
- scopeIndex = /* @__PURE__ */ new Map();
673
- constructor(ttlMs = 7e3, capacity = 500) {
674
- this.ttlMs = Math.max(1e3, ttlMs);
675
- this.capacity = Math.max(10, capacity);
676
- }
677
- makeScopeKey(project, userId, sessionId) {
678
- return `${project}:${userId || "_"}:${sessionId || "_"}`;
679
- }
680
- makeKey(input) {
681
- const normalized = {
682
- project: input.project,
683
- userId: input.userId || "",
684
- sessionId: input.sessionId || "",
685
- query: normalizeQuery(input.query),
686
- topK: input.topK,
687
- profile: input.profile,
688
- includePending: input.includePending
689
- };
690
- return `search:${stableHash(JSON.stringify(normalized))}`;
691
- }
692
- get(key) {
693
- const found = this.byKey.get(key);
694
- if (!found) return null;
695
- if (found.expiresAt <= Date.now()) {
696
- this.deleteByKey(key);
697
- return null;
698
- }
699
- found.touchedAt = Date.now();
700
- return found.value;
701
- }
702
- set(key, scopeKey, value) {
703
- this.byKey.set(key, {
704
- value,
705
- scopeKey,
706
- touchedAt: Date.now(),
707
- expiresAt: Date.now() + this.ttlMs
708
- });
709
- if (!this.scopeIndex.has(scopeKey)) {
710
- this.scopeIndex.set(scopeKey, /* @__PURE__ */ new Set());
711
- }
712
- this.scopeIndex.get(scopeKey).add(key);
713
- this.evictIfNeeded();
714
- }
715
- invalidateScope(scopeKey) {
716
- const keys = this.scopeIndex.get(scopeKey);
717
- if (!keys || keys.size === 0) {
718
- return 0;
719
- }
720
- const toDelete = Array.from(keys);
721
- for (const key of toDelete) {
722
- this.deleteByKey(key);
723
- }
724
- this.scopeIndex.delete(scopeKey);
725
- return toDelete.length;
726
- }
727
- evictIfNeeded() {
728
- if (this.byKey.size <= this.capacity) return;
729
- const ordered = Array.from(this.byKey.entries()).sort((a, b) => a[1].touchedAt - b[1].touchedAt);
730
- const removeCount = this.byKey.size - this.capacity;
731
- for (let i = 0; i < removeCount; i += 1) {
732
- this.deleteByKey(ordered[i][0]);
733
- }
734
- }
735
- deleteByKey(key) {
736
- const found = this.byKey.get(key);
737
- if (!found) return;
738
- this.byKey.delete(key);
739
- const scopeKeys = this.scopeIndex.get(found.scopeKey);
740
- if (!scopeKeys) return;
741
- scopeKeys.delete(key);
742
- if (scopeKeys.size === 0) {
743
- this.scopeIndex.delete(found.scopeKey);
450
+ deleteByKey(key) {
451
+ const found = this.byKey.get(key);
452
+ if (!found) return;
453
+ this.byKey.delete(key);
454
+ const scopeKeys = this.scopeIndex.get(found.scopeKey);
455
+ if (!scopeKeys) return;
456
+ scopeKeys.delete(key);
457
+ if (scopeKeys.size === 0) {
458
+ this.scopeIndex.delete(found.scopeKey);
744
459
  }
745
460
  }
746
461
  };
@@ -1638,6 +1353,29 @@ ${lines.join("\n")}`;
1638
1353
  function compactWhitespace(value) {
1639
1354
  return value.replace(/\s+/g, " ").trim();
1640
1355
  }
1356
+ function normalizeSummary(value) {
1357
+ return compactWhitespace(String(value || "").toLowerCase());
1358
+ }
1359
+ function tokenize(value) {
1360
+ return normalizeSummary(value).split(/[^a-z0-9_./-]+/i).map((token) => token.trim()).filter(Boolean);
1361
+ }
1362
+ function jaccardOverlap(left, right) {
1363
+ const leftTokens = new Set(tokenize(left));
1364
+ const rightTokens = new Set(tokenize(right));
1365
+ if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
1366
+ let intersection = 0;
1367
+ for (const token of leftTokens) {
1368
+ if (rightTokens.has(token)) intersection += 1;
1369
+ }
1370
+ const union = (/* @__PURE__ */ new Set([...leftTokens, ...rightTokens])).size;
1371
+ return union > 0 ? intersection / union : 0;
1372
+ }
1373
+ function clamp01(value) {
1374
+ if (!Number.isFinite(value)) return 0;
1375
+ if (value < 0) return 0;
1376
+ if (value > 1) return 1;
1377
+ return value;
1378
+ }
1641
1379
  function withTimeout(promise, timeoutMs) {
1642
1380
  return new Promise((resolve, reject) => {
1643
1381
  const timeout = setTimeout(() => {
@@ -1671,39 +1409,76 @@ function extractTimestamp(metadata) {
1671
1409
  }
1672
1410
  return 0;
1673
1411
  }
1674
- function salienceBoost(metadata) {
1675
- const value = metadata?.salience;
1676
- if (value === "high") return 0.12;
1677
- if (value === "medium") return 0.06;
1678
- return 0;
1679
- }
1412
+ var DEFAULT_RANK_WEIGHTS = {
1413
+ focusedPassBonus: 0.2,
1414
+ sourceMatchBonus: 0.18,
1415
+ touchedFileBonus: 0.12,
1416
+ clientMatchBonus: 0.1,
1417
+ highSalienceBonus: 0.12,
1418
+ mediumSalienceBonus: 0.06,
1419
+ staleBroadPenalty: -0.1,
1420
+ unrelatedClientPenalty: -0.18,
1421
+ lowSaliencePenalty: -0.12
1422
+ };
1423
+ var DEFAULT_SOURCE_ACTIVITY = {
1424
+ maxTurns: 10,
1425
+ maxIdleMs: 30 * 60 * 1e3,
1426
+ decayAfterTurns: 5,
1427
+ decayAfterIdleMs: 15 * 60 * 1e3,
1428
+ evictOnTaskSwitch: true
1429
+ };
1680
1430
  var WhisperAgentRuntime = class {
1681
1431
  constructor(args) {
1682
1432
  this.args = args;
1683
1433
  this.bindingStore = createBindingStore(args.options.bindingStorePath);
1684
- this.topK = args.options.topK ?? 6;
1434
+ const retrieval = args.options.retrieval || {};
1435
+ this.focusedTopK = retrieval.focusedTopK ?? args.options.topK ?? 6;
1436
+ this.broadTopK = retrieval.broadTopK ?? Math.max(args.options.topK ?? 6, 10);
1685
1437
  this.maxTokens = args.options.maxTokens ?? 4e3;
1686
1438
  this.targetRetrievalMs = args.options.targetRetrievalMs ?? 2500;
1687
1439
  this.hardRetrievalTimeoutMs = args.options.hardRetrievalTimeoutMs ?? 4e3;
1688
1440
  this.recentWorkLimit = args.options.recentWorkLimit ?? 40;
1689
1441
  this.baseContext = args.baseContext;
1690
1442
  this.clientName = args.baseContext.clientName || "whisper-agent-runtime";
1443
+ this.minFocusedResults = retrieval.minFocusedResults ?? 3;
1444
+ this.minFocusedTopScore = retrieval.minFocusedTopScore ?? 0.55;
1445
+ this.minProjectScore = retrieval.minProjectScore ?? 0.5;
1446
+ this.minMemoryScore = retrieval.minMemoryScore ?? 0.6;
1447
+ this.rankWeights = { ...DEFAULT_RANK_WEIGHTS, ...retrieval.rankWeights || {} };
1448
+ this.sourceActivityOptions = { ...DEFAULT_SOURCE_ACTIVITY, ...retrieval.sourceActivity || {} };
1691
1449
  }
1692
1450
  bindingStore;
1693
- topK;
1451
+ focusedTopK;
1452
+ broadTopK;
1694
1453
  maxTokens;
1695
1454
  targetRetrievalMs;
1696
1455
  hardRetrievalTimeoutMs;
1697
1456
  recentWorkLimit;
1698
1457
  baseContext;
1699
1458
  clientName;
1459
+ minFocusedResults;
1460
+ minFocusedTopScore;
1461
+ minProjectScore;
1462
+ minMemoryScore;
1463
+ rankWeights;
1464
+ sourceActivityOptions;
1700
1465
  bindings = null;
1701
1466
  touchedFiles = [];
1702
1467
  recentWork = [];
1468
+ recentSourceActivity = [];
1703
1469
  bufferedLowSalience = [];
1704
1470
  lastPreparedTurn = null;
1705
1471
  mergedCount = 0;
1706
1472
  droppedCount = 0;
1473
+ focusedPassHits = 0;
1474
+ fallbackTriggers = 0;
1475
+ floorDroppedCount = 0;
1476
+ injectedItemCount = 0;
1477
+ sourceScopedTurns = 0;
1478
+ broadScopedTurns = 0;
1479
+ totalTurns = 0;
1480
+ currentTurn = 0;
1481
+ lastTaskSummary = "";
1707
1482
  lastScope = {};
1708
1483
  async getBindings() {
1709
1484
  if (!this.bindings) {
@@ -1721,6 +1496,64 @@ var WhisperAgentRuntime = class {
1721
1496
  pushWorkEvent(event) {
1722
1497
  this.recentWork = [...this.recentWork, event].slice(-this.recentWorkLimit);
1723
1498
  }
1499
+ noteSourceActivity(sourceIds) {
1500
+ const now = Date.now();
1501
+ for (const sourceId of [...new Set((sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))]) {
1502
+ this.recentSourceActivity = [
1503
+ ...this.recentSourceActivity.filter((entry) => entry.sourceId !== sourceId),
1504
+ { sourceId, turn: this.currentTurn, at: now }
1505
+ ].slice(-24);
1506
+ }
1507
+ }
1508
+ refreshTaskSummary(taskSummary) {
1509
+ const next = normalizeSummary(taskSummary);
1510
+ if (!next) return;
1511
+ if (this.sourceActivityOptions.evictOnTaskSwitch && this.lastTaskSummary && this.lastTaskSummary !== next && jaccardOverlap(this.lastTaskSummary, next) < 0.6) {
1512
+ this.recentSourceActivity = [];
1513
+ }
1514
+ this.lastTaskSummary = next;
1515
+ }
1516
+ activeSourceIds() {
1517
+ const now = Date.now();
1518
+ const active = /* @__PURE__ */ new Map();
1519
+ const maxTurns = this.sourceActivityOptions.maxTurns;
1520
+ const maxIdleMs = this.sourceActivityOptions.maxIdleMs;
1521
+ const decayAfterTurns = this.sourceActivityOptions.decayAfterTurns;
1522
+ const decayAfterIdleMs = this.sourceActivityOptions.decayAfterIdleMs;
1523
+ const fresh = [];
1524
+ for (const entry of this.recentSourceActivity) {
1525
+ const turnDelta = this.currentTurn - entry.turn;
1526
+ const idleDelta = now - entry.at;
1527
+ if (turnDelta > maxTurns || idleDelta > maxIdleMs) continue;
1528
+ fresh.push(entry);
1529
+ let weight = 1;
1530
+ if (turnDelta > decayAfterTurns || idleDelta > decayAfterIdleMs) {
1531
+ weight = 0.5;
1532
+ }
1533
+ const current = active.get(entry.sourceId) || 0;
1534
+ active.set(entry.sourceId, Math.max(current, weight));
1535
+ }
1536
+ this.recentSourceActivity = fresh.slice(-24);
1537
+ return [...active.entries()].sort((left, right) => right[1] - left[1]).map(([sourceId]) => sourceId).slice(0, 4);
1538
+ }
1539
+ focusedScope(input) {
1540
+ const sourceIds = this.activeSourceIds();
1541
+ const fileHints = [...new Set([
1542
+ ...input.touchedFiles || [],
1543
+ ...this.touchedFiles,
1544
+ ...this.recentWork.flatMap((event) => event.filePaths || [])
1545
+ ].map((value) => String(value || "").trim()).filter(Boolean))].slice(-4);
1546
+ return {
1547
+ sourceIds,
1548
+ fileHints,
1549
+ clientName: this.clientName || void 0
1550
+ };
1551
+ }
1552
+ exactFileMetadataFilter(fileHints) {
1553
+ const exact = fileHints.find((value) => /[\\/]/.test(value));
1554
+ if (!exact) return void 0;
1555
+ return { filePath: exact };
1556
+ }
1724
1557
  makeTaskFrameQuery(input) {
1725
1558
  const task = compactWhitespace(input.taskSummary || "");
1726
1559
  const salient = this.recentWork.filter((event) => event.salience === "high").slice(-3).map((event) => `${event.kind}: ${event.summary}`);
@@ -1797,23 +1630,29 @@ var WhisperAgentRuntime = class {
1797
1630
  };
1798
1631
  }
1799
1632
  }
1800
- contextItems(result, sourceQuery) {
1633
+ contextItems(result, sourceQuery, pass) {
1634
+ const sourceScope = result.meta?.source_scope;
1635
+ if (sourceScope?.mode === "auto" || sourceScope?.mode === "explicit") {
1636
+ this.noteSourceActivity(sourceScope.source_ids || []);
1637
+ }
1801
1638
  return (result.results || []).map((item) => ({
1802
1639
  id: item.id,
1803
1640
  content: item.content,
1804
1641
  type: "project",
1805
1642
  score: item.score ?? 0,
1806
1643
  sourceQuery,
1644
+ pass,
1807
1645
  metadata: item.metadata || {}
1808
1646
  }));
1809
1647
  }
1810
- memoryItems(result, sourceQuery) {
1648
+ memoryItems(result, sourceQuery, pass) {
1811
1649
  return (result.results || []).map((item, index) => ({
1812
1650
  id: item.memory?.id || item.chunk?.id || `${sourceQuery}_memory_${index}`,
1813
1651
  content: item.chunk?.content || item.memory?.content || "",
1814
1652
  type: "memory",
1815
1653
  score: item.similarity ?? 0,
1816
1654
  sourceQuery,
1655
+ pass,
1817
1656
  metadata: {
1818
1657
  ...item.chunk?.metadata || {},
1819
1658
  ...item.memory?.temporal || {},
@@ -1821,22 +1660,99 @@ var WhisperAgentRuntime = class {
1821
1660
  }
1822
1661
  })).filter((item) => item.content);
1823
1662
  }
1824
- rerank(items) {
1663
+ stableItemKey(item) {
1664
+ const metadata = item.metadata || {};
1665
+ const sourceId = String(metadata.source_id || "");
1666
+ const documentId = String(metadata.document_id || metadata.documentId || "");
1667
+ const chunkId = String(metadata.chunk_id || metadata.chunkId || item.id || "");
1668
+ return stableHash(`${sourceId}|${documentId}|${chunkId}|${item.content.slice(0, 256)}`);
1669
+ }
1670
+ metadataStrings(item) {
1671
+ const metadata = item.metadata || {};
1672
+ return [
1673
+ metadata.filePath,
1674
+ metadata.file_path,
1675
+ metadata.path,
1676
+ metadata.section_path,
1677
+ metadata.parent_section_path,
1678
+ metadata.web_url,
1679
+ metadata.url
1680
+ ].map((value) => String(value || "").toLowerCase()).filter(Boolean);
1681
+ }
1682
+ hasSourceMatch(item, scope) {
1683
+ const sourceId = String(item.metadata?.source_id || "");
1684
+ return Boolean(sourceId && scope.sourceIds.includes(sourceId));
1685
+ }
1686
+ hasFileMatch(item, scope) {
1687
+ if (scope.fileHints.length === 0) return false;
1688
+ const metadata = this.metadataStrings(item);
1689
+ const lowerHints = scope.fileHints.map((hint) => hint.toLowerCase());
1690
+ return lowerHints.some((hint) => {
1691
+ const base = pathBase(hint).toLowerCase();
1692
+ return metadata.some((value) => value.includes(hint) || value.endsWith(base));
1693
+ });
1694
+ }
1695
+ hasClientMatch(item, scope) {
1696
+ const itemClient = String(item.metadata?.client_name || "");
1697
+ return Boolean(scope.clientName && itemClient && itemClient === scope.clientName);
1698
+ }
1699
+ salienceAdjustment(item) {
1700
+ const salience = item.metadata?.salience;
1701
+ if (salience === "high") return this.rankWeights.highSalienceBonus;
1702
+ if (salience === "medium") return this.rankWeights.mediumSalienceBonus;
1703
+ if (salience === "low") return this.rankWeights.lowSaliencePenalty;
1704
+ return 0;
1705
+ }
1706
+ narrowFocusedMemories(items, scope) {
1707
+ const hasSignals = scope.sourceIds.length > 0 || scope.fileHints.length > 0 || Boolean(scope.clientName);
1708
+ if (!hasSignals) return items;
1709
+ const narrowed = items.filter((item) => {
1710
+ const matchesClient = this.hasClientMatch(item, scope);
1711
+ const matchesFile = this.hasFileMatch(item, scope);
1712
+ const matchesSource = this.hasSourceMatch(item, scope);
1713
+ const salience = item.metadata?.salience;
1714
+ if (scope.clientName && item.metadata?.client_name && !matchesClient) {
1715
+ return false;
1716
+ }
1717
+ if (salience === "low" && !matchesFile && !matchesSource) {
1718
+ return false;
1719
+ }
1720
+ return matchesClient || matchesFile || matchesSource || !scope.clientName;
1721
+ });
1722
+ return narrowed.length > 0 ? narrowed : items;
1723
+ }
1724
+ applyRelevanceFloor(items) {
1725
+ const filtered = items.filter(
1726
+ (item) => item.type === "project" ? item.score >= this.minProjectScore : item.score >= this.minMemoryScore
1727
+ );
1728
+ return { items: filtered, dropped: Math.max(0, items.length - filtered.length) };
1729
+ }
1730
+ rerank(items, scope) {
1825
1731
  const deduped = /* @__PURE__ */ new Map();
1826
1732
  for (const item of items) {
1827
- const key = `${item.id}:${item.content.toLowerCase()}`;
1733
+ const key = this.stableItemKey(item);
1828
1734
  const recency = extractTimestamp(item.metadata) > 0 ? 0.04 : 0;
1829
1735
  const queryBonus = item.sourceQuery === "primary" ? 0.08 : item.sourceQuery === "task_frame" ? 0.04 : 0.03;
1736
+ const sourceMatch = this.hasSourceMatch(item, scope);
1737
+ const fileMatch = this.hasFileMatch(item, scope);
1738
+ const clientMatch = this.hasClientMatch(item, scope);
1739
+ const broadPenalty = item.pass === "broad" && !sourceMatch && !fileMatch && !clientMatch ? this.rankWeights.staleBroadPenalty : 0;
1740
+ const clientPenalty = scope.clientName && item.metadata?.client_name && !clientMatch ? this.rankWeights.unrelatedClientPenalty : 0;
1830
1741
  const next = {
1831
1742
  ...item,
1832
- score: item.score + queryBonus + salienceBoost(item.metadata) + recency
1743
+ score: clamp01(
1744
+ 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
1745
+ )
1833
1746
  };
1834
1747
  const existing = deduped.get(key);
1835
1748
  if (!existing || next.score > existing.score) {
1836
1749
  deduped.set(key, next);
1837
1750
  }
1838
1751
  }
1839
- return [...deduped.values()].sort((left, right) => right.score - left.score);
1752
+ return {
1753
+ items: [...deduped.values()].sort((left, right) => right.score - left.score),
1754
+ dedupedCount: Math.max(0, items.length - deduped.size)
1755
+ };
1840
1756
  }
1841
1757
  buildContext(items) {
1842
1758
  const maxChars = this.maxTokens * 4;
@@ -1875,7 +1791,7 @@ ${lines.join("\n")}`;
1875
1791
  this.runBranch("project_rules", () => this.args.adapter.query({
1876
1792
  project: scope.project,
1877
1793
  query: "project rules instructions constraints conventions open threads",
1878
- top_k: this.topK,
1794
+ top_k: this.focusedTopK,
1879
1795
  include_memories: false,
1880
1796
  user_id: scope.userId,
1881
1797
  session_id: scope.sessionId,
@@ -1893,7 +1809,7 @@ ${lines.join("\n")}`;
1893
1809
  continue;
1894
1810
  }
1895
1811
  if (branch.name === "project_rules") {
1896
- items.push(...this.contextItems(branch.value, "bootstrap"));
1812
+ items.push(...this.contextItems(branch.value, "bootstrap", "bootstrap"));
1897
1813
  continue;
1898
1814
  }
1899
1815
  const records = branch.value.memories || [];
@@ -1903,10 +1819,12 @@ ${lines.join("\n")}`;
1903
1819
  type: "memory",
1904
1820
  score: 0.4,
1905
1821
  sourceQuery: "bootstrap",
1822
+ pass: "bootstrap",
1906
1823
  metadata: memory
1907
1824
  })).filter((item) => item.content));
1908
1825
  }
1909
- const ranked = this.rerank(items).slice(0, this.topK * 2);
1826
+ const reranked = this.rerank(items, { sourceIds: [], fileHints: [], clientName: this.clientName });
1827
+ const ranked = reranked.items.slice(0, this.broadTopK * 2);
1910
1828
  const prepared = {
1911
1829
  scope,
1912
1830
  retrieval: {
@@ -1918,7 +1836,14 @@ ${lines.join("\n")}`;
1918
1836
  durationMs: Date.now() - startedAt,
1919
1837
  targetBudgetMs: this.targetRetrievalMs,
1920
1838
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1921
- branchStatus
1839
+ branchStatus,
1840
+ focusedScopeApplied: false,
1841
+ focusedSourceIds: [],
1842
+ focusedFileHints: [],
1843
+ clientScoped: false,
1844
+ fallbackUsed: false,
1845
+ droppedBelowFloor: 0,
1846
+ dedupedCount: reranked.dedupedCount
1922
1847
  },
1923
1848
  context: this.buildContext(ranked),
1924
1849
  items: ranked
@@ -1927,100 +1852,195 @@ ${lines.join("\n")}`;
1927
1852
  return prepared;
1928
1853
  }
1929
1854
  async beforeTurn(input, context = {}) {
1855
+ this.currentTurn += 1;
1930
1856
  this.pushTouchedFiles(input.touchedFiles);
1857
+ this.refreshTaskSummary(input.taskSummary);
1931
1858
  const { scope, warning } = await this.resolveScope(context);
1932
1859
  const primaryQuery = compactWhitespace(input.userMessage);
1933
1860
  const taskFrameQuery = this.makeTaskFrameQuery(input);
1861
+ const focusedScope = this.focusedScope(input);
1862
+ const focusedMetadataFilter = this.exactFileMetadataFilter(focusedScope.fileHints);
1863
+ const focusedScopeApplied = focusedScope.sourceIds.length > 0 || focusedScope.fileHints.length > 0 || Boolean(focusedScope.clientName);
1934
1864
  const warnings = warning ? [warning] : [];
1935
1865
  const startedAt = Date.now();
1936
- const branches = await Promise.all([
1937
- this.runBranch("context_primary", () => this.args.adapter.query({
1866
+ const branchStatus = {};
1867
+ const collectFromBranches = (branches, pass) => {
1868
+ const collected = [];
1869
+ let okCount = 0;
1870
+ for (const branch of branches) {
1871
+ branchStatus[branch.name] = branch.status;
1872
+ if (branch.status !== "ok") {
1873
+ if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1874
+ continue;
1875
+ }
1876
+ okCount += 1;
1877
+ if (branch.name.startsWith("context")) {
1878
+ collected.push(...this.contextItems(
1879
+ branch.value,
1880
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1881
+ pass
1882
+ ));
1883
+ } else {
1884
+ const memoryItems = this.memoryItems(
1885
+ branch.value,
1886
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1887
+ pass
1888
+ );
1889
+ collected.push(...pass === "focused" ? this.narrowFocusedMemories(memoryItems, focusedScope) : memoryItems);
1890
+ }
1891
+ }
1892
+ return { collected, okCount };
1893
+ };
1894
+ const focusedBranches = await Promise.all([
1895
+ this.runBranch("context_primary_focused", () => this.args.adapter.query({
1938
1896
  project: scope.project,
1939
1897
  query: primaryQuery,
1940
- top_k: this.topK,
1898
+ top_k: this.focusedTopK,
1941
1899
  include_memories: false,
1942
1900
  user_id: scope.userId,
1943
1901
  session_id: scope.sessionId,
1902
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1903
+ metadata_filter: focusedMetadataFilter,
1944
1904
  max_tokens: this.maxTokens,
1945
1905
  compress: true,
1946
1906
  compression_strategy: "adaptive"
1947
1907
  })),
1948
- this.runBranch("memory_primary", () => this.args.adapter.searchMemories({
1908
+ this.runBranch("memory_primary_focused", () => this.args.adapter.searchMemories({
1949
1909
  project: scope.project,
1950
1910
  query: primaryQuery,
1951
1911
  user_id: scope.userId,
1952
1912
  session_id: scope.sessionId,
1953
- top_k: this.topK,
1913
+ top_k: this.focusedTopK,
1954
1914
  include_pending: true,
1955
1915
  profile: "balanced"
1956
1916
  })),
1957
- taskFrameQuery ? this.runBranch("context_task_frame", () => this.args.adapter.query({
1917
+ taskFrameQuery ? this.runBranch("context_task_frame_focused", () => this.args.adapter.query({
1958
1918
  project: scope.project,
1959
1919
  query: taskFrameQuery,
1960
- top_k: this.topK,
1920
+ top_k: this.focusedTopK,
1961
1921
  include_memories: false,
1962
1922
  user_id: scope.userId,
1963
1923
  session_id: scope.sessionId,
1924
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1925
+ metadata_filter: focusedMetadataFilter,
1964
1926
  max_tokens: this.maxTokens,
1965
1927
  compress: true,
1966
1928
  compression_strategy: "adaptive"
1967
- })) : Promise.resolve({
1968
- name: "context_task_frame",
1969
- status: "skipped",
1970
- durationMs: 0
1971
- }),
1972
- taskFrameQuery ? this.runBranch("memory_task_frame", () => this.args.adapter.searchMemories({
1929
+ })) : Promise.resolve({ name: "context_task_frame_focused", status: "skipped", durationMs: 0 }),
1930
+ taskFrameQuery ? this.runBranch("memory_task_frame_focused", () => this.args.adapter.searchMemories({
1973
1931
  project: scope.project,
1974
1932
  query: taskFrameQuery,
1975
1933
  user_id: scope.userId,
1976
1934
  session_id: scope.sessionId,
1977
- top_k: this.topK,
1935
+ top_k: this.focusedTopK,
1978
1936
  include_pending: true,
1979
1937
  profile: "balanced"
1980
- })) : Promise.resolve({
1981
- name: "memory_task_frame",
1982
- status: "skipped",
1983
- durationMs: 0
1984
- })
1938
+ })) : Promise.resolve({ name: "memory_task_frame_focused", status: "skipped", durationMs: 0 })
1985
1939
  ]);
1986
- const branchStatus = {};
1987
- const collected = [];
1988
- let okCount = 0;
1989
- for (const branch of branches) {
1940
+ const focusedCollected = collectFromBranches(focusedBranches, "focused");
1941
+ const focusedRanked = this.rerank(focusedCollected.collected, focusedScope);
1942
+ const focusedFloored = this.applyRelevanceFloor(focusedRanked.items);
1943
+ let allCollected = [...focusedFloored.items];
1944
+ let totalOkCount = focusedCollected.okCount;
1945
+ let dedupedCount = focusedRanked.dedupedCount;
1946
+ let droppedBelowFloor = focusedFloored.dropped;
1947
+ const focusedTopScore = focusedFloored.items[0]?.score ?? 0;
1948
+ const fallbackUsed = focusedFloored.items.length < this.minFocusedResults || focusedTopScore < this.minFocusedTopScore;
1949
+ if (focusedScopeApplied) {
1950
+ this.sourceScopedTurns += 1;
1951
+ }
1952
+ if (!fallbackUsed) {
1953
+ this.focusedPassHits += 1;
1954
+ }
1955
+ const broadBranches = fallbackUsed ? await Promise.all([
1956
+ this.runBranch("context_primary_broad", () => this.args.adapter.query({
1957
+ project: scope.project,
1958
+ query: primaryQuery,
1959
+ top_k: this.broadTopK,
1960
+ include_memories: false,
1961
+ user_id: scope.userId,
1962
+ session_id: scope.sessionId,
1963
+ max_tokens: this.maxTokens,
1964
+ compress: true,
1965
+ compression_strategy: "adaptive"
1966
+ })),
1967
+ this.runBranch("memory_primary_broad", () => this.args.adapter.searchMemories({
1968
+ project: scope.project,
1969
+ query: primaryQuery,
1970
+ user_id: scope.userId,
1971
+ session_id: scope.sessionId,
1972
+ top_k: this.broadTopK,
1973
+ include_pending: true,
1974
+ profile: "balanced"
1975
+ })),
1976
+ taskFrameQuery ? this.runBranch("context_task_frame_broad", () => this.args.adapter.query({
1977
+ project: scope.project,
1978
+ query: taskFrameQuery,
1979
+ top_k: this.broadTopK,
1980
+ include_memories: false,
1981
+ user_id: scope.userId,
1982
+ session_id: scope.sessionId,
1983
+ max_tokens: this.maxTokens,
1984
+ compress: true,
1985
+ compression_strategy: "adaptive"
1986
+ })) : Promise.resolve({ name: "context_task_frame_broad", status: "skipped", durationMs: 0 }),
1987
+ taskFrameQuery ? this.runBranch("memory_task_frame_broad", () => this.args.adapter.searchMemories({
1988
+ project: scope.project,
1989
+ query: taskFrameQuery,
1990
+ user_id: scope.userId,
1991
+ session_id: scope.sessionId,
1992
+ top_k: this.broadTopK,
1993
+ include_pending: true,
1994
+ profile: "balanced"
1995
+ })) : Promise.resolve({ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 })
1996
+ ]) : [
1997
+ { name: "context_primary_broad", status: "skipped", durationMs: 0 },
1998
+ { name: "memory_primary_broad", status: "skipped", durationMs: 0 },
1999
+ { name: "context_task_frame_broad", status: "skipped", durationMs: 0 },
2000
+ { name: "memory_task_frame_broad", status: "skipped", durationMs: 0 }
2001
+ ];
2002
+ const broadCollected = collectFromBranches(broadBranches, "broad");
2003
+ totalOkCount += broadCollected.okCount;
2004
+ if (fallbackUsed) {
2005
+ this.fallbackTriggers += 1;
2006
+ this.broadScopedTurns += 1;
2007
+ allCollected = [...allCollected, ...broadCollected.collected];
2008
+ }
2009
+ const ranked = this.rerank(allCollected, focusedScope);
2010
+ dedupedCount += ranked.dedupedCount;
2011
+ const floored = this.applyRelevanceFloor(ranked.items);
2012
+ droppedBelowFloor += floored.dropped;
2013
+ this.floorDroppedCount += droppedBelowFloor;
2014
+ this.droppedCount += droppedBelowFloor;
2015
+ const finalItems = floored.items.slice(0, this.broadTopK);
2016
+ this.injectedItemCount += finalItems.length;
2017
+ this.totalTurns += 1;
2018
+ const executedBranches = [...focusedBranches, ...broadBranches].filter((branch) => branch.status !== "skipped");
2019
+ for (const branch of [...focusedBranches, ...broadBranches]) {
1990
2020
  branchStatus[branch.name] = branch.status;
1991
- if (branch.status !== "ok") {
1992
- if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1993
- continue;
1994
- }
1995
- okCount += 1;
1996
- if (branch.name.startsWith("context")) {
1997
- collected.push(...this.contextItems(
1998
- branch.value,
1999
- branch.name.includes("task_frame") ? "task_frame" : "primary"
2000
- ));
2001
- } else {
2002
- collected.push(...this.memoryItems(
2003
- branch.value,
2004
- branch.name.includes("task_frame") ? "task_frame" : "primary"
2005
- ));
2006
- }
2007
2021
  }
2008
- const ranked = this.rerank(collected).slice(0, this.topK * 2);
2009
2022
  const prepared = {
2010
2023
  scope,
2011
2024
  retrieval: {
2012
2025
  primaryQuery,
2013
2026
  taskFrameQuery,
2014
2027
  warnings,
2015
- degraded: okCount < branches.filter((branch) => branch.status !== "skipped").length,
2016
- degradedReason: okCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2028
+ degraded: totalOkCount < executedBranches.length,
2029
+ degradedReason: totalOkCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2017
2030
  durationMs: Date.now() - startedAt,
2018
2031
  targetBudgetMs: this.targetRetrievalMs,
2019
2032
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
2020
- branchStatus
2033
+ branchStatus,
2034
+ focusedScopeApplied,
2035
+ focusedSourceIds: focusedScope.sourceIds,
2036
+ focusedFileHints: focusedScope.fileHints.map((value) => pathBase(value)),
2037
+ clientScoped: Boolean(focusedScope.clientName),
2038
+ fallbackUsed,
2039
+ droppedBelowFloor,
2040
+ dedupedCount
2021
2041
  },
2022
- context: this.buildContext(ranked),
2023
- items: ranked
2042
+ context: this.buildContext(finalItems),
2043
+ items: finalItems
2024
2044
  };
2025
2045
  this.lastPreparedTurn = prepared.retrieval;
2026
2046
  return prepared;
@@ -2115,7 +2135,14 @@ ${lines.join("\n")}`;
2115
2135
  counters: {
2116
2136
  mergedCount: this.mergedCount,
2117
2137
  droppedCount: this.droppedCount,
2118
- bufferedLowSalience: this.bufferedLowSalience.length
2138
+ bufferedLowSalience: this.bufferedLowSalience.length,
2139
+ focusedPassHits: this.focusedPassHits,
2140
+ fallbackTriggers: this.fallbackTriggers,
2141
+ floorDroppedCount: this.floorDroppedCount,
2142
+ injectedItemCount: this.injectedItemCount,
2143
+ sourceScopedTurns: this.sourceScopedTurns,
2144
+ broadScopedTurns: this.broadScopedTurns,
2145
+ totalTurns: this.totalTurns
2119
2146
  }
2120
2147
  };
2121
2148
  }
@@ -2333,165 +2360,469 @@ var WhisperClient = class _WhisperClient {
2333
2360
  retryable: false
2334
2361
  });
2335
2362
  }
2336
- return resolved;
2363
+ return resolved;
2364
+ }
2365
+ async refreshProjectCache(force = false) {
2366
+ if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
2367
+ return this.projectCache;
2368
+ }
2369
+ const response = await this.runtimeClient.request({
2370
+ endpoint: "/v1/projects",
2371
+ method: "GET",
2372
+ operation: "get",
2373
+ idempotent: true
2374
+ });
2375
+ this.projectRefToId.clear();
2376
+ this.projectCache = response.data?.projects || [];
2377
+ for (const project of this.projectCache) {
2378
+ this.projectRefToId.set(project.id, project.id);
2379
+ this.projectRefToId.set(project.slug, project.id);
2380
+ this.projectRefToId.set(project.name, project.id);
2381
+ }
2382
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2383
+ return this.projectCache;
2384
+ }
2385
+ async fetchResolvedProject(projectRef) {
2386
+ try {
2387
+ const response = await this.runtimeClient.request({
2388
+ endpoint: `/v1/projects/resolve?project=${encodeURIComponent(projectRef)}`,
2389
+ method: "GET",
2390
+ operation: "get",
2391
+ idempotent: true
2392
+ });
2393
+ return response.data?.resolved || null;
2394
+ } catch (error) {
2395
+ if (error instanceof RuntimeClientError && error.status === 404) {
2396
+ return null;
2397
+ }
2398
+ throw error;
2399
+ }
2400
+ }
2401
+ async resolveProject(projectRef) {
2402
+ const resolvedRef = this.getRequiredProject(projectRef);
2403
+ const cachedProjects = await this.refreshProjectCache(false);
2404
+ const cachedProject = cachedProjects.find(
2405
+ (project) => project.id === resolvedRef || project.slug === resolvedRef || project.name === resolvedRef
2406
+ );
2407
+ if (cachedProject) {
2408
+ return cachedProject;
2409
+ }
2410
+ const resolvedProject = await this.fetchResolvedProject(resolvedRef);
2411
+ if (resolvedProject) {
2412
+ this.projectRefToId.set(resolvedProject.id, resolvedProject.id);
2413
+ this.projectRefToId.set(resolvedProject.slug, resolvedProject.id);
2414
+ this.projectRefToId.set(resolvedProject.name, resolvedProject.id);
2415
+ this.projectCache = [
2416
+ ...this.projectCache.filter((project) => project.id !== resolvedProject.id),
2417
+ resolvedProject
2418
+ ];
2419
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2420
+ return resolvedProject;
2421
+ }
2422
+ if (isLikelyProjectId(resolvedRef)) {
2423
+ return {
2424
+ id: resolvedRef,
2425
+ orgId: "",
2426
+ name: resolvedRef,
2427
+ slug: resolvedRef,
2428
+ createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2429
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
2430
+ };
2431
+ }
2432
+ throw new RuntimeClientError({
2433
+ code: "PROJECT_NOT_FOUND",
2434
+ message: `Project '${resolvedRef}' not found`,
2435
+ retryable: false
2436
+ });
2437
+ }
2438
+ async query(params) {
2439
+ const project = (await this.resolveProject(params.project)).id;
2440
+ const response = await this.runtimeClient.request({
2441
+ endpoint: "/v1/context/query",
2442
+ method: "POST",
2443
+ operation: "search",
2444
+ body: {
2445
+ ...params,
2446
+ project
2447
+ },
2448
+ idempotent: true
2449
+ });
2450
+ return response.data;
2451
+ }
2452
+ async ingestSession(params) {
2453
+ const project = (await this.resolveProject(params.project)).id;
2454
+ const response = await this.runtimeClient.request({
2455
+ endpoint: "/v1/memory/ingest/session",
2456
+ method: "POST",
2457
+ operation: "session",
2458
+ body: {
2459
+ ...params,
2460
+ project
2461
+ }
2462
+ });
2463
+ return response.data;
2464
+ }
2465
+ createAgentRuntime(options = {}) {
2466
+ const baseContext = {
2467
+ workspacePath: options.workspacePath,
2468
+ project: options.project || this.config.project,
2469
+ userId: options.userId,
2470
+ sessionId: options.sessionId,
2471
+ traceId: options.traceId,
2472
+ clientName: options.clientName
2473
+ };
2474
+ return new WhisperAgentRuntime({
2475
+ baseContext,
2476
+ options,
2477
+ adapter: {
2478
+ resolveProject: (project) => this.resolveProject(project),
2479
+ query: (params) => this.query(params),
2480
+ ingestSession: (params) => this.ingestSession(params),
2481
+ getSessionMemories: (params) => this.memory.getSessionMemories(params),
2482
+ getUserProfile: (params) => this.memory.getUserProfile(params),
2483
+ searchMemories: (params) => this.memory.search(params),
2484
+ addMemory: (params) => this.memory.add(params),
2485
+ queueStatus: () => this.queue.status(),
2486
+ flushQueue: () => this.queue.flush()
2487
+ }
2488
+ });
2489
+ }
2490
+ withRunContext(context) {
2491
+ const base = this;
2492
+ return {
2493
+ memory: {
2494
+ add: (params) => base.memory.add({
2495
+ ...params,
2496
+ project: params.project || context.project || base.config.project,
2497
+ user_id: params.user_id || context.userId,
2498
+ session_id: params.session_id || context.sessionId
2499
+ }),
2500
+ search: (params) => base.memory.search({
2501
+ ...params,
2502
+ project: params.project || context.project || base.config.project,
2503
+ user_id: params.user_id || context.userId,
2504
+ session_id: params.session_id || context.sessionId
2505
+ })
2506
+ },
2507
+ session: {
2508
+ event: (params) => base.session.event({
2509
+ ...params,
2510
+ sessionId: params.sessionId || context.sessionId || ""
2511
+ })
2512
+ },
2513
+ queue: base.queue,
2514
+ diagnostics: base.diagnostics
2515
+ };
2516
+ }
2517
+ async shutdown() {
2518
+ await this.writeQueue.stop();
2519
+ }
2520
+ };
2521
+ var whisper_default = WhisperClient;
2522
+
2523
+ // ../src/sdk/whisper-agent.ts
2524
+ var DEPRECATION_WARNINGS = /* @__PURE__ */ new Set();
2525
+ function warnDeprecatedOnce(key, message) {
2526
+ if (DEPRECATION_WARNINGS.has(key)) return;
2527
+ DEPRECATION_WARNINGS.add(key);
2528
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
2529
+ console.warn(message);
2530
+ }
2531
+ }
2532
+ var Whisper = class {
2533
+ client;
2534
+ runtimeClient;
2535
+ options;
2536
+ sessionId;
2537
+ userId;
2538
+ constructor(options) {
2539
+ if (!options.apiKey) {
2540
+ throw new Error("API key is required");
2541
+ }
2542
+ const clientConfig = {
2543
+ apiKey: options.apiKey,
2544
+ baseUrl: options.baseUrl,
2545
+ project: options.project || "default"
2546
+ };
2547
+ if (options.timeoutMs) clientConfig.timeoutMs = options.timeoutMs;
2548
+ if (options.retry) clientConfig.retry = options.retry;
2549
+ this.client = new WhisperContext(clientConfig);
2550
+ this.runtimeClient = new WhisperClient({
2551
+ apiKey: options.apiKey,
2552
+ baseUrl: options.baseUrl,
2553
+ project: options.project || "default"
2554
+ });
2555
+ warnDeprecatedOnce(
2556
+ "whisper_agent_wrapper",
2557
+ "[Whisper SDK] Whisper wrapper is supported for v2 compatibility. Prefer WhisperClient for new integrations."
2558
+ );
2559
+ const finalRetry = options.retry || { maxAttempts: 3, baseDelayMs: 250, maxDelayMs: 2e3 };
2560
+ this.options = {
2561
+ apiKey: options.apiKey,
2562
+ baseUrl: options.baseUrl || "https://context.usewhisper.dev",
2563
+ project: options.project || "default",
2564
+ timeoutMs: options.timeoutMs || 15e3,
2565
+ retry: finalRetry,
2566
+ contextLimit: options.contextLimit ?? 10,
2567
+ memoryTypes: options.memoryTypes ?? ["factual", "preference", "event", "goal", "relationship", "opinion", "instruction"],
2568
+ contextPrefix: options.contextPrefix ?? "Relevant context:",
2569
+ autoExtract: options.autoExtract ?? true,
2570
+ autoExtractMinConfidence: options.autoExtractMinConfidence ?? 0.65,
2571
+ maxMemoriesPerCapture: options.maxMemoriesPerCapture ?? 5
2572
+ };
2573
+ }
2574
+ /**
2575
+ * Set session ID for conversation tracking
2576
+ */
2577
+ session(sessionId) {
2578
+ this.sessionId = sessionId;
2579
+ return this;
2580
+ }
2581
+ /**
2582
+ * Set user ID for user-specific memories
2583
+ */
2584
+ user(userId) {
2585
+ this.userId = userId;
2586
+ return this;
2587
+ }
2588
+ /**
2589
+ * Get relevant context BEFORE your LLM call
2590
+ *
2591
+ * @param query - What you want to know / user question
2592
+ * @returns Context string and raw results
2593
+ *
2594
+ * @example
2595
+ * ```typescript
2596
+ * const { context, results, count } = await whisper.getContext(
2597
+ * "What are user's preferences?",
2598
+ * { userId: "user-123" }
2599
+ * );
2600
+ *
2601
+ * // Results: [
2602
+ * // { content: "User prefers dark mode", type: "preference", score: 0.95 },
2603
+ * // { content: "Allergic to nuts", type: "factual", score: 0.89 }
2604
+ * // ]
2605
+ * ```
2606
+ */
2607
+ async getContext(query, options) {
2608
+ const runtime = this.runtimeClient.createAgentRuntime({
2609
+ project: options?.project ?? this.options.project,
2610
+ userId: options?.userId ?? this.userId,
2611
+ sessionId: options?.sessionId ?? this.sessionId,
2612
+ topK: options?.limit ?? this.options.contextLimit,
2613
+ clientName: "whisper-wrapper"
2614
+ });
2615
+ const prepared = await runtime.beforeTurn({
2616
+ userMessage: query
2617
+ });
2618
+ const results = prepared.items.map((item, index) => ({
2619
+ id: item.id || `runtime_${index}`,
2620
+ content: item.content,
2621
+ score: item.score,
2622
+ metadata: item.metadata || {},
2623
+ source: item.type === "memory" ? "memory" : "runtime",
2624
+ document: item.sourceQuery,
2625
+ type: item.type,
2626
+ retrieval_source: item.type === "memory" ? "memory" : "runtime"
2627
+ }));
2628
+ const context = results.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
2629
+ return {
2630
+ context: context ? `${this.options.contextPrefix}
2631
+ ${context}` : "",
2632
+ results,
2633
+ count: prepared.items.length
2634
+ };
2635
+ }
2636
+ /**
2637
+ * Remember what happened AFTER your LLM response
2638
+ *
2639
+ * Fire-and-forget - doesn't block your response
2640
+ *
2641
+ * @param content - What your LLM responded with
2642
+ * @returns Promise that resolves when stored (or fails silently)
2643
+ *
2644
+ * @example
2645
+ * ```typescript
2646
+ * const llmResponse = "I've set your theme to dark mode and removed nuts from recommendations.";
2647
+ *
2648
+ * await whisper.remember(llmResponse, { userId: "user-123" });
2649
+ * // → Auto-extracts: "theme set to dark mode", "nut allergy"
2650
+ * // → Stored as preferences
2651
+ * ```
2652
+ */
2653
+ async remember(content, options) {
2654
+ if (!content || content.length < 5) {
2655
+ return { success: false };
2656
+ }
2657
+ try {
2658
+ if (this.options.autoExtract) {
2659
+ const extraction = await this.client.extractMemories({
2660
+ project: options?.project ?? this.options.project,
2661
+ message: content,
2662
+ user_id: options?.userId ?? this.userId,
2663
+ session_id: options?.sessionId ?? this.sessionId,
2664
+ enable_pattern: true,
2665
+ enable_inference: true,
2666
+ min_confidence: this.options.autoExtractMinConfidence
2667
+ });
2668
+ const extractedMemories = (extraction.all || []).filter((m) => (m.confidence || 0) >= this.options.autoExtractMinConfidence).slice(0, this.options.maxMemoriesPerCapture);
2669
+ if (extractedMemories.length > 0) {
2670
+ const bulk = await this.client.addMemoriesBulk({
2671
+ project: options?.project ?? this.options.project,
2672
+ write_mode: "async",
2673
+ memories: extractedMemories.map((m) => ({
2674
+ content: m.content,
2675
+ memory_type: m.memoryType,
2676
+ user_id: options?.userId ?? this.userId,
2677
+ session_id: options?.sessionId ?? this.sessionId,
2678
+ importance: Math.max(0.5, Math.min(1, m.confidence || 0.7)),
2679
+ confidence: m.confidence || 0.7,
2680
+ entity_mentions: m.entityMentions || [],
2681
+ event_date: m.eventDate || void 0,
2682
+ metadata: {
2683
+ extracted: true,
2684
+ extraction_method: extraction.extractionMethod,
2685
+ extraction_reasoning: m.reasoning,
2686
+ inferred: Boolean(m.inferred)
2687
+ }
2688
+ }))
2689
+ });
2690
+ const memoryIds = this.extractMemoryIdsFromBulkResponse(bulk);
2691
+ return {
2692
+ success: true,
2693
+ memoryId: memoryIds[0],
2694
+ memoryIds: memoryIds.length > 0 ? memoryIds : void 0,
2695
+ extracted: extractedMemories.length
2696
+ };
2697
+ }
2698
+ }
2699
+ const result = await this.client.addMemory({
2700
+ project: options?.project ?? this.options.project,
2701
+ content,
2702
+ user_id: options?.userId ?? this.userId,
2703
+ session_id: options?.sessionId ?? this.sessionId
2704
+ });
2705
+ return {
2706
+ success: true,
2707
+ memoryId: result?.id
2708
+ };
2709
+ } catch (error) {
2710
+ console.error("[Whisper] Remember failed:", error);
2711
+ return { success: false };
2712
+ }
2337
2713
  }
2338
- async refreshProjectCache(force = false) {
2339
- if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
2340
- return this.projectCache;
2341
- }
2342
- const response = await this.runtimeClient.request({
2343
- endpoint: "/v1/projects",
2344
- method: "GET",
2345
- operation: "get",
2346
- idempotent: true
2347
- });
2348
- this.projectRefToId.clear();
2349
- this.projectCache = response.data?.projects || [];
2350
- for (const project of this.projectCache) {
2351
- this.projectRefToId.set(project.id, project.id);
2352
- this.projectRefToId.set(project.slug, project.id);
2353
- this.projectRefToId.set(project.name, project.id);
2354
- }
2355
- this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2356
- return this.projectCache;
2714
+ /**
2715
+ * Alias for remember() - same thing
2716
+ */
2717
+ async capture(content, options) {
2718
+ return this.remember(content, options);
2357
2719
  }
2358
- async fetchResolvedProject(projectRef) {
2720
+ /**
2721
+ * Capture from multiple messages (e.g., full conversation)
2722
+ */
2723
+ async captureSession(messages, options) {
2359
2724
  try {
2360
- const response = await this.runtimeClient.request({
2361
- endpoint: `/v1/projects/resolve?project=${encodeURIComponent(projectRef)}`,
2362
- method: "GET",
2363
- operation: "get",
2364
- idempotent: true
2725
+ const filteredMessages = messages.filter((m) => m.role !== "system");
2726
+ const runtime = this.runtimeClient.createAgentRuntime({
2727
+ project: options?.project ?? this.options.project,
2728
+ userId: options?.userId ?? this.userId,
2729
+ sessionId: options?.sessionId ?? this.sessionId ?? "default",
2730
+ clientName: "whisper-wrapper"
2365
2731
  });
2366
- return response.data?.resolved || null;
2732
+ const result = await runtime.afterTurn({
2733
+ userMessage: [...filteredMessages].reverse().find((m) => m.role === "user")?.content || "",
2734
+ assistantMessage: [...filteredMessages].reverse().find((m) => m.role === "assistant")?.content || ""
2735
+ });
2736
+ return {
2737
+ success: true,
2738
+ extracted: result.memoriesCreated ?? 0
2739
+ };
2367
2740
  } catch (error) {
2368
- if (error instanceof RuntimeClientError && error.status === 404) {
2369
- return null;
2741
+ const fallback = await this.fallbackCaptureViaAddMemory(messages, options);
2742
+ if (fallback.success) {
2743
+ return fallback;
2370
2744
  }
2371
- throw error;
2745
+ console.error("[Whisper] Session capture failed:", error);
2746
+ return { success: false, extracted: 0 };
2372
2747
  }
2373
2748
  }
2374
- async resolveProject(projectRef) {
2375
- const resolvedRef = this.getRequiredProject(projectRef);
2376
- const cachedProjects = await this.refreshProjectCache(false);
2377
- const cachedProject = cachedProjects.find(
2378
- (project) => project.id === resolvedRef || project.slug === resolvedRef || project.name === resolvedRef
2379
- );
2380
- if (cachedProject) {
2381
- return cachedProject;
2382
- }
2383
- const resolvedProject = await this.fetchResolvedProject(resolvedRef);
2384
- if (resolvedProject) {
2385
- this.projectRefToId.set(resolvedProject.id, resolvedProject.id);
2386
- this.projectRefToId.set(resolvedProject.slug, resolvedProject.id);
2387
- this.projectRefToId.set(resolvedProject.name, resolvedProject.id);
2388
- this.projectCache = [
2389
- ...this.projectCache.filter((project) => project.id !== resolvedProject.id),
2390
- resolvedProject
2391
- ];
2392
- this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2393
- return resolvedProject;
2394
- }
2395
- if (isLikelyProjectId(resolvedRef)) {
2396
- return {
2397
- id: resolvedRef,
2398
- orgId: "",
2399
- name: resolvedRef,
2400
- slug: resolvedRef,
2401
- createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2402
- updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
2403
- };
2404
- }
2405
- throw new RuntimeClientError({
2406
- code: "PROJECT_NOT_FOUND",
2407
- message: `Project '${resolvedRef}' not found`,
2408
- retryable: false
2749
+ /**
2750
+ * Run a full agent turn with automatic memory read (before) + write (after).
2751
+ */
2752
+ async runTurn(params) {
2753
+ const contextResult = await this.getContext(params.userMessage, {
2754
+ userId: params.userId,
2755
+ sessionId: params.sessionId,
2756
+ project: params.project,
2757
+ limit: params.limit
2409
2758
  });
2759
+ const prompt = contextResult.context ? `${contextResult.context}
2760
+
2761
+ User: ${params.userMessage}` : params.userMessage;
2762
+ const response = await params.generate(prompt);
2763
+ const captureResult = await this.captureSession(
2764
+ [
2765
+ { role: "user", content: params.userMessage },
2766
+ { role: "assistant", content: response }
2767
+ ],
2768
+ {
2769
+ userId: params.userId,
2770
+ sessionId: params.sessionId,
2771
+ project: params.project
2772
+ }
2773
+ );
2774
+ return {
2775
+ response,
2776
+ context: contextResult.context,
2777
+ count: contextResult.count,
2778
+ extracted: captureResult.extracted
2779
+ };
2410
2780
  }
2411
- async query(params) {
2412
- const project = (await this.resolveProject(params.project)).id;
2413
- const response = await this.runtimeClient.request({
2414
- endpoint: "/v1/context/query",
2415
- method: "POST",
2416
- operation: "search",
2417
- body: {
2418
- ...params,
2419
- project
2420
- },
2421
- idempotent: true
2422
- });
2423
- return response.data;
2781
+ /**
2782
+ * Direct access to WhisperContext for advanced usage
2783
+ */
2784
+ raw() {
2785
+ return this.client;
2424
2786
  }
2425
- async ingestSession(params) {
2426
- const project = (await this.resolveProject(params.project)).id;
2427
- const response = await this.runtimeClient.request({
2428
- endpoint: "/v1/memory/ingest/session",
2429
- method: "POST",
2430
- operation: "session",
2431
- body: {
2432
- ...params,
2433
- project
2787
+ extractMemoryIdsFromBulkResponse(bulkResponse) {
2788
+ const ids = [];
2789
+ if (Array.isArray(bulkResponse?.memories)) {
2790
+ for (const memory of bulkResponse.memories) {
2791
+ if (memory?.id) ids.push(memory.id);
2434
2792
  }
2435
- });
2436
- return response.data;
2793
+ }
2794
+ if (bulkResponse?.memory?.id) {
2795
+ ids.push(bulkResponse.memory.id);
2796
+ }
2797
+ if (bulkResponse?.id) {
2798
+ ids.push(bulkResponse.id);
2799
+ }
2800
+ return Array.from(new Set(ids));
2437
2801
  }
2438
- createAgentRuntime(options = {}) {
2439
- const baseContext = {
2440
- workspacePath: options.workspacePath,
2441
- project: options.project || this.config.project,
2442
- userId: options.userId,
2443
- sessionId: options.sessionId,
2444
- traceId: options.traceId,
2445
- clientName: options.clientName
2446
- };
2447
- return new WhisperAgentRuntime({
2448
- baseContext,
2449
- options,
2450
- adapter: {
2451
- resolveProject: (project) => this.resolveProject(project),
2452
- query: (params) => this.query(params),
2453
- ingestSession: (params) => this.ingestSession(params),
2454
- getSessionMemories: (params) => this.memory.getSessionMemories(params),
2455
- getUserProfile: (params) => this.memory.getUserProfile(params),
2456
- searchMemories: (params) => this.memory.search(params),
2457
- addMemory: (params) => this.memory.add(params),
2458
- queueStatus: () => this.queue.status(),
2459
- flushQueue: () => this.queue.flush()
2802
+ async fallbackCaptureViaAddMemory(messages, options) {
2803
+ const userMessages = messages.filter((m) => m.role === "user").map((m) => (m.content || "").trim()).filter((content) => content.length >= 5).slice(-2);
2804
+ if (userMessages.length === 0) {
2805
+ return { success: false, extracted: 0 };
2806
+ }
2807
+ let extracted = 0;
2808
+ for (const content of userMessages) {
2809
+ try {
2810
+ await this.client.addMemory({
2811
+ project: options?.project ?? this.options.project,
2812
+ content,
2813
+ memory_type: "factual",
2814
+ user_id: options?.userId ?? this.userId,
2815
+ session_id: options?.sessionId ?? this.sessionId,
2816
+ allow_legacy_fallback: true
2817
+ });
2818
+ extracted += 1;
2819
+ } catch {
2460
2820
  }
2461
- });
2462
- }
2463
- withRunContext(context) {
2464
- const base = this;
2465
- return {
2466
- memory: {
2467
- add: (params) => base.memory.add({
2468
- ...params,
2469
- project: params.project || context.project || base.config.project,
2470
- user_id: params.user_id || context.userId,
2471
- session_id: params.session_id || context.sessionId
2472
- }),
2473
- search: (params) => base.memory.search({
2474
- ...params,
2475
- project: params.project || context.project || base.config.project,
2476
- user_id: params.user_id || context.userId,
2477
- session_id: params.session_id || context.sessionId
2478
- })
2479
- },
2480
- session: {
2481
- event: (params) => base.session.event({
2482
- ...params,
2483
- sessionId: params.sessionId || context.sessionId || ""
2484
- })
2485
- },
2486
- queue: base.queue,
2487
- diagnostics: base.diagnostics
2488
- };
2489
- }
2490
- async shutdown() {
2491
- await this.writeQueue.stop();
2821
+ }
2822
+ return { success: extracted > 0, extracted };
2492
2823
  }
2493
2824
  };
2494
- var whisper_default = WhisperClient;
2825
+ var whisper_agent_default = Whisper;
2495
2826
 
2496
2827
  // ../src/sdk/middleware.ts
2497
2828
  var WhisperAgentMiddleware = class {