@relayflows/sdk 2.0.11 → 2.0.12

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 (156) hide show
  1. package/dist/adapters/base.d.ts +6 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +1 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/authored-admission.d.ts +5 -0
  7. package/dist/authored-admission.d.ts.map +1 -0
  8. package/dist/authored-admission.js +14 -0
  9. package/dist/authored-admission.js.map +1 -0
  10. package/dist/authored-budget.d.ts +1 -1
  11. package/dist/authored-budget.d.ts.map +1 -1
  12. package/dist/authored-budget.js +3 -3
  13. package/dist/authored-budget.js.map +1 -1
  14. package/dist/authored-flow-error.d.ts +1 -1
  15. package/dist/authored-flow-error.d.ts.map +1 -1
  16. package/dist/authored-flow-error.js.map +1 -1
  17. package/dist/authored-flow-executor.d.ts +10 -0
  18. package/dist/authored-flow-executor.d.ts.map +1 -1
  19. package/dist/authored-flow-executor.js +10 -7
  20. package/dist/authored-flow-executor.js.map +1 -1
  21. package/dist/authored-flow-loader.d.ts +20 -0
  22. package/dist/authored-flow-loader.d.ts.map +1 -1
  23. package/dist/authored-flow-loader.js +86 -11
  24. package/dist/authored-flow-loader.js.map +1 -1
  25. package/dist/authored-helper-effect.d.ts +1 -1
  26. package/dist/authored-helper-effect.d.ts.map +1 -1
  27. package/dist/authored-helper-effect.js +5 -3
  28. package/dist/authored-helper-effect.js.map +1 -1
  29. package/dist/authored-mcp.d.ts +1 -1
  30. package/dist/authored-mcp.d.ts.map +1 -1
  31. package/dist/authored-mcp.js +7 -4
  32. package/dist/authored-mcp.js.map +1 -1
  33. package/dist/authored-node-entry.d.ts +2 -0
  34. package/dist/authored-node-entry.d.ts.map +1 -0
  35. package/dist/authored-node-entry.js +111 -0
  36. package/dist/authored-node-entry.js.map +1 -0
  37. package/dist/authored-node-runner.d.ts +11 -0
  38. package/dist/authored-node-runner.d.ts.map +1 -0
  39. package/dist/authored-node-runner.js +246 -0
  40. package/dist/authored-node-runner.js.map +1 -0
  41. package/dist/authored-plugin-effect.d.ts +1 -1
  42. package/dist/authored-plugin-effect.d.ts.map +1 -1
  43. package/dist/authored-plugin-effect.js +7 -4
  44. package/dist/authored-plugin-effect.js.map +1 -1
  45. package/dist/authored-root.d.ts +38 -0
  46. package/dist/authored-root.d.ts.map +1 -0
  47. package/dist/authored-root.js +329 -0
  48. package/dist/authored-root.js.map +1 -0
  49. package/dist/authored-runtime-capability.d.ts +6 -0
  50. package/dist/authored-runtime-capability.d.ts.map +1 -0
  51. package/dist/authored-runtime-capability.js +40 -0
  52. package/dist/authored-runtime-capability.js.map +1 -0
  53. package/dist/authored-source-authority.d.ts +4 -0
  54. package/dist/authored-source-authority.d.ts.map +1 -0
  55. package/dist/authored-source-authority.js +61 -0
  56. package/dist/authored-source-authority.js.map +1 -0
  57. package/dist/authored-worker-step.d.ts +2 -2
  58. package/dist/authored-worker-step.d.ts.map +1 -1
  59. package/dist/authored-worker-step.js +11 -6
  60. package/dist/authored-worker-step.js.map +1 -1
  61. package/dist/budget-preflight.d.ts +17 -3
  62. package/dist/budget-preflight.d.ts.map +1 -1
  63. package/dist/budget-preflight.js +31 -24
  64. package/dist/budget-preflight.js.map +1 -1
  65. package/dist/cli/check.js +3 -1
  66. package/dist/cli/check.js.map +1 -1
  67. package/dist/cli/direct-run.d.ts.map +1 -1
  68. package/dist/cli/direct-run.js +18 -14
  69. package/dist/cli/direct-run.js.map +1 -1
  70. package/dist/cli/run.d.ts +2 -0
  71. package/dist/cli/run.d.ts.map +1 -1
  72. package/dist/cli/run.js +56 -1
  73. package/dist/cli/run.js.map +1 -1
  74. package/dist/cli-adapter.d.ts +16 -0
  75. package/dist/cli-adapter.d.ts.map +1 -1
  76. package/dist/cli-adapter.js +17 -0
  77. package/dist/cli-adapter.js.map +1 -1
  78. package/dist/cli.js +4 -4
  79. package/dist/cli.js.map +1 -1
  80. package/dist/cloud-run.d.ts +11 -1
  81. package/dist/cloud-run.d.ts.map +1 -1
  82. package/dist/cloud-run.js +65 -19
  83. package/dist/cloud-run.js.map +1 -1
  84. package/dist/failure-kinds.d.ts +7 -3
  85. package/dist/failure-kinds.d.ts.map +1 -1
  86. package/dist/failure-kinds.js +5 -1
  87. package/dist/failure-kinds.js.map +1 -1
  88. package/dist/journal-client.d.ts +1 -1
  89. package/dist/journal-client.d.ts.map +1 -1
  90. package/dist/journal-client.js +6 -2
  91. package/dist/journal-client.js.map +1 -1
  92. package/dist/llm-worker.d.ts.map +1 -1
  93. package/dist/llm-worker.js +4 -2
  94. package/dist/llm-worker.js.map +1 -1
  95. package/dist/local-agent.d.ts +1 -1
  96. package/dist/local-agent.d.ts.map +1 -1
  97. package/dist/local-agent.js +2 -2
  98. package/dist/local-agent.js.map +1 -1
  99. package/dist/model-pricing.d.ts +3 -4
  100. package/dist/model-pricing.d.ts.map +1 -1
  101. package/dist/model-pricing.js +4 -4
  102. package/dist/model-pricing.js.map +1 -1
  103. package/dist/preflight.d.ts +4 -1
  104. package/dist/preflight.d.ts.map +1 -1
  105. package/dist/preflight.js +36 -18
  106. package/dist/preflight.js.map +1 -1
  107. package/dist/protocol.d.ts +2 -0
  108. package/dist/protocol.d.ts.map +1 -1
  109. package/dist/spec.d.ts +5 -4
  110. package/dist/spec.d.ts.map +1 -1
  111. package/dist/spec.js.map +1 -1
  112. package/dist/worker-cli.d.ts.map +1 -1
  113. package/dist/worker-cli.js +6 -5
  114. package/dist/worker-cli.js.map +1 -1
  115. package/dist/worker-spend.d.ts +4 -3
  116. package/dist/worker-spend.d.ts.map +1 -1
  117. package/dist/worker-spend.js +13 -4
  118. package/dist/worker-spend.js.map +1 -1
  119. package/dist/worker.d.ts.map +1 -1
  120. package/dist/worker.js +4 -2
  121. package/dist/worker.js.map +1 -1
  122. package/package.json +2 -2
  123. package/src/adapters/base.ts +7 -0
  124. package/src/adapters/claude.ts +1 -0
  125. package/src/authored-admission.ts +17 -0
  126. package/src/authored-budget.ts +4 -3
  127. package/src/authored-flow-error.ts +0 -1
  128. package/src/authored-flow-executor.ts +27 -7
  129. package/src/authored-flow-loader.ts +96 -13
  130. package/src/authored-helper-effect.ts +5 -3
  131. package/src/authored-mcp.ts +6 -4
  132. package/src/authored-node-entry.ts +90 -0
  133. package/src/authored-node-runner.ts +219 -0
  134. package/src/authored-plugin-effect.ts +6 -3
  135. package/src/authored-root.ts +396 -0
  136. package/src/authored-runtime-capability.ts +40 -0
  137. package/src/authored-source-authority.ts +62 -0
  138. package/src/authored-worker-step.ts +15 -4
  139. package/src/budget-preflight.ts +41 -25
  140. package/src/cli/check.ts +3 -1
  141. package/src/cli/direct-run.ts +24 -16
  142. package/src/cli/run.ts +58 -1
  143. package/src/cli-adapter.ts +27 -0
  144. package/src/cli.ts +5 -5
  145. package/src/cloud-run.ts +74 -21
  146. package/src/failure-kinds.ts +5 -1
  147. package/src/journal-client.ts +6 -2
  148. package/src/llm-worker.ts +4 -2
  149. package/src/local-agent.ts +7 -2
  150. package/src/model-pricing.ts +4 -4
  151. package/src/preflight.ts +40 -18
  152. package/src/protocol.ts +2 -0
  153. package/src/spec.ts +5 -4
  154. package/src/worker-cli.ts +7 -5
  155. package/src/worker-spend.ts +15 -4
  156. package/src/worker.ts +4 -2
package/src/preflight.ts CHANGED
@@ -8,6 +8,7 @@ import type { TriggerSource } from '@relayflows/surface';
8
8
  import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
9
9
  import { compileSpec, CompileError } from './compile.js';
10
10
  import { helperCall } from './yaml-helpers.js';
11
+ import { resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
11
12
  import { isNamedGate, NAMED_GATE_FAILURE_KINDS, type NamedGateFailureKind } from './named-gates.js';
12
13
  import { compileScopes, type ScopeInput, type MountRegistry } from './scope-compiler.js';
13
14
  import { readMountRegistry } from './mount-registry.js';
@@ -22,8 +23,10 @@ export interface CliResolution {
22
23
  stepId: string;
23
24
  cli: string;
24
25
  source: CliResolutionSource;
25
- /** Model the step or selected named agent declared, probed with the CLI. */
26
+ /** Effective model probed with the CLI. */
26
27
  model?: string;
28
+ /** Exact source selected by step > named agent > adapter default priority. */
29
+ modelSource?: CliModelSource;
27
30
  }
28
31
 
29
32
  export interface CliProbeResult {
@@ -208,18 +211,17 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
208
211
  };
209
212
  }
210
213
  const diagnostics: PreflightDiagnostic[] = [];
214
+ const cliResolutionDiagnostics: PreflightDiagnostic[] = [];
211
215
  const resolutions: CliResolution[] = [];
212
216
  const resolutionByStep = new Map<string, CliResolution>();
213
217
  const cliProbeResults = new Map<string, CliProbeOutcome>();
214
218
 
215
- diagnostics.push(...unknownModelDiagnostics(compiled, options));
216
219
  diagnostics.push(...scopeDiagnostics(compiled, options));
217
220
  for (const server of new Set(options.mcpServers ?? [])) {
218
221
  if (options.mcp !== undefined && Object.hasOwn(options.mcp, server)) continue;
219
222
  diagnostics.push({ severity: 'refusal', kind: 'mcp_undeclared_server', server,
220
223
  message: `MCP server "${server}" is not declared in the nearest flows.json mcp map.` });
221
224
  }
222
- diagnostics.push(...budgetDiagnostics(compiled));
223
225
  // Resolve the complete flow before touching any environment fact. A later
224
226
  // statically unresolved CLI makes the whole submission impossible, so no
225
227
  // earlier command, provider/model, or trigger probe may run first.
@@ -228,7 +230,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
228
230
  if (step.type === 'agent' && helperCall(step) !== undefined) continue;
229
231
  const resolution = resolveCli(step, compiled, options.projectCli);
230
232
  if (resolution === undefined) {
231
- diagnostics.push({
233
+ cliResolutionDiagnostics.push({
232
234
  severity: 'refusal',
233
235
  kind: 'cli_unresolved',
234
236
  stepId: step.id,
@@ -239,7 +241,19 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
239
241
  resolutionByStep.set(step.id, resolution);
240
242
  }
241
243
  }
242
- if (diagnostics.length > 0) {
244
+ diagnostics.push(...cliResolutionDiagnostics);
245
+ if (cliResolutionDiagnostics.length > 0) {
246
+ return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
247
+ }
248
+ diagnostics.push(...unknownModelDiagnostics(compiled, options, resolutionByStep));
249
+ diagnostics.push(...budgetDiagnostics(
250
+ compiled,
251
+ new Map(resolutions.map(resolution => [resolution.stepId, {
252
+ cli: resolution.cli,
253
+ ...(resolution.model === undefined ? {} : { model: resolution.model }),
254
+ }])),
255
+ ));
256
+ if (diagnostics.some((diagnostic) => diagnostic.severity === 'refusal')) {
243
257
  return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
244
258
  }
245
259
 
@@ -311,6 +325,7 @@ function scopeDiagnostics(flow: FlowSpec, options: PreflightOptions): PreflightR
311
325
  function unknownModelDiagnostics(
312
326
  flow: FlowSpec,
313
327
  options: PreflightOptions,
328
+ resolutionByStep: ReadonlyMap<string, CliResolution> = new Map(),
314
329
  ): PreflightRefusal[] {
315
330
  const diagnostics: PreflightRefusal[] = [];
316
331
  // model_unknown is a governance check: it exists to enforce a project's
@@ -340,16 +355,21 @@ function unknownModelDiagnostics(
340
355
  }
341
356
 
342
357
  for (const step of flow.steps) {
343
- if (step.type === 'deterministic' || step.model === undefined) continue;
344
- if (isKnownModel(step.model, options.models)) continue;
345
- const resolution = resolveCli(step, flow, options.projectCli);
358
+ if (step.type === 'deterministic') continue;
359
+ const resolution = resolutionByStep.get(step.id);
360
+ // Selected named declarations were checked once above, including unused
361
+ // declarations. Other sources are step declarations or adapter defaults.
362
+ if (resolution?.modelSource === 'named') continue;
363
+ const model = resolution?.model ?? step.model;
364
+ if (model === undefined || isKnownModel(model, options.models)) continue;
365
+ if (!enforceRegistry) continue;
346
366
  diagnostics.push({
347
367
  severity: 'refusal',
348
368
  kind: 'model_unknown',
349
369
  stepId: step.id,
350
370
  ...(resolution === undefined ? {} : { cli: resolution.cli }),
351
- model: step.model,
352
- message: unknownModelMessage(step.id, step.model, resolution?.cli, options.modelRegistryPath),
371
+ model,
372
+ message: unknownModelMessage(step.id, model, resolution?.cli, options.modelRegistryPath),
353
373
  });
354
374
  }
355
375
 
@@ -402,14 +422,16 @@ function resolveCli(
402
422
  const named = step.type === 'agent' && step.agent !== undefined
403
423
  ? flow.agents?.[step.agent]
404
424
  : undefined;
405
- // Model comes only from the step or its explicitly selected declaration.
406
- // There is deliberately no flow/project or host default.
407
- const effectiveModel = step.model ?? named?.model;
408
- const model = effectiveModel !== undefined ? { model: effectiveModel } : {};
409
- if (step.cli !== undefined) return { stepId: step.id, cli: step.cli, source: 'step', ...model };
410
- if (named !== undefined) return { stepId: step.id, cli: named.cli, source: 'named', ...model };
411
- if (flow.cli !== undefined) return { stepId: step.id, cli: flow.cli, source: 'flow', ...model };
412
- if (projectCli !== undefined) return { stepId: step.id, cli: projectCli, source: 'project', ...model };
425
+ const resolved = (cli: string, source: CliResolutionSource): CliResolution => {
426
+ const effectiveModel = resolveCliModelSelection(cli, { step: step.model, named: named?.model });
427
+ return { stepId: step.id, cli, source,
428
+ ...(effectiveModel.model === undefined ? {} : { model: effectiveModel.model }),
429
+ ...(effectiveModel.source === undefined ? {} : { modelSource: effectiveModel.source }) };
430
+ };
431
+ if (step.cli !== undefined) return resolved(step.cli, 'step');
432
+ if (named !== undefined) return resolved(named.cli, 'named');
433
+ if (flow.cli !== undefined) return resolved(flow.cli, 'flow');
434
+ if (projectCli !== undefined) return resolved(projectCli, 'project');
413
435
  return undefined;
414
436
  }
415
437
 
package/src/protocol.ts CHANGED
@@ -77,6 +77,8 @@ export interface StepSpend {
77
77
  }
78
78
 
79
79
  export interface RunStartParams {
80
+ /** Caller-owned retry identity. Reuse with a different spec is refused. */
81
+ admission_key?: string;
80
82
  reuse_from_run_id?: string;
81
83
  /**
82
84
  * The kernel spec dialect — the ONE boundary shape `RunSpec::parse`
package/src/spec.ts CHANGED
@@ -238,10 +238,11 @@ export interface AgentStepSpec extends BaseStepSpec {
238
238
  /** Inert preflight declaration; overrides the flow/project CLI default. */
239
239
  cli?: string;
240
240
  /**
241
- * Model the declared CLI must use. Raw Claude/Codex adapters receive their
242
- * real model flag; an identified Relayflows wrapper receives it in its
243
- * same-process execution request. Declared here so the choice is journaled with the step
244
- * instead of being ambient host state.
241
+ * Model the declared CLI must use. A step declaration wins over its selected
242
+ * named agent and any registered adapter default. Raw Claude/Codex adapters
243
+ * receive the effective model as a real flag; an identified Relayflows
244
+ * wrapper receives an explicitly declared model in its same-process request.
245
+ * The effective choice is journaled instead of being ambient host state.
245
246
  */
246
247
  model?: string;
247
248
  surfaces?: AgentSurfaces;
package/src/worker-cli.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  agentExecution,
7
7
  llmExecution,
8
8
  cliAdapterKind,
9
+ resolveCliModel,
9
10
  type CliInvocation,
10
11
  type CliAdapterKind,
11
12
  } from './cli-adapter.js';
@@ -68,9 +69,10 @@ export async function runAgentCli(
68
69
  throw new Error('Lease-bound agent execution requires macOS or Linux process-group cancellation; Windows is unsupported.');
69
70
  }
70
71
  const kind = cliAdapterKind(cli);
72
+ const effectiveModel = resolveCliModel(cli, model);
71
73
 
72
74
  if (mode === 'agent' && transport === 'relay') {
73
- return runViaAgentRelay(kind, instruction, wakeContext, model, relayContext, cwd, signal);
75
+ return runViaAgentRelay(kind, instruction, wakeContext, effectiveModel, relayContext, cwd, signal);
74
76
  }
75
77
 
76
78
  if (kind === 'relayflows-wrapper-v1') {
@@ -78,17 +80,17 @@ export async function runAgentCli(
78
80
  cli,
79
81
  instruction,
80
82
  wakeContext,
81
- model,
83
+ effectiveModel,
82
84
  wrapperEnvironment(process.env),
83
85
  wrapperLimits,
84
86
  signal,
85
- )), model);
87
+ )), effectiveModel);
86
88
  }
87
89
 
88
90
  const env: NodeJS.ProcessEnv = { ...process.env };
89
91
  delete env[WAKE_CONTEXT_ENV];
90
92
  delete env[MODEL_ENV];
91
- const invocation = mode === 'llm' ? llmExecution(kind, instruction, model) : agentExecution(kind, instruction, model);
93
+ const invocation = mode === 'llm' ? llmExecution(kind, instruction, effectiveModel) : agentExecution(kind, instruction, effectiveModel);
92
94
 
93
95
  if (wakeContext !== undefined) {
94
96
  try {
@@ -106,7 +108,7 @@ export async function runAgentCli(
106
108
  // Structured provider output carries the authoritative token counts.
107
109
  const args = [...invocation.args];
108
110
  args.splice(args.length - 1, 0, ...(kind === 'claude' ? ['--output-format', 'json'] : ['--json']));
109
- return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd), kind), model);
111
+ return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd), kind), effectiveModel);
110
112
  }
111
113
 
112
114
  /** Wait under the same worker lease for an authoritative task receipt. */
@@ -1,17 +1,28 @@
1
1
  import { pricedUsage } from './model-pricing.js';
2
2
  import type { WorkerCliResult } from './worker-cli.js';
3
3
 
4
+ /** Zero-dollar usage for an unpriced step, only when the CLI reported tokens. */
5
+ function unmeteredUsage(input: number | undefined, output: number | undefined) {
6
+ if (input === undefined || output === undefined) return undefined;
7
+ return { tokens_in: input, tokens_out: output, dollars: '0.000000' };
8
+ }
9
+
4
10
  /**
5
11
  * Attach token/dollar usage to a worker's CLI result. Invalid token counts
6
12
  * (non-integer or negative) are the one remaining failure mode — those are
7
13
  * journaled as `worker_error` with the usage projected from clamped counts.
8
14
  *
9
- * Unpriced models are NOT a failure here: `pricedUsage` returns
10
- * `dollars: null` and preflight (see `budgetDiagnostics`) has already refused
11
- * declared dollar budgets against unpriced models before the CLI dispatched.
15
+ * Unpriced models are NOT a failure here: the step journals zero dollars, so it
16
+ * never trips a dollar budget, but its reported tokens still count toward any
17
+ * token budget. Preflight (see `budgetDiagnostics`) warns that such a step is
18
+ * unmetered for dollars.
12
19
  */
13
20
  export function workerSpend(result: WorkerCliResult, model?: string) {
14
- try { return { result, usage: pricedUsage(model, result.tokens_input, result.tokens_output) }; }
21
+ try {
22
+ const usage = pricedUsage(model, result.tokens_input, result.tokens_output)
23
+ ?? unmeteredUsage(result.tokens_input, result.tokens_output);
24
+ return { result, usage };
25
+ }
15
26
  catch (error) {
16
27
  return {
17
28
  result: { ...result, exit_code: null, stderr_tail: error instanceof Error ? error.message : 'Invalid model usage' },
package/src/worker.ts CHANGED
@@ -5,6 +5,7 @@ import type { JournalClient } from './journal-client.js';
5
5
  import type { Pins, StepDispatchEvent } from './protocol.js';
6
6
  import type { KernelAgentStep } from './spec.js';
7
7
  import { runAgentCli } from './worker-cli.js';
8
+ import { resolveCliModel } from './cli-adapter.js';
8
9
  import { withWorkerLease } from './worker-lease.js';
9
10
  import { workerInstruction } from './worker-input.js';
10
11
  import { helperCall } from './yaml-helpers.js';
@@ -105,9 +106,10 @@ export class AgentWorker extends EventEmitter {
105
106
  return;
106
107
  }
107
108
  let humanIntervention = false;
109
+ const effectiveModel = typeof spec.cli === 'string' ? resolveCliModel(spec.cli, spec.model) : spec.model;
108
110
  const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
109
111
  typeof spec.cli === 'string' && typeof spec.instruction === 'string'
110
- ? runAgentCli(spec.cli, workerInstruction(spec.instruction, dispatch), dispatch.wake_context, spec.model, undefined, signal, 'agent', this.options.dataDir === undefined ? undefined : {
112
+ ? runAgentCli(spec.cli, workerInstruction(spec.instruction, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'agent', this.options.dataDir === undefined ? undefined : {
111
113
  dataDir: this.options.dataDir, runId: dispatch.run_id, stepId: dispatch.step_id,
112
114
  onReady: this.options.onPtyReady, onDrive: () => { humanIntervention = true; },
113
115
  }, typeof spec.cwd === 'string' ? spec.cwd : undefined,
@@ -115,7 +117,7 @@ export class AgentWorker extends EventEmitter {
115
117
  { runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
116
118
  dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema })
117
119
  : Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
118
- const { result, usage } = workerSpend(completed, spec.model);
120
+ const { result, usage } = workerSpend(completed, effectiveModel);
119
121
  const completionReason = result.exit_code === 0 ? 'success' : 'worker_error';
120
122
 
121
123
  // Output shape: if the CLI's stdout parses as JSON, promote THAT