Skip to content

DRU-1146 -- A workflow reads the cost of its own run - #1161

Merged
czpython merged 1 commit into
mainfrom
ksocha/dru-1146-a-workflow-reads-the-cost-of-its-own-run
Nov 10, 2026
Merged

czpython merged 1 commit into
mainfrom
ksocha/dru-1146-a-workflow-reads-the-cost-of-its-own-run

Conversation

@chaosk

@chaosk chaosk commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

A workflow can now read what its own run has cost so far, with await self.get_cost(). The security sweep needs it to put the run's cost in its summary, as the demo does.

  • What it returns: the sum, in US dollars, of the costs the run's agent calls recorded. A call that recorded no cost counts as zero.
  • Replay: the read is a checkpoint, like announce(), so a replay returns the cost the run read the first time. Inside a @step or run(), it shares that checkpoint.
  • Where: Workflow.get_cost() reads through a new AgentCall.get_cost_for_run(). The author guide documents it after announcing domain events.

It only reads what calls record. The open fixes to what a call records (#1158, #1159) change what it returns, with no change here.

@chaosk
chaosk requested a review from czpython as a code owner November 10, 2026 10:53
@mintlify

mintlify Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
druks 🟢 Ready View Preview Nov 10, 2026, 2:11 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@czpython czpython left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change makes sense. An app cannot read AgentCall, an agent call hands the body no cost, and a checkpointed read on Workflow is the right shape for a value the body uses.

One finding blocks the merge: get_cost() raises in every run() workflow. The other comments are cuts. I read the blocking finding from the source. I did not run it.

Comment thread backend/druks/workflows.py Outdated
Comment on lines +934 to +935
if _in_step.get():
raise WorkflowError("get_cost() runs in the workflow body, not inside a @step")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard makes get_cost() raise in every run() workflow. _resolve_body_method stamps run() as a step, and the step wrapper sets _in_step. The author then reads "not inside a @step" in a workflow that has no @step.

announce() needs the guard, because a retried step would emit the event again. A read has nothing to protect. Inside a step, DBOS runs a nested step as a plain function, so the read shares the enclosing checkpoint, as an agent call does.

Delete the guard.

Comment thread docs/writing-an-app.md Outdated
Comment on lines +371 to +374
It is the sum, in US dollars, of the costs that the run's agent calls recorded. A
call that recorded no cost counts as zero, as on the dashboard. The read is a
checkpoint, so a replay returns the cost the run read the first time. Call this
method outside a `@step`.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two changes here:

  • With the guard gone, replace "Call this method outside a @step" with what happens there: inside a @step or run(), the read shares that checkpoint.
  • Drop "as on the dashboard". Only the software_factory work item page adds up call costs, and this guide is for every app.

Comment thread backend/druks/durable/models.py Outdated
Comment on lines +699 to +701
# A call that recorded no cost counts as zero, as the dashboard counts it.
stmt = select(func.coalesce(func.sum(cls.cost_usd), 0.0)).where(cls.run_id == run_id)
return float(await session.scalar(stmt))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUM already skips a call with no cost. The coalesce only covers a run with no recorded cost at all, and or 0.0 does that without the float() or the comment.

Suggested change
# A call that recorded no cost counts as zero, as the dashboard counts it.
stmt = select(func.coalesce(func.sum(cls.cost_usd), 0.0)).where(cls.run_id == run_id)
return float(await session.scalar(stmt))
stmt = select(func.sum(cls.cost_usd)).where(cls.run_id == run_id)
return await session.scalar(stmt) or 0.0

Comment thread backend/tests/test_durable_sdk.py Outdated
await DBOS.cancel_workflow_async(workflow_id)


async def test_a_replay_reads_the_cost_the_run_read_first(runtime):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test and CostFlow prove that a DBOS step returns its recorded result on replay. That is a DBOS guarantee. Our part is the one run_step_async line. Delete both. The sum test in test_run_cost.py is the pin.

Comment thread backend/tests/test_run_cost.py Outdated
from druks.testing import seed_run


async def test_a_runs_cost_sums_its_calls_and_counts_a_missing_cost_as_zero(druks_db):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_cost_capture.py already holds the call-cost tests. Put this test there instead of in a new file.

@chaosk
chaosk force-pushed the ksocha/dru-1146-a-workflow-reads-the-cost-of-its-own-run branch from 7eed3a1 to 65ba44f Compare November 10, 2026 14:11
@chaosk
chaosk requested a review from czpython November 10, 2026 14:12

@czpython czpython left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏆

@czpython
czpython enabled auto-merge (squash) November 10, 2026 16:09
@czpython
czpython disabled auto-merge November 10, 2026 16:10
@czpython
czpython merged commit 4bbb7c6 into main Nov 10, 2026
7 checks passed
@czpython
czpython deleted the ksocha/dru-1146-a-workflow-reads-the-cost-of-its-own-run branch November 10, 2026 16:10

This branch was successfully deployed

1 active deployment
staging - docs — 65ba44fe Deployed Nov 10, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants