@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.js CHANGED
@@ -428,368 +428,83 @@ var RuntimeClient = class {
428
428
  }
429
429
  };
430
430
 
431
- // ../src/sdk/whisper-agent.ts
432
- var DEPRECATION_WARNINGS = /* @__PURE__ */ new Set();
433
- function warnDeprecatedOnce(key, message) {
434
- if (DEPRECATION_WARNINGS.has(key)) return;
435
- DEPRECATION_WARNINGS.add(key);
436
- if (typeof console !== "undefined" && typeof console.warn === "function") {
437
- console.warn(message);
431
+ // ../src/sdk/core/cache.ts
432
+ var SearchResponseCache = class {
433
+ ttlMs;
434
+ capacity;
435
+ byKey = /* @__PURE__ */ new Map();
436
+ scopeIndex = /* @__PURE__ */ new Map();
437
+ constructor(ttlMs = 7e3, capacity = 500) {
438
+ this.ttlMs = Math.max(1e3, ttlMs);
439
+ this.capacity = Math.max(10, capacity);
438
440
  }
439
- }
440
- var Whisper = class {
441
- client;
442
- options;
443
- sessionId;
444
- userId;
445
- constructor(options) {
446
- if (!options.apiKey) {
447
- throw new Error("API key is required");
448
- }
449
- const clientConfig = {
450
- apiKey: options.apiKey,
451
- baseUrl: options.baseUrl,
452
- project: options.project || "default"
453
- };
454
- if (options.timeoutMs) clientConfig.timeoutMs = options.timeoutMs;
455
- if (options.retry) clientConfig.retry = options.retry;
456
- this.client = new WhisperContext(clientConfig);
457
- warnDeprecatedOnce(
458
- "whisper_agent_wrapper",
459
- "[Whisper SDK] Whisper wrapper is supported for v2 compatibility. Prefer WhisperClient for new integrations."
460
- );
461
- const finalRetry = options.retry || { maxAttempts: 3, baseDelayMs: 250, maxDelayMs: 2e3 };
462
- this.options = {
463
- apiKey: options.apiKey,
464
- baseUrl: options.baseUrl || "https://context.usewhisper.dev",
465
- project: options.project || "default",
466
- timeoutMs: options.timeoutMs || 15e3,
467
- retry: finalRetry,
468
- contextLimit: options.contextLimit ?? 10,
469
- memoryTypes: options.memoryTypes ?? ["factual", "preference", "event", "goal", "relationship", "opinion", "instruction"],
470
- contextPrefix: options.contextPrefix ?? "Relevant context:",
471
- autoExtract: options.autoExtract ?? true,
472
- autoExtractMinConfidence: options.autoExtractMinConfidence ?? 0.65,
473
- maxMemoriesPerCapture: options.maxMemoriesPerCapture ?? 5
474
- };
441
+ makeScopeKey(project, userId, sessionId) {
442
+ return `${project}:${userId || "_"}:${sessionId || "_"}`;
475
443
  }
476
- /**
477
- * Set session ID for conversation tracking
478
- */
479
- session(sessionId) {
480
- this.sessionId = sessionId;
481
- return this;
444
+ makeKey(input) {
445
+ const normalized = {
446
+ project: input.project,
447
+ userId: input.userId || "",
448
+ sessionId: input.sessionId || "",
449
+ query: normalizeQuery(input.query),
450
+ topK: input.topK,
451
+ profile: input.profile,
452
+ includePending: input.includePending
453
+ };
454
+ return `search:${stableHash(JSON.stringify(normalized))}`;
482
455
  }
483
- /**
484
- * Set user ID for user-specific memories
485
- */
486
- user(userId) {
487
- this.userId = userId;
488
- return this;
456
+ get(key) {
457
+ const found = this.byKey.get(key);
458
+ if (!found) return null;
459
+ if (found.expiresAt <= Date.now()) {
460
+ this.deleteByKey(key);
461
+ return null;
462
+ }
463
+ found.touchedAt = Date.now();
464
+ return found.value;
489
465
  }
490
- /**
491
- * Get relevant context BEFORE your LLM call
492
- *
493
- * @param query - What you want to know / user question
494
- * @returns Context string and raw results
495
- *
496
- * @example
497
- * ```typescript
498
- * const { context, results, count } = await whisper.getContext(
499
- * "What are user's preferences?",
500
- * { userId: "user-123" }
501
- * );
502
- *
503
- * // Results: [
504
- * // { content: "User prefers dark mode", type: "preference", score: 0.95 },
505
- * // { content: "Allergic to nuts", type: "factual", score: 0.89 }
506
- * // ]
507
- * ```
508
- */
509
- async getContext(query, options) {
510
- const result = await this.client.query({
511
- project: options?.project ?? this.options.project,
512
- query,
513
- top_k: options?.limit ?? this.options.contextLimit,
514
- include_memories: true,
515
- user_id: options?.userId ?? this.userId,
516
- session_id: options?.sessionId ?? this.sessionId
466
+ set(key, scopeKey, value) {
467
+ this.byKey.set(key, {
468
+ value,
469
+ scopeKey,
470
+ touchedAt: Date.now(),
471
+ expiresAt: Date.now() + this.ttlMs
517
472
  });
518
- const context = result.results.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
519
- return {
520
- context: context ? `${this.options.contextPrefix}
521
- ${context}` : "",
522
- results: result.results,
523
- count: result.meta.total
524
- };
473
+ if (!this.scopeIndex.has(scopeKey)) {
474
+ this.scopeIndex.set(scopeKey, /* @__PURE__ */ new Set());
475
+ }
476
+ this.scopeIndex.get(scopeKey).add(key);
477
+ this.evictIfNeeded();
525
478
  }
526
- /**
527
- * Remember what happened AFTER your LLM response
528
- *
529
- * Fire-and-forget - doesn't block your response
530
- *
531
- * @param content - What your LLM responded with
532
- * @returns Promise that resolves when stored (or fails silently)
533
- *
534
- * @example
535
- * ```typescript
536
- * const llmResponse = "I've set your theme to dark mode and removed nuts from recommendations.";
537
- *
538
- * await whisper.remember(llmResponse, { userId: "user-123" });
539
- * // → Auto-extracts: "theme set to dark mode", "nut allergy"
540
- * // → Stored as preferences
541
- * ```
542
- */
543
- async remember(content, options) {
544
- if (!content || content.length < 5) {
545
- return { success: false };
479
+ invalidateScope(scopeKey) {
480
+ const keys = this.scopeIndex.get(scopeKey);
481
+ if (!keys || keys.size === 0) {
482
+ return 0;
546
483
  }
547
- try {
548
- if (this.options.autoExtract) {
549
- const extraction = await this.client.extractMemories({
550
- project: options?.project ?? this.options.project,
551
- message: content,
552
- user_id: options?.userId ?? this.userId,
553
- session_id: options?.sessionId ?? this.sessionId,
554
- enable_pattern: true,
555
- enable_inference: true,
556
- min_confidence: this.options.autoExtractMinConfidence
557
- });
558
- const extractedMemories = (extraction.all || []).filter((m) => (m.confidence || 0) >= this.options.autoExtractMinConfidence).slice(0, this.options.maxMemoriesPerCapture);
559
- if (extractedMemories.length > 0) {
560
- const bulk = await this.client.addMemoriesBulk({
561
- project: options?.project ?? this.options.project,
562
- write_mode: "async",
563
- memories: extractedMemories.map((m) => ({
564
- content: m.content,
565
- memory_type: m.memoryType,
566
- user_id: options?.userId ?? this.userId,
567
- session_id: options?.sessionId ?? this.sessionId,
568
- importance: Math.max(0.5, Math.min(1, m.confidence || 0.7)),
569
- confidence: m.confidence || 0.7,
570
- entity_mentions: m.entityMentions || [],
571
- event_date: m.eventDate || void 0,
572
- metadata: {
573
- extracted: true,
574
- extraction_method: extraction.extractionMethod,
575
- extraction_reasoning: m.reasoning,
576
- inferred: Boolean(m.inferred)
577
- }
578
- }))
579
- });
580
- const memoryIds = this.extractMemoryIdsFromBulkResponse(bulk);
581
- return {
582
- success: true,
583
- memoryId: memoryIds[0],
584
- memoryIds: memoryIds.length > 0 ? memoryIds : void 0,
585
- extracted: extractedMemories.length
586
- };
587
- }
588
- }
589
- const result = await this.client.addMemory({
590
- project: options?.project ?? this.options.project,
591
- content,
592
- user_id: options?.userId ?? this.userId,
593
- session_id: options?.sessionId ?? this.sessionId
594
- });
595
- return {
596
- success: true,
597
- memoryId: result?.id
598
- };
599
- } catch (error) {
600
- console.error("[Whisper] Remember failed:", error);
601
- return { success: false };
484
+ const toDelete = Array.from(keys);
485
+ for (const key of toDelete) {
486
+ this.deleteByKey(key);
602
487
  }
488
+ this.scopeIndex.delete(scopeKey);
489
+ return toDelete.length;
603
490
  }
604
- /**
605
- * Alias for remember() - same thing
606
- */
607
- async capture(content, options) {
608
- return this.remember(content, options);
491
+ evictIfNeeded() {
492
+ if (this.byKey.size <= this.capacity) return;
493
+ const ordered = Array.from(this.byKey.entries()).sort((a, b) => a[1].touchedAt - b[1].touchedAt);
494
+ const removeCount = this.byKey.size - this.capacity;
495
+ for (let i = 0; i < removeCount; i += 1) {
496
+ this.deleteByKey(ordered[i][0]);
497
+ }
609
498
  }
610
- /**
611
- * Capture from multiple messages (e.g., full conversation)
612
- */
613
- async captureSession(messages, options) {
614
- try {
615
- const result = await this.client.ingestSession({
616
- project: options?.project ?? this.options.project,
617
- session_id: options?.sessionId ?? this.sessionId ?? "default",
618
- user_id: options?.userId ?? this.userId,
619
- messages: messages.filter((m) => m.role !== "system").map((m) => ({
620
- role: m.role,
621
- content: m.content,
622
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
623
- }))
624
- });
625
- return {
626
- success: true,
627
- extracted: result?.memories_created ?? 0
628
- };
629
- } catch (error) {
630
- const fallback = await this.fallbackCaptureViaAddMemory(messages, options);
631
- if (fallback.success) {
632
- return fallback;
633
- }
634
- console.error("[Whisper] Session capture failed:", error);
635
- return { success: false, extracted: 0 };
636
- }
637
- }
638
- /**
639
- * Run a full agent turn with automatic memory read (before) + write (after).
640
- */
641
- async runTurn(params) {
642
- const contextResult = await this.getContext(params.userMessage, {
643
- userId: params.userId,
644
- sessionId: params.sessionId,
645
- project: params.project,
646
- limit: params.limit
647
- });
648
- const prompt = contextResult.context ? `${contextResult.context}
649
-
650
- User: ${params.userMessage}` : params.userMessage;
651
- const response = await params.generate(prompt);
652
- const captureResult = await this.captureSession(
653
- [
654
- { role: "user", content: params.userMessage },
655
- { role: "assistant", content: response }
656
- ],
657
- {
658
- userId: params.userId,
659
- sessionId: params.sessionId,
660
- project: params.project
661
- }
662
- );
663
- return {
664
- response,
665
- context: contextResult.context,
666
- count: contextResult.count,
667
- extracted: captureResult.extracted
668
- };
669
- }
670
- /**
671
- * Direct access to WhisperContext for advanced usage
672
- */
673
- raw() {
674
- return this.client;
675
- }
676
- extractMemoryIdsFromBulkResponse(bulkResponse) {
677
- const ids = [];
678
- if (Array.isArray(bulkResponse?.memories)) {
679
- for (const memory of bulkResponse.memories) {
680
- if (memory?.id) ids.push(memory.id);
681
- }
682
- }
683
- if (bulkResponse?.memory?.id) {
684
- ids.push(bulkResponse.memory.id);
685
- }
686
- if (bulkResponse?.id) {
687
- ids.push(bulkResponse.id);
688
- }
689
- return Array.from(new Set(ids));
690
- }
691
- async fallbackCaptureViaAddMemory(messages, options) {
692
- const userMessages = messages.filter((m) => m.role === "user").map((m) => (m.content || "").trim()).filter((content) => content.length >= 5).slice(-2);
693
- if (userMessages.length === 0) {
694
- return { success: false, extracted: 0 };
695
- }
696
- let extracted = 0;
697
- for (const content of userMessages) {
698
- try {
699
- await this.client.addMemory({
700
- project: options?.project ?? this.options.project,
701
- content,
702
- memory_type: "factual",
703
- user_id: options?.userId ?? this.userId,
704
- session_id: options?.sessionId ?? this.sessionId,
705
- allow_legacy_fallback: true
706
- });
707
- extracted += 1;
708
- } catch {
709
- }
710
- }
711
- return { success: extracted > 0, extracted };
712
- }
713
- };
714
- var whisper_agent_default = Whisper;
715
-
716
- // ../src/sdk/core/cache.ts
717
- var SearchResponseCache = class {
718
- ttlMs;
719
- capacity;
720
- byKey = /* @__PURE__ */ new Map();
721
- scopeIndex = /* @__PURE__ */ new Map();
722
- constructor(ttlMs = 7e3, capacity = 500) {
723
- this.ttlMs = Math.max(1e3, ttlMs);
724
- this.capacity = Math.max(10, capacity);
725
- }
726
- makeScopeKey(project, userId, sessionId) {
727
- return `${project}:${userId || "_"}:${sessionId || "_"}`;
728
- }
729
- makeKey(input) {
730
- const normalized = {
731
- project: input.project,
732
- userId: input.userId || "",
733
- sessionId: input.sessionId || "",
734
- query: normalizeQuery(input.query),
735
- topK: input.topK,
736
- profile: input.profile,
737
- includePending: input.includePending
738
- };
739
- return `search:${stableHash(JSON.stringify(normalized))}`;
740
- }
741
- get(key) {
742
- const found = this.byKey.get(key);
743
- if (!found) return null;
744
- if (found.expiresAt <= Date.now()) {
745
- this.deleteByKey(key);
746
- return null;
747
- }
748
- found.touchedAt = Date.now();
749
- return found.value;
750
- }
751
- set(key, scopeKey, value) {
752
- this.byKey.set(key, {
753
- value,
754
- scopeKey,
755
- touchedAt: Date.now(),
756
- expiresAt: Date.now() + this.ttlMs
757
- });
758
- if (!this.scopeIndex.has(scopeKey)) {
759
- this.scopeIndex.set(scopeKey, /* @__PURE__ */ new Set());
760
- }
761
- this.scopeIndex.get(scopeKey).add(key);
762
- this.evictIfNeeded();
763
- }
764
- invalidateScope(scopeKey) {
765
- const keys = this.scopeIndex.get(scopeKey);
766
- if (!keys || keys.size === 0) {
767
- return 0;
768
- }
769
- const toDelete = Array.from(keys);
770
- for (const key of toDelete) {
771
- this.deleteByKey(key);
772
- }
773
- this.scopeIndex.delete(scopeKey);
774
- return toDelete.length;
775
- }
776
- evictIfNeeded() {
777
- if (this.byKey.size <= this.capacity) return;
778
- const ordered = Array.from(this.byKey.entries()).sort((a, b) => a[1].touchedAt - b[1].touchedAt);
779
- const removeCount = this.byKey.size - this.capacity;
780
- for (let i = 0; i < removeCount; i += 1) {
781
- this.deleteByKey(ordered[i][0]);
782
- }
783
- }
784
- deleteByKey(key) {
785
- const found = this.byKey.get(key);
786
- if (!found) return;
787
- this.byKey.delete(key);
788
- const scopeKeys = this.scopeIndex.get(found.scopeKey);
789
- if (!scopeKeys) return;
790
- scopeKeys.delete(key);
791
- if (scopeKeys.size === 0) {
792
- this.scopeIndex.delete(found.scopeKey);
499
+ deleteByKey(key) {
500
+ const found = this.byKey.get(key);
501
+ if (!found) return;
502
+ this.byKey.delete(key);
503
+ const scopeKeys = this.scopeIndex.get(found.scopeKey);
504
+ if (!scopeKeys) return;
505
+ scopeKeys.delete(key);
506
+ if (scopeKeys.size === 0) {
507
+ this.scopeIndex.delete(found.scopeKey);
793
508
  }
794
509
  }
795
510
  };
@@ -1687,6 +1402,29 @@ ${lines.join("\n")}`;
1687
1402
  function compactWhitespace(value) {
1688
1403
  return value.replace(/\s+/g, " ").trim();
1689
1404
  }
1405
+ function normalizeSummary(value) {
1406
+ return compactWhitespace(String(value || "").toLowerCase());
1407
+ }
1408
+ function tokenize(value) {
1409
+ return normalizeSummary(value).split(/[^a-z0-9_./-]+/i).map((token) => token.trim()).filter(Boolean);
1410
+ }
1411
+ function jaccardOverlap(left, right) {
1412
+ const leftTokens = new Set(tokenize(left));
1413
+ const rightTokens = new Set(tokenize(right));
1414
+ if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
1415
+ let intersection = 0;
1416
+ for (const token of leftTokens) {
1417
+ if (rightTokens.has(token)) intersection += 1;
1418
+ }
1419
+ const union = (/* @__PURE__ */ new Set([...leftTokens, ...rightTokens])).size;
1420
+ return union > 0 ? intersection / union : 0;
1421
+ }
1422
+ function clamp01(value) {
1423
+ if (!Number.isFinite(value)) return 0;
1424
+ if (value < 0) return 0;
1425
+ if (value > 1) return 1;
1426
+ return value;
1427
+ }
1690
1428
  function withTimeout(promise, timeoutMs) {
1691
1429
  return new Promise((resolve, reject) => {
1692
1430
  const timeout = setTimeout(() => {
@@ -1720,39 +1458,76 @@ function extractTimestamp(metadata) {
1720
1458
  }
1721
1459
  return 0;
1722
1460
  }
1723
- function salienceBoost(metadata) {
1724
- const value = metadata?.salience;
1725
- if (value === "high") return 0.12;
1726
- if (value === "medium") return 0.06;
1727
- return 0;
1728
- }
1461
+ var DEFAULT_RANK_WEIGHTS = {
1462
+ focusedPassBonus: 0.2,
1463
+ sourceMatchBonus: 0.18,
1464
+ touchedFileBonus: 0.12,
1465
+ clientMatchBonus: 0.1,
1466
+ highSalienceBonus: 0.12,
1467
+ mediumSalienceBonus: 0.06,
1468
+ staleBroadPenalty: -0.1,
1469
+ unrelatedClientPenalty: -0.18,
1470
+ lowSaliencePenalty: -0.12
1471
+ };
1472
+ var DEFAULT_SOURCE_ACTIVITY = {
1473
+ maxTurns: 10,
1474
+ maxIdleMs: 30 * 60 * 1e3,
1475
+ decayAfterTurns: 5,
1476
+ decayAfterIdleMs: 15 * 60 * 1e3,
1477
+ evictOnTaskSwitch: true
1478
+ };
1729
1479
  var WhisperAgentRuntime = class {
1730
1480
  constructor(args) {
1731
1481
  this.args = args;
1732
1482
  this.bindingStore = createBindingStore(args.options.bindingStorePath);
1733
- this.topK = args.options.topK ?? 6;
1483
+ const retrieval = args.options.retrieval || {};
1484
+ this.focusedTopK = retrieval.focusedTopK ?? args.options.topK ?? 6;
1485
+ this.broadTopK = retrieval.broadTopK ?? Math.max(args.options.topK ?? 6, 10);
1734
1486
  this.maxTokens = args.options.maxTokens ?? 4e3;
1735
1487
  this.targetRetrievalMs = args.options.targetRetrievalMs ?? 2500;
1736
1488
  this.hardRetrievalTimeoutMs = args.options.hardRetrievalTimeoutMs ?? 4e3;
1737
1489
  this.recentWorkLimit = args.options.recentWorkLimit ?? 40;
1738
1490
  this.baseContext = args.baseContext;
1739
1491
  this.clientName = args.baseContext.clientName || "whisper-agent-runtime";
1492
+ this.minFocusedResults = retrieval.minFocusedResults ?? 3;
1493
+ this.minFocusedTopScore = retrieval.minFocusedTopScore ?? 0.55;
1494
+ this.minProjectScore = retrieval.minProjectScore ?? 0.5;
1495
+ this.minMemoryScore = retrieval.minMemoryScore ?? 0.6;
1496
+ this.rankWeights = { ...DEFAULT_RANK_WEIGHTS, ...retrieval.rankWeights || {} };
1497
+ this.sourceActivityOptions = { ...DEFAULT_SOURCE_ACTIVITY, ...retrieval.sourceActivity || {} };
1740
1498
  }
1741
1499
  bindingStore;
1742
- topK;
1500
+ focusedTopK;
1501
+ broadTopK;
1743
1502
  maxTokens;
1744
1503
  targetRetrievalMs;
1745
1504
  hardRetrievalTimeoutMs;
1746
1505
  recentWorkLimit;
1747
1506
  baseContext;
1748
1507
  clientName;
1508
+ minFocusedResults;
1509
+ minFocusedTopScore;
1510
+ minProjectScore;
1511
+ minMemoryScore;
1512
+ rankWeights;
1513
+ sourceActivityOptions;
1749
1514
  bindings = null;
1750
1515
  touchedFiles = [];
1751
1516
  recentWork = [];
1517
+ recentSourceActivity = [];
1752
1518
  bufferedLowSalience = [];
1753
1519
  lastPreparedTurn = null;
1754
1520
  mergedCount = 0;
1755
1521
  droppedCount = 0;
1522
+ focusedPassHits = 0;
1523
+ fallbackTriggers = 0;
1524
+ floorDroppedCount = 0;
1525
+ injectedItemCount = 0;
1526
+ sourceScopedTurns = 0;
1527
+ broadScopedTurns = 0;
1528
+ totalTurns = 0;
1529
+ currentTurn = 0;
1530
+ lastTaskSummary = "";
1756
1531
  lastScope = {};
1757
1532
  async getBindings() {
1758
1533
  if (!this.bindings) {
@@ -1770,6 +1545,64 @@ var WhisperAgentRuntime = class {
1770
1545
  pushWorkEvent(event) {
1771
1546
  this.recentWork = [...this.recentWork, event].slice(-this.recentWorkLimit);
1772
1547
  }
1548
+ noteSourceActivity(sourceIds) {
1549
+ const now = Date.now();
1550
+ for (const sourceId of [...new Set((sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))]) {
1551
+ this.recentSourceActivity = [
1552
+ ...this.recentSourceActivity.filter((entry) => entry.sourceId !== sourceId),
1553
+ { sourceId, turn: this.currentTurn, at: now }
1554
+ ].slice(-24);
1555
+ }
1556
+ }
1557
+ refreshTaskSummary(taskSummary) {
1558
+ const next = normalizeSummary(taskSummary);
1559
+ if (!next) return;
1560
+ if (this.sourceActivityOptions.evictOnTaskSwitch && this.lastTaskSummary && this.lastTaskSummary !== next && jaccardOverlap(this.lastTaskSummary, next) < 0.6) {
1561
+ this.recentSourceActivity = [];
1562
+ }
1563
+ this.lastTaskSummary = next;
1564
+ }
1565
+ activeSourceIds() {
1566
+ const now = Date.now();
1567
+ const active = /* @__PURE__ */ new Map();
1568
+ const maxTurns = this.sourceActivityOptions.maxTurns;
1569
+ const maxIdleMs = this.sourceActivityOptions.maxIdleMs;
1570
+ const decayAfterTurns = this.sourceActivityOptions.decayAfterTurns;
1571
+ const decayAfterIdleMs = this.sourceActivityOptions.decayAfterIdleMs;
1572
+ const fresh = [];
1573
+ for (const entry of this.recentSourceActivity) {
1574
+ const turnDelta = this.currentTurn - entry.turn;
1575
+ const idleDelta = now - entry.at;
1576
+ if (turnDelta > maxTurns || idleDelta > maxIdleMs) continue;
1577
+ fresh.push(entry);
1578
+ let weight = 1;
1579
+ if (turnDelta > decayAfterTurns || idleDelta > decayAfterIdleMs) {
1580
+ weight = 0.5;
1581
+ }
1582
+ const current = active.get(entry.sourceId) || 0;
1583
+ active.set(entry.sourceId, Math.max(current, weight));
1584
+ }
1585
+ this.recentSourceActivity = fresh.slice(-24);
1586
+ return [...active.entries()].sort((left, right) => right[1] - left[1]).map(([sourceId]) => sourceId).slice(0, 4);
1587
+ }
1588
+ focusedScope(input) {
1589
+ const sourceIds = this.activeSourceIds();
1590
+ const fileHints = [...new Set([
1591
+ ...input.touchedFiles || [],
1592
+ ...this.touchedFiles,
1593
+ ...this.recentWork.flatMap((event) => event.filePaths || [])
1594
+ ].map((value) => String(value || "").trim()).filter(Boolean))].slice(-4);
1595
+ return {
1596
+ sourceIds,
1597
+ fileHints,
1598
+ clientName: this.clientName || void 0
1599
+ };
1600
+ }
1601
+ exactFileMetadataFilter(fileHints) {
1602
+ const exact = fileHints.find((value) => /[\\/]/.test(value));
1603
+ if (!exact) return void 0;
1604
+ return { filePath: exact };
1605
+ }
1773
1606
  makeTaskFrameQuery(input) {
1774
1607
  const task = compactWhitespace(input.taskSummary || "");
1775
1608
  const salient = this.recentWork.filter((event) => event.salience === "high").slice(-3).map((event) => `${event.kind}: ${event.summary}`);
@@ -1846,23 +1679,29 @@ var WhisperAgentRuntime = class {
1846
1679
  };
1847
1680
  }
1848
1681
  }
1849
- contextItems(result, sourceQuery) {
1682
+ contextItems(result, sourceQuery, pass) {
1683
+ const sourceScope = result.meta?.source_scope;
1684
+ if (sourceScope?.mode === "auto" || sourceScope?.mode === "explicit") {
1685
+ this.noteSourceActivity(sourceScope.source_ids || []);
1686
+ }
1850
1687
  return (result.results || []).map((item) => ({
1851
1688
  id: item.id,
1852
1689
  content: item.content,
1853
1690
  type: "project",
1854
1691
  score: item.score ?? 0,
1855
1692
  sourceQuery,
1693
+ pass,
1856
1694
  metadata: item.metadata || {}
1857
1695
  }));
1858
1696
  }
1859
- memoryItems(result, sourceQuery) {
1697
+ memoryItems(result, sourceQuery, pass) {
1860
1698
  return (result.results || []).map((item, index) => ({
1861
1699
  id: item.memory?.id || item.chunk?.id || `${sourceQuery}_memory_${index}`,
1862
1700
  content: item.chunk?.content || item.memory?.content || "",
1863
1701
  type: "memory",
1864
1702
  score: item.similarity ?? 0,
1865
1703
  sourceQuery,
1704
+ pass,
1866
1705
  metadata: {
1867
1706
  ...item.chunk?.metadata || {},
1868
1707
  ...item.memory?.temporal || {},
@@ -1870,22 +1709,99 @@ var WhisperAgentRuntime = class {
1870
1709
  }
1871
1710
  })).filter((item) => item.content);
1872
1711
  }
1873
- rerank(items) {
1712
+ stableItemKey(item) {
1713
+ const metadata = item.metadata || {};
1714
+ const sourceId = String(metadata.source_id || "");
1715
+ const documentId = String(metadata.document_id || metadata.documentId || "");
1716
+ const chunkId = String(metadata.chunk_id || metadata.chunkId || item.id || "");
1717
+ return stableHash(`${sourceId}|${documentId}|${chunkId}|${item.content.slice(0, 256)}`);
1718
+ }
1719
+ metadataStrings(item) {
1720
+ const metadata = item.metadata || {};
1721
+ return [
1722
+ metadata.filePath,
1723
+ metadata.file_path,
1724
+ metadata.path,
1725
+ metadata.section_path,
1726
+ metadata.parent_section_path,
1727
+ metadata.web_url,
1728
+ metadata.url
1729
+ ].map((value) => String(value || "").toLowerCase()).filter(Boolean);
1730
+ }
1731
+ hasSourceMatch(item, scope) {
1732
+ const sourceId = String(item.metadata?.source_id || "");
1733
+ return Boolean(sourceId && scope.sourceIds.includes(sourceId));
1734
+ }
1735
+ hasFileMatch(item, scope) {
1736
+ if (scope.fileHints.length === 0) return false;
1737
+ const metadata = this.metadataStrings(item);
1738
+ const lowerHints = scope.fileHints.map((hint) => hint.toLowerCase());
1739
+ return lowerHints.some((hint) => {
1740
+ const base = pathBase(hint).toLowerCase();
1741
+ return metadata.some((value) => value.includes(hint) || value.endsWith(base));
1742
+ });
1743
+ }
1744
+ hasClientMatch(item, scope) {
1745
+ const itemClient = String(item.metadata?.client_name || "");
1746
+ return Boolean(scope.clientName && itemClient && itemClient === scope.clientName);
1747
+ }
1748
+ salienceAdjustment(item) {
1749
+ const salience = item.metadata?.salience;
1750
+ if (salience === "high") return this.rankWeights.highSalienceBonus;
1751
+ if (salience === "medium") return this.rankWeights.mediumSalienceBonus;
1752
+ if (salience === "low") return this.rankWeights.lowSaliencePenalty;
1753
+ return 0;
1754
+ }
1755
+ narrowFocusedMemories(items, scope) {
1756
+ const hasSignals = scope.sourceIds.length > 0 || scope.fileHints.length > 0 || Boolean(scope.clientName);
1757
+ if (!hasSignals) return items;
1758
+ const narrowed = items.filter((item) => {
1759
+ const matchesClient = this.hasClientMatch(item, scope);
1760
+ const matchesFile = this.hasFileMatch(item, scope);
1761
+ const matchesSource = this.hasSourceMatch(item, scope);
1762
+ const salience = item.metadata?.salience;
1763
+ if (scope.clientName && item.metadata?.client_name && !matchesClient) {
1764
+ return false;
1765
+ }
1766
+ if (salience === "low" && !matchesFile && !matchesSource) {
1767
+ return false;
1768
+ }
1769
+ return matchesClient || matchesFile || matchesSource || !scope.clientName;
1770
+ });
1771
+ return narrowed.length > 0 ? narrowed : items;
1772
+ }
1773
+ applyRelevanceFloor(items) {
1774
+ const filtered = items.filter(
1775
+ (item) => item.type === "project" ? item.score >= this.minProjectScore : item.score >= this.minMemoryScore
1776
+ );
1777
+ return { items: filtered, dropped: Math.max(0, items.length - filtered.length) };
1778
+ }
1779
+ rerank(items, scope) {
1874
1780
  const deduped = /* @__PURE__ */ new Map();
1875
1781
  for (const item of items) {
1876
- const key = `${item.id}:${item.content.toLowerCase()}`;
1782
+ const key = this.stableItemKey(item);
1877
1783
  const recency = extractTimestamp(item.metadata) > 0 ? 0.04 : 0;
1878
1784
  const queryBonus = item.sourceQuery === "primary" ? 0.08 : item.sourceQuery === "task_frame" ? 0.04 : 0.03;
1785
+ const sourceMatch = this.hasSourceMatch(item, scope);
1786
+ const fileMatch = this.hasFileMatch(item, scope);
1787
+ const clientMatch = this.hasClientMatch(item, scope);
1788
+ const broadPenalty = item.pass === "broad" && !sourceMatch && !fileMatch && !clientMatch ? this.rankWeights.staleBroadPenalty : 0;
1789
+ const clientPenalty = scope.clientName && item.metadata?.client_name && !clientMatch ? this.rankWeights.unrelatedClientPenalty : 0;
1879
1790
  const next = {
1880
1791
  ...item,
1881
- score: item.score + queryBonus + salienceBoost(item.metadata) + recency
1792
+ score: clamp01(
1793
+ 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
1794
+ )
1882
1795
  };
1883
1796
  const existing = deduped.get(key);
1884
1797
  if (!existing || next.score > existing.score) {
1885
1798
  deduped.set(key, next);
1886
1799
  }
1887
1800
  }
1888
- return [...deduped.values()].sort((left, right) => right.score - left.score);
1801
+ return {
1802
+ items: [...deduped.values()].sort((left, right) => right.score - left.score),
1803
+ dedupedCount: Math.max(0, items.length - deduped.size)
1804
+ };
1889
1805
  }
1890
1806
  buildContext(items) {
1891
1807
  const maxChars = this.maxTokens * 4;
@@ -1924,7 +1840,7 @@ ${lines.join("\n")}`;
1924
1840
  this.runBranch("project_rules", () => this.args.adapter.query({
1925
1841
  project: scope.project,
1926
1842
  query: "project rules instructions constraints conventions open threads",
1927
- top_k: this.topK,
1843
+ top_k: this.focusedTopK,
1928
1844
  include_memories: false,
1929
1845
  user_id: scope.userId,
1930
1846
  session_id: scope.sessionId,
@@ -1942,7 +1858,7 @@ ${lines.join("\n")}`;
1942
1858
  continue;
1943
1859
  }
1944
1860
  if (branch.name === "project_rules") {
1945
- items.push(...this.contextItems(branch.value, "bootstrap"));
1861
+ items.push(...this.contextItems(branch.value, "bootstrap", "bootstrap"));
1946
1862
  continue;
1947
1863
  }
1948
1864
  const records = branch.value.memories || [];
@@ -1952,10 +1868,12 @@ ${lines.join("\n")}`;
1952
1868
  type: "memory",
1953
1869
  score: 0.4,
1954
1870
  sourceQuery: "bootstrap",
1871
+ pass: "bootstrap",
1955
1872
  metadata: memory
1956
1873
  })).filter((item) => item.content));
1957
1874
  }
1958
- const ranked = this.rerank(items).slice(0, this.topK * 2);
1875
+ const reranked = this.rerank(items, { sourceIds: [], fileHints: [], clientName: this.clientName });
1876
+ const ranked = reranked.items.slice(0, this.broadTopK * 2);
1959
1877
  const prepared = {
1960
1878
  scope,
1961
1879
  retrieval: {
@@ -1967,7 +1885,14 @@ ${lines.join("\n")}`;
1967
1885
  durationMs: Date.now() - startedAt,
1968
1886
  targetBudgetMs: this.targetRetrievalMs,
1969
1887
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1970
- branchStatus
1888
+ branchStatus,
1889
+ focusedScopeApplied: false,
1890
+ focusedSourceIds: [],
1891
+ focusedFileHints: [],
1892
+ clientScoped: false,
1893
+ fallbackUsed: false,
1894
+ droppedBelowFloor: 0,
1895
+ dedupedCount: reranked.dedupedCount
1971
1896
  },
1972
1897
  context: this.buildContext(ranked),
1973
1898
  items: ranked
@@ -1976,100 +1901,195 @@ ${lines.join("\n")}`;
1976
1901
  return prepared;
1977
1902
  }
1978
1903
  async beforeTurn(input, context = {}) {
1904
+ this.currentTurn += 1;
1979
1905
  this.pushTouchedFiles(input.touchedFiles);
1906
+ this.refreshTaskSummary(input.taskSummary);
1980
1907
  const { scope, warning } = await this.resolveScope(context);
1981
1908
  const primaryQuery = compactWhitespace(input.userMessage);
1982
1909
  const taskFrameQuery = this.makeTaskFrameQuery(input);
1910
+ const focusedScope = this.focusedScope(input);
1911
+ const focusedMetadataFilter = this.exactFileMetadataFilter(focusedScope.fileHints);
1912
+ const focusedScopeApplied = focusedScope.sourceIds.length > 0 || focusedScope.fileHints.length > 0 || Boolean(focusedScope.clientName);
1983
1913
  const warnings = warning ? [warning] : [];
1984
1914
  const startedAt = Date.now();
1985
- const branches = await Promise.all([
1986
- this.runBranch("context_primary", () => this.args.adapter.query({
1915
+ const branchStatus = {};
1916
+ const collectFromBranches = (branches, pass) => {
1917
+ const collected = [];
1918
+ let okCount = 0;
1919
+ for (const branch of branches) {
1920
+ branchStatus[branch.name] = branch.status;
1921
+ if (branch.status !== "ok") {
1922
+ if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1923
+ continue;
1924
+ }
1925
+ okCount += 1;
1926
+ if (branch.name.startsWith("context")) {
1927
+ collected.push(...this.contextItems(
1928
+ branch.value,
1929
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1930
+ pass
1931
+ ));
1932
+ } else {
1933
+ const memoryItems = this.memoryItems(
1934
+ branch.value,
1935
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1936
+ pass
1937
+ );
1938
+ collected.push(...pass === "focused" ? this.narrowFocusedMemories(memoryItems, focusedScope) : memoryItems);
1939
+ }
1940
+ }
1941
+ return { collected, okCount };
1942
+ };
1943
+ const focusedBranches = await Promise.all([
1944
+ this.runBranch("context_primary_focused", () => this.args.adapter.query({
1987
1945
  project: scope.project,
1988
1946
  query: primaryQuery,
1989
- top_k: this.topK,
1947
+ top_k: this.focusedTopK,
1990
1948
  include_memories: false,
1991
1949
  user_id: scope.userId,
1992
1950
  session_id: scope.sessionId,
1951
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1952
+ metadata_filter: focusedMetadataFilter,
1993
1953
  max_tokens: this.maxTokens,
1994
1954
  compress: true,
1995
1955
  compression_strategy: "adaptive"
1996
1956
  })),
1997
- this.runBranch("memory_primary", () => this.args.adapter.searchMemories({
1957
+ this.runBranch("memory_primary_focused", () => this.args.adapter.searchMemories({
1998
1958
  project: scope.project,
1999
1959
  query: primaryQuery,
2000
1960
  user_id: scope.userId,
2001
1961
  session_id: scope.sessionId,
2002
- top_k: this.topK,
1962
+ top_k: this.focusedTopK,
2003
1963
  include_pending: true,
2004
1964
  profile: "balanced"
2005
1965
  })),
2006
- taskFrameQuery ? this.runBranch("context_task_frame", () => this.args.adapter.query({
1966
+ taskFrameQuery ? this.runBranch("context_task_frame_focused", () => this.args.adapter.query({
2007
1967
  project: scope.project,
2008
1968
  query: taskFrameQuery,
2009
- top_k: this.topK,
1969
+ top_k: this.focusedTopK,
2010
1970
  include_memories: false,
2011
1971
  user_id: scope.userId,
2012
1972
  session_id: scope.sessionId,
1973
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1974
+ metadata_filter: focusedMetadataFilter,
2013
1975
  max_tokens: this.maxTokens,
2014
1976
  compress: true,
2015
1977
  compression_strategy: "adaptive"
2016
- })) : Promise.resolve({
2017
- name: "context_task_frame",
2018
- status: "skipped",
2019
- durationMs: 0
2020
- }),
2021
- taskFrameQuery ? this.runBranch("memory_task_frame", () => this.args.adapter.searchMemories({
1978
+ })) : Promise.resolve({ name: "context_task_frame_focused", status: "skipped", durationMs: 0 }),
1979
+ taskFrameQuery ? this.runBranch("memory_task_frame_focused", () => this.args.adapter.searchMemories({
2022
1980
  project: scope.project,
2023
1981
  query: taskFrameQuery,
2024
1982
  user_id: scope.userId,
2025
1983
  session_id: scope.sessionId,
2026
- top_k: this.topK,
1984
+ top_k: this.focusedTopK,
2027
1985
  include_pending: true,
2028
1986
  profile: "balanced"
2029
- })) : Promise.resolve({
2030
- name: "memory_task_frame",
2031
- status: "skipped",
2032
- durationMs: 0
2033
- })
1987
+ })) : Promise.resolve({ name: "memory_task_frame_focused", status: "skipped", durationMs: 0 })
2034
1988
  ]);
2035
- const branchStatus = {};
2036
- const collected = [];
2037
- let okCount = 0;
2038
- for (const branch of branches) {
1989
+ const focusedCollected = collectFromBranches(focusedBranches, "focused");
1990
+ const focusedRanked = this.rerank(focusedCollected.collected, focusedScope);
1991
+ const focusedFloored = this.applyRelevanceFloor(focusedRanked.items);
1992
+ let allCollected = [...focusedFloored.items];
1993
+ let totalOkCount = focusedCollected.okCount;
1994
+ let dedupedCount = focusedRanked.dedupedCount;
1995
+ let droppedBelowFloor = focusedFloored.dropped;
1996
+ const focusedTopScore = focusedFloored.items[0]?.score ?? 0;
1997
+ const fallbackUsed = focusedFloored.items.length < this.minFocusedResults || focusedTopScore < this.minFocusedTopScore;
1998
+ if (focusedScopeApplied) {
1999
+ this.sourceScopedTurns += 1;
2000
+ }
2001
+ if (!fallbackUsed) {
2002
+ this.focusedPassHits += 1;
2003
+ }
2004
+ const broadBranches = fallbackUsed ? await Promise.all([
2005
+ this.runBranch("context_primary_broad", () => this.args.adapter.query({
2006
+ project: scope.project,
2007
+ query: primaryQuery,
2008
+ top_k: this.broadTopK,
2009
+ include_memories: false,
2010
+ user_id: scope.userId,
2011
+ session_id: scope.sessionId,
2012
+ max_tokens: this.maxTokens,
2013
+ compress: true,
2014
+ compression_strategy: "adaptive"
2015
+ })),
2016
+ this.runBranch("memory_primary_broad", () => this.args.adapter.searchMemories({
2017
+ project: scope.project,
2018
+ query: primaryQuery,
2019
+ user_id: scope.userId,
2020
+ session_id: scope.sessionId,
2021
+ top_k: this.broadTopK,
2022
+ include_pending: true,
2023
+ profile: "balanced"
2024
+ })),
2025
+ taskFrameQuery ? this.runBranch("context_task_frame_broad", () => this.args.adapter.query({
2026
+ project: scope.project,
2027
+ query: taskFrameQuery,
2028
+ top_k: this.broadTopK,
2029
+ include_memories: false,
2030
+ user_id: scope.userId,
2031
+ session_id: scope.sessionId,
2032
+ max_tokens: this.maxTokens,
2033
+ compress: true,
2034
+ compression_strategy: "adaptive"
2035
+ })) : Promise.resolve({ name: "context_task_frame_broad", status: "skipped", durationMs: 0 }),
2036
+ taskFrameQuery ? this.runBranch("memory_task_frame_broad", () => this.args.adapter.searchMemories({
2037
+ project: scope.project,
2038
+ query: taskFrameQuery,
2039
+ user_id: scope.userId,
2040
+ session_id: scope.sessionId,
2041
+ top_k: this.broadTopK,
2042
+ include_pending: true,
2043
+ profile: "balanced"
2044
+ })) : Promise.resolve({ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 })
2045
+ ]) : [
2046
+ { name: "context_primary_broad", status: "skipped", durationMs: 0 },
2047
+ { name: "memory_primary_broad", status: "skipped", durationMs: 0 },
2048
+ { name: "context_task_frame_broad", status: "skipped", durationMs: 0 },
2049
+ { name: "memory_task_frame_broad", status: "skipped", durationMs: 0 }
2050
+ ];
2051
+ const broadCollected = collectFromBranches(broadBranches, "broad");
2052
+ totalOkCount += broadCollected.okCount;
2053
+ if (fallbackUsed) {
2054
+ this.fallbackTriggers += 1;
2055
+ this.broadScopedTurns += 1;
2056
+ allCollected = [...allCollected, ...broadCollected.collected];
2057
+ }
2058
+ const ranked = this.rerank(allCollected, focusedScope);
2059
+ dedupedCount += ranked.dedupedCount;
2060
+ const floored = this.applyRelevanceFloor(ranked.items);
2061
+ droppedBelowFloor += floored.dropped;
2062
+ this.floorDroppedCount += droppedBelowFloor;
2063
+ this.droppedCount += droppedBelowFloor;
2064
+ const finalItems = floored.items.slice(0, this.broadTopK);
2065
+ this.injectedItemCount += finalItems.length;
2066
+ this.totalTurns += 1;
2067
+ const executedBranches = [...focusedBranches, ...broadBranches].filter((branch) => branch.status !== "skipped");
2068
+ for (const branch of [...focusedBranches, ...broadBranches]) {
2039
2069
  branchStatus[branch.name] = branch.status;
2040
- if (branch.status !== "ok") {
2041
- if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
2042
- continue;
2043
- }
2044
- okCount += 1;
2045
- if (branch.name.startsWith("context")) {
2046
- collected.push(...this.contextItems(
2047
- branch.value,
2048
- branch.name.includes("task_frame") ? "task_frame" : "primary"
2049
- ));
2050
- } else {
2051
- collected.push(...this.memoryItems(
2052
- branch.value,
2053
- branch.name.includes("task_frame") ? "task_frame" : "primary"
2054
- ));
2055
- }
2056
2070
  }
2057
- const ranked = this.rerank(collected).slice(0, this.topK * 2);
2058
2071
  const prepared = {
2059
2072
  scope,
2060
2073
  retrieval: {
2061
2074
  primaryQuery,
2062
2075
  taskFrameQuery,
2063
2076
  warnings,
2064
- degraded: okCount < branches.filter((branch) => branch.status !== "skipped").length,
2065
- degradedReason: okCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2077
+ degraded: totalOkCount < executedBranches.length,
2078
+ degradedReason: totalOkCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2066
2079
  durationMs: Date.now() - startedAt,
2067
2080
  targetBudgetMs: this.targetRetrievalMs,
2068
2081
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
2069
- branchStatus
2082
+ branchStatus,
2083
+ focusedScopeApplied,
2084
+ focusedSourceIds: focusedScope.sourceIds,
2085
+ focusedFileHints: focusedScope.fileHints.map((value) => pathBase(value)),
2086
+ clientScoped: Boolean(focusedScope.clientName),
2087
+ fallbackUsed,
2088
+ droppedBelowFloor,
2089
+ dedupedCount
2070
2090
  },
2071
- context: this.buildContext(ranked),
2072
- items: ranked
2091
+ context: this.buildContext(finalItems),
2092
+ items: finalItems
2073
2093
  };
2074
2094
  this.lastPreparedTurn = prepared.retrieval;
2075
2095
  return prepared;
@@ -2164,7 +2184,14 @@ ${lines.join("\n")}`;
2164
2184
  counters: {
2165
2185
  mergedCount: this.mergedCount,
2166
2186
  droppedCount: this.droppedCount,
2167
- bufferedLowSalience: this.bufferedLowSalience.length
2187
+ bufferedLowSalience: this.bufferedLowSalience.length,
2188
+ focusedPassHits: this.focusedPassHits,
2189
+ fallbackTriggers: this.fallbackTriggers,
2190
+ floorDroppedCount: this.floorDroppedCount,
2191
+ injectedItemCount: this.injectedItemCount,
2192
+ sourceScopedTurns: this.sourceScopedTurns,
2193
+ broadScopedTurns: this.broadScopedTurns,
2194
+ totalTurns: this.totalTurns
2168
2195
  }
2169
2196
  };
2170
2197
  }
@@ -2382,165 +2409,469 @@ var WhisperClient = class _WhisperClient {
2382
2409
  retryable: false
2383
2410
  });
2384
2411
  }
2385
- return resolved;
2412
+ return resolved;
2413
+ }
2414
+ async refreshProjectCache(force = false) {
2415
+ if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
2416
+ return this.projectCache;
2417
+ }
2418
+ const response = await this.runtimeClient.request({
2419
+ endpoint: "/v1/projects",
2420
+ method: "GET",
2421
+ operation: "get",
2422
+ idempotent: true
2423
+ });
2424
+ this.projectRefToId.clear();
2425
+ this.projectCache = response.data?.projects || [];
2426
+ for (const project of this.projectCache) {
2427
+ this.projectRefToId.set(project.id, project.id);
2428
+ this.projectRefToId.set(project.slug, project.id);
2429
+ this.projectRefToId.set(project.name, project.id);
2430
+ }
2431
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2432
+ return this.projectCache;
2433
+ }
2434
+ async fetchResolvedProject(projectRef) {
2435
+ try {
2436
+ const response = await this.runtimeClient.request({
2437
+ endpoint: `/v1/projects/resolve?project=${encodeURIComponent(projectRef)}`,
2438
+ method: "GET",
2439
+ operation: "get",
2440
+ idempotent: true
2441
+ });
2442
+ return response.data?.resolved || null;
2443
+ } catch (error) {
2444
+ if (error instanceof RuntimeClientError && error.status === 404) {
2445
+ return null;
2446
+ }
2447
+ throw error;
2448
+ }
2449
+ }
2450
+ async resolveProject(projectRef) {
2451
+ const resolvedRef = this.getRequiredProject(projectRef);
2452
+ const cachedProjects = await this.refreshProjectCache(false);
2453
+ const cachedProject = cachedProjects.find(
2454
+ (project) => project.id === resolvedRef || project.slug === resolvedRef || project.name === resolvedRef
2455
+ );
2456
+ if (cachedProject) {
2457
+ return cachedProject;
2458
+ }
2459
+ const resolvedProject = await this.fetchResolvedProject(resolvedRef);
2460
+ if (resolvedProject) {
2461
+ this.projectRefToId.set(resolvedProject.id, resolvedProject.id);
2462
+ this.projectRefToId.set(resolvedProject.slug, resolvedProject.id);
2463
+ this.projectRefToId.set(resolvedProject.name, resolvedProject.id);
2464
+ this.projectCache = [
2465
+ ...this.projectCache.filter((project) => project.id !== resolvedProject.id),
2466
+ resolvedProject
2467
+ ];
2468
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2469
+ return resolvedProject;
2470
+ }
2471
+ if (isLikelyProjectId(resolvedRef)) {
2472
+ return {
2473
+ id: resolvedRef,
2474
+ orgId: "",
2475
+ name: resolvedRef,
2476
+ slug: resolvedRef,
2477
+ createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2478
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
2479
+ };
2480
+ }
2481
+ throw new RuntimeClientError({
2482
+ code: "PROJECT_NOT_FOUND",
2483
+ message: `Project '${resolvedRef}' not found`,
2484
+ retryable: false
2485
+ });
2486
+ }
2487
+ async query(params) {
2488
+ const project = (await this.resolveProject(params.project)).id;
2489
+ const response = await this.runtimeClient.request({
2490
+ endpoint: "/v1/context/query",
2491
+ method: "POST",
2492
+ operation: "search",
2493
+ body: {
2494
+ ...params,
2495
+ project
2496
+ },
2497
+ idempotent: true
2498
+ });
2499
+ return response.data;
2500
+ }
2501
+ async ingestSession(params) {
2502
+ const project = (await this.resolveProject(params.project)).id;
2503
+ const response = await this.runtimeClient.request({
2504
+ endpoint: "/v1/memory/ingest/session",
2505
+ method: "POST",
2506
+ operation: "session",
2507
+ body: {
2508
+ ...params,
2509
+ project
2510
+ }
2511
+ });
2512
+ return response.data;
2513
+ }
2514
+ createAgentRuntime(options = {}) {
2515
+ const baseContext = {
2516
+ workspacePath: options.workspacePath,
2517
+ project: options.project || this.config.project,
2518
+ userId: options.userId,
2519
+ sessionId: options.sessionId,
2520
+ traceId: options.traceId,
2521
+ clientName: options.clientName
2522
+ };
2523
+ return new WhisperAgentRuntime({
2524
+ baseContext,
2525
+ options,
2526
+ adapter: {
2527
+ resolveProject: (project) => this.resolveProject(project),
2528
+ query: (params) => this.query(params),
2529
+ ingestSession: (params) => this.ingestSession(params),
2530
+ getSessionMemories: (params) => this.memory.getSessionMemories(params),
2531
+ getUserProfile: (params) => this.memory.getUserProfile(params),
2532
+ searchMemories: (params) => this.memory.search(params),
2533
+ addMemory: (params) => this.memory.add(params),
2534
+ queueStatus: () => this.queue.status(),
2535
+ flushQueue: () => this.queue.flush()
2536
+ }
2537
+ });
2538
+ }
2539
+ withRunContext(context) {
2540
+ const base = this;
2541
+ return {
2542
+ memory: {
2543
+ add: (params) => base.memory.add({
2544
+ ...params,
2545
+ project: params.project || context.project || base.config.project,
2546
+ user_id: params.user_id || context.userId,
2547
+ session_id: params.session_id || context.sessionId
2548
+ }),
2549
+ search: (params) => base.memory.search({
2550
+ ...params,
2551
+ project: params.project || context.project || base.config.project,
2552
+ user_id: params.user_id || context.userId,
2553
+ session_id: params.session_id || context.sessionId
2554
+ })
2555
+ },
2556
+ session: {
2557
+ event: (params) => base.session.event({
2558
+ ...params,
2559
+ sessionId: params.sessionId || context.sessionId || ""
2560
+ })
2561
+ },
2562
+ queue: base.queue,
2563
+ diagnostics: base.diagnostics
2564
+ };
2565
+ }
2566
+ async shutdown() {
2567
+ await this.writeQueue.stop();
2568
+ }
2569
+ };
2570
+ var whisper_default = WhisperClient;
2571
+
2572
+ // ../src/sdk/whisper-agent.ts
2573
+ var DEPRECATION_WARNINGS = /* @__PURE__ */ new Set();
2574
+ function warnDeprecatedOnce(key, message) {
2575
+ if (DEPRECATION_WARNINGS.has(key)) return;
2576
+ DEPRECATION_WARNINGS.add(key);
2577
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
2578
+ console.warn(message);
2579
+ }
2580
+ }
2581
+ var Whisper = class {
2582
+ client;
2583
+ runtimeClient;
2584
+ options;
2585
+ sessionId;
2586
+ userId;
2587
+ constructor(options) {
2588
+ if (!options.apiKey) {
2589
+ throw new Error("API key is required");
2590
+ }
2591
+ const clientConfig = {
2592
+ apiKey: options.apiKey,
2593
+ baseUrl: options.baseUrl,
2594
+ project: options.project || "default"
2595
+ };
2596
+ if (options.timeoutMs) clientConfig.timeoutMs = options.timeoutMs;
2597
+ if (options.retry) clientConfig.retry = options.retry;
2598
+ this.client = new WhisperContext(clientConfig);
2599
+ this.runtimeClient = new WhisperClient({
2600
+ apiKey: options.apiKey,
2601
+ baseUrl: options.baseUrl,
2602
+ project: options.project || "default"
2603
+ });
2604
+ warnDeprecatedOnce(
2605
+ "whisper_agent_wrapper",
2606
+ "[Whisper SDK] Whisper wrapper is supported for v2 compatibility. Prefer WhisperClient for new integrations."
2607
+ );
2608
+ const finalRetry = options.retry || { maxAttempts: 3, baseDelayMs: 250, maxDelayMs: 2e3 };
2609
+ this.options = {
2610
+ apiKey: options.apiKey,
2611
+ baseUrl: options.baseUrl || "https://context.usewhisper.dev",
2612
+ project: options.project || "default",
2613
+ timeoutMs: options.timeoutMs || 15e3,
2614
+ retry: finalRetry,
2615
+ contextLimit: options.contextLimit ?? 10,
2616
+ memoryTypes: options.memoryTypes ?? ["factual", "preference", "event", "goal", "relationship", "opinion", "instruction"],
2617
+ contextPrefix: options.contextPrefix ?? "Relevant context:",
2618
+ autoExtract: options.autoExtract ?? true,
2619
+ autoExtractMinConfidence: options.autoExtractMinConfidence ?? 0.65,
2620
+ maxMemoriesPerCapture: options.maxMemoriesPerCapture ?? 5
2621
+ };
2622
+ }
2623
+ /**
2624
+ * Set session ID for conversation tracking
2625
+ */
2626
+ session(sessionId) {
2627
+ this.sessionId = sessionId;
2628
+ return this;
2629
+ }
2630
+ /**
2631
+ * Set user ID for user-specific memories
2632
+ */
2633
+ user(userId) {
2634
+ this.userId = userId;
2635
+ return this;
2636
+ }
2637
+ /**
2638
+ * Get relevant context BEFORE your LLM call
2639
+ *
2640
+ * @param query - What you want to know / user question
2641
+ * @returns Context string and raw results
2642
+ *
2643
+ * @example
2644
+ * ```typescript
2645
+ * const { context, results, count } = await whisper.getContext(
2646
+ * "What are user's preferences?",
2647
+ * { userId: "user-123" }
2648
+ * );
2649
+ *
2650
+ * // Results: [
2651
+ * // { content: "User prefers dark mode", type: "preference", score: 0.95 },
2652
+ * // { content: "Allergic to nuts", type: "factual", score: 0.89 }
2653
+ * // ]
2654
+ * ```
2655
+ */
2656
+ async getContext(query, options) {
2657
+ const runtime = this.runtimeClient.createAgentRuntime({
2658
+ project: options?.project ?? this.options.project,
2659
+ userId: options?.userId ?? this.userId,
2660
+ sessionId: options?.sessionId ?? this.sessionId,
2661
+ topK: options?.limit ?? this.options.contextLimit,
2662
+ clientName: "whisper-wrapper"
2663
+ });
2664
+ const prepared = await runtime.beforeTurn({
2665
+ userMessage: query
2666
+ });
2667
+ const results = prepared.items.map((item, index) => ({
2668
+ id: item.id || `runtime_${index}`,
2669
+ content: item.content,
2670
+ score: item.score,
2671
+ metadata: item.metadata || {},
2672
+ source: item.type === "memory" ? "memory" : "runtime",
2673
+ document: item.sourceQuery,
2674
+ type: item.type,
2675
+ retrieval_source: item.type === "memory" ? "memory" : "runtime"
2676
+ }));
2677
+ const context = results.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
2678
+ return {
2679
+ context: context ? `${this.options.contextPrefix}
2680
+ ${context}` : "",
2681
+ results,
2682
+ count: prepared.items.length
2683
+ };
2684
+ }
2685
+ /**
2686
+ * Remember what happened AFTER your LLM response
2687
+ *
2688
+ * Fire-and-forget - doesn't block your response
2689
+ *
2690
+ * @param content - What your LLM responded with
2691
+ * @returns Promise that resolves when stored (or fails silently)
2692
+ *
2693
+ * @example
2694
+ * ```typescript
2695
+ * const llmResponse = "I've set your theme to dark mode and removed nuts from recommendations.";
2696
+ *
2697
+ * await whisper.remember(llmResponse, { userId: "user-123" });
2698
+ * // → Auto-extracts: "theme set to dark mode", "nut allergy"
2699
+ * // → Stored as preferences
2700
+ * ```
2701
+ */
2702
+ async remember(content, options) {
2703
+ if (!content || content.length < 5) {
2704
+ return { success: false };
2705
+ }
2706
+ try {
2707
+ if (this.options.autoExtract) {
2708
+ const extraction = await this.client.extractMemories({
2709
+ project: options?.project ?? this.options.project,
2710
+ message: content,
2711
+ user_id: options?.userId ?? this.userId,
2712
+ session_id: options?.sessionId ?? this.sessionId,
2713
+ enable_pattern: true,
2714
+ enable_inference: true,
2715
+ min_confidence: this.options.autoExtractMinConfidence
2716
+ });
2717
+ const extractedMemories = (extraction.all || []).filter((m) => (m.confidence || 0) >= this.options.autoExtractMinConfidence).slice(0, this.options.maxMemoriesPerCapture);
2718
+ if (extractedMemories.length > 0) {
2719
+ const bulk = await this.client.addMemoriesBulk({
2720
+ project: options?.project ?? this.options.project,
2721
+ write_mode: "async",
2722
+ memories: extractedMemories.map((m) => ({
2723
+ content: m.content,
2724
+ memory_type: m.memoryType,
2725
+ user_id: options?.userId ?? this.userId,
2726
+ session_id: options?.sessionId ?? this.sessionId,
2727
+ importance: Math.max(0.5, Math.min(1, m.confidence || 0.7)),
2728
+ confidence: m.confidence || 0.7,
2729
+ entity_mentions: m.entityMentions || [],
2730
+ event_date: m.eventDate || void 0,
2731
+ metadata: {
2732
+ extracted: true,
2733
+ extraction_method: extraction.extractionMethod,
2734
+ extraction_reasoning: m.reasoning,
2735
+ inferred: Boolean(m.inferred)
2736
+ }
2737
+ }))
2738
+ });
2739
+ const memoryIds = this.extractMemoryIdsFromBulkResponse(bulk);
2740
+ return {
2741
+ success: true,
2742
+ memoryId: memoryIds[0],
2743
+ memoryIds: memoryIds.length > 0 ? memoryIds : void 0,
2744
+ extracted: extractedMemories.length
2745
+ };
2746
+ }
2747
+ }
2748
+ const result = await this.client.addMemory({
2749
+ project: options?.project ?? this.options.project,
2750
+ content,
2751
+ user_id: options?.userId ?? this.userId,
2752
+ session_id: options?.sessionId ?? this.sessionId
2753
+ });
2754
+ return {
2755
+ success: true,
2756
+ memoryId: result?.id
2757
+ };
2758
+ } catch (error) {
2759
+ console.error("[Whisper] Remember failed:", error);
2760
+ return { success: false };
2761
+ }
2386
2762
  }
2387
- async refreshProjectCache(force = false) {
2388
- if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
2389
- return this.projectCache;
2390
- }
2391
- const response = await this.runtimeClient.request({
2392
- endpoint: "/v1/projects",
2393
- method: "GET",
2394
- operation: "get",
2395
- idempotent: true
2396
- });
2397
- this.projectRefToId.clear();
2398
- this.projectCache = response.data?.projects || [];
2399
- for (const project of this.projectCache) {
2400
- this.projectRefToId.set(project.id, project.id);
2401
- this.projectRefToId.set(project.slug, project.id);
2402
- this.projectRefToId.set(project.name, project.id);
2403
- }
2404
- this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2405
- return this.projectCache;
2763
+ /**
2764
+ * Alias for remember() - same thing
2765
+ */
2766
+ async capture(content, options) {
2767
+ return this.remember(content, options);
2406
2768
  }
2407
- async fetchResolvedProject(projectRef) {
2769
+ /**
2770
+ * Capture from multiple messages (e.g., full conversation)
2771
+ */
2772
+ async captureSession(messages, options) {
2408
2773
  try {
2409
- const response = await this.runtimeClient.request({
2410
- endpoint: `/v1/projects/resolve?project=${encodeURIComponent(projectRef)}`,
2411
- method: "GET",
2412
- operation: "get",
2413
- idempotent: true
2774
+ const filteredMessages = messages.filter((m) => m.role !== "system");
2775
+ const runtime = this.runtimeClient.createAgentRuntime({
2776
+ project: options?.project ?? this.options.project,
2777
+ userId: options?.userId ?? this.userId,
2778
+ sessionId: options?.sessionId ?? this.sessionId ?? "default",
2779
+ clientName: "whisper-wrapper"
2414
2780
  });
2415
- return response.data?.resolved || null;
2781
+ const result = await runtime.afterTurn({
2782
+ userMessage: [...filteredMessages].reverse().find((m) => m.role === "user")?.content || "",
2783
+ assistantMessage: [...filteredMessages].reverse().find((m) => m.role === "assistant")?.content || ""
2784
+ });
2785
+ return {
2786
+ success: true,
2787
+ extracted: result.memoriesCreated ?? 0
2788
+ };
2416
2789
  } catch (error) {
2417
- if (error instanceof RuntimeClientError && error.status === 404) {
2418
- return null;
2790
+ const fallback = await this.fallbackCaptureViaAddMemory(messages, options);
2791
+ if (fallback.success) {
2792
+ return fallback;
2419
2793
  }
2420
- throw error;
2794
+ console.error("[Whisper] Session capture failed:", error);
2795
+ return { success: false, extracted: 0 };
2421
2796
  }
2422
2797
  }
2423
- async resolveProject(projectRef) {
2424
- const resolvedRef = this.getRequiredProject(projectRef);
2425
- const cachedProjects = await this.refreshProjectCache(false);
2426
- const cachedProject = cachedProjects.find(
2427
- (project) => project.id === resolvedRef || project.slug === resolvedRef || project.name === resolvedRef
2428
- );
2429
- if (cachedProject) {
2430
- return cachedProject;
2431
- }
2432
- const resolvedProject = await this.fetchResolvedProject(resolvedRef);
2433
- if (resolvedProject) {
2434
- this.projectRefToId.set(resolvedProject.id, resolvedProject.id);
2435
- this.projectRefToId.set(resolvedProject.slug, resolvedProject.id);
2436
- this.projectRefToId.set(resolvedProject.name, resolvedProject.id);
2437
- this.projectCache = [
2438
- ...this.projectCache.filter((project) => project.id !== resolvedProject.id),
2439
- resolvedProject
2440
- ];
2441
- this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
2442
- return resolvedProject;
2443
- }
2444
- if (isLikelyProjectId(resolvedRef)) {
2445
- return {
2446
- id: resolvedRef,
2447
- orgId: "",
2448
- name: resolvedRef,
2449
- slug: resolvedRef,
2450
- createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2451
- updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
2452
- };
2453
- }
2454
- throw new RuntimeClientError({
2455
- code: "PROJECT_NOT_FOUND",
2456
- message: `Project '${resolvedRef}' not found`,
2457
- retryable: false
2798
+ /**
2799
+ * Run a full agent turn with automatic memory read (before) + write (after).
2800
+ */
2801
+ async runTurn(params) {
2802
+ const contextResult = await this.getContext(params.userMessage, {
2803
+ userId: params.userId,
2804
+ sessionId: params.sessionId,
2805
+ project: params.project,
2806
+ limit: params.limit
2458
2807
  });
2808
+ const prompt = contextResult.context ? `${contextResult.context}
2809
+
2810
+ User: ${params.userMessage}` : params.userMessage;
2811
+ const response = await params.generate(prompt);
2812
+ const captureResult = await this.captureSession(
2813
+ [
2814
+ { role: "user", content: params.userMessage },
2815
+ { role: "assistant", content: response }
2816
+ ],
2817
+ {
2818
+ userId: params.userId,
2819
+ sessionId: params.sessionId,
2820
+ project: params.project
2821
+ }
2822
+ );
2823
+ return {
2824
+ response,
2825
+ context: contextResult.context,
2826
+ count: contextResult.count,
2827
+ extracted: captureResult.extracted
2828
+ };
2459
2829
  }
2460
- async query(params) {
2461
- const project = (await this.resolveProject(params.project)).id;
2462
- const response = await this.runtimeClient.request({
2463
- endpoint: "/v1/context/query",
2464
- method: "POST",
2465
- operation: "search",
2466
- body: {
2467
- ...params,
2468
- project
2469
- },
2470
- idempotent: true
2471
- });
2472
- return response.data;
2830
+ /**
2831
+ * Direct access to WhisperContext for advanced usage
2832
+ */
2833
+ raw() {
2834
+ return this.client;
2473
2835
  }
2474
- async ingestSession(params) {
2475
- const project = (await this.resolveProject(params.project)).id;
2476
- const response = await this.runtimeClient.request({
2477
- endpoint: "/v1/memory/ingest/session",
2478
- method: "POST",
2479
- operation: "session",
2480
- body: {
2481
- ...params,
2482
- project
2836
+ extractMemoryIdsFromBulkResponse(bulkResponse) {
2837
+ const ids = [];
2838
+ if (Array.isArray(bulkResponse?.memories)) {
2839
+ for (const memory of bulkResponse.memories) {
2840
+ if (memory?.id) ids.push(memory.id);
2483
2841
  }
2484
- });
2485
- return response.data;
2842
+ }
2843
+ if (bulkResponse?.memory?.id) {
2844
+ ids.push(bulkResponse.memory.id);
2845
+ }
2846
+ if (bulkResponse?.id) {
2847
+ ids.push(bulkResponse.id);
2848
+ }
2849
+ return Array.from(new Set(ids));
2486
2850
  }
2487
- createAgentRuntime(options = {}) {
2488
- const baseContext = {
2489
- workspacePath: options.workspacePath,
2490
- project: options.project || this.config.project,
2491
- userId: options.userId,
2492
- sessionId: options.sessionId,
2493
- traceId: options.traceId,
2494
- clientName: options.clientName
2495
- };
2496
- return new WhisperAgentRuntime({
2497
- baseContext,
2498
- options,
2499
- adapter: {
2500
- resolveProject: (project) => this.resolveProject(project),
2501
- query: (params) => this.query(params),
2502
- ingestSession: (params) => this.ingestSession(params),
2503
- getSessionMemories: (params) => this.memory.getSessionMemories(params),
2504
- getUserProfile: (params) => this.memory.getUserProfile(params),
2505
- searchMemories: (params) => this.memory.search(params),
2506
- addMemory: (params) => this.memory.add(params),
2507
- queueStatus: () => this.queue.status(),
2508
- flushQueue: () => this.queue.flush()
2851
+ async fallbackCaptureViaAddMemory(messages, options) {
2852
+ const userMessages = messages.filter((m) => m.role === "user").map((m) => (m.content || "").trim()).filter((content) => content.length >= 5).slice(-2);
2853
+ if (userMessages.length === 0) {
2854
+ return { success: false, extracted: 0 };
2855
+ }
2856
+ let extracted = 0;
2857
+ for (const content of userMessages) {
2858
+ try {
2859
+ await this.client.addMemory({
2860
+ project: options?.project ?? this.options.project,
2861
+ content,
2862
+ memory_type: "factual",
2863
+ user_id: options?.userId ?? this.userId,
2864
+ session_id: options?.sessionId ?? this.sessionId,
2865
+ allow_legacy_fallback: true
2866
+ });
2867
+ extracted += 1;
2868
+ } catch {
2509
2869
  }
2510
- });
2511
- }
2512
- withRunContext(context) {
2513
- const base = this;
2514
- return {
2515
- memory: {
2516
- add: (params) => base.memory.add({
2517
- ...params,
2518
- project: params.project || context.project || base.config.project,
2519
- user_id: params.user_id || context.userId,
2520
- session_id: params.session_id || context.sessionId
2521
- }),
2522
- search: (params) => base.memory.search({
2523
- ...params,
2524
- project: params.project || context.project || base.config.project,
2525
- user_id: params.user_id || context.userId,
2526
- session_id: params.session_id || context.sessionId
2527
- })
2528
- },
2529
- session: {
2530
- event: (params) => base.session.event({
2531
- ...params,
2532
- sessionId: params.sessionId || context.sessionId || ""
2533
- })
2534
- },
2535
- queue: base.queue,
2536
- diagnostics: base.diagnostics
2537
- };
2538
- }
2539
- async shutdown() {
2540
- await this.writeQueue.stop();
2870
+ }
2871
+ return { success: extracted > 0, extracted };
2541
2872
  }
2542
2873
  };
2543
- var whisper_default = WhisperClient;
2874
+ var whisper_agent_default = Whisper;
2544
2875
 
2545
2876
  // ../src/sdk/middleware.ts
2546
2877
  var WhisperAgentMiddleware = class {