station-v0.82's new "model" workflow-node kind (the orchestration router's
workflow-node surface -- offloads classify/extract/summarize work to a local
or cloud model) is invisible to both places the engine accounts for real
money: the pre-approval blast-radius spend estimate, and the runtime
cumulative spend cap.
plan_workflow()'s per-node dispatch has a branch for "model" nodes that adds
the touched system ("llm") to the blast radius but never adds anything to
spend_cents, and never multiplies by the node's for_each iteration count the
way every other spend-capable node kind does (effect, subworkflow, agent all
do). A workflow with a model node fanned out over any number of items -- the
router's own marketed use case is bulk classification of "support tickets,
rows, documents" -- plans with spend_cents=0 and requires="auto_approve" no
matter how many real cloud-model calls the for_each will make.
run_workflow()'s cumulative capabilities.max_spend_cents enforcement -- its
own docstring calls this "the hard guarantee that makes max_spend_cents
load-bearing rather than advisory" -- is gated on kind == "effect" at both
the pre-iteration check and the post-execution credit. A "model" node's real
spend (the router's own cost estimator computes a genuine nonzero
estimated_cost_cents for every cloud-served unit, recorded in its own node
receipt) is never checked against the cap and never credited toward it.
Proven end-to-end against the real, unmodified station-v0.82 engine: (1) a
workflow with a model node inside a 500-item for_each plans with
blast_radius={"spend_cents": 0, "requires": "auto_approve"}; (2) a separate
workflow declaring capabilities.max_spend_cents=0 -- meaning it may spend
nothing -- completes successfully (outcome COMPLETED, no SpendCapExceeded)
after executing a model node the router's own estimator prices at a nonzero
cloud cost.
Reproduction steps:
- Extract a clean station-v0.82 tarball.
- Call plan_workflow() on a workflow with one "model"-kind node
(task_type="classify") set to for_each over a list of 500 items.
- Call run_workflow() on a separate workflow with one "model"-kind node and
capabilities={"max_spend_cents": 0}, using the engine's own agent_llm
injection hook (a standard test seam already exposed by run_workflow) to
avoid needing a live provider key.
Expected: step 2's plan reports a nonzero (or at least non-$0/auto-approve)
spend signal proportional to the for_each count; step 3 raises
SpendCapExceeded before or during the model node, matching the guarantee the
engine's own docstring describes.
Actual: step 2 returns blast_radius={"spend_cents": 0, "spend_unbounded":
false, "requires": "auto_approve"} regardless of the for_each count. Step 3
completes with outcome="COMPLETED", ok=true, no SpendCapExceeded raised, and
the run's own routing summary shows a real nonzero estimated_cloud_cents that
was simply never checked against the declared zero-spend cap.
Root cause: workflow_engine.py's spend accounting has three touch points, all
three hardcoded to kind == "effect" and never extended to the new "model"
kind: the pre-iteration spend-cap check, the post-execution spend credit
(both inside run_workflow()'s for_each loop), and plan_workflow()'s per-node
spend_cents accumulation (which does branch on kind for other node types --
subworkflow, agent, effect -- but the "model" branch has no equivalent line).
Suggested fix: extend all three sites to also cover kind == "model", crediting
against the model node's own receipt/route estimated_cost_cents field the same
way an effect node's receipt-declared spend_cents is already preferred over a
re-estimate.