runcycles 0.1.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/index.js ADDED
@@ -0,0 +1,1374 @@
1
+ // src/constants.ts
2
+ var API_KEY_HEADER = "X-Cycles-API-Key";
3
+ var IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key";
4
+ var RESERVATIONS_PATH = "/v1/reservations";
5
+ var DECIDE_PATH = "/v1/decide";
6
+ var BALANCES_PATH = "/v1/balances";
7
+ var EVENTS_PATH = "/v1/events";
8
+
9
+ // src/mappers.ts
10
+ function stripUndefined(obj) {
11
+ const result = {};
12
+ for (const [key, value] of Object.entries(obj)) {
13
+ if (value !== void 0) {
14
+ result[key] = value;
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function metricsToWire(metrics) {
20
+ return stripUndefined({
21
+ tokens_input: metrics.tokensInput,
22
+ tokens_output: metrics.tokensOutput,
23
+ latency_ms: metrics.latencyMs,
24
+ model_version: metrics.modelVersion,
25
+ custom: metrics.custom
26
+ });
27
+ }
28
+ function capsFromWire(wire) {
29
+ if (!wire) return void 0;
30
+ return stripUndefined({
31
+ maxTokens: wire.max_tokens,
32
+ maxStepsRemaining: wire.max_steps_remaining,
33
+ toolAllowlist: wire.tool_allowlist,
34
+ toolDenylist: wire.tool_denylist,
35
+ cooldownMs: wire.cooldown_ms
36
+ });
37
+ }
38
+ function amountFromWire(wire) {
39
+ if (!wire) return void 0;
40
+ return { unit: wire.unit, amount: wire.amount };
41
+ }
42
+ function signedAmountFromWire(wire) {
43
+ if (!wire) return void 0;
44
+ return { unit: wire.unit, amount: wire.amount };
45
+ }
46
+ function balanceFromWire(wire) {
47
+ return {
48
+ scope: wire.scope,
49
+ scopePath: wire.scope_path,
50
+ remaining: signedAmountFromWire(wire.remaining),
51
+ reserved: amountFromWire(wire.reserved),
52
+ spent: amountFromWire(wire.spent),
53
+ allocated: amountFromWire(wire.allocated),
54
+ debt: amountFromWire(wire.debt),
55
+ overdraftLimit: amountFromWire(wire.overdraft_limit),
56
+ isOverLimit: wire.is_over_limit
57
+ };
58
+ }
59
+ function balancesFromWire(wire) {
60
+ if (!wire) return void 0;
61
+ return wire.map((b) => balanceFromWire(b));
62
+ }
63
+ function reservationCreateResponseFromWire(wire) {
64
+ return {
65
+ decision: wire.decision,
66
+ reservationId: wire.reservation_id,
67
+ affectedScopes: wire.affected_scopes ?? [],
68
+ expiresAtMs: wire.expires_at_ms,
69
+ scopePath: wire.scope_path,
70
+ reserved: amountFromWire(wire.reserved),
71
+ caps: capsFromWire(wire.caps),
72
+ reasonCode: wire.reason_code,
73
+ retryAfterMs: wire.retry_after_ms,
74
+ balances: balancesFromWire(wire.balances)
75
+ };
76
+ }
77
+ function commitResponseFromWire(wire) {
78
+ return {
79
+ status: wire.status,
80
+ charged: amountFromWire(wire.charged),
81
+ released: amountFromWire(wire.released),
82
+ balances: balancesFromWire(wire.balances)
83
+ };
84
+ }
85
+ function releaseResponseFromWire(wire) {
86
+ return {
87
+ status: wire.status,
88
+ released: amountFromWire(wire.released),
89
+ balances: balancesFromWire(wire.balances)
90
+ };
91
+ }
92
+ function reservationExtendResponseFromWire(wire) {
93
+ return {
94
+ status: wire.status,
95
+ expiresAtMs: wire.expires_at_ms,
96
+ balances: balancesFromWire(wire.balances)
97
+ };
98
+ }
99
+ function decisionResponseFromWire(wire) {
100
+ return stripUndefined({
101
+ decision: wire.decision,
102
+ caps: capsFromWire(wire.caps),
103
+ reasonCode: wire.reason_code,
104
+ retryAfterMs: wire.retry_after_ms,
105
+ affectedScopes: wire.affected_scopes
106
+ });
107
+ }
108
+ function eventCreateResponseFromWire(wire) {
109
+ return {
110
+ status: wire.status,
111
+ eventId: wire.event_id,
112
+ balances: balancesFromWire(wire.balances)
113
+ };
114
+ }
115
+ function subjectFromWire(wire) {
116
+ const result = {};
117
+ if (wire.tenant !== void 0) result.tenant = wire.tenant;
118
+ if (wire.workspace !== void 0) result.workspace = wire.workspace;
119
+ if (wire.app !== void 0) result.app = wire.app;
120
+ if (wire.workflow !== void 0) result.workflow = wire.workflow;
121
+ if (wire.agent !== void 0) result.agent = wire.agent;
122
+ if (wire.toolset !== void 0) result.toolset = wire.toolset;
123
+ if (wire.dimensions !== void 0) result.dimensions = wire.dimensions;
124
+ return result;
125
+ }
126
+ function actionFromWire(wire) {
127
+ const result = {
128
+ kind: wire.kind,
129
+ name: wire.name
130
+ };
131
+ if (wire.tags !== void 0) result.tags = wire.tags;
132
+ return result;
133
+ }
134
+ function reservationDetailFromWire(wire) {
135
+ return {
136
+ reservationId: wire.reservation_id,
137
+ status: wire.status,
138
+ subject: subjectFromWire(wire.subject),
139
+ action: actionFromWire(wire.action),
140
+ reserved: amountFromWire(wire.reserved),
141
+ createdAtMs: wire.created_at_ms,
142
+ expiresAtMs: wire.expires_at_ms,
143
+ scopePath: wire.scope_path,
144
+ affectedScopes: wire.affected_scopes,
145
+ idempotencyKey: wire.idempotency_key,
146
+ committed: amountFromWire(wire.committed),
147
+ finalizedAtMs: wire.finalized_at_ms,
148
+ metadata: wire.metadata
149
+ };
150
+ }
151
+ function reservationSummaryFromWire(wire) {
152
+ return {
153
+ reservationId: wire.reservation_id,
154
+ status: wire.status,
155
+ subject: subjectFromWire(wire.subject),
156
+ action: actionFromWire(wire.action),
157
+ reserved: amountFromWire(wire.reserved),
158
+ createdAtMs: wire.created_at_ms,
159
+ expiresAtMs: wire.expires_at_ms,
160
+ scopePath: wire.scope_path,
161
+ affectedScopes: wire.affected_scopes,
162
+ idempotencyKey: wire.idempotency_key
163
+ };
164
+ }
165
+ function reservationListResponseFromWire(wire) {
166
+ const reservations = wire.reservations.map(
167
+ (r) => reservationSummaryFromWire(r)
168
+ );
169
+ return {
170
+ reservations,
171
+ nextCursor: wire.next_cursor,
172
+ hasMore: wire.has_more
173
+ };
174
+ }
175
+ function balanceResponseFromWire(wire) {
176
+ return {
177
+ balances: balancesFromWire(wire.balances) ?? [],
178
+ nextCursor: wire.next_cursor,
179
+ hasMore: wire.has_more
180
+ };
181
+ }
182
+ function errorResponseFromWire(wire) {
183
+ if (typeof wire.error !== "string" || typeof wire.message !== "string" || typeof wire.request_id !== "string") {
184
+ return void 0;
185
+ }
186
+ return {
187
+ error: wire.error,
188
+ message: wire.message,
189
+ requestId: wire.request_id,
190
+ details: wire.details
191
+ };
192
+ }
193
+ function actionToWire(action) {
194
+ return stripUndefined({
195
+ kind: action.kind,
196
+ name: action.name,
197
+ tags: action.tags
198
+ });
199
+ }
200
+ function subjectToWire(subject) {
201
+ return stripUndefined({
202
+ tenant: subject.tenant,
203
+ workspace: subject.workspace,
204
+ app: subject.app,
205
+ workflow: subject.workflow,
206
+ agent: subject.agent,
207
+ toolset: subject.toolset,
208
+ dimensions: subject.dimensions
209
+ });
210
+ }
211
+ function reservationCreateRequestToWire(req) {
212
+ return stripUndefined({
213
+ idempotency_key: req.idempotencyKey,
214
+ subject: subjectToWire(req.subject),
215
+ action: actionToWire(req.action),
216
+ estimate: req.estimate,
217
+ ttl_ms: req.ttlMs,
218
+ grace_period_ms: req.gracePeriodMs,
219
+ overage_policy: req.overagePolicy,
220
+ dry_run: req.dryRun,
221
+ metadata: req.metadata
222
+ });
223
+ }
224
+ function commitRequestToWire(req) {
225
+ return stripUndefined({
226
+ idempotency_key: req.idempotencyKey,
227
+ actual: req.actual,
228
+ metrics: req.metrics ? metricsToWire(req.metrics) : void 0,
229
+ metadata: req.metadata
230
+ });
231
+ }
232
+ function releaseRequestToWire(req) {
233
+ return stripUndefined({
234
+ idempotency_key: req.idempotencyKey,
235
+ reason: req.reason
236
+ });
237
+ }
238
+ function reservationExtendRequestToWire(req) {
239
+ return stripUndefined({
240
+ idempotency_key: req.idempotencyKey,
241
+ extend_by_ms: req.extendByMs,
242
+ metadata: req.metadata
243
+ });
244
+ }
245
+ function decisionRequestToWire(req) {
246
+ return stripUndefined({
247
+ idempotency_key: req.idempotencyKey,
248
+ subject: subjectToWire(req.subject),
249
+ action: actionToWire(req.action),
250
+ estimate: req.estimate,
251
+ metadata: req.metadata
252
+ });
253
+ }
254
+ function eventCreateRequestToWire(req) {
255
+ return stripUndefined({
256
+ idempotency_key: req.idempotencyKey,
257
+ subject: subjectToWire(req.subject),
258
+ action: actionToWire(req.action),
259
+ actual: req.actual,
260
+ overage_policy: req.overagePolicy,
261
+ metrics: req.metrics ? metricsToWire(req.metrics) : void 0,
262
+ client_time_ms: req.clientTimeMs,
263
+ metadata: req.metadata
264
+ });
265
+ }
266
+
267
+ // src/response.ts
268
+ var CyclesResponse = class _CyclesResponse {
269
+ status;
270
+ body;
271
+ errorMessage;
272
+ headers;
273
+ _isTransportError;
274
+ transportError;
275
+ constructor(options) {
276
+ this.status = options.status;
277
+ this.body = options.body;
278
+ this.errorMessage = options.errorMessage;
279
+ this.headers = options.headers ?? {};
280
+ this._isTransportError = options.isTransportError ?? false;
281
+ this.transportError = options.transportError;
282
+ }
283
+ static success(status, body, headers) {
284
+ return new _CyclesResponse({ status, body, headers });
285
+ }
286
+ static httpError(status, errorMessage, body, headers) {
287
+ return new _CyclesResponse({ status, body, errorMessage, headers });
288
+ }
289
+ static transportError(err) {
290
+ return new _CyclesResponse({
291
+ status: -1,
292
+ errorMessage: err.message,
293
+ isTransportError: true,
294
+ transportError: err
295
+ });
296
+ }
297
+ get requestId() {
298
+ return this.headers["x-request-id"];
299
+ }
300
+ get rateLimitRemaining() {
301
+ const val = this.headers["x-ratelimit-remaining"];
302
+ return val !== void 0 ? parseInt(val, 10) : void 0;
303
+ }
304
+ get rateLimitReset() {
305
+ const val = this.headers["x-ratelimit-reset"];
306
+ return val !== void 0 ? parseInt(val, 10) : void 0;
307
+ }
308
+ get cyclesTenant() {
309
+ return this.headers["x-cycles-tenant"];
310
+ }
311
+ get isSuccess() {
312
+ return this.status >= 200 && this.status < 300;
313
+ }
314
+ get isClientError() {
315
+ return this.status >= 400 && this.status < 500;
316
+ }
317
+ get isServerError() {
318
+ return this.status >= 500 && this.status < 600;
319
+ }
320
+ get isTransportError() {
321
+ return this._isTransportError;
322
+ }
323
+ getBodyAttribute(key) {
324
+ if (this.body && key in this.body) {
325
+ return this.body[key];
326
+ }
327
+ return void 0;
328
+ }
329
+ getErrorResponse() {
330
+ if (this.body && typeof this.body === "object") {
331
+ return errorResponseFromWire(this.body);
332
+ }
333
+ return void 0;
334
+ }
335
+ };
336
+
337
+ // src/client.ts
338
+ var RESPONSE_HEADERS = [
339
+ "x-request-id",
340
+ "x-ratelimit-remaining",
341
+ "x-ratelimit-reset",
342
+ "x-cycles-tenant"
343
+ ];
344
+ var BALANCE_FILTER_PARAMS = /* @__PURE__ */ new Set([
345
+ "tenant",
346
+ "workspace",
347
+ "app",
348
+ "workflow",
349
+ "agent",
350
+ "toolset"
351
+ ]);
352
+ function extractResponseHeaders(resp) {
353
+ const result = {};
354
+ for (const name of RESPONSE_HEADERS) {
355
+ const val = resp.headers.get(name);
356
+ if (val !== null) {
357
+ result[name] = val;
358
+ }
359
+ }
360
+ return result;
361
+ }
362
+ var CyclesClient = class {
363
+ _config;
364
+ constructor(config) {
365
+ this._config = config;
366
+ }
367
+ get config() {
368
+ return this._config;
369
+ }
370
+ async createReservation(request) {
371
+ return this._post(RESERVATIONS_PATH, request);
372
+ }
373
+ async commitReservation(reservationId, request) {
374
+ return this._post(
375
+ `${RESERVATIONS_PATH}/${reservationId}/commit`,
376
+ request
377
+ );
378
+ }
379
+ async releaseReservation(reservationId, request) {
380
+ return this._post(
381
+ `${RESERVATIONS_PATH}/${reservationId}/release`,
382
+ request
383
+ );
384
+ }
385
+ async extendReservation(reservationId, request) {
386
+ return this._post(
387
+ `${RESERVATIONS_PATH}/${reservationId}/extend`,
388
+ request
389
+ );
390
+ }
391
+ async decide(request) {
392
+ return this._post(DECIDE_PATH, request);
393
+ }
394
+ async listReservations(params) {
395
+ return this._get(RESERVATIONS_PATH, params);
396
+ }
397
+ async getReservation(reservationId) {
398
+ return this._get(`${RESERVATIONS_PATH}/${reservationId}`);
399
+ }
400
+ async getBalances(params) {
401
+ const hasFilter = Object.keys(params).some(
402
+ (k) => BALANCE_FILTER_PARAMS.has(k)
403
+ );
404
+ if (!hasFilter) {
405
+ throw new Error(
406
+ "getBalances requires at least one subject filter (tenant, workspace, app, workflow, agent, or toolset)"
407
+ );
408
+ }
409
+ return this._get(BALANCES_PATH, params);
410
+ }
411
+ async createEvent(request) {
412
+ return this._post(EVENTS_PATH, request);
413
+ }
414
+ async _post(path, body) {
415
+ try {
416
+ const headers = {
417
+ "Content-Type": "application/json",
418
+ [API_KEY_HEADER]: this._config.apiKey
419
+ };
420
+ const idemKey = body.idempotency_key;
421
+ if (typeof idemKey === "string") {
422
+ headers[IDEMPOTENCY_KEY_HEADER] = idemKey;
423
+ }
424
+ const url = `${this._config.baseUrl}${path}`;
425
+ const resp = await fetch(url, {
426
+ method: "POST",
427
+ headers,
428
+ body: JSON.stringify(body),
429
+ signal: AbortSignal.timeout(
430
+ this._config.connectTimeout + this._config.readTimeout
431
+ )
432
+ });
433
+ return this._handleResponse(resp);
434
+ } catch (err) {
435
+ return CyclesResponse.transportError(
436
+ err instanceof Error ? err : new Error(String(err))
437
+ );
438
+ }
439
+ }
440
+ async _get(path, params) {
441
+ try {
442
+ let url = `${this._config.baseUrl}${path}`;
443
+ if (params && Object.keys(params).length > 0) {
444
+ const qs = new URLSearchParams(params).toString();
445
+ url = `${url}?${qs}`;
446
+ }
447
+ const resp = await fetch(url, {
448
+ method: "GET",
449
+ headers: {
450
+ [API_KEY_HEADER]: this._config.apiKey
451
+ },
452
+ signal: AbortSignal.timeout(
453
+ this._config.connectTimeout + this._config.readTimeout
454
+ )
455
+ });
456
+ return this._handleResponse(resp);
457
+ } catch (err) {
458
+ return CyclesResponse.transportError(
459
+ err instanceof Error ? err : new Error(String(err))
460
+ );
461
+ }
462
+ }
463
+ async _handleResponse(resp) {
464
+ let body;
465
+ try {
466
+ body = await resp.json();
467
+ } catch {
468
+ body = void 0;
469
+ }
470
+ const headers = extractResponseHeaders(resp);
471
+ if (resp.status >= 200 && resp.status < 300) {
472
+ return CyclesResponse.success(resp.status, body ?? {}, headers);
473
+ }
474
+ let errorMsg;
475
+ if (body && typeof body === "object") {
476
+ errorMsg = body.message ?? body.error ?? void 0;
477
+ }
478
+ return CyclesResponse.httpError(
479
+ resp.status,
480
+ errorMsg ?? resp.statusText ?? "Unknown error",
481
+ body,
482
+ headers
483
+ );
484
+ }
485
+ };
486
+
487
+ // src/config.ts
488
+ var CyclesConfig = class _CyclesConfig {
489
+ baseUrl;
490
+ apiKey;
491
+ tenant;
492
+ workspace;
493
+ app;
494
+ workflow;
495
+ agent;
496
+ toolset;
497
+ connectTimeout;
498
+ readTimeout;
499
+ retryEnabled;
500
+ retryMaxAttempts;
501
+ retryInitialDelay;
502
+ retryMultiplier;
503
+ retryMaxDelay;
504
+ constructor(options) {
505
+ this.baseUrl = options.baseUrl;
506
+ this.apiKey = options.apiKey;
507
+ this.tenant = options.tenant;
508
+ this.workspace = options.workspace;
509
+ this.app = options.app;
510
+ this.workflow = options.workflow;
511
+ this.agent = options.agent;
512
+ this.toolset = options.toolset;
513
+ this.connectTimeout = options.connectTimeout ?? 2e3;
514
+ this.readTimeout = options.readTimeout ?? 5e3;
515
+ this.retryEnabled = options.retryEnabled ?? true;
516
+ this.retryMaxAttempts = options.retryMaxAttempts ?? 5;
517
+ this.retryInitialDelay = options.retryInitialDelay ?? 500;
518
+ this.retryMultiplier = options.retryMultiplier ?? 2;
519
+ this.retryMaxDelay = options.retryMaxDelay ?? 3e4;
520
+ }
521
+ static fromEnv(prefix = "CYCLES_") {
522
+ const baseUrl = process.env[`${prefix}BASE_URL`];
523
+ const apiKey = process.env[`${prefix}API_KEY`];
524
+ if (!baseUrl) {
525
+ throw new Error(`${prefix}BASE_URL environment variable is required`);
526
+ }
527
+ if (!apiKey) {
528
+ throw new Error(`${prefix}API_KEY environment variable is required`);
529
+ }
530
+ return new _CyclesConfig({
531
+ baseUrl,
532
+ apiKey,
533
+ tenant: process.env[`${prefix}TENANT`],
534
+ workspace: process.env[`${prefix}WORKSPACE`],
535
+ app: process.env[`${prefix}APP`],
536
+ workflow: process.env[`${prefix}WORKFLOW`],
537
+ agent: process.env[`${prefix}AGENT`],
538
+ toolset: process.env[`${prefix}TOOLSET`],
539
+ connectTimeout: optionalFloat(process.env[`${prefix}CONNECT_TIMEOUT`], 2e3),
540
+ readTimeout: optionalFloat(process.env[`${prefix}READ_TIMEOUT`], 5e3),
541
+ retryEnabled: process.env[`${prefix}RETRY_ENABLED`]?.toLowerCase() !== "false",
542
+ retryMaxAttempts: optionalInt(process.env[`${prefix}RETRY_MAX_ATTEMPTS`], 5),
543
+ retryInitialDelay: optionalFloat(process.env[`${prefix}RETRY_INITIAL_DELAY`], 500),
544
+ retryMultiplier: optionalFloat(process.env[`${prefix}RETRY_MULTIPLIER`], 2),
545
+ retryMaxDelay: optionalFloat(process.env[`${prefix}RETRY_MAX_DELAY`], 3e4)
546
+ });
547
+ }
548
+ };
549
+ function optionalInt(val, fallback) {
550
+ return val !== void 0 ? parseInt(val, 10) : fallback;
551
+ }
552
+ function optionalFloat(val, fallback) {
553
+ return val !== void 0 ? parseFloat(val) : fallback;
554
+ }
555
+
556
+ // src/lifecycle.ts
557
+ import { randomUUID } from "crypto";
558
+
559
+ // src/context.ts
560
+ import { AsyncLocalStorage } from "async_hooks";
561
+ var storage = new AsyncLocalStorage();
562
+ function getCyclesContext() {
563
+ return storage.getStore();
564
+ }
565
+ function runWithContext(ctx, fn) {
566
+ return storage.run(ctx, fn);
567
+ }
568
+
569
+ // src/exceptions.ts
570
+ var CyclesError = class extends Error {
571
+ constructor(message) {
572
+ super(message);
573
+ this.name = "CyclesError";
574
+ }
575
+ };
576
+ var CyclesProtocolError = class extends CyclesError {
577
+ status;
578
+ errorCode;
579
+ reasonCode;
580
+ retryAfterMs;
581
+ requestId;
582
+ details;
583
+ constructor(message, options = {}) {
584
+ super(message);
585
+ this.name = "CyclesProtocolError";
586
+ this.status = options.status ?? 0;
587
+ this.errorCode = options.errorCode;
588
+ this.reasonCode = options.reasonCode;
589
+ this.retryAfterMs = options.retryAfterMs;
590
+ this.requestId = options.requestId;
591
+ this.details = options.details;
592
+ }
593
+ isBudgetExceeded() {
594
+ return this.errorCode === "BUDGET_EXCEEDED";
595
+ }
596
+ isOverdraftLimitExceeded() {
597
+ return this.errorCode === "OVERDRAFT_LIMIT_EXCEEDED";
598
+ }
599
+ isDebtOutstanding() {
600
+ return this.errorCode === "DEBT_OUTSTANDING";
601
+ }
602
+ isReservationExpired() {
603
+ return this.errorCode === "RESERVATION_EXPIRED";
604
+ }
605
+ isReservationFinalized() {
606
+ return this.errorCode === "RESERVATION_FINALIZED";
607
+ }
608
+ isIdempotencyMismatch() {
609
+ return this.errorCode === "IDEMPOTENCY_MISMATCH";
610
+ }
611
+ isUnitMismatch() {
612
+ return this.errorCode === "UNIT_MISMATCH";
613
+ }
614
+ isRetryable() {
615
+ return this.errorCode === "INTERNAL_ERROR" || this.errorCode === "UNKNOWN" || this.status >= 500;
616
+ }
617
+ };
618
+ var BudgetExceededError = class extends CyclesProtocolError {
619
+ constructor(message, options = {}) {
620
+ super(message, options);
621
+ this.name = "BudgetExceededError";
622
+ }
623
+ };
624
+ var OverdraftLimitExceededError = class extends CyclesProtocolError {
625
+ constructor(message, options = {}) {
626
+ super(message, options);
627
+ this.name = "OverdraftLimitExceededError";
628
+ }
629
+ };
630
+ var DebtOutstandingError = class extends CyclesProtocolError {
631
+ constructor(message, options = {}) {
632
+ super(message, options);
633
+ this.name = "DebtOutstandingError";
634
+ }
635
+ };
636
+ var ReservationExpiredError = class extends CyclesProtocolError {
637
+ constructor(message, options = {}) {
638
+ super(message, options);
639
+ this.name = "ReservationExpiredError";
640
+ }
641
+ };
642
+ var ReservationFinalizedError = class extends CyclesProtocolError {
643
+ constructor(message, options = {}) {
644
+ super(message, options);
645
+ this.name = "ReservationFinalizedError";
646
+ }
647
+ };
648
+ var CyclesTransportError = class extends CyclesError {
649
+ cause;
650
+ constructor(message, options) {
651
+ super(message);
652
+ this.name = "CyclesTransportError";
653
+ this.cause = options?.cause;
654
+ }
655
+ };
656
+
657
+ // src/errors.ts
658
+ function buildProtocolException(prefix, response) {
659
+ const errorResp = response.getErrorResponse();
660
+ let errorCode;
661
+ let reasonCode;
662
+ let message = prefix;
663
+ let requestId;
664
+ let details;
665
+ if (errorResp) {
666
+ errorCode = errorResp.error;
667
+ requestId = errorResp.requestId;
668
+ details = errorResp.details;
669
+ if (errorResp.message) {
670
+ message = `${prefix}: ${errorResp.message}`;
671
+ }
672
+ } else {
673
+ const rawError = response.getBodyAttribute("error");
674
+ if (typeof rawError === "string") {
675
+ errorCode = rawError;
676
+ }
677
+ if (response.errorMessage) {
678
+ message = `${prefix}: ${response.errorMessage}`;
679
+ }
680
+ }
681
+ reasonCode = response.getBodyAttribute("reason_code");
682
+ if (reasonCode === void 0 && errorCode !== void 0) {
683
+ reasonCode = errorCode;
684
+ }
685
+ const retryRaw = response.getBodyAttribute("retry_after_ms");
686
+ const retryAfterMs = retryRaw !== void 0 ? Number(retryRaw) : void 0;
687
+ const opts = {
688
+ status: response.status,
689
+ errorCode,
690
+ reasonCode,
691
+ retryAfterMs,
692
+ requestId,
693
+ details
694
+ };
695
+ switch (errorCode) {
696
+ case "BUDGET_EXCEEDED":
697
+ return new BudgetExceededError(message, opts);
698
+ case "OVERDRAFT_LIMIT_EXCEEDED":
699
+ return new OverdraftLimitExceededError(message, opts);
700
+ case "DEBT_OUTSTANDING":
701
+ return new DebtOutstandingError(message, opts);
702
+ case "RESERVATION_EXPIRED":
703
+ return new ReservationExpiredError(message, opts);
704
+ case "RESERVATION_FINALIZED":
705
+ return new ReservationFinalizedError(message, opts);
706
+ default:
707
+ return new CyclesProtocolError(message, opts);
708
+ }
709
+ }
710
+
711
+ // src/validation.ts
712
+ function validateSubject(subject) {
713
+ if (subject === void 0) return;
714
+ const hasField = !!(subject.tenant || subject.workspace || subject.app || subject.workflow || subject.agent || subject.toolset);
715
+ if (!hasField) {
716
+ throw new Error(
717
+ "Subject must have at least one standard field (tenant, workspace, app, workflow, agent, or toolset)"
718
+ );
719
+ }
720
+ }
721
+ function validateNonNegative(value, name) {
722
+ if (value < 0) {
723
+ throw new Error(`${name} must be non-negative, got ${value}`);
724
+ }
725
+ }
726
+ function validateTtlMs(ttlMs) {
727
+ if (ttlMs < 1e3 || ttlMs > 864e5) {
728
+ throw new Error(`ttl_ms must be between 1000 and 86400000, got ${ttlMs}`);
729
+ }
730
+ }
731
+ function validateGracePeriodMs(gracePeriodMs) {
732
+ if (gracePeriodMs !== void 0 && (gracePeriodMs < 0 || gracePeriodMs > 6e4)) {
733
+ throw new Error(`grace_period_ms must be between 0 and 60000, got ${gracePeriodMs}`);
734
+ }
735
+ }
736
+ function validateExtendByMs(extendByMs) {
737
+ if (extendByMs < 1 || extendByMs > 864e5) {
738
+ throw new Error(`extend_by_ms must be between 1 and 86400000, got ${extendByMs}`);
739
+ }
740
+ }
741
+
742
+ // src/lifecycle.ts
743
+ function evaluateAmount(expr, args) {
744
+ if (typeof expr === "function") {
745
+ return expr(...args);
746
+ }
747
+ return expr;
748
+ }
749
+ function evaluateActual(expr, result, estimate, useEstimateFallback) {
750
+ if (expr !== void 0) {
751
+ if (typeof expr === "function") {
752
+ return expr(result);
753
+ }
754
+ return expr;
755
+ }
756
+ if (useEstimateFallback) {
757
+ return estimate;
758
+ }
759
+ throw new Error(
760
+ "actual expression is required when useEstimateIfActualNotProvided is false"
761
+ );
762
+ }
763
+ function buildReservationBody(cfg, estimate, defaultSubject) {
764
+ validateNonNegative(estimate, "estimate");
765
+ const ttlMs = cfg.ttlMs ?? 6e4;
766
+ validateTtlMs(ttlMs);
767
+ const subject = {};
768
+ for (const field of [
769
+ "tenant",
770
+ "workspace",
771
+ "app",
772
+ "workflow",
773
+ "agent",
774
+ "toolset"
775
+ ]) {
776
+ const val = cfg[field] ?? defaultSubject[field];
777
+ if (val) {
778
+ subject[field] = val;
779
+ }
780
+ }
781
+ if (cfg.dimensions) {
782
+ subject.dimensions = cfg.dimensions;
783
+ }
784
+ validateSubject(subject);
785
+ const action = {
786
+ kind: cfg.actionKind ?? "unknown",
787
+ name: cfg.actionName ?? "unknown"
788
+ };
789
+ if (cfg.actionTags) {
790
+ action.tags = cfg.actionTags;
791
+ }
792
+ const unit = cfg.unit ?? "USD_MICROCENTS";
793
+ const body = {
794
+ idempotency_key: randomUUID(),
795
+ subject,
796
+ action,
797
+ estimate: { unit, amount: estimate },
798
+ ttl_ms: ttlMs,
799
+ overage_policy: cfg.overagePolicy ?? "REJECT"
800
+ };
801
+ validateGracePeriodMs(cfg.gracePeriodMs);
802
+ if (cfg.gracePeriodMs !== void 0) {
803
+ body.grace_period_ms = cfg.gracePeriodMs;
804
+ }
805
+ if (cfg.dryRun) {
806
+ body.dry_run = true;
807
+ }
808
+ return body;
809
+ }
810
+ function buildCommitBody(actual, unit, metrics, metadata) {
811
+ const body = {
812
+ idempotency_key: randomUUID(),
813
+ actual: { unit, amount: actual }
814
+ };
815
+ if (metrics && !isMetricsEmpty(metrics)) {
816
+ body.metrics = metricsToWire(metrics);
817
+ }
818
+ if (metadata) {
819
+ body.metadata = metadata;
820
+ }
821
+ return body;
822
+ }
823
+ function isMetricsEmpty(metrics) {
824
+ return metrics.tokensInput === void 0 && metrics.tokensOutput === void 0 && metrics.latencyMs === void 0 && metrics.modelVersion === void 0 && !metrics.custom;
825
+ }
826
+ function buildReleaseBody(reason) {
827
+ return { idempotency_key: randomUUID(), reason };
828
+ }
829
+ function buildExtendBody(extendByMs) {
830
+ validateExtendByMs(extendByMs);
831
+ return { idempotency_key: randomUUID(), extend_by_ms: extendByMs };
832
+ }
833
+ var AsyncCyclesLifecycle = class {
834
+ _client;
835
+ _retryEngine;
836
+ _defaultSubject;
837
+ constructor(client, retryEngine, defaultSubject) {
838
+ this._client = client;
839
+ this._retryEngine = retryEngine;
840
+ this._retryEngine.setClient(client);
841
+ this._defaultSubject = defaultSubject;
842
+ }
843
+ async execute(fn, args, cfg) {
844
+ const estimate = evaluateAmount(cfg.estimate, args);
845
+ const createBody = buildReservationBody(cfg, estimate, this._defaultSubject);
846
+ const resT1 = performance.now();
847
+ const resResponse = await this._client.createReservation(createBody);
848
+ if (!resResponse.isSuccess) {
849
+ throw buildProtocolException("Failed to create reservation", resResponse);
850
+ }
851
+ const resResult = reservationCreateResponseFromWire(
852
+ resResponse.body
853
+ );
854
+ const resT2 = performance.now();
855
+ const decision = resResult.decision;
856
+ const reservationId = resResult.reservationId;
857
+ const reasonCode = resResult.reasonCode;
858
+ if (cfg.dryRun) {
859
+ if (decision === "DENY") {
860
+ throw buildProtocolException("Dry-run denied", resResponse);
861
+ }
862
+ return {
863
+ decision,
864
+ caps: resResult.caps,
865
+ affectedScopes: resResult.affectedScopes,
866
+ scopePath: resResult.scopePath,
867
+ reserved: resResult.reserved,
868
+ balances: resResult.balances,
869
+ reasonCode,
870
+ retryAfterMs: resResult.retryAfterMs
871
+ };
872
+ }
873
+ if (decision === "DENY") {
874
+ throw buildProtocolException("Reservation denied", resResponse);
875
+ }
876
+ if (!reservationId) {
877
+ throw new CyclesProtocolError(
878
+ "Reservation successful but reservation_id missing",
879
+ { status: resResponse.status }
880
+ );
881
+ }
882
+ const unit = cfg.unit ?? "USD_MICROCENTS";
883
+ const ttlMs = cfg.ttlMs ?? 6e4;
884
+ const ctx = {
885
+ reservationId,
886
+ estimate,
887
+ decision,
888
+ caps: resResult.caps,
889
+ expiresAtMs: resResult.expiresAtMs,
890
+ affectedScopes: resResult.affectedScopes,
891
+ scopePath: resResult.scopePath,
892
+ reserved: resResult.reserved,
893
+ balances: resResult.balances
894
+ };
895
+ const heartbeatRef = this._startHeartbeat(reservationId, ttlMs, ctx);
896
+ try {
897
+ const result = await runWithContext(ctx, () => fn(...args));
898
+ const methodElapsed = Math.round(performance.now() - resT2);
899
+ const useEstimateFallback = cfg.useEstimateIfActualNotProvided !== false;
900
+ const actualAmount = evaluateActual(
901
+ cfg.actual,
902
+ result,
903
+ estimate,
904
+ useEstimateFallback
905
+ );
906
+ let metrics = ctx.metrics;
907
+ if (!metrics) {
908
+ metrics = {};
909
+ }
910
+ if (metrics.latencyMs === void 0) {
911
+ metrics = { ...metrics, latencyMs: methodElapsed };
912
+ }
913
+ const commitBody = buildCommitBody(
914
+ actualAmount,
915
+ unit,
916
+ metrics,
917
+ ctx.commitMetadata
918
+ );
919
+ await this._handleCommit(reservationId, commitBody);
920
+ return result;
921
+ } catch (err) {
922
+ await this._handleRelease(reservationId, "guarded_method_failed");
923
+ throw err;
924
+ } finally {
925
+ if (heartbeatRef) {
926
+ heartbeatRef.stop();
927
+ }
928
+ }
929
+ }
930
+ async _handleCommit(reservationId, commitBody) {
931
+ try {
932
+ const response = await this._client.commitReservation(
933
+ reservationId,
934
+ commitBody
935
+ );
936
+ if (response.isSuccess) {
937
+ return;
938
+ }
939
+ if (response.isTransportError || response.isServerError) {
940
+ this._retryEngine.schedule(reservationId, commitBody);
941
+ return;
942
+ }
943
+ const errorResp = response.getErrorResponse();
944
+ const errorCode = errorResp?.error;
945
+ if (errorCode === "RESERVATION_FINALIZED" || errorCode === "RESERVATION_EXPIRED") {
946
+ return;
947
+ }
948
+ if (errorCode === "IDEMPOTENCY_MISMATCH") {
949
+ return;
950
+ }
951
+ if (response.isClientError) {
952
+ await this._handleRelease(
953
+ reservationId,
954
+ `commit_rejected_${errorCode}`
955
+ );
956
+ return;
957
+ }
958
+ } catch {
959
+ this._retryEngine.schedule(reservationId, commitBody);
960
+ }
961
+ }
962
+ async _handleRelease(reservationId, reason) {
963
+ try {
964
+ const body = buildReleaseBody(reason);
965
+ await this._client.releaseReservation(reservationId, body);
966
+ } catch {
967
+ }
968
+ }
969
+ _startHeartbeat(reservationId, ttlMs, ctx) {
970
+ if (ttlMs <= 0) return void 0;
971
+ const intervalMs = Math.max(ttlMs / 2, 1e3);
972
+ let stopped = false;
973
+ let currentTimer;
974
+ const tick = () => {
975
+ if (stopped) return;
976
+ currentTimer = setTimeout(() => {
977
+ if (stopped) return;
978
+ const body = buildExtendBody(ttlMs);
979
+ void this._client.extendReservation(reservationId, body).then((response) => {
980
+ if (response.isSuccess) {
981
+ const newExpires = response.getBodyAttribute("expires_at_ms");
982
+ if (typeof newExpires === "number") {
983
+ ctx.expiresAtMs = newExpires;
984
+ }
985
+ }
986
+ }).catch(() => {
987
+ }).finally(() => {
988
+ tick();
989
+ });
990
+ }, intervalMs);
991
+ };
992
+ tick();
993
+ return {
994
+ stop: () => {
995
+ stopped = true;
996
+ clearTimeout(currentTimer);
997
+ }
998
+ };
999
+ }
1000
+ };
1001
+
1002
+ // src/retry.ts
1003
+ function delay(ms) {
1004
+ return new Promise((resolve) => setTimeout(resolve, ms));
1005
+ }
1006
+ var CommitRetryEngine = class {
1007
+ _enabled;
1008
+ _maxAttempts;
1009
+ _initialDelay;
1010
+ _multiplier;
1011
+ _maxDelay;
1012
+ _client;
1013
+ constructor(config) {
1014
+ this._enabled = config.retryEnabled;
1015
+ this._maxAttempts = config.retryMaxAttempts;
1016
+ this._initialDelay = config.retryInitialDelay;
1017
+ this._multiplier = config.retryMultiplier;
1018
+ this._maxDelay = config.retryMaxDelay;
1019
+ }
1020
+ setClient(client) {
1021
+ this._client = client;
1022
+ }
1023
+ schedule(reservationId, commitBody) {
1024
+ if (!this._enabled) {
1025
+ return;
1026
+ }
1027
+ void this._retryLoop(reservationId, commitBody);
1028
+ }
1029
+ async _retryLoop(reservationId, commitBody) {
1030
+ for (let attempt = 0; attempt < this._maxAttempts; attempt++) {
1031
+ const backoff = Math.min(
1032
+ this._initialDelay * this._multiplier ** attempt,
1033
+ this._maxDelay
1034
+ );
1035
+ await delay(backoff);
1036
+ try {
1037
+ if (!this._client) {
1038
+ return;
1039
+ }
1040
+ const response = await this._client.commitReservation(
1041
+ reservationId,
1042
+ commitBody
1043
+ );
1044
+ if (response.isSuccess) {
1045
+ return;
1046
+ }
1047
+ if (response.isClientError) {
1048
+ return;
1049
+ }
1050
+ } catch {
1051
+ }
1052
+ }
1053
+ }
1054
+ };
1055
+
1056
+ // src/withCycles.ts
1057
+ var _defaultClient;
1058
+ var _defaultConfig;
1059
+ function setDefaultClient(client) {
1060
+ _defaultClient = client;
1061
+ }
1062
+ function setDefaultConfig(config) {
1063
+ _defaultConfig = config;
1064
+ }
1065
+ function getEffectiveClient(explicitClient) {
1066
+ if (explicitClient) return explicitClient;
1067
+ if (_defaultClient) return _defaultClient;
1068
+ if (_defaultConfig) {
1069
+ _defaultClient = new CyclesClient(_defaultConfig);
1070
+ return _defaultClient;
1071
+ }
1072
+ throw new Error(
1073
+ "No Cycles client available. Either pass client in options, call setDefaultClient(), or call setDefaultConfig()."
1074
+ );
1075
+ }
1076
+ function withCycles(options, fn) {
1077
+ return async (...args) => {
1078
+ const client = getEffectiveClient(options.client);
1079
+ const config = client.config;
1080
+ const defaultSubject = {
1081
+ tenant: config.tenant,
1082
+ workspace: config.workspace,
1083
+ app: config.app,
1084
+ workflow: config.workflow,
1085
+ agent: config.agent,
1086
+ toolset: config.toolset
1087
+ };
1088
+ const retryEngine = new CommitRetryEngine(config);
1089
+ const lifecycle = new AsyncCyclesLifecycle(
1090
+ client,
1091
+ retryEngine,
1092
+ defaultSubject
1093
+ );
1094
+ return lifecycle.execute(
1095
+ fn,
1096
+ args,
1097
+ options
1098
+ );
1099
+ };
1100
+ }
1101
+
1102
+ // src/streaming.ts
1103
+ import { randomUUID as randomUUID2 } from "crypto";
1104
+
1105
+ // src/models.ts
1106
+ var Unit = /* @__PURE__ */ ((Unit2) => {
1107
+ Unit2["USD_MICROCENTS"] = "USD_MICROCENTS";
1108
+ Unit2["TOKENS"] = "TOKENS";
1109
+ Unit2["CREDITS"] = "CREDITS";
1110
+ Unit2["RISK_POINTS"] = "RISK_POINTS";
1111
+ return Unit2;
1112
+ })(Unit || {});
1113
+ var CommitOveragePolicy = /* @__PURE__ */ ((CommitOveragePolicy2) => {
1114
+ CommitOveragePolicy2["REJECT"] = "REJECT";
1115
+ CommitOveragePolicy2["ALLOW_IF_AVAILABLE"] = "ALLOW_IF_AVAILABLE";
1116
+ CommitOveragePolicy2["ALLOW_WITH_OVERDRAFT"] = "ALLOW_WITH_OVERDRAFT";
1117
+ return CommitOveragePolicy2;
1118
+ })(CommitOveragePolicy || {});
1119
+ var Decision = /* @__PURE__ */ ((Decision2) => {
1120
+ Decision2["ALLOW"] = "ALLOW";
1121
+ Decision2["ALLOW_WITH_CAPS"] = "ALLOW_WITH_CAPS";
1122
+ Decision2["DENY"] = "DENY";
1123
+ return Decision2;
1124
+ })(Decision || {});
1125
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
1126
+ ReservationStatus2["ACTIVE"] = "ACTIVE";
1127
+ ReservationStatus2["COMMITTED"] = "COMMITTED";
1128
+ ReservationStatus2["RELEASED"] = "RELEASED";
1129
+ ReservationStatus2["EXPIRED"] = "EXPIRED";
1130
+ return ReservationStatus2;
1131
+ })(ReservationStatus || {});
1132
+ var CommitStatus = /* @__PURE__ */ ((CommitStatus2) => {
1133
+ CommitStatus2["COMMITTED"] = "COMMITTED";
1134
+ return CommitStatus2;
1135
+ })(CommitStatus || {});
1136
+ var ReleaseStatus = /* @__PURE__ */ ((ReleaseStatus2) => {
1137
+ ReleaseStatus2["RELEASED"] = "RELEASED";
1138
+ return ReleaseStatus2;
1139
+ })(ReleaseStatus || {});
1140
+ var ExtendStatus = /* @__PURE__ */ ((ExtendStatus2) => {
1141
+ ExtendStatus2["ACTIVE"] = "ACTIVE";
1142
+ return ExtendStatus2;
1143
+ })(ExtendStatus || {});
1144
+ var EventStatus = /* @__PURE__ */ ((EventStatus2) => {
1145
+ EventStatus2["APPLIED"] = "APPLIED";
1146
+ return EventStatus2;
1147
+ })(EventStatus || {});
1148
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
1149
+ ErrorCode2["INVALID_REQUEST"] = "INVALID_REQUEST";
1150
+ ErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
1151
+ ErrorCode2["FORBIDDEN"] = "FORBIDDEN";
1152
+ ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
1153
+ ErrorCode2["BUDGET_EXCEEDED"] = "BUDGET_EXCEEDED";
1154
+ ErrorCode2["RESERVATION_EXPIRED"] = "RESERVATION_EXPIRED";
1155
+ ErrorCode2["RESERVATION_FINALIZED"] = "RESERVATION_FINALIZED";
1156
+ ErrorCode2["IDEMPOTENCY_MISMATCH"] = "IDEMPOTENCY_MISMATCH";
1157
+ ErrorCode2["UNIT_MISMATCH"] = "UNIT_MISMATCH";
1158
+ ErrorCode2["OVERDRAFT_LIMIT_EXCEEDED"] = "OVERDRAFT_LIMIT_EXCEEDED";
1159
+ ErrorCode2["DEBT_OUTSTANDING"] = "DEBT_OUTSTANDING";
1160
+ ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
1161
+ ErrorCode2["UNKNOWN"] = "UNKNOWN";
1162
+ return ErrorCode2;
1163
+ })(ErrorCode || {});
1164
+ function isAllowed(decision) {
1165
+ return decision === "ALLOW" /* ALLOW */ || decision === "ALLOW_WITH_CAPS" /* ALLOW_WITH_CAPS */;
1166
+ }
1167
+ function isDenied(decision) {
1168
+ return decision === "DENY" /* DENY */;
1169
+ }
1170
+ function isRetryableErrorCode(code) {
1171
+ return code === "INTERNAL_ERROR" /* INTERNAL_ERROR */ || code === "UNKNOWN" /* UNKNOWN */;
1172
+ }
1173
+ function errorCodeFromString(value) {
1174
+ if (value === void 0) return void 0;
1175
+ if (Object.values(ErrorCode).includes(value)) {
1176
+ return value;
1177
+ }
1178
+ return "UNKNOWN" /* UNKNOWN */;
1179
+ }
1180
+ function isToolAllowed(caps, tool) {
1181
+ if (caps.toolAllowlist && caps.toolAllowlist.length > 0) {
1182
+ return caps.toolAllowlist.includes(tool);
1183
+ }
1184
+ if (caps.toolDenylist && caps.toolDenylist.length > 0) {
1185
+ return !caps.toolDenylist.includes(tool);
1186
+ }
1187
+ return true;
1188
+ }
1189
+ function isMetricsEmpty2(metrics) {
1190
+ return metrics.tokensInput === void 0 && metrics.tokensOutput === void 0 && metrics.latencyMs === void 0 && metrics.modelVersion === void 0 && !metrics.custom;
1191
+ }
1192
+
1193
+ // src/streaming.ts
1194
+ async function reserveForStream(options) {
1195
+ const {
1196
+ client,
1197
+ estimate,
1198
+ unit = "USD_MICROCENTS",
1199
+ actionKind = "unknown",
1200
+ actionName = "unknown",
1201
+ actionTags,
1202
+ ttlMs = 6e4,
1203
+ gracePeriodMs,
1204
+ overagePolicy = "REJECT",
1205
+ dimensions
1206
+ } = options;
1207
+ validateNonNegative(estimate, "estimate");
1208
+ validateTtlMs(ttlMs);
1209
+ validateGracePeriodMs(gracePeriodMs);
1210
+ const configDefaults = client.config;
1211
+ const subject = {};
1212
+ for (const field of ["tenant", "workspace", "app", "workflow", "agent", "toolset"]) {
1213
+ const val = options[field] ?? configDefaults[field];
1214
+ if (val) {
1215
+ subject[field] = val;
1216
+ }
1217
+ }
1218
+ if (dimensions) {
1219
+ subject.dimensions = dimensions;
1220
+ }
1221
+ validateSubject(subject);
1222
+ const action = { kind: actionKind, name: actionName };
1223
+ if (actionTags) {
1224
+ action.tags = actionTags;
1225
+ }
1226
+ const body = {
1227
+ idempotency_key: randomUUID2(),
1228
+ subject,
1229
+ action,
1230
+ estimate: { unit, amount: estimate },
1231
+ ttl_ms: ttlMs,
1232
+ overage_policy: overagePolicy
1233
+ };
1234
+ if (gracePeriodMs !== void 0) {
1235
+ body.grace_period_ms = gracePeriodMs;
1236
+ }
1237
+ const response = await client.createReservation(body);
1238
+ if (!response.isSuccess) {
1239
+ throw buildProtocolException("Failed to create reservation", response);
1240
+ }
1241
+ const parsed = reservationCreateResponseFromWire(
1242
+ response.body
1243
+ );
1244
+ if (parsed.decision === "DENY") {
1245
+ throw buildProtocolException("Reservation denied", response);
1246
+ }
1247
+ const reservationId = parsed.reservationId;
1248
+ if (!reservationId) {
1249
+ throw new CyclesProtocolError(
1250
+ "Reservation successful but reservation_id missing",
1251
+ { status: response.status }
1252
+ );
1253
+ }
1254
+ let heartbeatStopped = false;
1255
+ let finalized = false;
1256
+ let currentTimer;
1257
+ const stopHeartbeat = () => {
1258
+ if (!heartbeatStopped) {
1259
+ heartbeatStopped = true;
1260
+ clearTimeout(currentTimer);
1261
+ }
1262
+ };
1263
+ const startHeartbeat = () => {
1264
+ if (ttlMs <= 0) return;
1265
+ const intervalMs = Math.max(ttlMs / 2, 1e3);
1266
+ const tick = () => {
1267
+ if (heartbeatStopped) return;
1268
+ currentTimer = setTimeout(() => {
1269
+ if (heartbeatStopped) return;
1270
+ validateExtendByMs(ttlMs);
1271
+ const extendBody = { idempotency_key: randomUUID2(), extend_by_ms: ttlMs };
1272
+ void client.extendReservation(reservationId, extendBody).catch(() => {
1273
+ }).finally(() => {
1274
+ tick();
1275
+ });
1276
+ }, intervalMs);
1277
+ };
1278
+ tick();
1279
+ };
1280
+ startHeartbeat();
1281
+ return {
1282
+ reservationId,
1283
+ decision: parsed.decision,
1284
+ caps: parsed.caps,
1285
+ get finalized() {
1286
+ return finalized;
1287
+ },
1288
+ async commit(actual, metrics, metadata) {
1289
+ if (finalized) {
1290
+ throw new CyclesError("StreamReservation already finalized");
1291
+ }
1292
+ finalized = true;
1293
+ stopHeartbeat();
1294
+ const commitBody = {
1295
+ idempotency_key: randomUUID2(),
1296
+ actual: { unit, amount: actual }
1297
+ };
1298
+ if (metrics && !isMetricsEmpty2(metrics)) {
1299
+ commitBody.metrics = metricsToWire(metrics);
1300
+ }
1301
+ if (metadata) {
1302
+ commitBody.metadata = metadata;
1303
+ }
1304
+ await client.commitReservation(reservationId, commitBody);
1305
+ },
1306
+ async release(reason) {
1307
+ if (finalized) return;
1308
+ finalized = true;
1309
+ stopHeartbeat();
1310
+ try {
1311
+ const releaseBody = { idempotency_key: randomUUID2(), reason: reason ?? "stream_aborted" };
1312
+ await client.releaseReservation(reservationId, releaseBody);
1313
+ } catch {
1314
+ }
1315
+ },
1316
+ dispose() {
1317
+ if (finalized) return;
1318
+ finalized = true;
1319
+ stopHeartbeat();
1320
+ }
1321
+ };
1322
+ }
1323
+ export {
1324
+ BudgetExceededError,
1325
+ CommitOveragePolicy,
1326
+ CommitStatus,
1327
+ CyclesClient,
1328
+ CyclesConfig,
1329
+ CyclesError,
1330
+ CyclesProtocolError,
1331
+ CyclesResponse,
1332
+ CyclesTransportError,
1333
+ DebtOutstandingError,
1334
+ Decision,
1335
+ ErrorCode,
1336
+ EventStatus,
1337
+ ExtendStatus,
1338
+ OverdraftLimitExceededError,
1339
+ ReleaseStatus,
1340
+ ReservationExpiredError,
1341
+ ReservationFinalizedError,
1342
+ ReservationStatus,
1343
+ Unit,
1344
+ balanceResponseFromWire,
1345
+ capsFromWire,
1346
+ commitRequestToWire,
1347
+ commitResponseFromWire,
1348
+ decisionRequestToWire,
1349
+ decisionResponseFromWire,
1350
+ errorCodeFromString,
1351
+ errorResponseFromWire,
1352
+ eventCreateRequestToWire,
1353
+ eventCreateResponseFromWire,
1354
+ getCyclesContext,
1355
+ isAllowed,
1356
+ isDenied,
1357
+ isMetricsEmpty2 as isMetricsEmpty,
1358
+ isRetryableErrorCode,
1359
+ isToolAllowed,
1360
+ metricsToWire,
1361
+ releaseRequestToWire,
1362
+ releaseResponseFromWire,
1363
+ reservationCreateRequestToWire,
1364
+ reservationCreateResponseFromWire,
1365
+ reservationDetailFromWire,
1366
+ reservationExtendRequestToWire,
1367
+ reservationExtendResponseFromWire,
1368
+ reservationListResponseFromWire,
1369
+ reservationSummaryFromWire,
1370
+ reserveForStream,
1371
+ setDefaultClient,
1372
+ setDefaultConfig,
1373
+ withCycles
1374
+ };