Repository navigation
feat(bundle): import a stored task or eval as a bundle folder, with its references pinned - #219
Open
earakely-scale wants to merge 3 commits into
Open
earakely-scale wants to merge 3 commits into
earakely-scale wants to merge 3 commits into
Conversation
…ts references pinned agent-env bundle init <folder> --from-task ID [--version N] writes a task a store holds as a bundle that agent-env run runs: tasks/<name>.json is the stored steps read through the current step models, with every store reference pinned at the version the store holds now, so the folder keeps running what the store ran. An id a step of the task writes stays unpinned, since the run writes it again. --from-eval writes an eval's tasks and evals/<name>.toml. Values that look like secrets are left out: env-var entries whose names look secret or whose values hold a URL's credentials are dropped, and a URL's credentials elsewhere are masked. The README records what was imported, pinned, floating and left out. Nothing is written to a store; the folder is written beside its destination, checked as agent-env run checks a bundle, and moved into place, so a failed import leaves nothing. The parser's id-root derivation and segment check, the planner's store getters and config's secret-key predicate are now public, so the importer reuses them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e errors when the write fails - An @Local output the stored task writes is renamed to its last segment, and the steps reading it follow. - `bundle init .` takes the folder's absolute path. - An env ref without a version floats unless the task deploys it; an existing pin is read at its version. - A step's upstream steps are worked out on demand and in linear time, rather than for every step up front in cubic time; a 3,000-step task took ~400 s to resolve and now takes under a second. - A folder the import can't write in, a destination something else wrote meanwhile, and a failed move each give one line rather than a traceback; SIGTERM cleans up like Ctrl-C. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment on lines
+228
to
+232
| if self.dest.is_dir(): | ||
| self.dest.rmdir() # empty, as _check_destination found it | ||
| emptied = True | ||
| os.replace(staging, self.dest) | ||
| except OSError as e: |
There was a problem hiding this comment.
Interrupted imports delete empty folders
The new SIGTERM handler raises KeyboardInterrupt throughout the import, but _move restores an empty destination only after OSError. If the signal arrives after self.dest.rmdir() and before os.replace, the destination stays deleted and write removes the staged bundle.
Defer interrupts across the move or restore the destination when an interrupt stops it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/bundle/importer.py
Line: 228-232
Comment:
**Interrupted imports delete empty folders**
The new `SIGTERM` handler raises `KeyboardInterrupt` throughout the import, but `_move` restores an empty destination only after `OSError`. If the signal arrives after `self.dest.rmdir()` and before `os.replace`, the destination stays deleted and `write` removes the staged bundle.
Defer interrupts across the move or restore the destination when an interrupt stops it.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Collaborator
Author
There was a problem hiding this comment.
Fixed in 5a3c2bf: _move now puts the emptied destination back on any exception, an interrupt included, and re-raises the ones that are not OSError. Covered by test_an_empty_destination_is_put_back_when_an_interrupt_stops_the_move.
…empty destination back on an interrupt - An @Local output that takes its last segment is numbered (-2, -3, ...) when another output or a store reference of its kind in the task already has that name, and the steps reading it follow. - An interrupt between emptying the destination and moving the import into place puts the empty folder back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A new
bundlegroup with one command,bundle init, which writes a task that a store holds, or an eval and its tasks, as a bundle folder thatagent-env runruns. You can then edit the folder and run it locally.That's the real output on the dev registry, with the long paths shortened.
What it writes
tasks/<name>.json: the stored steps, read through the current step models. This is exactly whattask getprints, with no defaults stripped.An
@localone, from a task a bundle wrote, takes its last segment, numbered-2,-3and so on when another output or a store reference of its kind has that name. The steps that read it follow.env_vars,default_env_vars) is dropped when its name looks like a secret, using config's existing secret-key check, or when its value holds a URL's credentials. A URL's credentials anywhere else are masked. A user name likegitisn't treated as a credential.--from-eval: writes each task the eval names, plusevals/<name>.tomllisting them. A task the eval names twice is written once./segment of its id. A character an id can't hold becomes-; a name with no letter or number left falls back totask; caseless clashes get-2,-3and so on.README.md: its first line, which is the bundle's description, says what was imported. Below it are what was pinned, what floats and what was left out.Safety
agent-env runchecks a bundle before it reads a store, then moved into place. A failed or interrupted import (Ctrl-C or SIGTERM) leaves nothing behind, and an empty destination is put back if the move fails or is interrupted.Small changes to existing code, so the importer reuses them
bundle.parse: the id-root derivation moves into_id_root, which the parser still uses. A newroot_problems(root)checks a path that doesn't exist yet._refusedbecomes the publicsegment_problem.bundle.plan:_GETTERSbecomesSTORE_GETTERS.config.describe:_is_secret_keybecomesis_secret_key.bundle.resolve: the steps a step runs after are now worked out only for a step that reads an output the task writes, and in linear time. Before, they were worked out for every step, in cubic time when steps name nodepends_on. A 3,000-step task took ~400 s to resolve, inrunandcheckas much as in an import, and now takes under a second.cli.utils:interruptible(), which turns SIGTERM into Ctrl-C, moves here fromcli._plugin_changes, sobundle initshares it.bundle/_toml.py: a small TOML writer, sincetomllibonly reads. It's checked by atomllibround trip; this PR uses it for eval tomls, and the follow-up for env and agent tomls.Tests
Unit,
importer_test.py(36 tests), against a seeded local store holding bare ids:toml_writer_test.pycovers round trips, layout, and values TOML can't hold.resolve_test.pychecks the new upstream walk against the plain transitive walk on 500 random step graphs (cycles and later steps included), and resolves a 3,000-step task under a 60 s timeout.Integration, fast tier (
bundle_init_test.py), a hello-shaped task stored under bare ids:agent-env runs, and reruns writing nothing;Integration, slow tier (
run_bundle_built_envs_test.py): an MCP server env, its check file and task are stored under bare ids, and the task imports and runs through the gateway twice; the second run writes nothing.Unit tier: 8,150 passed.
ruff checkpasses, and the plugin-API check reports no break.Live, dev registry:
run --dry-run --sandbox beta_scaleaccepts the folder.@localtask against registry v7 step by step and field by field, with a scratch script that isn't shipped, shows 5 of 5 steps equal. The only differences are the three pins the import added:env_version2,artifacts[0].version2 anda2a_agent_version65.Chaos testing
Run on two EC2 VMs, a t3.large (amd64) and a t4g.large (arm64), both Ubuntu 24.04, each with its own seeded local store. The full battery ran on
eddda2c. The scenarios the last commit touches (s01–s04, s08) ran again on5a3c2bf, where s03 also imports into an empty folder in half its trials.~and relative paths#,/, Unicode, zero-width, 300 chars, caseless clashes) import under valid names, and all runIt found three problems, all fixed here:
Still expected:
.<name>.import-<hex>folder beside the destination (2–8 of 30 trials). The destination itself is never partial, and a retry works.task getdoes too, so that's the store's error handling and not this PR's.Not in this PR
--with env,agent,datacomes in the follow-up PR: it brings envs, agents and data into the folder from build contexts and downloads.This is not hot path: it's the CLI and bundle code that only the CLI runs, so there's no release of its own.
🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge, with one remaining non-blocking interrupt edge case.Fix with agent prompt
Summary
Adds
agent-env bundle initto turn a stored task or eval into an editable bundle folder.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Check destination] --> B[Read task or eval] B --> C[Rename outputs and pin references] C --> D[Drop likely secrets] D --> E[Write sibling staging folder] E --> F[Check bundle] F --> G[Move into destination]Reviews (3) · Last reviewed commit: "fix(bundle): number a renamed output who..." · Reviewed by Greptile