@belticlabs/agent-risk-sdk 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.d.ts +2 -2
- package/dist/ai/index.js +16 -21
- package/dist/chunk-DI3LZR65.js +103 -0
- package/dist/{chunk-ZMPKY7AX.js → chunk-JGVVQUXQ.js} +1 -2
- package/dist/{chunk-77D74TWX.js → chunk-MWXXX35V.js} +53 -23
- package/dist/chunk-P3VZNH5W.js +57 -0
- package/dist/chunk-PDE55ZZV.js +556 -0
- package/dist/client-DFZL4Pt3.d.ts +298 -0
- package/dist/{index-IfY4XCvJ.d.ts → index-CBUSXzgG.d.ts} +177 -138
- package/dist/index.d.ts +3 -3
- package/dist/index.js +562 -171
- package/dist/protocol/index.d.ts +51 -30
- package/dist/protocol/index.js +54 -529
- package/dist/{session-Dcof4UIn.d.ts → session-DTIzd1EQ.d.ts} +119 -44
- package/dist/x402/hono.d.ts +3 -4
- package/dist/x402/hono.js +4 -5
- package/dist/x402/index.d.ts +7 -77
- package/dist/x402/index.js +8 -71
- package/package.json +1 -1
- package/dist/adapter-AjCgj-KM.d.ts +0 -6
- package/dist/chunk-FQDHFTVR.js +0 -29
- package/dist/chunk-IUWC6HT5.js +0 -144
- package/dist/chunk-M4I3FGZG.js +0 -13
- package/dist/client-CgCjOrRP.d.ts +0 -65
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
openCall
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JGVVQUXQ.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
SESSION_EXTENSION,
|
|
6
|
+
SESSION_HEADER,
|
|
7
|
+
summaryOf,
|
|
8
|
+
x402Moments
|
|
9
|
+
} from "./chunk-DI3LZR65.js";
|
|
7
10
|
import {
|
|
8
11
|
Chain,
|
|
9
12
|
canonicalBytes,
|
|
@@ -12,7 +15,13 @@ import {
|
|
|
12
15
|
memorySigner,
|
|
13
16
|
sha256,
|
|
14
17
|
toHex
|
|
15
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MWXXX35V.js";
|
|
19
|
+
import {
|
|
20
|
+
ApiErrorSchema,
|
|
21
|
+
Hex64Schema,
|
|
22
|
+
JsonValueSchema,
|
|
23
|
+
Money
|
|
24
|
+
} from "./chunk-PDE55ZZV.js";
|
|
16
25
|
|
|
17
26
|
// src/core/api-client.ts
|
|
18
27
|
var TIMEOUT_MS = 1e4;
|
|
@@ -41,15 +50,26 @@ var ApiClient = class {
|
|
|
41
50
|
"user-agent": userAgent
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
|
-
|
|
53
|
+
post(path, body) {
|
|
54
|
+
return this.request("POST", path, JSON.stringify(body));
|
|
55
|
+
}
|
|
56
|
+
/** A read; `query` entries left undefined are not sent. */
|
|
57
|
+
get(path, query = {}) {
|
|
58
|
+
const params = new URLSearchParams();
|
|
59
|
+
for (const [key, value] of Object.entries(query))
|
|
60
|
+
if (value !== void 0) params.set(key, String(value));
|
|
61
|
+
const search = params.toString();
|
|
62
|
+
return this.request("GET", search ? `${path}?${search}` : path);
|
|
63
|
+
}
|
|
64
|
+
async request(method, path, body) {
|
|
45
65
|
const controller = new AbortController();
|
|
46
66
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
47
67
|
let res;
|
|
48
68
|
try {
|
|
49
69
|
res = await globalThis.fetch(`${this.baseUrl}${path}`, {
|
|
50
|
-
method
|
|
70
|
+
method,
|
|
51
71
|
headers: this.headers,
|
|
52
|
-
body:
|
|
72
|
+
...body !== void 0 ? { body } : {},
|
|
53
73
|
signal: controller.signal
|
|
54
74
|
});
|
|
55
75
|
} catch (err) {
|
|
@@ -69,7 +89,7 @@ var ApiClient = class {
|
|
|
69
89
|
json = null;
|
|
70
90
|
}
|
|
71
91
|
if (!res.ok) {
|
|
72
|
-
const e = json?.error;
|
|
92
|
+
const e = ApiErrorSchema.safeParse(json).data?.error;
|
|
73
93
|
throw new BelticApiError(
|
|
74
94
|
res.status,
|
|
75
95
|
e?.code ?? `HTTP_${res.status}`,
|
|
@@ -134,10 +154,11 @@ var Decision = class _Decision {
|
|
|
134
154
|
};
|
|
135
155
|
|
|
136
156
|
// src/core/identity.ts
|
|
137
|
-
var SEED_HEX = /^[0-9a-f]{64}$/i;
|
|
138
157
|
function identityFromSeed(seedHex) {
|
|
139
|
-
if (!
|
|
140
|
-
throw new BelticConfigError(
|
|
158
|
+
if (!Hex64Schema.safeParse(seedHex).success)
|
|
159
|
+
throw new BelticConfigError(
|
|
160
|
+
"agentSeed must be 64 lowercase hex characters (a 32-byte Ed25519 seed)"
|
|
161
|
+
);
|
|
141
162
|
const signer = memorySigner(fromHex(seedHex));
|
|
142
163
|
const did = didKeyFromEd25519(signer.publicKey);
|
|
143
164
|
return { did, signer: { ...signer, keyId: did } };
|
|
@@ -157,14 +178,16 @@ var Memory = class {
|
|
|
157
178
|
}
|
|
158
179
|
};
|
|
159
180
|
var Session = class _Session {
|
|
160
|
-
constructor(deps,
|
|
181
|
+
constructor(deps, opts = {}, resume = null) {
|
|
161
182
|
this.deps = deps;
|
|
162
|
-
this.key = key;
|
|
163
183
|
this.opts = opts;
|
|
184
|
+
this.conversationId = resume;
|
|
164
185
|
this.openedWith = opts.intent ? _Session.hash(opts.intent) : null;
|
|
165
186
|
}
|
|
166
187
|
opened = null;
|
|
167
188
|
current = null;
|
|
189
|
+
/** The conversation id: what was resumed, then what the platform answered. */
|
|
190
|
+
conversationId;
|
|
168
191
|
/** JCS hash of the mandate on the chain, and of the one the options carried. */
|
|
169
192
|
declared = null;
|
|
170
193
|
openedWith;
|
|
@@ -172,18 +195,29 @@ var Session = class _Session {
|
|
|
172
195
|
timer = null;
|
|
173
196
|
calls = new Memory();
|
|
174
197
|
decisions = new Memory();
|
|
175
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* The conversation id — what the host stores and resumes with (GAP-84).
|
|
200
|
+
* Opened on first use; `null` while the platform has not answered and
|
|
201
|
+
* nothing was resumed.
|
|
202
|
+
*/
|
|
176
203
|
id() {
|
|
177
|
-
return this.stream().then((stream) => stream?.
|
|
204
|
+
return this.stream().then((stream) => stream?.conversationId ?? this.conversationId);
|
|
178
205
|
}
|
|
179
206
|
/**
|
|
180
|
-
* The stream this session records into — opened on first use,
|
|
207
|
+
* The stream this session records into — opened on first use, reopened
|
|
208
|
+
* in the same conversation once the platform ended it (GAP-85), `null`
|
|
181
209
|
* when there is none. For the integrations; a host never holds it.
|
|
182
210
|
* @internal
|
|
183
211
|
*/
|
|
184
212
|
stream() {
|
|
185
|
-
const
|
|
213
|
+
const prior = this.opened ?? Promise.resolve(null);
|
|
214
|
+
const next = prior.catch(() => null).then(
|
|
215
|
+
(stream) => stream && (this.deps.streams.halted(stream) || !stream.isClosed) ? stream : this.open()
|
|
216
|
+
);
|
|
186
217
|
this.opened = next;
|
|
218
|
+
next.catch(() => {
|
|
219
|
+
if (this.opened === next) this.opened = null;
|
|
220
|
+
});
|
|
187
221
|
return next.then((stream) => {
|
|
188
222
|
if (stream !== this.current) {
|
|
189
223
|
this.current = stream;
|
|
@@ -192,6 +226,17 @@ var Session = class _Session {
|
|
|
192
226
|
return stream;
|
|
193
227
|
});
|
|
194
228
|
}
|
|
229
|
+
async open() {
|
|
230
|
+
const stream = await this.deps.streams.open({
|
|
231
|
+
...this.opts,
|
|
232
|
+
resume: this.conversationId ?? void 0
|
|
233
|
+
});
|
|
234
|
+
if (stream) {
|
|
235
|
+
this.conversationId = stream.conversationId;
|
|
236
|
+
this.deps.onOpened(this, stream.conversationId);
|
|
237
|
+
}
|
|
238
|
+
return stream;
|
|
239
|
+
}
|
|
195
240
|
/**
|
|
196
241
|
* The platform's verdict on a payment about to be presented. Asked once
|
|
197
242
|
* per call id: a host that re-runs its approval step reads the same
|
|
@@ -205,7 +250,7 @@ var Session = class _Session {
|
|
|
205
250
|
if (!stream) return Decision.absent();
|
|
206
251
|
if (opts.intent) await this.declare(stream, opts.intent);
|
|
207
252
|
const summary = summaryOf(payment);
|
|
208
|
-
const decision = await this.deps.evaluate(stream.id, summary);
|
|
253
|
+
const decision = await this.deps.evaluate(stream.id, summary, opts.callId);
|
|
209
254
|
if (decision.absent) return decision;
|
|
210
255
|
if (opts.callId) this.decisions.set(opts.callId, decision);
|
|
211
256
|
const call = opts.callId ? this.calls.get(opts.callId)?.call : void 0;
|
|
@@ -304,86 +349,46 @@ var Session = class _Session {
|
|
|
304
349
|
}
|
|
305
350
|
};
|
|
306
351
|
|
|
307
|
-
// src/core/
|
|
308
|
-
var
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
333
|
-
if (!this.deps.transport.hasRoom()) {
|
|
334
|
-
this.dropped++;
|
|
335
|
-
this.droppedFirstTs ??= ts;
|
|
336
|
-
this.droppedLastTs = ts;
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
if (this.dropped > 0) {
|
|
340
|
-
this.deps.transport.enqueue(
|
|
341
|
-
await this.next(
|
|
342
|
-
"transport.gap",
|
|
343
|
-
{ dropped: this.dropped, firstTs: this.droppedFirstTs, lastTs: this.droppedLastTs },
|
|
344
|
-
ts
|
|
345
|
-
)
|
|
346
|
-
);
|
|
347
|
-
this.dropped = 0;
|
|
348
|
-
this.droppedFirstTs = this.droppedLastTs = null;
|
|
349
|
-
}
|
|
350
|
-
this.deps.transport.enqueue(await this.next(kind, payload, ts));
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
/** The tool call whose `execute` the host runs itself; see `ToolCallSpan`. */
|
|
354
|
-
toolCall(call) {
|
|
355
|
-
const { callId, ...start } = call;
|
|
356
|
-
return openCall(this, "tool_call", callId, { transport: "local", ...start });
|
|
357
|
-
}
|
|
358
|
-
async close(reason = "completed", extra = {}) {
|
|
359
|
-
if (this.closed) return;
|
|
360
|
-
this.closed = true;
|
|
361
|
-
await this.emit("session.close", { reason, ...extra });
|
|
362
|
-
await this.flush();
|
|
363
|
-
this.deps.onClosed?.(this);
|
|
364
|
-
}
|
|
365
|
-
/** Read-your-writes: the platform must hold the evidence before anyone judges it (GAP-16/66). */
|
|
366
|
-
flush() {
|
|
367
|
-
return this.deps.transport.flush();
|
|
368
|
-
}
|
|
369
|
-
/** Serialized: two concurrent emits get consecutive seqs, never the same one. */
|
|
370
|
-
next(kind, payload, ts) {
|
|
371
|
-
const run = this.building.then(async () => {
|
|
372
|
-
const built = await this.chain.append({ ts, kind, payload }, this.deps.signer);
|
|
373
|
-
this.chain = built.chain;
|
|
374
|
-
return built.event;
|
|
375
|
-
});
|
|
376
|
-
this.building = run.catch(() => void 0);
|
|
377
|
-
return run;
|
|
352
|
+
// src/core/prompt-ledger.ts
|
|
353
|
+
var PromptLedger = class _PromptLedger {
|
|
354
|
+
last;
|
|
355
|
+
constructor(base = null) {
|
|
356
|
+
this.last = base;
|
|
357
|
+
}
|
|
358
|
+
/** How the platform digests one message: over its canonical bytes. */
|
|
359
|
+
static digest(message) {
|
|
360
|
+
return toHex(sha256(canonicalBytes(message)));
|
|
361
|
+
}
|
|
362
|
+
/** The prompt as the wire carries it — whole, or against the last one — and this call becomes the last. */
|
|
363
|
+
encode(callId, messages) {
|
|
364
|
+
const digests = messages.map(_PromptLedger.digest);
|
|
365
|
+
const last = this.last;
|
|
366
|
+
this.last = { prior: callId, digests };
|
|
367
|
+
if (!last) return { prompt: messages };
|
|
368
|
+
let shared = 0;
|
|
369
|
+
while (shared < last.digests.length && shared < digests.length && last.digests[shared] === digests[shared])
|
|
370
|
+
shared++;
|
|
371
|
+
if (shared === 0) return { prompt: messages };
|
|
372
|
+
return { promptDelta: { prior: last.prior, shared, messages: messages.slice(shared) } };
|
|
373
|
+
}
|
|
374
|
+
/** The last call never reached the platform: the next one ships whole. */
|
|
375
|
+
forget() {
|
|
376
|
+
this.last = null;
|
|
378
377
|
}
|
|
379
378
|
};
|
|
380
379
|
|
|
381
380
|
// src/core/transport.ts
|
|
382
381
|
var FLUSH_MS = 1e3;
|
|
383
382
|
var MAX_BATCH = 50;
|
|
383
|
+
var MAX_BATCH_BYTES = 8 * 1024 * 1024;
|
|
384
|
+
var MAX_EVENT_BYTES = 1024 * 1024;
|
|
384
385
|
var MAX_BUFFERED = 5e3;
|
|
386
|
+
var MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
387
|
+
var MAX_IN_FLIGHT = 8;
|
|
388
|
+
var ENVELOPE_BYTES = 512;
|
|
385
389
|
var BACKOFF_BASE_MS = 200;
|
|
386
390
|
var BACKOFF_MAX_MS = 3e4;
|
|
391
|
+
var ENDED_CODES = /* @__PURE__ */ new Set(["SESSION_CLOSED", "SESSION_EXPIRED"]);
|
|
387
392
|
var ChainRejectedError = class extends Error {
|
|
388
393
|
constructor(sessionId, source, result, options) {
|
|
389
394
|
super(
|
|
@@ -396,18 +401,15 @@ var ChainRejectedError = class extends Error {
|
|
|
396
401
|
this.name = "ChainRejectedError";
|
|
397
402
|
}
|
|
398
403
|
};
|
|
399
|
-
var TransportClosedError = class extends Error {
|
|
400
|
-
constructor() {
|
|
401
|
-
super("transport is closed");
|
|
402
|
-
this.name = "TransportClosedError";
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
404
|
var Transport = class _Transport {
|
|
406
405
|
constructor(api) {
|
|
407
406
|
this.api = api;
|
|
408
407
|
}
|
|
409
408
|
chains = /* @__PURE__ */ new Map();
|
|
409
|
+
waiting = [];
|
|
410
|
+
running = 0;
|
|
410
411
|
buffered = 0;
|
|
412
|
+
bufferedBytes = 0;
|
|
411
413
|
timer = null;
|
|
412
414
|
closed = false;
|
|
413
415
|
/**
|
|
@@ -419,35 +421,62 @@ var Transport = class _Transport {
|
|
|
419
421
|
static outage(err) {
|
|
420
422
|
return err instanceof BelticApiError && err.retryable;
|
|
421
423
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
+
/** The bytes the platform will measure for an event with this payload (GAP-59), known before a `seq` is spent. */
|
|
425
|
+
static measure(payload) {
|
|
426
|
+
return canonicalBytes(payload).length + ENVELOPE_BYTES;
|
|
427
|
+
}
|
|
428
|
+
/** Whether the platform would take an event of this size at all. */
|
|
429
|
+
static fits(bytes) {
|
|
430
|
+
return bytes <= MAX_EVENT_BYTES;
|
|
424
431
|
}
|
|
425
|
-
hasRoom() {
|
|
426
|
-
return !this.closed && this.buffered < MAX_BUFFERED;
|
|
432
|
+
hasRoom(bytes) {
|
|
433
|
+
return !this.closed && this.buffered < MAX_BUFFERED && this.bufferedBytes + bytes <= MAX_BUFFERED_BYTES;
|
|
427
434
|
}
|
|
428
435
|
haltedError(sessionId, source) {
|
|
429
436
|
return this.chains.get(`${sessionId}:${source}`)?.halted ?? null;
|
|
430
437
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
438
|
+
/** Whether the platform ended the session under this chain (GAP-85). */
|
|
439
|
+
ended(sessionId, source) {
|
|
440
|
+
return this.chains.get(`${sessionId}:${source}`)?.ended ?? false;
|
|
441
|
+
}
|
|
442
|
+
/** Callers check `hasRoom(bytes)` first and assign `seq` only then (GAP-38). */
|
|
443
|
+
enqueue(ev, bytes) {
|
|
444
|
+
if (this.closed) throw new Error("transport is closed");
|
|
434
445
|
const key = `${ev.sessionId}:${ev.source}`;
|
|
435
446
|
let chain = this.chains.get(key);
|
|
436
447
|
if (!chain) {
|
|
437
|
-
chain = {
|
|
448
|
+
chain = {
|
|
449
|
+
sessionId: ev.sessionId,
|
|
450
|
+
pending: [],
|
|
451
|
+
inFlight: null,
|
|
452
|
+
start: null,
|
|
453
|
+
retry: null,
|
|
454
|
+
attempts: 0,
|
|
455
|
+
halted: null,
|
|
456
|
+
ended: false
|
|
457
|
+
};
|
|
438
458
|
this.chains.set(key, chain);
|
|
439
459
|
}
|
|
440
460
|
if (chain.halted) throw chain.halted;
|
|
441
|
-
if (
|
|
442
|
-
|
|
461
|
+
if (chain.ended) return;
|
|
462
|
+
if (!this.hasRoom(bytes)) throw new Error("transport buffer is full");
|
|
463
|
+
chain.pending.push({ ev, bytes });
|
|
443
464
|
this.buffered++;
|
|
465
|
+
this.bufferedBytes += bytes;
|
|
444
466
|
if (chain.pending.length >= MAX_BATCH) void this.deliver(chain, false);
|
|
445
467
|
else this.schedule();
|
|
446
468
|
}
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
469
|
+
/**
|
|
470
|
+
* One attempt per chain, now — a chain waiting out its backoff included;
|
|
471
|
+
* resolves once every attempt settled. With a scope, only that session's
|
|
472
|
+
* chains (GAP-88).
|
|
473
|
+
*/
|
|
474
|
+
async flush(scope) {
|
|
475
|
+
if (!scope) this.unschedule();
|
|
476
|
+
const chains = [...this.chains.values()].filter(
|
|
477
|
+
(chain) => !scope || chain.sessionId === scope.sessionId
|
|
478
|
+
);
|
|
479
|
+
await Promise.all(chains.map((chain) => this.deliver(chain, true)));
|
|
451
480
|
}
|
|
452
481
|
async close() {
|
|
453
482
|
await this.flush();
|
|
@@ -470,28 +499,72 @@ var Transport = class _Transport {
|
|
|
470
499
|
clearTimeout(this.timer);
|
|
471
500
|
this.timer = null;
|
|
472
501
|
}
|
|
473
|
-
/**
|
|
502
|
+
/**
|
|
503
|
+
* Queues the chain for a delivery slot — at the front when forced. A
|
|
504
|
+
* chain waiting out its backoff is left alone unless forced: only
|
|
505
|
+
* `flush` cuts a backoff short.
|
|
506
|
+
*/
|
|
474
507
|
deliver(chain, force) {
|
|
475
|
-
if (chain.inFlight)
|
|
476
|
-
|
|
508
|
+
if (chain.inFlight) {
|
|
509
|
+
if (force && chain.start) this.promote(chain);
|
|
510
|
+
return chain.inFlight;
|
|
511
|
+
}
|
|
512
|
+
if (chain.halted || chain.ended || chain.pending.length === 0) return Promise.resolve();
|
|
477
513
|
if (chain.retry) {
|
|
478
514
|
if (!force) return Promise.resolve();
|
|
479
515
|
clearTimeout(chain.retry);
|
|
480
516
|
chain.retry = null;
|
|
481
517
|
}
|
|
482
|
-
chain.inFlight =
|
|
483
|
-
chain.
|
|
518
|
+
chain.inFlight = new Promise((resolve) => {
|
|
519
|
+
chain.start = () => {
|
|
520
|
+
chain.start = null;
|
|
521
|
+
this.running++;
|
|
522
|
+
void this.attempt(chain).finally(() => {
|
|
523
|
+
this.running--;
|
|
524
|
+
chain.inFlight = null;
|
|
525
|
+
resolve();
|
|
526
|
+
this.pump();
|
|
527
|
+
});
|
|
528
|
+
};
|
|
484
529
|
});
|
|
530
|
+
if (force) this.waiting.unshift(chain);
|
|
531
|
+
else this.waiting.push(chain);
|
|
532
|
+
this.pump();
|
|
485
533
|
return chain.inFlight;
|
|
486
534
|
}
|
|
535
|
+
promote(chain) {
|
|
536
|
+
const at = this.waiting.indexOf(chain);
|
|
537
|
+
if (at > 0) {
|
|
538
|
+
this.waiting.splice(at, 1);
|
|
539
|
+
this.waiting.unshift(chain);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
pump() {
|
|
543
|
+
while (this.running < MAX_IN_FLIGHT && this.waiting.length > 0) this.waiting.shift().start?.();
|
|
544
|
+
}
|
|
545
|
+
/** The head of the queue that fits one batch: by count or by bytes, whichever comes first, and never empty. */
|
|
546
|
+
static batchOf(chain) {
|
|
547
|
+
const batch = [];
|
|
548
|
+
let bytes = 0;
|
|
549
|
+
for (const item of chain.pending) {
|
|
550
|
+
if (batch.length >= MAX_BATCH) break;
|
|
551
|
+
if (batch.length > 0 && bytes + item.bytes > MAX_BATCH_BYTES) break;
|
|
552
|
+
batch.push(item);
|
|
553
|
+
bytes += item.bytes;
|
|
554
|
+
}
|
|
555
|
+
return batch;
|
|
556
|
+
}
|
|
487
557
|
/** Batches until the chain drains; a retryable failure schedules the next attempt and returns. */
|
|
488
558
|
async attempt(chain) {
|
|
489
559
|
while (chain.pending.length > 0 && !chain.halted) {
|
|
490
|
-
const batch =
|
|
491
|
-
const first = batch[0];
|
|
560
|
+
const batch = _Transport.batchOf(chain);
|
|
561
|
+
const first = batch[0].ev;
|
|
492
562
|
let ack;
|
|
493
563
|
try {
|
|
494
|
-
ack = await this.api.post(
|
|
564
|
+
ack = await this.api.post(
|
|
565
|
+
"/v1/evidence",
|
|
566
|
+
batch.map((item) => item.ev)
|
|
567
|
+
);
|
|
495
568
|
} catch (err) {
|
|
496
569
|
console.error("[beltic]", err);
|
|
497
570
|
if (!_Transport.outage(err)) {
|
|
@@ -522,19 +595,142 @@ var Transport = class _Transport {
|
|
|
522
595
|
chain.retry.unref?.();
|
|
523
596
|
return;
|
|
524
597
|
}
|
|
525
|
-
|
|
526
|
-
this.buffered -= batch.length;
|
|
598
|
+
this.release(chain, batch.length);
|
|
527
599
|
chain.attempts = 0;
|
|
528
600
|
const bad = ack.results.find((r) => r.status === "fork" || r.status === "rejected");
|
|
529
|
-
if (bad)
|
|
601
|
+
if (!bad) continue;
|
|
602
|
+
if (bad.status === "rejected" && bad.code && ENDED_CODES.has(bad.code)) this.end(chain);
|
|
603
|
+
else this.halt(chain, new ChainRejectedError(first.sessionId, first.source, bad));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
release(chain, count) {
|
|
607
|
+
for (const item of chain.pending.splice(0, count)) {
|
|
608
|
+
this.buffered--;
|
|
609
|
+
this.bufferedBytes -= item.bytes;
|
|
530
610
|
}
|
|
531
611
|
}
|
|
612
|
+
drop(chain) {
|
|
613
|
+
this.release(chain, chain.pending.length);
|
|
614
|
+
}
|
|
532
615
|
halt(chain, error) {
|
|
533
616
|
chain.halted = error;
|
|
534
|
-
this.
|
|
535
|
-
chain.pending = [];
|
|
617
|
+
this.drop(chain);
|
|
536
618
|
console.error("[beltic]", error);
|
|
537
619
|
}
|
|
620
|
+
end(chain) {
|
|
621
|
+
chain.ended = true;
|
|
622
|
+
this.drop(chain);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/core/stream.ts
|
|
627
|
+
var Stream = class {
|
|
628
|
+
constructor(deps, id, conversationId, source, born) {
|
|
629
|
+
this.deps = deps;
|
|
630
|
+
this.id = id;
|
|
631
|
+
this.conversationId = conversationId;
|
|
632
|
+
this.source = source;
|
|
633
|
+
this.born = born;
|
|
634
|
+
this.chain = Chain.genesis(id, source);
|
|
635
|
+
this.ledger = new PromptLedger(deps.prompt ?? null);
|
|
636
|
+
}
|
|
637
|
+
chain;
|
|
638
|
+
ledger;
|
|
639
|
+
building = Promise.resolve();
|
|
640
|
+
dropped = 0;
|
|
641
|
+
oversized = 0;
|
|
642
|
+
droppedFirstTs = null;
|
|
643
|
+
droppedLastTs = null;
|
|
644
|
+
closed = false;
|
|
645
|
+
/** Closed by this side, or ended by the platform (GAP-85). */
|
|
646
|
+
get isClosed() {
|
|
647
|
+
return this.closed || this.deps.transport.ended(this.id, this.source);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Resolves once the event is sequenced and buffered — not once it is
|
|
651
|
+
* acknowledged. `false` when the event was dropped for lack of room.
|
|
652
|
+
*/
|
|
653
|
+
async emit(kind, payload) {
|
|
654
|
+
const halted = this.deps.transport.haltedError(this.id, this.source);
|
|
655
|
+
if (halted) throw halted;
|
|
656
|
+
if (this.deps.transport.ended(this.id, this.source)) return false;
|
|
657
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
658
|
+
const bytes = Transport.measure(payload);
|
|
659
|
+
const gap = this.dropped > 0 ? this.gap() : null;
|
|
660
|
+
const gapBytes = gap ? Transport.measure(gap) : 0;
|
|
661
|
+
if (!Transport.fits(bytes) || !this.deps.transport.hasRoom(bytes + gapBytes)) {
|
|
662
|
+
this.dropped++;
|
|
663
|
+
if (!Transport.fits(bytes)) this.oversized++;
|
|
664
|
+
this.droppedFirstTs ??= ts;
|
|
665
|
+
this.droppedLastTs = ts;
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (gap) {
|
|
669
|
+
this.dropped = this.oversized = 0;
|
|
670
|
+
this.droppedFirstTs = this.droppedLastTs = null;
|
|
671
|
+
this.deps.transport.enqueue(
|
|
672
|
+
await this.next("transport.gap", gap, ts),
|
|
673
|
+
gapBytes
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
this.deps.transport.enqueue(await this.next(kind, payload, ts), bytes);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
/** What the drops since the last accepted event add up to (GAP-38/87). */
|
|
680
|
+
gap() {
|
|
681
|
+
return {
|
|
682
|
+
dropped: this.dropped,
|
|
683
|
+
...this.oversized > 0 ? { oversized: this.oversized } : {},
|
|
684
|
+
firstTs: this.droppedFirstTs,
|
|
685
|
+
lastTs: this.droppedLastTs
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* A model call as a span: `llm_call.start` now, with the prompt as a
|
|
690
|
+
* delta against the last one on this chain (GAP-89), `llm_call.end` when
|
|
691
|
+
* the host reports the result. A start that never left (dropped) forgets
|
|
692
|
+
* the ledger, so the next call ships its prompt whole.
|
|
693
|
+
*/
|
|
694
|
+
llmCall(call) {
|
|
695
|
+
const { callId, params, ...start } = call;
|
|
696
|
+
const { prompt, ...rest } = params;
|
|
697
|
+
const encoded = Array.isArray(prompt) ? this.ledger.encode(callId, prompt) : prompt === void 0 ? {} : { prompt };
|
|
698
|
+
const span = openCall(this, "llm_call", callId, { ...start, params: { ...rest, ...encoded } });
|
|
699
|
+
if (Array.isArray(prompt))
|
|
700
|
+
span.opened.then(
|
|
701
|
+
(sequenced) => {
|
|
702
|
+
if (!sequenced) this.ledger.forget();
|
|
703
|
+
},
|
|
704
|
+
() => this.ledger.forget()
|
|
705
|
+
);
|
|
706
|
+
return span;
|
|
707
|
+
}
|
|
708
|
+
/** The tool call whose `execute` the host runs itself; see `ToolCallSpan`. */
|
|
709
|
+
toolCall(call) {
|
|
710
|
+
const { callId, ...start } = call;
|
|
711
|
+
return openCall(this, "tool_call", callId, { transport: "local", ...start });
|
|
712
|
+
}
|
|
713
|
+
async close(reason = "completed", extra = {}) {
|
|
714
|
+
if (this.closed) return;
|
|
715
|
+
this.closed = true;
|
|
716
|
+
await this.emit("session.close", { reason, ...extra });
|
|
717
|
+
await this.flush();
|
|
718
|
+
this.deps.onClosed?.(this);
|
|
719
|
+
}
|
|
720
|
+
/** Read-your-writes for this session's chains, and only them (GAP-16/66/88). */
|
|
721
|
+
flush() {
|
|
722
|
+
return this.deps.transport.flush({ sessionId: this.id });
|
|
723
|
+
}
|
|
724
|
+
/** Serialized: two concurrent emits get consecutive seqs, never the same one. */
|
|
725
|
+
next(kind, payload, ts) {
|
|
726
|
+
const run = this.building.then(async () => {
|
|
727
|
+
const built = await this.chain.append({ ts, kind, payload }, this.deps.signer);
|
|
728
|
+
this.chain = built.chain;
|
|
729
|
+
return built.event;
|
|
730
|
+
});
|
|
731
|
+
this.building = run.catch(() => void 0);
|
|
732
|
+
return run;
|
|
733
|
+
}
|
|
538
734
|
};
|
|
539
735
|
|
|
540
736
|
// src/core/streams.ts
|
|
@@ -544,35 +740,12 @@ var Streams = class {
|
|
|
544
740
|
this.deps = deps;
|
|
545
741
|
}
|
|
546
742
|
attached = /* @__PURE__ */ new Map();
|
|
547
|
-
/** Buyer streams by the host's own key (GAP-71). */
|
|
548
|
-
opened = /* @__PURE__ */ new Map();
|
|
549
743
|
retryAt = 0;
|
|
550
744
|
/**
|
|
551
|
-
* Buyer half:
|
|
552
|
-
*
|
|
553
|
-
* that continues the same key; a closed key is forgotten.
|
|
745
|
+
* Buyer half: a fresh AGENT_TRACE stream — a new conversation, or a new
|
|
746
|
+
* session of the one `resume` names. `null` while the platform is out.
|
|
554
747
|
*/
|
|
555
|
-
open(
|
|
556
|
-
const prior = this.opened.get(key) ?? Promise.resolve(null);
|
|
557
|
-
const next = prior.catch(() => null).then(
|
|
558
|
-
(stream) => stream && !stream.isClosed && !this.deps.transport.haltedError(stream.id, stream.source) ? stream : this.openFresh(input, () => this.forget(key, next))
|
|
559
|
-
);
|
|
560
|
-
this.opened.set(key, next);
|
|
561
|
-
next.catch(() => this.forget(key, next));
|
|
562
|
-
return next;
|
|
563
|
-
}
|
|
564
|
-
/** Seller half: emit INTERNAL_NETWORK evidence into a session the buyer bound, or open a seller-born one. */
|
|
565
|
-
async ensure(sessionId) {
|
|
566
|
-
if (sessionId) return this.attach(sessionId, "INTERNAL_NETWORK", "buyer");
|
|
567
|
-
const out = await this.deps.api.post("/v1/sessions", {
|
|
568
|
-
source: "INTERNAL_NETWORK"
|
|
569
|
-
});
|
|
570
|
-
return this.attach(out.sessionId, "INTERNAL_NETWORK", "seller");
|
|
571
|
-
}
|
|
572
|
-
forget(key, entry) {
|
|
573
|
-
if (this.opened.get(key) === entry) this.opened.delete(key);
|
|
574
|
-
}
|
|
575
|
-
async openFresh(input, onClosed) {
|
|
748
|
+
async open(input = {}, onClosed) {
|
|
576
749
|
const identity = this.deps.identity;
|
|
577
750
|
if (!identity)
|
|
578
751
|
throw new BelticConfigError(
|
|
@@ -590,14 +763,30 @@ var Streams = class {
|
|
|
590
763
|
return null;
|
|
591
764
|
}
|
|
592
765
|
}
|
|
766
|
+
/** Seller half: emit INTERNAL_NETWORK evidence into a session the buyer bound, or open a seller-born one. */
|
|
767
|
+
async ensure(sessionId) {
|
|
768
|
+
if (sessionId) return this.attach(sessionId, sessionId, "INTERNAL_NETWORK", "buyer");
|
|
769
|
+
const out = await this.deps.api.post("/v1/sessions", {
|
|
770
|
+
source: "INTERNAL_NETWORK"
|
|
771
|
+
});
|
|
772
|
+
return this.attach(out.sessionId, out.conversationId, "INTERNAL_NETWORK", "seller");
|
|
773
|
+
}
|
|
774
|
+
/** Whether the platform refused this stream's chain: its next `emit` throws (GAP-85). */
|
|
775
|
+
halted(stream) {
|
|
776
|
+
return this.deps.transport.haltedError(stream.id, stream.source) !== null;
|
|
777
|
+
}
|
|
593
778
|
async create(identity, input, onClosed) {
|
|
594
779
|
const body = {
|
|
595
780
|
source: "AGENT_TRACE",
|
|
596
781
|
agent: { did: identity.did, credential: identity.did },
|
|
597
|
-
...input.intent ? { intent: input.intent } : {}
|
|
782
|
+
...input.intent ? { intent: input.intent } : {},
|
|
783
|
+
...input.resume ? { resume: input.resume } : {}
|
|
598
784
|
};
|
|
599
785
|
const out = await this.deps.api.post("/v1/sessions", body);
|
|
600
|
-
const stream = this.attach(out.sessionId, "AGENT_TRACE", "buyer",
|
|
786
|
+
const stream = this.attach(out.sessionId, out.conversationId, "AGENT_TRACE", "buyer", {
|
|
787
|
+
prompt: out.prompt,
|
|
788
|
+
onClosed
|
|
789
|
+
});
|
|
601
790
|
await stream.emit("session.open", {
|
|
602
791
|
runtime: {
|
|
603
792
|
sdk: "@belticlabs/agent-risk-sdk",
|
|
@@ -609,7 +798,7 @@ var Streams = class {
|
|
|
609
798
|
if (input.intent) await stream.emit("intent.declared", input.intent);
|
|
610
799
|
return stream;
|
|
611
800
|
}
|
|
612
|
-
attach(id, source, born,
|
|
801
|
+
attach(id, conversationId, source, born, opts = {}) {
|
|
613
802
|
const key = `${id}:${source}`;
|
|
614
803
|
const existing = this.attached.get(key);
|
|
615
804
|
if (existing) return existing;
|
|
@@ -617,12 +806,14 @@ var Streams = class {
|
|
|
617
806
|
{
|
|
618
807
|
transport: this.deps.transport,
|
|
619
808
|
signer: source === "AGENT_TRACE" ? this.deps.identity?.signer : void 0,
|
|
809
|
+
prompt: opts.prompt,
|
|
620
810
|
onClosed: () => {
|
|
621
811
|
this.attached.delete(key);
|
|
622
|
-
onClosed?.();
|
|
812
|
+
opts.onClosed?.();
|
|
623
813
|
}
|
|
624
814
|
},
|
|
625
815
|
id,
|
|
816
|
+
conversationId,
|
|
626
817
|
source,
|
|
627
818
|
born
|
|
628
819
|
);
|
|
@@ -631,13 +822,201 @@ var Streams = class {
|
|
|
631
822
|
}
|
|
632
823
|
};
|
|
633
824
|
|
|
825
|
+
// src/x402/headers.ts
|
|
826
|
+
import { z } from "zod";
|
|
827
|
+
var Base64Json = z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).transform((value, ctx) => {
|
|
828
|
+
try {
|
|
829
|
+
return JSON.parse(Buffer.from(value, "base64").toString("utf8"));
|
|
830
|
+
} catch {
|
|
831
|
+
ctx.addIssue({ code: "custom", message: "not base64 JSON" });
|
|
832
|
+
return z.NEVER;
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
var Accepts = z.looseObject({
|
|
836
|
+
payTo: z.string().optional().catch(void 0),
|
|
837
|
+
amount: z.string().optional().catch(void 0),
|
|
838
|
+
network: z.string().optional().catch(void 0),
|
|
839
|
+
asset: z.string().optional().catch(void 0)
|
|
840
|
+
});
|
|
841
|
+
var PaymentRequired = Base64Json.pipe(
|
|
842
|
+
z.looseObject({ accepts: z.array(Accepts).optional().catch(void 0) })
|
|
843
|
+
);
|
|
844
|
+
var PaymentPayload = Base64Json.pipe(
|
|
845
|
+
z.looseObject({
|
|
846
|
+
accepted: Accepts.optional().catch(void 0),
|
|
847
|
+
payload: z.record(z.string(), z.unknown()).optional().catch(void 0),
|
|
848
|
+
extensions: z.record(z.string(), z.unknown()).optional().catch(void 0)
|
|
849
|
+
})
|
|
850
|
+
);
|
|
851
|
+
var Requirement = z.looseObject({
|
|
852
|
+
scheme: z.string().min(1),
|
|
853
|
+
network: z.string().min(1),
|
|
854
|
+
asset: z.string().min(1),
|
|
855
|
+
amount: z.string().regex(/^\d+$/),
|
|
856
|
+
payTo: z.string().min(1),
|
|
857
|
+
maxTimeoutSeconds: z.number().int().positive().optional()
|
|
858
|
+
});
|
|
859
|
+
var Shape = JsonValueSchema.optional().catch(void 0);
|
|
860
|
+
var Challenge = Base64Json.pipe(
|
|
861
|
+
z.looseObject({
|
|
862
|
+
x402Version: z.literal(2),
|
|
863
|
+
accepts: z.array(z.unknown()),
|
|
864
|
+
resource: z.looseObject({ url: z.string(), description: z.string().optional().catch(void 0) }).optional().catch(void 0),
|
|
865
|
+
extensions: z.looseObject({
|
|
866
|
+
bazaar: z.looseObject({
|
|
867
|
+
info: z.looseObject({ input: Shape }).optional().catch(void 0),
|
|
868
|
+
schema: z.looseObject({
|
|
869
|
+
properties: z.looseObject({ input: Shape }).optional().catch(void 0)
|
|
870
|
+
}).optional().catch(void 0)
|
|
871
|
+
}).optional().catch(void 0)
|
|
872
|
+
}).optional().catch(void 0)
|
|
873
|
+
})
|
|
874
|
+
).transform((c) => ({
|
|
875
|
+
resource: c.resource ? { url: c.resource.url, description: c.resource.description } : null,
|
|
876
|
+
accepts: c.accepts.flatMap((a) => {
|
|
877
|
+
const r = Requirement.safeParse(a);
|
|
878
|
+
return r.success ? [r.data] : [];
|
|
879
|
+
}),
|
|
880
|
+
input: c.extensions?.bazaar?.info?.input ?? null,
|
|
881
|
+
inputSchema: c.extensions?.bazaar?.schema?.properties?.input ?? null
|
|
882
|
+
}));
|
|
883
|
+
var Settlement = Base64Json.pipe(
|
|
884
|
+
z.looseObject({
|
|
885
|
+
success: z.boolean(),
|
|
886
|
+
transaction: z.string().optional().catch(void 0),
|
|
887
|
+
network: z.string().optional().catch(void 0),
|
|
888
|
+
payer: z.string().optional().catch(void 0),
|
|
889
|
+
errorReason: z.string().optional().catch(void 0),
|
|
890
|
+
errorMessage: z.string().optional().catch(void 0)
|
|
891
|
+
})
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
// src/x402/x402.ts
|
|
895
|
+
var X402 = class _X402 {
|
|
896
|
+
constructor(api) {
|
|
897
|
+
this.api = api;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* x402-payable APIs matching `q`, grouped by vendor, as the platform finds
|
|
901
|
+
* them live in public catalogs (GAP-90); with `network`, only what can be
|
|
902
|
+
* paid there. `sources` says which catalogs answered. Null when the
|
|
903
|
+
* platform could not be reached (GAP-70); a rejected query throws.
|
|
904
|
+
*/
|
|
905
|
+
async catalog(search) {
|
|
906
|
+
try {
|
|
907
|
+
return await this.api.get("/v1/x402/catalog", {
|
|
908
|
+
q: search.q,
|
|
909
|
+
network: search.network,
|
|
910
|
+
limit: search.limit
|
|
911
|
+
});
|
|
912
|
+
} catch (err) {
|
|
913
|
+
if (!Transport.outage(err)) throw err;
|
|
914
|
+
console.error("[beltic]", err);
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* A fetch bound to `session` that records what x402 crosses it. A request
|
|
920
|
+
* made while the session has no stream (the platform could not open one,
|
|
921
|
+
* GAP-70) goes through unrecorded.
|
|
922
|
+
*/
|
|
923
|
+
fetch(session) {
|
|
924
|
+
return async (input, init) => {
|
|
925
|
+
const stream = await session.stream();
|
|
926
|
+
if (!stream) return globalThis.fetch(input, init);
|
|
927
|
+
const headers = new Headers(
|
|
928
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
929
|
+
);
|
|
930
|
+
headers.set(SESSION_HEADER, stream.id);
|
|
931
|
+
const payload = PaymentPayload.safeParse(headers.get("PAYMENT-SIGNATURE")).data;
|
|
932
|
+
if (payload) {
|
|
933
|
+
const bound = {
|
|
934
|
+
...payload,
|
|
935
|
+
extensions: { ...payload.extensions, [SESSION_EXTENSION]: stream.id }
|
|
936
|
+
};
|
|
937
|
+
headers.set("PAYMENT-SIGNATURE", _X402.encode(bound));
|
|
938
|
+
await stream.emit("payment.presented", x402Moments.payload(bound));
|
|
939
|
+
await stream.flush();
|
|
940
|
+
}
|
|
941
|
+
const res = await globalThis.fetch(input, { ...init, headers });
|
|
942
|
+
if (res.status !== 402) return res;
|
|
943
|
+
const required = PaymentRequired.safeParse(res.headers.get("PAYMENT-REQUIRED")).data;
|
|
944
|
+
if (required) await stream.emit("payment.requested", x402Moments.required(required));
|
|
945
|
+
return res;
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/** The unpaid request, sent as it will be paid, and the challenge it answered. */
|
|
949
|
+
async quote(session, url, request = {}) {
|
|
950
|
+
const res = await this.fetch(session)(url, _X402.init(request));
|
|
951
|
+
await res.body?.cancel();
|
|
952
|
+
if (res.status !== 402) return { status: res.status, challenge: null };
|
|
953
|
+
const challenge = Challenge.safeParse(res.headers.get("PAYMENT-REQUIRED"));
|
|
954
|
+
return { status: 402, challenge: challenge.success ? challenge.data : null };
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* The request, sent as it was quoted, carrying `signed` as an x402 v2
|
|
958
|
+
* `PAYMENT-SIGNATURE` through `fetch` — so the presentation is recorded,
|
|
959
|
+
* bound to the session and flushed before it leaves (GAP-66). What the
|
|
960
|
+
* seller settled is read, not recorded (GAP-77).
|
|
961
|
+
*/
|
|
962
|
+
async pay(session, url, signed, request = {}) {
|
|
963
|
+
const payment = {
|
|
964
|
+
x402Version: 2,
|
|
965
|
+
resource: { url },
|
|
966
|
+
accepted: signed.accepted,
|
|
967
|
+
payload: signed.payload
|
|
968
|
+
};
|
|
969
|
+
const response = await this.fetch(session)(
|
|
970
|
+
url,
|
|
971
|
+
_X402.init(request, { "PAYMENT-SIGNATURE": _X402.encode(payment) })
|
|
972
|
+
);
|
|
973
|
+
const settlement = Settlement.safeParse(response.headers.get("PAYMENT-RESPONSE"));
|
|
974
|
+
return { response, settlement: settlement.success ? settlement.data : null };
|
|
975
|
+
}
|
|
976
|
+
/** An `accepts` entry as the payment `session.decide` takes — the normalization the recorded moments use. */
|
|
977
|
+
summary(accepts, opts = {}) {
|
|
978
|
+
return x402Moments.summary(accepts, opts);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* The declared intent for an x402 mandate (Fraud SDK RFC › Session ›
|
|
982
|
+
* `intent.declared`). The cap must be in the currency the rail's moments
|
|
983
|
+
* carry — `<network>/<asset>`, atomic units (GAP-49) — or the platform's
|
|
984
|
+
* spend detectors compare two currencies and never meet.
|
|
985
|
+
*/
|
|
986
|
+
intent(input) {
|
|
987
|
+
return {
|
|
988
|
+
mandate: input.mandate,
|
|
989
|
+
maxAmount: Money.x402(input.network, input.asset, input.maxAmount).toAmount(),
|
|
990
|
+
validUntil: new Date(input.validUntil).toISOString(),
|
|
991
|
+
...input.merchantAllowlist ? { merchantAllowlist: input.merchantAllowlist } : {}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
/** The request as `quote` and `pay` send it: `GET` bare, `POST` with a JSON body, unless told otherwise. */
|
|
995
|
+
static init(request, extra = {}) {
|
|
996
|
+
const headers = { accept: "*/*", ...extra };
|
|
997
|
+
if (request.body === void 0) return { method: request.method ?? "GET", headers };
|
|
998
|
+
return {
|
|
999
|
+
method: request.method ?? "POST",
|
|
1000
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
1001
|
+
body: JSON.stringify(request.body)
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
/** An x402 header value: base64 of the JSON. */
|
|
1005
|
+
static encode(value) {
|
|
1006
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
634
1010
|
// src/client.ts
|
|
635
|
-
var SDK_VERSION = "0.
|
|
1011
|
+
var SDK_VERSION = "0.8.0";
|
|
636
1012
|
var Beltic = class _Beltic {
|
|
1013
|
+
/** x402 for the buyer: `catalog`, `fetch`, `quote`, `pay`, `summary`, `intent`. */
|
|
1014
|
+
x402;
|
|
637
1015
|
/** The stream registry, for the protocol adapters. @internal */
|
|
638
1016
|
streams;
|
|
639
1017
|
api;
|
|
640
1018
|
transport;
|
|
1019
|
+
/** Open handles by conversation id (GAP-84). */
|
|
641
1020
|
sessions = /* @__PURE__ */ new Map();
|
|
642
1021
|
/**
|
|
643
1022
|
* The client the environment describes: `BELTIC_API_KEY` and
|
|
@@ -665,6 +1044,7 @@ var Beltic = class _Beltic {
|
|
|
665
1044
|
`@belticlabs/agent-risk-sdk/${SDK_VERSION}`
|
|
666
1045
|
);
|
|
667
1046
|
this.transport = new Transport(this.api);
|
|
1047
|
+
this.x402 = new X402(this.api);
|
|
668
1048
|
this.streams = new Streams({
|
|
669
1049
|
api: this.api,
|
|
670
1050
|
transport: this.transport,
|
|
@@ -673,38 +1053,48 @@ var Beltic = class _Beltic {
|
|
|
673
1053
|
});
|
|
674
1054
|
}
|
|
675
1055
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
1056
|
+
* A conversation: new without an id, resumed with one — the id `session.id()`
|
|
1057
|
+
* answered in this or any earlier process (GAP-84). One handle per
|
|
1058
|
+
* conversation in-process until it closes; the options count when a
|
|
1059
|
+
* session is opened. See `Session`.
|
|
679
1060
|
*/
|
|
680
|
-
session(
|
|
681
|
-
const existing = this.sessions.get(
|
|
1061
|
+
session(id, opts = {}) {
|
|
1062
|
+
const existing = id ? this.sessions.get(id) : void 0;
|
|
682
1063
|
if (existing) return existing;
|
|
683
1064
|
const session = new Session(
|
|
684
1065
|
{
|
|
685
1066
|
streams: this.streams,
|
|
686
|
-
evaluate: (sessionId, payment) => this.evaluate(sessionId, payment),
|
|
1067
|
+
evaluate: (sessionId, payment, callId) => this.evaluate(sessionId, payment, { callId }),
|
|
1068
|
+
onOpened: (opened, conversationId) => {
|
|
1069
|
+
if (!this.sessions.has(conversationId)) this.sessions.set(conversationId, opened);
|
|
1070
|
+
},
|
|
687
1071
|
onClosed: (closed) => {
|
|
688
|
-
|
|
1072
|
+
for (const [key, s] of this.sessions) if (s === closed) this.sessions.delete(key);
|
|
689
1073
|
}
|
|
690
1074
|
},
|
|
691
|
-
|
|
692
|
-
|
|
1075
|
+
opts,
|
|
1076
|
+
id ?? null
|
|
693
1077
|
);
|
|
694
|
-
this.sessions.set(
|
|
1078
|
+
if (id) this.sessions.set(id, session);
|
|
695
1079
|
return session;
|
|
696
1080
|
}
|
|
697
1081
|
/**
|
|
698
1082
|
* The platform's verdict on a payment — the seller's before it verifies,
|
|
699
1083
|
* the buyer's before it presents (Fraud SDK RFC › Evaluation Client).
|
|
700
|
-
* Read-your-writes: the buffered evidence is flushed first so
|
|
701
|
-
* platform judges what the caller already saw (GAP-16).
|
|
702
|
-
* platform
|
|
1084
|
+
* Read-your-writes: the session's buffered evidence is flushed first so
|
|
1085
|
+
* the platform judges what the caller already saw (GAP-16/88). With a `callId`
|
|
1086
|
+
* the platform answers the decision already taken for that call anywhere
|
|
1087
|
+
* in the conversation (GAP-84). Absent when the platform could not be
|
|
1088
|
+
* reached (GAP-70).
|
|
703
1089
|
*/
|
|
704
|
-
async evaluate(sessionId, payment) {
|
|
705
|
-
const input = {
|
|
1090
|
+
async evaluate(sessionId, payment, opts = {}) {
|
|
1091
|
+
const input = {
|
|
1092
|
+
sessionId,
|
|
1093
|
+
payment,
|
|
1094
|
+
...opts.callId ? { callId: opts.callId } : {}
|
|
1095
|
+
};
|
|
706
1096
|
try {
|
|
707
|
-
await this.transport.flush();
|
|
1097
|
+
await this.transport.flush({ sessionId });
|
|
708
1098
|
return Decision.of(await this.api.post("/v1/evaluate", input));
|
|
709
1099
|
} catch (err) {
|
|
710
1100
|
if (!Transport.outage(err)) throw err;
|
|
@@ -712,7 +1102,7 @@ var Beltic = class _Beltic {
|
|
|
712
1102
|
return Decision.absent();
|
|
713
1103
|
}
|
|
714
1104
|
}
|
|
715
|
-
/** Send everything buffered now and wait for that attempt. */
|
|
1105
|
+
/** Send everything buffered now — every session's — and wait for that attempt. */
|
|
716
1106
|
flush() {
|
|
717
1107
|
return this.transport.flush();
|
|
718
1108
|
}
|
|
@@ -731,5 +1121,6 @@ export {
|
|
|
731
1121
|
ChainRejectedError,
|
|
732
1122
|
Decision,
|
|
733
1123
|
SDK_VERSION,
|
|
734
|
-
Session
|
|
1124
|
+
Session,
|
|
1125
|
+
X402
|
|
735
1126
|
};
|