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 +53 -1
- package/dist/index.cjs +5 -2
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/runcycles)
|
|
2
|
+
[](https://github.com/runcycles/cycles-client-typescript/actions)
|
|
3
|
+
[](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; // "
|
|
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 ?? "
|
|
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 = "
|
|
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 ?? "
|
|
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 = "
|
|
1215
|
+
overagePolicy = "ALLOW_IF_AVAILABLE",
|
|
1213
1216
|
dimensions
|
|
1214
1217
|
} = options;
|
|
1215
1218
|
validateNonNegative(estimate, "estimate");
|