diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c9daf3f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,66 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [ main ]
+ push:
+ branches: [ main ]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: Test - Python ${{ matrix.python-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
+ fail-fast: false
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+
+ - name: Install hatch
+ run: pip install hatch
+
+ - name: Run tests
+ run: hatch run test
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+ cache: 'pip'
+
+ - name: Install hatch
+ run: pip install hatch
+
+ - name: Check formatting
+ run: hatch run format
+
+ - name: Run linter
+ run: hatch run lint
+
+ - name: Type check
+ run: hatch run typecheck
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..4f052c8
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,79 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types:
+ - published
+
+jobs:
+ check:
+ name: Run checks
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+
+ - name: Install hatch
+ run: pip install hatch
+
+ - name: Run checks
+ run: hatch run prepare
+
+ build:
+ name: Build distribution
+ needs: check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+
+ - name: Install hatch
+ run: pip install hatch twine
+
+ - name: Build package
+ run: hatch build
+
+ - name: Check distribution
+ run: twine check dist/*
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+
+ publish:
+ name: Publish to PyPI
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/${{ github.event.repository.name }}
+ permissions:
+ id-token: write
+
+ steps:
+ - name: Download distributions
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c5328ae
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,133 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+Pipfile.lock
+
+# PEP 582
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# IDEs
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# macOS
+.DS_Store
diff --git a/README.md b/README.md
index 847260c..602f35f 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,181 @@
-## My Project
+
-TODO: Fill this README out!
+This template helps you build and publish custom components for [Strands Agents](https://github.com/strands-agents/sdk-python). Whether you're creating a new tool, model provider, or session manager, this repo gives you a starting point with the right structure and conventions.
-Be sure to:
+## Getting started
-* Change the title in this README
-* Edit your repository description on GitHub
+### 1. Create your repository
+
+Click "Use this template" on GitHub to create your own repository. Then clone it locally:
+
+```bash
+git clone https://github.com/yourusername/your-repo-name
+cd your-repo-name
+```
+
+### 2. Run the setup script
+
+The setup script customizes the template for your project. It renames files, updates imports, configures `pyproject.toml`, and removes components you don't need.
+
+```bash
+python setup_template.py
+```
+
+You'll be prompted for:
+
+- **Package name** — A short identifier like `amazon`, `slack`, or `redis`. This becomes your module name (`strands_amazon`) and PyPI package name (`strands-amazon`).
+- **Components** — Which extension points you want to include (tool, model, etc.)
+- **Author info** — Your name, email, and GitHub username for `pyproject.toml`.
+- **Description** — A one-line description of your package.
+
+### 3. Install dependencies
+
+```bash
+pip install -e ".[dev]"
+```
+
+## What's in this template
+
+The template includes skeleton implementations for all major Strands extension points.
+
+| File | Component | Purpose |
+|------|-----------|---------|
+| `tool.py` | Tool | Add capabilities to agents using the `@tool` decorator |
+| `model.py` | Model provider | Integrate custom LLM APIs |
+| `plugin.py` | Plugin | Extend agent behavior with hooks and tools in a composable package |
+| `session_manager.py` | Session manager | Persist conversations across restarts |
+| `conversation_manager.py` | Conversation manager | Control context window and message history |
+
+The setup script will remove components you don't select, so you only keep what you need.
+
+## Implementing your components
+
+Each file contains a minimal skeleton. Here's what to implement:
+
+### Tools
+
+Tools let agents interact with external systems and perform actions. Implement your logic inside the decorated function and return a result dict.
+
+- [Creating custom tools](https://strandsagents.com/latest/user-guide/concepts/tools/custom-tools/) — Documentation
+- [sleep](https://github.com/strands-agents/tools/blob/main/src/strands_tools/sleep.py) — Simple tool with error handling
+- [browser](https://github.com/strands-agents/tools/blob/main/src/strands_tools/browser/__init__.py) — Multi-tool package example
+
+### Plugins
+
+Plugins provide a composable way to extend agent behavior by bundling hooks and tools into a single package. Use `@hook` to react to agent lifecycle events and `@tool` to add capabilities, all auto-discovered and registered when the plugin is attached to an agent.
+
+- [Plugins](https://strandsagents.com/latest/user-guide/concepts/plugins/) — Documentation
+- [AgentSkills](https://github.com/strands-agents/sdk-python/tree/main/src/strands/vended_plugins/skills) — Plugin example with hooks and tools
+- [Steering](https://github.com/strands-agents/sdk-python/tree/main/src/strands/vended_plugins/steering) — Advanced plugin example
+
+### Model providers
+
+Model providers connect agents to LLM APIs. Implement the `stream()` method to receive messages and yield streaming events.
+
+- [Custom providers](https://strandsagents.com/latest/user-guide/concepts/model-providers/custom_model_provider/) — Documentation
+- [strands-clova](https://github.com/aidendef/strands-clova) — Community model provider example
+
+### Session managers
+
+Session managers persist conversations to external storage, enabling conversations to resume after restarts or be shared across instances.
+
+- [Session management](https://strandsagents.com/latest/user-guide/concepts/agents/session-management/) — Documentation
+- [File session manager](https://github.com/strands-agents/sdk-python/blob/main/src/strands/session/file_session_manager.py) — Implementation example
+
+### Conversation managers
+
+Conversation managers control the context window and how message history grows over time. They handle trimming old messages or summarizing context to stay within model limits.
+
+- [Conversation management](https://strandsagents.com/latest/user-guide/concepts/agents/conversation-management/) — Documentation
+- [Sliding window manager](https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py) — Implementation example
+
+## Testing
+
+Run all checks (format, lint, typecheck, test):
+
+```bash
+hatch run prepare
+```
+
+Or run them individually:
+
+```bash
+hatch run test # Run tests
+hatch run lint # Run linter
+hatch run typecheck # Run type checker
+hatch run format # Format code
+```
+
+## Publishing to PyPI
+
+You can publish manually or through GitHub Actions.
+
+### Option 1: GitHub release (recommended)
+
+The included workflow automatically publishes to PyPI when you create a GitHub release. Version is derived from the git tag automatically.
+
+1. Configure PyPI trusted publishing first (see below)
+2. Create a release on GitHub with a tag like `v0.1.0`
+3. The workflow runs checks, builds, and publishes
+
+To configure PyPI trusted publishing:
+
+1. Go to PyPI → Your projects → Publishing
+2. Add a new pending publisher with your GitHub repo details
+3. Set environment name to `pypi`
+
+**Note:** If you create a release without configuring trusted publishing, the workflow will fail. Set this up before your first release.
+
+### Option 2: Manual publish
+
+```bash
+hatch build
+pip install twine
+twine upload dist/*
+```
+
+## Naming conventions
+
+Follow these conventions so your package fits the Strands ecosystem:
+
+| Item | Convention | Example |
+|------|------------|---------|
+| PyPI package | `strands-{name}` | `strands-amazon` |
+| Python module | `strands_{name}` | `strands_amazon` |
+| Model class | `{Name}Model` | `AmazonModel` |
+| Plugin class | `{Name}Plugin` | `AmazonPlugin` |
+| Session manager | `{Name}SessionManager` | `RedisSessionManager` |
+| Conversation manager | `{Name}ConversationManager` | `SummarizingConversationManager` |
+| Tool function | `{descriptive_name}` | `search_web`, `send_email` |
+
+## Get featured
+
+Help others discover your package by adding the `strands-agents` topic to your GitHub repository. This makes it easier for the community to find Strands extensions.
+
+To add topics: go to your repo → click the ⚙️ gear next to "About" → add `strands-agents` and other relevant topics.
+
+You can also submit your package to be featured on the Strands website. See [Get Featured](https://strandsagents.com/latest/community/get-featured/) for details.
+
+## Resources
+
+- [Strands Agents documentation](https://strandsagents.com/)
+- [SDK Python repository](https://github.com/strands-agents/sdk-python)
+- [Official tools repository](https://github.com/strands-agents/tools)
+- [Community packages](https://strandsagents.com/latest/community/community-packages/)
## Security
@@ -13,5 +183,4 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform
## License
-This project is licensed under the Apache-2.0 License.
-
+Apache 2.0 — see [LICENSE](LICENSE) for details.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..33a1481
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,89 @@
+[build-system]
+requires = ["hatchling", "hatch-vcs"]
+build-backend = "hatchling.build"
+
+[project]
+name = "strands-template"
+dynamic = ["version"]
+description = "Your package description"
+readme = "README.md"
+requires-python = ">=3.10"
+license = {text = "Apache-2.0"}
+authors = [
+ {name = "Your Name", email = "your.email@example.com"}
+]
+keywords = ["strands", "agents", "ai", "tool"]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+
+dependencies = [
+ "strands-agents>=1.0.0",
+ "pydantic>=2.0.0",
+]
+
+[project.urls]
+Homepage = "https://github.com/yourusername/strands-template"
+Documentation = "https://github.com/yourusername/strands-template#readme"
+Repository = "https://github.com/yourusername/strands-template"
+Issues = "https://github.com/yourusername/strands-template/issues"
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=8.0.0,<9.0.0",
+ "pytest-asyncio>=0.25.0,<1.0.0",
+ "ruff>=0.11.0,<1.0.0",
+ "mypy>=1.15.0,<2.0.0",
+ "hatch",
+]
+
+[tool.hatch.version]
+source = "vcs"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/strands_template"]
+
+[tool.hatch.envs.default]
+dependencies = [
+ "pytest>=8.0.0,<9.0.0",
+ "pytest-asyncio>=0.25.0,<1.0.0",
+ "ruff>=0.11.0,<1.0.0",
+ "mypy>=1.15.0,<2.0.0",
+]
+
+[tool.hatch.envs.default.scripts]
+test = "pytest {args}"
+lint = "ruff check src tests"
+format = "ruff format src tests"
+typecheck = "mypy src"
+prepare = ["format", "lint", "typecheck", "test"]
+
+[tool.ruff]
+line-length = 120
+include = ["src/**/*.py", "tests/**/*.py"]
+
+[tool.ruff.lint]
+select = [
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
+ "B", # flake8-bugbear
+]
+
+[tool.mypy]
+python_version = "3.10"
+warn_return_any = true
+warn_unused_configs = true
+ignore_missing_imports = true
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+pythonpath = ["src"]
+asyncio_mode = "auto"
diff --git a/setup_template.py b/setup_template.py
new file mode 100644
index 0000000..97efb1c
--- /dev/null
+++ b/setup_template.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+"""
+Template Setup Script
+
+Run this after cloning the template to customize it for your project.
+This script will:
+1. Ask for your project details
+2. Ask which components you want to keep
+3. Rename files and directories
+4. Replace placeholder values throughout the codebase
+5. Delete unused components
+6. Delete itself when done
+
+Usage:
+ python setup_template.py
+"""
+
+import os
+import re
+import shutil
+import sys
+
+COMPONENTS = {
+ "tool": {
+ "name": "Tool",
+ "description": "Add capabilities to agents using the @tool decorator",
+ "files": ["tool.py"],
+ "test_files": ["test_tool.py"],
+ "exports": ["template_tool"],
+ },
+ "model": {
+ "name": "Model Provider",
+ "description": "Integrate custom LLM APIs",
+ "files": ["model.py"],
+ "test_files": ["test_model.py"],
+ "exports": ["TemplateModel"],
+ },
+ "plugin": {
+ "name": "Plugin",
+ "description": "Extend agent behavior with hooks and tools in a composable package",
+ "files": ["plugin.py"],
+ "test_files": ["test_plugin.py"],
+ "exports": ["TemplatePlugin"],
+ },
+ "session_manager": {
+ "name": "Session Manager",
+ "description": "Persist conversations across restarts",
+ "files": ["session_manager.py"],
+ "test_files": ["test_session_manager.py"],
+ "exports": ["TemplateSessionManager"],
+ },
+ "conversation_manager": {
+ "name": "Conversation Manager",
+ "description": "Control context window and message history",
+ "files": ["conversation_manager.py"],
+ "test_files": ["test_conversation_manager.py"],
+ "exports": ["TemplateConversationManager"],
+ },
+}
+
+
+def to_snake_case(name: str) -> str:
+ """Convert to snake_case (e.g., my_tool)."""
+ s = re.sub(r"[-\s]+", "_", name)
+ s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)
+ s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s)
+ return s.lower()
+
+
+def to_pascal_case(name: str) -> str:
+ """Convert to PascalCase (e.g., MyTool)."""
+ words = re.split(r"[-_\s]+", name)
+ return "".join(word.capitalize() for word in words)
+
+
+def to_kebab_case(name: str) -> str:
+ """Convert to kebab-case (e.g., my-tool)."""
+ s = to_snake_case(name)
+ return s.replace("_", "-")
+
+
+def get_input(prompt: str, default: str = "") -> str:
+ """Get user input with optional default."""
+ if default:
+ result = input(f"{prompt} [{default}]: ").strip()
+ return result if result else default
+ return input(f"{prompt}: ").strip()
+
+
+def select_components() -> list[str]:
+ """Prompt user to select which components to keep."""
+ print("\nWhich components do you want to include?\n")
+
+ for i, (key, info) in enumerate(COMPONENTS.items(), 1):
+ print(f" {i}. {info['name']} - {info['description']}")
+
+ print()
+ selection = get_input("Enter numbers separated by commas (e.g., 1,2)", "1")
+
+ selected = []
+ for num in selection.split(","):
+ num = num.strip()
+ if num.isdigit():
+ idx = int(num) - 1
+ if 0 <= idx < len(COMPONENTS):
+ selected.append(list(COMPONENTS.keys())[idx])
+
+ if not selected:
+ print("❌ No valid components selected")
+ sys.exit(1)
+
+ return selected
+
+
+def replace_in_file(filepath: str, replacements: dict[str, str]) -> None:
+ """Replace all occurrences in a file."""
+ with open(filepath, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ for old, new in replacements.items():
+ content = content.replace(old, new)
+
+ with open(filepath, "w", encoding="utf-8") as f:
+ f.write(content)
+
+
+def update_init_file(src_dir: str, selected: list[str], replacements: dict[str, str]) -> None:
+ """Update __init__.py to only export selected components."""
+ init_path = os.path.join(src_dir, "__init__.py")
+
+ imports = []
+ exports = []
+
+ for key in selected:
+ info = COMPONENTS[key]
+ for export in info["exports"]:
+ # Apply replacements to get the new name
+ new_export = export
+ for old, new in replacements.items():
+ new_export = new_export.replace(old, new)
+
+ module = info["files"][0].replace(".py", "")
+ # Apply replacements to module name too
+ new_module = module
+ for old, new in replacements.items():
+ new_module = new_module.replace(old, new)
+
+ # Get the package name from src_dir (apply replacements for renamed package)
+ package_name = os.path.basename(src_dir)
+ for old, new in replacements.items():
+ package_name = package_name.replace(old, new)
+ imports.append(f"from {package_name}.{new_module} import {new_export}")
+ exports.append(f' "{new_export}",')
+
+ imports.sort()
+ exports.sort()
+
+ content = f'''"""Strands Package."""
+
+{chr(10).join(imports)}
+
+__all__ = [
+{chr(10).join(exports)}
+]
+'''
+
+ with open(init_path, "w", encoding="utf-8") as f:
+ f.write(content)
+
+
+def delete_unused_components(src_dir: str, selected: list[str]) -> None:
+ """Delete component files that weren't selected."""
+ for key, info in COMPONENTS.items():
+ if key not in selected:
+ # Delete source files
+ for filename in info["files"]:
+ filepath = os.path.join(src_dir, filename)
+ if os.path.exists(filepath):
+ os.remove(filepath)
+ print(f" ✓ Removed {filepath}")
+
+ # Delete test files
+ for filename in info["test_files"]:
+ filepath = os.path.join("tests", filename)
+ if os.path.exists(filepath):
+ os.remove(filepath)
+ print(f" ✓ Removed {filepath}")
+
+
+def main() -> None:
+ print("\n🔧 Strands Template Setup\n")
+ print("This will customize the template for your project.\n")
+
+ # Gather information
+ package_name = get_input("Package name (e.g., 'google', 'aws', 'slack')")
+ if not package_name:
+ print("❌ Package name is required")
+ sys.exit(1)
+
+ # Generate variations
+ snake_name = to_snake_case(package_name)
+ pascal_name = to_pascal_case(package_name)
+ kebab_name = to_kebab_case(package_name)
+
+ print(f"\n PyPI package: strands-{kebab_name}")
+ print(f" Module: strands_{snake_name}")
+ print(f" Classes: {pascal_name}Model, {pascal_name}Hooks, etc.")
+
+ # Select components
+ selected = select_components()
+ selected_names = [COMPONENTS[k]["name"] for k in selected]
+ print(f"\n Selected: {', '.join(selected_names)}")
+
+ # Optional info
+ print()
+ author_name = get_input("Author name", "Your Name")
+ author_email = get_input("Author email", "your.email@example.com")
+ github_username = get_input("GitHub username", "yourusername")
+ description = get_input("Package description", f"Strands Agents components for {package_name}")
+
+ # Confirm
+ print("\n" + "=" * 50)
+ confirm = get_input("\nProceed with setup? (y/n)", "y")
+ if confirm.lower() != "y":
+ print("Setup cancelled.")
+ sys.exit(0)
+
+ print("\n⏳ Setting up project...\n")
+
+ # Define replacements
+ replacements = {
+ # Package/module names
+ "strands-template": f"strands-{kebab_name}",
+ "strands_template": f"strands_{snake_name}",
+ # Class names
+ "TemplateModel": f"{pascal_name}Model",
+ "TemplatePlugin": f"{pascal_name}Plugin",
+ "TemplateSessionManager": f"{pascal_name}SessionManager",
+ "TemplateConversationManager": f"{pascal_name}ConversationManager",
+ # Function names
+ "template_tool": f"{snake_name}_tool",
+ # Plugin name
+ "template-plugin": f"{kebab_name}-plugin",
+ # Author info
+ "Your Name": author_name,
+ "your.email@example.com": author_email,
+ "yourusername": github_username,
+ "Your package description": description,
+ }
+
+ # Determine which files to process based on selection
+ files_to_process = ["pyproject.toml", "README.md"]
+
+ for key in selected:
+ info = COMPONENTS[key]
+ for filename in info["files"]:
+ files_to_process.append(f"src/strands_template/{filename}")
+ for filename in info["test_files"]:
+ files_to_process.append(f"tests/{filename}")
+
+ # Always process __init__.py
+ files_to_process.append("src/strands_template/__init__.py")
+
+ # Process files
+ for filepath in files_to_process:
+ if os.path.exists(filepath):
+ replace_in_file(filepath, replacements)
+ print(f" ✓ Updated {filepath}")
+
+ # Delete unused components
+ print("\n🗑️ Removing unused components...")
+ delete_unused_components("src/strands_template", selected)
+
+ # Update __init__.py with only selected exports
+ new_src = f"src/strands_{snake_name}"
+ update_init_file("src/strands_template", selected, replacements)
+
+ # Rename source directory
+ old_src = "src/strands_template"
+ if os.path.exists(old_src) and old_src != new_src:
+ shutil.move(old_src, new_src)
+ print(f"\n ✓ Renamed {old_src} → {new_src}")
+
+ # Clean up
+ print("\n🧹 Cleaning up...")
+
+ # Remove this setup script
+ script_path = os.path.abspath(__file__)
+ if os.path.exists(script_path):
+ os.remove(script_path)
+ print(" ✓ Removed setup_template.py")
+
+ print("\n✅ Setup complete!\n")
+ print("Next steps:")
+ print(" 1. Review the generated files")
+ print(" 2. Install dev dependencies: pip install -e '.[dev]'")
+ print(" 3. Run checks: hatch run prepare")
+ print(" 4. Start implementing your components")
+ print()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/strands_template/__init__.py b/src/strands_template/__init__.py
new file mode 100644
index 0000000..2590ba0
--- /dev/null
+++ b/src/strands_template/__init__.py
@@ -0,0 +1,15 @@
+"""Strands Template Package."""
+
+from strands_template.conversation_manager import TemplateConversationManager
+from strands_template.model import TemplateModel
+from strands_template.plugin import TemplatePlugin
+from strands_template.session_manager import TemplateSessionManager
+from strands_template.tool import template_tool
+
+__all__ = [
+ "template_tool",
+ "TemplateModel",
+ "TemplatePlugin",
+ "TemplateSessionManager",
+ "TemplateConversationManager",
+]
diff --git a/src/strands_template/conversation_manager.py b/src/strands_template/conversation_manager.py
new file mode 100644
index 0000000..331f79f
--- /dev/null
+++ b/src/strands_template/conversation_manager.py
@@ -0,0 +1,27 @@
+"""Conversation Manager Implementation."""
+
+import logging
+from typing import TYPE_CHECKING, Any
+
+from strands.agent.conversation_manager.conversation_manager import ConversationManager
+
+if TYPE_CHECKING:
+ from strands.agent.agent import Agent
+
+logger = logging.getLogger(__name__)
+
+
+class TemplateConversationManager(ConversationManager):
+ """Template conversation manager implementation."""
+
+ def __init__(self) -> None:
+ """Initialize the conversation manager."""
+ super().__init__()
+
+ def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
+ """Apply conversation management strategy."""
+ pass
+
+ def reduce_context(self, agent: "Agent", e: Exception | None = None, **kwargs: Any) -> None:
+ """Reduce conversation context when overflow occurs."""
+ pass
diff --git a/src/strands_template/model.py b/src/strands_template/model.py
new file mode 100644
index 0000000..a288da6
--- /dev/null
+++ b/src/strands_template/model.py
@@ -0,0 +1,61 @@
+"""Model Provider Implementation."""
+
+import logging
+from collections.abc import AsyncGenerator, AsyncIterable
+from typing import Any, TypeVar
+
+from pydantic import BaseModel
+from strands.models.model import Model
+from strands.types.content import Messages
+from strands.types.streaming import StreamEvent
+from strands.types.tools import ToolSpec
+from typing_extensions import override
+
+logger = logging.getLogger(__name__)
+
+T = TypeVar("T", bound=BaseModel)
+
+
+class TemplateModel(Model):
+ """Template model provider implementation."""
+
+ def __init__(self, api_key: str, model_id: str) -> None:
+ """Initialize the model provider."""
+ self.api_key = api_key
+ self.model_id = model_id
+
+ @override
+ def update_config(self, **model_config: Any) -> None:
+ """Update the model configuration."""
+ pass
+
+ @override
+ def get_config(self) -> dict[str, Any]:
+ """Get the current model configuration."""
+ return {"api_key": self.api_key, "model_id": self.model_id}
+
+ @override
+ async def stream(
+ self,
+ messages: Messages,
+ tool_specs: list[ToolSpec] | None = None,
+ system_prompt: str | None = None,
+ **kwargs: Any,
+ ) -> AsyncIterable[StreamEvent]:
+ """Stream conversation with the model."""
+ # TODO: Implement streaming logic
+ raise NotImplementedError
+ yield # type: ignore
+
+ @override
+ async def structured_output(
+ self,
+ output_model: type[T],
+ prompt: Messages,
+ system_prompt: str | None = None,
+ **kwargs: Any,
+ ) -> AsyncGenerator[dict[str, T | Any], None]:
+ """Generate structured output from the model."""
+ # TODO: Implement structured output logic
+ raise NotImplementedError
+ yield # type: ignore
diff --git a/src/strands_template/plugin.py b/src/strands_template/plugin.py
new file mode 100644
index 0000000..469ade2
--- /dev/null
+++ b/src/strands_template/plugin.py
@@ -0,0 +1,68 @@
+"""Plugin Implementation."""
+
+import logging
+from typing import TYPE_CHECKING
+
+from strands import tool
+from strands.hooks import BeforeToolCallEvent
+from strands.plugins import Plugin, hook
+
+if TYPE_CHECKING:
+ from strands.agent.agent import Agent
+
+logger = logging.getLogger(__name__)
+
+
+class TemplatePlugin(Plugin):
+ """Template plugin implementation.
+
+ Plugins provide a composable way to extend agent behavior through
+ automatic hook and tool registration. Methods decorated with @hook
+ and @tool are discovered and registered automatically.
+
+ Example:
+ ```python
+ from strands import Agent
+ from strands_template import TemplatePlugin
+
+ plugin = TemplatePlugin()
+ agent = Agent(plugins=[plugin])
+ ```
+ """
+
+ name = "template-plugin"
+
+ def __init__(self) -> None:
+ """Initialize the plugin."""
+ super().__init__()
+
+ def init_agent(self, agent: "Agent") -> None:
+ """Initialize the plugin with an agent instance.
+
+ Decorated hooks and tools are auto-registered by the plugin registry.
+ Override this method to add custom initialization logic.
+
+ Args:
+ agent: The agent instance to extend.
+ """
+ pass
+
+ @hook # type: ignore[call-overload]
+ def on_before_tool_call(self, event: BeforeToolCallEvent) -> None:
+ """Hook that runs before each tool call.
+
+ Args:
+ event: The before-tool-call event with tool_use and agent reference.
+ """
+ # TODO: Implement your hook logic
+ pass
+
+ @tool
+ def template_plugin_tool(self, param1: str) -> str:
+ """Brief description of what your plugin tool does.
+
+ Args:
+ param1: Description of parameter 1.
+ """
+ # TODO: Implement your tool logic
+ raise NotImplementedError
diff --git a/src/strands_template/session_manager.py b/src/strands_template/session_manager.py
new file mode 100644
index 0000000..4770490
--- /dev/null
+++ b/src/strands_template/session_manager.py
@@ -0,0 +1,36 @@
+"""Session Manager Implementation."""
+
+import logging
+from typing import TYPE_CHECKING, Any
+
+from strands.session.session_manager import SessionManager
+from strands.types.content import Message
+
+if TYPE_CHECKING:
+ from strands.agent.agent import Agent
+
+logger = logging.getLogger(__name__)
+
+
+class TemplateSessionManager(SessionManager):
+ """Template session manager implementation."""
+
+ def __init__(self, session_id: str) -> None:
+ """Initialize the session manager."""
+ self.session_id = session_id
+
+ def initialize(self, agent: "Agent", **kwargs: Any) -> None:
+ """Initialize an agent with session data."""
+ pass
+
+ def append_message(self, message: Message, agent: "Agent", **kwargs: Any) -> None:
+ """Append a message to the session storage."""
+ pass
+
+ def redact_latest_message(self, redact_message: Message, agent: "Agent", **kwargs: Any) -> None:
+ """Redact (replace) the most recently appended message."""
+ pass
+
+ def sync_agent(self, agent: "Agent", **kwargs: Any) -> None:
+ """Synchronize agent state with session storage."""
+ pass
diff --git a/src/strands_template/tool.py b/src/strands_template/tool.py
new file mode 100644
index 0000000..eb55d43
--- /dev/null
+++ b/src/strands_template/tool.py
@@ -0,0 +1,22 @@
+"""Tool Implementation."""
+
+import logging
+from typing import Any
+
+from strands import tool
+
+logger = logging.getLogger(__name__)
+
+
+@tool
+def template_tool(param1: str) -> dict[str, Any]:
+ """Brief description of what your tool does.
+
+ Args:
+ param1: Description of parameter 1.
+
+ Returns:
+ Dict containing status and response content.
+ """
+ # TODO: Implement your tool logic
+ raise NotImplementedError
diff --git a/tests/test_conversation_manager.py b/tests/test_conversation_manager.py
new file mode 100644
index 0000000..84a9b21
--- /dev/null
+++ b/tests/test_conversation_manager.py
@@ -0,0 +1,9 @@
+"""Tests for TemplateConversationManager."""
+
+from strands_template import TemplateConversationManager
+
+
+def test_template_conversation_manager_init():
+ """Test initialization."""
+ cm = TemplateConversationManager()
+ assert cm is not None
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 0000000..8fe7411
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,11 @@
+"""Tests for TemplateModel."""
+
+from strands_template import TemplateModel
+
+
+def test_template_model_init():
+ """Test initialization."""
+ model = TemplateModel(api_key="test-key", model_id="test-model")
+ config = model.get_config()
+ assert config["api_key"] == "test-key"
+ assert config["model_id"] == "test-model"
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 0000000..34047ee
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,9 @@
+"""Tests for TemplatePlugin."""
+
+from strands_template import TemplatePlugin
+
+
+def test_template_plugin_init():
+ """Test initialization."""
+ plugin = TemplatePlugin()
+ assert plugin.name is not None
diff --git a/tests/test_session_manager.py b/tests/test_session_manager.py
new file mode 100644
index 0000000..d60c225
--- /dev/null
+++ b/tests/test_session_manager.py
@@ -0,0 +1,9 @@
+"""Tests for TemplateSessionManager."""
+
+from strands_template import TemplateSessionManager
+
+
+def test_template_session_manager_init():
+ """Test initialization."""
+ session = TemplateSessionManager(session_id="test-session")
+ assert session.session_id == "test-session"
diff --git a/tests/test_tool.py b/tests/test_tool.py
new file mode 100644
index 0000000..3425d5a
--- /dev/null
+++ b/tests/test_tool.py
@@ -0,0 +1,7 @@
+"""Tests for template_tool."""
+
+
+def test_template_tool():
+ """Test basic functionality."""
+ # TODO: Implement tests
+ ...