agentbnb 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,666 @@
1
+ import {
2
+ findPeer
3
+ } from "./chunk-BEI5MTNZ.js";
4
+ import {
5
+ AgentBnBError
6
+ } from "./chunk-TQMI73LL.js";
7
+
8
+ // src/credit/ledger.ts
9
+ import Database from "better-sqlite3";
10
+ import { randomUUID } from "crypto";
11
+ var CREDIT_SCHEMA = `
12
+ CREATE TABLE IF NOT EXISTS credit_balances (
13
+ owner TEXT PRIMARY KEY,
14
+ balance INTEGER NOT NULL DEFAULT 0,
15
+ updated_at TEXT NOT NULL
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS credit_transactions (
19
+ id TEXT PRIMARY KEY,
20
+ owner TEXT NOT NULL,
21
+ amount INTEGER NOT NULL,
22
+ reason TEXT NOT NULL,
23
+ reference_id TEXT,
24
+ created_at TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS credit_escrow (
28
+ id TEXT PRIMARY KEY,
29
+ owner TEXT NOT NULL,
30
+ amount INTEGER NOT NULL,
31
+ card_id TEXT NOT NULL,
32
+ status TEXT NOT NULL DEFAULT 'held',
33
+ created_at TEXT NOT NULL,
34
+ settled_at TEXT
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS idx_transactions_owner ON credit_transactions(owner, created_at);
38
+ CREATE INDEX IF NOT EXISTS idx_escrow_owner ON credit_escrow(owner);
39
+ `;
40
+ function openCreditDb(path = ":memory:") {
41
+ const db = new Database(path);
42
+ db.pragma("journal_mode = WAL");
43
+ db.pragma("foreign_keys = ON");
44
+ db.exec(CREDIT_SCHEMA);
45
+ return db;
46
+ }
47
+ function bootstrapAgent(db, owner, amount = 100) {
48
+ const now = (/* @__PURE__ */ new Date()).toISOString();
49
+ db.transaction(() => {
50
+ const result = db.prepare("INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, ?, ?)").run(owner, amount, now);
51
+ if (result.changes > 0) {
52
+ db.prepare(
53
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
54
+ ).run(randomUUID(), owner, amount, "bootstrap", null, now);
55
+ }
56
+ })();
57
+ }
58
+ function getBalance(db, owner) {
59
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
60
+ return row?.balance ?? 0;
61
+ }
62
+ function getTransactions(db, owner, limit = 100) {
63
+ return db.prepare(
64
+ "SELECT id, owner, amount, reason, reference_id, created_at FROM credit_transactions WHERE owner = ? ORDER BY created_at DESC LIMIT ?"
65
+ ).all(owner, limit);
66
+ }
67
+ function recordEarning(db, owner, amount, _cardId, receiptNonce) {
68
+ const now = (/* @__PURE__ */ new Date()).toISOString();
69
+ db.transaction(() => {
70
+ const existing = db.prepare(
71
+ "SELECT id FROM credit_transactions WHERE reference_id = ? AND reason = 'remote_earning'"
72
+ ).get(receiptNonce);
73
+ if (existing) return;
74
+ db.prepare(
75
+ "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
76
+ ).run(owner, now);
77
+ db.prepare(
78
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
79
+ ).run(amount, now, owner);
80
+ db.prepare(
81
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
82
+ ).run(randomUUID(), owner, amount, "remote_earning", receiptNonce, now);
83
+ })();
84
+ }
85
+
86
+ // src/autonomy/tiers.ts
87
+ import { randomUUID as randomUUID2 } from "crypto";
88
+ var DEFAULT_AUTONOMY_CONFIG = {
89
+ tier1_max_credits: 0,
90
+ tier2_max_credits: 0
91
+ };
92
+ function getAutonomyTier(creditAmount, config) {
93
+ if (creditAmount < config.tier1_max_credits) return 1;
94
+ if (creditAmount < config.tier2_max_credits) return 2;
95
+ return 3;
96
+ }
97
+ function insertAuditEvent(db, event) {
98
+ const isShareEvent = event.type === "auto_share" || event.type === "auto_share_notify" || event.type === "auto_share_pending";
99
+ const cardId = isShareEvent ? "system" : event.card_id;
100
+ const creditsCharged = isShareEvent ? 0 : event.credits;
101
+ const stmt = db.prepare(`
102
+ INSERT INTO request_log (
103
+ id, card_id, card_name, requester, status, latency_ms, credits_charged,
104
+ created_at, skill_id, action_type, tier_invoked
105
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
106
+ `);
107
+ stmt.run(
108
+ randomUUID2(),
109
+ cardId,
110
+ "autonomy-audit",
111
+ "self",
112
+ "success",
113
+ 0,
114
+ creditsCharged,
115
+ (/* @__PURE__ */ new Date()).toISOString(),
116
+ event.skill_id,
117
+ event.type,
118
+ event.tier_invoked
119
+ );
120
+ }
121
+
122
+ // src/credit/budget.ts
123
+ var DEFAULT_BUDGET_CONFIG = {
124
+ reserve_credits: 20
125
+ };
126
+ var BudgetManager = class {
127
+ /**
128
+ * Creates a new BudgetManager.
129
+ *
130
+ * @param creditDb - The credit SQLite database instance.
131
+ * @param owner - Agent owner identifier.
132
+ * @param config - Budget configuration. Defaults to DEFAULT_BUDGET_CONFIG (20 credit reserve).
133
+ */
134
+ constructor(creditDb, owner, config = DEFAULT_BUDGET_CONFIG) {
135
+ this.creditDb = creditDb;
136
+ this.owner = owner;
137
+ this.config = config;
138
+ }
139
+ /**
140
+ * Returns the number of credits available for spending.
141
+ * Computed as: max(0, balance - reserve_credits).
142
+ * Always returns a non-negative number — never goes below zero.
143
+ *
144
+ * @returns Available credits (balance minus reserve, floored at 0).
145
+ */
146
+ availableCredits() {
147
+ const balance = getBalance(this.creditDb, this.owner);
148
+ return Math.max(0, balance - this.config.reserve_credits);
149
+ }
150
+ /**
151
+ * Returns true if spending `amount` credits is permitted by budget rules.
152
+ *
153
+ * Rules:
154
+ * - Zero-cost calls (amount <= 0) always return true.
155
+ * - Any positive amount requires availableCredits() >= amount.
156
+ * - If balance is at or below the reserve floor, all positive-cost calls return false.
157
+ *
158
+ * @param amount - Number of credits to spend.
159
+ * @returns true if the spend is allowed, false if it would breach the reserve floor.
160
+ */
161
+ canSpend(amount) {
162
+ if (amount <= 0) return true;
163
+ return this.availableCredits() >= amount;
164
+ }
165
+ };
166
+
167
+ // src/registry/matcher.ts
168
+ function searchCards(db, query, filters = {}) {
169
+ const words = query.trim().split(/\s+/).map((w) => w.replace(/["*^{}():]/g, "")).filter((w) => w.length > 0);
170
+ if (words.length === 0) return [];
171
+ const ftsQuery = words.map((w) => `"${w}"`).join(" OR ");
172
+ const conditions = [];
173
+ const params = [ftsQuery];
174
+ if (filters.level !== void 0) {
175
+ conditions.push(`json_extract(cc.data, '$.level') = ?`);
176
+ params.push(filters.level);
177
+ }
178
+ if (filters.online !== void 0) {
179
+ conditions.push(`json_extract(cc.data, '$.availability.online') = ?`);
180
+ params.push(filters.online ? 1 : 0);
181
+ }
182
+ const whereClause = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
183
+ const sql = `
184
+ SELECT cc.data
185
+ FROM capability_cards cc
186
+ JOIN cards_fts ON cc.rowid = cards_fts.rowid
187
+ WHERE cards_fts MATCH ?
188
+ ${whereClause}
189
+ ORDER BY bm25(cards_fts)
190
+ LIMIT 50
191
+ `;
192
+ const stmt = db.prepare(sql);
193
+ const rows = stmt.all(...params);
194
+ const results = rows.map((row) => JSON.parse(row.data));
195
+ if (filters.apis_used && filters.apis_used.length > 0) {
196
+ const requiredApis = filters.apis_used;
197
+ return results.filter((card) => {
198
+ const cardApis = card.metadata?.apis_used ?? [];
199
+ return requiredApis.every((api) => cardApis.includes(api));
200
+ });
201
+ }
202
+ return results;
203
+ }
204
+ function filterCards(db, filters) {
205
+ const conditions = [];
206
+ const params = [];
207
+ if (filters.level !== void 0) {
208
+ conditions.push(`json_extract(data, '$.level') = ?`);
209
+ params.push(filters.level);
210
+ }
211
+ if (filters.online !== void 0) {
212
+ conditions.push(`json_extract(data, '$.availability.online') = ?`);
213
+ params.push(filters.online ? 1 : 0);
214
+ }
215
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
216
+ const sql = `SELECT data FROM capability_cards ${whereClause}`;
217
+ const stmt = db.prepare(sql);
218
+ const rows = stmt.all(...params);
219
+ return rows.map((row) => JSON.parse(row.data));
220
+ }
221
+
222
+ // src/gateway/client.ts
223
+ import { randomUUID as randomUUID3 } from "crypto";
224
+ async function requestCapability(opts) {
225
+ const { gatewayUrl, token, cardId, params = {}, timeoutMs = 3e4, escrowReceipt } = opts;
226
+ const id = randomUUID3();
227
+ const payload = {
228
+ jsonrpc: "2.0",
229
+ id,
230
+ method: "capability.execute",
231
+ params: {
232
+ card_id: cardId,
233
+ ...params,
234
+ ...escrowReceipt ? { escrow_receipt: escrowReceipt } : {}
235
+ }
236
+ };
237
+ const controller = new AbortController();
238
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
239
+ let response;
240
+ try {
241
+ response = await fetch(`${gatewayUrl}/rpc`, {
242
+ method: "POST",
243
+ headers: {
244
+ "Content-Type": "application/json",
245
+ Authorization: `Bearer ${token}`
246
+ },
247
+ body: JSON.stringify(payload),
248
+ signal: controller.signal
249
+ });
250
+ } catch (err) {
251
+ clearTimeout(timer);
252
+ const isTimeout = err instanceof Error && err.name === "AbortError";
253
+ throw new AgentBnBError(
254
+ isTimeout ? "Request timed out" : `Network error: ${String(err)}`,
255
+ isTimeout ? "TIMEOUT" : "NETWORK_ERROR"
256
+ );
257
+ } finally {
258
+ clearTimeout(timer);
259
+ }
260
+ const body = await response.json();
261
+ if (body.error) {
262
+ throw new AgentBnBError(body.error.message, `RPC_ERROR_${body.error.code}`);
263
+ }
264
+ return body.result;
265
+ }
266
+
267
+ // src/credit/escrow.ts
268
+ import { randomUUID as randomUUID4 } from "crypto";
269
+ function holdEscrow(db, owner, amount, cardId) {
270
+ const escrowId = randomUUID4();
271
+ const now = (/* @__PURE__ */ new Date()).toISOString();
272
+ const hold = db.transaction(() => {
273
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
274
+ if (!row || row.balance < amount) {
275
+ throw new AgentBnBError("Insufficient credits", "INSUFFICIENT_CREDITS");
276
+ }
277
+ db.prepare(
278
+ "UPDATE credit_balances SET balance = balance - ?, updated_at = ? WHERE owner = ? AND balance >= ?"
279
+ ).run(amount, now, owner, amount);
280
+ db.prepare(
281
+ "INSERT INTO credit_escrow (id, owner, amount, card_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)"
282
+ ).run(escrowId, owner, amount, cardId, "held", now);
283
+ db.prepare(
284
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
285
+ ).run(randomUUID4(), owner, -amount, "escrow_hold", escrowId, now);
286
+ });
287
+ hold();
288
+ return escrowId;
289
+ }
290
+ function settleEscrow(db, escrowId, recipientOwner) {
291
+ const now = (/* @__PURE__ */ new Date()).toISOString();
292
+ const settle = db.transaction(() => {
293
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
294
+ if (!escrow) {
295
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
296
+ }
297
+ if (escrow.status !== "held") {
298
+ throw new AgentBnBError(
299
+ `Escrow ${escrowId} is already ${escrow.status}`,
300
+ "ESCROW_ALREADY_SETTLED"
301
+ );
302
+ }
303
+ db.prepare(
304
+ "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
305
+ ).run(recipientOwner, now);
306
+ db.prepare(
307
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
308
+ ).run(escrow.amount, now, recipientOwner);
309
+ db.prepare(
310
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
311
+ ).run("settled", now, escrowId);
312
+ db.prepare(
313
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
314
+ ).run(randomUUID4(), recipientOwner, escrow.amount, "settlement", escrowId, now);
315
+ });
316
+ settle();
317
+ }
318
+ function releaseEscrow(db, escrowId) {
319
+ const now = (/* @__PURE__ */ new Date()).toISOString();
320
+ const release = db.transaction(() => {
321
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
322
+ if (!escrow) {
323
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
324
+ }
325
+ if (escrow.status !== "held") {
326
+ throw new AgentBnBError(
327
+ `Escrow ${escrowId} is already ${escrow.status}`,
328
+ "ESCROW_ALREADY_SETTLED"
329
+ );
330
+ }
331
+ db.prepare(
332
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
333
+ ).run(escrow.amount, now, escrow.owner);
334
+ db.prepare(
335
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
336
+ ).run("released", now, escrowId);
337
+ db.prepare(
338
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
339
+ ).run(randomUUID4(), escrow.owner, escrow.amount, "refund", escrowId, now);
340
+ });
341
+ release();
342
+ }
343
+ function confirmEscrowDebit(db, escrowId) {
344
+ const now = (/* @__PURE__ */ new Date()).toISOString();
345
+ const confirm = db.transaction(() => {
346
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
347
+ if (!escrow) {
348
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
349
+ }
350
+ if (escrow.status !== "held") {
351
+ throw new AgentBnBError(
352
+ `Escrow ${escrowId} is already ${escrow.status}`,
353
+ "ESCROW_ALREADY_SETTLED"
354
+ );
355
+ }
356
+ db.prepare(
357
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
358
+ ).run("settled", now, escrowId);
359
+ db.prepare(
360
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
361
+ ).run(randomUUID4(), escrow.owner, 0, "remote_settlement_confirmed", escrowId, now);
362
+ });
363
+ confirm();
364
+ }
365
+
366
+ // src/autonomy/pending-requests.ts
367
+ import { randomUUID as randomUUID5 } from "crypto";
368
+ function createPendingRequest(db, opts) {
369
+ const id = randomUUID5();
370
+ const now = (/* @__PURE__ */ new Date()).toISOString();
371
+ const paramsJson = opts.params !== void 0 ? JSON.stringify(opts.params) : null;
372
+ db.prepare(`
373
+ INSERT INTO pending_requests (
374
+ id, skill_query, max_cost_credits, selected_peer, selected_card_id,
375
+ selected_skill_id, credits, status, params, created_at, resolved_at
376
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, NULL)
377
+ `).run(
378
+ id,
379
+ opts.skill_query,
380
+ opts.max_cost_credits,
381
+ opts.selected_peer ?? null,
382
+ opts.selected_card_id ?? null,
383
+ opts.selected_skill_id ?? null,
384
+ opts.credits,
385
+ paramsJson,
386
+ now
387
+ );
388
+ return id;
389
+ }
390
+ function listPendingRequests(db) {
391
+ const rows = db.prepare(`SELECT * FROM pending_requests WHERE status = 'pending' ORDER BY created_at DESC`).all();
392
+ return rows;
393
+ }
394
+ function resolvePendingRequest(db, id, resolution) {
395
+ const now = (/* @__PURE__ */ new Date()).toISOString();
396
+ const result = db.prepare(
397
+ `UPDATE pending_requests SET status = ?, resolved_at = ? WHERE id = ?`
398
+ ).run(resolution, now, id);
399
+ if (result.changes === 0) {
400
+ throw new AgentBnBError(
401
+ `Pending request not found: ${id}`,
402
+ "NOT_FOUND"
403
+ );
404
+ }
405
+ }
406
+
407
+ // src/autonomy/auto-request.ts
408
+ function minMaxNormalize(values) {
409
+ if (values.length === 0) return [];
410
+ if (values.length === 1) return [1];
411
+ const min = Math.min(...values);
412
+ const max = Math.max(...values);
413
+ if (max === min) {
414
+ return values.map(() => 1);
415
+ }
416
+ return values.map((v) => (v - min) / (max - min));
417
+ }
418
+ function scorePeers(candidates, selfOwner) {
419
+ const eligible = candidates.filter((c) => c.card.owner !== selfOwner);
420
+ if (eligible.length === 0) return [];
421
+ const successRates = eligible.map((c) => c.card.metadata?.success_rate ?? 0.5);
422
+ const costEfficiencies = eligible.map((c) => c.cost === 0 ? 1 : 1 / c.cost);
423
+ const idleRates = eligible.map((c) => {
424
+ const internal = c.card._internal;
425
+ const idleRate = internal?.idle_rate;
426
+ return typeof idleRate === "number" ? idleRate : 1;
427
+ });
428
+ const normSuccess = minMaxNormalize(successRates);
429
+ const normCost = minMaxNormalize(costEfficiencies);
430
+ const normIdle = minMaxNormalize(idleRates);
431
+ const scored = eligible.map((c, i) => ({
432
+ ...c,
433
+ rawScore: (normSuccess[i] ?? 0) * (normCost[i] ?? 0) * (normIdle[i] ?? 0)
434
+ }));
435
+ scored.sort((a, b) => b.rawScore - a.rawScore);
436
+ return scored;
437
+ }
438
+ var AutoRequestor = class {
439
+ owner;
440
+ registryDb;
441
+ creditDb;
442
+ autonomyConfig;
443
+ budgetManager;
444
+ /**
445
+ * Creates a new AutoRequestor.
446
+ *
447
+ * @param opts - Configuration for this AutoRequestor instance.
448
+ */
449
+ constructor(opts) {
450
+ this.owner = opts.owner;
451
+ this.registryDb = opts.registryDb;
452
+ this.creditDb = opts.creditDb;
453
+ this.autonomyConfig = opts.autonomyConfig;
454
+ this.budgetManager = opts.budgetManager;
455
+ }
456
+ /**
457
+ * Executes an autonomous capability request.
458
+ *
459
+ * Performs the full flow:
460
+ * 1. Search for matching capability cards
461
+ * 2. Filter self-owned and over-budget candidates
462
+ * 3. Score candidates using min-max normalized composite scoring
463
+ * 4. Resolve peer gateway config
464
+ * 5. Check autonomy tier (Tier 3 queues to pending_requests)
465
+ * 6. Check budget reserve
466
+ * 7. Hold escrow
467
+ * 8. Execute via peer gateway
468
+ * 9. Settle or release escrow based on outcome
469
+ * 10. Log audit event (for Tier 2 notifications and all failures)
470
+ *
471
+ * @param need - The capability need to fulfill.
472
+ * @returns The result of the auto-request attempt.
473
+ */
474
+ async requestWithAutonomy(need) {
475
+ const cards = searchCards(this.registryDb, need.query, { online: true });
476
+ const candidates = [];
477
+ for (const card of cards) {
478
+ const cardAsV2 = card;
479
+ if (Array.isArray(cardAsV2.skills)) {
480
+ for (const skill of cardAsV2.skills) {
481
+ const cost = skill.pricing.credits_per_call;
482
+ if (cost <= need.maxCostCredits) {
483
+ candidates.push({ card, cost, skillId: skill.id });
484
+ }
485
+ }
486
+ } else {
487
+ const cost = card.pricing.credits_per_call;
488
+ if (cost <= need.maxCostCredits) {
489
+ candidates.push({ card, cost, skillId: void 0 });
490
+ }
491
+ }
492
+ }
493
+ const scored = scorePeers(candidates, this.owner);
494
+ if (scored.length === 0) {
495
+ this.logFailure("auto_request_failed", "system", "none", 3, 0, "none", "No eligible peer found");
496
+ return { status: "no_peer", reason: "No eligible peer found" };
497
+ }
498
+ const top = scored[0];
499
+ const peerConfig = findPeer(top.card.owner);
500
+ if (!peerConfig) {
501
+ this.logFailure("auto_request_failed", top.card.id, top.skillId ?? "none", 3, top.cost, top.card.owner, "No gateway config for peer");
502
+ return { status: "no_peer", reason: "No gateway config for peer" };
503
+ }
504
+ const tier = getAutonomyTier(top.cost, this.autonomyConfig);
505
+ if (tier === 3) {
506
+ createPendingRequest(this.registryDb, {
507
+ skill_query: need.query,
508
+ max_cost_credits: need.maxCostCredits,
509
+ credits: top.cost,
510
+ selected_peer: top.card.owner,
511
+ selected_card_id: top.card.id,
512
+ selected_skill_id: top.skillId,
513
+ params: need.params
514
+ });
515
+ insertAuditEvent(this.registryDb, {
516
+ type: "auto_request_pending",
517
+ card_id: top.card.id,
518
+ skill_id: top.skillId ?? top.card.id,
519
+ tier_invoked: 3,
520
+ credits: top.cost,
521
+ peer: top.card.owner
522
+ });
523
+ return {
524
+ status: "tier_blocked",
525
+ reason: "Tier 3: owner approval required",
526
+ peer: top.card.owner
527
+ };
528
+ }
529
+ if (!this.budgetManager.canSpend(top.cost)) {
530
+ this.logFailure("auto_request_failed", top.card.id, top.skillId ?? "none", tier, top.cost, top.card.owner, "Budget reserve would be breached");
531
+ return { status: "budget_blocked", reason: "Insufficient credits \u2014 reserve floor would be breached" };
532
+ }
533
+ const escrowId = holdEscrow(this.creditDb, this.owner, top.cost, top.card.id);
534
+ try {
535
+ const execResult = await requestCapability({
536
+ gatewayUrl: peerConfig.url,
537
+ token: peerConfig.token,
538
+ cardId: top.card.id,
539
+ params: top.skillId ? { skill_id: top.skillId, ...need.params } : need.params
540
+ });
541
+ settleEscrow(this.creditDb, escrowId, top.card.owner);
542
+ if (tier === 2) {
543
+ insertAuditEvent(this.registryDb, {
544
+ type: "auto_request_notify",
545
+ card_id: top.card.id,
546
+ skill_id: top.skillId ?? top.card.id,
547
+ tier_invoked: 2,
548
+ credits: top.cost,
549
+ peer: top.card.owner
550
+ });
551
+ } else {
552
+ insertAuditEvent(this.registryDb, {
553
+ type: "auto_request",
554
+ card_id: top.card.id,
555
+ skill_id: top.skillId ?? top.card.id,
556
+ tier_invoked: 1,
557
+ credits: top.cost,
558
+ peer: top.card.owner
559
+ });
560
+ }
561
+ return {
562
+ status: "success",
563
+ result: execResult,
564
+ escrowId,
565
+ peer: top.card.owner,
566
+ creditsSpent: top.cost
567
+ };
568
+ } catch (err) {
569
+ releaseEscrow(this.creditDb, escrowId);
570
+ const reason = err instanceof Error ? err.message : String(err);
571
+ this.logFailure("auto_request_failed", top.card.id, top.skillId ?? "none", tier, top.cost, top.card.owner, `Execution failed: ${reason}`);
572
+ return {
573
+ status: "failed",
574
+ reason: `Execution failed: ${reason}`,
575
+ peer: top.card.owner
576
+ };
577
+ }
578
+ }
579
+ /**
580
+ * Logs a failure audit event to request_log.
581
+ * Used for all non-success paths to satisfy REQ-06.
582
+ */
583
+ logFailure(type, cardId, skillId, tier, credits, peer, reason) {
584
+ insertAuditEvent(this.registryDb, {
585
+ type,
586
+ card_id: cardId,
587
+ skill_id: skillId,
588
+ tier_invoked: tier,
589
+ credits,
590
+ peer,
591
+ reason
592
+ });
593
+ }
594
+ };
595
+
596
+ // src/utils/interpolation.ts
597
+ function resolvePath(obj, path) {
598
+ const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter((s) => s.length > 0);
599
+ let current = obj;
600
+ for (const segment of segments) {
601
+ if (current === null || current === void 0) {
602
+ return void 0;
603
+ }
604
+ if (typeof current !== "object") {
605
+ return void 0;
606
+ }
607
+ current = current[segment];
608
+ }
609
+ return current;
610
+ }
611
+ function interpolate(template, context) {
612
+ return template.replace(/\$\{([^}]+)\}/g, (_match, expression) => {
613
+ const resolved = resolvePath(context, expression.trim());
614
+ if (resolved === void 0 || resolved === null) {
615
+ return "";
616
+ }
617
+ if (typeof resolved === "object") {
618
+ return JSON.stringify(resolved);
619
+ }
620
+ return String(resolved);
621
+ });
622
+ }
623
+ function interpolateObject(obj, context) {
624
+ const result = {};
625
+ for (const [key, value] of Object.entries(obj)) {
626
+ result[key] = interpolateValue(value, context);
627
+ }
628
+ return result;
629
+ }
630
+ function interpolateValue(value, context) {
631
+ if (typeof value === "string") {
632
+ return interpolate(value, context);
633
+ }
634
+ if (Array.isArray(value)) {
635
+ return value.map((item) => interpolateValue(item, context));
636
+ }
637
+ if (value !== null && typeof value === "object") {
638
+ return interpolateObject(value, context);
639
+ }
640
+ return value;
641
+ }
642
+
643
+ export {
644
+ holdEscrow,
645
+ settleEscrow,
646
+ releaseEscrow,
647
+ confirmEscrowDebit,
648
+ openCreditDb,
649
+ bootstrapAgent,
650
+ getBalance,
651
+ getTransactions,
652
+ recordEarning,
653
+ DEFAULT_AUTONOMY_CONFIG,
654
+ getAutonomyTier,
655
+ insertAuditEvent,
656
+ DEFAULT_BUDGET_CONFIG,
657
+ BudgetManager,
658
+ searchCards,
659
+ filterCards,
660
+ requestCapability,
661
+ listPendingRequests,
662
+ resolvePendingRequest,
663
+ scorePeers,
664
+ AutoRequestor,
665
+ interpolateObject
666
+ };