Skip to content

Latest commit

 

History

History
1528 lines (1087 loc) · 47.7 KB

File metadata and controls

1528 lines (1087 loc) · 47.7 KB

Creating Bundles with amplifier-foundation

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.)


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:, or hooks: 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.yaml AND bundle.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

  1. Put your main value in /behaviors/ - this is what others compose onto their bundles
  2. Root bundle includes its own behavior - DRY, root bundle stays thin
  3. /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 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:

  1. Shared Python code across modules — multiple modules/tool-* or modules/hook-* need to import common types, clients, or helpers from the same bundle.
  2. Standalone CLI + bundle assets — the bundle also ships a uv tool install-able CLI that needs bundle assets at runtime. Example: amplifier-bundle-shadow provides the amplifier-shadow CLI which needs container configs.

Consider these alternatives first

Before using this pattern, ask whether one of these solves your problem more cleanly:

  1. 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.

  2. 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.

  3. 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.

  4. 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.toml files. Foundation installs modules with --no-sources, which silently strips those overrides — the install looks successful but the shared package never reaches sys.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-modules for 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"]}

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.

❌ 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):

  1. Don't include it in your bundle.md - provide it as a separate behavior
  2. Document it as a policy behavior - so apps know to compose it
  3. Check parent_id in 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.yaml defines 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.toml with 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 with name and description
  • 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 behavior YAML syntax is correct
  • Check include path: my-bundle:behaviors/name (not my-bundle:behaviors/name.yaml)
  • Ensure behavior declares agents: and/or context: sections

Reference