@relayflows/sdk 2.0.36 → 2.0.38

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.
Files changed (230) hide show
  1. package/dist/agent-reaper.d.ts +1 -1
  2. package/dist/agent-reaper.d.ts.map +1 -1
  3. package/dist/agent-reaper.js +6 -4
  4. package/dist/agent-reaper.js.map +1 -1
  5. package/dist/authored-budget.d.ts +26 -1
  6. package/dist/authored-budget.d.ts.map +1 -1
  7. package/dist/authored-budget.js +72 -2
  8. package/dist/authored-budget.js.map +1 -1
  9. package/dist/authored-flow-error.d.ts +7 -1
  10. package/dist/authored-flow-error.d.ts.map +1 -1
  11. package/dist/authored-flow-error.js.map +1 -1
  12. package/dist/authored-flow-executor.d.ts +2 -0
  13. package/dist/authored-flow-executor.d.ts.map +1 -1
  14. package/dist/authored-flow-executor.js +2 -1
  15. package/dist/authored-flow-executor.js.map +1 -1
  16. package/dist/authored-helper-effect.d.ts.map +1 -1
  17. package/dist/authored-helper-effect.js +3 -2
  18. package/dist/authored-helper-effect.js.map +1 -1
  19. package/dist/authored-mcp.d.ts.map +1 -1
  20. package/dist/authored-mcp.js +2 -0
  21. package/dist/authored-mcp.js.map +1 -1
  22. package/dist/authored-node-entry.js +6 -0
  23. package/dist/authored-node-entry.js.map +1 -1
  24. package/dist/authored-node-runner.d.ts.map +1 -1
  25. package/dist/authored-node-runner.js +5 -3
  26. package/dist/authored-node-runner.js.map +1 -1
  27. package/dist/authored-plugin-effect.d.ts.map +1 -1
  28. package/dist/authored-plugin-effect.js +2 -0
  29. package/dist/authored-plugin-effect.js.map +1 -1
  30. package/dist/authored-preflight.d.ts +1 -1
  31. package/dist/authored-preflight.d.ts.map +1 -1
  32. package/dist/authored-preflight.js +2 -2
  33. package/dist/authored-preflight.js.map +1 -1
  34. package/dist/authored-root.d.ts +2 -0
  35. package/dist/authored-root.d.ts.map +1 -1
  36. package/dist/authored-root.js +146 -6
  37. package/dist/authored-root.js.map +1 -1
  38. package/dist/authored-verdict.d.ts +2 -0
  39. package/dist/authored-verdict.d.ts.map +1 -1
  40. package/dist/authored-verdict.js +2 -1
  41. package/dist/authored-verdict.js.map +1 -1
  42. package/dist/authored-worker-step.d.ts +1 -1
  43. package/dist/authored-worker-step.d.ts.map +1 -1
  44. package/dist/authored-worker-step.js +3 -2
  45. package/dist/authored-worker-step.js.map +1 -1
  46. package/dist/child-stop.d.ts +3 -1
  47. package/dist/child-stop.d.ts.map +1 -1
  48. package/dist/child-stop.js +42 -2
  49. package/dist/child-stop.js.map +1 -1
  50. package/dist/cli/add-extension.js +1 -1
  51. package/dist/cli/add-extension.js.map +1 -1
  52. package/dist/cli/check.d.ts +2 -0
  53. package/dist/cli/check.d.ts.map +1 -1
  54. package/dist/cli/check.js +28 -10
  55. package/dist/cli/check.js.map +1 -1
  56. package/dist/cli/cli-probe.d.ts +3 -2
  57. package/dist/cli/cli-probe.d.ts.map +1 -1
  58. package/dist/cli/cli-probe.js +79 -42
  59. package/dist/cli/cli-probe.js.map +1 -1
  60. package/dist/cli/direct-run.d.ts.map +1 -1
  61. package/dist/cli/direct-run.js +11 -3
  62. package/dist/cli/direct-run.js.map +1 -1
  63. package/dist/cli/pinned-cli-alias.d.ts +13 -0
  64. package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
  65. package/dist/cli/pinned-cli-alias.js +58 -0
  66. package/dist/cli/pinned-cli-alias.js.map +1 -0
  67. package/dist/cli/run.d.ts +8 -0
  68. package/dist/cli/run.d.ts.map +1 -1
  69. package/dist/cli/run.js +106 -17
  70. package/dist/cli/run.js.map +1 -1
  71. package/dist/cli/step-failure.d.ts.map +1 -1
  72. package/dist/cli/step-failure.js +18 -0
  73. package/dist/cli/step-failure.js.map +1 -1
  74. package/dist/cloud-run.d.ts.map +1 -1
  75. package/dist/cloud-run.js +9 -2
  76. package/dist/cloud-run.js.map +1 -1
  77. package/dist/communication/local.d.ts +1 -1
  78. package/dist/communication/local.d.ts.map +1 -1
  79. package/dist/communication/local.js +2 -2
  80. package/dist/communication/local.js.map +1 -1
  81. package/dist/communication/worker.d.ts +1 -1
  82. package/dist/communication/worker.d.ts.map +1 -1
  83. package/dist/communication/worker.js +27 -9
  84. package/dist/communication/worker.js.map +1 -1
  85. package/dist/compile.d.ts.map +1 -1
  86. package/dist/compile.js +16 -7
  87. package/dist/compile.js.map +1 -1
  88. package/dist/decimal-dollars.d.ts +12 -0
  89. package/dist/decimal-dollars.d.ts.map +1 -0
  90. package/dist/decimal-dollars.js +39 -0
  91. package/dist/decimal-dollars.js.map +1 -0
  92. package/dist/effect-channel.d.ts.map +1 -1
  93. package/dist/effect-channel.js +2 -0
  94. package/dist/effect-channel.js.map +1 -1
  95. package/dist/failure-kinds.d.ts +9 -1
  96. package/dist/failure-kinds.d.ts.map +1 -1
  97. package/dist/failure-kinds.js +6 -0
  98. package/dist/failure-kinds.js.map +1 -1
  99. package/dist/flow-extension-loader.d.ts +1 -1
  100. package/dist/flow-extension-loader.d.ts.map +1 -1
  101. package/dist/flow-extension-loader.js +143 -14
  102. package/dist/flow-extension-loader.js.map +1 -1
  103. package/dist/flow-extension-manifest.d.ts +1 -0
  104. package/dist/flow-extension-manifest.d.ts.map +1 -1
  105. package/dist/flow-extension-manifest.js +12 -3
  106. package/dist/flow-extension-manifest.js.map +1 -1
  107. package/dist/helper-instruction.d.ts +3 -0
  108. package/dist/helper-instruction.d.ts.map +1 -0
  109. package/dist/helper-instruction.js +3 -0
  110. package/dist/helper-instruction.js.map +1 -0
  111. package/dist/hosted-extension-manifest.d.ts.map +1 -1
  112. package/dist/hosted-extension-manifest.js +25 -4
  113. package/dist/hosted-extension-manifest.js.map +1 -1
  114. package/dist/hosted-extension-runtime.js +1 -1
  115. package/dist/hosted-extension-sandbox.js +2 -2
  116. package/dist/journal-client.d.ts +5 -2
  117. package/dist/journal-client.d.ts.map +1 -1
  118. package/dist/journal-client.js +12 -4
  119. package/dist/journal-client.js.map +1 -1
  120. package/dist/llm-worker.d.ts +2 -1
  121. package/dist/llm-worker.d.ts.map +1 -1
  122. package/dist/llm-worker.js +10 -3
  123. package/dist/llm-worker.js.map +1 -1
  124. package/dist/local-agent-environment.d.ts +13 -0
  125. package/dist/local-agent-environment.d.ts.map +1 -0
  126. package/dist/local-agent-environment.js +104 -0
  127. package/dist/local-agent-environment.js.map +1 -0
  128. package/dist/local-agent.d.ts +1 -1
  129. package/dist/local-agent.d.ts.map +1 -1
  130. package/dist/local-agent.js +2 -2
  131. package/dist/local-agent.js.map +1 -1
  132. package/dist/mcp-stdio.d.ts +2 -1
  133. package/dist/mcp-stdio.d.ts.map +1 -1
  134. package/dist/mcp-stdio.js +38 -21
  135. package/dist/mcp-stdio.js.map +1 -1
  136. package/dist/preflight.d.ts +2 -0
  137. package/dist/preflight.d.ts.map +1 -1
  138. package/dist/preflight.js.map +1 -1
  139. package/dist/protocol.d.ts +30 -0
  140. package/dist/protocol.d.ts.map +1 -1
  141. package/dist/reported-cost-total.d.ts +41 -0
  142. package/dist/reported-cost-total.d.ts.map +1 -0
  143. package/dist/reported-cost-total.js +63 -0
  144. package/dist/reported-cost-total.js.map +1 -0
  145. package/dist/reported-cost.d.ts +24 -0
  146. package/dist/reported-cost.d.ts.map +1 -0
  147. package/dist/reported-cost.js +49 -0
  148. package/dist/reported-cost.js.map +1 -0
  149. package/dist/resolved-cli-identity.d.ts +14 -0
  150. package/dist/resolved-cli-identity.d.ts.map +1 -0
  151. package/dist/resolved-cli-identity.js +19 -0
  152. package/dist/resolved-cli-identity.js.map +1 -0
  153. package/dist/run-state.d.ts +9 -11
  154. package/dist/run-state.d.ts.map +1 -1
  155. package/dist/run-state.js +15 -38
  156. package/dist/run-state.js.map +1 -1
  157. package/dist/worker-cli.d.ts +3 -1
  158. package/dist/worker-cli.d.ts.map +1 -1
  159. package/dist/worker-cli.js +118 -42
  160. package/dist/worker-cli.js.map +1 -1
  161. package/dist/worker-lease.d.ts +3 -1
  162. package/dist/worker-lease.d.ts.map +1 -1
  163. package/dist/worker-lease.js +47 -9
  164. package/dist/worker-lease.js.map +1 -1
  165. package/dist/worker.d.ts +2 -0
  166. package/dist/worker.d.ts.map +1 -1
  167. package/dist/worker.js +8 -3
  168. package/dist/worker.js.map +1 -1
  169. package/dist/wrapper-session.d.ts +3 -1
  170. package/dist/wrapper-session.d.ts.map +1 -1
  171. package/dist/wrapper-session.js +93 -53
  172. package/dist/wrapper-session.js.map +1 -1
  173. package/dist/yaml-helper-effect.d.ts.map +1 -1
  174. package/dist/yaml-helper-effect.js +2 -1
  175. package/dist/yaml-helper-effect.js.map +1 -1
  176. package/dist/yaml-helpers.d.ts.map +1 -1
  177. package/dist/yaml-helpers.js +2 -1
  178. package/dist/yaml-helpers.js.map +1 -1
  179. package/package.json +2 -2
  180. package/src/agent-reaper.ts +7 -4
  181. package/src/authored-budget.ts +73 -1
  182. package/src/authored-flow-error.ts +6 -0
  183. package/src/authored-flow-executor.ts +4 -1
  184. package/src/authored-helper-effect.ts +3 -2
  185. package/src/authored-mcp.ts +2 -0
  186. package/src/authored-node-entry.ts +8 -1
  187. package/src/authored-node-runner.ts +5 -3
  188. package/src/authored-plugin-effect.ts +2 -0
  189. package/src/authored-preflight.ts +2 -2
  190. package/src/authored-root.ts +157 -7
  191. package/src/authored-verdict.ts +2 -1
  192. package/src/authored-worker-step.ts +3 -1
  193. package/src/child-stop.ts +40 -1
  194. package/src/cli/add-extension.ts +1 -1
  195. package/src/cli/check.ts +31 -7
  196. package/src/cli/cli-probe.ts +80 -42
  197. package/src/cli/direct-run.ts +11 -2
  198. package/src/cli/pinned-cli-alias.ts +74 -0
  199. package/src/cli/run.ts +118 -14
  200. package/src/cli/step-failure.ts +18 -0
  201. package/src/cloud-run.ts +9 -2
  202. package/src/communication/local.ts +3 -2
  203. package/src/communication/worker.ts +29 -11
  204. package/src/compile.ts +28 -13
  205. package/src/decimal-dollars.ts +38 -0
  206. package/src/effect-channel.ts +2 -0
  207. package/src/failure-kinds.ts +14 -0
  208. package/src/flow-extension-loader.ts +154 -14
  209. package/src/flow-extension-manifest.ts +6 -3
  210. package/src/helper-instruction.ts +2 -0
  211. package/src/hosted-extension-manifest.ts +28 -5
  212. package/src/hosted-extension-runtime.ts +1 -1
  213. package/src/hosted-extension-sandbox.ts +2 -2
  214. package/src/journal-client.ts +14 -5
  215. package/src/llm-worker.ts +11 -4
  216. package/src/local-agent-environment.ts +92 -0
  217. package/src/local-agent.ts +2 -1
  218. package/src/mcp-stdio.ts +34 -20
  219. package/src/preflight.ts +2 -0
  220. package/src/protocol.ts +31 -0
  221. package/src/reported-cost-total.ts +82 -0
  222. package/src/reported-cost.ts +51 -0
  223. package/src/resolved-cli-identity.ts +42 -0
  224. package/src/run-state.ts +25 -37
  225. package/src/worker-cli.ts +126 -39
  226. package/src/worker-lease.ts +50 -9
  227. package/src/worker.ts +14 -5
  228. package/src/wrapper-session.ts +89 -56
  229. package/src/yaml-helper-effect.ts +2 -1
  230. package/src/yaml-helpers.ts +2 -1
package/src/mcp-stdio.ts CHANGED
@@ -13,8 +13,9 @@ export class McpStdioTransport implements Transport {
13
13
  onmessage?: Transport['onmessage'];
14
14
  private child?: ChildProcessWithoutNullStreams;
15
15
  private stopTree?: ChildStop;
16
+ private childClosed = false;
16
17
  private readonly buffer = new ReadBuffer({ maxBufferSize: 1_048_576 });
17
- private closed?: Promise<void>;
18
+ private stopped?: Promise<void>;
18
19
  private closing?: Promise<void>;
19
20
  constructor(private readonly config: Extract<McpServerConfig, { command: string }>) {}
20
21
 
@@ -23,11 +24,24 @@ export class McpStdioTransport implements Transport {
23
24
  for (const name of this.config.env ?? []) {
24
25
  if (process.env[name] !== undefined) env[name] = process.env[name];
25
26
  }
27
+ const ownsGroup = process.platform !== 'win32';
26
28
  const child = this.child = spawn(this.config.command, this.config.args ?? [], {
27
- env, stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32',
29
+ env, stdio: ['pipe', 'pipe', 'pipe'], detached: ownsGroup,
30
+ });
31
+ let resolveStopped!: () => void;
32
+ let rejectStopped!: (error: Error) => void;
33
+ this.stopped = new Promise<void>((resolve, reject) => {
34
+ resolveStopped = resolve;
35
+ rejectStopped = reject;
36
+ });
37
+ this.stopTree = childStop(child, ownsGroup, undefined, error => {
38
+ if (error === undefined) resolveStopped();
39
+ else rejectStopped(error);
40
+ });
41
+ child.once('close', () => {
42
+ this.childClosed = true;
43
+ this.stopTree?.maySettleOnChildExit();
28
44
  });
29
- this.stopTree = childStop(child, process.platform !== 'win32');
30
- this.closed = new Promise(resolve => child.once('close', () => resolve()));
31
45
  child.stderr.resume();
32
46
  child.stdout.on('data', (chunk: Buffer) => {
33
47
  if (this.closing) return;
@@ -63,22 +77,22 @@ export class McpStdioTransport implements Transport {
63
77
  if (!child) return;
64
78
  child.stdin.end();
65
79
  this.stopTree!.terminate();
66
- // A direct-child close is insufficient: wrappers can leave descendants
67
- // alive with either inherited pipes or completely detached stdio.
68
- let timer: ReturnType<typeof setTimeout> | undefined;
69
- const deadline = new Promise<void>(resolve => { timer = setTimeout(resolve, 1000); });
70
- await Promise.race([this.closed, deadline]);
71
- if (!this.stopTree!.maySettleOnChildExit()) await deadline;
72
- clearTimeout(timer);
73
- if (child.exitCode === null && child.signalCode === null && child.pid !== undefined) {
74
- const exited = new Promise<void>(resolve => child.once('exit', () => resolve()));
75
- this.stopTree!.kill();
76
- await exited;
80
+ // `close()` may be called after the direct child already emitted close and
81
+ // resolved `stopped`. Refund the just-armed escalation in that case before
82
+ // awaiting the already-settled promise; otherwise its referenced timer can
83
+ // fire later against a captured, potentially reused process-group id.
84
+ if (this.childClosed) this.stopTree!.maySettleOnChildExit();
85
+ try {
86
+ // A direct-child close is insufficient: wrappers can leave descendants
87
+ // alive with either inherited pipes or completely detached stdio. The
88
+ // shared stop owns the bound and rejects when group death is unprovable.
89
+ await this.stopped;
90
+ } finally {
91
+ child.stdin.destroy();
92
+ child.stdout.destroy();
93
+ child.stderr.destroy();
94
+ this.buffer.clear();
95
+ this.onclose?.();
77
96
  }
78
- child.stdin.destroy();
79
- child.stdout.destroy();
80
- child.stderr.destroy();
81
- this.buffer.clear();
82
- this.onclose?.();
83
97
  }
84
98
  }
package/src/preflight.ts CHANGED
@@ -35,6 +35,8 @@ export interface CliResolution {
35
35
  export interface CliProbeResult {
36
36
  exists: boolean;
37
37
  authenticated: boolean | 'unverified';
38
+ /** Absolute executable selected once for both this probe and later dispatch. */
39
+ executable?: string;
38
40
  /** False when a custom executable did not identify as a wrapper adapter. */
39
41
  supported?: boolean;
40
42
  /** Exact declared model passed the CLI's model-scoped readiness probe. */
package/src/protocol.ts CHANGED
@@ -79,6 +79,11 @@ export interface HelloParams {
79
79
  export interface HelloResult {
80
80
  protocol: 0;
81
81
  server: string;
82
+ /**
83
+ * Additive request capabilities within protocol 0. Absent from daemons that
84
+ * predate it, which then accept none of the optional fields it gates.
85
+ */
86
+ features?: string[];
82
87
  }
83
88
 
84
89
  export interface StepSpend {
@@ -97,6 +102,21 @@ export interface StepSpend {
97
102
  * `dollars_unmetered: true` and no dollar amount, so unknown cost is never
98
103
  * journaled as a measured $0. See `workerSpend` for the full contract.
99
104
  */
105
+ /**
106
+ * A step attempt's actual cost (`reported-cost.ts`), journaled beside the
107
+ * metered `usage` as `reported_cost`. Display only: the kernel never charges it
108
+ * against `maxDollars`.
109
+ */
110
+ export interface ReportedCost {
111
+ /** Non-negative decimal string, six places. */
112
+ dollars: string;
113
+ /**
114
+ * `cli`: the CLI's own reported total. `priced`: estimated from full token
115
+ * usage. `no_model`: an internal effect worker completed the step; $0.
116
+ */
117
+ source: 'cli' | 'priced' | 'no_model';
118
+ }
119
+
100
120
  export type StepUsage =
101
121
  | { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: never }
102
122
  | { tokens_in: number; tokens_out: number; dollars_unmetered: true; dollars?: never };
@@ -233,6 +253,8 @@ export interface StepDispatchEvent {
233
253
  lease_id: string;
234
254
  idempotency_key: string;
235
255
  pins: Pins;
256
+ /** Remaining lease duration at dispatch; `lease_deadline_ms` stays absolute. */
257
+ lease_ttl_ms?: number;
236
258
  /**
237
259
  * The `wake_context` payload from the run's `subscription.matched`
238
260
  * journal entry — carries the triggering event and any epoch summary
@@ -271,6 +293,13 @@ export interface StepHeartbeatParams {
271
293
  }
272
294
  export interface StepHeartbeatResult {
273
295
  lease_deadline_ms: number;
296
+ /**
297
+ * The renewed lease as a duration on the daemon's clock. Absent from older
298
+ * daemons; a worker that has it times the lease on its own monotonic clock
299
+ * instead of comparing the absolute deadline with a wall clock that may
300
+ * disagree with the daemon's.
301
+ */
302
+ lease_ttl_ms?: number;
274
303
  }
275
304
 
276
305
  /** completionReason mirrors kernel DESIGN.md §1.3. */
@@ -342,6 +371,8 @@ export interface StepCompleteParams {
342
371
  effects?: EffectRef[];
343
372
  /** `inspect` evidence for the next attempt. Rejected over 16 KiB of JSON. */
344
373
  trajectory_tail?: unknown;
374
+ /** Display-only actual cost; never charged to the budget. */
375
+ reported_cost?: ReportedCost;
345
376
  }
346
377
  export type StepCompleteResult = RunOutcome;
347
378
 
@@ -0,0 +1,82 @@
1
+ // The display-only actual-cost fold beside run-state's metered spend.
2
+ // Nothing here is enforcement: the kernel charges `budget`, never `reported_cost`.
3
+
4
+ import { declaresAuthoredRoot } from './authored-verdict.js';
5
+ import { addDollars } from './decimal-dollars.js';
6
+ import { HELPER_INSTRUCTION_PREFIX } from './helper-instruction.js';
7
+ import type { JournalEvent } from './journal-reader.js';
8
+
9
+ type Payload = Record<string, unknown>;
10
+
11
+ /**
12
+ * What steps actually cost, summed from each attempt's journaled
13
+ * `reported_cost` (the CLI's own total, or a full-usage estimate). Display
14
+ * only: the kernel charges `spend` against `maxDollars`, never this. `complete`
15
+ * is false when some completed model attempt reported no cost (e.g. Codex, or
16
+ * an older runtime), so `dollars` is then a lower bound. Segment rollover keeps
17
+ * earlier completions in the journal, so an epoch summary does not lose any.
18
+ */
19
+ export interface ReportedCostTotal {
20
+ /** Decimal string. */
21
+ dollars: string;
22
+ complete: boolean;
23
+ /** Where the summed figures came from; `mixed` when attempts differ. */
24
+ source: 'cli' | 'priced' | 'mixed' | null;
25
+ }
26
+
27
+ /**
28
+ * Steps known to run no model, read from the spawned spec. Deliberately
29
+ * narrow: a crash-recovered or cancelled model attempt journals a default zero
30
+ * budget although the model ran, so metering nothing proves nothing. A step
31
+ * not listed here that reports no cost stays unknown, and the total reads as a
32
+ * lower bound rather than a false complete figure.
33
+ * - a deterministic step runs a command;
34
+ * - a provider helper (`relayflows:helper:v1`) performs a relayfile write;
35
+ * - the authored root runs the flow body, whose model calls are child runs with
36
+ * journals of their own. Only a spec carrying the authored-root discriminator
37
+ * counts: an ordinary flow may name a step `authored-root`.
38
+ * A memoized reuse is free too, per completion (`reused_from`): the source run
39
+ * paid for the model.
40
+ */
41
+ export function modelFreeSteps(spawned: JournalEvent): Set<string> {
42
+ const payload = spawned.payload !== null && typeof spawned.payload === 'object' ? spawned.payload as Payload : {};
43
+ const spec = payload['spec'] !== null && typeof payload['spec'] === 'object' ? payload['spec'] as Payload : {};
44
+ const steps = Array.isArray(spec['steps']) ? spec['steps'] as unknown[] : [];
45
+ const authoredRoot = declaresAuthoredRoot(spawned);
46
+ const free = new Set<string>();
47
+ for (const entry of steps) {
48
+ const step = entry !== null && typeof entry === 'object' ? entry as Payload : {};
49
+ if (typeof step['id'] !== 'string') continue;
50
+ const helper = step['type'] === 'agent' && typeof step['instruction'] === 'string'
51
+ && step['instruction'].startsWith(HELPER_INSTRUCTION_PREFIX);
52
+ if (step['type'] === 'deterministic' || helper || (authoredRoot && step['id'] === 'authored-root')) free.add(step['id']);
53
+ }
54
+ return free;
55
+ }
56
+
57
+ export const NO_REPORTED_COST: ReportedCostTotal = { dollars: '0', complete: true, source: null };
58
+
59
+
60
+ /**
61
+ * Add one completed attempt's journaled `reported_cost` to a total. A model
62
+ * attempt without one (older runtime, Codex, unpriced) leaves the sum unchanged
63
+ * and marks it incomplete rather than counting an unknown cost as zero.
64
+ */
65
+ export function addReportedCost(total: ReportedCostTotal, completion: Payload, modelFree: boolean): ReportedCostTotal {
66
+ const charge = completion['reported_cost'];
67
+ const cost = charge !== null && typeof charge === 'object' && !Array.isArray(charge) ? charge as Payload : null;
68
+ const reused = completion['reused_from'] !== undefined && completion['reused_from'] !== null;
69
+ if (cost === null && (modelFree || reused)) return total;
70
+ const dollars = cost === null ? null : cost['dollars'];
71
+ const source = cost === null ? null : cost['source'];
72
+ // An internal effect worker ran no model: a known $0 that names no cost source.
73
+ if (source === 'no_model') return total;
74
+ if (typeof dollars !== 'string' || !/^\d+(?:\.\d+)?$/.test(dollars) || (source !== 'cli' && source !== 'priced')) {
75
+ return { ...total, complete: false };
76
+ }
77
+ return {
78
+ dollars: addDollars(total.dollars, dollars),
79
+ complete: total.complete,
80
+ source: total.source === null || total.source === source ? source : 'mixed',
81
+ };
82
+ }
@@ -0,0 +1,51 @@
1
+ import { MODEL_PRICING } from './model-pricing.js';
2
+ import type { ReportedCost } from './protocol.js';
3
+ import type { WorkerCliResult } from './worker-cli.js';
4
+
5
+ /**
6
+ * What a step attempt actually cost, for display. Never enforcement.
7
+ *
8
+ * The metered `usage` (`workerSpend`) is what the kernel charges against
9
+ * `maxDollars`, and it prices only plain input and output tokens. That
10
+ * undercounts cache-heavy agents badly. This is the real figure a reader wants
11
+ * to see, and it rides beside the metered charge as `reported_cost`: the kernel
12
+ * journals it and never folds it into the budget. Changing which charges count
13
+ * toward a cap is a separate decision, and this module takes no part in it.
14
+ *
15
+ * Sources, in order:
16
+ * - `cli`: the CLI's own reported total (Claude `total_cost_usd`);
17
+ * - `priced`: an estimate from the digest's full token usage at the frozen
18
+ * `MODEL_PRICING` rates, with cache writes at 1.25x and cache reads at 0.1x
19
+ * the input rate (Codex's input total already includes its cached tokens, so
20
+ * they are split out, not added). Only for a model that already has a price.
21
+ * Otherwise undefined: an unknown cost is left unknown, never shown as $0.
22
+ */
23
+ export function reportedCost(result: WorkerCliResult, model: string | undefined): ReportedCost | undefined {
24
+ const digest = result.transcript?.result;
25
+ const cli = digest?.total_cost_usd;
26
+ if (typeof cli === 'number' && Number.isFinite(cli) && cli >= 0) {
27
+ return { dollars: microDollars(BigInt(Math.round(cli * 1_000_000))), source: 'cli' };
28
+ }
29
+ const usage = digest?.usage;
30
+ const priceModel = digest?.model ?? model;
31
+ if (usage === undefined || priceModel === undefined || !Object.hasOwn(MODEL_PRICING, priceModel)) return undefined;
32
+ const counts = [usage.input, usage.output, usage.cache_read, usage.cache_creation].map(n => n ?? 0);
33
+ if (!counts.every(n => Number.isSafeInteger(n) && n >= 0)) return undefined;
34
+ const [reportedInput, output, cacheRead, cacheCreation] = counts.map(BigInt) as [bigint, bigint, bigint, bigint];
35
+ // Claude reports cache tokens beside `input_tokens`; Codex's `input_tokens`
36
+ // already includes its `cached_input_tokens`. Charge each token once.
37
+ const input = digest?.provider === 'codex'
38
+ ? (reportedInput > cacheRead ? reportedInput - cacheRead : 0n) : reportedInput;
39
+ const price = MODEL_PRICING[priceModel]!;
40
+ const inRate = BigInt(price.input);
41
+ const micro = input * inRate + output * BigInt(price.output)
42
+ + (cacheCreation * inRate * 125n) / 100n + (cacheRead * inRate * 10n) / 100n;
43
+ return { dollars: microDollars(micro), source: 'priced' };
44
+ }
45
+
46
+ /** What an internal effect worker (provider helper, plugin, MCP) reports: no model ran. */
47
+ export const NO_MODEL_COST: ReportedCost = Object.freeze({ dollars: '0.000000', source: 'no_model' });
48
+
49
+ function microDollars(micro: bigint): string {
50
+ return `${micro / 1_000_000n}.${String(micro % 1_000_000n).padStart(6, '0')}`;
51
+ }
@@ -0,0 +1,42 @@
1
+ import type {
2
+ FlowSpec,
3
+ KernelAgentStep,
4
+ KernelLlmStep,
5
+ KernelStepCommon,
6
+ KernelStepSpec,
7
+ } from './spec.js';
8
+
9
+ interface ResolvedCliIdentityField {
10
+ /** Host-proved authored identity used to select the adapter for canonical CLI bytes. */
11
+ cli_identity?: string;
12
+ }
13
+
14
+ export type ResolvedKernelStepCommon = KernelStepCommon & ResolvedCliIdentityField;
15
+ export type ResolvedKernelAgentStep = KernelAgentStep & ResolvedCliIdentityField;
16
+ export type ResolvedKernelLlmStep = KernelLlmStep & ResolvedCliIdentityField;
17
+ export type ResolvedKernelStepSpec = KernelStepSpec & ResolvedCliIdentityField;
18
+
19
+ /**
20
+ * Host-proved CLI identities are deliberately out-of-band from the authoring
21
+ * schema. A flow cannot claim that a canonical `cli.js` target is Claude; only
22
+ * the successful preflight that resolved the authored `claude` declaration
23
+ * can attach that identity before kernel lowering.
24
+ */
25
+ const IDENTITIES = new WeakMap<object, ReadonlyMap<string, string>>();
26
+
27
+ export function resolvedCliIdentities(value: unknown): ReadonlyMap<string, string> | undefined {
28
+ return typeof value === 'object' && value !== null ? IDENTITIES.get(value) : undefined;
29
+ }
30
+
31
+ export function rememberResolvedCliIdentities<T extends FlowSpec>(
32
+ flow: T,
33
+ identities: ReadonlyMap<string, string>,
34
+ ): T {
35
+ IDENTITIES.set(flow, new Map(identities));
36
+ return flow;
37
+ }
38
+
39
+ export function inheritResolvedCliIdentities<T extends FlowSpec>(source: unknown, flow: T): T {
40
+ const identities = resolvedCliIdentities(source);
41
+ return identities === undefined ? flow : rememberResolvedCliIdentities(flow, identities);
42
+ }
package/src/run-state.ts CHANGED
@@ -10,8 +10,13 @@
10
10
  // No I/O, no clock: the caller passes `now_ms`, so a test folds a hand-built
11
11
  // journal against a fixed instant and asserts exact elapsed and overdue values.
12
12
 
13
+ import { addDollars } from './decimal-dollars.js';
14
+ import { addReportedCost, modelFreeSteps, NO_REPORTED_COST, type ReportedCostTotal } from './reported-cost-total.js';
13
15
  import type { JournalEvent } from './journal-reader.js';
14
16
 
17
+ export { addDollars } from './decimal-dollars.js';
18
+ export type { ReportedCostTotal } from './reported-cost-total.js';
19
+
15
20
  export type RunStatus = 'running' | 'parked' | 'completed' | 'failed' | 'cancelling' | 'cancelled';
16
21
 
17
22
  /** `StepState` variant names from state.rs, snake_case. */
@@ -25,6 +30,7 @@ export interface Spend {
25
30
  dollars_unmetered: boolean;
26
31
  }
27
32
 
33
+
28
34
  export interface StepCounts {
29
35
  total: number;
30
36
  done: number;
@@ -91,6 +97,10 @@ export interface StepView {
91
97
  completion_reason: string | null;
92
98
  /** Paths the worker journaled for the last attempt, when its output carried them. */
93
99
  artifacts: { paths: string[]; journaled: boolean };
100
+ /** Metered charges of this step's attempts: what counted toward the budget. */
101
+ spend: Spend;
102
+ /** Actual cost of this step's attempts. Null until an attempt completes. */
103
+ reported_cost: ReportedCostTotal | null;
94
104
  }
95
105
 
96
106
  export interface RunView {
@@ -101,6 +111,8 @@ export interface RunView {
101
111
  spawned_at_ms: number;
102
112
  now_ms: number;
103
113
  spend: Spend;
114
+ /** Actual cost of this run's own attempts; see {@link ReportedCostTotal}. */
115
+ reported_cost: ReportedCostTotal;
104
116
  counts: StepCounts;
105
117
  steps: StepView[];
106
118
  }
@@ -127,42 +139,6 @@ function text(value: unknown, fallback = ''): string {
127
139
  return typeof value === 'string' ? value : fallback;
128
140
  }
129
141
 
130
- /**
131
- * Sum two decimal dollar strings exactly, at whatever scale they carry — the
132
- * kernel's budget fold (`relayflowd-core/src/state/budget.rs`) aligns every
133
- * fractional digit before adding, and it accepts arbitrary precision. Scaling
134
- * to a fixed six fractional digits dropped a charge of `0.0000001` to zero, so
135
- * `flows status` underreported spend the kernel had totalled exactly.
136
- *
137
- * A value that is not a decimal number is left out of the sum rather than
138
- * counted as zero, and the caller is told by `malformed`.
139
- */
140
- export function addDollars(left: string, right: string): string {
141
- const parsed = [left, right].map((value) => /^\d+(?:\.\d+)?$/.test(value) ? value : null);
142
- const usable = parsed.filter((value): value is string => value !== null);
143
- if (usable.length === 0) return '0';
144
- if (usable.length === 1) return normalizeDollars(usable[0]!);
145
- const scale = Math.max(...usable.map((value) => (value.split('.')[1] ?? '').length));
146
- const scaled = usable.map((value) => {
147
- const [whole, fraction = ''] = value.split('.');
148
- return BigInt(`${whole}${fraction.padEnd(scale, '0')}`);
149
- });
150
- return fromScaled(scaled[0]! + scaled[1]!, scale);
151
- }
152
-
153
- /** `12.3400` -> `12.34`, `0.000` -> `0`; the journal's own spelling otherwise. */
154
- function normalizeDollars(value: string): string {
155
- const [whole, fraction = ''] = value.split('.');
156
- return fromScaled(BigInt(`${whole}${fraction}`), fraction.length);
157
- }
158
-
159
- function fromScaled(total: bigint, scale: number): string {
160
- if (scale === 0) return String(total);
161
- const digits = String(total).padStart(scale + 1, '0');
162
- const whole = digits.slice(0, digits.length - scale);
163
- const fraction = digits.slice(digits.length - scale).replace(/0+$/, '');
164
- return fraction.length === 0 ? whole : `${whole}.${fraction}`;
165
- }
166
142
 
167
143
  /**
168
144
  * Reduce `trajectory_tail.transcript` (the digest flows#491 journals) to the
@@ -209,6 +185,7 @@ function addSpend(total: Spend, charge: unknown): Spend {
209
185
 
210
186
  const ZERO_SPEND: Spend = { tokens_in: 0, tokens_out: 0, dollars: '0', dollars_unmetered: false };
211
187
 
188
+
212
189
  interface StepFold {
213
190
  view: StepView;
214
191
  depends_on: string[];
@@ -221,6 +198,7 @@ function freshStep(id: string, type: string, maxIterations: number | null, depen
221
198
  id, type, state: 'pending', attempt: 0, max_iterations: maxIterations,
222
199
  started_at_ms: null, elapsed_ms: null, lease: null, backoff_until_ms: null, wait: null,
223
200
  last_attempt: null, completion_reason: null, artifacts: { paths: [], journaled: false },
201
+ spend: ZERO_SPEND, reported_cost: null,
224
202
  },
225
203
  };
226
204
  }
@@ -268,6 +246,8 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
268
246
  // An authored child run carries its parent's totals so ceilings hold across
269
247
  // the family (state.rs `prior_spend`); the view reports the same running total.
270
248
  let spend = addSpend(ZERO_SPEND, budget['prior_spend']);
249
+ let reportedCost = NO_REPORTED_COST;
250
+ const modelFree = modelFreeSteps(first);
271
251
  let completion: string | null = null;
272
252
  let cancelRequested = false;
273
253
 
@@ -297,6 +277,9 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
297
277
  const attempt = event.attempt ?? view.attempt;
298
278
  view.attempt = Math.max(view.attempt, attempt);
299
279
  spend = addSpend(spend, payload['budget']);
280
+ view.spend = addSpend(view.spend, payload['budget']);
281
+ reportedCost = addReportedCost(reportedCost, payload, modelFree.has(view.id));
282
+ view.reported_cost = addReportedCost(view.reported_cost ?? NO_REPORTED_COST, payload, modelFree.has(view.id));
300
283
  const verification = payload['verification'] !== null && typeof payload['verification'] === 'object'
301
284
  ? payload['verification'] as Payload : undefined;
302
285
  view.last_attempt = {
@@ -362,8 +345,12 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
362
345
  // A new epoch restarts every step (state.rs `apply_epoch`); the
363
346
  // summary then restores the ones it carries forward.
364
347
  for (const step of steps.values()) {
365
- const { id, type, max_iterations } = step.view;
348
+ const { id, type, max_iterations, spend: stepSpend, reported_cost: stepCost } = step.view;
366
349
  step.view = freshStep(id, type, max_iterations, step.depends_on).view;
350
+ // Rollover keeps the earlier segments' completions in the journal,
351
+ // and this fold has already read them: what the step cost stays.
352
+ step.view.spend = stepSpend;
353
+ step.view.reported_cost = stepCost;
367
354
  }
368
355
  spend = addSpend(ZERO_SPEND, payload['budget_spent']);
369
356
  const done = payload['steps_done'] !== null && typeof payload['steps_done'] === 'object' ? payload['steps_done'] as Payload : {};
@@ -462,6 +449,7 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
462
449
  spawned_at_ms: first.at_ms,
463
450
  now_ms,
464
451
  spend,
452
+ reported_cost: reportedCost,
465
453
  counts,
466
454
  steps: views,
467
455
  };