stirrup 0.1.3__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/clients/__init__.py +5 -0
- stirrup/clients/open_responses_client.py +434 -0
- stirrup/core/agent.py +18 -2
- stirrup/core/models.py +4 -2
- stirrup/tools/__init__.py +1 -0
- stirrup/tools/browser_use.py +591 -0
- stirrup/tools/code_backends/base.py +88 -12
- stirrup/tools/code_backends/docker.py +66 -0
- stirrup/tools/code_backends/e2b.py +80 -0
- stirrup/tools/code_backends/local.py +60 -0
- stirrup/tools/finish.py +27 -1
- stirrup/utils/logging.py +8 -7
- {stirrup-0.1.3.dist-info → stirrup-0.1.5.dist-info}/METADATA +18 -15
- {stirrup-0.1.3.dist-info → stirrup-0.1.5.dist-info}/RECORD +15 -13
- {stirrup-0.1.3.dist-info → stirrup-0.1.5.dist-info}/WHEEL +2 -2
|
@@ -228,6 +228,56 @@ class CodeExecToolProvider(ToolProvider, ABC):
|
|
|
228
228
|
"""
|
|
229
229
|
...
|
|
230
230
|
|
|
231
|
+
@abstractmethod
|
|
232
|
+
async def file_exists(self, path: str) -> bool:
|
|
233
|
+
"""Check if a file exists in this execution environment.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
path: File path within this execution environment (relative or absolute
|
|
237
|
+
within the env's working directory).
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if the file exists, False otherwise.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
RuntimeError: If execution environment not started.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
...
|
|
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
|
+
|
|
231
281
|
async def save_output_files(
|
|
232
282
|
self,
|
|
233
283
|
paths: list[str],
|
|
@@ -317,18 +367,44 @@ class CodeExecToolProvider(ToolProvider, ABC):
|
|
|
317
367
|
try:
|
|
318
368
|
if source_env:
|
|
319
369
|
# Cross-environment transfer: read from source_env
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
path_str
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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)))
|
|
332
408
|
else:
|
|
333
409
|
# Local filesystem upload - must be handled by subclass
|
|
334
410
|
# This is a fallback that reads from local fs and writes to env
|
|
@@ -486,6 +486,72 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
486
486
|
host_path.parent.mkdir(parents=True, exist_ok=True)
|
|
487
487
|
host_path.write_bytes(content)
|
|
488
488
|
|
|
489
|
+
async def file_exists(self, path: str) -> bool:
|
|
490
|
+
"""Check if a file exists in the container.
|
|
491
|
+
|
|
492
|
+
Since files are volume-mounted, checks directly on the host temp directory.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
path: File path (relative or absolute container path).
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
True if the file exists, False otherwise.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
RuntimeError: If environment not started.
|
|
502
|
+
ValueError: If path is outside mounted directory.
|
|
503
|
+
|
|
504
|
+
"""
|
|
505
|
+
host_path = self._container_path_to_host(path)
|
|
506
|
+
return host_path.exists() and host_path.is_file()
|
|
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
|
+
|
|
489
555
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
490
556
|
"""Execute a shell command in the Docker container.
|
|
491
557
|
|
|
@@ -132,6 +132,86 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
132
132
|
|
|
133
133
|
await self._sbx.files.write(path, content, request_timeout=self._request_timeout)
|
|
134
134
|
|
|
135
|
+
async def file_exists(self, path: str) -> bool:
|
|
136
|
+
"""Check if a file exists in the E2B sandbox.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
path: File path within the sandbox.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if the file exists, False otherwise.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
RuntimeError: If environment not started.
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
if self._sbx is None:
|
|
149
|
+
raise RuntimeError("ExecutionEnvironment not started.")
|
|
150
|
+
|
|
151
|
+
return await self._sbx.files.exists(path)
|
|
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
|
+
|
|
135
215
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
136
216
|
"""Execute command in E2B execution environment, returning raw CommandResult."""
|
|
137
217
|
if self._sbx is None:
|
|
@@ -205,6 +205,66 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
205
205
|
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
206
206
|
resolved.write_bytes(content)
|
|
207
207
|
|
|
208
|
+
async def file_exists(self, path: str) -> bool:
|
|
209
|
+
"""Check if a file exists in the temp directory.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
path: File path (relative or absolute within the temp dir).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if the file exists, False otherwise.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
RuntimeError: If environment not started.
|
|
219
|
+
ValueError: If path is outside temp directory.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
resolved = self._resolve_and_validate_path(path)
|
|
223
|
+
return resolved.exists() and resolved.is_file()
|
|
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
|
+
|
|
208
268
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
209
269
|
"""Execute command in the temp directory.
|
|
210
270
|
|
stirrup/tools/finish.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""Simple finish tool with file existence validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
1
5
|
from typing import Annotated
|
|
2
6
|
|
|
3
7
|
from pydantic import BaseModel, Field
|
|
@@ -15,9 +19,31 @@ class FinishParams(BaseModel):
|
|
|
15
19
|
]
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
async def _validating_finish_executor(params: FinishParams) -> ToolResult[ToolUseCountMetadata]:
|
|
23
|
+
"""Validates all reported files exist before completing."""
|
|
24
|
+
from stirrup.core.agent import _SESSION_STATE
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
state = _SESSION_STATE.get(None)
|
|
28
|
+
exec_env = state.exec_env if state else None
|
|
29
|
+
except LookupError:
|
|
30
|
+
exec_env = None
|
|
31
|
+
|
|
32
|
+
if exec_env and params.paths:
|
|
33
|
+
missing = [p for p in params.paths if not await exec_env.file_exists(p)]
|
|
34
|
+
if missing:
|
|
35
|
+
return ToolResult(
|
|
36
|
+
content=f"ERROR: Files do not exist: {missing}. Verify paths and ensure files were saved.",
|
|
37
|
+
metadata=ToolUseCountMetadata(),
|
|
38
|
+
success=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return ToolResult(content=params.reason, metadata=ToolUseCountMetadata(), success=True)
|
|
42
|
+
|
|
43
|
+
|
|
18
44
|
SIMPLE_FINISH_TOOL: Tool[FinishParams, ToolUseCountMetadata] = Tool[FinishParams, ToolUseCountMetadata](
|
|
19
45
|
name=FINISH_TOOL_NAME,
|
|
20
46
|
description="Signal task completion with a reason. Use when the task is finished or cannot proceed further. Note that you will need a separate turn to finish.",
|
|
21
47
|
parameters=FinishParams,
|
|
22
|
-
executor=
|
|
48
|
+
executor=_validating_finish_executor,
|
|
23
49
|
)
|
stirrup/utils/logging.py
CHANGED
|
@@ -23,6 +23,7 @@ from rich.text import Text
|
|
|
23
23
|
from rich.tree import Tree
|
|
24
24
|
|
|
25
25
|
from stirrup.core.models import AssistantMessage, ToolMessage, UserMessage, _aggregate_list, aggregate_metadata
|
|
26
|
+
from stirrup.utils.text import truncate_msg
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
28
29
|
"AgentLogger",
|
|
@@ -769,11 +770,12 @@ class AgentLogger(AgentLoggerBase):
|
|
|
769
770
|
content.append("\n\n")
|
|
770
771
|
content.append("Tool Calls:\n", style="bold magenta")
|
|
771
772
|
for tc in assistant_message.tool_calls:
|
|
772
|
-
args_parsed = json.loads(tc.arguments)
|
|
773
|
-
args_formatted = json.dumps(args_parsed, indent=2, ensure_ascii=False)
|
|
774
|
-
args_preview = args_formatted[:1000] + "..." if len(args_formatted) > 1000 else args_formatted
|
|
775
773
|
content.append(f" 🔧 {tc.name}", style="magenta")
|
|
776
|
-
|
|
774
|
+
if tc.arguments and tc.arguments.strip():
|
|
775
|
+
args_parsed = json.loads(tc.arguments)
|
|
776
|
+
args_formatted = json.dumps(args_parsed, indent=2, ensure_ascii=False)
|
|
777
|
+
args_preview = args_formatted[:1000] + "..." if len(args_formatted) > 1000 else args_formatted
|
|
778
|
+
content.append(args_preview, style="dim")
|
|
777
779
|
|
|
778
780
|
# Create and print panel with agent name in title
|
|
779
781
|
title = f"[bold]AssistantMessage[/bold] │ {self.name} │ Turn {turn}/{max_turns}"
|
|
@@ -875,9 +877,8 @@ class AgentLogger(AgentLoggerBase):
|
|
|
875
877
|
# Unescape HTML entities (e.g., < -> <, > -> >, & -> &)
|
|
876
878
|
result_text = html.unescape(result_text)
|
|
877
879
|
|
|
878
|
-
# Truncate long results
|
|
879
|
-
|
|
880
|
-
result_text = result_text[:1000] + "..."
|
|
880
|
+
# Truncate long results (keeps start and end, removes middle)
|
|
881
|
+
result_text = truncate_msg(result_text, 1000)
|
|
881
882
|
|
|
882
883
|
# Format as XML with syntax highlighting
|
|
883
884
|
content = Syntax(result_text, "xml", theme="monokai", word_wrap=True)
|
|
@@ -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.
|
|
@@ -47,17 +47,19 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
47
47
|
Requires-Dist: rich>=13.0.0
|
|
48
48
|
Requires-Dist: tenacity>=5.0.0
|
|
49
49
|
Requires-Dist: trafilatura>=1.9.0
|
|
50
|
-
Requires-Dist: stirrup[litellm,e2b,docker,mcp] ; extra == 'all'
|
|
50
|
+
Requires-Dist: stirrup[litellm,e2b,docker,mcp,browser] ; extra == 'all'
|
|
51
|
+
Requires-Dist: browser-use>=0.11.3 ; extra == 'browser'
|
|
51
52
|
Requires-Dist: docker>=7.0.0 ; extra == 'docker'
|
|
52
53
|
Requires-Dist: python-dotenv>=1.0.0 ; extra == 'docker'
|
|
53
54
|
Requires-Dist: e2b-code-interpreter>=2.3.0 ; extra == 'e2b'
|
|
54
55
|
Requires-Dist: litellm>=1.79.3 ; extra == 'litellm'
|
|
55
56
|
Requires-Dist: mcp>=1.9.0 ; extra == 'mcp'
|
|
56
57
|
Requires-Python: >=3.12
|
|
57
|
-
Project-URL: Documentation, https://stirrup.artificialanalysis.ai
|
|
58
58
|
Project-URL: Homepage, https://github.com/ArtificialAnalysis/Stirrup
|
|
59
|
+
Project-URL: Documentation, https://stirrup.artificialanalysis.ai
|
|
59
60
|
Project-URL: Repository, https://github.com/ArtificialAnalysis/Stirrup
|
|
60
61
|
Provides-Extra: all
|
|
62
|
+
Provides-Extra: browser
|
|
61
63
|
Provides-Extra: docker
|
|
62
64
|
Provides-Extra: e2b
|
|
63
65
|
Provides-Extra: litellm
|
|
@@ -75,32 +77,32 @@ Description-Content-Type: text/markdown
|
|
|
75
77
|
<br>
|
|
76
78
|
</div>
|
|
77
79
|
|
|
78
|
-
|
|
79
80
|
<p align="center">
|
|
80
81
|
<a href="https://pypi.python.org/pypi/stirrup"><img src="https://img.shields.io/pypi/v/stirrup" alt="PyPI version" /></a> <!--
|
|
81
82
|
--><a href="https://github.com/ArtificialAnalysis/Stirrup/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ArtificialAnalysis/Stirrup" alt="License" /></a> <!--
|
|
82
83
|
--><a href="https://stirrup.artificialanalysis.ai"><img src="https://img.shields.io/badge/MkDocs-4F46E5?logo=materialformkdocs&logoColor=fff" alt="MkDocs" /></a>
|
|
83
84
|
</p>
|
|
84
85
|
|
|
85
|
-
|
|
86
86
|
Stirrup is a lightweight framework, or starting point template, for building agents. It differs from other agent frameworks by:
|
|
87
87
|
|
|
88
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.
|
|
89
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).
|
|
90
90
|
- **Fully customizable:** Use Stirrup as a package or as a starting template to build your own fully customized agents.
|
|
91
91
|
|
|
92
|
+
> **Note:** This is the Python implementation, [StirrupJS](https://github.com/ArtificialAnalysis/StirrupJS) is the Typescript implementation.
|
|
93
|
+
|
|
92
94
|
## Features
|
|
93
95
|
|
|
94
|
-
- **
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
- **
|
|
100
|
-
- **
|
|
101
|
-
- **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
102
|
-
- **Flexible provider support:** Pre-built support for OpenAI-compatible APIs
|
|
103
|
-
- **Multimodal support:** Process images, video, and audio with automatic format conversion
|
|
96
|
+
- 🧪 **Code execution:** Run code locally, in Docker, or in an E2B sandbox
|
|
97
|
+
- 🔎 **Online search / web browsing:** Search and fetch web pages
|
|
98
|
+
- 🔌 **MCP client support:** Connect to MCP servers and use their tools/resources
|
|
99
|
+
- 📄 **Document input and output:** Import files into context and produce file outputs
|
|
100
|
+
- 🧩 **Skills system:** Extend agents with modular, domain-specific instruction packages
|
|
101
|
+
- 🛠️ **Flexible tool execution:** A generic `Tool` interface allows easy tool definition
|
|
102
|
+
- 👤 **Human-in-the-loop:** Includes a built-in user input tool that enables human feedback or clarification during agent execution
|
|
103
|
+
- 🧠 **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
104
|
+
- 🔁 **Flexible provider support:** Pre-built support for OpenAI-compatible APIs, LiteLLM, or bring your own client
|
|
105
|
+
- 🖼️ **Multimodal support:** Process images, video, and audio with automatic format conversion
|
|
104
106
|
|
|
105
107
|
## Installation
|
|
106
108
|
|
|
@@ -116,6 +118,7 @@ pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
|
116
118
|
pip install 'stirrup[docker]' # or: uv add 'stirrup[docker]'
|
|
117
119
|
pip install 'stirrup[e2b]' # or: uv add 'stirrup[e2b]'
|
|
118
120
|
pip install 'stirrup[mcp]' # or: uv add 'stirrup[mcp]'
|
|
121
|
+
pip install 'stirrup[browser]' # or: uv add 'stirrup[browser]'
|
|
119
122
|
```
|
|
120
123
|
|
|
121
124
|
## Quick Start
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
stirrup/__init__.py,sha256=4p5Rw7f_wdxVu1FJTgJROe0aTlnc8tOsainBEzDRGEY,1905
|
|
2
|
-
stirrup/clients/__init__.py,sha256=
|
|
2
|
+
stirrup/clients/__init__.py,sha256=vHtR1rqK7w9LMWCBJXJde_R1pwdbuj9f6AbAJ4ib3xk,622
|
|
3
3
|
stirrup/clients/chat_completions_client.py,sha256=P0VOGFQcfLIVf7zCGYbopQDjNVEJlpeLBeGvB87sQQg,7390
|
|
4
4
|
stirrup/clients/litellm_client.py,sha256=2ZrZKKAEV2dEFs6ze4qBl3it5VwiGH8s7wHVuHdw-uY,5507
|
|
5
|
+
stirrup/clients/open_responses_client.py,sha256=AEM2z_LZhT4hW7VUSFb4mqzV5q_mavnLCwiBTUmt340,15303
|
|
5
6
|
stirrup/clients/utils.py,sha256=Z_8KiENDZVD9fChUm7PA-RLhvoChswHVQsjrHXlIfkg,5684
|
|
6
7
|
stirrup/constants.py,sha256=WpVPm2jRN2AqYMyoMYeJimiggoquP7M3IrcHNpduFF4,644
|
|
7
8
|
stirrup/core/__init__.py,sha256=ReBVl7B9h_FNkZ77vCx2xlfuK1JuQ0yTSXrEgc4tONU,39
|
|
8
|
-
stirrup/core/agent.py,sha256=
|
|
9
|
+
stirrup/core/agent.py,sha256=I-SDjXxUOYSVxowy7SHXP_bjEEZD2JIouNnS7VarmxI,58120
|
|
9
10
|
stirrup/core/cache.py,sha256=lAbbBJzgYInewoBBPMzNooroU2Q7JbF21riggfdIDa8,16697
|
|
10
11
|
stirrup/core/exceptions.py,sha256=CzLVAi7Ns-t9BWSkqQUCB7ypVHAesV2s4a09-i0NXyQ,213
|
|
11
|
-
stirrup/core/models.py,sha256=
|
|
12
|
+
stirrup/core/models.py,sha256=hAwquChulASzwf0yirmsWkfgBEnLTxX9pTAkJ7CwVvM,22591
|
|
12
13
|
stirrup/prompts/__init__.py,sha256=e4bpTktBaFPuO_bIW5DelGNWtT6_NIUqnD2lRv8n89I,796
|
|
13
14
|
stirrup/prompts/base_system_prompt.txt,sha256=D_UlDWEnG2yaPCMFrE7IqMHI8VCzi4BZ-GnuL3qs5q4,288
|
|
14
15
|
stirrup/prompts/message_summarizer.txt,sha256=uQoTxreMuC42rTGSZmoH1Dnj06WrEQb0gLkDvVMhosQ,1173
|
|
@@ -16,21 +17,22 @@ stirrup/prompts/message_summarizer_bridge.txt,sha256=sWbfnHtI6RWemBIyQsnqHMGpnU-
|
|
|
16
17
|
stirrup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
18
|
stirrup/skills/__init__.py,sha256=BEcmdSskfBzx_QK4eKXECucndIKRjHXzzwwwsaez8k4,700
|
|
18
19
|
stirrup/skills/skills.py,sha256=qhA3HI55kaRqLyvn_56Cs71833Xacg-8qP7muHrwruE,4282
|
|
19
|
-
stirrup/tools/__init__.py,sha256=
|
|
20
|
+
stirrup/tools/__init__.py,sha256=TJ3AqPhAkfVPboyMGHscIfq-oUc8brPPVnKtUrf9ICU,2879
|
|
21
|
+
stirrup/tools/browser_use.py,sha256=m7y5F9mO_Qhitmpu6NFh9-wYAxqgXb4_T3VuZdgGInM,21111
|
|
20
22
|
stirrup/tools/calculator.py,sha256=Cckt-8TtltxtuyY_Hh0wOr8efUzBZzg7rG4dBpvpuRM,1293
|
|
21
23
|
stirrup/tools/code_backends/__init__.py,sha256=O3Rs76r0YcQ27voTrx_zuhIEFawK3b1TQdKi70MORG8,987
|
|
22
|
-
stirrup/tools/code_backends/base.py,sha256=
|
|
23
|
-
stirrup/tools/code_backends/docker.py,sha256=
|
|
24
|
-
stirrup/tools/code_backends/e2b.py,sha256=
|
|
25
|
-
stirrup/tools/code_backends/local.py,sha256=
|
|
26
|
-
stirrup/tools/finish.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
|
+
stirrup/tools/finish.py,sha256=xprl0z1N_e-8VddLyEIA9pLeMXjh5A9d4LpkKxhOh5A,1803
|
|
27
29
|
stirrup/tools/mcp.py,sha256=4wWYae95y8Bs7e36hHwnxRfVVj0PABrsRStw492lLaw,18749
|
|
28
30
|
stirrup/tools/user_input.py,sha256=XwK14FvRQly3vGwgNzPVGoSXfbco0WWaSVpTDyjV09E,4508
|
|
29
31
|
stirrup/tools/view_image.py,sha256=zazCpZMtLOD6lplLPYGNQ8JeYfc0oUDJoUUyVAp3AMU,3126
|
|
30
32
|
stirrup/tools/web.py,sha256=B-zp5i1WhjOOMAlYtnvU3N5hNYnJYm8qVXAtNx_ZaRw,12292
|
|
31
33
|
stirrup/utils/__init__.py,sha256=4kcuExrphSXqgxRgu1q8_Z6Rrb9aAZpIo4Xq4S9Twuk,230
|
|
32
|
-
stirrup/utils/logging.py,sha256=
|
|
34
|
+
stirrup/utils/logging.py,sha256=vZ7P7MotZnjbTyhMZ0p1YVTGhKQBAXBHOGbku3gEzuk,35166
|
|
33
35
|
stirrup/utils/text.py,sha256=3lGlcXFzQ-Mclsbu7wJciG3CcHvQ_Sk98tqOZxYLlGw,479
|
|
34
|
-
stirrup-0.1.
|
|
35
|
-
stirrup-0.1.
|
|
36
|
-
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,,
|