Skip to content

Bundle System Deep Dive

Detailed guide to the bundle system: loading, composition, validation, and preparation. For the mental model, see Core Concepts.

For complete bundle creation guidance, see BUNDLE_GUIDE.md in the amplifier-foundation repository.

Loading Bundles

Bundles can be loaded from multiple source types using load_bundle():

from amplifier_foundation import load_bundle

# From git URL
bundle = await load_bundle("git+https://github.com/org/my-bundle@main")

# From local file
bundle = await load_bundle("./bundle.md")

# From git URL with subdirectory
bundle = await load_bundle(
    "git+https://github.com/org/repo@main#subdirectory=providers/anthropic-opus.yaml"
)

# Strict mode: include failures raise exceptions instead of logging warnings (useful for CI)
bundle = await load_bundle("git+https://github.com/org/my-bundle@main", strict=True)

Source URI Formats

Format Example Use Case
Local path ./modules/my-module Modules within the bundle
Relative path ../shared-module Sibling directories
Git URL git+https://github.com/org/repo@main External modules
Git with subpath git+https://github.com/org/repo@main#subdirectory=modules/foo Module within larger repo

Local paths are resolved relative to the bundle's location.

Bundle File Formats

Bundles support two file formats:

  • Markdown (.md) — YAML frontmatter + markdown body for system instruction
  • YAML (.yaml, .yml) — Pure YAML configuration (no instruction body)

When loading a directory, the registry looks for bundle.md first, then bundle.yaml.

Bundle File Structure

A bundle is a markdown file with YAML frontmatter:

---
bundle:
  name: my-bundle
  version: 1.0.0
  description: What this bundle provides
  namespace_root: ..              # Optional: relative path to where agents/ and context/ live

includes:
  - bundle: foundation              # Inherit from other bundles
  - bundle: my-bundle:behaviors/x   # Include behaviors

# Only declare tools NOT inherited from includes
tools:
  - module: tool-name
    source: ./modules/tool-name     # Local path
    config:
      setting: value

# Control what tools spawned agents inherit
spawn:
  exclude_tools: [tool-task]        # Agents inherit all EXCEPT these
  # OR use explicit list:
  # tools: [tool-a, tool-b]         # Agents get ONLY these tools

agents:
  include:
    - my-bundle:agent-name          # Reference agents in this bundle

# Only declare hooks NOT inherited from includes
hooks:
  - module: hooks-custom
    source: git+https://github.com/...
---

# System Instructions

Your markdown instructions here. This becomes the system prompt.

Reference documentation with @mentions:
@my-bundle:docs/GUIDE.md

namespace_root: When your bundle YAML file lives in a subdirectory (e.g., behaviors/my-feature.yaml) but your agents/ and context/ directories live in a parent directory, use namespace_root to point to that parent. For example, if your structure is:

my-bundle/
├── bundle.md
├── behaviors/
│   └── my-feature.yaml           # YAML is here
├── agents/
│   └── my-agent.md               # But agents live here
└── context/
    └── instructions.md           # And context lives here

Your behaviors/my-feature.yaml would have:

bundle:
  name: my-feature
  namespace_root: ..               # Points to my-bundle/

This ensures my-feature:agents/my-agent and my-feature:context/instructions.md resolve correctly.

Composition

Bundles compose via the includes: directive. Later bundles override earlier ones.

Merge Rules

  • session: Deep-merged (nested dicts merged recursively, later wins for scalars)
  • spawn: Deep-merged (later overrides earlier)
  • providers, tools, hooks: Merged by module ID (configs for same module are deep-merged)
  • agents: Merged by agent name (later wins)
  • context: Accumulates with namespace prefix (each bundle contributes without collision)
  • Markdown instructions: Replace entirely (later wins)

Composition Example

from amplifier_foundation import load_bundle

base = await load_bundle("foundation")
overlay = await load_bundle("./my-customizations.md")
composed = base.compose(overlay)

Validation

The BundleValidator checks bundle structure:

from amplifier_foundation import validate_bundle

result = validate_bundle(bundle)
if not result.valid:
    for error in result.errors:
        print(f"Error: {error}")

Validation Rules

  • Bundle must have a name
  • Module entries must be dicts with a module field
  • Module config must be a dict if present
  • session.orchestrator must be a string or dict (with module/source key)
  • session.context must be a string or dict (with module/source key)
  • Agent entries must be dicts
  • Context file paths must exist (warning in default mode; error in strict=True mode)

Mode Files

Bundles can include mode files in a modes/ directory that extend bundle functionality:

---
mode:
  name: debug-mode
  description: Enhanced debugging capabilities
  advertised: true
  contributes:
    agents:
      debug-inspector:
        description: Inspector agent for this mode
        instructions: ./context/debug-instructions.md
    context:
      - ./context/debug-guide.md
    skills:
      - debug-patterns
---

# Debug Mode Instructions

Your mode-specific instructions here.

Mode files are validated during prepare() via validate_modes(). The contributes section can include: - agents: Dict of agent name → definition - context: List of context file paths - skills: List of skill names

Validation warns about unknown keys and type errors in the contributes block.

Preparation

bundle.prepare() downloads and installs all modules specified in the mount plan, making them importable. Returns a PreparedBundle containing mount_plan and a BundleModuleResolver for use with AmplifierSession.

# Standard preparation
prepared = await bundle.prepare()

# Skip package installation (use pre-installed modules)
prepared = await bundle.prepare(install_deps=False)

# With source override policy (app-layer module routing)
def resolve_with_overrides(module_id: str, source: str) -> str:
    return overrides.get(module_id) or source

prepared = await bundle.prepare(source_resolver=resolve_with_overrides)

Parameters:

  • install_deps (bool, default True) — Whether to install Python dependencies for modules.
  • source_resolver (Callable[[str, str], str] | None) — Optional callback (module_id, original_source) -> resolved_source. Allows app-layer source override policy to be applied before activation.
  • progress_callback (Callable[[str, str], None] | None) — Optional callback (action, detail) for progress reporting. Actions include "installing_package", "activating", "installing".

Use prepared.create_session() to create an AmplifierSession:

prepared = await bundle.prepare()
async with prepared.create_session() as session:
    response = await session.execute("Hello!")

Use prepared.spawn() to spawn a sub-session with a child bundle:

from amplifier_foundation.bundle import Bundle

# Resolve child bundle
child_bundle = await load_bundle("./agents/bug-hunter.md")

# Spawn sub-session (composed with parent by default)
result = await prepared.spawn(
    child_bundle,
    "Find the bug in auth.py",
)
# Returns: {"output": str, "session_id": str, "status": str, "turn_count": int, "metadata": dict}

# Resume existing sub-session
result = await prepared.spawn(
    child_bundle,
    "Continue investigating",
    session_id=result["session_id"],
)

# Spawn without composition (standalone bundle, no parent merge)
result = await prepared.spawn(
    child_bundle,
    "Do something",
    compose=False,
)

# With provider preferences (fallback chain)
from amplifier_foundation.spawn_utils import ProviderPreference

result = await prepared.spawn(
    child_bundle,
    "Analyze this code",
    provider_preferences=[
        ProviderPreference(provider="anthropic", model="claude-haiku-*"),
        ProviderPreference(provider="openai", model="gpt-5-mini"),
    ],
)

PreparedBundle.spawn() parameters:

  • child_bundle (Bundle) — Bundle to spawn (already resolved by app layer).
  • instruction (str) — Task instruction for the sub-session.
  • compose (bool, default True) — Whether to compose child with parent bundle.
  • parent_session (Any, default None) — Parent session for lineage tracking and UX inheritance.
  • session_id (str | None) — Optional session ID for resuming an existing session.
  • orchestrator_config (dict | None) — Optional orchestrator config to override/merge (e.g., min_delay_between_calls_ms).
  • parent_messages (list | None) — Optional parent messages to inject into child context.
  • session_cwd (Path | None) — Optional working directory override for the child session.
  • provider_preferences (list[ProviderPreference] | None) — Ordered provider/model fallback chain.
  • self_delegation_depth (int, default 0) — Current delegation depth for depth limiting.

Note: The app layer (CLI, API server) typically wraps PreparedBundle.spawn() in a "spawn capability" function that handles additional concerns such as agent name resolution, tool/hook inheritance filtering, and state persistence. See amplifier-app-cli/session_spawner.py for the reference production implementation.

Registry Management

The BundleRegistry manages named bundles:

from amplifier_foundation import BundleRegistry

registry = BundleRegistry()
registry.register({"foundation": "git+https://github.com/microsoft/amplifier-foundation@main"})
bundle = await registry.load("foundation")

# Strict mode: include failures raise exceptions instead of logging warnings
registry = BundleRegistry(strict=True)

# With include source resolver: override include URI resolution (app-layer routing)
registry = BundleRegistry(
    include_source_resolver=lambda source: overrides.get(source)
)

# Or set/clear the resolver after construction
registry.set_include_source_resolver(lambda source: overrides.get(source))
registry.set_include_source_resolver(None)  # Clear

BundleRegistry constructor parameters:

  • home (Path | None) — Base directory. Resolves in order: explicit parameter → AMPLIFIER_HOME env var → ~/.amplifier.
  • strict (bool, default False) — If True, include failures raise exceptions instead of logging warnings.
  • include_source_resolver (Callable[[str], str | None] | None) — Optional callback (source_uri) -> resolved_uri | None. Called before default include resolution logic; return None to fall back to default behavior. Can also be set after construction via set_include_source_resolver().

Registry Features

  • Named bundles - Register bundles by name
  • Caching - Downloaded bundles cached locally
  • Update checking - Check for available updates
  • Dependency tracking - Track includes relationships
  • Include source override - App-layer callback for custom include URI routing (include_source_resolver)

Best Practices

Use the Thin Bundle Pattern

When including foundation, don't redeclare what it provides. Your bundle.md should be minimal.

# ✅ GOOD: Thin bundle inherits from foundation
---
bundle:
  name: my-capability
  version: 1.0.0

includes:
  - bundle: git+https://github.com/microsoft/amplifier-foundation@main
  - bundle: my-capability:behaviors/my-capability    # Behavior pattern
---

# My Capability

@my-capability:context/instructions.md

---

@foundation:context/shared/common-system-base.md

Create Behaviors for Reusability

Package your agents + context in behaviors/ so others can include just your capability.

# behaviors/my-capability.yaml
bundle:
  name: my-capability-behavior
  version: 1.0.0
  description: Adds X capability with agents and context

agents:
  include:
    - my-capability:agent-one
    - my-capability:agent-two

context:
  include:
    - my-capability:context/instructions.md

Consolidate Instructions

Put instructions in context/instructions.md, not inline in bundle.md.

# context/instructions.md
# My Capability Instructions

You have access to the my-capability tool...

## Usage

[Detailed instructions]

## Agents Available

[Agent descriptions]

Reference from bundle.md:

@my-capability:context/instructions.md

Load-on-Demand Pattern

Not all context needs to load at session start. Use soft references (text without @) to make content available without consuming tokens until needed.

**Documentation (load on demand):**
- Schema: recipes:docs/RECIPE_SCHEMA.md
- Examples: recipes:examples/code-review-recipe.yaml
- Guide: foundation:docs/BUNDLE_GUIDE.md

The AI can load these via read_file when actually needed.

See Also