runcycles 0.2.0 → 0.3.1

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,12 +1,14 @@
1
1
  [![npm](https://img.shields.io/npm/v/runcycles)](https://www.npmjs.com/package/runcycles)
2
+ [![npm Downloads](https://img.shields.io/npm/dm/runcycles)](https://www.npmjs.com/package/runcycles)
2
3
  [![CI](https://github.com/runcycles/cycles-client-typescript/actions/workflows/ci.yml/badge.svg)](https://github.com/runcycles/cycles-client-typescript/actions)
3
4
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
5
+ [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen)](https://github.com/runcycles/cycles-client-typescript/actions)
4
6
 
5
- # Cycles TypeScript Client
7
+ # Cycles TypeScript Client — AI agent budget and action authority SDK
6
8
 
7
- TypeScript client for the [Cycles](https://runcycles.io) budget-management protocolgovern spend on AI calls, API usage, and any metered resource.
9
+ **TypeScript/Node.js SDK for AI agent budget governanceenforce cost limits, tool permissions, and multi-tenant policies before LLM calls or agent actions execute.** Works with OpenAI, Anthropic, MCP servers, OpenAI Agents SDK, LangChain.js, and any Node.js agent runtime.
8
10
 
9
- Cycles lets you set budgets, reserve capacity before expensive operations, and track actual usage. This client handles the full reservation lifecycle: reserve budget up front, execute your work, then commit or release — with automatic heartbeats, retries, and typed error handling.
11
+ Higher-order function and `AsyncLocalStorage`-based API for the [Cycles Protocol](https://github.com/runcycles/cycles-protocol): reserve capacity before expensive operations, execute your work, commit or release — with automatic heartbeats, retries, and typed error handling. Install via `npm install runcycles`.
10
12
 
11
13
  ## Requirements
12
14
 
@@ -314,9 +316,9 @@ interface WithCyclesConfig {
314
316
  actual?: number | ((result) => number); // Actual cost (static or computed from result)
315
317
  useEstimateIfActualNotProvided?: boolean; // Default: true — use estimate as actual
316
318
 
317
- // Action identification
318
- actionKind?: string; // e.g. "llm.completion" (default: "unknown")
319
- actionName?: string; // e.g. "gpt-4" (default: "unknown")
319
+ // Action identification (static or computed from args)
320
+ actionKind?: string | ((...args) => string | undefined); // e.g. "llm.completion" (default: "unknown")
321
+ actionName?: string | ((...args) => string | undefined); // e.g. "gpt-4" (default: "unknown")
320
322
  actionTags?: string[]; // Optional tags for categorization
321
323
 
322
324
  // Budget unit
@@ -328,13 +330,13 @@ interface WithCyclesConfig {
328
330
  overagePolicy?: string; // "ALLOW_IF_AVAILABLE" (default), "REJECT", "ALLOW_WITH_OVERDRAFT"
329
331
  dryRun?: boolean; // Shadow mode — evaluates budget without executing
330
332
 
331
- // Subject fields (override config defaults)
332
- tenant?: string;
333
- workspace?: string;
334
- app?: string;
335
- workflow?: string;
336
- agent?: string;
337
- toolset?: string;
333
+ // Subject fields (override config defaults; static or computed from args)
334
+ tenant?: string | ((...args) => string | undefined);
335
+ workspace?: string | ((...args) => string | undefined);
336
+ app?: string | ((...args) => string | undefined);
337
+ workflow?: string | ((...args) => string | undefined);
338
+ agent?: string | ((...args) => string | undefined);
339
+ toolset?: string | ((...args) => string | undefined);
338
340
  dimensions?: Record<string, string>; // Custom key-value dimensions
339
341
 
340
342
  // Client
@@ -342,6 +344,28 @@ interface WithCyclesConfig {
342
344
  }
343
345
  ```
344
346
 
347
+ A callable returning `undefined` falls through to the client-config default for subject fields, or to `"unknown"` for `actionKind` / `actionName` — same fallback semantics as a missing static. Callables run before the reservation is created; if one throws, the reservation is never attempted and the error propagates to the caller.
348
+
349
+ ### Dynamic subject and action fields
350
+
351
+ Derive the subject scope or action identity from per-call arguments:
352
+
353
+ ```typescript
354
+ const runRequest = withCycles(
355
+ {
356
+ estimate: (req, workspaceId) => req.tokens * 10,
357
+ workspace: (_req, workspaceId) => workspaceId,
358
+ actionKind: "llm.completion",
359
+ actionName: (req) => req.model,
360
+ client,
361
+ },
362
+ async (req: { tokens: number; model: string }, workspaceId: string) => {
363
+ // ... the reservation routes to this workspaceId ...
364
+ return callLLM(req);
365
+ },
366
+ );
367
+ ```
368
+
345
369
  ## Context Access
346
370
 
347
371
  Inside a `withCycles`-guarded function, access the active reservation via `getCyclesContext()`:
package/dist/index.cjs CHANGED
@@ -929,7 +929,13 @@ function evaluateActual(expr, result, estimate, useEstimateFallback) {
929
929
  "actual expression is required when useEstimateIfActualNotProvided is false"
930
930
  );
931
931
  }
932
- function buildReservationBody(cfg, estimate, defaultSubject) {
932
+ function evaluateStringField(expr, args) {
933
+ if (typeof expr === "function") {
934
+ return expr(...args);
935
+ }
936
+ return expr;
937
+ }
938
+ function buildReservationBody(cfg, estimate, defaultSubject, args) {
933
939
  validateNonNegative(estimate, "estimate");
934
940
  const ttlMs = cfg.ttlMs ?? DEFAULT_TTL_MS;
935
941
  validateTtlMs(ttlMs);
@@ -942,7 +948,8 @@ function buildReservationBody(cfg, estimate, defaultSubject) {
942
948
  "agent",
943
949
  "toolset"
944
950
  ]) {
945
- const val = cfg[field] ?? defaultSubject[field];
951
+ const resolved = evaluateStringField(cfg[field], args);
952
+ const val = resolved ?? defaultSubject[field];
946
953
  if (val) {
947
954
  subject[field] = val;
948
955
  }
@@ -952,8 +959,8 @@ function buildReservationBody(cfg, estimate, defaultSubject) {
952
959
  }
953
960
  validateSubject(subject);
954
961
  const action = {
955
- kind: cfg.actionKind ?? "unknown",
956
- name: cfg.actionName ?? "unknown"
962
+ kind: evaluateStringField(cfg.actionKind, args) ?? "unknown",
963
+ name: evaluateStringField(cfg.actionName, args) ?? "unknown"
957
964
  };
958
965
  if (cfg.actionTags) {
959
966
  action.tags = cfg.actionTags;
@@ -1008,7 +1015,12 @@ var AsyncCyclesLifecycle = class {
1008
1015
  }
1009
1016
  async execute(fn, args, cfg) {
1010
1017
  const estimate = evaluateAmount(cfg.estimate, args);
1011
- const createBody = buildReservationBody(cfg, estimate, this._defaultSubject);
1018
+ const createBody = buildReservationBody(
1019
+ cfg,
1020
+ estimate,
1021
+ this._defaultSubject,
1022
+ args
1023
+ );
1012
1024
  const resResponse = await this._client.createReservation(createBody);
1013
1025
  if (!resResponse.isSuccess) {
1014
1026
  throw buildProtocolException("Failed to create reservation", resResponse);
package/dist/index.d.cts CHANGED
@@ -342,20 +342,20 @@ declare class CyclesClient {
342
342
  interface WithCyclesConfig<TArgs extends unknown[] = unknown[], TResult = unknown> {
343
343
  estimate: number | ((...args: TArgs) => number);
344
344
  actual?: number | ((result: TResult) => number);
345
- actionKind?: string;
346
- actionName?: string;
345
+ actionKind?: string | ((...args: TArgs) => string | undefined);
346
+ actionName?: string | ((...args: TArgs) => string | undefined);
347
347
  actionTags?: string[];
348
348
  unit?: string;
349
349
  ttlMs?: number;
350
350
  gracePeriodMs?: number;
351
351
  overagePolicy?: string;
352
352
  dryRun?: boolean;
353
- tenant?: string;
354
- workspace?: string;
355
- app?: string;
356
- workflow?: string;
357
- agent?: string;
358
- toolset?: string;
353
+ tenant?: string | ((...args: TArgs) => string | undefined);
354
+ workspace?: string | ((...args: TArgs) => string | undefined);
355
+ app?: string | ((...args: TArgs) => string | undefined);
356
+ workflow?: string | ((...args: TArgs) => string | undefined);
357
+ agent?: string | ((...args: TArgs) => string | undefined);
358
+ toolset?: string | ((...args: TArgs) => string | undefined);
359
359
  dimensions?: Record<string, string>;
360
360
  useEstimateIfActualNotProvided?: boolean;
361
361
  }
package/dist/index.d.ts CHANGED
@@ -342,20 +342,20 @@ declare class CyclesClient {
342
342
  interface WithCyclesConfig<TArgs extends unknown[] = unknown[], TResult = unknown> {
343
343
  estimate: number | ((...args: TArgs) => number);
344
344
  actual?: number | ((result: TResult) => number);
345
- actionKind?: string;
346
- actionName?: string;
345
+ actionKind?: string | ((...args: TArgs) => string | undefined);
346
+ actionName?: string | ((...args: TArgs) => string | undefined);
347
347
  actionTags?: string[];
348
348
  unit?: string;
349
349
  ttlMs?: number;
350
350
  gracePeriodMs?: number;
351
351
  overagePolicy?: string;
352
352
  dryRun?: boolean;
353
- tenant?: string;
354
- workspace?: string;
355
- app?: string;
356
- workflow?: string;
357
- agent?: string;
358
- toolset?: string;
353
+ tenant?: string | ((...args: TArgs) => string | undefined);
354
+ workspace?: string | ((...args: TArgs) => string | undefined);
355
+ app?: string | ((...args: TArgs) => string | undefined);
356
+ workflow?: string | ((...args: TArgs) => string | undefined);
357
+ agent?: string | ((...args: TArgs) => string | undefined);
358
+ toolset?: string | ((...args: TArgs) => string | undefined);
359
359
  dimensions?: Record<string, string>;
360
360
  useEstimateIfActualNotProvided?: boolean;
361
361
  }
package/dist/index.js CHANGED
@@ -854,7 +854,13 @@ function evaluateActual(expr, result, estimate, useEstimateFallback) {
854
854
  "actual expression is required when useEstimateIfActualNotProvided is false"
855
855
  );
856
856
  }
857
- function buildReservationBody(cfg, estimate, defaultSubject) {
857
+ function evaluateStringField(expr, args) {
858
+ if (typeof expr === "function") {
859
+ return expr(...args);
860
+ }
861
+ return expr;
862
+ }
863
+ function buildReservationBody(cfg, estimate, defaultSubject, args) {
858
864
  validateNonNegative(estimate, "estimate");
859
865
  const ttlMs = cfg.ttlMs ?? DEFAULT_TTL_MS;
860
866
  validateTtlMs(ttlMs);
@@ -867,7 +873,8 @@ function buildReservationBody(cfg, estimate, defaultSubject) {
867
873
  "agent",
868
874
  "toolset"
869
875
  ]) {
870
- const val = cfg[field] ?? defaultSubject[field];
876
+ const resolved = evaluateStringField(cfg[field], args);
877
+ const val = resolved ?? defaultSubject[field];
871
878
  if (val) {
872
879
  subject[field] = val;
873
880
  }
@@ -877,8 +884,8 @@ function buildReservationBody(cfg, estimate, defaultSubject) {
877
884
  }
878
885
  validateSubject(subject);
879
886
  const action = {
880
- kind: cfg.actionKind ?? "unknown",
881
- name: cfg.actionName ?? "unknown"
887
+ kind: evaluateStringField(cfg.actionKind, args) ?? "unknown",
888
+ name: evaluateStringField(cfg.actionName, args) ?? "unknown"
882
889
  };
883
890
  if (cfg.actionTags) {
884
891
  action.tags = cfg.actionTags;
@@ -933,7 +940,12 @@ var AsyncCyclesLifecycle = class {
933
940
  }
934
941
  async execute(fn, args, cfg) {
935
942
  const estimate = evaluateAmount(cfg.estimate, args);
936
- const createBody = buildReservationBody(cfg, estimate, this._defaultSubject);
943
+ const createBody = buildReservationBody(
944
+ cfg,
945
+ estimate,
946
+ this._defaultSubject,
947
+ args
948
+ );
937
949
  const resResponse = await this._client.createReservation(createBody);
938
950
  if (!resResponse.isSuccess) {
939
951
  throw buildProtocolException("Failed to create reservation", resResponse);
package/package.json CHANGED
@@ -1,25 +1,43 @@
1
1
  {
2
2
  "name": "runcycles",
3
- "version": "0.2.0",
4
- "description": "TypeScript client for the Cycles budget-management protocol",
3
+ "version": "0.3.1",
4
+ "description": "TypeScript AI agent runtime control — enforce LLM cost limits, action permissions, and audit trails for agents before execution.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "runcycles",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/runcycles/cycles-client-typescript.git"
10
10
  },
11
- "homepage": "https://github.com/runcycles/cycles-client-typescript#readme",
11
+ "homepage": "https://runcycles.io",
12
12
  "bugs": {
13
13
  "url": "https://github.com/runcycles/cycles-client-typescript/issues"
14
14
  },
15
15
  "keywords": [
16
+ "ai-agent",
17
+ "agent-budget",
18
+ "agent-governance",
19
+ "budget-control",
20
+ "cost-control",
21
+ "cost-enforcement",
22
+ "spending-limit",
23
+ "llm-cost",
24
+ "runtime-authority",
25
+ "action-control",
26
+ "action-authority",
27
+ "audit-trail",
28
+ "audit",
29
+ "compliance",
30
+ "multi-tenant",
31
+ "langchain",
32
+ "langgraph",
33
+ "openai-agents",
34
+ "vercel-ai-sdk",
35
+ "mcp",
36
+ "openai",
37
+ "anthropic",
38
+ "typescript-sdk",
39
+ "nodejs",
16
40
  "cycles",
17
- "budget",
18
- "billing",
19
- "metering",
20
- "api-client",
21
- "ai",
22
- "llm",
23
41
  "runcycles"
24
42
  ],
25
43
  "type": "module",
@@ -56,14 +74,17 @@
56
74
  "prepublishOnly": "npm run lint && npm run build"
57
75
  },
58
76
  "devDependencies": {
59
- "@types/node": "^22.19.15",
60
- "@typescript-eslint/eslint-plugin": "^8.57.0",
61
- "@typescript-eslint/parser": "^8.57.0",
77
+ "@types/node": "^25.5.0",
78
+ "@typescript-eslint/eslint-plugin": "^8.58.0",
79
+ "@typescript-eslint/parser": "^8.58.0",
62
80
  "@vitest/coverage-v8": "^4.1.0",
81
+ "ajv": "^8.18.0",
82
+ "ajv-formats": "^3.0.1",
63
83
  "eslint": "^10.0.3",
64
84
  "tsup": "^8.0.0",
65
- "typescript": "^5.9.3",
66
- "vite": "^6.4.1",
67
- "vitest": "^4.1.0"
85
+ "typescript": "^6.0.2",
86
+ "vite": "^8.0.3",
87
+ "vitest": "^4.1.0",
88
+ "yaml": "^2.8.3"
68
89
  }
69
90
  }