Creating Bundles with amplifier-foundation
May 20, 2026 · View on GitHub
Purpose: Guide for creating bundles to package AI agent capabilities using amplifier-foundation.
What is a Bundle?
A bundle is a composable unit of configuration that produces a mount plan for AmplifierSession. Bundles package:
- Tools - Capabilities the agent can use
- Agents - Sub-agent definitions for task delegation
- Hooks - Observability and control mechanisms
- Providers - LLM backend configurations
- Instructions - System prompts and context
- Spawn Policy - Controls what tools spawned agents inherit
Bundles are the primary way to share and compose AI agent configurations.
Key insight: Bundles are configuration, not Python packages. A bundle repo does not need a root pyproject.toml. (For the rare exception — a bundle that needs to share Python code across its modules, or that also ships a standalone CLI — see Bundle with Root Python Package.)
See also: For mechanism selection guidance and bundle design methodology, see
context/understanding-mechanisms/designing-with-mechanisms.md.
The Thin Bundle Pattern (Recommended)
Most bundles should be thin - inheriting from foundation and adding only their unique capabilities.
The Problem
When creating bundles that include foundation, a common mistake is to redeclare things foundation already provides:
# ❌ BAD: Fat bundle that duplicates foundation
includes:
- bundle: foundation
session: # ❌ Foundation already defines this!
orchestrator:
module: loop-streaming
source: git+https://github.com/...
context:
module: context-simple
tools: # ❌ Foundation already has these!
- module: tool-filesystem
source: git+https://github.com/...
- module: tool-bash
source: git+https://github.com/...
hooks: # ❌ Foundation already has these!
- module: hooks-streaming-ui
source: git+https://github.com/...
This duplication:
- Creates maintenance burden (update in two places)
- Can cause version conflicts
- Misses foundation updates automatically
The Solution: Thin Bundles
A thin bundle only declares what it uniquely provides:
# ✅ GOOD: Thin bundle inherits from foundation
---
bundle:
name: my-capability
version: 1.0.0
description: Adds X capability
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
That's it. All tools, session config, and hooks come from foundation.
Exemplar: amplifier-bundle-recipes
See amplifier-bundle-recipes for the canonical example:
# amplifier-bundle-recipes/bundle.md - Only 14 lines of YAML!
---
bundle:
name: recipes
version: 1.0.0
description: Multi-step AI agent orchestration for repeatable workflows
includes:
- bundle: git+https://github.com/microsoft/amplifier-foundation@main
- bundle: recipes:behaviors/recipes
---
# Recipe System
@recipes:context/recipe-instructions.md
---
@foundation:context/shared/common-system-base.md
Key observations:
- No
tools:,session:, orhooks:declarations (inherited from foundation) - Uses behavior pattern for its unique capabilities
- References consolidated instructions file
- Minimal markdown body
The Behavior Pattern
A behavior is a reusable capability add-on that bundles agents + context (and optionally tools/hooks). Behaviors live in behaviors/ and can be included by any bundle.
Why Behaviors?
Behaviors enable:
- Reusability - Add capability to any bundle
- Modularity - Separate concerns cleanly
- Composition - Mix and match behaviors
Behavior File Structure
# behaviors/my-capability.yaml
bundle:
name: my-capability-behavior
version: 1.0.0
description: Adds X capability with agents and context
# Optional: Add tools specific to this capability
tools:
- module: tool-my-capability
source: git+https://github.com/microsoft/amplifier-bundle-my-capability@main#subdirectory=modules/tool-my-capability
# Declare agents this behavior provides
agents:
include:
- my-capability:agent-one
- my-capability:agent-two
# Declare context files this behavior includes
context:
include:
- my-capability:context/instructions.md
Using Behaviors
Include a behavior in your bundle:
includes:
- bundle: foundation
- bundle: my-capability:behaviors/my-capability # From same bundle
- bundle: git+https://github.com/org/bundle@main#subdirectory=behaviors/foo.yaml # External
Exemplar: recipes behavior
See amplifier-bundle-recipes/behaviors/recipes.yaml:
bundle:
name: recipes-behavior
version: 1.0.0
description: Multi-step AI agent orchestration via declarative YAML recipes
tools:
- module: tool-recipes
source: git+https://github.com/microsoft/amplifier-bundle-recipes@main#subdirectory=modules/tool-recipes
config:
session_dir: ~/.amplifier/projects/{project}/recipe-sessions
auto_cleanup_days: 7
agents:
include:
- recipes:recipe-author
- recipes:result-validator
context:
include:
- recipes:context/recipe-instructions.md
Key observations:
- Adds a tool specific to this capability
- Declares the agents this behavior provides
- References consolidated context file
- Can be included by foundation OR any other bundle
Agent Definition Patterns: Include vs Inline
Both patterns are fully supported by the code. Choose based on your needs:
Pattern 1: Include (Recommended for most cases)
agents:
include:
- my-bundle:my-agent # Loads agents/my-agent.md
Use when: Agent is self-contained with its own instructions in a separate .md file.
Pattern 2: Inline (Valid for tool-scoped agents)
agents:
my-agent:
description: "Agent with bundle-specific tool access"
instructions: my-bundle:agents/my-agent.md
tools:
- module: tool-special # This agent gets specific tools
source: ./modules/tool-special
Use when: Agent needs bundle-specific tool configurations that differ from the parent bundle.
When to Use Each
| Scenario | Pattern | Why |
|---|---|---|
| Standard agent with own instructions | Include | Cleaner separation, context sink pattern |
| Agent needs specific tools | Inline | Can specify tools: for just this agent |
| Agent reused across bundles | Include | Separate file is more portable |
| Agent tightly coupled to bundle | Inline | Keep definition with bundle config |
Key insight: The code in bundle.py:_parse_agents() explicitly handles both patterns:
"Handles both include lists and direct definitions."
Neither pattern is deprecated. Both are intentional design choices for different use cases.
Context De-duplication
Consolidate instructions into a single file rather than inline in bundle.md.
The Problem
Inline instructions in bundle.md cause:
- Duplication if behavior also needs to reference them
- Large bundle.md files that are hard to maintain
- Harder to reuse context across bundles
The Solution: Consolidated Context Files
Create context/instructions.md with all the instructions:
# My Capability Instructions
You have access to the my-capability tool...
## Usage
[Detailed instructions]
## Agents Available
[Agent descriptions]
Reference it from your behavior:
# behaviors/my-capability.yaml
context:
include:
- my-capability:context/instructions.md
And from your bundle.md:
---
bundle:
name: my-capability
includes:
- bundle: foundation
- bundle: my-capability:behaviors/my-capability
---
# My Capability
@my-capability:context/instructions.md
---
@foundation:context/shared/common-system-base.md
Exemplar: recipes instructions
See amplifier-bundle-recipes/context/recipe-instructions.md:
- Single source of truth for recipe system instructions
- Referenced by both
behaviors/recipes.yamlANDbundle.md - No duplication
Directory Conventions
Bundle repos follow conventions that enable maximum reusability and composition. These are patterns, not code-enforced rules.
Structural vs Conventional: Bundles have two independent classification systems. For structural concepts (root bundles, nested bundles, namespace registration), see CONCEPTS.md. This section covers conventional organization patterns.
Standard Directory Layout
| Directory | Convention Name | Purpose |
|---|---|---|
/bundle.md | Root bundle | Repo's primary entry point, establishes namespace |
/bundles/*.yaml | Standalone bundles | Pre-composed, ready-to-use variants (e.g., "with-anthropic") |
/behaviors/*.yaml | Behavior bundles | "The value this repo provides" - compose onto YOUR bundle |
/providers/*.yaml | Provider bundles | Provider configurations to compose |
/agents/*.md | Agent files | Specialized agent definitions |
/context/*.md | Context files | Shared instructions, knowledge |
/modules/ | Local modules | Tool implementations specific to this bundle |
/docs/ | Documentation | Guides, references, examples |
Directory Purposes
Root bundle (/bundle.md): The primary entry point for your bundle. Establishes the namespace (from bundle.name) and typically includes its own behavior for DRY. This is both structurally a "root bundle" and conventionally the main entry point.
Standalone bundles (/bundles/*.yaml): Pre-composed variants ready to use as-is. Typically combine the root bundle with a provider choice. Examples: with-anthropic.yaml, minimal.yaml. These are structurally "nested bundles" (loaded via namespace:bundles/foo) but conventionally "standalone" because they're complete and ready to use.
Behavior bundles (/behaviors/*.yaml): The reusable capability this repo provides. When someone wants to add your capability to THEIR bundle, they include your behavior. Contains agents, context, and optionally tools. The root bundle should include its own behavior (DRY pattern).
Provider bundles (/providers/*.yaml): Provider configurations that can be composed onto other bundles. Allows users to choose which provider to use without the bundle author making that decision.
The Recommended Pattern
- Put your main value in
/behaviors/— this is what others compose onto their bundles (a "partial" bundle) - The standalone bundle MUST include its own behavior — this is the wiring path that makes the behavior's
context:,tools:, andhooks:reachable at runtime. The behavior file is inert until included. DRY is a secondary benefit; reachability is the primary one. /bundles/offers pre-composed variants — additional standalone bundles for users who want ready-to-run combinations
# bundle.md (root) - thin, includes own behavior
bundle:
name: my-capability
version: 1.0.0
includes:
- bundle: foundation
- bundle: my-capability:behaviors/my-capability # DRY: include own behavior
# bundles/with-anthropic.yaml - standalone variant
bundle:
name: my-capability-anthropic
version: 1.0.0
includes:
- bundle: my-capability # Root already has behavior
- bundle: foundation:providers/anthropic-opus # Add provider choice
Structural vs Conventional Classification
A bundle can be classified in BOTH systems independently:
| Bundle | Structural | Conventional |
|---|---|---|
/bundle.md | Root (is_root=True) | Root bundle |
/bundles/with-anthropic.yaml | Nested (is_root=False) | Standalone bundle |
/behaviors/my-capability.yaml | Nested (is_root=False) | Behavior bundle |
/providers/anthropic-opus.yaml | Nested (is_root=False) | Provider bundle |
Key insight: A "standalone bundle" (conventional) is still a "nested bundle" (structural) when loaded via namespace:bundles/foo.yaml. These aren't contradictions—they describe different aspects.
Bundle Directory Structure
Thin Bundle (Recommended)
my-bundle/
├── bundle.md # Thin: includes + context refs only
├── behaviors/
│ └── my-capability.yaml # Reusable behavior
├── agents/ # Agent definitions
│ ├── agent-one.md
│ └── agent-two.md
├── context/
│ └── instructions.md # Consolidated instructions
├── docs/ # Additional documentation
├── README.md
├── LICENSE
├── SECURITY.md
└── CODE_OF_CONDUCT.md
Bundle with Local Modules
my-bundle/
├── bundle.md
├── behaviors/
│ └── my-capability.yaml
├── agents/
├── context/
├── modules/ # Local modules (when needed)
│ └── tool-my-capability/
│ ├── pyproject.toml # Module's package config
│ └── my_module/
├── docs/
├── README.md
└── ...
Note: No pyproject.toml at the root. Only modules inside modules/ need their own pyproject.toml. (For the advanced case where modules need to share Python code, see Bundle with Root Python Package below.)
Bundle with Root Python Package (Advanced — Avoid If Possible)
You probably don't need this. Bundles are configuration, not Python packages. Before reaching for a root
pyproject.toml, work through the alternatives below. This pattern exists for cases that legitimately need it, but most bundles that reach for it don't.
A bundle can declare itself an installable Python package by adding a root pyproject.toml with [project] and [build-system]. When foundation loads such a bundle, its module activator (activate_bundle_package()) installs this package editable before any modules activate, and propagates the package's source root to sys.path so the bundle's modules — and child sessions spawned from them — can import from it.
Two legitimate uses for this pattern:
- Shared Python code across modules — multiple
modules/tool-*ormodules/hook-*need to import common types, clients, or helpers from the same bundle. - Standalone CLI + bundle assets — the bundle also ships a
uv tool install-able CLI that needs bundle assets at runtime. Example:amplifier-bundle-digital-twin-universeprovides theamplifier-digital-twinCLI which needs profile templates and container configs at runtime.
Consider these alternatives first
Before using this pattern, ask whether one of these solves your problem more cleanly:
-
Collapse into one larger module. If two modules need to share code, that's often a signal the module boundary is wrong. A single
modules/tool-my-thing/avoids the problem entirely. The "bricks and studs" philosophy says the right answer is often to redraw the module boundary, not to share code across it. -
Publish the shared code as its own package. If the shared code has independent value, put it on PyPI (or an internal registry) and let each module depend on it normally. The shared code becomes a real dependency rather than an ambient import. Cleanest separation.
-
Duplicate the helper. For a few utility functions, duplication is preferable to coupling. The dependency tax of a shared library isn't worth paying for a one-liner.
-
Use a root bundle package (this section). When the shared code is substantial, genuinely coupled to this bundle, and not publishable independently — or when you're shipping a standalone CLI alongside bundle assets — then use the patterns below.
Case (a): Shared Python code across modules
my-bundle/
├── bundle.md
├── pyproject.toml # Installable root package
├── src/
│ └── my_bundle_shared/ # Shared Python package
│ ├── __init__.py
│ ├── client.py
│ └── types.py
└── modules/
├── tool-foo/
│ └── pyproject.toml # Imports from my_bundle_shared
└── hook-bar/
└── pyproject.toml # Imports from my_bundle_shared
Modules import from the shared package by its normal package name:
# modules/tool-foo/__init__.py
from my_bundle_shared import SharedClient
from my_bundle_shared.types import Request
Root pyproject.toml:
[project]
name = "my-bundle-shared"
version = "0.1.0"
dependencies = []
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_bundle_shared"]
Foundation handles the rest automatically: activate_bundle_package() installs the root package editable before any modules activate, and adds src/ to sys.path so the modules and any child sessions inherit the import paths.
Anti-pattern warning: Do NOT try to wire shared code via
[tool.uv.sources]path overrides inside your modules'pyproject.tomlfiles. Foundation installs modules with--no-sources, which silently strips those overrides — the install looks successful but the shared package never reachessys.path. See[tool.uv.sources]Path Dependencies Silently Fail in Anti-Patterns.
Case (b): Standalone CLI + bundle assets
When the bundle also ships a standalone Python CLI tool, packaging needs extra care to avoid conflicts between the Python package namespace and bundle assets.
my-hybrid-bundle/
├── pyproject.toml # Python package config
├── src/my_package/ # Python code
│ ├── __init__.py
│ ├── cli.py
│ └── _bundle/ # Bundle assets INSIDE package
│ ├── bundle.yaml
│ ├── agents/
│ └── context/
├── modules/ # Tool modules (separate packages)
│ └── tool-my-tool/
├── bundle.md # Root entry point
└── README.md
Key pattern: Bundle assets go in a _bundle/ subdirectory INSIDE the Python package, not at the package root.
Why? When using hatch's force-include to put non-Python files in a wheel, the target path must NOT shadow the Python package namespace. See Packaging Anti-Patterns below.
pyproject.toml for case (b):
[project]
name = "my-hybrid-bundle"
version = "0.1.0"
dependencies = [...]
[project.scripts]
my-cli = "my_package.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
[tool.hatch.build.targets.wheel.force-include]
# Assets go INSIDE package, in _bundle/ subdirectory
"bundle.yaml" = "my_package/_bundle/bundle.yaml"
"agents" = "my_package/_bundle/agents"
"context" = "my_package/_bundle/context"
Testing case (b) packages: Always test with a built wheel, not just editable installs:
uv build --wheel
uv pip install dist/*.whl --force-reinstall
python -c "from my_package import SomeClass" # Verify imports work
Editable installs use source directories and may mask packaging bugs that only appear in built wheels.
Creating a Bundle Step-by-Step
Step 1: Decide Your Pattern
Ask yourself:
- Does my bundle add capability to foundation? → Use thin bundle + behavior pattern
- Is my bundle standalone (no foundation dependency)? → Declare everything you need
- Do I want my capability reusable by other bundles? → Create a behavior
Step 2: Create Behavior (if adding to foundation)
Create behaviors/my-capability.yaml:
bundle:
name: my-capability-behavior
version: 1.0.0
description: Adds X capability
agents:
include:
- my-capability:my-agent
context:
include:
- my-capability:context/instructions.md
Step 3: Create Consolidated Instructions
Create context/instructions.md:
# My Capability Instructions
You have access to the my-capability tool for [purpose].
## Available Agents
- **my-agent** - Does X, useful for Y
## Usage Guidelines
[Instructions for the AI on how to use this capability]
Step 4: Create Agent Definitions
Place agent files in agents/ with proper frontmatter:
---
meta:
name: my-agent
description: "Description shown when listing agents. Include usage examples..."
---
# My Agent
You are a specialized agent for [specific purpose].
## Your Capabilities
[Agent-specific instructions]
Step 5: Create Thin bundle.md
---
bundle:
name: my-capability
version: 1.0.0
description: Provides X capability
includes:
- bundle: git+https://github.com/microsoft/amplifier-foundation@main
- bundle: my-capability:behaviors/my-capability
---
# My Capability
@my-capability:context/instructions.md
---
@foundation:context/shared/common-system-base.md
Step 6: Add README and Standard Files
Create README.md documenting:
- What the bundle provides
- The architecture (thin bundle + behavior pattern)
- How to load/use it
Creating Tool Modules
When your behavior includes a tools: entry, you need a Python module that registers the tool at session startup. This section covers the full contract.
Full skill available: Load
creating-amplifier-modulesfor complete examples, test patterns, and anti-rationalization guidance.
Module Directory Structure
modules/tool-{name}/
├── pyproject.toml # Package config with entry point
└── amplifier_module_tool_{name}/
└── __init__.py # Defines tool class + mount()
The mount() Contract — Iron Law
mount() MUST call coordinator.mount(). A mount() that logs and returns None WILL fail with:
protocol_compliance: No tool was mounted and mount() did not return a Tool instance
This error fires every time any agent using the behavior is spawned — not just in testing.
Minimal Complete Example
"""Amplifier tool module for {name}."""
import logging
from typing import Any
from amplifier_core import ToolResult
logger = logging.getLogger(__name__)
class MyTool:
@property
def name(self) -> str:
return "my_tool"
@property
def description(self) -> str:
return "What this tool does."
@property
def input_schema(self) -> dict:
return {"type": "object", "properties": {"param": {"type": "string"}}, "required": ["param"]}
async def execute(self, input_data: dict[str, Any]) -> ToolResult:
return ToolResult(success=True, output=do_the_work(input_data["param"]))
async def mount(coordinator: Any, config: dict[str, Any] | None = None) -> dict[str, Any]:
tool = MyTool()
await coordinator.mount("tools", tool, name=tool.name) # ← REQUIRED
return {"name": "tool-my-tool", "version": "0.1.0", "provides": ["my_tool"]}
Phase 1 Placeholder Pattern
When the tool logic isn't implemented yet, create a real tool class that returns "not yet implemented." Do not skip the class or skip coordinator.mount(). A placeholder IS a real tool — it just tells callers it's pending:
class MyToolPlaceholder:
@property
def name(self) -> str: return "my_tool"
@property
def description(self) -> str: return "My tool — Phase 2 implementation pending."
@property
def input_schema(self) -> dict: return {"type": "object", "properties": {}}
async def execute(self, input_data: dict[str, Any]) -> ToolResult:
return ToolResult(success=False, output="Not yet implemented. Phase 2 pending.")
async def mount(coordinator: Any, config: dict[str, Any] | None = None) -> dict[str, Any]:
tool = MyToolPlaceholder()
await coordinator.mount("tools", tool, name=tool.name) # ← still REQUIRED
return {"name": "tool-my-tool", "version": "0.1.0", "provides": ["my_tool"]}
async def on_session_ready(coordinator) → None (optional)
Called once, after every module across all phases has completed mount(). The coordinator is fully composed at this point: all providers, tools, hooks, capabilities, and contributions are registered and visible. Return value is ignored. Exceptions are non-fatal — logged as warnings, remaining on_session_ready() calls continue.
When to use it:
- You need to discover what other modules registered (events, capabilities, contributions) and can't know that at
mount()time - You need to wire your module to another module's capability and want a guarantee it's present
- You're replacing a defensive "check if capability exists yet" pattern with a clean single path
Example — eliminating a defensive dual-path:
# Before — defensive dual-path in mount() (register handler AND check immediately,
# because mount order determines whether the capability exists yet)
async def mount(coordinator, config):
coordinator.hooks.register("skills:discovered", on_skills_discovered)
# Also check immediately in case tool-skills already ran
if coordinator.get_capability("skills.discovery") is not None:
await _fetch_skills(coordinator) # duplicate path
# After — single path in on_session_ready() (all modules mounted, no ordering assumption)
async def mount(coordinator, config):
pass # or any setup that doesn't need full composition
async def on_session_ready(coordinator):
# tools are guaranteed mounted; one path, capability is either present or absent
if coordinator.get_capability("skills.discovery") is not None:
await _fetch_skills(coordinator)
For the full contract, see core:CONTRACTS.md § on_session_ready.
pyproject.toml Entry Point
[project]
name = "amplifier-module-tool-{name}"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [] # amplifier-core is a peer dep — do NOT declare it here
[project.entry-points."amplifier.modules"]
tool-{name} = "amplifier_module_tool_{name}:mount"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["amplifier_module_tool_{name}"]
Behavior YAML Reference
tools:
- module: tool-{name}
source: ./modules/tool-{name} # local path
# OR for published modules:
# source: git+https://github.com/org/repo@main#subdirectory=modules/tool-{name}
Anti-Patterns to Avoid
❌ Duplicating Foundation
# DON'T DO THIS when you include foundation
includes:
- bundle: foundation
tools:
- module: tool-filesystem # Foundation has this!
source: git+https://...
session:
orchestrator: # Foundation has this!
module: loop-streaming
Why it's bad: Creates maintenance burden, version conflicts, misses foundation updates.
Fix: Remove duplicated declarations. Foundation provides them.
❌ Inline Instructions in bundle.md
---
bundle:
name: my-bundle
---
# Instructions
[500 lines of instructions here]
## Usage
[More instructions]
Why it's bad: Can't be reused by behavior, hard to maintain, can't be referenced separately.
Fix: Move to context/instructions.md and reference with @my-bundle:context/instructions.md.
❌ Skipping the Behavior Pattern
# DON'T DO THIS for capability bundles
---
bundle:
name: my-capability
includes:
- bundle: foundation
agents:
include:
- my-capability:agent-one
- my-capability:agent-two
---
[All instructions inline]
Why it's bad: Your capability can't be added to other bundles without including your whole bundle.
Fix: Create behaviors/my-capability.yaml with agents + context, then include it.
❌ Fat Bundles for Simple Capabilities
# DON'T create complex bundles when a behavior would suffice
---
bundle:
name: simple-feature
version: 1.0.0
includes:
- bundle: foundation
session:
orchestrator: ... # Unnecessary
context: ... # Unnecessary
tools:
- module: tool-x # Could be in behavior
source: ...
agents:
include: # Could be in behavior
- simple-feature:agent-a
---
[Instructions that could be in context/]
Fix: If you're just adding agents + maybe a tool, use a behavior YAML only.
❌ Using @ Prefix in YAML
# DON'T DO THIS - @ prefix is for markdown only
context:
include:
- "@my-bundle:context/instructions.md" # ❌ @ doesn't belong here
agents:
include:
- "@my-bundle:my-agent" # ❌ @ doesn't belong here
# DO THIS - bare namespace:path in YAML
context:
include:
- my-bundle:context/instructions.md # ✅ No @ in YAML
agents:
include:
- my-bundle:my-agent # ✅ No @ in YAML
Why it's wrong: The @ prefix is markdown syntax for eager file loading. YAML sections use bare namespace:path references. Using @ in YAML causes silent failure - the path won't resolve and content won't load, with no error message.
❌ Using Repository Name as Namespace
# If loading: git+https://github.com/microsoft/amplifier-bundle-recipes@main
# And bundle.name in that repo is: "recipes"
# DON'T DO THIS
agents:
include:
- amplifier-bundle-recipes:recipe-author # ❌ Repo name
# DO THIS
agents:
include:
- recipes:recipe-author # ✅ bundle.name value
Why it's wrong: The namespace is ALWAYS bundle.name from the YAML frontmatter, regardless of the git URL, repository name, or file path.
❌ Including Subdirectory in Paths
# If loading: git+https://...@main#subdirectory=bundles/foo
# And bundle.name is: "foo"
# DON'T DO THIS
context:
include:
- foo:bundles/foo/context/instructions.md # ❌ Redundant path
# DO THIS
context:
include:
- foo:context/instructions.md # ✅ Relative to bundle location
Why it's wrong: When loaded via #subdirectory=X, the bundle root IS X/. Paths are relative to that root, so including the subdirectory in the path duplicates it.
Understanding context.include vs @mentions - They Have Different Semantics!
These two patterns are NOT interchangeable - they have fundamentally different composition behavior:
| Pattern | Composition Behavior | Use When |
|---|---|---|
context.include | ACCUMULATES - content propagates to including bundles | Behaviors that inject context into parents |
@mentions | REPLACES - stays with this instruction only | Direct references in your own instruction |
How context.include Works (bundle.py:174-186)
When Bundle A includes Bundle B, all context from both bundles merges:
# During compose(): context ACCUMULATES
for key, path in other.context.items():
result.context[prefixed_key] = path # Added to composed result!
Content is appended to the system prompt with # Context: {name} headers.
How @mentions Work (bundle.py:958-977)
@mentions are resolved from the final instruction and content is prepended as XML:
<context_file paths="@my-bundle:context/file.md → /abs/path">
[file content]
</context_file>
---
[instruction with @mention still present as semantic reference]
When to Use Each Pattern
Use context.include in behaviors (.yaml files):
# behaviors/my-behavior.yaml
# This context will propagate to ANY bundle that includes this behavior
context:
include:
- my-bundle:context/behavior-instructions.md
Use @mentions in root bundles (.md files):
---
bundle:
name: my-bundle
---
# Instructions
@my-bundle:context/my-instructions.md # Stays with THIS instruction
Why This Matters
If you use context.include in a root bundle.md:
- That context will propagate to any bundle that includes yours
- May not be what you intended for a "final" bundle
If you use @mentions in a behavior:
- The instruction (containing the @mention) replaces during composition
- Your @mention may get overwritten by the including bundle's instruction
The pattern exists for a reason: Behaviors use context.include because they WANT their context to propagate. Root bundles use @mentions because they're the final instruction.
Behavior context.include Policy — Lightweight Awareness Only
The "context propagates to including bundles" property of context.include is the reason it's powerful — and exactly the reason it must be used sparingly. Anything you put in a behavior's context.include lands in the always-on system prompt for every session that composes that behavior. Heavy reference docs in context.include are the single most common source of session prompt bloat in the ecosystem.
Policy (also enforced by foundation:recipes/validate-bundle-repo.yaml):
Per-file size in context.include | Status |
|---|---|
| < 500 tokens | OK if it's a genuine awareness pointer ("X exists; delegate to Y for the heavy work") |
| 500–1,000 tokens | WARNING — must justify universal relevance, or move to a context-sink mechanism |
| > 1,000 tokens | ERROR — move it. Period. |
The right home for heavy content:
- Expert agent body (
@-mentionin agent.md) — loaded only when the agent is delegated to. This is the canonical "context-sink" pattern foundation recommends. See AGENT_AUTHORING.md §"Agents as Context Sinks." - Mode contribution (
@-mentionin mode.mdbody ORcontributes.contextin mode YAML frontmatter) — loaded only when the mode is active. Best for workflow-specific reference docs. - Skill (
load_skillon demand) — loaded only when explicitly invoked. Best for procedural workflows. - Soft reference in prose (no
@prefix, just the path) — agent mustread_fileit when needed. Useful for occasional inline lookup.
Worked example (good):
# behaviors/my-domain.yaml
bundle:
name: my-domain-behavior
version: 1.0.0
agents:
include:
- my-bundle:my-domain-expert # Heavy domain knowledge lives in the agent body
# context.include is intentionally empty.
# The agent's meta.description is the discovery surface; the LLM finds it in the
# delegate tool catalog and delegates when the domain matches.
# behaviors/my-domain-with-awareness.yaml
# Acceptable variant: a tiny breadcrumb if delegation isn't reliable enough
# in the target audience of this behavior. Keep it under 500 tokens.
context:
include:
- my-bundle:context/my-domain-awareness.md # ~30 lines, "domain exists, delegate"
Worked example (bad — anti-pattern):
# ❌ DON'T DO THIS
context:
include:
- my-bundle:docs/FULL_REFERENCE.md # 2,000 tokens loaded every session
- my-bundle:docs/METHODOLOGY.md # 3,000 tokens loaded every session
- my-bundle:docs/CASE_STUDIES.md # 5,000 tokens loaded every session
These belong in agents/my-domain-expert.md as @-mention lines, OR behind a mode's contributes.context. They do NOT belong in always-on context.
The audit pattern: any file you'd consider putting in behavior context.include must justify "every session that uses this behavior NEEDS this content always loaded." If the answer is "well, sometimes…" — find another mechanism. The default for files >1,000 tokens is not behavior context.include.
This policy was added after several real bundles (systems-design, superpowers, parallax-discovery, rust-dev, python-dev, foundation/amplifier-dev, core, notify, containers, dot-graph, lsp) shipped with heavy context.include lists that turned out to be loading in every session for users who never used the affected workflow. The cumulative cost was ~15-20K tokens per session prompt. Migration shipped May 2026 to enforce the policy. See those bundles' commit history for worked migration examples.
❌ force-include Shadowing Python Namespace
# DON'T DO THIS - shadows the Python package!
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
[tool.hatch.build.targets.wheel.force-include]
"agents" = "my_package/agents" # ❌ Creates my_package/ with no __init__.py!
"context" = "my_package/context" # ❌ Shadows the actual Python package
# DO THIS - use _bundle/ subdirectory
[tool.hatch.build.targets.wheel.force-include]
"agents" = "my_package/_bundle/agents" # ✅ Inside package, won't shadow
"context" = "my_package/_bundle/context" # ✅ Python imports still work
Why it's wrong: hatch's force-include creates directories in the wheel. If you target my_package/agents, it creates a my_package/ directory with just agents/ inside (no __init__.py, no Python code). Python finds this directory first and treats it as a namespace package, shadowing your actual Python package. Result: from my_package import X fails with ImportError.
The fix: Put non-Python assets in a subdirectory like _bundle/ or data/ inside the package namespace.
Critical: This bug only appears in built wheels, not editable installs. Always test with uv build && uv pip install dist/*.whl.
❌ [tool.uv.sources] Path Dependencies Silently Fail
# DON'T DO THIS in modules/tool-foo/pyproject.toml
[project]
dependencies = ["my-bundle-shared"]
[tool.uv.sources]
my-bundle-shared = { path = "../../src/my_bundle_shared" } # ❌ Silently stripped
# DO THIS - rely on the bundle's root package (see Bundle with Root Python Package)
[project]
dependencies = [] # ✅ Shared code arrives via activate_bundle_package() → sys.path
Why it's wrong: Foundation's module activator passes --no-sources to every uv pip install when activating modules. This prevents rebuild surprises but also silently strips any [tool.uv.sources] overrides — the install succeeds, the module imports fail at runtime.
The failure mode: Install logs look clean. Unit tests may pass in a dev environment where the shared package happens to be on sys.path for other reasons. Then the bundle fails in production with ImportError: No module named 'my_bundle_shared'.
The fix: If modules legitimately need shared code, use the Bundle with Root Python Package pattern. Foundation's activate_bundle_package() installs the bundle's root package editable and adds its source directory to sys.path before modules activate — shared imports work automatically in modules and in any child sessions they spawn.
Rule of thumb: [tool.uv.sources] is fine for local development of a module repo in isolation, but it has no effect at runtime when foundation activates the module. Don't rely on it to wire shared code across a bundle's modules.
❌ Declaring amplifier-core as Runtime Dependency
# DON'T DO THIS in modules/tool-*/pyproject.toml
[project]
dependencies = [
"amplifier-core>=1.0.0", # ❌ Not on PyPI, will fail
"amplifier-bundle-foo>=0.1.0", # ❌ Not on PyPI, will fail
]
# DO THIS - no runtime dependencies for tool modules
[project]
dependencies = [] # ✅ amplifier-core is a peer dependency
Why it's wrong: Tool modules run inside the host application's process (amplifier-app-cli), which already has amplifier-core loaded. These packages aren't on PyPI, so declaring them as dependencies causes installation failures.
The pattern: amplifier-core and bundle packages are peer dependencies - they're provided by the runtime environment, not installed as dependencies.
Decision Framework
When to Include Foundation
| Scenario | Recommendation |
|---|---|
| Adding capability to AI assistants | ✅ Include foundation |
| Creating standalone tool | ❌ Don't need foundation |
| Need base tools (filesystem, bash, web) | ✅ Include foundation |
| Building on existing bundle | ✅ Include that bundle |
When to Use Behaviors
| Scenario | Recommendation |
|---|---|
| Adding agents + context | ✅ Use behavior |
| Adding tool + agents | ✅ Use behavior |
| Want others to use your capability | ✅ Use behavior |
| Creating a simple bundle variant | ❌ Just use includes |
When to Create Local Modules
| Scenario | Recommendation |
|---|---|
| Tool is bundle-specific | ✅ Keep in modules/ |
| Tool is generally useful | ❌ Extract to separate repo |
| Multiple bundles need the tool | ❌ Extract to separate repo |
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
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
Source URI Formats
Bundles support multiple source formats for modules:
| 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.
Composition with includes:
Bundles can inherit from other bundles:
includes:
- bundle: foundation # Well-known bundle name
- bundle: git+https://github.com/... # Git URL
- bundle: ./bundles/variant.yaml # Local file
- bundle: my-bundle:behaviors/foo # Behavior within same bundle
Merge rules:
- Later bundles override earlier ones
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)
App-Level Runtime Injection
Bundles define what capabilities exist. Apps inject how they run at runtime.
What Apps Inject
| Injection | Source | Example |
|---|---|---|
| Provider configs | settings.yaml providers | API keys, model selection |
| Tool configs | settings.yaml modules.tools | allowed_write_paths for filesystem |
| Session overrides | Session-scoped settings | Temporary path permissions |
Settings Structure
# ~/.amplifier/settings.yaml
providers:
- module: provider-anthropic
config:
api_key: ${ANTHROPIC_API_KEY}
modules:
tools:
- module: tool-filesystem
config:
allowed_write_paths:
- /home/user/projects
- ~/.amplifier
Tool configs are deep-merged by module ID - your settings extend the bundle's config, not replace it.
Implications for Bundle Authors
Don't declare in bundles:
- Provider API keys or model preferences → App injects from settings
- Environment-specific paths → App injects via tool config
- User preferences → App handles them
This enables:
- Same bundle works across environments
- Secrets stay out of version control
- Apps can restrict/expand tool capabilities per context
The Full Composition Chain
Foundation → Your bundle → App settings → Session overrides
↓ ↓ ↓ ↓
(tools) (agents) (providers, (temporary
tool configs) permissions)
Policy Behaviors
Some behaviors are app-level policies that should:
- Only apply to root/interactive sessions (not sub-agents or recipe steps)
- Be added by the app, not baked into bundles
- Be configurable per-app context
Examples of policy behaviors:
- Notifications (don't notify for every sub-agent)
- Cost tracking alerts
- Session duration limits
Pattern for bundle authors: If your behavior should be a policy (root-only, app-controlled):
- Don't include it in your bundle.md - provide it as a separate behavior
- Document it as a policy behavior - so apps know to compose it
- Check
parent_idin hooks - skip sub-sessions by default
# In your hook
async def handle_event(self, event: str, data: dict) -> HookResult:
# Policy behavior: skip sub-sessions
if data.get("parent_id"):
return HookResult(action="continue")
# ... root session logic
Pattern for app developers:
Configure policy behaviors in settings.yaml:
config:
notifications:
desktop:
enabled: true
push:
enabled: true
service: ntfy
topic: "my-topic"
The app composes these behaviors onto bundles at runtime, only for root sessions.
For detailed guidance, see POLICY_BEHAVIORS.md.
Using @mentions for Context
Reference files in your bundle's instructions without a separate context: section:
---
bundle:
name: my-bundle
---
# Instructions
Follow the guidelines in @my-bundle:docs/GUIDELINES.md
For API details, see @my-bundle:docs/API.md
Format: @namespace:path/to/file.md
The namespace is the bundle name. Paths are relative to the bundle root.
Syntax Quick Reference
There are two different syntaxes for referencing files, and they are NOT interchangeable:
| Location | Syntax | Example |
|---|---|---|
| Markdown body (bundle.md, agents/*.md) | @namespace:path | @my-bundle:context/guide.md |
| YAML sections (context.include, agents.include) | namespace:path (NO @) | my-bundle:context/guide.md |
The @ prefix is only for markdown text that gets processed during instruction loading. YAML sections use bare namespace:path references.
See Anti-Patterns to Avoid for common syntax mistakes.
Load-on-Demand Pattern (Soft References)
Not all context needs to load at session start. Use soft references (text without @) to make content available without consuming tokens until needed.
The Problem
Every @mention loads content eagerly at session creation, consuming tokens immediately:
# These ALL load at session start (~15,000 tokens)
# Syntax: @<bundle>:<path>
foundation:docs/BUNDLE_GUIDE.md # ~5,700 tokens
amplifier:docs/MODULES.md # ~4,600 tokens
recipes:examples/code-review.yaml # ~5,000 tokens
(Prepend @ to each line above to see actual eager loading)
The Solution: Soft References
Reference files by path WITHOUT the @ prefix. The AI can load them on-demand via read_file:
**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 sees these references and can load them when actually needed.
When to Use Each Pattern
| Pattern | Syntax | Loads | Use When |
|---|---|---|---|
| @mention | @bundle:path | Immediately | Content is ALWAYS needed |
| Soft reference | bundle:path (no @) | On-demand | Content is SOMETIMES needed |
| Agent delegation | Delegate to expert agent | When spawned | Content belongs to a specialist |
Best Practice: Context Sink Agents
For heavy documentation, create specialized "context sink" agents that @mention the docs. The root session stays light; heavy context loads only when that agent is spawned.
Example: Instead of @mentioning MODULES.md (~4,600 tokens) in the root bundle:
# BAD: Heavy root context (in bundle.md)
amplifier:docs/MODULES.md # <- @mention loads ~4,600 tokens every session
Create an expert agent that owns that knowledge:
# GOOD: In agents/ecosystem-expert.md (agent owns this knowledge)
amplifier:docs/MODULES.md # <- @mention here loads only when agent spawns
amplifier:docs/REPOSITORY_RULES.md # <- same - deferred loading
The root bundle uses a soft reference and delegates:
# Root bundle.md
For ecosystem questions, delegate to amplifier:amplifier-expert which has
authoritative access to amplifier:docs/MODULES.md and related documentation.
Key Insight
Every @mention is a token budget decision. Ask yourself:
- Is this content needed for EVERY conversation? -> @mention
- Is this content needed for SOME conversations? -> Soft reference
- Does this content belong to a specific domain? -> Move to specialist agent
Loading a Bundle
# Load from local file
amplifier run --bundle ./bundle.md "prompt"
# Load from git URL
amplifier run --bundle git+https://github.com/org/amplifier-bundle-foo@main "prompt"
# Include in another bundle
includes:
- bundle: git+https://github.com/org/amplifier-bundle-foo@main
Best Practices
Use the Thin Bundle Pattern
When including foundation, don't redeclare what it provides. Your bundle.md should be minimal.
Create Behaviors for Reusability
Package your agents + context in behaviors/ so others can include just your capability.
Consolidate Instructions
Put instructions in context/instructions.md, not inline in bundle.md.
Keep Modules Local When Possible
For bundle-specific tools, keep them in modules/ within the bundle:
- Simpler distribution (one repo)
- Versioning stays synchronized
- No external dependency management
Extract to separate repo only when:
- Multiple bundles need the same module
- Module needs independent versioning
- Module is generally useful outside the bundle
Use Descriptive Agent Metadata
The meta.description is shown when listing agents. Include:
- What the agent does
- When to use it
- Usage examples in the description string
No Root pyproject.toml
Bundles are configuration, not Python packages. Don't add a pyproject.toml at the bundle root. See Bundle with Root Python Package for the rare exception — bundles that need to share Python code across modules, or that also ship a standalone CLI.
Complete Example: amplifier-bundle-recipes
See amplifier-bundle-recipes for the canonical example of the thin bundle + behavior pattern:
amplifier-bundle-recipes/
├── bundle.md # THIN: 14 lines of YAML, just includes
├── behaviors/
│ └── recipes.yaml # Behavior: tool + agents + context
├── agents/
│ ├── recipe-author.md # Conversational recipe creation
│ └── result-validator.md # Pass/fail validation
├── context/
│ └── recipe-instructions.md # Consolidated instructions
├── modules/
│ └── tool-recipes/ # Local tool implementation
├── docs/ # Comprehensive documentation
├── examples/ # Working examples
├── templates/ # Starter templates
├── README.md
└── ...
Key patterns demonstrated:
- Thin bundle.md - Only includes foundation + behavior
- Behavior pattern -
behaviors/recipes.yamldefines the capability - Context de-duplication - Instructions in
context/recipe-instructions.md - Local module -
modules/tool-recipes/with source reference - No duplication - Nothing from foundation is redeclared
Troubleshooting
"Module not found" errors
- Verify
source:path is correct relative to bundle location - Check module has
pyproject.tomlwith entry point - Ensure
mount()function exists in module
"protocol_compliance: No tool was mounted" error
This fires when mount() returns None without calling coordinator.mount(). The validator requires that every module registers something.
Fix: Your mount() must call await coordinator.mount("tools", tool, name=tool.name). A placeholder tool class (that returns "not yet implemented" when called) is fine — but you MUST have a class and MUST call coordinator.mount(). See the Creating Tool Modules section above for the complete pattern, or load the creating-amplifier-modules skill.
Agent not loading
- Verify
meta:frontmatter exists withnameanddescription - Check agent file is in
agents/directory - Verify
agents: include:uses correct namespace prefix
@mentions not resolving
- Verify file exists at the referenced path
- Check namespace matches bundle name
- Ensure path is relative to bundle root
Behavior not applying
- Verify the standalone bundle's
includes:list contains- bundle: <name>:behaviors/<name>. A behavior file on disk that is never included is silently inert — the most common cause of "my behavior isn't loading." - Verify behavior YAML syntax is correct
- Check include path:
my-bundle:behaviors/name(notmy-bundle:behaviors/name.yaml) - Ensure behavior declares
agents:and/orcontext:sections
Reference
- amplifier-bundle-recipes - Canonical example of thin bundle + behavior pattern
- URI Formats - Complete source URI documentation
- Validation - Bundle validation rules
- API Reference - Programmatic bundle loading