@clocknext/sdk 0.6.0 → 0.8.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
@@ -55,10 +55,45 @@ await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", agentKe
55
55
  // Wallet — debits the customer's USD wallet at model cost
56
56
  await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
57
57
 
58
- // Outcome — advances one step of a workflow (agentKey = the OutcomeStep's agent key)
59
- await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", agentKey: "resolved_ticket.triage" });
58
+ // Outcome — one step of a run (agentKey = the OutcomeStep's agent key)
59
+ await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", agentKey: "resolved_ticket.triage", runId: "ticket_9021", tokens: { input: 600, output: 150 } });
60
60
  ```
61
61
 
62
+ ### Outcome runs
63
+
64
+ An outcome is billed per **run**, not per step. Every signal for one run shares a
65
+ `runId` (opaque — any id you can correlate, e.g. your ticket/session id), so a
66
+ step can fire as many times as it needs to. The run bills **once**, when you mark
67
+ the final step `complete: true` — completion is never inferred from step
68
+ coverage. Sending `complete` twice is a safe no-op.
69
+
70
+ ```ts
71
+ const runId = "ticket_9021";
72
+ // each step of the run carries the same runId…
73
+ await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.triage", runId, tokens: { input: 600, output: 150 } });
74
+ await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.verify", runId, tokens: { input: 200, output: 80 } }); // may repeat
75
+ // …and the last one closes (and bills) the run:
76
+ await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.resolve", runId, complete: true, tokens: { input: 500, output: 200 } });
77
+ ```
78
+
79
+ ### Pricing metrics
80
+
81
+ Tag any credit or outcome signal with a **pricing metric** for reporting. `ref`
82
+ is the metric's ref ID (Settings → Pricing metrics) and `value` is your own
83
+ correlation id; the metric rolls usage up one row per distinct value.
84
+
85
+ ```ts
86
+ await cnk.signals.credit({
87
+ customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit",
88
+ tokens: { input: 1200, output: 340 },
89
+ metric: { ref: "voice_ai", value: "call_5f3a" },
90
+ });
91
+ ```
92
+
93
+ Metric tags are **reporting only** — they never change what's billed, a tag that
94
+ names no live metric is silently ignored, and a bad tag never fails the signal.
95
+ Tags on wallet signals are ignored.
96
+
62
97
  ### Async vs sync
63
98
 
64
99
  By default the client is in **`async`** mode: `track()` buffers the signal and
@@ -71,17 +106,27 @@ path.
71
106
  > how many run at once (`batch.maxConcurrency`). There is no single bulk-ingest
72
107
  > call, so size your request budget per signal.
73
108
 
74
- To get the computed result back (and any real-time allowance rejection), either:
109
+ To await the request itself (rather than buffering it), either:
75
110
 
76
111
  ```ts
77
112
  // per call
78
113
  const res = await cnk.signals.credit({ … }, { wait: true });
79
- console.log(res.usageLog);
80
114
 
81
115
  // or globally
82
116
  const cnk = new ClockNext({ apiKey, mode: "sync" });
83
117
  ```
84
118
 
119
+ > **Awaiting is not the same as billed.** Where the server has a durable queue
120
+ > configured it answers `202` and prices the signal in a worker, so you get
121
+ > `{ queued: true, messageId, usageLog: null }` — accepted, not yet recorded, and
122
+ > validated only later (a bad `agentKey` is dropped there, silently). Only when
123
+ > the server prices inline do you get `{ queued: false, usageLog }`. Branch on
124
+ > `queued`; never assume `usageLog` is populated.
125
+ >
126
+ > To validate and price a signal synchronously, use `signals.verify()` (a dry
127
+ > run — it always returns the projected log). To confirm a real signal landed,
128
+ > read it back with `signals.list({ customerId })`.
129
+
85
130
  > In `async` mode, server-side rejections (e.g. insufficient allowance, `422`)
86
131
  > surface via the `onError` hook or `flush()`, not from the `track()` call.
87
132
 
@@ -195,9 +240,12 @@ Define the billable *types*; record usage against them with `signals.*`.
195
240
  await cnk.credits.create({ name: "API", agentKey: "api_credit", basePrice: 1, marginPercent: 20, pricePerCredit: 0.01 });
196
241
  await cnk.credits.list({ active: true });
197
242
 
198
- // outcome type — a multi-step workflow; each step has its own agentKey
243
+ // outcome type — a multi-step workflow. TWO kinds of agentKey, both required and
244
+ // both unique per org: the outcome's own (its identity) and each step's (what
245
+ // signals.outcome() reports against). They are separate namespaces.
199
246
  await cnk.outcomes.create({
200
247
  name: "Resolved ticket",
248
+ agentKey: "resolved_ticket",
201
249
  basePrice: 1, marginPercent: 20, pricePerOutcome: 2,
202
250
  steps: [{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 }],
203
251
  });
package/dist/index.cjs CHANGED
@@ -229,7 +229,7 @@ function sleep(ms) {
229
229
  }
230
230
 
231
231
  // src/http.ts
232
- var SDK_VERSION = "0.6.0" ;
232
+ var SDK_VERSION = "0.8.0" ;
233
233
  var Transport = class {
234
234
  constructor(cfg) {
235
235
  this.cfg = cfg;
@@ -870,6 +870,13 @@ function signalToWire(signal) {
870
870
  if (signal.type === "credit" || signal.type === "outcome") {
871
871
  body.agentKey = signal.agentKey;
872
872
  }
873
+ if (signal.type === "outcome") {
874
+ body.runId = signal.runId;
875
+ if (signal.complete != null) body.complete = signal.complete;
876
+ }
877
+ if (signal.type !== "wallet" && signal.metric) {
878
+ body[signal.metric.ref] = signal.metric.value;
879
+ }
873
880
  if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
874
881
  return body;
875
882
  }
@@ -882,6 +889,13 @@ var Signals = class {
882
889
  transport;
883
890
  flusher;
884
891
  cfg;
892
+ /**
893
+ * Record one usage signal. `POST /api/v1/usage`.
894
+ *
895
+ * Returns as soon as the signal is buffered (async mode) or the request has
896
+ * been answered (`{ wait: true }` / sync mode). See {@link TrackResult}: a
897
+ * resolved promise means "accepted", which is not the same as "billed".
898
+ */
885
899
  async track(signal, opts = {}) {
886
900
  const body = signalToWire(signal);
887
901
  if (body.idempotencyKey == null) body.idempotencyKey = newKey();
@@ -898,8 +912,9 @@ var Signals = class {
898
912
  retry: true
899
913
  });
900
914
  return {
901
- queued: false,
902
- usageLog: res.usageLog ?? null
915
+ queued: res.queued === true,
916
+ usageLog: res.usageLog ?? null,
917
+ ...res.messageId != null ? { messageId: res.messageId } : {}
903
918
  };
904
919
  }
905
920
  /**