Skip to content

feat(bundle): use an installed bundle's envs, agents and artifacts from any bundle, the default agent included - #244

Open
earakely-scale wants to merge 13 commits into
mainfrom
feat/installed-bundle-entities
Open

earakely-scale wants to merge 13 commits into
mainfrom
feat/installed-bundle-entities

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A bundle can now use an env, agent or artifact that an installed bundle ships, either by naming its id or by leaving an agent field unset when the configured default agent is one. It works on the first agent-env run, with no agent-env run <installed bundle> beforehand.

Until now, a user bundle naming @local/<dist>/<bundle>/<entity> failed its plan with "not in the store". Even after the installed bundle had been run, each user bundle rewrote the shared entity, because ledger rows and locks belonged to the bundle being run.

To the planner, an entity from another bundle is just an entity.

  • One lookup. Every store-bound env, agent or artifact reference goes to this bundle, then installed bundles (an id under one's @local/<dist>/<bundle>/ root), then the store.
  • Pulled in like the bundle's own. The installed bundle is parsed and resolved once per run. The entity, and what it references in its bundle, become ordinary plan nodes, and the reference points at it as a local one does. Every check, preflight and write then treats it as local. For example, a pulled multi env's servers are checked against what a load seeds into it.
  • Chained. A pulled entity's references are looked up the same way: in the running bundle first, then in installed bundles. An installed bundle's ids are mapped as resolve maps a bundle's own, image folders included.
  • The default agent. A step that leaves an agent field unset deploys [agents] default_a2a_agent_id. When this bundle or an installed one defines that agent, the planner adds the reference and pulls it in. A default that lives in the store is still checked by preflight, as today.
  • The unset-agent rule is one rule. An unset EntityRef.agent field falls back to the configured default (entity.refs.default_agent_refs), for every step, the rubrics judge's included. The importer reads it instead of naming step types. A judge that deploys no agent of its own then has a run plan a default it won't deploy. That costs a shared write and at most one build per machine. Preflight still skips such a judge, and still checks a store default itself.
  • The origin shows only where it must. Plan.origins maps a pulled entry to its bundle (Plan.bundle_of, Plan.path). It's used for:
    • the entry's files (the build context, the folder listing, AuthoringContext);
    • its ledger key;
    • its label, <dist>/<bundle>:<path>, e.g. agentenv-framework/agents:agents/claude-code. A pulled entry's own problems carry the same prefix.
  • One write per shared entity. A ledger row is keyed by the bundle its write comes from. Every user bundle, and a run of the installed bundle itself, reuses one write of a shared agent. materializing() takes the ids to lock, and a run locks the entities it pulls in, so two runs writing one shared agent take turns.
  • Rules.
    • An installed id takes no version pin.
    • An id under an installed root that the bundle doesn't define, or a bundle that can't be read, is refused where it's named, not read from the store.
    • The running bundle's own definition of an id wins.
    • A pin on a default agent this bundle or an installed one defines is refused, like any pin on one.
    • Tasks and evals aren't pulled.
    • check_bundle (plugin check, the importer's check) does the same lookup without reading a store.
  • Importing. bundle init --from-task leaves a reference to an installed bundle's entity unpinned and reports it as floating, dropping a pin the stored task held, so the import passes its own check.
  • Outside bundle runs, deploy_agent and the judge's own deploy explain an @local agent the store doesn't hold: an agent-env run of a bundle writes one. They name the fields that point elsewhere. A store id is reported as before.

Not changed: preflight still models each deploy step itself, as it does for envs and sandboxes. The bundle's own entities, store references and dry runs behave as before.

A built image is its build context, built where each deploy runs. Sharing one write of a pulled agent across bundles exposed a problem. How an image was written used to depend on the run:

  • a local build on this machine for local deploys;
  • a build context for providers that build it.

So bundles on different sandboxes rewrote the shared image back and forth, and a task could pick up another bundle's form mid-run.

Now an image's version depends only on its sources:

  • Always a context. Every image the bundle builds is written as its build context (put_context), and a context's tag is its sources.
  • Built here for local deploys. When a deploy runs on this machine, the run builds the image with this machine's Docker as it writes it. An image only an unmodelled step uses, such as install_agent's, is built where it runs. A failed build still writes nothing, and "building with docker" prints as before. A reused image that Docker no longer holds is built again.
  • Lazy builds. The local provider builds a context it's given (prepare_image) and skips pulling an image it built. A smol VM loads a saved copy of this machine's build, rather than building one cold.
  • The build cache. Builds here are native, labeled with their sources, and built once per sources under a lock (providers/sandbox_providers/host_images.py).
  • Remote providers build where they run, as before.
  • Built for each sandbox's platform. A bundle image records no platform, so a VM builds it for its own, and an arm64 SmolVM on an arm64 machine loads this machine's native build.
  • Preflight no longer picks a form per run, so one run can deploy an image both here and on VMs.
  • The ledger digest drops the form, and SCHEME 2 rewrites every version once.

Tests

  • Unit: installed_entities_test.py covers:

    • pulls by id and by default (deploy_agent and an agent judge; a direct LLM judge and a task-deployed judge don't pull);
    • a default the bundle itself defines, and one only the store holds;
    • an env with its children and an artifact;
    • a pulled multi env checked from its planned config, and a chained pull;
    • an installed entity naming its bundle's image folder, and one naming the running bundle's own entity;
    • the running bundle winning, the refusals (pin, missing, wrong kind, unreadable bundle, a pinned default), a pulled entity's own problem, and check mode without a store;
    • dry-run labels;
    • two bundles and the installed bundle itself sharing one ledger write, and a changed installed folder rewriting it;
    • a run waiting on another's lock of a pulled agent.

    Plus the unset-agent rule per step in test_entity_refs.py, an import of an installed entity in importer_test.py, and the outside-run error in unwritten_local_agent_test.py. Unit tier: 8118 passed.

  • Integration: run_bundle_installed_agent_test.py installs a package registering a bundle with an echo agent folder. One bundle names the agent by id and builds and runs it on local docker. A second bundle deploys it as the configured default and reuses the build.

  • Time to an image running, old vs new, on fresh x86 and arm64 VMs: each round starts from a clean build cache, and wall time is per agent-env run. New is equal or faster everywhere:

    Scenario x86 old → new arm64 old → new
    Agent, cold 122 → 97 s 143 → 118 s
    Agent, warm 4.8 → 4.6 s 5.3 → 5.4 s
    A second bundle using the same agent 29 → 6 s 34 → 7 s
    After a file edit 28 → 5 s 32 → 6 s
    Env through the local gateway 110/27/27 → 108/26/26 s 123/28/28 → 124/27/27 s
    smol VMs 181/56/143/64 → 183/56/145/60 s n/a

    The agent is a Claude-Code-sized image (Node, a global npm install, pip). Builds on arm64 are native.

  • On a fresh Linux VM: real wheels and the real CLI, covering reuse, concurrency, edits, refusals, a back-reference, an import and a modal_vm dry run, plus live sail_vm deploys and arm64 runs. Results are in the verification comment.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

No new issue in the latest changes blocks merging.

Summary

Bundles can use installed bundles' environments, agents, and artifacts without running those bundles first.

  • Bundle runs can pull installed entities into their plans.
  • Bundles share one recorded write for each installed entity.
  • Bundle-built images keep one source-based build context across deploys.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Bundle reference or default agent] --> B[Look in running bundle]
  B --> C[Look in installed bundles]
  C --> D[Add entity and its dependencies]
  D --> E[Lock shared IDs]
  E --> F[Reuse or write through ledger]
  F --> G[Store image build context]
  G --> H[Build on deployment target]
  H --> I[Run tasks]
Loading

Reviews (6) · Last reviewed commit: "fix(bundle): a bundle image records no p..." · Reviewed by Greptile

@earakely-scale
earakely-scale requested a review from a team as a code owner October 19, 2026 00:17
Comment thread src/agent_env/bundle/importer.py
Comment thread src/agent_env/bundle/plan.py Outdated
@earakely-scale

earakely-scale commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verification on fresh Linux VMs (Ubuntu 24.04, Docker 29), at d5c143f, e3108ee and 61437c2

The test packages were real wheels, installed with uv pip:

  • demo-kit registers the bundle kit, holding an echo agent folder (agents/echo/) and an agent coder whose image names base-kit's image folder.
  • base-kit registers the bundle base, holding that image folder and a task deploying @local/demo-kit/kit/coder.

"Old" is agentenv-framework==0.9.1334 from PyPI. Every run is the real agent-env CLI, deploying on local docker.

Scenario Result
A user bundle names @local/demo-kit/kit/echo builds demo-kit/kit:agents/echo (Dockerfile image) v1 and writes the agent v1; the task passes
A second user bundle deploys it as [agents] default_a2a_agent_id image and agent v1, unchanged, no build; passes
agent-env run kit (the installed bundle itself) agents/echo v1, unchanged; its smoke task passes
The installed agent.py is edited image v2 (files changed: agent.py), agent v2 (… written anew (v1 → v2)); passes
Two user bundles run at once on fresh state one builds; the other prints waiting for another agent-env run…, then v1, unchanged; both pass
A pin on the installed id; an id the bundle doesn't define refused before writing anything: …so it has no fixed version; drop the pin / there is no agent '…/nope' in the installed bundle demo-kit/kit
agent-env run base: kit's agent built on base's own image folder artifacts/base (Dockerfile image) v1 is built as base's own, demo-kit/kit:agents/coder v1 is pulled; passes
bundle init --from-task of a task a user bundle's run wrote, then run the import the installed agent floats unpinned (a2a_agent_version: null); the import runs and passes
--sandbox modal_vm --dry-run the pulled agent's image is planned as a build context, which each deploy builds where it runs
agent-env plugin check passes
Old: the same user bundle, by id and by default there is no agent '@local/demo-kit/kit/echo' in the store

Integration: tst/integration/cli/run_bundle_* and bundle_init_test.py (the new run_bundle_installed_agent_test.py included) pass on the VM, 32/32 on both heads.

sail_vm, live, at e3108ee (the image change)

"Old" is #244 before an image became its build context. Every run deploys on real Sail VMs.

Run Old New
A bundle-built Claude-Code-sized agent, first run / second run 101 / 98 s, pass 99 / 89 s, pass
echo-mcp (an MCP server through the gateway) 89 s, pass 100 s, pass
An installed bundle's agent, by id 32 s, pass 44 s, pass
One run deploying one image both here and on sail_vm refused: …a run writes it one way; run its deploys on one kind passes (99 s)

The single-run gaps are VM-start noise. Over 8 alternating rounds, the installed agent's deploy step averaged 28.0 s old and 30.6 s new. echo-mcp averaged 79 s old and 83 s new over 4 rounds. The ranges overlap, and the deploy path is the same code on both. No run log held the Sail key.

arm64, at 61437c2

  • build_context_local_test.py passes on x86 and arm64 VMs (3/3 each). It includes a context recording no platform, as a bundle writes it, built for linux/arm64 on the arm64 VM.
  • The bundle integration tests pass on arm64 (32/32). Two need amd64 emulation (binfmt) for the shared echo-agent fixture, which builds for linux/amd64.

Providers

  • Live: local and smol (x86), and sail_vm.
  • Not run: a real modal_vm deploy. The Modal workspace used was over its spend limit, and sandbox creation failed before anything ran.
  • Untested: e2b, tensorlake, tenki, vercel and beta_scale. They take the shared VM or by-name path, which this PR changes only in the image's tag and recorded platform.

🤖 Generated with Claude Code

@earakely-scale
earakely-scale force-pushed the feat/installed-bundle-entities branch from d5c143f to 6252839 Compare October 19, 2026 03:55
Comment thread src/agent_env/providers/sandbox_providers/host_images.py Outdated
Comment thread src/agent_env/bundle/preflight.py Outdated
earakely-scale and others added 10 commits October 18, 2026 22:44
…sages

Plan.origins maps an entry from another bundle to that bundle; Plan.bundle_of and Plan.path read it, and the
planner, ledger, materialize, preflight and run use them where they read an entry's files or name its path.
Nothing is pulled from another bundle yet, so origins is empty and behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…agent

default_agent_refs names the agent fields a step dict leaves unset that deploy the configured default. Every core
step's unset agent field does, but the rubrics judge's: it declares default_when, true only when the judge deploys
its own agent. The importer reads the declarations instead of naming the two step types.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…to the run

The planner resolves a store-bound env, agent or artifact id under an installed bundle's id root by parsing and
resolving that bundle, once per run, and adding the entity and what it references in its bundle to the plan as the
bundle's own entities are added. The reference then points at it, so every check, preflight and write treats it as
local. A step falling back to the configured default agent does the same when this bundle or an installed one defines
that agent; a store default is still preflight's to check.

An installed id takes no version pin, and one its bundle doesn't define, or a bundle that can't be read, is refused
where it's named rather than read from the store. A pulled entry is named after its bundle, as
<dist>/<bundle>:<path>. run, installed.checked and the importer's check pass installed_bundles.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ndle uses it

A ledger row is keyed by the bundle its write comes from, not the bundle being run, so every bundle using an
installed bundle's agent, and a run of that bundle itself, reuse one write of it instead of rewriting it as "last
written by" another bundle. materializing takes the ids to lock, and a run locks the entities it pulls in with its
own, so two runs writing one shared agent take turns and the second reuses the first's write.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e run writes one

Outside a bundle run, nothing writes an @Local agent: deploy_agent and the rubrics judge's own deploy now say so when
the store doesn't hold one, naming the fields to point elsewhere, instead of only that it isn't found. A store id is
reported as before. Adds an integration test running an installed package's agent from two bundles on local docker,
by id and as the configured default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nset agent field means

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le first, and import installed entities unpinned

A pulled entity naming an entity of the bundle being run now points at it, rather than reading it as a store id the
bundle doesn't write. An installed bundle's ids are mapped as resolve maps a bundle's own, so a pulled entity naming
its bundle's image folder pulls that image. A pin on a default agent this bundle or an installed one defines is
refused, as a pin on any reference to one is.

The importer leaves a reference to an installed bundle's entity unpinned, reported as floating, and drops a pin the
stored task held, so the import passes its own check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…builds for the deploys here

An image a bundle builds is written as its build context whichever way a run deploys it, so its version depends only on
its sources, and a pulled agent shared by bundles on different sandboxes is no longer rewritten by each, nor swapped
under a running task. Each deploy builds it where it runs:

- A run builds it with this machine's Docker as it writes it, when a deploy here runs it or none the run models does,
  so a failed build writes nothing. A reused one Docker no longer holds is built again.
- This machine's builds are native and labeled with their sources, built once per sources and locked, and a context's
  tag is its sources. The local provider builds a context it's given lazily too, and skips pulling one it built. A smol
  VM loads a saved copy of this machine's build rather than build one cold.
- Preflight no longer chooses a form per run, so one run can deploy an image both here and on VMs. The ledger digest
  drops the form, and SCHEME 2 rewrites every version once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ls back to the default

The rubrics judge's unset judge_a2a_agent_id follows the one rule too. A judge that deploys no agent of its own then
has a bundle run plan the default it won't deploy, which costs a shared write and at most one build per machine; a
store default is still preflight's to check, and preflight still skips such a judge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale force-pushed the feat/installed-bundle-entities branch from f72f5bf to 64a6fcf Compare October 19, 2026 05:47
…one copy per image name for VMs here

An image only a step preflight doesn't model uses, such as install_agent's on a VM elsewhere, is built where it runs
rather than on this machine, so a run deploying nowhere here needs no Docker for it, as before. A saved copy of this
machine's build is kept per image name as well as per sources, since a VM loads it under the name it was saved with.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@greptile-apps

This comment has been minimized.

earakely-scale and others added 2 commits October 18, 2026 23:34
… does

The integration tests stand the local sandbox in for a remote VM by giving it a platform, and check the build a VM
makes for it; only a local sandbox recording none, as it does for real, builds natively with this machine's Docker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ds it for its own

A bundle wrote its build contexts recorded for linux/amd64, which an arm64 VM, such as a SmolVM on Apple silicon,
refused before building. A context recording no platform is built for the platform of the VM that runs it, as the
local sandbox and this machine's build already did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Re Greptile's outside-diff P1, Arm64 SmolVM deploys fail (materialize.py:176): fixed in 61437c2.

A bundle image now records no platform (put_context(..., platform=None)), and a VM builds a context that records no platform for its own platform. On an arm64 SmolVM on an arm64 machine, load_problems passes and the VM loads this machine's native build. A remote VM builds it for the VM's platform. Explicit put_context platforms are unchanged.

Verified:

  • build_context_local_test.py passes on x86 and arm64 VMs (3/3 each). The new test asserts "for linux/arm64" on arm64.
  • The bundle integration tests pass on arm64 (32/32). Two need amd64 emulation for the shared echo-agent fixture, which builds for linux/amd64.
  • Unit tests: 8118 passed.

Also in this push, ebafb28: a local sandbox given a platform builds a context as a VM does. This fixes integration-local-slow.

This branch has not been deployed

No deployments
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.

1 participant