The lightweight framework for building agents
$ npx -y skills add artificialanalysis/stirrup --agent claude-code
Run the curl in your terminal, the rest in Claude Code.
Repo: artificialanalysis/stirrup
What's inside

Stirrup is a lightweight framework, or starting point template, for building agents. It differs from other agent frameworks by:
Note: This is the Python implementation, StirrupJS is the Typescript implementation.
Tool interface allows easy tool definition# Core framework
pip install stirrup # or: uv add stirrup
# With all optional components
pip install 'stirrup[all]' # or: uv add 'stirrup[all]'
# Individual extras
pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
pip install 'stirrup[docker]' # or: uv add 'stirrup[docker]'
pip install 'stirrup[e2b]' # or: uv add 'stirrup[e2b]'
pip install 'stirrup[mcp]' # or: uv add 'stirrup[mcp]'
pip install 'stirrup[browser]' # or: uv add 'stirrup[browser]'
import asyncio
from stirrup import Agent
from stirrup.clients.chat_completions_client import ChatCompletionsClient
async def main() -> None:
"""Run an agent that searches the web and creates a chart."""
# Create client using ChatCompletionsClient
# Automatically uses OPENROUTER_API_KEY environment variable
client = ChatCompletionsClient(
base_url="https://openrouter.ai/api/v1",
model="anthropic/claude-opus-5",
max_tokens=8_192,
context_window_tokens=1_000_000,
)
# As no tools are provided, the agent will use the default tools, which consist of:
# - Web tools (web search and web fetching, note web search requires BRAVE_API_KEY)
# - Local code execution tool (to execute shell commands)
agent = Agent(client=client, name="agent", max_turns=15)
# Run with session context - handles tool lifecycle, logging and file outputs
async with agent.session(output_dir="./output/getting_started_example") as session:
finish_params, history, metadata = await session.run(
"""
What is the population of Australia over the last 3 years? Search the web to find out and create a
simple chart using matplotlib showing the current population per year."""
)
print("Finish params: ", finish_params)
print("History: ", history)
print("Metadata: ", metadata)
if __name__ == "__main__":
asyncio.run(main())
Note: This example uses OpenRouter. Set
OPENROUTER_API_KEYin your environment before running. Web search requires aBRAVE_API_KEY. The agent will still work without it, but web search will be unavailable.
For using Stirrup as a foundation for your own fully customized agent, you can clone and import Stirrup locally:
# Clone the repository
git clone https://github.com/ArtificialAnalysis/Stirrup.git
cd stirrup
# Install in editable mode
pip install -e . # or: uv venv && uv pip install -e .
# Or with all optional dependencies
pip install -e '.[all]' # or: uv venv && uv pip install -e '.[all]'
See the Full Customization guide for more details.
Agent - Configures and runs the agent loop until a finish tool is called or max turns reachedsession() - Context manager that sets up tools, manages files, and handles cleanupTool - Define tools with Pydantic parametersToolProvider - Manage tools that require lifecycle (connections, temp directories, etc.)default_tools() - Standard tools included by default: code execution and web toolsFor non-OpenAI providers, change the base URL of the ChatCompletionsClient, use the LiteLLMClient (requires installation of optional stirrup[litellm] dependencies), or create your own client.
# Create client using Deepseek's OpenAI-compatible endpoint
client = ChatCompletionsClient(
base_url="https://api.deepseek.com",
model="deepseek-v4-flash", # or "deepseek-v4-pro" for the larger model
max_tokens=8_192,
context_window_tokens=1_000_000,
api_key=os.environ["DEEPSEEK_API_KEY"],
)
agent = Agent(client=client, name="deepseek_agent")
# Ensure LiteLLM is added with: pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
# Create LiteLLM client for Anthropic Claude
# See https://docs.litellm.ai/docs/providers for all supported providers
client = LiteLLMClient(
model_slug="anthropic/claude-opus-5",
max_tokens=8_192,
context_window_tokens=1_000_000,
)
# Pass client to Agent - model info comes from client.model_slug
agent = Agent(
client=client,
name="claude_agent",
)
See LiteLLM Example or Deepseek Example for complete examples.
When you create an Agent without specifying tools, it uses default_tools():
| Tool Provider | Tools Provided | Description |
|---|---|---|
LocalCodeExecToolProvider | code_exec | Execute shell commands in an isolated temp directory |
WebToolProvider | web_fetch, web_search | Fetch web pages and search (search requires BRAVE_API_KEY) |
Each call returns fresh provider instances. Provider instances hold per-session state (a temp directory, an HTTP client), so concurrent sessions must not share them.
Breaking change: the DEFAULT_TOOLS list was removed because every caller shared the same two
provider instances. Migrate tools=DEFAULT_TOOLS to tools=default_tools(), and
tools=[*DEFAULT_TOOLS, extra_tool] to tools=[*default_tools(), extra_tool].
import asyncio
from stirrup import Agent
from stirrup.clients.chat_completions_client import ChatCompletionsClient
from stirrup.tools import CALCULATOR_TOOL, default_tools
# Create client for OpenRouter
client = ChatCompletionsClient(
base_url="https://openrouter.ai/api/v1",
model="anthropic/claude-opus-5",
max_tokens=8_192,
context_window_tokens=1_000_000,
)
# Create agent with default tools + calculator tool
agent = Agent(
client=client,
name="web_calculator_agent",
tools=[*default_tools(), CALCULATOR_TOOL],
)
from pydantic import BaseModel, Field
from stirrup import Agent, Tool, ToolResult, ToolUseCountMetadata
from stirrup.clients.chat_completions_client import ChatCompletionsClient
from stirrup.tools import default_tools
class GreetParams(BaseModel):
"""Parameters for the greet tool."""
name: str = Field(description="Name of the person to greet")
formal: bool = Field(default=False, description="Use formal greeting")
def greet(params: GreetParams) -> ToolResult[ToolUseCountMetadata]:
greeting = f"Good day, {params.name}." if params.formal else f"Hey {params.name}!"
return ToolResult(
content=greeting,
metadata=ToolUseCountMetadata(),
)
GREET_TOOL = Tool(
name="greet",
description="Greet someone by name",
parameters=GreetParams,
executor=greet,
)
# Create client for OpenRouter
client = ChatCompletionsClient(
base_url="https://openrouter.ai/api/v1",
model="anthropic/claude-opus-5",
max_tokens=8_192,
context_window_tokens=1_000_000,
)
# Add custom tool to default tools
agent = Agent(
client=client,
name="greeting_agent",
tools=[*default_tools(), GREET_TOOL],
)
Full documentation: artificialanalysis.github.io/Stirrup
Build and serve locally:
uv run mkdocs serve
# Format and lint code
uv run ruff format
uv run ruff check
# Type check
uv run ty check
# Run tests
uv run pytest tests
Licensed under the MIT LICENSE.
.env.example
.github/
ISSUE_TEMPLATE/
bug_report.yml
config.yml
docs.yml
feature_request.yml
workflows/
ci.yaml
mkdocs.yaml
release.yaml
.gitignore
.python-version
assets/
stirrup-banner.png
CHANGELOG.md
docs/
api/
clients/
chat_completions.md
litellm.md
open_responses.md
core/
agent.md
exceptions.md
models.md
tools/
browser-use.md
code_backends.md
index.md
mcp.md
view-image.md
web.md
utils/
logging.md
text.md
assets/
extra.css
favicon.ico
favicon.png
logo.png
CNAME
concepts.md
examples.md
extending/
clients.md
code_backends.md
full-customization.md
loggers.md
tools.md
getting-started.md
guides/
caching.md
code-execution.md
mcp.md
skills.md
slack.md
sub-agents.md
tool-providers.md
tools.md
index.md
examples/
__init__.py
browser_use_example.py
code_executor/
code_executor.py
Dockerfile
task.txt
custom_tool_example.py
deepseek_example.py
e2b_example.py
getting_started.py
litellm_example.py
mcp_example.py
open_responses_example.py
skills/
Dockerfile
sample_data.csv
skills_example.py
slack_bot_example.py
sub_agent_example.py
user_input_example.py
view_image_example.py
web_calculator.py
LICENSE
mkdocs.yml
pyproject.toml
README.md
scripts/
release.sh
skills/
data_analysis/
reference/
aggregations.md
loading.md
statistics.md
time_series.md
transformations.md
visualization.md
scripts/
explore_data.py
summary_stats.py
SKILL.md
src/
stirrup/
__init__.py
clients/
__init__.py
chat_completions_client.py
litellm_client.py
open_responses_client.py
utils.py
constants.py
core/
__init__.py
agent.py
cache.py
exceptions.py
models.py
integrations/
__init__.py
slack/
__init__.py
__main__.py
Dockerfile
README.md
slack.py
prompts/
__init__.py
base_system_prompt.txt
message_summarizer_bridge.txt
message_summarizer_text_only.txt
message_summarizer.txt
py.typed
skills/
__init__.py
skills.py
tools/
__init__.py
browser_use.py
calculator.py
code_backends/
__init__.py
base.py
docker.py
e2b.py
local.py
finish.py
mcp.py
user_input.py
view_image.py
web.py
utils/
__init__.py
logging.py
text.py
tests/
__init__.py
conftest.py
test_agent.py
test_assistant_blocks.py
test_browser_use.py
test_chat_completions_client.py
test_clients_utils.py
test_docker_execution.py
test_e2b_execution.py
test_litellm_client.py
test_local_execution.py
test_logging.py
test_mcp_image_smoke.py
test_model_speed.py
test_open_responses_client.py
test_output_paths.py
test_view_image.py
test_web_fetch.py
test_web_search.py
uv.lockFAQ
stirrup is a Claude Code plugin with 1 hand-picked skill for development work, indexed on Flowy. Install it with the command on its page. It includes data_analysis. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.