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.d.mts +90 -23
- package/dist/index.d.ts +90 -23
- package/dist/index.js +510 -284
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +510 -284
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
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,
|
|
91
|
-
this.storageAdapter.save(STORAGE_KEYS.transactions,
|
|
92
|
-
this.storageAdapter.save(
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >=
|
|
1540
|
-
const recommendation = riskScore >=
|
|
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
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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:
|
|
1644
|
+
weight: WEIGHT_HISTORY,
|
|
1577
1645
|
description: `Agent has ${count} recorded actions`
|
|
1578
1646
|
};
|
|
1579
1647
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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:
|
|
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:
|
|
1676
|
+
weight: WEIGHT_TASK_COMPLETION,
|
|
1601
1677
|
description: `${confirmed}/${totalTasks} tasks confirmed (${Math.round(completionRate * 100)}% rate)`
|
|
1602
1678
|
};
|
|
1603
1679
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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:
|
|
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:
|
|
1720
|
+
weight: WEIGHT_ANOMALY,
|
|
1632
1721
|
description: `${anomalyCount} anomalies across ${actionCount} actions (${Math.round(anomalyRate * 100)}% rate)`
|
|
1633
1722
|
};
|
|
1634
1723
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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:
|
|
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 >=
|
|
1813
|
-
if (score >=
|
|
1814
|
-
if (score >=
|
|
1815
|
-
if (score >=
|
|
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 >=
|
|
1820
|
-
if (score >=
|
|
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
|
-
|
|
2189
|
-
|
|
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
|
|
3250
|
+
const contentHash = hash.digest("hex");
|
|
3037
3251
|
return {
|
|
3038
3252
|
...certificateContent,
|
|
3039
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
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
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
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
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
}
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
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: "
|
|
5106
|
-
description: `
|
|
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
|
-
|
|
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:
|
|
5452
|
+
projectId: resolvedProjectId,
|
|
5227
5453
|
environment: options?.environment ?? (process.env["NODE_ENV"] === "production" ? "production" : "development"),
|
|
5228
|
-
apiKey:
|
|
5454
|
+
apiKey: resolvedApiKey,
|
|
5229
5455
|
debug: options?.debug
|
|
5230
5456
|
});
|
|
5231
5457
|
const agentId = options?.agentId ?? "nextjs-route";
|