stirrup 0.1.4__tar.gz → 0.1.5__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.
Files changed (39) hide show
  1. {stirrup-0.1.4 → stirrup-0.1.5}/PKG-INFO +3 -3
  2. {stirrup-0.1.4 → stirrup-0.1.5}/README.md +2 -2
  3. {stirrup-0.1.4 → stirrup-0.1.5}/pyproject.toml +1 -1
  4. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/code_backends/base.py +71 -12
  5. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/code_backends/docker.py +47 -0
  6. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/code_backends/e2b.py +62 -0
  7. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/code_backends/local.py +43 -0
  8. {stirrup-0.1.4 → stirrup-0.1.5}/LICENSE +0 -0
  9. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/__init__.py +0 -0
  10. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/clients/__init__.py +0 -0
  11. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/clients/chat_completions_client.py +0 -0
  12. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/clients/litellm_client.py +0 -0
  13. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/clients/open_responses_client.py +0 -0
  14. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/clients/utils.py +0 -0
  15. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/constants.py +0 -0
  16. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/core/__init__.py +0 -0
  17. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/core/agent.py +0 -0
  18. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/core/cache.py +0 -0
  19. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/core/exceptions.py +0 -0
  20. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/core/models.py +0 -0
  21. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/prompts/__init__.py +0 -0
  22. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/prompts/base_system_prompt.txt +0 -0
  23. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/prompts/message_summarizer.txt +0 -0
  24. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/prompts/message_summarizer_bridge.txt +0 -0
  25. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/py.typed +0 -0
  26. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/skills/__init__.py +0 -0
  27. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/skills/skills.py +0 -0
  28. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/__init__.py +0 -0
  29. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/browser_use.py +0 -0
  30. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/calculator.py +0 -0
  31. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/code_backends/__init__.py +0 -0
  32. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/finish.py +0 -0
  33. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/mcp.py +0 -0
  34. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/user_input.py +0 -0
  35. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/view_image.py +0 -0
  36. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/tools/web.py +0 -0
  37. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/utils/__init__.py +0 -0
  38. {stirrup-0.1.4 → stirrup-0.1.5}/src/stirrup/utils/logging.py +0 -0
  39. {stirrup-0.1.4 → stirrup-0.1.5}/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.4
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>&nbsp;<!--
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>&nbsp;<!--
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
@@ -9,20 +9,20 @@
9
9
  <br>
10
10
  </div>
11
11
 
12
-
13
12
  <p align="center">
14
13
  <a href="https://pypi.python.org/pypi/stirrup"><img src="https://img.shields.io/pypi/v/stirrup" alt="PyPI version" /></a>&nbsp;<!--
15
14
  --><a href="https://github.com/ArtificialAnalysis/Stirrup/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ArtificialAnalysis/Stirrup" alt="License" /></a>&nbsp;<!--
16
15
  --><a href="https://stirrup.artificialanalysis.ai"><img src="https://img.shields.io/badge/MkDocs-4F46E5?logo=materialformkdocs&logoColor=fff" alt="MkDocs" /></a>
17
16
  </p>
18
17
 
19
-
20
18
  Stirrup is a lightweight framework, or starting point template, for building agents. It differs from other agent frameworks by:
21
19
 
22
20
  - **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.
23
21
  - **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).
24
22
  - **Fully customizable:** Use Stirrup as a package or as a starting template to build your own fully customized agents.
25
23
 
24
+ > **Note:** This is the Python implementation, [StirrupJS](https://github.com/ArtificialAnalysis/StirrupJS) is the Typescript implementation.
25
+
26
26
  ## Features
27
27
 
28
28
  - 🧪 **Code execution:** Run code locally, in Docker, or in an E2B sandbox
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stirrup"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "The lightweight foundation for building agents"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -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
- content = await source_env.read_file_bytes(path_str)
338
- filename = Path(path_str).name
339
- dest_path = f"{dest_dir_str}/{filename}" if dest_dir_str else filename
340
- logger.debug(
341
- "UPLOAD CROSS-ENV: %s (%d bytes) from %s -> %s",
342
- path_str,
343
- len(content),
344
- type(source_env).__name__,
345
- dest_path,
346
- )
347
- await self.write_file_bytes(dest_path, content)
348
- result.uploaded.append(UploadedFile(Path(path_str), dest_path, len(content)))
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
 
File without changes
File without changes
File without changes