Back to Articles

Strands Agents: Building AI Agents With Decorators Instead of Chains

[ View on GitHub ]

Strands Agents: Building AI Agents With Decorators Instead of Chains

Hook

Most AI agent frameworks make you write 50+ lines of boilerplate to connect a function to an LLM. Strands Agents does it in three: a decorator, a function definition, and an agent initialization.

Context

The explosion of AI agent frameworks in 2023-2024 brought sophisticated orchestration tools like LangChain and AutoGen, but they also brought complexity. Defining a simple agent that can check weather and send emails shouldn’t require understanding chains, graphs, executors, and callback managers. Developers wanted something closer to Flask’s simplicity than Django’s comprehensiveness.

Strands Agents emerged as a response to this boilerplate fatigue. It takes a model-driven approach where the LLM itself handles orchestration logic, and your job is simply to provide tools as plain Python functions. The framework’s core insight is that with modern function-calling capabilities in GPT-4, Claude, and Llama models, you don’t need complex workflow engines—you just need a clean way to expose functions and let the model figure out how to use them. This shifts the complexity from your framework code into the model weights, where it arguably belongs.

Technical Insight

Strands Agents implements a decorator-based tool system that feels native to Python. Instead of inheriting from base classes or instantiating tool objects, you simply annotate functions with @agent.tool(). The framework introspects the function signature, extracts parameter types from type hints, and uses the docstring as the tool description that gets sent to the LLM. Here’s how you’d create an agent that can perform calculations:

from strands import Agent

agent = Agent(
    name="calculator",
    instructions="You are a helpful math assistant."
)

@agent.tool()
def calculate(expression: str) -> float:
    """Evaluates a mathematical expression and returns the result.
    
    Args:
        expression: A string containing a math expression like '2 + 2' or '10 * 5'
    """
    return eval(expression)  # Don't do this in production!

# The agent automatically gets access to the tool
response = agent.run("What's 127 times 43?")
print(response.content)

Under the hood, Strands translates your function into a tool schema that LLMs understand. The framework supports multiple model providers through a provider abstraction layer that normalizes API differences. When you call agent.run(), it enters an agent loop: send the user message to the LLM, check if the model wants to call a tool, execute that tool, send the result back, and repeat until the model generates a final response. This loop is hidden from you—you just define tools and call run().

What sets Strands apart architecturally is its native Model Context Protocol (MCP) integration. MCP is an emerging standard for connecting AI systems to external data sources and tools. Instead of writing tool wrappers yourself, you can connect to MCP servers that expose hundreds of pre-built integrations:

from strands import Agent

agent = Agent(name="research-assistant")

# Connect to an MCP server that provides web search
agent.connect_mcp(
    server="stdio",
    command="npx",
    args=["-y", "@modelcontextprotocol/server-brave-search"]
)

# Agent now has access to all tools from that MCP server
response = agent.run("What are the latest developments in quantum computing?")

The framework spawns the MCP server as a subprocess and communicates via JSON-RPC over stdin/stdout. This “stdio” transport mode is the simplest MCP integration pattern, though the protocol also supports HTTP. The key insight is that tool discovery happens dynamically—when you connect to an MCP server, Strands queries it for available tools and automatically registers them with your agent.

For rapid development, Strands includes a hot-reload feature that monitors a directory for Python files and automatically loads any functions decorated with tool markers. Drop a new file into your tools directory, and the agent picks it up without restarting. This is particularly valuable during prototyping when you’re iterating on tool implementations.

The provider abstraction deserves special attention. Strands supports 12+ LLM providers through a consistent interface—you change models by modifying configuration, not code:

# Using AWS Bedrock
agent = Agent(
    name="assistant",
    model="anthropic.claude-3-5-sonnet-20241022-v2:0",
    provider="bedrock"
)

# Switch to OpenAI by changing two parameters
agent = Agent(
    name="assistant",
    model="gpt-4",
    provider="openai"
)

# Or use a local Ollama instance
agent = Agent(
    name="assistant",
    model="llama3.2",
    provider="ollama"
)

This abstraction extends to streaming responses. The framework provides a unified streaming interface regardless of the underlying provider, which is non-trivial given that different APIs handle streaming differently. There’s even experimental support for bidirectional streaming, where the agent can process input while generating output—useful for voice interactions where users might interrupt mid-response.

Gotcha

The framework’s simplicity comes with trade-offs. Error handling in the agent loop is relatively basic—if a tool raises an exception, the error message goes back to the LLM, which may or may not handle it gracefully depending on the model’s training. There’s no built-in retry logic, rate limiting, or circuit breakers, which means production deployments need additional infrastructure around the SDK. The README shows happy-path examples but doesn’t cover scenarios like tool timeouts, malformed LLM responses, or network failures when communicating with MCP servers.

The default configuration assumes AWS Bedrock access, which creates unnecessary friction for developers who just want to experiment. You need to configure AWS credentials, ensure the right model is available in your region, and understand Bedrock’s pricing model before running your first agent. A better default would be OpenAI or a local Ollama setup that works out of the box. The experimental bidirectional streaming feature is particularly unstable—the maintainers explicitly warn that the API will change, so avoid building critical features on it. The MCP integration, while powerful, depends on external server implementations that vary widely in quality and documentation.

Verdict

Use if: You’re building AI agents in Python and value simplicity over features, need to support multiple LLM providers without vendor lock-in, want native MCP integration to leverage the growing ecosystem of pre-built tools, or are prototyping and need fast iteration with hot-reload. The decorator-based API will feel natural if you’ve used Flask or FastAPI. Skip if: You need production-grade error handling and observability out of the box, are building complex multi-step workflows that require orchestration primitives like conditional branching or human-in-the-loop approval, require extensive documentation and large community support, or don’t want to deal with AWS Bedrock setup. For production systems, you’ll need to wrap Strands in additional infrastructure for monitoring, rate limiting, and error recovery—which might negate the simplicity advantage.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-agents/strands-agents-sdk-python.svg)](https://starlog.is/api/badge-click/ai-agents/strands-agents-sdk-python)