runcycles 0.1.0 → 0.1.2

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/constants.ts
2
2
  var API_KEY_HEADER = "X-Cycles-API-Key";
3
3
  var IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key";
4
+ var DEFAULT_TTL_MS = 6e4;
4
5
  var RESERVATIONS_PATH = "/v1/reservations";
5
6
  var DECIDE_PATH = "/v1/decide";
6
7
  var BALANCES_PATH = "/v1/balances";
@@ -460,6 +461,8 @@ var CyclesClient = class {
460
461
  );
461
462
  }
462
463
  }
464
+ async [Symbol.asyncDispose]() {
465
+ }
463
466
  async _handleResponse(resp) {
464
467
  let body;
465
468
  try {
@@ -708,6 +711,94 @@ function buildProtocolException(prefix, response) {
708
711
  }
709
712
  }
710
713
 
714
+ // src/models.ts
715
+ var Unit = /* @__PURE__ */ ((Unit2) => {
716
+ Unit2["USD_MICROCENTS"] = "USD_MICROCENTS";
717
+ Unit2["TOKENS"] = "TOKENS";
718
+ Unit2["CREDITS"] = "CREDITS";
719
+ Unit2["RISK_POINTS"] = "RISK_POINTS";
720
+ return Unit2;
721
+ })(Unit || {});
722
+ var CommitOveragePolicy = /* @__PURE__ */ ((CommitOveragePolicy2) => {
723
+ CommitOveragePolicy2["REJECT"] = "REJECT";
724
+ CommitOveragePolicy2["ALLOW_IF_AVAILABLE"] = "ALLOW_IF_AVAILABLE";
725
+ CommitOveragePolicy2["ALLOW_WITH_OVERDRAFT"] = "ALLOW_WITH_OVERDRAFT";
726
+ return CommitOveragePolicy2;
727
+ })(CommitOveragePolicy || {});
728
+ var Decision = /* @__PURE__ */ ((Decision2) => {
729
+ Decision2["ALLOW"] = "ALLOW";
730
+ Decision2["ALLOW_WITH_CAPS"] = "ALLOW_WITH_CAPS";
731
+ Decision2["DENY"] = "DENY";
732
+ return Decision2;
733
+ })(Decision || {});
734
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
735
+ ReservationStatus2["ACTIVE"] = "ACTIVE";
736
+ ReservationStatus2["COMMITTED"] = "COMMITTED";
737
+ ReservationStatus2["RELEASED"] = "RELEASED";
738
+ ReservationStatus2["EXPIRED"] = "EXPIRED";
739
+ return ReservationStatus2;
740
+ })(ReservationStatus || {});
741
+ var CommitStatus = /* @__PURE__ */ ((CommitStatus2) => {
742
+ CommitStatus2["COMMITTED"] = "COMMITTED";
743
+ return CommitStatus2;
744
+ })(CommitStatus || {});
745
+ var ReleaseStatus = /* @__PURE__ */ ((ReleaseStatus2) => {
746
+ ReleaseStatus2["RELEASED"] = "RELEASED";
747
+ return ReleaseStatus2;
748
+ })(ReleaseStatus || {});
749
+ var ExtendStatus = /* @__PURE__ */ ((ExtendStatus2) => {
750
+ ExtendStatus2["ACTIVE"] = "ACTIVE";
751
+ return ExtendStatus2;
752
+ })(ExtendStatus || {});
753
+ var EventStatus = /* @__PURE__ */ ((EventStatus2) => {
754
+ EventStatus2["APPLIED"] = "APPLIED";
755
+ return EventStatus2;
756
+ })(EventStatus || {});
757
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
758
+ ErrorCode2["INVALID_REQUEST"] = "INVALID_REQUEST";
759
+ ErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
760
+ ErrorCode2["FORBIDDEN"] = "FORBIDDEN";
761
+ ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
762
+ ErrorCode2["BUDGET_EXCEEDED"] = "BUDGET_EXCEEDED";
763
+ ErrorCode2["RESERVATION_EXPIRED"] = "RESERVATION_EXPIRED";
764
+ ErrorCode2["RESERVATION_FINALIZED"] = "RESERVATION_FINALIZED";
765
+ ErrorCode2["IDEMPOTENCY_MISMATCH"] = "IDEMPOTENCY_MISMATCH";
766
+ ErrorCode2["UNIT_MISMATCH"] = "UNIT_MISMATCH";
767
+ ErrorCode2["OVERDRAFT_LIMIT_EXCEEDED"] = "OVERDRAFT_LIMIT_EXCEEDED";
768
+ ErrorCode2["DEBT_OUTSTANDING"] = "DEBT_OUTSTANDING";
769
+ ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
770
+ ErrorCode2["UNKNOWN"] = "UNKNOWN";
771
+ return ErrorCode2;
772
+ })(ErrorCode || {});
773
+ function isAllowed(decision) {
774
+ return decision === "ALLOW" /* ALLOW */ || decision === "ALLOW_WITH_CAPS" /* ALLOW_WITH_CAPS */;
775
+ }
776
+ function isDenied(decision) {
777
+ return decision === "DENY" /* DENY */;
778
+ }
779
+ function isRetryableErrorCode(code) {
780
+ return code === "INTERNAL_ERROR" /* INTERNAL_ERROR */ || code === "UNKNOWN" /* UNKNOWN */;
781
+ }
782
+ function errorCodeFromString(value) {
783
+ if (value === void 0) return void 0;
784
+ if (Object.values(ErrorCode).includes(value)) {
785
+ return value;
786
+ }
787
+ return "UNKNOWN" /* UNKNOWN */;
788
+ }
789
+ function isToolAllowed(caps, tool) {
790
+ if (caps.toolAllowlist && caps.toolAllowlist.length > 0) {
791
+ return caps.toolAllowlist.includes(tool);
792
+ }
793
+ if (caps.toolDenylist && caps.toolDenylist.length > 0) {
794
+ return !caps.toolDenylist.includes(tool);
795
+ }
796
+ return true;
797
+ }
798
+ function isMetricsEmpty(metrics) {
799
+ return metrics.tokensInput === void 0 && metrics.tokensOutput === void 0 && metrics.latencyMs === void 0 && metrics.modelVersion === void 0 && !metrics.custom;
800
+ }
801
+
711
802
  // src/validation.ts
712
803
  function validateSubject(subject) {
713
804
  if (subject === void 0) return;
@@ -762,7 +853,7 @@ function evaluateActual(expr, result, estimate, useEstimateFallback) {
762
853
  }
763
854
  function buildReservationBody(cfg, estimate, defaultSubject) {
764
855
  validateNonNegative(estimate, "estimate");
765
- const ttlMs = cfg.ttlMs ?? 6e4;
856
+ const ttlMs = cfg.ttlMs ?? DEFAULT_TTL_MS;
766
857
  validateTtlMs(ttlMs);
767
858
  const subject = {};
768
859
  for (const field of [
@@ -820,9 +911,6 @@ function buildCommitBody(actual, unit, metrics, metadata) {
820
911
  }
821
912
  return body;
822
913
  }
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
914
  function buildReleaseBody(reason) {
827
915
  return { idempotency_key: randomUUID(), reason };
828
916
  }
@@ -843,7 +931,6 @@ var AsyncCyclesLifecycle = class {
843
931
  async execute(fn, args, cfg) {
844
932
  const estimate = evaluateAmount(cfg.estimate, args);
845
933
  const createBody = buildReservationBody(cfg, estimate, this._defaultSubject);
846
- const resT1 = performance.now();
847
934
  const resResponse = await this._client.createReservation(createBody);
848
935
  if (!resResponse.isSuccess) {
849
936
  throw buildProtocolException("Failed to create reservation", resResponse);
@@ -880,7 +967,7 @@ var AsyncCyclesLifecycle = class {
880
967
  );
881
968
  }
882
969
  const unit = cfg.unit ?? "USD_MICROCENTS";
883
- const ttlMs = cfg.ttlMs ?? 6e4;
970
+ const ttlMs = cfg.ttlMs ?? DEFAULT_TTL_MS;
884
971
  const ctx = {
885
972
  reservationId,
886
973
  estimate,
@@ -941,7 +1028,13 @@ var AsyncCyclesLifecycle = class {
941
1028
  return;
942
1029
  }
943
1030
  const errorResp = response.getErrorResponse();
944
- const errorCode = errorResp?.error;
1031
+ let errorCode = errorResp?.error;
1032
+ if (errorCode === void 0) {
1033
+ const rawError = response.getBodyAttribute("error");
1034
+ if (typeof rawError === "string") {
1035
+ errorCode = rawError;
1036
+ }
1037
+ }
945
1038
  if (errorCode === "RESERVATION_FINALIZED" || errorCode === "RESERVATION_EXPIRED") {
946
1039
  return;
947
1040
  }
@@ -951,7 +1044,7 @@ var AsyncCyclesLifecycle = class {
951
1044
  if (response.isClientError) {
952
1045
  await this._handleRelease(
953
1046
  reservationId,
954
- `commit_rejected_${errorCode}`
1047
+ `commit_rejected_${errorCode ?? "unknown"}`
955
1048
  );
956
1049
  return;
957
1050
  }
@@ -1050,6 +1143,9 @@ var CommitRetryEngine = class {
1050
1143
  } catch {
1051
1144
  }
1052
1145
  }
1146
+ console.warn(
1147
+ `[runcycles] Commit retry exhausted after ${this._maxAttempts} attempts for reservation ${reservationId}`
1148
+ );
1053
1149
  }
1054
1150
  };
1055
1151
 
@@ -1074,24 +1170,26 @@ function getEffectiveClient(explicitClient) {
1074
1170
  );
1075
1171
  }
1076
1172
  function withCycles(options, fn) {
1173
+ let lifecycle;
1174
+ function ensureLifecycle() {
1175
+ if (!lifecycle) {
1176
+ const client = getEffectiveClient(options.client);
1177
+ const config = client.config;
1178
+ const defaultSubject = {
1179
+ tenant: config.tenant,
1180
+ workspace: config.workspace,
1181
+ app: config.app,
1182
+ workflow: config.workflow,
1183
+ agent: config.agent,
1184
+ toolset: config.toolset
1185
+ };
1186
+ const retryEngine = new CommitRetryEngine(config);
1187
+ lifecycle = new AsyncCyclesLifecycle(client, retryEngine, defaultSubject);
1188
+ }
1189
+ return lifecycle;
1190
+ }
1077
1191
  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(
1192
+ return ensureLifecycle().execute(
1095
1193
  fn,
1096
1194
  args,
1097
1195
  options
@@ -1101,96 +1199,6 @@ function withCycles(options, fn) {
1101
1199
 
1102
1200
  // src/streaming.ts
1103
1201
  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
1202
  async function reserveForStream(options) {
1195
1203
  const {
1196
1204
  client,
@@ -1199,7 +1207,7 @@ async function reserveForStream(options) {
1199
1207
  actionKind = "unknown",
1200
1208
  actionName = "unknown",
1201
1209
  actionTags,
1202
- ttlMs = 6e4,
1210
+ ttlMs = DEFAULT_TTL_MS,
1203
1211
  gracePeriodMs,
1204
1212
  overagePolicy = "REJECT",
1205
1213
  dimensions
@@ -1295,13 +1303,23 @@ async function reserveForStream(options) {
1295
1303
  idempotency_key: randomUUID2(),
1296
1304
  actual: { unit, amount: actual }
1297
1305
  };
1298
- if (metrics && !isMetricsEmpty2(metrics)) {
1306
+ if (metrics && !isMetricsEmpty(metrics)) {
1299
1307
  commitBody.metrics = metricsToWire(metrics);
1300
1308
  }
1301
1309
  if (metadata) {
1302
1310
  commitBody.metadata = metadata;
1303
1311
  }
1304
- await client.commitReservation(reservationId, commitBody);
1312
+ try {
1313
+ const response2 = await client.commitReservation(reservationId, commitBody);
1314
+ if (!response2.isSuccess) {
1315
+ throw new CyclesError(
1316
+ `Commit failed with status ${response2.status}: ${response2.errorMessage ?? "unknown error"}`
1317
+ );
1318
+ }
1319
+ } catch (err) {
1320
+ finalized = false;
1321
+ throw err;
1322
+ }
1305
1323
  },
1306
1324
  async release(reason) {
1307
1325
  if (finalized) return;
@@ -1354,7 +1372,7 @@ export {
1354
1372
  getCyclesContext,
1355
1373
  isAllowed,
1356
1374
  isDenied,
1357
- isMetricsEmpty2 as isMetricsEmpty,
1375
+ isMetricsEmpty,
1358
1376
  isRetryableErrorCode,
1359
1377
  isToolAllowed,
1360
1378
  metricsToWire,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcycles",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "TypeScript client for the Cycles budget-management protocol",
5
5
  "license": "Apache-2.0",
6
6
  "author": "runcycles",
@@ -48,14 +48,19 @@
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsup",
51
+ "lint": "eslint src/",
51
52
  "test": "vitest run",
52
53
  "test:watch": "vitest",
53
- "typecheck": "tsc --noEmit",
54
- "prepublishOnly": "npm run build"
54
+ "test:coverage": "vitest run --coverage",
55
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
56
+ "prepublishOnly": "npm run lint && npm run build"
55
57
  },
56
58
  "devDependencies": {
57
59
  "@types/node": "^22.19.15",
60
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
61
+ "@typescript-eslint/parser": "^8.57.0",
58
62
  "@vitest/coverage-v8": "^4.1.0",
63
+ "eslint": "^10.0.3",
59
64
  "tsup": "^8.0.0",
60
65
  "typescript": "^5.9.3",
61
66
  "vite": "^6.4.1",