runcycles 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ [![npm](https://img.shields.io/npm/v/runcycles)](https://www.npmjs.com/package/runcycles)
2
+ [![CI](https://github.com/runcycles/cycles-client-typescript/actions/workflows/ci.yml/badge.svg)](https://github.com/runcycles/cycles-client-typescript/actions)
3
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
4
+
1
5
  # Cycles TypeScript Client
2
6
 
3
7
  TypeScript client for the [Cycles](https://runcycles.io) budget-management protocol — govern spend on AI calls, API usage, and any metered resource.
@@ -69,6 +73,24 @@ const result = await callLlm("Hello", 100);
69
73
 
70
74
  **What happens:** `withCycles` reserves budget before calling your function, runs it inside an async context (so `getCyclesContext()` works), commits the actual cost on success, or releases the reservation on failure. A background heartbeat keeps the reservation alive.
71
75
 
76
+ ### Budget lifecycle
77
+
78
+ | Scenario | Outcome | Detail |
79
+ |---|---|---|
80
+ | Reservation denied | **Neither** | `BudgetExceededError`, `OverdraftLimitExceededError`, or `DebtOutstandingError` thrown; function never executes |
81
+ | `dryRun: true`, any decision | **Neither** | Returns `DryRunResult` or throws; no real reservation created |
82
+ | Function returns successfully | **Commit** | Actual amount charged; unused remainder auto-released |
83
+ | Function throws any error | **Release** | Full reserved amount returned to budget; error re-thrown |
84
+ | Commit fails (5xx / network) | **Retry** | Exponential backoff with configurable attempts |
85
+ | Commit fails (non-retryable 4xx) | **Release** | Reservation released after non-retryable client error |
86
+ | Commit gets RESERVATION_EXPIRED | **Neither** | Server already reclaimed budget on TTL expiry |
87
+ | Commit gets RESERVATION_FINALIZED | **Neither** | Already committed or released (idempotent replay) |
88
+ | Commit gets IDEMPOTENCY_MISMATCH | **Neither** | Previous commit already processed; no release attempted |
89
+
90
+ **Streaming (`reserveForStream`):** Call `handle.commit(actual)` on success or `handle.release(reason)` on failure. If neither is called, the server reclaims the budget when the reservation TTL expires.
91
+
92
+ All thrown errors from the guarded function trigger release. See [How Reserve-Commit Works](https://runcycles.io/protocol/how-reserve-commit-works-in-cycles) for the full protocol-level explanation.
93
+
72
94
  ### 2. Streaming adapter
73
95
 
74
96
  For LLM streaming where usage is only known after the stream finishes:
@@ -303,7 +325,7 @@ interface WithCyclesConfig {
303
325
  // Reservation settings
304
326
  ttlMs?: number; // Time-to-live in ms (default: 60000, range: 1000–86400000)
305
327
  gracePeriodMs?: number; // Grace period in ms (range: 0–60000)
306
- overagePolicy?: string; // "REJECT" (default), "ALLOW_IF_AVAILABLE", "ALLOW_WITH_OVERDRAFT"
328
+ overagePolicy?: string; // "ALLOW_IF_AVAILABLE" (default), "REJECT", "ALLOW_WITH_OVERDRAFT"
307
329
  dryRun?: boolean; // Shadow mode — evaluates budget without executing
308
330
 
309
331
  // Subject fields (override config defaults)
@@ -795,6 +817,29 @@ ErrorCode.INTERNAL_ERROR
795
817
  ErrorCode.UNKNOWN
796
818
  ```
797
819
 
820
+ ## Nested `withCycles` Calls
821
+
822
+ Calling a `withCycles`-wrapped function from inside another `withCycles`-wrapped function is allowed — it will not throw an error. However, each wrapper creates an **independent reservation** that deducts budget separately:
823
+
824
+ ```typescript
825
+ const inner = withCycles({ estimate: 100, actionName: "inner" }, async () => "done");
826
+ const outer = withCycles({ estimate: 500, actionName: "outer" }, async () => {
827
+ return await inner(); // creates a SECOND reservation — 600 total deducted, not 500
828
+ });
829
+ ```
830
+
831
+ This means nested guards **double-count budget**. The outer reservation already covers the full estimated cost of the operation, so an inner reservation deducts additional budget from the same pool.
832
+
833
+ **Recommended pattern:** Place `withCycles` at the outermost entry point only. Inner functions should be plain async functions without their own guard:
834
+
835
+ ```typescript
836
+ const inner = async () => "done"; // no withCycles — called within a guarded operation
837
+
838
+ const outer = withCycles({ estimate: 500, actionName: "outer" }, async () => {
839
+ return await inner(); // single reservation — 500 total
840
+ });
841
+ ```
842
+
798
843
  ## Examples
799
844
 
800
845
  See the [`examples/`](./examples/) directory:
@@ -824,6 +869,13 @@ See the [`examples/`](./examples/) directory:
824
869
  - **Dual ESM/CJS**: Works with both module systems
825
870
  - **Input validation**: Client-side validation of TTL, amounts, subject fields, and more
826
871
 
872
+ ## Documentation
873
+
874
+ - [Cycles Documentation](https://runcycles.io) — full docs site
875
+ - [TypeScript Quickstart](https://runcycles.io/quickstart/getting-started-with-the-typescript-client) — getting started guide
876
+ - [TypeScript Client Configuration Reference](https://runcycles.io/configuration/typescript-client-configuration-reference) — all configuration options
877
+ - [Error Handling Patterns in TypeScript](https://runcycles.io/how-to/error-handling-patterns-in-typescript) — handling budget errors
878
+
827
879
  ## License
828
880
 
829
881
  Apache-2.0
package/dist/index.cjs CHANGED
@@ -835,12 +835,15 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
835
835
  ErrorCode2["FORBIDDEN"] = "FORBIDDEN";
836
836
  ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
837
837
  ErrorCode2["BUDGET_EXCEEDED"] = "BUDGET_EXCEEDED";
838
+ ErrorCode2["BUDGET_FROZEN"] = "BUDGET_FROZEN";
839
+ ErrorCode2["BUDGET_CLOSED"] = "BUDGET_CLOSED";
838
840
  ErrorCode2["RESERVATION_EXPIRED"] = "RESERVATION_EXPIRED";
839
841
  ErrorCode2["RESERVATION_FINALIZED"] = "RESERVATION_FINALIZED";
840
842
  ErrorCode2["IDEMPOTENCY_MISMATCH"] = "IDEMPOTENCY_MISMATCH";
841
843
  ErrorCode2["UNIT_MISMATCH"] = "UNIT_MISMATCH";
842
844
  ErrorCode2["OVERDRAFT_LIMIT_EXCEEDED"] = "OVERDRAFT_LIMIT_EXCEEDED";
843
845
  ErrorCode2["DEBT_OUTSTANDING"] = "DEBT_OUTSTANDING";
846
+ ErrorCode2["MAX_EXTENSIONS_EXCEEDED"] = "MAX_EXTENSIONS_EXCEEDED";
844
847
  ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
845
848
  ErrorCode2["UNKNOWN"] = "UNKNOWN";
846
849
  return ErrorCode2;
@@ -962,7 +965,7 @@ function buildReservationBody(cfg, estimate, defaultSubject) {
962
965
  action,
963
966
  estimate: { unit, amount: estimate },
964
967
  ttl_ms: ttlMs,
965
- overage_policy: cfg.overagePolicy ?? "REJECT"
968
+ overage_policy: cfg.overagePolicy ?? "ALLOW_IF_AVAILABLE"
966
969
  };
967
970
  validateGracePeriodMs(cfg.gracePeriodMs);
968
971
  if (cfg.gracePeriodMs !== void 0) {
@@ -1284,7 +1287,7 @@ async function reserveForStream(options) {
1284
1287
  actionTags,
1285
1288
  ttlMs = DEFAULT_TTL_MS,
1286
1289
  gracePeriodMs,
1287
- overagePolicy = "REJECT",
1290
+ overagePolicy = "ALLOW_IF_AVAILABLE",
1288
1291
  dimensions
1289
1292
  } = options;
1290
1293
  validateNonNegative(estimate, "estimate");
package/dist/index.d.cts CHANGED
@@ -76,12 +76,15 @@ declare enum ErrorCode {
76
76
  FORBIDDEN = "FORBIDDEN",
77
77
  NOT_FOUND = "NOT_FOUND",
78
78
  BUDGET_EXCEEDED = "BUDGET_EXCEEDED",
79
+ BUDGET_FROZEN = "BUDGET_FROZEN",
80
+ BUDGET_CLOSED = "BUDGET_CLOSED",
79
81
  RESERVATION_EXPIRED = "RESERVATION_EXPIRED",
80
82
  RESERVATION_FINALIZED = "RESERVATION_FINALIZED",
81
83
  IDEMPOTENCY_MISMATCH = "IDEMPOTENCY_MISMATCH",
82
84
  UNIT_MISMATCH = "UNIT_MISMATCH",
83
85
  OVERDRAFT_LIMIT_EXCEEDED = "OVERDRAFT_LIMIT_EXCEEDED",
84
86
  DEBT_OUTSTANDING = "DEBT_OUTSTANDING",
87
+ MAX_EXTENSIONS_EXCEEDED = "MAX_EXTENSIONS_EXCEEDED",
85
88
  INTERNAL_ERROR = "INTERNAL_ERROR",
86
89
  UNKNOWN = "UNKNOWN"
87
90
  }
@@ -213,6 +216,7 @@ interface DecisionResponse {
213
216
  interface EventCreateResponse {
214
217
  status: EventStatus;
215
218
  eventId: string;
219
+ charged?: Amount;
216
220
  balances?: Balance[];
217
221
  }
218
222
  interface DryRunResult {
package/dist/index.d.ts CHANGED
@@ -76,12 +76,15 @@ declare enum ErrorCode {
76
76
  FORBIDDEN = "FORBIDDEN",
77
77
  NOT_FOUND = "NOT_FOUND",
78
78
  BUDGET_EXCEEDED = "BUDGET_EXCEEDED",
79
+ BUDGET_FROZEN = "BUDGET_FROZEN",
80
+ BUDGET_CLOSED = "BUDGET_CLOSED",
79
81
  RESERVATION_EXPIRED = "RESERVATION_EXPIRED",
80
82
  RESERVATION_FINALIZED = "RESERVATION_FINALIZED",
81
83
  IDEMPOTENCY_MISMATCH = "IDEMPOTENCY_MISMATCH",
82
84
  UNIT_MISMATCH = "UNIT_MISMATCH",
83
85
  OVERDRAFT_LIMIT_EXCEEDED = "OVERDRAFT_LIMIT_EXCEEDED",
84
86
  DEBT_OUTSTANDING = "DEBT_OUTSTANDING",
87
+ MAX_EXTENSIONS_EXCEEDED = "MAX_EXTENSIONS_EXCEEDED",
85
88
  INTERNAL_ERROR = "INTERNAL_ERROR",
86
89
  UNKNOWN = "UNKNOWN"
87
90
  }
@@ -213,6 +216,7 @@ interface DecisionResponse {
213
216
  interface EventCreateResponse {
214
217
  status: EventStatus;
215
218
  eventId: string;
219
+ charged?: Amount;
216
220
  balances?: Balance[];
217
221
  }
218
222
  interface DryRunResult {
package/dist/index.js CHANGED
@@ -760,12 +760,15 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
760
760
  ErrorCode2["FORBIDDEN"] = "FORBIDDEN";
761
761
  ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
762
762
  ErrorCode2["BUDGET_EXCEEDED"] = "BUDGET_EXCEEDED";
763
+ ErrorCode2["BUDGET_FROZEN"] = "BUDGET_FROZEN";
764
+ ErrorCode2["BUDGET_CLOSED"] = "BUDGET_CLOSED";
763
765
  ErrorCode2["RESERVATION_EXPIRED"] = "RESERVATION_EXPIRED";
764
766
  ErrorCode2["RESERVATION_FINALIZED"] = "RESERVATION_FINALIZED";
765
767
  ErrorCode2["IDEMPOTENCY_MISMATCH"] = "IDEMPOTENCY_MISMATCH";
766
768
  ErrorCode2["UNIT_MISMATCH"] = "UNIT_MISMATCH";
767
769
  ErrorCode2["OVERDRAFT_LIMIT_EXCEEDED"] = "OVERDRAFT_LIMIT_EXCEEDED";
768
770
  ErrorCode2["DEBT_OUTSTANDING"] = "DEBT_OUTSTANDING";
771
+ ErrorCode2["MAX_EXTENSIONS_EXCEEDED"] = "MAX_EXTENSIONS_EXCEEDED";
769
772
  ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
770
773
  ErrorCode2["UNKNOWN"] = "UNKNOWN";
771
774
  return ErrorCode2;
@@ -887,7 +890,7 @@ function buildReservationBody(cfg, estimate, defaultSubject) {
887
890
  action,
888
891
  estimate: { unit, amount: estimate },
889
892
  ttl_ms: ttlMs,
890
- overage_policy: cfg.overagePolicy ?? "REJECT"
893
+ overage_policy: cfg.overagePolicy ?? "ALLOW_IF_AVAILABLE"
891
894
  };
892
895
  validateGracePeriodMs(cfg.gracePeriodMs);
893
896
  if (cfg.gracePeriodMs !== void 0) {
@@ -1209,7 +1212,7 @@ async function reserveForStream(options) {
1209
1212
  actionTags,
1210
1213
  ttlMs = DEFAULT_TTL_MS,
1211
1214
  gracePeriodMs,
1212
- overagePolicy = "REJECT",
1215
+ overagePolicy = "ALLOW_IF_AVAILABLE",
1213
1216
  dimensions
1214
1217
  } = options;
1215
1218
  validateNonNegative(estimate, "estimate");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcycles",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript client for the Cycles budget-management protocol",
5
5
  "license": "Apache-2.0",
6
6
  "author": "runcycles",