Repository navigation
DRU-1146 -- A workflow reads the cost of its own run - #1161
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
czpython
left a comment
There was a problem hiding this comment.
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.
| if _in_step.get(): | ||
| raise WorkflowError("get_cost() runs in the workflow body, not inside a @step") |
There was a problem hiding this comment.
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.
| 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`. |
There was a problem hiding this comment.
Two changes here:
- With the guard gone, replace "Call this method outside a
@step" with what happens there: inside a@steporrun(), 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.
| # 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)) |
There was a problem hiding this comment.
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.
| # 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 |
| await DBOS.cancel_workflow_async(workflow_id) | ||
|
|
||
|
|
||
| async def test_a_replay_reads_the_cost_the_run_read_first(runtime): |
There was a problem hiding this comment.
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.
| from druks.testing import seed_run | ||
|
|
||
|
|
||
| async def test_a_runs_cost_sums_its_calls_and_counts_a_missing_cost_as_zero(druks_db): |
There was a problem hiding this comment.
test_cost_capture.py already holds the call-cost tests. Put this test there instead of in a new file.
7eed3a1 to
65ba44f
Compare
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.announce(), so a replay returns the cost the run read the first time. Inside a@steporrun(), it shares that checkpoint.Workflow.get_cost()reads through a newAgentCall.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.