Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .kiro/skills/library-development/references/project-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ for the chapter-by-chapter reference.
## Stack notes

- **Python ≥ 3.11** (ruff/ty target 3.13). Runtime deps: `strands-agents`
(>=1.35,<2), `pydantic` v2, `pyyaml`, `mcp`. Optional extras:
`agentcore-memory`, `ollama`, `openai`, `gemini`.
(>=1.48,<2), `pydantic` v2, `pyyaml`, `mcp`. Optional extras:
`agentcore-memory`, `ollama`, `openai`, `gemini`, `anthropic`.
- **MCP servers** run on a background daemon thread with a self-managed
`uvicorn.Server` (HTTP transports only — `streamable-http`, `sse`); `stdio`
is client-side (the client spawns a subprocess).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<p>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.11+-blue.svg" alt="Python 3.11+"></a>
<a href="https://pypi.org/project/strands-compose/"><img src="https://img.shields.io/pypi/v/strands-compose.svg" alt="PyPI version"></a>
<a href="https://github.com/strands-agents/harness-sdk"><img src="https://img.shields.io/badge/strands--agents-1.35.0+-green.svg" alt="Strands Agents"></a>
<a href="https://github.com/strands-agents/harness-sdk"><img src="https://img.shields.io/badge/strands--agents-1.48.0+-green.svg" alt="Strands Agents"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License"></a>
</p>
</div>
Expand Down
43 changes: 43 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Strands Compose — Documentation

**[YAML Configuration Guide](configuration/README.md)** — the complete reference, from your
first one-agent config to nested multi-orchestration systems with MCP servers, hooks,
plugins, session persistence, and streaming. Read it in order, or jump to a chapter:

| # | Chapter | What it covers |
|---|---------|----------------|
| 1 | [The Basics](configuration/Chapter_01.md) | Your first config |
| 2 | [Models](configuration/Chapter_02.md) | Choosing your LLM |
| 3 | [Variables](configuration/Chapter_03.md) | Environment-driven config |
| 4 | [YAML Anchors](configuration/Chapter_04.md) | DRY config blocks |
| 5 | [Tools](configuration/Chapter_05.md) | Giving agents capabilities |
| 6 | [Hooks](configuration/Chapter_06.md) | Lifecycle middleware |
| 7 | [Session Persistence](configuration/Chapter_07.md) | Memory that survives restarts |
| 8 | [Conversation Managers](configuration/Chapter_08.md) | Controlling context windows |
| 9 | [MCP](configuration/Chapter_09.md) | External tool servers |
| 10 | [Orchestrations](configuration/Chapter_10.md) | Multi-agent systems |
| 11 | [Graph Conditions](configuration/Chapter_11.md) | Dynamic routing |
| 12 | [Nested Orchestrations](configuration/Chapter_12.md) | Composing systems |
| 13 | [Multi-File Configs](configuration/Chapter_13.md) | Splitting and merging |
| 14 | [Agent Factories](configuration/Chapter_14.md) | Custom agent construction |
| 15 | [Event Streaming](configuration/Chapter_15.md) | Real-time observability |
| 16 | [Name Sanitization](configuration/Chapter_16.md) | How names are handled |
| 17 | [The Loading Pipeline](configuration/Chapter_17.md) | What happens under the hood |
| 18 | [Full Reference](configuration/Chapter_18.md) | Every field at a glance |
| 19 | [Plugins](configuration/Chapter_19.md) | Reusable agent behaviors |

**[Quick Recipes](configuration/Quick_Recipes.md)** — copy-paste-ready configs for common patterns.

## Learn by example

Every concept above has a runnable demo in **[examples/](../examples/)** — each a
self-contained folder with a `config.yaml`, a `main.py`, and its own README.

## What's in this folder

- `configuration/` — the YAML configuration guide (chapters plus quick recipes).
- `img/` — brand assets

---

Back to the [project README](../README.md).
4 changes: 4 additions & 0 deletions docs/configuration/Chapter_06.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ orchestrations:
> - Hooks are the right place for cross-cutting concerns: rate limiting, audit logging, cost tracking, safety guardrails.
> - Combine `MaxToolCallsGuard` and `ToolNameSanitizer` as a baseline for any agent that uses tools — they handle the most common edge cases.

## Related: Plugins

A hook is a single lifecycle callback. A plugin is a reusable behaviour package — [skills](https://strandsagents.com/docs/user-guide/concepts/plugins/skills/), [steering](https://strandsagents.com/docs/user-guide/concepts/plugins/steering/), [context management](https://strandsagents.com/docs/user-guide/concepts/plugins/context-offloader/), [goal loops](https://strandsagents.com/docs/user-guide/concepts/plugins/goal-loop/) — that can bundle hooks, tools, and setup in one object. Reach for a plugin when you want packaged behaviour instead of a one-off callback. See [Chapter 19 — Plugins](Chapter_19.md).

---

[Next: Chapter 7 — Session Persistence →](Chapter_07.md)
17 changes: 17 additions & 0 deletions docs/configuration/Chapter_18.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ agents:
description: "..." # Agent description (used in orchestration tools)
tools: [] # List of tool spec strings
hooks: [] # List of HookDef objects or import path strings
plugins: [] # List of PluginDef objects or import path strings
mcp: [] # List of MCP client names
tool_labels: {} # Tool name -> display label mapping
conversation_manager: null # ConversationManagerDef
Expand All @@ -59,6 +60,22 @@ hooks:
- module.path:ClassName
```

## PluginDef

A plugin is a reusable behaviour package — skills, context injection, steering, goal loops —
resolved to a `strands.plugins.Plugin` and passed to the agent. See
[Chapter 19 — Plugins](Chapter_19.md) for the full reference.

```yaml
plugins:
# Inline object form
- type: module.path:ClassName # or ./file.py:ClassName — class or factory
params: {} # Constructor / factory kwargs

# String shorthand (no params)
- module.path:ClassName
```

## SessionManagerDef

```yaml
Expand Down
148 changes: 148 additions & 0 deletions docs/configuration/Chapter_19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Chapter 19: Plugins — Reusable Agent Behaviors

[← Back to Table of Contents](README.md) | [← Previous: Full Reference](Chapter_18.md)

---

A plugin changes how an agent behaves. Rather than pack every instruction and safeguard
into one system prompt, you attach self-contained behaviour packages that plug into the
agent loop. The strands SDK ships several, and you can write your own. The ones you reach
for most often:

- **[Skills](https://strandsagents.com/docs/user-guide/concepts/plugins/skills/)** — on-demand instructions the agent discovers and loads only when relevant, so the base prompt stays lean.
- **[Steering](https://strandsagents.com/docs/user-guide/concepts/plugins/steering/)** — just-in-time guidance and guardrails for long, multi-step tasks.
- **[Context Offloader](https://strandsagents.com/docs/user-guide/concepts/plugins/context-offloader/)** — moves oversized tool results to storage and gives the agent a preview plus a retrieval tool.
- **[Context Injector](https://strandsagents.com/docs/user-guide/concepts/plugins/context-injector/)** — folds real-time facts (a clock, environment data, a lookup) into each model call without persisting them.
- **[GoalLoop](https://strandsagents.com/docs/user-guide/concepts/plugins/goal-loop/)** — validates a response and retries with feedback until it meets a quality bar.

The [strands plugins guide](https://strandsagents.com/docs/user-guide/concepts/plugins/)
covers the full set and how to build and distribute your own.

strands-compose does not reinvent any of this. It resolves each `plugins:` entry to one
live `strands.plugins.Plugin` and passes the list to `strands.Agent(plugins=[...])`. The
resolver validates that the result is a `Plugin` and otherwise gets out of the way.

---

## YAML Shape

A plugin entry is either an **inline object** or a **string shorthand**:

```yaml
agents:
assistant:
model: default
plugins:
# Inline object — type + optional params
- type: strands:AgentSkills
params:
skills: ./skills

# String shorthand — no params, just the import spec
- ./plugins.py:MyPlugin
```

Both forms accept two import-spec styles:

- `module.path:Name` — resolved from an installed package (e.g. `strands:AgentSkills`, `strands.vended_plugins.goal:GoalLoop`).
- `./file.py:Name` — resolved from a local file, **relative to the config file**.

`params` is spread as keyword arguments to the resolved class or factory.

---

## Class vs. Factory

An entry may resolve to a **class** (a `Plugin` subclass) or a **factory** (any callable
that returns a `Plugin`). strands-compose calls the resolved object with `params` and
checks the result — a non-`Plugin` raises `TypeError`. The config looks the same either way.

Use a factory when a plugin needs a live argument that YAML can't express — a callable, a
storage backend, a set of providers. `ContextInjector`, for example, takes a render
callback:

```python
# plugins.py
from datetime import datetime, timezone

from strands.vended_plugins.context_injector import ContextInjector


def make_clock_injector() -> ContextInjector:
"""Build a ContextInjector that folds the current UTC time into each model call."""
def render(_context: object) -> str:
return f"<current_utc_time>{datetime.now(timezone.utc).isoformat()}</current_utc_time>"

return ContextInjector(render, name="clock")
```

```yaml
agents:
assistant:
plugins:
- type: ./plugins.py:make_clock_injector # factory — no params needed
```

---

## Skills

`strands.AgentSkills` loads a directory of skills and exposes them through progressive
disclosure: each skill's name and description go into the system prompt at startup, and the
full instructions load only when the agent activates the skill. This keeps specialized,
occasionally-needed procedures out of the base prompt.

```yaml
agents:
skilled_agent:
model: default
system_prompt: "You are a helpful assistant."
plugins:
- type: strands:AgentSkills
params:
skills: ./skills/ # one skill directory, or a parent of several
```

> **Note — path params are relative to the working directory.** A plugin's `type:` is
> resolved relative to the config file, but any path *inside* `params` (such as a skills
> directory) is forwarded verbatim and resolves against the current working directory. Run
> the config from a directory where that path is valid. (This may change in a future
> release.)

A skill is just a `SKILL.md` directory that `AgentSkills` discovers. Authoring one — the
frontmatter fields, progressive disclosure, resource files — is owned by strands and the
open Agent Skills format, not by strands-compose; `examples/15_plugins/` ships a working one.

### Further reading

- [strands — Skills](https://strandsagents.com/docs/user-guide/concepts/plugins/skills/) — how `AgentSkills` discovers, activates, and persists skills (start here).
- [Agent Skills specification](https://agentskills.io/specification) and the [agentskills/agentskills](https://github.com/agentskills/agentskills) repo — the open format itself, plus the `skills-ref` validator.
- [Anthropic — Equipping agents for the real world with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills) — the rationale behind the format.

---

## Error Reference

| Condition | Exception |
|-----------|-----------|
| Malformed spec (no `:` separator) / missing file / module / attribute | `ImportResolutionError` (a `ValueError` subclass, from `load_object`) |
| Resolved object is not callable | `TypeError` |
| Resolved object is callable but does not return a `Plugin` | `TypeError` |
| Constructor or factory raises | the original exception, unwrapped |
| Two plugins on one agent share a `name` | `ValueError`, from the strands plugin registry |
| `plugins` also passed via `agent_kwargs` | `TypeError` (duplicate keyword), from `Agent()` |

strands-compose adds no plugin-specific exception types; everything propagates unwrapped.

---

> **Tips**
>
> - Every agent gets fresh plugin instances — two agents with the same entry never share state.
> - `plugins:` and `hooks:` can both appear on one agent and fire together.
> - A plugin's `@tool` methods are discoverable on the agent via `agent.tool_names` after construction.
> - Need only a lifecycle callback and no bundled tools or setup? A [hook](Chapter_06.md) is the lighter choice.

---

[← Previous: Full Reference](Chapter_18.md) | [Back to Table of Contents](README.md)
1 change: 1 addition & 0 deletions docs/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ No prior YAML expertise required. We start simple and build up.
16. [Name Sanitization — How Names Are Handled](Chapter_16.md)
17. [The Loading Pipeline — What Happens Under the Hood](Chapter_17.md)
18. [Full Reference — Every Field at a Glance](Chapter_18.md)
19. [Plugins — Reusable Agent Behaviors](Chapter_19.md)

**Bonus**: [Quick Recipes](Quick_Recipes.md) — Copy-paste-ready configs for common patterns.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.html → docs/img/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<body>
<div class="container">
<div class="logo-section">
<img src="img/logo.png" alt="strands-compose">
<img src="logo.png" alt="strands-compose">
<div class="logo-text">
<div class="strands">strands</div>
<div class="compose">Compose</div>
Expand Down
87 changes: 87 additions & 0 deletions examples/15_plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 15 — Plugins

> Extend an agent with reusable behaviour packages — skills, context injection, and quality loops.

## What plugins are for

A plugin changes how an agent behaves. Instead of cramming every instruction and
safeguard into one system prompt, you attach self-contained packages that hook into
the agent loop. The strands SDK ships several out of the box:

- **[Skills](https://strandsagents.com/docs/user-guide/concepts/plugins/skills/)** — on-demand instructions the agent discovers and loads only when relevant, keeping the base prompt lean.
- **[Steering](https://strandsagents.com/docs/user-guide/concepts/plugins/steering/)** — just-in-time guidance and guardrails for long, multi-step tasks.
- **[Context Offloader](https://strandsagents.com/docs/user-guide/concepts/plugins/context-offloader/)** — moves oversized tool results to storage and hands the agent a preview plus a retrieval tool.
- **[Context Injector](https://strandsagents.com/docs/user-guide/concepts/plugins/context-injector/)** — folds real-time facts (a clock, environment data, a lookup) into each model call without persisting them.
- **[GoalLoop](https://strandsagents.com/docs/user-guide/concepts/plugins/goal-loop/)** — validates a response and retries with feedback until it meets a quality bar.

See the [strands plugins guide](https://strandsagents.com/docs/user-guide/concepts/plugins/)
for the full list and how to build your own.

## What this example wires

Three vended plugins, each solving a distinct problem:

```yaml
plugins:
- type: strands:AgentSkills # on-demand skills from ./skills
params:
skills: ./skills

- type: ./plugins.py:make_clock_injector # live UTC time, via a factory

- type: strands.vended_plugins.goal:GoalLoop # retry until the answer is concise
params:
goal: "Answer in at most three sentences, in plain language with no jargon."
max_attempts: 2
```

Each entry is a `type:` import spec plus optional `params:`. Two forms of import spec work:

- `module.path:Name` — resolved from an installed package (`strands:AgentSkills`, `strands.vended_plugins.goal:GoalLoop`).
- `./file.py:Name` — resolved from a local file, relative to `config.yaml`.

## Class vs. factory

`GoalLoop` and `AgentSkills` take plain values, so they configure straight from `params`.
`ContextInjector` needs a *callable* to render the injected text, which a YAML scalar
can't express. `make_clock_injector` (in `plugins.py`) builds that callable and returns
the ready plugin. strands-compose calls the resolved object with `params` either way, so
a factory and a class look identical in the config. Reach for a factory whenever a plugin
needs a live argument — a callable, a storage backend, a set of providers.

## The skill

`skills/conventional-commits/SKILL.md` teaches the agent to write git commit messages
that follow the [Conventional Commits](https://www.conventionalcommits.org/) standard —
procedural knowledge that would bloat a system prompt but belongs in an on-demand skill.
It is a self-contained skill: YAML frontmatter (`name`, `description`, `license`) plus a
markdown body with format, steps, examples, and edge cases.

`AgentSkills` injects only the `name` and `description` into the system prompt at startup;
the full body loads when the agent activates the skill. Skills that ship resource files
(scripts, references, assets) also need a file-reading tool such as `file_read` from
[`strands-agents-tools`](https://strandsagents.com/docs/user-guide/concepts/plugins/skills/#providing-tools-for-resource-access) —
this instruction-only skill needs none.

For the SKILL.md anatomy, the frontmatter fields, and the resource-directory layout,
see the authoring guide in [Chapter 19 — Plugins](../../docs/configuration/Chapter_19.md#authoring-a-skill-skillmd).
Agent Skills is an open, cross-vendor format — the authoritative sources are the
[strands skills docs](https://strandsagents.com/docs/user-guide/concepts/plugins/skills/)
and the [Agent Skills specification](https://agentskills.io/specification).

## Prerequisites

- AWS credentials configured (`aws configure` or environment variables)
- Dependencies installed: `uv sync`

## Run

```bash
uv run python examples/15_plugins/main.py
```

## Try these prompts

- `Write a commit message for: added pagination to the /users endpoint.` — the agent activates the Conventional Commits skill.
- `What time is it right now?` — answered from the injected UTC time.
- `Explain how strands plugins work.` — `GoalLoop` keeps the answer tight and filler-free.
Loading