stirrup 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stirrup-0.1.1 → stirrup-0.1.2}/PKG-INFO +36 -16
- {stirrup-0.1.1 → stirrup-0.1.2}/README.md +34 -14
- {stirrup-0.1.1 → stirrup-0.1.2}/pyproject.toml +2 -2
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/core/agent.py +26 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/core/models.py +4 -0
- stirrup-0.1.2/src/stirrup/skills/__init__.py +24 -0
- stirrup-0.1.2/src/stirrup/skills/skills.py +145 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/code_backends/docker.py +16 -4
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/code_backends/e2b.py +7 -2
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/code_backends/local.py +14 -2
- {stirrup-0.1.1 → stirrup-0.1.2}/LICENSE +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/clients/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/clients/chat_completions_client.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/clients/litellm_client.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/clients/utils.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/constants.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/core/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/core/exceptions.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/prompts/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/prompts/base_system_prompt.txt +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/prompts/message_summarizer.txt +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/prompts/message_summarizer_bridge.txt +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/py.typed +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/calculator.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/code_backends/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/code_backends/base.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/finish.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/mcp.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/view_image.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/tools/web.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/utils/__init__.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/utils/logging.py +0 -0
- {stirrup-0.1.1 → stirrup-0.1.2}/src/stirrup/utils/text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: stirrup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: The lightweight foundation for building agents
|
|
5
5
|
Keywords: ai,agent,llm,openai,anthropic,tools,framework
|
|
6
6
|
Author: Artificial Analysis, Inc.
|
|
@@ -54,7 +54,7 @@ Requires-Dist: e2b-code-interpreter>=2.3.0 ; extra == 'e2b'
|
|
|
54
54
|
Requires-Dist: litellm>=1.79.3 ; extra == 'litellm'
|
|
55
55
|
Requires-Dist: mcp>=1.9.0 ; extra == 'mcp'
|
|
56
56
|
Requires-Python: >=3.12
|
|
57
|
-
Project-URL: Documentation, https://artificialanalysis.
|
|
57
|
+
Project-URL: Documentation, https://stirrup.artificialanalysis.ai
|
|
58
58
|
Project-URL: Homepage, https://github.com/ArtificialAnalysis/Stirrup
|
|
59
59
|
Project-URL: Repository, https://github.com/ArtificialAnalysis/Stirrup
|
|
60
60
|
Provides-Extra: all
|
|
@@ -65,7 +65,7 @@ Provides-Extra: mcp
|
|
|
65
65
|
Description-Content-Type: text/markdown
|
|
66
66
|
|
|
67
67
|
<div align="center">
|
|
68
|
-
<a href="">
|
|
68
|
+
<a href="https://stirrup.artificialanalysis.ai">
|
|
69
69
|
<picture>
|
|
70
70
|
<img alt="Stirrup" src="https://github.com/ArtificialAnalysis/Stirrup/blob/048653717d8662b0b81d152a037995af1c926afc/assets/stirrup-banner.png?raw=true" width="700">
|
|
71
71
|
</picture>
|
|
@@ -79,7 +79,7 @@ Description-Content-Type: text/markdown
|
|
|
79
79
|
<p align="center">
|
|
80
80
|
<a href="https://pypi.python.org/pypi/stirrup"><img src="https://img.shields.io/pypi/v/stirrup" alt="PyPI version" /></a> <!--
|
|
81
81
|
--><a href="https://github.com/ArtificialAnalysis/Stirrup/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ArtificialAnalysis/Stirrup" alt="License" /></a> <!--
|
|
82
|
-
--><a href="https://artificialanalysis.
|
|
82
|
+
--><a href="https://stirrup.artificialanalysis.ai"><img src="https://img.shields.io/badge/MkDocs-4F46E5?logo=materialformkdocs&logoColor=fff" alt="MkDocs" /></a>
|
|
83
83
|
</p>
|
|
84
84
|
|
|
85
85
|
|
|
@@ -96,6 +96,7 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
96
96
|
- Code execution (local, Docker container, E2B sandbox)
|
|
97
97
|
- MCP client
|
|
98
98
|
- Document input and output
|
|
99
|
+
- **Skills system:** Extend agent capabilities with modular, domain-specific instruction packages
|
|
99
100
|
- **Flexible tool execution:** A generic `Tool` class allows easy tool definition and extension
|
|
100
101
|
- **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
101
102
|
- **Flexible provider support:** Pre-built support for OpenAI-compatible APIs and LiteLLM, or bring your own client
|
|
@@ -105,16 +106,16 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
105
106
|
|
|
106
107
|
```bash
|
|
107
108
|
# Core framework
|
|
108
|
-
|
|
109
|
+
pip install stirrup # or: uv add stirrup
|
|
109
110
|
|
|
110
111
|
# With all optional components
|
|
111
|
-
|
|
112
|
+
pip install 'stirrup[all]' # or: uv add 'stirrup[all]'
|
|
112
113
|
|
|
113
114
|
# Individual extras
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
116
|
+
pip install 'stirrup[docker]' # or: uv add 'stirrup[docker]'
|
|
117
|
+
pip install 'stirrup[e2b]' # or: uv add 'stirrup[e2b]'
|
|
118
|
+
pip install 'stirrup[mcp]' # or: uv add 'stirrup[mcp]'
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
## Quick Start
|
|
@@ -160,6 +161,24 @@ if __name__ == "__main__":
|
|
|
160
161
|
|
|
161
162
|
> **Note:** This example uses OpenRouter. Set `OPENROUTER_API_KEY` in your environment before running. Web search requires a `BRAVE_API_KEY`. The agent will still work without it, but web search will be unavailable.
|
|
162
163
|
|
|
164
|
+
## Full Customization
|
|
165
|
+
|
|
166
|
+
For using Stirrup as a foundation for your own fully customized agent, you can clone and import Stirrup locally:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Clone the repository
|
|
170
|
+
git clone https://github.com/ArtificialAnalysis/Stirrup.git
|
|
171
|
+
cd stirrup
|
|
172
|
+
|
|
173
|
+
# Install in editable mode
|
|
174
|
+
pip install -e . # or: uv venv && uv pip install -e .
|
|
175
|
+
|
|
176
|
+
# Or with all optional dependencies
|
|
177
|
+
pip install -e '.[all]' # or: uv venv && uv pip install -e '.[all]'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
See the [Full Customization guide](https://stirrup.artificialanalysis.ai/extending/full-customization/) for more details.
|
|
181
|
+
|
|
163
182
|
## How It Works
|
|
164
183
|
|
|
165
184
|
- **`Agent`** - Configures and runs the agent loop until a finish tool is called or max turns reached
|
|
@@ -188,6 +207,7 @@ agent = Agent(client=client, name="deepseek_agent")
|
|
|
188
207
|
### LiteLLM (Anthropic, Google, etc.)
|
|
189
208
|
|
|
190
209
|
```python
|
|
210
|
+
# Ensure LiteLLM is added with: pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
191
211
|
# Create LiteLLM client for Anthropic Claude
|
|
192
212
|
# See https://docs.litellm.ai/docs/providers for all supported providers
|
|
193
213
|
client = LiteLLMClient(
|
|
@@ -202,7 +222,7 @@ agent = Agent(
|
|
|
202
222
|
)
|
|
203
223
|
```
|
|
204
224
|
|
|
205
|
-
See [LiteLLM Example](https://artificialanalysis.
|
|
225
|
+
See [LiteLLM Example](https://stirrup.artificialanalysis.ai/examples/#litellm-multi-provider-support) or [Deepseek Example](https://stirrup.artificialanalysis.ai/examples/#openai-compatible-apis-deepseek-vllm-ollama) for complete examples.
|
|
206
226
|
|
|
207
227
|
## Default Tools
|
|
208
228
|
|
|
@@ -285,14 +305,14 @@ agent = Agent(
|
|
|
285
305
|
|
|
286
306
|
## Next Steps
|
|
287
307
|
|
|
288
|
-
- [Getting Started](https://artificialanalysis.
|
|
289
|
-
- [Core Concepts](https://artificialanalysis.
|
|
290
|
-
- [Examples](https://artificialanalysis.
|
|
291
|
-
- [Creating Tools](https://artificialanalysis.
|
|
308
|
+
- [Getting Started](https://stirrup.artificialanalysis.ai/getting-started/) - Installation and first agent tutorial
|
|
309
|
+
- [Core Concepts](https://stirrup.artificialanalysis.ai/concepts/) - Understand Agent, Tools, and Sessions
|
|
310
|
+
- [Examples](https://stirrup.artificialanalysis.ai/examples/) - Working examples for common patterns
|
|
311
|
+
- [Creating Tools](https://stirrup.artificialanalysis.ai/guides/tools/) - Build your own tools
|
|
292
312
|
|
|
293
313
|
## Documentation
|
|
294
314
|
|
|
295
|
-
Full documentation: [artificialanalysis.github.io/Stirrup](https://artificialanalysis.
|
|
315
|
+
Full documentation: [artificialanalysis.github.io/Stirrup](https://stirrup.artificialanalysis.ai)
|
|
296
316
|
|
|
297
317
|
Build and serve locally:
|
|
298
318
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<a href="">
|
|
2
|
+
<a href="https://stirrup.artificialanalysis.ai">
|
|
3
3
|
<picture>
|
|
4
4
|
<img alt="Stirrup" src="https://github.com/ArtificialAnalysis/Stirrup/blob/048653717d8662b0b81d152a037995af1c926afc/assets/stirrup-banner.png?raw=true" width="700">
|
|
5
5
|
</picture>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://pypi.python.org/pypi/stirrup"><img src="https://img.shields.io/pypi/v/stirrup" alt="PyPI version" /></a> <!--
|
|
15
15
|
--><a href="https://github.com/ArtificialAnalysis/Stirrup/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ArtificialAnalysis/Stirrup" alt="License" /></a> <!--
|
|
16
|
-
--><a href="https://artificialanalysis.
|
|
16
|
+
--><a href="https://stirrup.artificialanalysis.ai"><img src="https://img.shields.io/badge/MkDocs-4F46E5?logo=materialformkdocs&logoColor=fff" alt="MkDocs" /></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
|
|
@@ -30,6 +30,7 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
30
30
|
- Code execution (local, Docker container, E2B sandbox)
|
|
31
31
|
- MCP client
|
|
32
32
|
- Document input and output
|
|
33
|
+
- **Skills system:** Extend agent capabilities with modular, domain-specific instruction packages
|
|
33
34
|
- **Flexible tool execution:** A generic `Tool` class allows easy tool definition and extension
|
|
34
35
|
- **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
35
36
|
- **Flexible provider support:** Pre-built support for OpenAI-compatible APIs and LiteLLM, or bring your own client
|
|
@@ -39,16 +40,16 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
42
|
# Core framework
|
|
42
|
-
|
|
43
|
+
pip install stirrup # or: uv add stirrup
|
|
43
44
|
|
|
44
45
|
# With all optional components
|
|
45
|
-
|
|
46
|
+
pip install 'stirrup[all]' # or: uv add 'stirrup[all]'
|
|
46
47
|
|
|
47
48
|
# Individual extras
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
50
|
+
pip install 'stirrup[docker]' # or: uv add 'stirrup[docker]'
|
|
51
|
+
pip install 'stirrup[e2b]' # or: uv add 'stirrup[e2b]'
|
|
52
|
+
pip install 'stirrup[mcp]' # or: uv add 'stirrup[mcp]'
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
## Quick Start
|
|
@@ -94,6 +95,24 @@ if __name__ == "__main__":
|
|
|
94
95
|
|
|
95
96
|
> **Note:** This example uses OpenRouter. Set `OPENROUTER_API_KEY` in your environment before running. Web search requires a `BRAVE_API_KEY`. The agent will still work without it, but web search will be unavailable.
|
|
96
97
|
|
|
98
|
+
## Full Customization
|
|
99
|
+
|
|
100
|
+
For using Stirrup as a foundation for your own fully customized agent, you can clone and import Stirrup locally:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Clone the repository
|
|
104
|
+
git clone https://github.com/ArtificialAnalysis/Stirrup.git
|
|
105
|
+
cd stirrup
|
|
106
|
+
|
|
107
|
+
# Install in editable mode
|
|
108
|
+
pip install -e . # or: uv venv && uv pip install -e .
|
|
109
|
+
|
|
110
|
+
# Or with all optional dependencies
|
|
111
|
+
pip install -e '.[all]' # or: uv venv && uv pip install -e '.[all]'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See the [Full Customization guide](https://stirrup.artificialanalysis.ai/extending/full-customization/) for more details.
|
|
115
|
+
|
|
97
116
|
## How It Works
|
|
98
117
|
|
|
99
118
|
- **`Agent`** - Configures and runs the agent loop until a finish tool is called or max turns reached
|
|
@@ -122,6 +141,7 @@ agent = Agent(client=client, name="deepseek_agent")
|
|
|
122
141
|
### LiteLLM (Anthropic, Google, etc.)
|
|
123
142
|
|
|
124
143
|
```python
|
|
144
|
+
# Ensure LiteLLM is added with: pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
125
145
|
# Create LiteLLM client for Anthropic Claude
|
|
126
146
|
# See https://docs.litellm.ai/docs/providers for all supported providers
|
|
127
147
|
client = LiteLLMClient(
|
|
@@ -136,7 +156,7 @@ agent = Agent(
|
|
|
136
156
|
)
|
|
137
157
|
```
|
|
138
158
|
|
|
139
|
-
See [LiteLLM Example](https://artificialanalysis.
|
|
159
|
+
See [LiteLLM Example](https://stirrup.artificialanalysis.ai/examples/#litellm-multi-provider-support) or [Deepseek Example](https://stirrup.artificialanalysis.ai/examples/#openai-compatible-apis-deepseek-vllm-ollama) for complete examples.
|
|
140
160
|
|
|
141
161
|
## Default Tools
|
|
142
162
|
|
|
@@ -219,14 +239,14 @@ agent = Agent(
|
|
|
219
239
|
|
|
220
240
|
## Next Steps
|
|
221
241
|
|
|
222
|
-
- [Getting Started](https://artificialanalysis.
|
|
223
|
-
- [Core Concepts](https://artificialanalysis.
|
|
224
|
-
- [Examples](https://artificialanalysis.
|
|
225
|
-
- [Creating Tools](https://artificialanalysis.
|
|
242
|
+
- [Getting Started](https://stirrup.artificialanalysis.ai/getting-started/) - Installation and first agent tutorial
|
|
243
|
+
- [Core Concepts](https://stirrup.artificialanalysis.ai/concepts/) - Understand Agent, Tools, and Sessions
|
|
244
|
+
- [Examples](https://stirrup.artificialanalysis.ai/examples/) - Working examples for common patterns
|
|
245
|
+
- [Creating Tools](https://stirrup.artificialanalysis.ai/guides/tools/) - Build your own tools
|
|
226
246
|
|
|
227
247
|
## Documentation
|
|
228
248
|
|
|
229
|
-
Full documentation: [artificialanalysis.github.io/Stirrup](https://artificialanalysis.
|
|
249
|
+
Full documentation: [artificialanalysis.github.io/Stirrup](https://stirrup.artificialanalysis.ai)
|
|
230
250
|
|
|
231
251
|
Build and serve locally:
|
|
232
252
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stirrup"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "The lightweight foundation for building agents"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -45,7 +45,7 @@ all = ["stirrup[litellm,e2b,docker,mcp]"]
|
|
|
45
45
|
|
|
46
46
|
[project.urls]
|
|
47
47
|
Homepage = "https://github.com/ArtificialAnalysis/Stirrup"
|
|
48
|
-
Documentation = "https://artificialanalysis.
|
|
48
|
+
Documentation = "https://stirrup.artificialanalysis.ai"
|
|
49
49
|
Repository = "https://github.com/ArtificialAnalysis/Stirrup"
|
|
50
50
|
|
|
51
51
|
[build-system]
|
|
@@ -36,6 +36,7 @@ from stirrup.core.models import (
|
|
|
36
36
|
UserMessage,
|
|
37
37
|
)
|
|
38
38
|
from stirrup.prompts import MESSAGE_SUMMARIZER, MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE
|
|
39
|
+
from stirrup.skills import SkillMetadata, format_skills_section, load_skills_metadata
|
|
39
40
|
from stirrup.tools import DEFAULT_TOOLS
|
|
40
41
|
from stirrup.tools.code_backends.base import CodeExecToolProvider
|
|
41
42
|
from stirrup.tools.code_backends.local import LocalCodeExecToolProvider
|
|
@@ -70,6 +71,7 @@ class SessionState:
|
|
|
70
71
|
parent_exec_env: CodeExecToolProvider | None = None
|
|
71
72
|
depth: int = 0
|
|
72
73
|
uploaded_file_paths: list[str] = field(default_factory=list) # Paths of files uploaded to exec_env
|
|
74
|
+
skills_metadata: list[SkillMetadata] = field(default_factory=list) # Loaded skills metadata
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
_SESSION_STATE: contextvars.ContextVar[SessionState] = contextvars.ContextVar("session_state")
|
|
@@ -222,6 +224,7 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
222
224
|
# Session configuration (set during session(), used in __aenter__)
|
|
223
225
|
self._pending_output_dir: Path | None = None
|
|
224
226
|
self._pending_input_files: str | Path | list[str | Path] | None = None
|
|
227
|
+
self._pending_skills_dir: Path | None = None
|
|
225
228
|
|
|
226
229
|
# Instance-scoped state (populated during __aenter__, isolated per agent instance)
|
|
227
230
|
self._active_tools: dict[str, Tool] = {}
|
|
@@ -258,6 +261,7 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
258
261
|
self,
|
|
259
262
|
output_dir: Path | str | None = None,
|
|
260
263
|
input_files: str | Path | list[str | Path] | None = None,
|
|
264
|
+
skills_dir: Path | str | None = None,
|
|
261
265
|
) -> Self:
|
|
262
266
|
"""Configure a session and return self for use as async context manager.
|
|
263
267
|
|
|
@@ -270,6 +274,9 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
270
274
|
- Glob patterns (e.g., "data/*.csv", "**/*.py")
|
|
271
275
|
Raises ValueError if no CodeExecToolProvider is configured
|
|
272
276
|
or if a glob pattern matches no files.
|
|
277
|
+
skills_dir: Directory containing skill definitions to load and make available
|
|
278
|
+
to the agent. Skills are uploaded to the execution environment
|
|
279
|
+
and their metadata is included in the system prompt.
|
|
273
280
|
|
|
274
281
|
Returns:
|
|
275
282
|
Self, for use with `async with agent.session(...) as session:`
|
|
@@ -285,6 +292,7 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
285
292
|
"""
|
|
286
293
|
self._pending_output_dir = Path(output_dir) if output_dir else None
|
|
287
294
|
self._pending_input_files = input_files
|
|
295
|
+
self._pending_skills_dir = Path(skills_dir) if skills_dir else None
|
|
288
296
|
return self
|
|
289
297
|
|
|
290
298
|
def _resolve_input_files(self, input_files: str | Path | list[str | Path]) -> list[Path]:
|
|
@@ -410,6 +418,12 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
410
418
|
files_section += f"\n- {file_path}"
|
|
411
419
|
parts.append(files_section)
|
|
412
420
|
|
|
421
|
+
# Skills section (if skills were loaded)
|
|
422
|
+
if state and state.skills_metadata:
|
|
423
|
+
skills_section = format_skills_section(state.skills_metadata)
|
|
424
|
+
if skills_section:
|
|
425
|
+
parts.append(f"\n\n{skills_section}")
|
|
426
|
+
|
|
413
427
|
# User's custom system prompt (if provided)
|
|
414
428
|
if self._system_prompt:
|
|
415
429
|
parts.append(f"\n\nFollow these instructions from the User:\n{self._system_prompt}")
|
|
@@ -588,6 +602,18 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
588
602
|
raise RuntimeError(f"Failed to upload files: {result.failed}")
|
|
589
603
|
self._pending_input_files = None # Clear pending state
|
|
590
604
|
|
|
605
|
+
# Upload skills directory if it exists and load metadata
|
|
606
|
+
if self._pending_skills_dir:
|
|
607
|
+
skills_path = self._pending_skills_dir
|
|
608
|
+
if skills_path.exists() and skills_path.is_dir():
|
|
609
|
+
if state.exec_env:
|
|
610
|
+
logger.debug("[%s __aenter__] Uploading skills directory: %s", self._name, skills_path)
|
|
611
|
+
await state.exec_env.upload_files(skills_path, dest_dir="skills")
|
|
612
|
+
# Load skills metadata (even if no exec_env, for system prompt)
|
|
613
|
+
state.skills_metadata = load_skills_metadata(skills_path)
|
|
614
|
+
logger.debug("[%s __aenter__] Loaded %d skills", self._name, len(state.skills_metadata))
|
|
615
|
+
self._pending_skills_dir = None # Clear pending state
|
|
616
|
+
|
|
591
617
|
# Configure and enter logger context
|
|
592
618
|
self._logger.name = self._name
|
|
593
619
|
self._logger.model = self._client.model_slug
|
|
@@ -430,6 +430,7 @@ class Tool[P: BaseModel, M](BaseModel):
|
|
|
430
430
|
(setup/teardown, resource pooling), use a ToolProvider instead.
|
|
431
431
|
|
|
432
432
|
Example with parameters:
|
|
433
|
+
```python
|
|
433
434
|
class CalcParams(BaseModel):
|
|
434
435
|
expression: str
|
|
435
436
|
|
|
@@ -439,13 +440,16 @@ class Tool[P: BaseModel, M](BaseModel):
|
|
|
439
440
|
parameters=CalcParams,
|
|
440
441
|
executor=lambda p: ToolResult(content=str(eval(p.expression))),
|
|
441
442
|
)
|
|
443
|
+
```
|
|
442
444
|
|
|
443
445
|
Example without parameters:
|
|
446
|
+
```python
|
|
444
447
|
time_tool = Tool[None, None](
|
|
445
448
|
name="time",
|
|
446
449
|
description="Get current time",
|
|
447
450
|
executor=lambda _: ToolResult(content=datetime.now().isoformat()),
|
|
448
451
|
)
|
|
452
|
+
```
|
|
449
453
|
"""
|
|
450
454
|
|
|
451
455
|
name: str
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Skills module for agent capabilities.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for loading and managing agent skills.
|
|
4
|
+
Skills are modular packages with instructions and resources that agents
|
|
5
|
+
can discover and use dynamically.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
from stirrup.skills import load_skills_metadata, format_skills_section
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Load skills from directory
|
|
12
|
+
skills = load_skills_metadata(Path("skills"))
|
|
13
|
+
|
|
14
|
+
# Format for system prompt
|
|
15
|
+
prompt_section = format_skills_section(skills)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from stirrup.skills.skills import SkillMetadata, format_skills_section, load_skills_metadata
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SkillMetadata",
|
|
22
|
+
"format_skills_section",
|
|
23
|
+
"load_skills_metadata",
|
|
24
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Skills loader for agent capabilities.
|
|
2
|
+
|
|
3
|
+
Skills are modular packages that extend agent capabilities with instructions,
|
|
4
|
+
scripts, and resources. Each skill is a directory containing a SKILL.md file
|
|
5
|
+
with YAML frontmatter (name, description) and detailed instructions.
|
|
6
|
+
|
|
7
|
+
Based on: https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SkillMetadata:
|
|
20
|
+
"""Metadata extracted from a skill's SKILL.md frontmatter."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
description: str
|
|
24
|
+
path: str # Relative path like "skills/data_analysis"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_frontmatter(content: str) -> dict[str, str]:
|
|
28
|
+
"""Parse YAML frontmatter from markdown content.
|
|
29
|
+
|
|
30
|
+
Extracts metadata between --- markers at the start of the file.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
content: Markdown content with optional YAML frontmatter
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dictionary of frontmatter key-value pairs, empty if no frontmatter found
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
# Match YAML frontmatter between --- markers
|
|
40
|
+
pattern = r"^---\s*\n(.*?)\n---"
|
|
41
|
+
match = re.match(pattern, content, re.DOTALL)
|
|
42
|
+
|
|
43
|
+
if not match:
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
frontmatter_text = match.group(1)
|
|
47
|
+
result: dict[str, str] = {}
|
|
48
|
+
|
|
49
|
+
# Simple YAML parsing for key: value pairs
|
|
50
|
+
for line in frontmatter_text.strip().split("\n"):
|
|
51
|
+
line = line.strip()
|
|
52
|
+
if ":" in line:
|
|
53
|
+
key, value = line.split(":", 1)
|
|
54
|
+
result[key.strip()] = value.strip()
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_skills_metadata(skills_dir: Path) -> list[SkillMetadata]:
|
|
60
|
+
"""Scan skills directory for SKILL.md files and extract metadata.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
skills_dir: Path to the skills directory
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of SkillMetadata for each valid skill found.
|
|
67
|
+
Returns empty list if skills_dir doesn't exist or has no skills.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
if not skills_dir.exists():
|
|
71
|
+
logger.debug("Skills directory does not exist: %s", skills_dir)
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
if not skills_dir.is_dir():
|
|
75
|
+
logger.warning("Skills path is not a directory: %s", skills_dir)
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
skills: list[SkillMetadata] = []
|
|
79
|
+
|
|
80
|
+
for skill_path in skills_dir.iterdir():
|
|
81
|
+
if not skill_path.is_dir():
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
skill_md = skill_path / "SKILL.md"
|
|
85
|
+
if not skill_md.exists():
|
|
86
|
+
logger.debug("Skill directory missing SKILL.md: %s", skill_path)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
91
|
+
metadata = parse_frontmatter(content)
|
|
92
|
+
|
|
93
|
+
name = metadata.get("name")
|
|
94
|
+
description = metadata.get("description")
|
|
95
|
+
|
|
96
|
+
if not name or not description:
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Skill %s missing required frontmatter (name, description)",
|
|
99
|
+
skill_path.name,
|
|
100
|
+
)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
skills.append(
|
|
104
|
+
SkillMetadata(
|
|
105
|
+
name=name,
|
|
106
|
+
description=description,
|
|
107
|
+
path=f"skills/{skill_path.name}",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
logger.debug("Loaded skill: %s", name)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning("Failed to load skill %s: %s", skill_path.name, e)
|
|
114
|
+
|
|
115
|
+
return skills
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_skills_section(skills: list[SkillMetadata]) -> str:
|
|
119
|
+
"""Format skills metadata as a system prompt section.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
skills: List of skill metadata to include
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Formatted string for inclusion in system prompt.
|
|
126
|
+
Returns empty string if no skills provided.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
if not skills:
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
lines = [
|
|
133
|
+
"## Available Skills",
|
|
134
|
+
"",
|
|
135
|
+
"You have access to the following skills located in the `skills/` directory. "
|
|
136
|
+
"Each skill contains a SKILL.md file with detailed instructions and potentially bundled scripts.",
|
|
137
|
+
"",
|
|
138
|
+
"To use a skill:",
|
|
139
|
+
"1. Read the full instructions: `cat <skill_path>/SKILL.md`",
|
|
140
|
+
"2. Follow the instructions and use any bundled resources as described",
|
|
141
|
+
"",
|
|
142
|
+
]
|
|
143
|
+
lines.extend([f"- **{skill.name}**: {skill.description} (`{skill.path}/SKILL.md`)" for skill in skills])
|
|
144
|
+
|
|
145
|
+
return "\n".join(lines)
|
|
@@ -330,7 +330,7 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
330
330
|
context_path = self._dockerfile_context.resolve() if self._dockerfile_context else dockerfile_path.parent
|
|
331
331
|
|
|
332
332
|
# Generate unique tag based on dockerfile path
|
|
333
|
-
tag = f"
|
|
333
|
+
tag = f"stirrup-exec-env-{hashlib.md5(str(dockerfile_path).encode()).hexdigest()[:8]}"
|
|
334
334
|
|
|
335
335
|
logger.info("Building image from %s with tag %s", dockerfile_path, tag)
|
|
336
336
|
|
|
@@ -710,8 +710,20 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
710
710
|
logger.debug("Uploaded file: %s -> %s", source, container_path)
|
|
711
711
|
|
|
712
712
|
elif source.is_dir():
|
|
713
|
-
|
|
714
|
-
|
|
713
|
+
# If dest_dir was explicitly provided, copy contents directly to dest_base
|
|
714
|
+
# Otherwise, create a subdirectory with the source's name
|
|
715
|
+
if dest_dir:
|
|
716
|
+
dest = dest_base
|
|
717
|
+
# Copy contents of source directory into dest_base
|
|
718
|
+
for item in source.iterdir():
|
|
719
|
+
item_dest = dest / item.name
|
|
720
|
+
if item.is_file():
|
|
721
|
+
shutil.copy2(item, item_dest)
|
|
722
|
+
else:
|
|
723
|
+
shutil.copytree(item, item_dest, dirs_exist_ok=True)
|
|
724
|
+
else:
|
|
725
|
+
dest = dest_base / source.name
|
|
726
|
+
shutil.copytree(source, dest, dirs_exist_ok=True)
|
|
715
727
|
# Track all individual files uploaded
|
|
716
728
|
for file_path in source.rglob("*"):
|
|
717
729
|
if file_path.is_file():
|
|
@@ -725,7 +737,7 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
725
737
|
size=file_path.stat().st_size,
|
|
726
738
|
),
|
|
727
739
|
)
|
|
728
|
-
logger.debug("Uploaded directory: %s -> %s
|
|
740
|
+
logger.debug("Uploaded directory: %s -> %s", source, dest)
|
|
729
741
|
|
|
730
742
|
except Exception as exc:
|
|
731
743
|
result.failed[str(source)] = str(exc)
|
|
@@ -320,10 +320,12 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
320
320
|
|
|
321
321
|
elif source.is_dir():
|
|
322
322
|
# Upload all files in directory recursively
|
|
323
|
+
# If dest_dir was explicitly provided, copy contents directly to dest_base
|
|
324
|
+
# Otherwise, create a subdirectory with the source's name
|
|
323
325
|
for file_path in source.rglob("*"):
|
|
324
326
|
if file_path.is_file():
|
|
325
327
|
relative = file_path.relative_to(source)
|
|
326
|
-
dest = f"{dest_base}/{source.name}/{relative}"
|
|
328
|
+
dest = f"{dest_base}/{relative}" if dest_dir else f"{dest_base}/{source.name}/{relative}"
|
|
327
329
|
content = file_path.read_bytes()
|
|
328
330
|
await self._sbx.files.write(dest, content)
|
|
329
331
|
result.uploaded.append(
|
|
@@ -333,7 +335,10 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
333
335
|
size=len(content),
|
|
334
336
|
),
|
|
335
337
|
)
|
|
336
|
-
|
|
338
|
+
if dest_dir:
|
|
339
|
+
logger.debug("Uploaded directory contents: %s -> %s", source, dest_base)
|
|
340
|
+
else:
|
|
341
|
+
logger.debug("Uploaded directory: %s -> %s/%s", source, dest_base, source.name)
|
|
337
342
|
|
|
338
343
|
except Exception as exc:
|
|
339
344
|
result.failed[str(source)] = str(exc)
|
|
@@ -440,8 +440,20 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
440
440
|
logger.debug("Uploaded file: %s -> %s", source, dest)
|
|
441
441
|
|
|
442
442
|
elif source.is_dir():
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
# If dest_dir was explicitly provided, copy contents directly to dest_base
|
|
444
|
+
# Otherwise, create a subdirectory with the source's name
|
|
445
|
+
if dest_dir:
|
|
446
|
+
dest = dest_base
|
|
447
|
+
# Copy contents of source directory into dest_base
|
|
448
|
+
for item in source.iterdir():
|
|
449
|
+
item_dest = dest / item.name
|
|
450
|
+
if item.is_file():
|
|
451
|
+
shutil.copy2(item, item_dest)
|
|
452
|
+
else:
|
|
453
|
+
shutil.copytree(item, item_dest, dirs_exist_ok=True)
|
|
454
|
+
else:
|
|
455
|
+
dest = dest_base / source.name
|
|
456
|
+
shutil.copytree(source, dest, dirs_exist_ok=True)
|
|
445
457
|
# Track all individual files uploaded
|
|
446
458
|
for file_path in source.rglob("*"):
|
|
447
459
|
if file_path.is_file():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|