stirrup 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
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/tools/code_backends/base.py +71 -12
- stirrup/tools/code_backends/docker.py +47 -0
- stirrup/tools/code_backends/e2b.py +62 -0
- stirrup/tools/code_backends/local.py +43 -0
- {stirrup-0.1.4.dist-info → stirrup-0.1.5.dist-info}/METADATA +3 -3
- {stirrup-0.1.4.dist-info → stirrup-0.1.5.dist-info}/RECORD +7 -7
- {stirrup-0.1.4.dist-info → stirrup-0.1.5.dist-info}/WHEEL +0 -0
|
@@ -245,6 +245,39 @@ class CodeExecToolProvider(ToolProvider, ABC):
|
|
|
245
245
|
"""
|
|
246
246
|
...
|
|
247
247
|
|
|
248
|
+
@abstractmethod
|
|
249
|
+
async def is_directory(self, path: str) -> bool:
|
|
250
|
+
"""Check if a path is a directory in this execution environment.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
path: Path within this execution environment.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if the path exists and is a directory, False otherwise.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
RuntimeError: If execution environment not started.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
@abstractmethod
|
|
265
|
+
async def list_files(self, path: str) -> list[str]:
|
|
266
|
+
"""List all files recursively in a directory within this execution environment.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
path: Directory path within this execution environment.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of file paths (relative to the given path) for all files in the directory.
|
|
273
|
+
Returns an empty list if the path is a file or doesn't exist.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
RuntimeError: If execution environment not started.
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
...
|
|
280
|
+
|
|
248
281
|
async def save_output_files(
|
|
249
282
|
self,
|
|
250
283
|
paths: list[str],
|
|
@@ -334,18 +367,44 @@ class CodeExecToolProvider(ToolProvider, ABC):
|
|
|
334
367
|
try:
|
|
335
368
|
if source_env:
|
|
336
369
|
# Cross-environment transfer: read from source_env
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
path_str
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
370
|
+
# Check if it's a directory first
|
|
371
|
+
if await source_env.is_directory(path_str):
|
|
372
|
+
# Handle directory recursively
|
|
373
|
+
# Preserve directory name when dest_dir not specified
|
|
374
|
+
dir_name = Path(path_str).name
|
|
375
|
+
files = await source_env.list_files(path_str)
|
|
376
|
+
for rel_file_path in files:
|
|
377
|
+
src_file_path = f"{path_str}/{rel_file_path}"
|
|
378
|
+
# If dest_dir specified, put files directly there
|
|
379
|
+
# Otherwise, preserve the source directory name
|
|
380
|
+
if dest_dir_str:
|
|
381
|
+
dest_path = f"{dest_dir_str}/{rel_file_path}"
|
|
382
|
+
else:
|
|
383
|
+
dest_path = f"{dir_name}/{rel_file_path}"
|
|
384
|
+
content = await source_env.read_file_bytes(src_file_path)
|
|
385
|
+
logger.debug(
|
|
386
|
+
"UPLOAD CROSS-ENV (dir): %s (%d bytes) from %s -> %s",
|
|
387
|
+
src_file_path,
|
|
388
|
+
len(content),
|
|
389
|
+
type(source_env).__name__,
|
|
390
|
+
dest_path,
|
|
391
|
+
)
|
|
392
|
+
await self.write_file_bytes(dest_path, content)
|
|
393
|
+
result.uploaded.append(UploadedFile(Path(src_file_path), dest_path, len(content)))
|
|
394
|
+
else:
|
|
395
|
+
# Single file transfer
|
|
396
|
+
content = await source_env.read_file_bytes(path_str)
|
|
397
|
+
filename = Path(path_str).name
|
|
398
|
+
dest_path = f"{dest_dir_str}/{filename}" if dest_dir_str else filename
|
|
399
|
+
logger.debug(
|
|
400
|
+
"UPLOAD CROSS-ENV: %s (%d bytes) from %s -> %s",
|
|
401
|
+
path_str,
|
|
402
|
+
len(content),
|
|
403
|
+
type(source_env).__name__,
|
|
404
|
+
dest_path,
|
|
405
|
+
)
|
|
406
|
+
await self.write_file_bytes(dest_path, content)
|
|
407
|
+
result.uploaded.append(UploadedFile(Path(path_str), dest_path, len(content)))
|
|
349
408
|
else:
|
|
350
409
|
# Local filesystem upload - must be handled by subclass
|
|
351
410
|
# This is a fallback that reads from local fs and writes to env
|
|
@@ -505,6 +505,53 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
505
505
|
host_path = self._container_path_to_host(path)
|
|
506
506
|
return host_path.exists() and host_path.is_file()
|
|
507
507
|
|
|
508
|
+
async def is_directory(self, path: str) -> bool:
|
|
509
|
+
"""Check if a path is a directory in the container.
|
|
510
|
+
|
|
511
|
+
Since files are volume-mounted, checks directly on the host temp directory.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
path: Path (relative or absolute container path).
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
True if the path exists and is a directory, False otherwise.
|
|
518
|
+
|
|
519
|
+
Raises:
|
|
520
|
+
RuntimeError: If environment not started.
|
|
521
|
+
ValueError: If path is outside mounted directory.
|
|
522
|
+
|
|
523
|
+
"""
|
|
524
|
+
host_path = self._container_path_to_host(path)
|
|
525
|
+
return host_path.exists() and host_path.is_dir()
|
|
526
|
+
|
|
527
|
+
async def list_files(self, path: str) -> list[str]:
|
|
528
|
+
"""List all files recursively in a directory within the container.
|
|
529
|
+
|
|
530
|
+
Since files are volume-mounted, lists directly from the host temp directory.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
path: Directory path (relative or absolute container path).
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
List of file paths (relative to the given path) for all files in the directory.
|
|
537
|
+
Returns an empty list if the path is a file or doesn't exist.
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
RuntimeError: If environment not started.
|
|
541
|
+
ValueError: If path is outside mounted directory.
|
|
542
|
+
|
|
543
|
+
"""
|
|
544
|
+
host_path = self._container_path_to_host(path)
|
|
545
|
+
if not host_path.exists() or not host_path.is_dir():
|
|
546
|
+
return []
|
|
547
|
+
|
|
548
|
+
files = []
|
|
549
|
+
for file_path in host_path.rglob("*"):
|
|
550
|
+
if file_path.is_file():
|
|
551
|
+
rel_path = file_path.relative_to(host_path)
|
|
552
|
+
files.append(str(rel_path))
|
|
553
|
+
return files
|
|
554
|
+
|
|
508
555
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
509
556
|
"""Execute a shell command in the Docker container.
|
|
510
557
|
|
|
@@ -150,6 +150,68 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
150
150
|
|
|
151
151
|
return await self._sbx.files.exists(path)
|
|
152
152
|
|
|
153
|
+
async def is_directory(self, path: str) -> bool:
|
|
154
|
+
"""Check if a path is a directory in the E2B sandbox.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Path within the sandbox.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if the path exists and is a directory, False otherwise.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
RuntimeError: If environment not started.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
if self._sbx is None:
|
|
167
|
+
raise RuntimeError("ExecutionEnvironment not started.")
|
|
168
|
+
|
|
169
|
+
if not await self._sbx.files.exists(path):
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
info = await self._sbx.files.get_info(path)
|
|
173
|
+
return info.type == FileType.DIR
|
|
174
|
+
|
|
175
|
+
async def list_files(self, path: str) -> list[str]:
|
|
176
|
+
"""List all files recursively in a directory within the E2B sandbox.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: Directory path within the sandbox.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of file paths (relative to the given path) for all files in the directory.
|
|
183
|
+
Returns an empty list if the path is a file or doesn't exist.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
RuntimeError: If environment not started.
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
if self._sbx is None:
|
|
190
|
+
raise RuntimeError("ExecutionEnvironment not started.")
|
|
191
|
+
|
|
192
|
+
if not await self._sbx.files.exists(path):
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
info = await self._sbx.files.get_info(path)
|
|
196
|
+
if info.type != FileType.DIR:
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
# Use find command to list all files recursively
|
|
200
|
+
result = await self.run_command(f"find {path} -type f")
|
|
201
|
+
if result.exit_code != 0:
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
files = []
|
|
205
|
+
for line in result.stdout.strip().split("\n"):
|
|
206
|
+
if line:
|
|
207
|
+
# Convert absolute path to relative path
|
|
208
|
+
rel_path = line.removeprefix(f"{path}/").removeprefix(path)
|
|
209
|
+
if rel_path.startswith("/"):
|
|
210
|
+
rel_path = rel_path[1:]
|
|
211
|
+
if rel_path:
|
|
212
|
+
files.append(rel_path)
|
|
213
|
+
return files
|
|
214
|
+
|
|
153
215
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
154
216
|
"""Execute command in E2B execution environment, returning raw CommandResult."""
|
|
155
217
|
if self._sbx is None:
|
|
@@ -222,6 +222,49 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
222
222
|
resolved = self._resolve_and_validate_path(path)
|
|
223
223
|
return resolved.exists() and resolved.is_file()
|
|
224
224
|
|
|
225
|
+
async def is_directory(self, path: str) -> bool:
|
|
226
|
+
"""Check if a path is a directory in the temp directory.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
path: Path (relative or absolute within the temp dir).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if the path exists and is a directory, False otherwise.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
RuntimeError: If environment not started.
|
|
236
|
+
ValueError: If path is outside temp directory.
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
resolved = self._resolve_and_validate_path(path)
|
|
240
|
+
return resolved.exists() and resolved.is_dir()
|
|
241
|
+
|
|
242
|
+
async def list_files(self, path: str) -> list[str]:
|
|
243
|
+
"""List all files recursively in a directory within the temp directory.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
path: Directory path (relative or absolute within the temp dir).
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of file paths (relative to the given path) for all files in the directory.
|
|
250
|
+
Returns an empty list if the path is a file or doesn't exist.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
RuntimeError: If environment not started.
|
|
254
|
+
ValueError: If path is outside temp directory.
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
resolved = self._resolve_and_validate_path(path)
|
|
258
|
+
if not resolved.exists() or not resolved.is_dir():
|
|
259
|
+
return []
|
|
260
|
+
|
|
261
|
+
files = []
|
|
262
|
+
for file_path in resolved.rglob("*"):
|
|
263
|
+
if file_path.is_file():
|
|
264
|
+
rel_path = file_path.relative_to(resolved)
|
|
265
|
+
files.append(str(rel_path))
|
|
266
|
+
return files
|
|
267
|
+
|
|
225
268
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
226
269
|
"""Execute command in the temp directory.
|
|
227
270
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: stirrup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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.
|
|
@@ -77,20 +77,20 @@ Description-Content-Type: text/markdown
|
|
|
77
77
|
<br>
|
|
78
78
|
</div>
|
|
79
79
|
|
|
80
|
-
|
|
81
80
|
<p align="center">
|
|
82
81
|
<a href="https://pypi.python.org/pypi/stirrup"><img src="https://img.shields.io/pypi/v/stirrup" alt="PyPI version" /></a> <!--
|
|
83
82
|
--><a href="https://github.com/ArtificialAnalysis/Stirrup/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ArtificialAnalysis/Stirrup" alt="License" /></a> <!--
|
|
84
83
|
--><a href="https://stirrup.artificialanalysis.ai"><img src="https://img.shields.io/badge/MkDocs-4F46E5?logo=materialformkdocs&logoColor=fff" alt="MkDocs" /></a>
|
|
85
84
|
</p>
|
|
86
85
|
|
|
87
|
-
|
|
88
86
|
Stirrup is a lightweight framework, or starting point template, for building agents. It differs from other agent frameworks by:
|
|
89
87
|
|
|
90
88
|
- **Working with the model, not against it:** Stirrup gets out of the way and lets the model choose its own approach to completing tasks (similar to Claude Code). Many frameworks impose rigid workflows that can degrade results.
|
|
91
89
|
- **Best practices and tools built-in:** We analyzed the leading agents (Claude Code, Codex, and others) to understand and incorporate best practices relating to topics like context management and foundational tools (e.g., code execution).
|
|
92
90
|
- **Fully customizable:** Use Stirrup as a package or as a starting template to build your own fully customized agents.
|
|
93
91
|
|
|
92
|
+
> **Note:** This is the Python implementation, [StirrupJS](https://github.com/ArtificialAnalysis/StirrupJS) is the Typescript implementation.
|
|
93
|
+
|
|
94
94
|
## Features
|
|
95
95
|
|
|
96
96
|
- 🧪 **Code execution:** Run code locally, in Docker, or in an E2B sandbox
|
|
@@ -21,10 +21,10 @@ stirrup/tools/__init__.py,sha256=TJ3AqPhAkfVPboyMGHscIfq-oUc8brPPVnKtUrf9ICU,287
|
|
|
21
21
|
stirrup/tools/browser_use.py,sha256=m7y5F9mO_Qhitmpu6NFh9-wYAxqgXb4_T3VuZdgGInM,21111
|
|
22
22
|
stirrup/tools/calculator.py,sha256=Cckt-8TtltxtuyY_Hh0wOr8efUzBZzg7rG4dBpvpuRM,1293
|
|
23
23
|
stirrup/tools/code_backends/__init__.py,sha256=O3Rs76r0YcQ27voTrx_zuhIEFawK3b1TQdKi70MORG8,987
|
|
24
|
-
stirrup/tools/code_backends/base.py,sha256=
|
|
25
|
-
stirrup/tools/code_backends/docker.py,sha256=
|
|
26
|
-
stirrup/tools/code_backends/e2b.py,sha256=
|
|
27
|
-
stirrup/tools/code_backends/local.py,sha256=
|
|
24
|
+
stirrup/tools/code_backends/base.py,sha256=5kWr_sIcg5gchJUue-8olrj-cgD81gdgHZgGexGVYfs,20310
|
|
25
|
+
stirrup/tools/code_backends/docker.py,sha256=M6UeypwMqEb_em_oul_53UyWG8zWzkAb7NzgEt27SGc,32510
|
|
26
|
+
stirrup/tools/code_backends/e2b.py,sha256=Jgv1p1kWlTDHCQmJsX2tFqG2lSKtdRsZ_553juo4HGE,17358
|
|
27
|
+
stirrup/tools/code_backends/local.py,sha256=2GP0jo0-rDYr-KDTDCESA0_HIBWi8iuebUWdtitrogs,21592
|
|
28
28
|
stirrup/tools/finish.py,sha256=xprl0z1N_e-8VddLyEIA9pLeMXjh5A9d4LpkKxhOh5A,1803
|
|
29
29
|
stirrup/tools/mcp.py,sha256=4wWYae95y8Bs7e36hHwnxRfVVj0PABrsRStw492lLaw,18749
|
|
30
30
|
stirrup/tools/user_input.py,sha256=XwK14FvRQly3vGwgNzPVGoSXfbco0WWaSVpTDyjV09E,4508
|
|
@@ -33,6 +33,6 @@ stirrup/tools/web.py,sha256=B-zp5i1WhjOOMAlYtnvU3N5hNYnJYm8qVXAtNx_ZaRw,12292
|
|
|
33
33
|
stirrup/utils/__init__.py,sha256=4kcuExrphSXqgxRgu1q8_Z6Rrb9aAZpIo4Xq4S9Twuk,230
|
|
34
34
|
stirrup/utils/logging.py,sha256=vZ7P7MotZnjbTyhMZ0p1YVTGhKQBAXBHOGbku3gEzuk,35166
|
|
35
35
|
stirrup/utils/text.py,sha256=3lGlcXFzQ-Mclsbu7wJciG3CcHvQ_Sk98tqOZxYLlGw,479
|
|
36
|
-
stirrup-0.1.
|
|
37
|
-
stirrup-0.1.
|
|
38
|
-
stirrup-0.1.
|
|
36
|
+
stirrup-0.1.5.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
37
|
+
stirrup-0.1.5.dist-info/METADATA,sha256=8QNBKGrsYwWO4eJVKUSdZVPkyjodn5L96MtE0Ns8rc4,13445
|
|
38
|
+
stirrup-0.1.5.dist-info/RECORD,,
|
|
File without changes
|