kontext-sdk 0.2.0 → 0.2.1

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/dist/index.js CHANGED
@@ -51,6 +51,8 @@ var KontextError = class extends Error {
51
51
  };
52
52
 
53
53
  // src/store.ts
54
+ var DEFAULT_MAX_ENTRIES = 1e4;
55
+ var EVICTION_RATIO = 0.1;
54
56
  var STORAGE_KEYS = {
55
57
  actions: "kontext:actions",
56
58
  transactions: "kontext:transactions",
@@ -63,6 +65,10 @@ var KontextStore = class {
63
65
  tasks = /* @__PURE__ */ new Map();
64
66
  anomalies = [];
65
67
  storageAdapter = null;
68
+ maxEntries;
69
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
70
+ this.maxEntries = maxEntries;
71
+ }
66
72
  /**
67
73
  * Attach a storage adapter for persistence.
68
74
  *
@@ -86,14 +92,15 @@ var KontextStore = class {
86
92
  */
87
93
  async flush() {
88
94
  if (!this.storageAdapter) return;
95
+ const actionsSnapshot = [...this.actions];
96
+ const transactionsSnapshot = [...this.transactions];
97
+ const tasksSnapshot = Array.from(this.tasks.entries());
98
+ const anomaliesSnapshot = [...this.anomalies];
89
99
  await Promise.all([
90
- this.storageAdapter.save(STORAGE_KEYS.actions, this.actions),
91
- this.storageAdapter.save(STORAGE_KEYS.transactions, this.transactions),
92
- this.storageAdapter.save(
93
- STORAGE_KEYS.tasks,
94
- Array.from(this.tasks.entries())
95
- ),
96
- this.storageAdapter.save(STORAGE_KEYS.anomalies, this.anomalies)
100
+ this.storageAdapter.save(STORAGE_KEYS.actions, actionsSnapshot),
101
+ this.storageAdapter.save(STORAGE_KEYS.transactions, transactionsSnapshot),
102
+ this.storageAdapter.save(STORAGE_KEYS.tasks, tasksSnapshot),
103
+ this.storageAdapter.save(STORAGE_KEYS.anomalies, anomaliesSnapshot)
97
104
  ]);
98
105
  }
99
106
  /**
@@ -125,9 +132,12 @@ var KontextStore = class {
125
132
  // --------------------------------------------------------------------------
126
133
  // Actions
127
134
  // --------------------------------------------------------------------------
128
- /** Append an action log entry. */
135
+ /** Append an action log entry. Evicts oldest 10% when maxEntries is exceeded. */
129
136
  addAction(action) {
130
137
  this.actions.push(action);
138
+ if (this.actions.length > this.maxEntries) {
139
+ this.actions.splice(0, Math.ceil(this.maxEntries * EVICTION_RATIO));
140
+ }
131
141
  }
132
142
  /** Retrieve all action log entries. */
133
143
  getActions() {
@@ -144,9 +154,12 @@ var KontextStore = class {
144
154
  // --------------------------------------------------------------------------
145
155
  // Transactions
146
156
  // --------------------------------------------------------------------------
147
- /** Append a transaction record. */
157
+ /** Append a transaction record. Evicts oldest 10% when maxEntries is exceeded. */
148
158
  addTransaction(tx) {
149
159
  this.transactions.push(tx);
160
+ if (this.transactions.length > this.maxEntries) {
161
+ this.transactions.splice(0, Math.ceil(this.maxEntries * EVICTION_RATIO));
162
+ }
150
163
  }
151
164
  /** Retrieve all transaction records. */
152
165
  getTransactions() {
@@ -194,9 +207,12 @@ var KontextStore = class {
194
207
  // --------------------------------------------------------------------------
195
208
  // Anomalies
196
209
  // --------------------------------------------------------------------------
197
- /** Append an anomaly event. */
210
+ /** Append an anomaly event. Evicts oldest 10% when maxEntries is exceeded. */
198
211
  addAnomaly(anomaly) {
199
212
  this.anomalies.push(anomaly);
213
+ if (this.anomalies.length > this.maxEntries) {
214
+ this.anomalies.splice(0, Math.ceil(this.maxEntries * EVICTION_RATIO));
215
+ }
200
216
  }
201
217
  /** Retrieve all anomaly events. */
202
218
  getAnomalies() {
@@ -397,7 +413,8 @@ var DigestChain = class {
397
413
  */
398
414
  getPrecisionTimestamp() {
399
415
  const hrtime = process.hrtime.bigint();
400
- const microseconds = Number((hrtime - this.hrtimeBase) % 1000000n);
416
+ const delta = hrtime >= this.hrtimeBase ? hrtime - this.hrtimeBase : 0n;
417
+ const microseconds = Number(delta % 1000000n);
401
418
  return {
402
419
  iso: (/* @__PURE__ */ new Date()).toISOString(),
403
420
  hrtime,
@@ -492,6 +509,12 @@ function toCsv(records) {
492
509
  function clamp(value, min, max) {
493
510
  return Math.min(Math.max(value, min), max);
494
511
  }
512
+ var LOG_LEVEL_SEVERITY = {
513
+ debug: 0,
514
+ info: 1,
515
+ warn: 2,
516
+ error: 3
517
+ };
495
518
  var ActionLogger = class {
496
519
  config;
497
520
  store;
@@ -501,6 +524,7 @@ var ActionLogger = class {
501
524
  batchSize;
502
525
  flushIntervalMs;
503
526
  isCloudMode;
527
+ logLevel;
504
528
  constructor(config, store) {
505
529
  this.config = config;
506
530
  this.store = store;
@@ -508,6 +532,7 @@ var ActionLogger = class {
508
532
  this.batchSize = config.batchSize ?? 50;
509
533
  this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
510
534
  this.isCloudMode = !!config.apiKey;
535
+ this.logLevel = config.logLevel ?? (config.debug ? "debug" : "warn");
511
536
  this.flushTimer = setInterval(() => {
512
537
  void this.flush();
513
538
  }, this.flushIntervalMs);
@@ -550,9 +575,7 @@ var ActionLogger = class {
550
575
  if (this.batch.length >= this.batchSize) {
551
576
  await this.flush();
552
577
  }
553
- if (this.config.debug) {
554
- this.debugLog("Action logged", action);
555
- }
578
+ this.emitLog("debug", "Action logged", action);
556
579
  return action;
557
580
  }
558
581
  /**
@@ -604,9 +627,7 @@ var ActionLogger = class {
604
627
  if (this.batch.length >= this.batchSize) {
605
628
  await this.flush();
606
629
  }
607
- if (this.config.debug) {
608
- this.debugLog("Transaction logged", record);
609
- }
630
+ this.emitLog("debug", "Transaction logged", record);
610
631
  return record;
611
632
  }
612
633
  /**
@@ -716,9 +737,7 @@ var ActionLogger = class {
716
737
  const lines = actions.map((a) => JSON.stringify(a)).join("\n") + "\n";
717
738
  fs2__namespace.appendFileSync(filePath, lines, "utf-8");
718
739
  } catch (error) {
719
- if (this.config.debug) {
720
- this.debugLog("Failed to write log file", { error });
721
- }
740
+ this.emitLog("warn", "Failed to write log file", { error });
722
741
  }
723
742
  }
724
743
  async flushToApi(actions) {
@@ -742,15 +761,35 @@ var ActionLogger = class {
742
761
  }
743
762
  } catch (error) {
744
763
  if (error instanceof KontextError) throw error;
745
- if (this.config.debug) {
746
- this.debugLog("API flush failed, falling back to local file", { error });
747
- }
764
+ this.emitLog("warn", "API flush failed, falling back to local file", { error });
748
765
  this.flushToFile(actions);
749
766
  }
750
767
  }
751
- debugLog(message, data) {
768
+ /**
769
+ * Emit a log message at the specified severity level.
770
+ * Only outputs if the message level meets or exceeds the configured logLevel.
771
+ */
772
+ emitLog(level, message, data) {
773
+ if (LOG_LEVEL_SEVERITY[level] < LOG_LEVEL_SEVERITY[this.logLevel]) {
774
+ return;
775
+ }
752
776
  const timestamp = now();
753
- console.debug(`[Kontext ${timestamp}] ${message}`, data ? JSON.stringify(data, null, 2) : "");
777
+ const formatted = `[Kontext ${timestamp}] ${message}`;
778
+ const payload = data ? JSON.stringify(data, null, 2) : "";
779
+ switch (level) {
780
+ case "debug":
781
+ console.debug(formatted, payload);
782
+ break;
783
+ case "info":
784
+ console.info(formatted, payload);
785
+ break;
786
+ case "warn":
787
+ console.warn(formatted, payload);
788
+ break;
789
+ case "error":
790
+ console.error(formatted, payload);
791
+ break;
792
+ }
754
793
  }
755
794
  };
756
795
 
@@ -1478,6 +1517,18 @@ var AuditExporter = class {
1478
1517
  };
1479
1518
 
1480
1519
  // src/trust.ts
1520
+ var TRUST_LEVEL_VERIFIED = 90;
1521
+ var TRUST_LEVEL_HIGH = 70;
1522
+ var TRUST_LEVEL_MEDIUM = 50;
1523
+ var TRUST_LEVEL_LOW = 30;
1524
+ var RISK_FLAG_THRESHOLD = 60;
1525
+ var RISK_BLOCK_THRESHOLD = 80;
1526
+ var RISK_REVIEW_THRESHOLD = 50;
1527
+ var WEIGHT_HISTORY = 0.15;
1528
+ var WEIGHT_TASK_COMPLETION = 0.25;
1529
+ var WEIGHT_ANOMALY = 0.25;
1530
+ var WEIGHT_TX_CONSISTENCY = 0.2;
1531
+ var WEIGHT_COMPLIANCE = 0.15;
1481
1532
  var TrustScorer = class {
1482
1533
  config;
1483
1534
  store;
@@ -1536,8 +1587,8 @@ var TrustScorer = class {
1536
1587
  const totalScore = factors.reduce((sum, f) => sum + f.score, 0);
1537
1588
  const riskScore = clamp(Math.round(totalScore / Math.max(factors.length, 1)), 0, 100);
1538
1589
  const riskLevel = this.riskScoreToLevel(riskScore);
1539
- const flagged = riskScore >= 60;
1540
- const recommendation = riskScore >= 80 ? "block" : riskScore >= 50 ? "review" : "approve";
1590
+ const flagged = riskScore >= RISK_FLAG_THRESHOLD;
1591
+ const recommendation = riskScore >= RISK_BLOCK_THRESHOLD ? "block" : riskScore >= RISK_REVIEW_THRESHOLD ? "review" : "approve";
1541
1592
  return {
1542
1593
  txHash: tx.txHash,
1543
1594
  riskScore,
@@ -1552,17 +1603,34 @@ var TrustScorer = class {
1552
1603
  // Agent trust factor computation
1553
1604
  // --------------------------------------------------------------------------
1554
1605
  computeAgentFactors(agentId) {
1555
- const factors = [];
1556
- factors.push(this.computeHistoryDepthFactor(agentId));
1557
- factors.push(this.computeTaskCompletionFactor(agentId));
1558
- factors.push(this.computeAnomalyFrequencyFactor(agentId));
1559
- factors.push(this.computeTransactionConsistencyFactor(agentId));
1560
- factors.push(this.computeComplianceAdherenceFactor(agentId));
1561
- return factors;
1606
+ const data = {
1607
+ actions: this.store.getActionsByAgent(agentId),
1608
+ transactions: this.store.getTransactionsByAgent(agentId),
1609
+ tasks: this.store.queryTasks((t) => t.agentId === agentId),
1610
+ anomalies: this.store.queryAnomalies((a) => a.agentId === agentId)
1611
+ };
1612
+ return [
1613
+ this.computeHistoryDepthFactor(data),
1614
+ this.computeTaskCompletionFactor(data),
1615
+ this.computeAnomalyFrequencyFactor(data),
1616
+ this.computeTransactionConsistencyFactor(data),
1617
+ this.computeComplianceAdherenceFactor(data)
1618
+ ];
1562
1619
  }
1563
- computeHistoryDepthFactor(agentId) {
1564
- const actions = this.store.getActionsByAgent(agentId);
1565
- const count = actions.length;
1620
+ /**
1621
+ * History depth factor: more recorded actions = more data to assess trust.
1622
+ *
1623
+ * Scoring curve (step function, not linear) prevents gaming by spamming
1624
+ * low-value actions — crossing each tier requires meaningfully more history:
1625
+ * - 0 actions → 10 (minimal trust, new agent)
1626
+ * - 1-4 → 30 (some activity but too little to draw conclusions)
1627
+ * - 5-19 → 50 (neutral, moderate activity)
1628
+ * - 20-49 → 70 (established agent with reasonable track record)
1629
+ * - 50-99 → 85 (well-established agent)
1630
+ * - 100+ → 95 (capped below 100 because history alone doesn't guarantee trust)
1631
+ */
1632
+ computeHistoryDepthFactor(data) {
1633
+ const count = data.actions.length;
1566
1634
  let score;
1567
1635
  if (count === 0) score = 10;
1568
1636
  else if (count < 5) score = 30;
@@ -1573,44 +1641,65 @@ var TrustScorer = class {
1573
1641
  return {
1574
1642
  name: "history_depth",
1575
1643
  score,
1576
- weight: 0.15,
1644
+ weight: WEIGHT_HISTORY,
1577
1645
  description: `Agent has ${count} recorded actions`
1578
1646
  };
1579
1647
  }
1580
- computeTaskCompletionFactor(agentId) {
1581
- const tasks = this.store.queryTasks((t) => t.agentId === agentId);
1582
- const totalTasks = tasks.length;
1648
+ /**
1649
+ * Task completion factor: agents that confirm tasks build trust, failures erode it.
1650
+ *
1651
+ * Formula: score = (completionRate * 100) - (failureRate * 30)
1652
+ * - The 30x failure penalty means each failure costs 3x more than a confirmation gains.
1653
+ * This asymmetry reflects real-world trust: it takes many good actions to build trust
1654
+ * but only a few failures to lose it.
1655
+ * - Returns 50 (neutral) when no tasks exist, avoiding penalizing new agents.
1656
+ */
1657
+ computeTaskCompletionFactor(data) {
1658
+ const totalTasks = data.tasks.length;
1583
1659
  if (totalTasks === 0) {
1584
1660
  return {
1585
1661
  name: "task_completion",
1586
1662
  score: 50,
1587
1663
  // Neutral if no tasks
1588
- weight: 0.25,
1664
+ weight: WEIGHT_TASK_COMPLETION,
1589
1665
  description: "No tasks recorded yet"
1590
1666
  };
1591
1667
  }
1592
- const confirmed = tasks.filter((t) => t.status === "confirmed").length;
1593
- const failed = tasks.filter((t) => t.status === "failed").length;
1668
+ const confirmed = data.tasks.filter((t) => t.status === "confirmed").length;
1669
+ const failed = data.tasks.filter((t) => t.status === "failed").length;
1594
1670
  const completionRate = confirmed / totalTasks;
1595
1671
  const failureRate = failed / totalTasks;
1596
1672
  const score = Math.round(completionRate * 100 - failureRate * 30);
1597
1673
  return {
1598
1674
  name: "task_completion",
1599
1675
  score: clamp(score, 0, 100),
1600
- weight: 0.25,
1676
+ weight: WEIGHT_TASK_COMPLETION,
1601
1677
  description: `${confirmed}/${totalTasks} tasks confirmed (${Math.round(completionRate * 100)}% rate)`
1602
1678
  };
1603
1679
  }
1604
- computeAnomalyFrequencyFactor(agentId) {
1605
- const anomalies = this.store.queryAnomalies((a) => a.agentId === agentId);
1606
- const actions = this.store.getActionsByAgent(agentId);
1607
- const anomalyCount = anomalies.length;
1608
- const actionCount = actions.length;
1680
+ /**
1681
+ * Anomaly frequency factor: fewer anomalies relative to total actions = higher trust.
1682
+ *
1683
+ * Uses anomaly rate (anomalies / actions) with step-function scoring:
1684
+ * - 0% → 100 (clean record)
1685
+ * - <1% → 90 (near-perfect, occasional false positive acceptable)
1686
+ * - <5% → 70 (some noise but generally clean)
1687
+ * - <10% → 50 (neutral, warrants monitoring)
1688
+ * - <25% → 30 (concerning pattern)
1689
+ * - 25%+ → 10 (severe anomaly rate)
1690
+ *
1691
+ * Critical anomalies incur a -15 point penalty each, high anomalies -8 each.
1692
+ * This severity weighting ensures a single critical anomaly (e.g., sanctions hit)
1693
+ * has outsized impact compared to multiple low-severity anomalies.
1694
+ */
1695
+ computeAnomalyFrequencyFactor(data) {
1696
+ const anomalyCount = data.anomalies.length;
1697
+ const actionCount = data.actions.length;
1609
1698
  if (actionCount === 0) {
1610
1699
  return {
1611
1700
  name: "anomaly_frequency",
1612
1701
  score: 50,
1613
- weight: 0.25,
1702
+ weight: WEIGHT_ANOMALY,
1614
1703
  description: "No actions recorded yet"
1615
1704
  };
1616
1705
  }
@@ -1622,23 +1711,37 @@ var TrustScorer = class {
1622
1711
  else if (anomalyRate < 0.1) score = 50;
1623
1712
  else if (anomalyRate < 0.25) score = 30;
1624
1713
  else score = 10;
1625
- const criticalCount = anomalies.filter((a) => a.severity === "critical").length;
1626
- const highCount = anomalies.filter((a) => a.severity === "high").length;
1714
+ const criticalCount = data.anomalies.filter((a) => a.severity === "critical").length;
1715
+ const highCount = data.anomalies.filter((a) => a.severity === "high").length;
1627
1716
  const penaltyFromSeverity = criticalCount * 15 + highCount * 8;
1628
1717
  return {
1629
1718
  name: "anomaly_frequency",
1630
1719
  score: clamp(score - penaltyFromSeverity, 0, 100),
1631
- weight: 0.25,
1720
+ weight: WEIGHT_ANOMALY,
1632
1721
  description: `${anomalyCount} anomalies across ${actionCount} actions (${Math.round(anomalyRate * 100)}% rate)`
1633
1722
  };
1634
1723
  }
1635
- computeTransactionConsistencyFactor(agentId) {
1636
- const transactions = this.store.getTransactionsByAgent(agentId);
1724
+ /**
1725
+ * Transaction consistency factor: stable spending patterns indicate legitimate usage.
1726
+ *
1727
+ * Uses the coefficient of variation (CV = stdDev / mean) to measure amount regularity:
1728
+ * - CV < 0.1 → 95 (extremely consistent, e.g., recurring payroll)
1729
+ * - CV < 0.3 → 80 (fairly consistent with some variance)
1730
+ * - CV < 0.5 → 65 (moderate variance, common for operational spending)
1731
+ * - CV < 1.0 → 45 (high variance, warrants attention)
1732
+ * - CV < 2.0 → 30 (very erratic, potential structuring)
1733
+ * - CV 2.0+ → 15 (extreme variance, strong structuring indicator)
1734
+ *
1735
+ * Also penalizes -15 points when >80% of destinations are unique with >5 txs,
1736
+ * which is a common money-laundering pattern (spray-and-pray distribution).
1737
+ */
1738
+ computeTransactionConsistencyFactor(data) {
1739
+ const transactions = data.transactions;
1637
1740
  if (transactions.length < 2) {
1638
1741
  return {
1639
1742
  name: "transaction_consistency",
1640
1743
  score: 50,
1641
- weight: 0.2,
1744
+ weight: WEIGHT_TX_CONSISTENCY,
1642
1745
  description: "Insufficient transaction history for consistency analysis"
1643
1746
  };
1644
1747
  }
@@ -1647,7 +1750,7 @@ var TrustScorer = class {
1647
1750
  return {
1648
1751
  name: "transaction_consistency",
1649
1752
  score: 50,
1650
- weight: 0.2,
1753
+ weight: WEIGHT_TX_CONSISTENCY,
1651
1754
  description: "Insufficient valid amounts for consistency analysis"
1652
1755
  };
1653
1756
  }
@@ -1670,13 +1773,25 @@ var TrustScorer = class {
1670
1773
  return {
1671
1774
  name: "transaction_consistency",
1672
1775
  score: clamp(score, 0, 100),
1673
- weight: 0.2,
1776
+ weight: WEIGHT_TX_CONSISTENCY,
1674
1777
  description: `CV=${cv.toFixed(2)}, ${destinations.size} unique destinations across ${transactions.length} transactions`
1675
1778
  };
1676
1779
  }
1677
- computeComplianceAdherenceFactor(agentId) {
1678
- const tasks = this.store.queryTasks((t) => t.agentId === agentId);
1679
- const transactions = this.store.getTransactionsByAgent(agentId);
1780
+ /**
1781
+ * Compliance adherence factor: agents that follow the task-confirm-evidence workflow
1782
+ * demonstrate higher operational integrity.
1783
+ *
1784
+ * Scoring:
1785
+ * - Base score: 50 (neutral)
1786
+ * - +30 max for evidence rate (confirmedTasksWithEvidence / confirmedTasks)
1787
+ * - +20 max for coverage rate (tasks / transactions, capped at 1.0)
1788
+ *
1789
+ * The rationale: tasks with evidence prove the agent completed auditable work.
1790
+ * Coverage rate measures what fraction of financial activity has corresponding
1791
+ * task tracking — higher coverage means better compliance discipline.
1792
+ */
1793
+ computeComplianceAdherenceFactor(data) {
1794
+ const { tasks, transactions } = data;
1680
1795
  const confirmedTasks = tasks.filter((t) => t.status === "confirmed");
1681
1796
  const tasksWithEvidence = confirmedTasks.filter(
1682
1797
  (t) => t.providedEvidence !== null && Object.keys(t.providedEvidence).length > 0
@@ -1693,7 +1808,7 @@ var TrustScorer = class {
1693
1808
  return {
1694
1809
  name: "compliance_adherence",
1695
1810
  score: clamp(score, 0, 100),
1696
- weight: 0.15,
1811
+ weight: WEIGHT_COMPLIANCE,
1697
1812
  description: `${tasksWithEvidence.length} tasks with evidence, ${transactions.length} total transactions`
1698
1813
  };
1699
1814
  }
@@ -1709,6 +1824,20 @@ var TrustScorer = class {
1709
1824
  factors.push(this.computeRoundAmountRisk(tx));
1710
1825
  return factors;
1711
1826
  }
1827
+ /**
1828
+ * Amount risk: higher transaction amounts carry inherently higher risk.
1829
+ *
1830
+ * Tiers are aligned with common fintech thresholds:
1831
+ * - <$100: near-zero risk (5), micro-transactions
1832
+ * - <$1K: low risk (15), typical consumer spending
1833
+ * - <$10K: moderate (30), approaches CTR reporting thresholds
1834
+ * - <$50K: elevated (55), large business transactions
1835
+ * - <$100K: high (75), requires enhanced due diligence
1836
+ * - $100K+: very high (95), institutional-scale transfers
1837
+ *
1838
+ * Additional +20 penalty when amount exceeds 5x the agent's historical average,
1839
+ * detecting sudden spending spikes that could indicate account compromise.
1840
+ */
1712
1841
  computeAmountRisk(tx) {
1713
1842
  const amount = parseAmount(tx.amount);
1714
1843
  if (isNaN(amount)) {
@@ -1787,6 +1916,21 @@ var TrustScorer = class {
1787
1916
  description: `Agent anomaly rate: ${Math.round(anomalyRate * 100)}%`
1788
1917
  };
1789
1918
  }
1919
+ /**
1920
+ * Round amount risk: round numbers are a structuring indicator in AML heuristics.
1921
+ *
1922
+ * Money launderers often transact in round amounts (e.g., $10,000 exactly) or
1923
+ * just under regulatory thresholds (e.g., $9,500 to avoid $10K CTR filing).
1924
+ *
1925
+ * Scoring:
1926
+ * - Non-round amounts: 5 (baseline, most legitimate transactions)
1927
+ * - Multiples of $1,000: 15 (mildly suspicious)
1928
+ * - Multiples of $10,000: 25 (more suspicious)
1929
+ * - Amounts $9,000-$10,000: +20 penalty (classic structuring band)
1930
+ *
1931
+ * This factor alone rarely triggers flagging — it contributes to the composite
1932
+ * risk score alongside amount, frequency, and destination analysis.
1933
+ */
1790
1934
  computeRoundAmountRisk(tx) {
1791
1935
  const amount = parseAmount(tx.amount);
1792
1936
  if (isNaN(amount)) {
@@ -1809,15 +1953,15 @@ var TrustScorer = class {
1809
1953
  // Scoring helpers
1810
1954
  // --------------------------------------------------------------------------
1811
1955
  scoreToLevel(score) {
1812
- if (score >= 90) return "verified";
1813
- if (score >= 70) return "high";
1814
- if (score >= 50) return "medium";
1815
- if (score >= 30) return "low";
1956
+ if (score >= TRUST_LEVEL_VERIFIED) return "verified";
1957
+ if (score >= TRUST_LEVEL_HIGH) return "high";
1958
+ if (score >= TRUST_LEVEL_MEDIUM) return "medium";
1959
+ if (score >= TRUST_LEVEL_LOW) return "low";
1816
1960
  return "untrusted";
1817
1961
  }
1818
1962
  riskScoreToLevel(score) {
1819
- if (score >= 80) return "critical";
1820
- if (score >= 60) return "high";
1963
+ if (score >= RISK_BLOCK_THRESHOLD) return "critical";
1964
+ if (score >= RISK_FLAG_THRESHOLD) return "high";
1821
1965
  if (score >= 35) return "medium";
1822
1966
  return "low";
1823
1967
  }
@@ -2185,9 +2329,10 @@ var AnomalyDetector = class {
2185
2329
  try {
2186
2330
  cb(anomaly);
2187
2331
  } catch (error) {
2188
- if (this.config.debug) {
2189
- console.debug("[Kontext] Anomaly callback error:", error);
2190
- }
2332
+ console.warn(
2333
+ `[Kontext] Anomaly callback error for ${anomaly.type} (${anomaly.id}):`,
2334
+ error instanceof Error ? error.message : error
2335
+ );
2191
2336
  }
2192
2337
  }
2193
2338
  }
@@ -2354,6 +2499,48 @@ var UsdcCompliance = class _UsdcCompliance {
2354
2499
  static getSupportedChains() {
2355
2500
  return Object.keys(USDC_CONTRACTS);
2356
2501
  }
2502
+ /**
2503
+ * Add new sanctioned addresses at runtime.
2504
+ * Normalizes all addresses to lowercase for consistent matching.
2505
+ * Skips addresses that are already in the list.
2506
+ *
2507
+ * @param addresses - Array of Ethereum addresses to add
2508
+ * @returns The count of newly added addresses (excluding duplicates)
2509
+ */
2510
+ static addSanctionedAddresses(addresses) {
2511
+ let added = 0;
2512
+ for (const addr of addresses) {
2513
+ const lower = addr.toLowerCase();
2514
+ if (!SANCTIONED_SET.has(lower)) {
2515
+ SANCTIONED_ADDRESSES.push(addr);
2516
+ SANCTIONED_SET.add(lower);
2517
+ added++;
2518
+ }
2519
+ }
2520
+ return added;
2521
+ }
2522
+ /**
2523
+ * Replace the entire sanctioned addresses list at runtime.
2524
+ * Clears existing entries and rebuilds from the provided array.
2525
+ *
2526
+ * @param addresses - The new complete list of sanctioned addresses
2527
+ */
2528
+ static replaceSanctionedAddresses(addresses) {
2529
+ SANCTIONED_ADDRESSES.length = 0;
2530
+ SANCTIONED_ADDRESSES.push(...addresses);
2531
+ SANCTIONED_SET.clear();
2532
+ for (const addr of addresses) {
2533
+ SANCTIONED_SET.add(addr.toLowerCase());
2534
+ }
2535
+ }
2536
+ /**
2537
+ * Get the current number of addresses in the sanctions list.
2538
+ *
2539
+ * @returns The size of the current sanctions list
2540
+ */
2541
+ static getSanctionsListSize() {
2542
+ return SANCTIONED_SET.size;
2543
+ }
2357
2544
  // --------------------------------------------------------------------------
2358
2545
  // Individual compliance checks
2359
2546
  // --------------------------------------------------------------------------
@@ -2506,6 +2693,12 @@ var Kontext = class _Kontext {
2506
2693
  this.config = config;
2507
2694
  this.mode = config.apiKey ? "cloud" : "local";
2508
2695
  this.store = new KontextStore();
2696
+ if (config.metadataSchema && typeof config.metadataSchema.parse !== "function") {
2697
+ throw new KontextError(
2698
+ "INITIALIZATION_ERROR" /* INITIALIZATION_ERROR */,
2699
+ "metadataSchema must have a parse() method"
2700
+ );
2701
+ }
2509
2702
  if (config.storage) {
2510
2703
  this.store.setStorageAdapter(config.storage);
2511
2704
  }
@@ -2585,6 +2778,24 @@ var Kontext = class _Kontext {
2585
2778
  };
2586
2779
  }
2587
2780
  // --------------------------------------------------------------------------
2781
+ // Internal Helpers
2782
+ // --------------------------------------------------------------------------
2783
+ /**
2784
+ * Validate metadata against the configured schema, if any.
2785
+ * No-op when metadataSchema is not configured.
2786
+ */
2787
+ validateMetadata(metadata) {
2788
+ if (!metadata || !this.config.metadataSchema) return;
2789
+ try {
2790
+ this.config.metadataSchema.parse(metadata);
2791
+ } catch (err) {
2792
+ throw new KontextError(
2793
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
2794
+ `Metadata validation failed: ${err instanceof Error ? err.message : String(err)}`
2795
+ );
2796
+ }
2797
+ }
2798
+ // --------------------------------------------------------------------------
2588
2799
  // Action Logging
2589
2800
  // --------------------------------------------------------------------------
2590
2801
  /**
@@ -2594,6 +2805,7 @@ var Kontext = class _Kontext {
2594
2805
  * @returns The created action log entry
2595
2806
  */
2596
2807
  async log(input) {
2808
+ this.validateMetadata(input.metadata);
2597
2809
  const action = await this.logger.log(input);
2598
2810
  if (this.anomalyDetector.isEnabled()) {
2599
2811
  this.anomalyDetector.evaluateAction(action);
@@ -2607,6 +2819,7 @@ var Kontext = class _Kontext {
2607
2819
  * @returns The created transaction record
2608
2820
  */
2609
2821
  async logTransaction(input) {
2822
+ this.validateMetadata(input.metadata);
2610
2823
  const record = await this.logger.logTransaction(input);
2611
2824
  if (this.anomalyDetector.isEnabled()) {
2612
2825
  this.anomalyDetector.evaluateTransaction(record);
@@ -2648,6 +2861,7 @@ var Kontext = class _Kontext {
2648
2861
  * @returns The created task
2649
2862
  */
2650
2863
  async createTask(input) {
2864
+ this.validateMetadata(input.metadata);
2651
2865
  return this.taskManager.createTask(input);
2652
2866
  }
2653
2867
  /**
@@ -3033,10 +3247,10 @@ var Kontext = class _Kontext {
3033
3247
  };
3034
3248
  const hash = crypto$1.createHash("sha256");
3035
3249
  hash.update(JSON.stringify(certificateContent));
3036
- const signature = hash.digest("hex");
3250
+ const contentHash = hash.digest("hex");
3037
3251
  return {
3038
3252
  ...certificateContent,
3039
- signature
3253
+ contentHash
3040
3254
  };
3041
3255
  }
3042
3256
  // --------------------------------------------------------------------------
@@ -4708,8 +4922,6 @@ var GasStationManager = class {
4708
4922
  return NATIVE_TOKENS[chain] ?? "ETH";
4709
4923
  }
4710
4924
  };
4711
-
4712
- // src/webhooks.ts
4713
4925
  var DEFAULT_RETRY_CONFIG = {
4714
4926
  maxRetries: 3,
4715
4927
  baseDelayMs: 1e3,
@@ -4774,15 +4986,25 @@ var WebhookManager = class {
4774
4986
  }
4775
4987
  /**
4776
4988
  * Get all registered webhooks.
4989
+ * Secrets are redacted to prevent accidental exposure in logs or API responses.
4777
4990
  */
4778
4991
  getWebhooks() {
4779
- return Array.from(this.webhooks.values());
4992
+ return Array.from(this.webhooks.values()).map((w) => ({
4993
+ ...w,
4994
+ secret: w.secret ? "***REDACTED***" : w.secret
4995
+ }));
4780
4996
  }
4781
4997
  /**
4782
4998
  * Get a specific webhook by ID.
4999
+ * The secret is redacted to prevent accidental exposure in logs or API responses.
4783
5000
  */
4784
5001
  getWebhook(webhookId) {
4785
- return this.webhooks.get(webhookId);
5002
+ const webhook = this.webhooks.get(webhookId);
5003
+ if (!webhook) return void 0;
5004
+ return {
5005
+ ...webhook,
5006
+ secret: webhook.secret ? "***REDACTED***" : webhook.secret
5007
+ };
4786
5008
  }
4787
5009
  /**
4788
5010
  * Get delivery results for a specific webhook or all webhooks.
@@ -4949,227 +5171,223 @@ var WebhookManager = class {
4949
5171
  };
4950
5172
  }
4951
5173
  async computeSignature(payload, secret) {
4952
- const { createHmac } = await import('crypto');
4953
- const hmac = createHmac("sha256", secret);
5174
+ const hmac = crypto$1.createHmac("sha256", secret);
4954
5175
  hmac.update(JSON.stringify(payload));
4955
5176
  return hmac.digest("hex");
4956
5177
  }
4957
5178
  sleep(ms) {
4958
5179
  return new Promise((resolve2) => setTimeout(resolve2, ms));
4959
5180
  }
5181
+ /**
5182
+ * Verify a webhook signature using constant-time comparison to prevent
5183
+ * timing attacks. Use this in your webhook handler to validate incoming
5184
+ * payloads from Kontext.
5185
+ *
5186
+ * @param payload - The raw JSON payload body (string)
5187
+ * @param signature - The signature from the X-Kontext-Signature header
5188
+ * @param secret - The webhook secret used during registration
5189
+ * @returns Whether the signature is valid
5190
+ *
5191
+ * @example
5192
+ * ```typescript
5193
+ * const isValid = WebhookManager.verifySignature(
5194
+ * req.body, // raw JSON string
5195
+ * req.headers['x-kontext-signature'],
5196
+ * 'my-webhook-secret',
5197
+ * );
5198
+ * if (!isValid) return res.status(401).send('Invalid signature');
5199
+ * ```
5200
+ */
5201
+ static verifySignature(payload, signature, secret) {
5202
+ const hmac = crypto$1.createHmac("sha256", secret);
5203
+ hmac.update(payload);
5204
+ const expected = hmac.digest("hex");
5205
+ if (expected.length !== signature.length) {
5206
+ return false;
5207
+ }
5208
+ return crypto$1.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
5209
+ }
4960
5210
  };
4961
5211
 
4962
5212
  // src/integrations/vercel-ai.ts
4963
5213
  function kontextMiddleware(kontext, options) {
4964
- const agentId = options?.agentId ?? "vercel-ai";
4965
- const logToolArgs = options?.logToolArgs ?? false;
4966
- const financialTools = options?.financialTools ?? [];
4967
- const defaultCurrency = options?.defaultCurrency ?? "USDC";
4968
- const trustThreshold = options?.trustThreshold;
4969
- const onBlocked = options?.onBlocked;
5214
+ const cfg = {
5215
+ agentId: options?.agentId ?? "vercel-ai",
5216
+ logToolArgs: options?.logToolArgs ?? false,
5217
+ financialTools: options?.financialTools ?? [],
5218
+ defaultCurrency: options?.defaultCurrency ?? "USDC",
5219
+ trustThreshold: options?.trustThreshold,
5220
+ onBlocked: options?.onBlocked
5221
+ };
4970
5222
  return {
4971
- /**
4972
- * Logs the AI request parameters before the model is invoked.
4973
- * Captures the operation type, model ID, tool count, and generation settings.
4974
- */
4975
- transformParams: async ({ params, type }) => {
4976
- const modelInfo = params["model"];
4977
- const tools = params["tools"];
4978
- await kontext.log({
4979
- type: `ai_${type}`,
4980
- description: `AI ${type} request to ${modelInfo?.modelId ?? "unknown"} model`,
4981
- agentId,
4982
- metadata: {
4983
- model: modelInfo?.modelId ?? "unknown",
4984
- toolCount: Array.isArray(tools) ? tools.length : 0,
4985
- maxTokens: params["maxTokens"] ?? null,
4986
- temperature: params["temperature"] ?? null,
4987
- operationType: type
4988
- }
4989
- });
4990
- return params;
4991
- },
4992
- /**
4993
- * Wraps synchronous generation (`generateText`, `generateObject`).
4994
- * After the model returns, logs every tool call individually and the
4995
- * overall response. For financial tools, automatically creates
4996
- * compliance-tracked transaction records.
4997
- */
4998
- wrapGenerate: async ({
4999
- doGenerate,
5000
- params
5001
- }) => {
5002
- const startTime = Date.now();
5003
- if (trustThreshold !== void 0) {
5004
- const trustScore = await kontext.getTrustScore(agentId);
5005
- if (trustScore.score < trustThreshold) {
5006
- await kontext.log({
5007
- type: "ai_blocked",
5008
- description: `AI generation blocked: agent trust score ${trustScore.score} below threshold ${trustThreshold}`,
5009
- agentId,
5010
- metadata: {
5011
- trustScore: trustScore.score,
5012
- trustLevel: trustScore.level,
5013
- threshold: trustThreshold
5014
- }
5015
- });
5016
- throw new Error(
5017
- `Kontext: AI generation blocked. Agent "${agentId}" trust score (${trustScore.score}) is below the required threshold (${trustThreshold}).`
5018
- );
5019
- }
5020
- }
5021
- const result = await doGenerate();
5022
- const duration = Date.now() - startTime;
5023
- const modelInfo = params["model"];
5024
- const toolCalls = result["toolCalls"];
5025
- if (toolCalls && toolCalls.length > 0) {
5026
- for (const toolCall of toolCalls) {
5027
- if (trustThreshold !== void 0 && financialTools.includes(toolCall.toolName)) {
5028
- const trustScore = await kontext.getTrustScore(agentId);
5029
- if (trustScore.score < trustThreshold) {
5030
- if (onBlocked) {
5031
- onBlocked({ toolName: toolCall.toolName, args: toolCall.args }, `Trust score ${trustScore.score} below threshold ${trustThreshold}`);
5032
- }
5033
- await kontext.log({
5034
- type: "ai_tool_blocked",
5035
- description: `Financial tool "${toolCall.toolName}" blocked: trust score ${trustScore.score} below threshold ${trustThreshold}`,
5036
- agentId,
5037
- metadata: {
5038
- toolName: toolCall.toolName,
5039
- trustScore: trustScore.score,
5040
- threshold: trustThreshold
5041
- }
5042
- });
5043
- continue;
5044
- }
5045
- }
5046
- await kontext.log({
5047
- type: "ai_tool_call",
5048
- description: `Tool call: ${toolCall.toolName}`,
5049
- agentId,
5050
- metadata: {
5051
- toolName: toolCall.toolName,
5052
- args: logToolArgs ? toolCall.args : "[redacted]",
5053
- duration,
5054
- model: modelInfo?.modelId ?? "unknown"
5055
- }
5223
+ transformParams: (ctx) => logTransformParams(kontext, cfg, ctx),
5224
+ wrapGenerate: (ctx) => wrapGenerateWithAudit(kontext, cfg, ctx),
5225
+ wrapStream: (ctx) => wrapStreamWithAudit(kontext, cfg, ctx)
5226
+ };
5227
+ }
5228
+ async function logTransformParams(kontext, cfg, { params, type }) {
5229
+ const modelInfo = params["model"];
5230
+ const tools = params["tools"];
5231
+ await kontext.log({
5232
+ type: `ai_${type}`,
5233
+ description: `AI ${type} request to ${modelInfo?.modelId ?? "unknown"} model`,
5234
+ agentId: cfg.agentId,
5235
+ metadata: {
5236
+ model: modelInfo?.modelId ?? "unknown",
5237
+ toolCount: Array.isArray(tools) ? tools.length : 0,
5238
+ maxTokens: params["maxTokens"] ?? null,
5239
+ temperature: params["temperature"] ?? null,
5240
+ operationType: type
5241
+ }
5242
+ });
5243
+ return params;
5244
+ }
5245
+ async function wrapGenerateWithAudit(kontext, cfg, { doGenerate, params }) {
5246
+ const startTime = Date.now();
5247
+ await enforceAgentTrustThreshold(kontext, cfg);
5248
+ const result = await doGenerate();
5249
+ const duration = Date.now() - startTime;
5250
+ const modelId = extractModelId(params);
5251
+ const toolCalls = result["toolCalls"];
5252
+ if (toolCalls && toolCalls.length > 0) {
5253
+ for (const toolCall of toolCalls) {
5254
+ await processToolCall(kontext, cfg, toolCall, duration, modelId);
5255
+ }
5256
+ }
5257
+ await logGenerateCompletion(kontext, cfg, result, duration, modelId, toolCalls?.length ?? 0);
5258
+ return result;
5259
+ }
5260
+ async function wrapStreamWithAudit(kontext, cfg, { doStream, params }) {
5261
+ const startTime = Date.now();
5262
+ const modelId = extractModelId(params);
5263
+ await kontext.log({
5264
+ type: "ai_stream_start",
5265
+ description: `AI stream started for model ${modelId}`,
5266
+ agentId: cfg.agentId,
5267
+ metadata: { model: modelId, operationType: "stream" }
5268
+ });
5269
+ const { stream, ...rest } = await doStream();
5270
+ const toolCallsInStream = [];
5271
+ const transformedStream = stream.pipeThrough(
5272
+ new TransformStream({
5273
+ transform(chunk, controller) {
5274
+ controller.enqueue(chunk);
5275
+ if (chunk["type"] === "tool-call") {
5276
+ toolCallsInStream.push({
5277
+ toolName: chunk["toolName"],
5278
+ args: chunk["args"]
5056
5279
  });
5057
- if (financialTools.includes(toolCall.toolName)) {
5058
- const amount = extractAmount(toolCall.args);
5059
- if (amount !== null) {
5060
- await kontext.log({
5061
- type: "ai_financial_tool_call",
5062
- description: `Financial tool "${toolCall.toolName}" invoked with amount ${amount} ${defaultCurrency}`,
5063
- agentId,
5064
- metadata: {
5065
- toolName: toolCall.toolName,
5066
- amount: amount.toString(),
5067
- currency: defaultCurrency,
5068
- toolArgs: logToolArgs ? toolCall.args : "[redacted]"
5069
- }
5070
- });
5071
- }
5072
- }
5073
5280
  }
5281
+ },
5282
+ async flush() {
5283
+ const duration = Date.now() - startTime;
5284
+ await logStreamToolCalls(kontext, cfg, toolCallsInStream, duration, modelId);
5285
+ await kontext.log({
5286
+ type: "ai_stream_complete",
5287
+ description: `AI stream completed in ${duration}ms with ${toolCallsInStream.length} tool call(s)`,
5288
+ agentId: cfg.agentId,
5289
+ metadata: { duration, toolCallCount: toolCallsInStream.length, model: modelId }
5290
+ });
5074
5291
  }
5075
- const usage = result["usage"];
5076
- await kontext.log({
5077
- type: "ai_response",
5078
- description: `AI response completed in ${duration}ms`,
5079
- agentId,
5080
- metadata: {
5081
- duration,
5082
- toolCallCount: toolCalls?.length ?? 0,
5083
- finishReason: result["finishReason"] ?? "unknown",
5084
- promptTokens: usage?.promptTokens ?? null,
5085
- completionTokens: usage?.completionTokens ?? null,
5086
- totalTokens: usage?.totalTokens ?? null,
5087
- model: modelInfo?.modelId ?? "unknown"
5088
- }
5089
- });
5090
- return result;
5091
- },
5092
- /**
5093
- * Wraps streaming generation (`streamText`).
5094
- * Pipes the response stream through a transform that monitors for
5095
- * tool call chunks. On stream completion, logs the overall duration
5096
- * and any tool calls that occurred during the stream.
5097
- */
5098
- wrapStream: async ({
5099
- doStream,
5100
- params
5101
- }) => {
5102
- const startTime = Date.now();
5103
- const modelInfo = params["model"];
5292
+ })
5293
+ );
5294
+ return { stream: transformedStream, ...rest };
5295
+ }
5296
+ function extractModelId(params) {
5297
+ return params["model"]?.modelId ?? "unknown";
5298
+ }
5299
+ async function enforceAgentTrustThreshold(kontext, cfg) {
5300
+ if (cfg.trustThreshold === void 0) return;
5301
+ const trustScore = await kontext.getTrustScore(cfg.agentId);
5302
+ if (trustScore.score < cfg.trustThreshold) {
5303
+ await kontext.log({
5304
+ type: "ai_blocked",
5305
+ description: `AI generation blocked: agent trust score ${trustScore.score} below threshold ${cfg.trustThreshold}`,
5306
+ agentId: cfg.agentId,
5307
+ metadata: { trustScore: trustScore.score, trustLevel: trustScore.level, threshold: cfg.trustThreshold }
5308
+ });
5309
+ throw new Error(
5310
+ `Kontext: AI generation blocked. Agent "${cfg.agentId}" trust score (${trustScore.score}) is below the required threshold (${cfg.trustThreshold}).`
5311
+ );
5312
+ }
5313
+ }
5314
+ async function processToolCall(kontext, cfg, toolCall, duration, modelId) {
5315
+ if (cfg.trustThreshold !== void 0 && cfg.financialTools.includes(toolCall.toolName)) {
5316
+ const trustScore = await kontext.getTrustScore(cfg.agentId);
5317
+ if (trustScore.score < cfg.trustThreshold) {
5318
+ cfg.onBlocked?.({ toolName: toolCall.toolName, args: toolCall.args }, `Trust score ${trustScore.score} below threshold ${cfg.trustThreshold}`);
5104
5319
  await kontext.log({
5105
- type: "ai_stream_start",
5106
- description: `AI stream started for model ${modelInfo?.modelId ?? "unknown"}`,
5107
- agentId,
5108
- metadata: {
5109
- model: modelInfo?.modelId ?? "unknown",
5110
- operationType: "stream"
5111
- }
5320
+ type: "ai_tool_blocked",
5321
+ description: `Financial tool "${toolCall.toolName}" blocked: trust score ${trustScore.score} below threshold ${cfg.trustThreshold}`,
5322
+ agentId: cfg.agentId,
5323
+ metadata: { toolName: toolCall.toolName, trustScore: trustScore.score, threshold: cfg.trustThreshold }
5112
5324
  });
5113
- const { stream, ...rest } = await doStream();
5114
- const toolCallsInStream = [];
5115
- const transformedStream = stream.pipeThrough(
5116
- new TransformStream({
5117
- transform(chunk, controller) {
5118
- controller.enqueue(chunk);
5119
- if (chunk["type"] === "tool-call") {
5120
- const toolName = chunk["toolName"];
5121
- const args = chunk["args"];
5122
- toolCallsInStream.push({ toolName, args });
5123
- }
5124
- },
5125
- async flush() {
5126
- const duration = Date.now() - startTime;
5127
- for (const toolCall of toolCallsInStream) {
5128
- await kontext.log({
5129
- type: "ai_tool_call",
5130
- description: `Tool call (stream): ${toolCall.toolName}`,
5131
- agentId,
5132
- metadata: {
5133
- toolName: toolCall.toolName,
5134
- args: logToolArgs ? toolCall.args : "[redacted]",
5135
- duration,
5136
- model: modelInfo?.modelId ?? "unknown",
5137
- source: "stream"
5138
- }
5139
- });
5140
- if (financialTools.includes(toolCall.toolName)) {
5141
- const amount = extractAmount(toolCall.args);
5142
- if (amount !== null) {
5143
- await kontext.log({
5144
- type: "ai_financial_tool_call",
5145
- description: `Financial tool "${toolCall.toolName}" invoked via stream with amount ${amount} ${defaultCurrency}`,
5146
- agentId,
5147
- metadata: {
5148
- toolName: toolCall.toolName,
5149
- amount: amount.toString(),
5150
- currency: defaultCurrency,
5151
- source: "stream"
5152
- }
5153
- });
5154
- }
5155
- }
5156
- }
5157
- await kontext.log({
5158
- type: "ai_stream_complete",
5159
- description: `AI stream completed in ${duration}ms with ${toolCallsInStream.length} tool call(s)`,
5160
- agentId,
5161
- metadata: {
5162
- duration,
5163
- toolCallCount: toolCallsInStream.length,
5164
- model: modelInfo?.modelId ?? "unknown"
5165
- }
5166
- });
5167
- }
5168
- })
5169
- );
5170
- return { stream: transformedStream, ...rest };
5325
+ return;
5171
5326
  }
5172
- };
5327
+ }
5328
+ await kontext.log({
5329
+ type: "ai_tool_call",
5330
+ description: `Tool call: ${toolCall.toolName}`,
5331
+ agentId: cfg.agentId,
5332
+ metadata: {
5333
+ toolName: toolCall.toolName,
5334
+ args: cfg.logToolArgs ? toolCall.args : "[redacted]",
5335
+ duration,
5336
+ model: modelId
5337
+ }
5338
+ });
5339
+ await logFinancialToolCall(kontext, cfg, toolCall);
5340
+ }
5341
+ async function logFinancialToolCall(kontext, cfg, toolCall, source) {
5342
+ if (!cfg.financialTools.includes(toolCall.toolName)) return;
5343
+ const amount = extractAmount(toolCall.args);
5344
+ if (amount === null) return;
5345
+ await kontext.log({
5346
+ type: "ai_financial_tool_call",
5347
+ description: `Financial tool "${toolCall.toolName}" invoked${source ? ` via ${source}` : ""} with amount ${amount} ${cfg.defaultCurrency}`,
5348
+ agentId: cfg.agentId,
5349
+ metadata: {
5350
+ toolName: toolCall.toolName,
5351
+ amount: amount.toString(),
5352
+ currency: cfg.defaultCurrency,
5353
+ ...cfg.logToolArgs ? { toolArgs: toolCall.args } : {},
5354
+ ...source ? { source } : {}
5355
+ }
5356
+ });
5357
+ }
5358
+ async function logGenerateCompletion(kontext, cfg, result, duration, modelId, toolCallCount) {
5359
+ const usage = result["usage"];
5360
+ await kontext.log({
5361
+ type: "ai_response",
5362
+ description: `AI response completed in ${duration}ms`,
5363
+ agentId: cfg.agentId,
5364
+ metadata: {
5365
+ duration,
5366
+ toolCallCount,
5367
+ finishReason: result["finishReason"] ?? "unknown",
5368
+ promptTokens: usage?.promptTokens ?? null,
5369
+ completionTokens: usage?.completionTokens ?? null,
5370
+ totalTokens: usage?.totalTokens ?? null,
5371
+ model: modelId
5372
+ }
5373
+ });
5374
+ }
5375
+ async function logStreamToolCalls(kontext, cfg, toolCalls, duration, modelId) {
5376
+ for (const toolCall of toolCalls) {
5377
+ await kontext.log({
5378
+ type: "ai_tool_call",
5379
+ description: `Tool call (stream): ${toolCall.toolName}`,
5380
+ agentId: cfg.agentId,
5381
+ metadata: {
5382
+ toolName: toolCall.toolName,
5383
+ args: cfg.logToolArgs ? toolCall.args : "[redacted]",
5384
+ duration,
5385
+ model: modelId,
5386
+ source: "stream"
5387
+ }
5388
+ });
5389
+ await logFinancialToolCall(kontext, cfg, toolCall, "stream");
5390
+ }
5173
5391
  }
5174
5392
  function kontextWrapModel(model, kontext, options) {
5175
5393
  const middleware = kontextMiddleware(kontext, options);
@@ -5222,10 +5440,18 @@ function createKontextAI(model, input) {
5222
5440
  return { model: wrappedModel, kontext };
5223
5441
  }
5224
5442
  function withKontext(handler, options) {
5443
+ const resolvedProjectId = options?.projectId ?? process.env["KONTEXT_PROJECT_ID"];
5444
+ if (!resolvedProjectId) {
5445
+ throw new Error("Kontext: projectId is required. Provide it via options or set KONTEXT_PROJECT_ID env var.");
5446
+ }
5447
+ const resolvedApiKey = options?.apiKey ?? process.env["KONTEXT_API_KEY"];
5448
+ if (options?.apiKey !== void 0 && options.apiKey.trim() === "") {
5449
+ throw new Error("Kontext: apiKey was provided but is empty.");
5450
+ }
5225
5451
  const kontext = Kontext.init({
5226
- projectId: options?.projectId ?? process.env["KONTEXT_PROJECT_ID"] ?? "default",
5452
+ projectId: resolvedProjectId,
5227
5453
  environment: options?.environment ?? (process.env["NODE_ENV"] === "production" ? "production" : "development"),
5228
- apiKey: options?.apiKey ?? process.env["KONTEXT_API_KEY"],
5454
+ apiKey: resolvedApiKey,
5229
5455
  debug: options?.debug
5230
5456
  });
5231
5457
  const agentId = options?.agentId ?? "nextjs-route";