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:
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
modulefield - Module
configmust be a dict if present session.orchestratormust be a string or dict (withmodule/sourcekey)session.contextmust be a string or dict (withmodule/sourcekey)- Agent entries must be dicts
- Context file paths must exist (warning in default mode; error in
strict=Truemode)
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, defaultTrue) — 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, defaultTrue) — Whether to compose child with parent bundle.parent_session(Any, defaultNone) — 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, default0) — 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. Seeamplifier-app-cli/session_spawner.pyfor 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_HOMEenv var →~/.amplifier.strict(bool, defaultFalse) — 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; returnNoneto fall back to default behavior. Can also be set after construction viaset_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:
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¶
- BUNDLE_GUIDE.md - Complete bundle creation guide
- Core Concepts - Bundle mental model
- API Reference - Programmatic API