Purpose: Guide for creating bundles to package AI agent capabilities using amplifier-foundation.
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.)
Most bundles should be thin - inheriting from foundation and adding only their unique capabilities.
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
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.mdThat's it. All tools, session config, and hooks come from foundation.
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.mdKey observations:
- No
tools:,session:, orhooks:declarations (inherited from foundation) - Uses behavior pattern for its unique capabilities
- References consolidated instructions file
- Minimal markdown body
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.
Behaviors enable:
- Reusability - Add capability to any bundle
- Modularity - Separate concerns cleanly
- Composition - Mix and match behaviors
# 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.mdInclude 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 # ExternalSee 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.mdKey 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
Both patterns are fully supported by the code. Choose based on your needs:
agents:
include:
- my-bundle:my-agent # Loads agents/my-agent.mdUse when: Agent is self-contained with its own instructions in a separate .md file.
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-specialUse when: Agent needs bundle-specific tool configurations that differ from the parent bundle.
| 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.
Consolidate instructions into a single file rather than inline in bundle.md.
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
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.mdAnd 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.mdSee amplifier-bundle-recipes/context/recipe-instructions.md:
- Single source of truth for recipe system instructions
- Referenced by both
behaviors/recipes.yamlANDbundle.md - No duplication
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.
| 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 |
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.
- Put your main value in
/behaviors/- this is what others compose onto their bundles - Root bundle includes its own behavior - DRY, root bundle stays thin
/bundles/offers pre-composed variants - convenience 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 choiceA 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.
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
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.)
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-shadowprovides theamplifier-shadowCLI which needs container configs.
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.
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 RequestRoot 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.
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 workEditable installs use source directories and may mask packaging bugs that only appear in built wheels.
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
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.mdCreate 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]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]---
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.mdCreate README.md documenting:
- What the bundle provides
- The architecture (thin bundle + behavior pattern)
- How to load/use it
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.
modules/tool-{name}/
├── pyproject.toml # Package config with entry point
└── amplifier_module_tool_{name}/
└── __init__.py # Defines tool class + mount()
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.
"""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"]}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"]}[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}"]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}# 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-streamingWhy it's bad: Creates maintenance burden, version conflicts, misses foundation updates.
Fix: Remove duplicated declarations. Foundation provides them.
---
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.
# 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.
# 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.
# 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 YAMLWhy 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.
# 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 valueWhy it's wrong: The namespace is ALWAYS bundle.name from the YAML frontmatter, regardless of the git URL, repository name, or file path.
# 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 locationWhy 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.
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 |
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.
@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]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.mdUse @mentions in root bundles (.md files):
---
bundle:
name: my-bundle
---
# Instructions
@my-bundle:context/my-instructions.md # Stays with THIS instructionIf 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.
# 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 workWhy 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.
# 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.pathWhy 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.
# 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 dependencyWhy 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.
| 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 |
| 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 |
| 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 |
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.mdBundles 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.
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 bundleMerge 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)
Bundles define what capabilities exist. Apps inject how they run at runtime.
| 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 |
# ~/.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
- ~/.amplifierTool configs are deep-merged by module ID - your settings extend the bundle's config, not replace it.
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
Foundation → Your bundle → App settings → Session overrides
↓ ↓ ↓ ↓
(tools) (agents) (providers, (temporary
tool configs) permissions)
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 logicPattern 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.
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.mdFormat: @namespace:path/to/file.md
The namespace is the bundle name. Paths are relative to the bundle root.
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.
Not all context needs to load at session start. Use soft references (text without @) to make content available without consuming tokens until needed.
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)
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.mdThe AI sees these references and can load them when actually needed.
| 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 |
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.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
# 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@mainWhen including foundation, don't redeclare what it provides. Your bundle.md should be minimal.
Package your agents + context in behaviors/ so others can include just your capability.
Put instructions in context/instructions.md, not inline in bundle.md.
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
The meta.description is shown when listing agents. Include:
- What the agent does
- When to use it
- Usage examples in the description string
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.
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
- Verify
source:path is correct relative to bundle location - Check module has
pyproject.tomlwith entry point - Ensure
mount()function exists in module
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.
- Verify
meta:frontmatter exists withnameanddescription - Check agent file is in
agents/directory - Verify
agents: include:uses correct namespace prefix
- Verify file exists at the referenced path
- Check namespace matches bundle name
- Ensure path is relative to bundle root
- 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
- 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