Repository navigation
feat(bundle): run --dry-run, another bundle's entities by @local id, bare depends_on ids - #22
Conversation
TaskStepDependency.from_dict takes a step id or {"task_step_id": <id>}, and dependencies() reads a whole
depends_on: a lone string or any other shape is a ValueError rather than a list of characters. Steps are
still stored in the object form. _validate_dag now tells a step that comes later apart from an id that names
no step in the task.
The bundle resolver reads depends_on through the same parser, so a malformed one or an unknown id is
reported with its cause, not only as an output the step doesn't depend on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
materialize(plan, dry_run=True) makes every check and preflight a run makes before its first task and writes nothing: no entity, ledger row, lock or instance. Each write is reported at the version it would reuse or the store's next one. dry_run_bundle() returns the same to Python as a DryRun. Ledger.check(write, versions) hashes the versions the walk passes in for what an env or agent needs, so a need that would be written anew rewrites its dependent in the prediction as in the run. Versions are hashed as strings, as before, so the rows a run recorded stay reused. A step that reads what the run would write first can't be preflighted against the store; the dry run lists those whose type has a preflight. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An explicit @local/... id outside the bundle is checked against the @Local namespace's store like any store id. An agent is written with the version the plan read, and again once that bundle writes a new one; tasks and evals stay bare. An id under the bundle's own root that it doesn't write is still refused without a store read. Planning such a read with namespace routing off raises. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hecks-dry-run # Conflicts: # README.md
An artifact and an agent can share an id, so a dry run rewriting the agent skipped the preflight of a step over the reused artifact, and exited 0 where the run then stopped. It now compares (store, id); a step reading its own task's output is still matched by id, so the real run is unchanged. - Deriving a step's output id reads the step without its depends_on, so a malformed one is reported once. - dry_run_bundle's docstring says a failed preflight raises and that routing is on process-wide while it runs; run --dry-run's help says it takes no lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
{"task_step_id": <id>} is read as it always was, whatever the id holds, so every stored task main loads
still loads; only the new bare form must be a string. A lone string and any other shape are still refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| if isinstance(data, dict) and "task_step_id" in data: | ||
| return cls(task_step_id=data["task_step_id"]) |
There was a problem hiding this comment.
Bad dependency crashes bundle checks
A step with depends_on: [{"task_step_id": ["load"]}] now gets past TaskStepDependency.from_dict. The resolver then tries to look up that list in a set and raises TypeError. Bundle checks and dry runs crash instead of explaining the bad dependency. Reject object-form IDs that are not strings.
| if isinstance(data, dict) and "task_step_id" in data: | |
| return cls(task_step_id=data["task_step_id"]) | |
| if isinstance(data, dict) and isinstance(data.get("task_step_id"), str): | |
| return cls(task_step_id=data["task_step_id"]) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task_step/task_step.py
Line: 26-27
Comment:
**Bad dependency crashes bundle checks**
A step with `depends_on: [{"task_step_id": ["load"]}]` now gets past `TaskStepDependency.from_dict`. The resolver then tries to look up that list in a set and raises `TypeError`. Bundle checks and dry runs crash instead of explaining the bad dependency. Reject object-form IDs that are not strings.
```suggestion
if isinstance(data, dict) and isinstance(data.get("task_step_id"), str):
return cls(task_step_id=data["task_step_id"])
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in the resolver instead (next commit): a bundle's depends_on that names a step by anything but its id is now reported as a problem, not a crash. The object form stays lenient in TaskStepDependency on purpose, so every stored task that loads today keeps loading.
There was a problem hiding this comment.
That resolution is valid. Keeping TaskStepDependency.from_dict lenient preserves compatibility for already-stored tasks, while _depends_on now validates the bundle’s raw depends_on before dependency IDs are used. Malformed entries and non-string task_step_id values therefore become bundle problems instead of reaching an unhashable lookup. The added resolver tests cover the list-valued object form and confirm it reports the cause alongside the missing dependency edge, so no further change to TaskStepDependency.from_dict is needed.
…roblem
The object form reads any id, as before, so a bundle task could reach the resolver with
{"task_step_id": ["load"]} and crash on the lookup. The resolver now reports such an entry and
leaves it out of the step's edges.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- materialize checks each write in its own closure and gives the ledger only the versions of what the write needs, so Ledger.check(write, needs) matches digest(write, needs) and no versions map shadows done. - The resolver keeps a dependency id as an edge only when it names a step, with one message otherwise, so _upstream no longer filters. - _validate_dag raises one error with the clause that applies, and _preflight's docstring is rewrapped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three changes to how a bundle is checked before anything is written, one commit each:
depends_ontakes a bare step id.["load"]means[{"task_step_id": "load"}].agent-env run --dry-runmakes every check the run makes before its first task and writes nothing.@local/…id.A bare step id in
depends_onTaskStepDependency.from_dicttakes either form, and reads the object form exactly as before, so every stored task that loaded still loads; only a bare entry must be a string. The newdependencies()reads a wholedepends_on, and refuses a lone string, which would otherwise be read as a list of its characters.Steps are still stored in the object form, so every stored task and every reader of
to_dict()is unchanged.Task._validate_dagnow separates the two ways a dependency can be wrong: a step that comes later (still "forward refs are not allowed"), and an id that names no step in the task.The bundle resolver reads
depends_onthrough the same parser. A malformed one used to show up only as "…which this step doesn't depend on"; now it is reported with its cause:agent-env run --dry-runmaterialize(plan, dry_run=True): the same refusals, ledger checks and preflights as a run, without the lock, the writers orTask.put/Eval.put. Each write is reported at the version it would reuse or the store's next one.dry_run_bundle()returns the same to Python as aDryRun.Ledger.check(write, versions)hashes the versions the walk passes in for what an env or agent needs, instead of reading the ledger as it stands. So a need that would be written anew rewrites its dependent in the prediction, as it does in the run. Versions are still hashed as strings, so rows a run already recorded stay reused, and no ledger scheme bump is needed.run_codeover a changed artifact) can't be preflighted against the store. The dry run lists those whose type has its own preflight, underNot preflighted. Steps over reused writes are preflighted as in a run.--sandboxis checked;--modeland--keepchange nothing it shows.run --helpsays), so another run writing the same ids can change the answer first, and it doesn't see what another bundle's pending run would write. It reads the configured stores the way a run does, so a read can create an empty default local store.Another bundle's entities by their
@localid@local/…id outside the bundle is checked against the@localnamespace's store, like any store id. Until now it was refused as not supported yet.agent-env task runhint.Docs
The README now points to the docs site and keeps only the plugin contract, so this PR changes no README text; the user-facing docs for bare
depends_on,--dry-runand cross-bundle references go to the docs site separately.Testing
make unit-test): 5,684 passed, 13 skipped, with main merged in. Each of the three feature commits also passes on its own: 5,649, 5,665 and 5,671.tst/integration/cli/run_bundle_test.py, which runsagent-env runonhello: 7 passed.depends_on.hellowith baredepends_onscores 1, stores its steps in the object form, and a rerun reuses both writes.🤖 Generated with Claude Code
The PR appears safe to merge; no new issue or outstanding earlier finding remains.
Fix with agent prompt
Summary
Bundle runs can now be previewed with
--dry-run, and bundles can read entities from another bundle using@localIDs. Task steps also accept bare IDs independs_on, while keeping the stored format the same.Reviews (5) · Last reviewed commit: "refactor(bundle): simpler ledger walk an..."