Practical patterns for using Amplifier Foundation.
For simple configurations:
---
bundle:
name: my-app
version: 1.0.0
session:
orchestrator: {module: loop-streaming}
context: {module: context-simple}
providers:
- module: provider-anthropic
source: git+https://github.com/microsoft/amplifier-module-provider-anthropic@main
---
You are a helpful assistant.For bundles with agents and context:
my-bundle/
├── bundle.md # Main configuration
├── agents/
│ ├── bug-hunter.md
│ └── code-reviewer.md
└── context/
├── guidelines.md
└── style.md
The bundle can reference these:
agents:
include:
- bug-hunter
- code-reviewer
context:
include:
- guidelines
- styleLayer environment-specific config on a base:
# base.py
base = Bundle(
name="base",
session={"orchestrator": "loop-streaming", "context": "context-simple"},
providers=[{"module": "provider-anthropic", "source": "...", "config": {"default_model": "claude-sonnet-4-5"}}],
)
# dev.py
dev = Bundle(
name="dev",
providers=[{"module": "provider-anthropic", "config": {"debug": True}}],
)
# prod.py
prod = Bundle(
name="prod",
providers=[{"module": "provider-anthropic", "config": {"timeout": 60}}],
)
# Usage
env = os.getenv("ENV", "dev")
overlay = dev if env == "dev" else prod
bundle = base.compose(overlay)Use includes: for declarative layering:
# base.md
bundle:
name: base
version: 1.0.0
session:
orchestrator: {module: loop-streaming}
providers:
- module: provider-anthropic
source: git+...# dev.md
bundle:
name: dev
version: 1.0.0
includes:
- ./base.md
providers:
- module: provider-anthropic
config: {debug: true}Compose features additively:
# Features as partial bundles
filesystem_tools = Bundle(
name="filesystem",
tools=[{"module": "tool-filesystem", "source": "..."}],
)
web_tools = Bundle(
name="web",
tools=[{"module": "tool-web", "source": "..."}],
)
# Compose what you need
full = base.compose(filesystem_tools, web_tools)Configure multiple providers for failover:
providers:
- module: provider-anthropic
source: git+...
config:
default_model: claude-sonnet-4-5
priority: 1
- module: provider-openai
source: git+...
config:
default_model: gpt-5.4
priority: 2Use mock provider in tests:
test_bundle = Bundle(
name="test",
providers=[{
"module": "provider-mock",
"source": "git+https://github.com/microsoft/amplifier-module-provider-mock@main",
"config": {"responses": ["Hello!", "How can I help?"]},
}],
)When resolving a caller-supplied provider/module name against a collection of
mounted instances, do not return on the first structural match. If 2+
instances of the same module can share an id/type match (e.g. a bare type
name like "anthropic" matching multiple provider-anthropic instances that
each have a distinct id:), first-match-wins silently serves whichever
instance happens to be first in list order -- not the one the caller
actually wanted. This exact anti-pattern was independently found and fixed
four times in one week: _find_provider_instance()
(amplifier-foundation PR #267), find_provider_by_type()
(amplifier-bundle-routing-matrix PR #31), and
get_provider_config()/_get_provider_config()/_find_provider_entry()
(amplifier-app-cli PR #214/#215).
The accepted fix: collect all structural matches, then resolve
deterministically to the highest-priority (lowest priority number,
defaulting to 100 when absent) instance instead of the first one found:
matches = [p for p in providers if _matches(p, name)]
if not matches:
return None
def _priority(p):
return p.get("config", {}).get("priority", 100)
return min(matches, key=_priority)A CI tripwire (scripts/ci_checks/resolver_priority_tripwire.py) detects
new instances of this pattern via AST analysis -- see its module docstring
for the exact detection rules, and .pre-commit-hooks.yaml for the
resolver-priority-tripwire hook id.
Load, prepare, execute:
bundle = await load_bundle("/path/to/bundle.md")
prepared = await bundle.prepare()
async with prepared.create_session() as session:
response = await session.execute("Hello!")
print(response)Session maintains context:
async with prepared.create_session() as session:
await session.execute("My name is Alice")
response = await session.execute("What's my name?")
# Response knows about AliceResume from session ID:
# First session
async with prepared.create_session() as session:
await session.execute("Remember: X=42")
session_id = session.session_id
# Later: resume
async with prepared.create_session(session_id=session_id) as session:
response = await session.execute("What is X?")
# Knows X=42In agents/bug-hunter.md:
---
agent:
name: bug-hunter
description: Finds and fixes bugs
providers:
- module: provider-anthropic
config:
default_model: claude-sonnet-4-5
temperature: 0.3 # More deterministic
---
You are an expert bug hunter. Find bugs systematically.# Load agent as bundle
agent_bundle = await load_bundle("./agents/bug-hunter.md")
# Spawn sub-session
result = await prepared.spawn(
child_bundle=agent_bundle,
instruction="Find the bug in auth.py",
compose=True, # Compose with parent bundle (default: True)
parent_session=session, # Inherit UX from parent
session_id=None, # Or provide ID to resume existing sub-session
)
# Returns: {"output": response, "session_id": child_id}
print(result["output"])Use provider_preferences for ordered fallback chains when spawning agents:
from amplifier_foundation import ProviderPreference
# Spawn with provider preference chain (tries in order)
result = await prepared.spawn(
child_bundle=agent_bundle,
instruction="Quick analysis task",
provider_preferences=[
ProviderPreference(provider="anthropic", model="claude-haiku-*"),
ProviderPreference(provider="openai", model="gpt-5-mini"),
],
)
# Model patterns use glob matching (fnmatch)
# "claude-haiku-*" resolves to latest matching model (e.g., "claude-haiku-20250110")By default, spawned agents inherit all tools from their parent. Configure tool inheritance in the task tool's config section:
# In your bundle.md
tools:
- module: tool-task
source: git+https://github.com/microsoft/amplifier-module-tool-task@main
config:
exclude_tools: [tool-task] # Agents inherit all EXCEPT theseOr specify an explicit allowlist:
tools:
- module: tool-task
config:
inherit_tools: [tool-filesystem, tool-bash] # Agents get ONLY theseCommon pattern: Prevent agents from delegating further:
# Coordinator has task tool for orchestration
tools:
- module: tool-filesystem
- module: tool-bash
- module: tool-task
config:
exclude_tools: [tool-task] # Spawned agents can't delegateThis ensures agents do the work themselves rather than trying to spawn sub-agents.
Design rationale: Tool inheritance config belongs in tool-task's config section because:
- The task tool is the module that consumes this config
- Follows the pattern of all other tool configs
- Without tool-task mounted, this config is meaningless
- Standard module-config merge rules apply during bundle composition
Apps typically resolve agent names to bundles:
async def resolve_agent(name: str, agent_configs: dict) -> Bundle:
"""Resolve agent name to Bundle."""
if name in agent_configs:
# Inline agent definition
return Bundle.from_dict(agent_configs[name])
# Look for agent file
agent_path = find_agent_file(name)
if agent_path:
return await load_bundle(agent_path)
raise ValueError(f"Unknown agent: {name}")
# Usage
agent_bundle = await resolve_agent("bug-hunter", bundle.agents)
result = await prepared.spawn(agent_bundle, instruction)from amplifier_foundation import load_bundle, validate_bundle_or_raise
bundle = await load_bundle(path)
validate_bundle_or_raise(bundle) # Raises if invalid
prepared = await bundle.prepare()For bundles that should be directly mountable:
from amplifier_foundation import BundleValidator
validator = BundleValidator()
result = validator.validate_completeness(bundle)
if not result.valid:
print("Missing required sections:")
for error in result.errors:
print(f" - {error}")Extend validator for app-specific rules:
class AppValidator(BundleValidator):
def validate(self, bundle):
result = super().validate(bundle)
# Custom rule: must have filesystem tool
has_filesystem = any(
t.get("module") == "tool-filesystem"
for t in bundle.tools
)
if not has_filesystem:
result.add_warning("Missing filesystem tool")
return resultRegister commonly-used bundles:
registry = BundleRegistry()
registry.register("foundation", "git+https://github.com/microsoft/amplifier-foundation-bundle@main")
registry.register("dev", "./bundles/dev.md")
# Load by name
bundle = await registry.load("foundation")Auto-register bundles from directory:
from pathlib import Path
def register_bundles(registry: BundleRegistry, directory: Path):
for path in directory.glob("*.md"):
name = path.stem # filename without extension
registry.register(name, str(path))
# Register all bundles in directory
register_bundles(registry, Path("./bundles"))from amplifier_foundation import (
load_bundle,
BundleNotFoundError,
BundleLoadError,
BundleValidationError,
)
try:
bundle = await load_bundle(path)
except BundleNotFoundError:
print(f"Bundle not found: {path}")
except BundleLoadError as e:
print(f"Failed to load bundle: {e}")
except BundleValidationError as e:
print(f"Invalid bundle: {e}")async def load_with_fallback(primary: str, fallback: str) -> Bundle:
try:
return await load_bundle(primary)
except BundleError:
return await load_bundle(fallback)from amplifier_foundation import DiskCache, BundleRegistry
from pathlib import Path
# DiskCache: Apps decide the cache location
cache_dir = Path("~/.myapp/cache/bundles").expanduser()
cache = DiskCache(cache_dir)
# BundleRegistry uses ~/.amplifier by default, or specify custom home
registry = BundleRegistry(home=Path("~/.myapp").expanduser())
# First load: downloads from git
bundle = await registry.load("git+https://...")
# Subsequent loads: uses cache
bundle = await registry.load("git+https://...")# Prepare once (downloads modules)
bundle = await load_bundle(path)
prepared = await bundle.prepare()
# Execute many times
for prompt in prompts:
async with prepared.create_session() as session:
response = await session.execute(prompt)- APPLICATION_INTEGRATION_GUIDE.md - Application lifecycle patterns, the protocol boundary pattern, and production anti-patterns