@belticlabs/agent-risk-sdk 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.d.ts +2 -2
- package/dist/ai/index.js +15 -18
- package/dist/chunk-FNU4CRJJ.js +547 -0
- package/dist/{chunk-IUWC6HT5.js → chunk-HI656UWG.js} +12 -14
- package/dist/{chunk-ZMPKY7AX.js → chunk-JGVVQUXQ.js} +1 -2
- package/dist/{chunk-77D74TWX.js → chunk-UXXSE643.js} +25 -22
- package/dist/{client-CgCjOrRP.d.ts → client-Xn9M58OJ.d.ts} +18 -12
- package/dist/{index-IfY4XCvJ.d.ts → index-DAVKGJs5.d.ts} +129 -139
- package/dist/index.d.ts +3 -3
- package/dist/index.js +351 -165
- package/dist/protocol/index.d.ts +51 -30
- package/dist/protocol/index.js +50 -529
- package/dist/{session-Dcof4UIn.d.ts → session-DAN29XoB.d.ts} +115 -43
- package/dist/x402/hono.d.ts +3 -4
- package/dist/x402/hono.js +3 -3
- package/dist/x402/index.d.ts +7 -22
- package/dist/x402/index.js +33 -24
- package/package.json +1 -1
- package/dist/adapter-AjCgj-KM.d.ts +0 -6
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
openCall
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JGVVQUXQ.js";
|
|
4
4
|
import {
|
|
5
5
|
summaryOf
|
|
6
6
|
} from "./chunk-M4I3FGZG.js";
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
memorySigner,
|
|
13
13
|
sha256,
|
|
14
14
|
toHex
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-UXXSE643.js";
|
|
16
|
+
import {
|
|
17
|
+
ApiErrorSchema,
|
|
18
|
+
Hex64Schema
|
|
19
|
+
} from "./chunk-FNU4CRJJ.js";
|
|
16
20
|
|
|
17
21
|
// src/core/api-client.ts
|
|
18
22
|
var TIMEOUT_MS = 1e4;
|
|
@@ -69,7 +73,7 @@ var ApiClient = class {
|
|
|
69
73
|
json = null;
|
|
70
74
|
}
|
|
71
75
|
if (!res.ok) {
|
|
72
|
-
const e = json?.error;
|
|
76
|
+
const e = ApiErrorSchema.safeParse(json).data?.error;
|
|
73
77
|
throw new BelticApiError(
|
|
74
78
|
res.status,
|
|
75
79
|
e?.code ?? `HTTP_${res.status}`,
|
|
@@ -134,10 +138,11 @@ var Decision = class _Decision {
|
|
|
134
138
|
};
|
|
135
139
|
|
|
136
140
|
// src/core/identity.ts
|
|
137
|
-
var SEED_HEX = /^[0-9a-f]{64}$/i;
|
|
138
141
|
function identityFromSeed(seedHex) {
|
|
139
|
-
if (!
|
|
140
|
-
throw new BelticConfigError(
|
|
142
|
+
if (!Hex64Schema.safeParse(seedHex).success)
|
|
143
|
+
throw new BelticConfigError(
|
|
144
|
+
"agentSeed must be 64 lowercase hex characters (a 32-byte Ed25519 seed)"
|
|
145
|
+
);
|
|
141
146
|
const signer = memorySigner(fromHex(seedHex));
|
|
142
147
|
const did = didKeyFromEd25519(signer.publicKey);
|
|
143
148
|
return { did, signer: { ...signer, keyId: did } };
|
|
@@ -157,14 +162,16 @@ var Memory = class {
|
|
|
157
162
|
}
|
|
158
163
|
};
|
|
159
164
|
var Session = class _Session {
|
|
160
|
-
constructor(deps,
|
|
165
|
+
constructor(deps, opts = {}, resume = null) {
|
|
161
166
|
this.deps = deps;
|
|
162
|
-
this.key = key;
|
|
163
167
|
this.opts = opts;
|
|
168
|
+
this.conversationId = resume;
|
|
164
169
|
this.openedWith = opts.intent ? _Session.hash(opts.intent) : null;
|
|
165
170
|
}
|
|
166
171
|
opened = null;
|
|
167
172
|
current = null;
|
|
173
|
+
/** The conversation id: what was resumed, then what the platform answered. */
|
|
174
|
+
conversationId;
|
|
168
175
|
/** JCS hash of the mandate on the chain, and of the one the options carried. */
|
|
169
176
|
declared = null;
|
|
170
177
|
openedWith;
|
|
@@ -172,18 +179,29 @@ var Session = class _Session {
|
|
|
172
179
|
timer = null;
|
|
173
180
|
calls = new Memory();
|
|
174
181
|
decisions = new Memory();
|
|
175
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* The conversation id — what the host stores and resumes with (GAP-84).
|
|
184
|
+
* Opened on first use; `null` while the platform has not answered and
|
|
185
|
+
* nothing was resumed.
|
|
186
|
+
*/
|
|
176
187
|
id() {
|
|
177
|
-
return this.stream().then((stream) => stream?.
|
|
188
|
+
return this.stream().then((stream) => stream?.conversationId ?? this.conversationId);
|
|
178
189
|
}
|
|
179
190
|
/**
|
|
180
|
-
* The stream this session records into — opened on first use,
|
|
191
|
+
* The stream this session records into — opened on first use, reopened
|
|
192
|
+
* in the same conversation once the platform ended it (GAP-85), `null`
|
|
181
193
|
* when there is none. For the integrations; a host never holds it.
|
|
182
194
|
* @internal
|
|
183
195
|
*/
|
|
184
196
|
stream() {
|
|
185
|
-
const
|
|
197
|
+
const prior = this.opened ?? Promise.resolve(null);
|
|
198
|
+
const next = prior.catch(() => null).then(
|
|
199
|
+
(stream) => stream && (this.deps.streams.halted(stream) || !stream.isClosed) ? stream : this.open()
|
|
200
|
+
);
|
|
186
201
|
this.opened = next;
|
|
202
|
+
next.catch(() => {
|
|
203
|
+
if (this.opened === next) this.opened = null;
|
|
204
|
+
});
|
|
187
205
|
return next.then((stream) => {
|
|
188
206
|
if (stream !== this.current) {
|
|
189
207
|
this.current = stream;
|
|
@@ -192,6 +210,17 @@ var Session = class _Session {
|
|
|
192
210
|
return stream;
|
|
193
211
|
});
|
|
194
212
|
}
|
|
213
|
+
async open() {
|
|
214
|
+
const stream = await this.deps.streams.open({
|
|
215
|
+
...this.opts,
|
|
216
|
+
resume: this.conversationId ?? void 0
|
|
217
|
+
});
|
|
218
|
+
if (stream) {
|
|
219
|
+
this.conversationId = stream.conversationId;
|
|
220
|
+
this.deps.onOpened(this, stream.conversationId);
|
|
221
|
+
}
|
|
222
|
+
return stream;
|
|
223
|
+
}
|
|
195
224
|
/**
|
|
196
225
|
* The platform's verdict on a payment about to be presented. Asked once
|
|
197
226
|
* per call id: a host that re-runs its approval step reads the same
|
|
@@ -205,7 +234,7 @@ var Session = class _Session {
|
|
|
205
234
|
if (!stream) return Decision.absent();
|
|
206
235
|
if (opts.intent) await this.declare(stream, opts.intent);
|
|
207
236
|
const summary = summaryOf(payment);
|
|
208
|
-
const decision = await this.deps.evaluate(stream.id, summary);
|
|
237
|
+
const decision = await this.deps.evaluate(stream.id, summary, opts.callId);
|
|
209
238
|
if (decision.absent) return decision;
|
|
210
239
|
if (opts.callId) this.decisions.set(opts.callId, decision);
|
|
211
240
|
const call = opts.callId ? this.calls.get(opts.callId)?.call : void 0;
|
|
@@ -304,86 +333,46 @@ var Session = class _Session {
|
|
|
304
333
|
}
|
|
305
334
|
};
|
|
306
335
|
|
|
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;
|
|
336
|
+
// src/core/prompt-ledger.ts
|
|
337
|
+
var PromptLedger = class _PromptLedger {
|
|
338
|
+
last;
|
|
339
|
+
constructor(base = null) {
|
|
340
|
+
this.last = base;
|
|
341
|
+
}
|
|
342
|
+
/** How the platform digests one message: over its canonical bytes. */
|
|
343
|
+
static digest(message) {
|
|
344
|
+
return toHex(sha256(canonicalBytes(message)));
|
|
345
|
+
}
|
|
346
|
+
/** The prompt as the wire carries it — whole, or against the last one — and this call becomes the last. */
|
|
347
|
+
encode(callId, messages) {
|
|
348
|
+
const digests = messages.map(_PromptLedger.digest);
|
|
349
|
+
const last = this.last;
|
|
350
|
+
this.last = { prior: callId, digests };
|
|
351
|
+
if (!last) return { prompt: messages };
|
|
352
|
+
let shared = 0;
|
|
353
|
+
while (shared < last.digests.length && shared < digests.length && last.digests[shared] === digests[shared])
|
|
354
|
+
shared++;
|
|
355
|
+
if (shared === 0) return { prompt: messages };
|
|
356
|
+
return { promptDelta: { prior: last.prior, shared, messages: messages.slice(shared) } };
|
|
357
|
+
}
|
|
358
|
+
/** The last call never reached the platform: the next one ships whole. */
|
|
359
|
+
forget() {
|
|
360
|
+
this.last = null;
|
|
378
361
|
}
|
|
379
362
|
};
|
|
380
363
|
|
|
381
364
|
// src/core/transport.ts
|
|
382
365
|
var FLUSH_MS = 1e3;
|
|
383
366
|
var MAX_BATCH = 50;
|
|
367
|
+
var MAX_BATCH_BYTES = 8 * 1024 * 1024;
|
|
368
|
+
var MAX_EVENT_BYTES = 1024 * 1024;
|
|
384
369
|
var MAX_BUFFERED = 5e3;
|
|
370
|
+
var MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
371
|
+
var MAX_IN_FLIGHT = 8;
|
|
372
|
+
var ENVELOPE_BYTES = 512;
|
|
385
373
|
var BACKOFF_BASE_MS = 200;
|
|
386
374
|
var BACKOFF_MAX_MS = 3e4;
|
|
375
|
+
var ENDED_CODES = /* @__PURE__ */ new Set(["SESSION_CLOSED", "SESSION_EXPIRED"]);
|
|
387
376
|
var ChainRejectedError = class extends Error {
|
|
388
377
|
constructor(sessionId, source, result, options) {
|
|
389
378
|
super(
|
|
@@ -396,18 +385,15 @@ var ChainRejectedError = class extends Error {
|
|
|
396
385
|
this.name = "ChainRejectedError";
|
|
397
386
|
}
|
|
398
387
|
};
|
|
399
|
-
var TransportClosedError = class extends Error {
|
|
400
|
-
constructor() {
|
|
401
|
-
super("transport is closed");
|
|
402
|
-
this.name = "TransportClosedError";
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
388
|
var Transport = class _Transport {
|
|
406
389
|
constructor(api) {
|
|
407
390
|
this.api = api;
|
|
408
391
|
}
|
|
409
392
|
chains = /* @__PURE__ */ new Map();
|
|
393
|
+
waiting = [];
|
|
394
|
+
running = 0;
|
|
410
395
|
buffered = 0;
|
|
396
|
+
bufferedBytes = 0;
|
|
411
397
|
timer = null;
|
|
412
398
|
closed = false;
|
|
413
399
|
/**
|
|
@@ -419,35 +405,62 @@ var Transport = class _Transport {
|
|
|
419
405
|
static outage(err) {
|
|
420
406
|
return err instanceof BelticApiError && err.retryable;
|
|
421
407
|
}
|
|
422
|
-
|
|
423
|
-
|
|
408
|
+
/** The bytes the platform will measure for an event with this payload (GAP-59), known before a `seq` is spent. */
|
|
409
|
+
static measure(payload) {
|
|
410
|
+
return canonicalBytes(payload).length + ENVELOPE_BYTES;
|
|
411
|
+
}
|
|
412
|
+
/** Whether the platform would take an event of this size at all. */
|
|
413
|
+
static fits(bytes) {
|
|
414
|
+
return bytes <= MAX_EVENT_BYTES;
|
|
424
415
|
}
|
|
425
|
-
hasRoom() {
|
|
426
|
-
return !this.closed && this.buffered < MAX_BUFFERED;
|
|
416
|
+
hasRoom(bytes) {
|
|
417
|
+
return !this.closed && this.buffered < MAX_BUFFERED && this.bufferedBytes + bytes <= MAX_BUFFERED_BYTES;
|
|
427
418
|
}
|
|
428
419
|
haltedError(sessionId, source) {
|
|
429
420
|
return this.chains.get(`${sessionId}:${source}`)?.halted ?? null;
|
|
430
421
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
422
|
+
/** Whether the platform ended the session under this chain (GAP-85). */
|
|
423
|
+
ended(sessionId, source) {
|
|
424
|
+
return this.chains.get(`${sessionId}:${source}`)?.ended ?? false;
|
|
425
|
+
}
|
|
426
|
+
/** Callers check `hasRoom(bytes)` first and assign `seq` only then (GAP-38). */
|
|
427
|
+
enqueue(ev, bytes) {
|
|
428
|
+
if (this.closed) throw new Error("transport is closed");
|
|
434
429
|
const key = `${ev.sessionId}:${ev.source}`;
|
|
435
430
|
let chain = this.chains.get(key);
|
|
436
431
|
if (!chain) {
|
|
437
|
-
chain = {
|
|
432
|
+
chain = {
|
|
433
|
+
sessionId: ev.sessionId,
|
|
434
|
+
pending: [],
|
|
435
|
+
inFlight: null,
|
|
436
|
+
start: null,
|
|
437
|
+
retry: null,
|
|
438
|
+
attempts: 0,
|
|
439
|
+
halted: null,
|
|
440
|
+
ended: false
|
|
441
|
+
};
|
|
438
442
|
this.chains.set(key, chain);
|
|
439
443
|
}
|
|
440
444
|
if (chain.halted) throw chain.halted;
|
|
441
|
-
if (
|
|
442
|
-
|
|
445
|
+
if (chain.ended) return;
|
|
446
|
+
if (!this.hasRoom(bytes)) throw new Error("transport buffer is full");
|
|
447
|
+
chain.pending.push({ ev, bytes });
|
|
443
448
|
this.buffered++;
|
|
449
|
+
this.bufferedBytes += bytes;
|
|
444
450
|
if (chain.pending.length >= MAX_BATCH) void this.deliver(chain, false);
|
|
445
451
|
else this.schedule();
|
|
446
452
|
}
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
453
|
+
/**
|
|
454
|
+
* One attempt per chain, now — a chain waiting out its backoff included;
|
|
455
|
+
* resolves once every attempt settled. With a scope, only that session's
|
|
456
|
+
* chains (GAP-88).
|
|
457
|
+
*/
|
|
458
|
+
async flush(scope) {
|
|
459
|
+
if (!scope) this.unschedule();
|
|
460
|
+
const chains = [...this.chains.values()].filter(
|
|
461
|
+
(chain) => !scope || chain.sessionId === scope.sessionId
|
|
462
|
+
);
|
|
463
|
+
await Promise.all(chains.map((chain) => this.deliver(chain, true)));
|
|
451
464
|
}
|
|
452
465
|
async close() {
|
|
453
466
|
await this.flush();
|
|
@@ -470,28 +483,72 @@ var Transport = class _Transport {
|
|
|
470
483
|
clearTimeout(this.timer);
|
|
471
484
|
this.timer = null;
|
|
472
485
|
}
|
|
473
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* Queues the chain for a delivery slot — at the front when forced. A
|
|
488
|
+
* chain waiting out its backoff is left alone unless forced: only
|
|
489
|
+
* `flush` cuts a backoff short.
|
|
490
|
+
*/
|
|
474
491
|
deliver(chain, force) {
|
|
475
|
-
if (chain.inFlight)
|
|
476
|
-
|
|
492
|
+
if (chain.inFlight) {
|
|
493
|
+
if (force && chain.start) this.promote(chain);
|
|
494
|
+
return chain.inFlight;
|
|
495
|
+
}
|
|
496
|
+
if (chain.halted || chain.ended || chain.pending.length === 0) return Promise.resolve();
|
|
477
497
|
if (chain.retry) {
|
|
478
498
|
if (!force) return Promise.resolve();
|
|
479
499
|
clearTimeout(chain.retry);
|
|
480
500
|
chain.retry = null;
|
|
481
501
|
}
|
|
482
|
-
chain.inFlight =
|
|
483
|
-
chain.
|
|
502
|
+
chain.inFlight = new Promise((resolve) => {
|
|
503
|
+
chain.start = () => {
|
|
504
|
+
chain.start = null;
|
|
505
|
+
this.running++;
|
|
506
|
+
void this.attempt(chain).finally(() => {
|
|
507
|
+
this.running--;
|
|
508
|
+
chain.inFlight = null;
|
|
509
|
+
resolve();
|
|
510
|
+
this.pump();
|
|
511
|
+
});
|
|
512
|
+
};
|
|
484
513
|
});
|
|
514
|
+
if (force) this.waiting.unshift(chain);
|
|
515
|
+
else this.waiting.push(chain);
|
|
516
|
+
this.pump();
|
|
485
517
|
return chain.inFlight;
|
|
486
518
|
}
|
|
519
|
+
promote(chain) {
|
|
520
|
+
const at = this.waiting.indexOf(chain);
|
|
521
|
+
if (at > 0) {
|
|
522
|
+
this.waiting.splice(at, 1);
|
|
523
|
+
this.waiting.unshift(chain);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
pump() {
|
|
527
|
+
while (this.running < MAX_IN_FLIGHT && this.waiting.length > 0) this.waiting.shift().start?.();
|
|
528
|
+
}
|
|
529
|
+
/** The head of the queue that fits one batch: by count or by bytes, whichever comes first, and never empty. */
|
|
530
|
+
static batchOf(chain) {
|
|
531
|
+
const batch = [];
|
|
532
|
+
let bytes = 0;
|
|
533
|
+
for (const item of chain.pending) {
|
|
534
|
+
if (batch.length >= MAX_BATCH) break;
|
|
535
|
+
if (batch.length > 0 && bytes + item.bytes > MAX_BATCH_BYTES) break;
|
|
536
|
+
batch.push(item);
|
|
537
|
+
bytes += item.bytes;
|
|
538
|
+
}
|
|
539
|
+
return batch;
|
|
540
|
+
}
|
|
487
541
|
/** Batches until the chain drains; a retryable failure schedules the next attempt and returns. */
|
|
488
542
|
async attempt(chain) {
|
|
489
543
|
while (chain.pending.length > 0 && !chain.halted) {
|
|
490
|
-
const batch =
|
|
491
|
-
const first = batch[0];
|
|
544
|
+
const batch = _Transport.batchOf(chain);
|
|
545
|
+
const first = batch[0].ev;
|
|
492
546
|
let ack;
|
|
493
547
|
try {
|
|
494
|
-
ack = await this.api.post(
|
|
548
|
+
ack = await this.api.post(
|
|
549
|
+
"/v1/evidence",
|
|
550
|
+
batch.map((item) => item.ev)
|
|
551
|
+
);
|
|
495
552
|
} catch (err) {
|
|
496
553
|
console.error("[beltic]", err);
|
|
497
554
|
if (!_Transport.outage(err)) {
|
|
@@ -522,19 +579,142 @@ var Transport = class _Transport {
|
|
|
522
579
|
chain.retry.unref?.();
|
|
523
580
|
return;
|
|
524
581
|
}
|
|
525
|
-
|
|
526
|
-
this.buffered -= batch.length;
|
|
582
|
+
this.release(chain, batch.length);
|
|
527
583
|
chain.attempts = 0;
|
|
528
584
|
const bad = ack.results.find((r) => r.status === "fork" || r.status === "rejected");
|
|
529
|
-
if (bad)
|
|
585
|
+
if (!bad) continue;
|
|
586
|
+
if (bad.status === "rejected" && bad.code && ENDED_CODES.has(bad.code)) this.end(chain);
|
|
587
|
+
else this.halt(chain, new ChainRejectedError(first.sessionId, first.source, bad));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
release(chain, count) {
|
|
591
|
+
for (const item of chain.pending.splice(0, count)) {
|
|
592
|
+
this.buffered--;
|
|
593
|
+
this.bufferedBytes -= item.bytes;
|
|
530
594
|
}
|
|
531
595
|
}
|
|
596
|
+
drop(chain) {
|
|
597
|
+
this.release(chain, chain.pending.length);
|
|
598
|
+
}
|
|
532
599
|
halt(chain, error) {
|
|
533
600
|
chain.halted = error;
|
|
534
|
-
this.
|
|
535
|
-
chain.pending = [];
|
|
601
|
+
this.drop(chain);
|
|
536
602
|
console.error("[beltic]", error);
|
|
537
603
|
}
|
|
604
|
+
end(chain) {
|
|
605
|
+
chain.ended = true;
|
|
606
|
+
this.drop(chain);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// src/core/stream.ts
|
|
611
|
+
var Stream = class {
|
|
612
|
+
constructor(deps, id, conversationId, source, born) {
|
|
613
|
+
this.deps = deps;
|
|
614
|
+
this.id = id;
|
|
615
|
+
this.conversationId = conversationId;
|
|
616
|
+
this.source = source;
|
|
617
|
+
this.born = born;
|
|
618
|
+
this.chain = Chain.genesis(id, source);
|
|
619
|
+
this.ledger = new PromptLedger(deps.prompt ?? null);
|
|
620
|
+
}
|
|
621
|
+
chain;
|
|
622
|
+
ledger;
|
|
623
|
+
building = Promise.resolve();
|
|
624
|
+
dropped = 0;
|
|
625
|
+
oversized = 0;
|
|
626
|
+
droppedFirstTs = null;
|
|
627
|
+
droppedLastTs = null;
|
|
628
|
+
closed = false;
|
|
629
|
+
/** Closed by this side, or ended by the platform (GAP-85). */
|
|
630
|
+
get isClosed() {
|
|
631
|
+
return this.closed || this.deps.transport.ended(this.id, this.source);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Resolves once the event is sequenced and buffered — not once it is
|
|
635
|
+
* acknowledged. `false` when the event was dropped for lack of room.
|
|
636
|
+
*/
|
|
637
|
+
async emit(kind, payload) {
|
|
638
|
+
const halted = this.deps.transport.haltedError(this.id, this.source);
|
|
639
|
+
if (halted) throw halted;
|
|
640
|
+
if (this.deps.transport.ended(this.id, this.source)) return false;
|
|
641
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
642
|
+
const bytes = Transport.measure(payload);
|
|
643
|
+
const gap = this.dropped > 0 ? this.gap() : null;
|
|
644
|
+
const gapBytes = gap ? Transport.measure(gap) : 0;
|
|
645
|
+
if (!Transport.fits(bytes) || !this.deps.transport.hasRoom(bytes + gapBytes)) {
|
|
646
|
+
this.dropped++;
|
|
647
|
+
if (!Transport.fits(bytes)) this.oversized++;
|
|
648
|
+
this.droppedFirstTs ??= ts;
|
|
649
|
+
this.droppedLastTs = ts;
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (gap) {
|
|
653
|
+
this.dropped = this.oversized = 0;
|
|
654
|
+
this.droppedFirstTs = this.droppedLastTs = null;
|
|
655
|
+
this.deps.transport.enqueue(
|
|
656
|
+
await this.next("transport.gap", gap, ts),
|
|
657
|
+
gapBytes
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
this.deps.transport.enqueue(await this.next(kind, payload, ts), bytes);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
/** What the drops since the last accepted event add up to (GAP-38/87). */
|
|
664
|
+
gap() {
|
|
665
|
+
return {
|
|
666
|
+
dropped: this.dropped,
|
|
667
|
+
...this.oversized > 0 ? { oversized: this.oversized } : {},
|
|
668
|
+
firstTs: this.droppedFirstTs,
|
|
669
|
+
lastTs: this.droppedLastTs
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* A model call as a span: `llm_call.start` now, with the prompt as a
|
|
674
|
+
* delta against the last one on this chain (GAP-89), `llm_call.end` when
|
|
675
|
+
* the host reports the result. A start that never left (dropped) forgets
|
|
676
|
+
* the ledger, so the next call ships its prompt whole.
|
|
677
|
+
*/
|
|
678
|
+
llmCall(call) {
|
|
679
|
+
const { callId, params, ...start } = call;
|
|
680
|
+
const { prompt, ...rest } = params;
|
|
681
|
+
const encoded = Array.isArray(prompt) ? this.ledger.encode(callId, prompt) : prompt === void 0 ? {} : { prompt };
|
|
682
|
+
const span = openCall(this, "llm_call", callId, { ...start, params: { ...rest, ...encoded } });
|
|
683
|
+
if (Array.isArray(prompt))
|
|
684
|
+
span.opened.then(
|
|
685
|
+
(sequenced) => {
|
|
686
|
+
if (!sequenced) this.ledger.forget();
|
|
687
|
+
},
|
|
688
|
+
() => this.ledger.forget()
|
|
689
|
+
);
|
|
690
|
+
return span;
|
|
691
|
+
}
|
|
692
|
+
/** The tool call whose `execute` the host runs itself; see `ToolCallSpan`. */
|
|
693
|
+
toolCall(call) {
|
|
694
|
+
const { callId, ...start } = call;
|
|
695
|
+
return openCall(this, "tool_call", callId, { transport: "local", ...start });
|
|
696
|
+
}
|
|
697
|
+
async close(reason = "completed", extra = {}) {
|
|
698
|
+
if (this.closed) return;
|
|
699
|
+
this.closed = true;
|
|
700
|
+
await this.emit("session.close", { reason, ...extra });
|
|
701
|
+
await this.flush();
|
|
702
|
+
this.deps.onClosed?.(this);
|
|
703
|
+
}
|
|
704
|
+
/** Read-your-writes for this session's chains, and only them (GAP-16/66/88). */
|
|
705
|
+
flush() {
|
|
706
|
+
return this.deps.transport.flush({ sessionId: this.id });
|
|
707
|
+
}
|
|
708
|
+
/** Serialized: two concurrent emits get consecutive seqs, never the same one. */
|
|
709
|
+
next(kind, payload, ts) {
|
|
710
|
+
const run = this.building.then(async () => {
|
|
711
|
+
const built = await this.chain.append({ ts, kind, payload }, this.deps.signer);
|
|
712
|
+
this.chain = built.chain;
|
|
713
|
+
return built.event;
|
|
714
|
+
});
|
|
715
|
+
this.building = run.catch(() => void 0);
|
|
716
|
+
return run;
|
|
717
|
+
}
|
|
538
718
|
};
|
|
539
719
|
|
|
540
720
|
// src/core/streams.ts
|
|
@@ -544,35 +724,12 @@ var Streams = class {
|
|
|
544
724
|
this.deps = deps;
|
|
545
725
|
}
|
|
546
726
|
attached = /* @__PURE__ */ new Map();
|
|
547
|
-
/** Buyer streams by the host's own key (GAP-71). */
|
|
548
|
-
opened = /* @__PURE__ */ new Map();
|
|
549
727
|
retryAt = 0;
|
|
550
728
|
/**
|
|
551
|
-
* Buyer half:
|
|
552
|
-
*
|
|
553
|
-
* that continues the same key; a closed key is forgotten.
|
|
729
|
+
* Buyer half: a fresh AGENT_TRACE stream — a new conversation, or a new
|
|
730
|
+
* session of the one `resume` names. `null` while the platform is out.
|
|
554
731
|
*/
|
|
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) {
|
|
732
|
+
async open(input = {}, onClosed) {
|
|
576
733
|
const identity = this.deps.identity;
|
|
577
734
|
if (!identity)
|
|
578
735
|
throw new BelticConfigError(
|
|
@@ -590,14 +747,30 @@ var Streams = class {
|
|
|
590
747
|
return null;
|
|
591
748
|
}
|
|
592
749
|
}
|
|
750
|
+
/** Seller half: emit INTERNAL_NETWORK evidence into a session the buyer bound, or open a seller-born one. */
|
|
751
|
+
async ensure(sessionId) {
|
|
752
|
+
if (sessionId) return this.attach(sessionId, sessionId, "INTERNAL_NETWORK", "buyer");
|
|
753
|
+
const out = await this.deps.api.post("/v1/sessions", {
|
|
754
|
+
source: "INTERNAL_NETWORK"
|
|
755
|
+
});
|
|
756
|
+
return this.attach(out.sessionId, out.conversationId, "INTERNAL_NETWORK", "seller");
|
|
757
|
+
}
|
|
758
|
+
/** Whether the platform refused this stream's chain: its next `emit` throws (GAP-85). */
|
|
759
|
+
halted(stream) {
|
|
760
|
+
return this.deps.transport.haltedError(stream.id, stream.source) !== null;
|
|
761
|
+
}
|
|
593
762
|
async create(identity, input, onClosed) {
|
|
594
763
|
const body = {
|
|
595
764
|
source: "AGENT_TRACE",
|
|
596
765
|
agent: { did: identity.did, credential: identity.did },
|
|
597
|
-
...input.intent ? { intent: input.intent } : {}
|
|
766
|
+
...input.intent ? { intent: input.intent } : {},
|
|
767
|
+
...input.resume ? { resume: input.resume } : {}
|
|
598
768
|
};
|
|
599
769
|
const out = await this.deps.api.post("/v1/sessions", body);
|
|
600
|
-
const stream = this.attach(out.sessionId, "AGENT_TRACE", "buyer",
|
|
770
|
+
const stream = this.attach(out.sessionId, out.conversationId, "AGENT_TRACE", "buyer", {
|
|
771
|
+
prompt: out.prompt,
|
|
772
|
+
onClosed
|
|
773
|
+
});
|
|
601
774
|
await stream.emit("session.open", {
|
|
602
775
|
runtime: {
|
|
603
776
|
sdk: "@belticlabs/agent-risk-sdk",
|
|
@@ -609,7 +782,7 @@ var Streams = class {
|
|
|
609
782
|
if (input.intent) await stream.emit("intent.declared", input.intent);
|
|
610
783
|
return stream;
|
|
611
784
|
}
|
|
612
|
-
attach(id, source, born,
|
|
785
|
+
attach(id, conversationId, source, born, opts = {}) {
|
|
613
786
|
const key = `${id}:${source}`;
|
|
614
787
|
const existing = this.attached.get(key);
|
|
615
788
|
if (existing) return existing;
|
|
@@ -617,12 +790,14 @@ var Streams = class {
|
|
|
617
790
|
{
|
|
618
791
|
transport: this.deps.transport,
|
|
619
792
|
signer: source === "AGENT_TRACE" ? this.deps.identity?.signer : void 0,
|
|
793
|
+
prompt: opts.prompt,
|
|
620
794
|
onClosed: () => {
|
|
621
795
|
this.attached.delete(key);
|
|
622
|
-
onClosed?.();
|
|
796
|
+
opts.onClosed?.();
|
|
623
797
|
}
|
|
624
798
|
},
|
|
625
799
|
id,
|
|
800
|
+
conversationId,
|
|
626
801
|
source,
|
|
627
802
|
born
|
|
628
803
|
);
|
|
@@ -632,12 +807,13 @@ var Streams = class {
|
|
|
632
807
|
};
|
|
633
808
|
|
|
634
809
|
// src/client.ts
|
|
635
|
-
var SDK_VERSION = "0.
|
|
810
|
+
var SDK_VERSION = "0.7.0";
|
|
636
811
|
var Beltic = class _Beltic {
|
|
637
812
|
/** The stream registry, for the protocol adapters. @internal */
|
|
638
813
|
streams;
|
|
639
814
|
api;
|
|
640
815
|
transport;
|
|
816
|
+
/** Open handles by conversation id (GAP-84). */
|
|
641
817
|
sessions = /* @__PURE__ */ new Map();
|
|
642
818
|
/**
|
|
643
819
|
* The client the environment describes: `BELTIC_API_KEY` and
|
|
@@ -673,38 +849,48 @@ var Beltic = class _Beltic {
|
|
|
673
849
|
});
|
|
674
850
|
}
|
|
675
851
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
852
|
+
* A conversation: new without an id, resumed with one — the id `session.id()`
|
|
853
|
+
* answered in this or any earlier process (GAP-84). One handle per
|
|
854
|
+
* conversation in-process until it closes; the options count when a
|
|
855
|
+
* session is opened. See `Session`.
|
|
679
856
|
*/
|
|
680
|
-
session(
|
|
681
|
-
const existing = this.sessions.get(
|
|
857
|
+
session(id, opts = {}) {
|
|
858
|
+
const existing = id ? this.sessions.get(id) : void 0;
|
|
682
859
|
if (existing) return existing;
|
|
683
860
|
const session = new Session(
|
|
684
861
|
{
|
|
685
862
|
streams: this.streams,
|
|
686
|
-
evaluate: (sessionId, payment) => this.evaluate(sessionId, payment),
|
|
863
|
+
evaluate: (sessionId, payment, callId) => this.evaluate(sessionId, payment, { callId }),
|
|
864
|
+
onOpened: (opened, conversationId) => {
|
|
865
|
+
if (!this.sessions.has(conversationId)) this.sessions.set(conversationId, opened);
|
|
866
|
+
},
|
|
687
867
|
onClosed: (closed) => {
|
|
688
|
-
|
|
868
|
+
for (const [key, s] of this.sessions) if (s === closed) this.sessions.delete(key);
|
|
689
869
|
}
|
|
690
870
|
},
|
|
691
|
-
|
|
692
|
-
|
|
871
|
+
opts,
|
|
872
|
+
id ?? null
|
|
693
873
|
);
|
|
694
|
-
this.sessions.set(
|
|
874
|
+
if (id) this.sessions.set(id, session);
|
|
695
875
|
return session;
|
|
696
876
|
}
|
|
697
877
|
/**
|
|
698
878
|
* The platform's verdict on a payment — the seller's before it verifies,
|
|
699
879
|
* 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
|
|
880
|
+
* Read-your-writes: the session's buffered evidence is flushed first so
|
|
881
|
+
* the platform judges what the caller already saw (GAP-16/88). With a `callId`
|
|
882
|
+
* the platform answers the decision already taken for that call anywhere
|
|
883
|
+
* in the conversation (GAP-84). Absent when the platform could not be
|
|
884
|
+
* reached (GAP-70).
|
|
703
885
|
*/
|
|
704
|
-
async evaluate(sessionId, payment) {
|
|
705
|
-
const input = {
|
|
886
|
+
async evaluate(sessionId, payment, opts = {}) {
|
|
887
|
+
const input = {
|
|
888
|
+
sessionId,
|
|
889
|
+
payment,
|
|
890
|
+
...opts.callId ? { callId: opts.callId } : {}
|
|
891
|
+
};
|
|
706
892
|
try {
|
|
707
|
-
await this.transport.flush();
|
|
893
|
+
await this.transport.flush({ sessionId });
|
|
708
894
|
return Decision.of(await this.api.post("/v1/evaluate", input));
|
|
709
895
|
} catch (err) {
|
|
710
896
|
if (!Transport.outage(err)) throw err;
|
|
@@ -712,7 +898,7 @@ var Beltic = class _Beltic {
|
|
|
712
898
|
return Decision.absent();
|
|
713
899
|
}
|
|
714
900
|
}
|
|
715
|
-
/** Send everything buffered now and wait for that attempt. */
|
|
901
|
+
/** Send everything buffered now — every session's — and wait for that attempt. */
|
|
716
902
|
flush() {
|
|
717
903
|
return this.transport.flush();
|
|
718
904
|
}
|