stirrup 0.1.0__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.
@@ -0,0 +1,7 @@
1
+ """Custom exceptions for agent framework."""
2
+
3
+ __all__ = ["ContextOverflowError"]
4
+
5
+
6
+ class ContextOverflowError(Exception):
7
+ """Raised when LLM context window is exceeded (max_tokens or length finish_reason)."""
stirrup/core/models.py ADDED
@@ -0,0 +1,599 @@
1
+ import mimetypes
2
+ import warnings
3
+ from abc import ABC, abstractmethod
4
+ from base64 import b64encode
5
+ from collections.abc import Awaitable, Callable
6
+ from datetime import date, datetime, time, timedelta
7
+ from decimal import Decimal
8
+ from io import BytesIO
9
+ from math import isinf, isnan, sqrt
10
+ from tempfile import NamedTemporaryFile
11
+ from types import TracebackType
12
+ from typing import Annotated, Any, ClassVar, Literal, Protocol, Self, overload, runtime_checkable
13
+
14
+ import filetype
15
+ from moviepy import AudioFileClip, VideoFileClip
16
+ from moviepy.video.fx import Resize
17
+ from PIL import Image
18
+ from pydantic import BaseModel, Field, model_validator
19
+
20
+ from stirrup.constants import RESOLUTION_1MP, RESOLUTION_480P
21
+
22
+ __all__ = [
23
+ "Addable",
24
+ "AssistantMessage",
25
+ "AudioContentBlock",
26
+ "BinaryContentBlock",
27
+ "ChatMessage",
28
+ "Content",
29
+ "ContentBlock",
30
+ "ImageContentBlock",
31
+ "LLMClient",
32
+ "SubAgentMetadata",
33
+ "SystemMessage",
34
+ "TokenUsage",
35
+ "Tool",
36
+ "ToolCall",
37
+ "ToolMessage",
38
+ "ToolProvider",
39
+ "ToolResult",
40
+ "ToolUseCountMetadata",
41
+ "UserMessage",
42
+ "VideoContentBlock",
43
+ "aggregate_metadata",
44
+ ]
45
+
46
+
47
+ def downscale_image(w: int, h: int, max_pixels: int | None = 1_000_000) -> tuple[int, int]:
48
+ """Downscale image dimensions to fit within max pixel count while maintaining aspect ratio.
49
+
50
+ Returns even dimensions with minimum 2x2 size.
51
+ """
52
+ s = 1.0 if max_pixels is None or w * h <= max_pixels else sqrt(max_pixels / (w * h))
53
+ nw, nh = int(w * s) // 2 * 2, int(h * s) // 2 * 2
54
+ return max(nw, 2), max(nh, 2)
55
+
56
+
57
+ # Content
58
+ class BinaryContentBlock(BaseModel, ABC):
59
+ """Base class for binary content (images, video, audio) with MIME type validation."""
60
+
61
+ data: bytes
62
+ allowed_mime_types: ClassVar[set[str]]
63
+
64
+ @property
65
+ def mime_type(self) -> str:
66
+ """MIME type for data based on headers."""
67
+ match: filetype.Type = filetype.guess(self.data)
68
+ if match is None:
69
+ raise ValueError(f"Unsupported file type {self.data!r}")
70
+ return match.mime
71
+
72
+ @property
73
+ def extension(self) -> str:
74
+ """File extension for the content (e.g., 'png', 'mp4', 'mp3') without leading dot."""
75
+ _extension = mimetypes.guess_extension(self.mime_type)
76
+ if _extension is None:
77
+ raise ValueError(f"Unsupported mime_type {self.mime_type!r}")
78
+ return _extension[1:]
79
+
80
+ @model_validator(mode="after")
81
+ def _check_mime(self) -> Self:
82
+ """Validate MIME type against allowed list and verify content is readable."""
83
+ if self.allowed_mime_types and self.mime_type not in self.allowed_mime_types:
84
+ raise ValueError("Unsupported mime_type {self.mime_type!r}; allowed: {allowed}")
85
+ self._probe() # light corruption check; no heavy work
86
+ return self
87
+
88
+ @abstractmethod
89
+ def _probe(self) -> None:
90
+ """Verify content can be opened and read; subclasses implement format-specific checks."""
91
+
92
+
93
+ class ImageContentBlock(BinaryContentBlock):
94
+ """Image content supporting PNG, JPEG, WebP, PSD formats with automatic downscaling."""
95
+
96
+ kind: Literal["image_content_block"] = "image_content_block"
97
+ allowed_mime_types: ClassVar[set[str]] = {
98
+ "image/jpeg", # JPEG
99
+ "image/png", # PNG
100
+ "image/gif", # GIF
101
+ "image/bmp", # BMP
102
+ "image/tiff", # TIFF
103
+ "image/vnd.adobe.photoshop", # PSD
104
+ }
105
+
106
+ def _probe(self) -> None:
107
+ """Verify image data is valid by attempting to open and verify it with PIL."""
108
+ with Image.open(BytesIO(self.data)) as im:
109
+ im.verify()
110
+
111
+ def to_base64_url(self, max_pixels: int | None = RESOLUTION_1MP) -> str:
112
+ """Convert image to base64 data URL, optionally resizing to max pixel count."""
113
+ img: Image.Image = Image.open(BytesIO(self.data))
114
+ if max_pixels is not None and img.width * img.height > max_pixels:
115
+ tw, th = downscale_image(img.width, img.height, max_pixels)
116
+ img.thumbnail((tw, th), Image.Resampling.LANCZOS)
117
+ if img.mode != "RGB":
118
+ img = img.convert("RGB")
119
+ buf = BytesIO()
120
+ img.save(buf, format="PNG")
121
+ return f"data:image/png;base64,{b64encode(buf.getvalue()).decode()}"
122
+
123
+
124
+ class VideoContentBlock(BinaryContentBlock):
125
+ """MP4 video content with automatic transcoding and resolution downscaling."""
126
+
127
+ kind: Literal["video_content_block"] = "video_content_block"
128
+ allowed_mime_types: ClassVar[set[str]] = {
129
+ "video/x-msvideo", # AVI
130
+ "video/mp4", # MP4
131
+ "video/quicktime", # MOV
132
+ "video/x-matroska", # MKV
133
+ "video/x-ms-wmv", # WMV
134
+ "video/x-flv", # FLV
135
+ "video/mpeg", # MPEG
136
+ "video/webm", # WebM
137
+ "video/gif", # GIF (animated)
138
+ }
139
+
140
+ def _probe(self) -> None:
141
+ """Verify video data is valid by attempting to open it as a VideoFileClip."""
142
+ with warnings.catch_warnings():
143
+ warnings.filterwarnings("ignore", category=UserWarning, module="moviepy.*")
144
+ with NamedTemporaryFile(suffix=".bin") as f:
145
+ f.write(self.data)
146
+ f.flush()
147
+ clip = VideoFileClip(f.name)
148
+ clip.close()
149
+
150
+ def to_base64_url(self, max_pixels: int | None = RESOLUTION_480P, fps: int | None = None) -> str:
151
+ """Transcode to MP4 and return base64 data URL."""
152
+ with warnings.catch_warnings():
153
+ warnings.filterwarnings("ignore", category=UserWarning, module="moviepy.*")
154
+ with NamedTemporaryFile(suffix=".mp4") as fin, NamedTemporaryFile(suffix=".mp4") as fout:
155
+ fin.write(self.data)
156
+ fin.flush()
157
+ clip = VideoFileClip(fin.name)
158
+ tw, th = downscale_image(int(clip.w), int(clip.h), max_pixels)
159
+ clip = clip.with_effects([Resize(new_size=(tw, th))])
160
+
161
+ clip.write_videofile(
162
+ fout.name,
163
+ codec="libx264",
164
+ fps=fps,
165
+ audio=clip.audio is not None,
166
+ audio_codec="aac",
167
+ preset="veryfast",
168
+ logger=None,
169
+ )
170
+ clip.close()
171
+ return f"data:video/mp4;base64,{b64encode(fout.read()).decode()}"
172
+
173
+
174
+ class AudioContentBlock(BinaryContentBlock):
175
+ """Audio content supporting MPEG, WAV, AAC, and other common audio formats."""
176
+
177
+ kind: Literal["audio_content_block"] = "audio_content_block"
178
+ allowed_mime_types: ClassVar[set[str]] = {
179
+ "audio/x-aac",
180
+ "audio/flac",
181
+ "audio/mp3",
182
+ "audio/m4a",
183
+ "audio/mpeg",
184
+ "audio/mpga",
185
+ "audio/mp4",
186
+ "audio/ogg",
187
+ "audio/pcm",
188
+ "audio/wav",
189
+ "audio/webm",
190
+ "audio/x-wav",
191
+ "audio/aac",
192
+ }
193
+
194
+ def _probe(self) -> None:
195
+ """Verify audio data is valid by attempting to open it as an AudioFileClip."""
196
+ with warnings.catch_warnings():
197
+ warnings.filterwarnings("ignore", category=UserWarning, module="moviepy.*")
198
+ with NamedTemporaryFile(suffix=".bin") as fin:
199
+ fin.write(self.data)
200
+ fin.flush()
201
+ clip = AudioFileClip(fin.name)
202
+ clip.close()
203
+
204
+ def to_base64_url(self, bitrate: str = "192k") -> str:
205
+ """Transcode to MP3 and return base64 data URL."""
206
+ with warnings.catch_warnings():
207
+ warnings.filterwarnings("ignore", category=UserWarning, module="moviepy.*")
208
+ with NamedTemporaryFile(suffix=".bin") as fin, NamedTemporaryFile(suffix=".mp3") as fout:
209
+ fin.write(self.data)
210
+ fin.flush()
211
+ clip = AudioFileClip(fin.name)
212
+ clip.write_audiofile(fout.name, codec="libmp3lame", bitrate=bitrate, logger=None)
213
+ clip.close()
214
+ return f"data:audio/mpeg;base64,{b64encode(fout.read()).decode()}"
215
+
216
+
217
+ type ContentBlock = ImageContentBlock | VideoContentBlock | AudioContentBlock | str
218
+ """Union of all content block types (image, video, audio, or text)."""
219
+
220
+ type Content = list[ContentBlock] | str
221
+ """Message content: either a plain string or list of mixed content blocks."""
222
+
223
+
224
+ # Metadata Protocol and Aggregation
225
+ @runtime_checkable
226
+ class Addable(Protocol):
227
+ """Protocol for types that support aggregation via __add__."""
228
+
229
+ def __add__(self, other: Self) -> Self: ...
230
+
231
+
232
+ def _aggregate_list[T: Addable](metadata_list: list[T]) -> T | None:
233
+ """Aggregate a list of metadata using __add__."""
234
+ if not metadata_list:
235
+ return None
236
+ aggregated = metadata_list[0]
237
+ for m in metadata_list[1:]:
238
+ aggregated = aggregated + m
239
+ return aggregated
240
+
241
+
242
+ def to_json_serializable(value: object) -> object:
243
+ # None and JSON primitives
244
+ if value is None or isinstance(value, str | int | bool):
245
+ return value
246
+
247
+ # Floats need special handling for nan/inf
248
+ if isinstance(value, float):
249
+ if isnan(value) or isinf(value):
250
+ raise ValueError(f"Cannot serialize {value} to JSON")
251
+ return value
252
+
253
+ # Pydantic models
254
+ if isinstance(value, BaseModel):
255
+ return value.model_dump(mode="json")
256
+
257
+ # Common non-serializable types
258
+ if isinstance(value, datetime | date | time):
259
+ return value.isoformat()
260
+
261
+ if isinstance(value, timedelta):
262
+ return value.total_seconds()
263
+
264
+ if isinstance(value, Decimal):
265
+ return float(value)
266
+
267
+ if isinstance(value, dict):
268
+ return {k: to_json_serializable(v) for k, v in value.items()}
269
+
270
+ if isinstance(value, list | tuple | set | frozenset):
271
+ return [to_json_serializable(v) for v in value]
272
+
273
+ # We have not implemented other cases (e.g. Bytes, Enum, etc.)
274
+ raise TypeError(f"Cannot serialize {type(value).__name__} to JSON: {value!r}")
275
+
276
+
277
+ @overload
278
+ def aggregate_metadata(
279
+ metadata_dict: dict[str, list[Any]], prefix: str = "", return_json_serializable: Literal[True] = True
280
+ ) -> object: ...
281
+
282
+
283
+ @overload
284
+ def aggregate_metadata(
285
+ metadata_dict: dict[str, list[Any]], prefix: str = "", return_json_serializable: Literal[False] = ...
286
+ ) -> dict: ...
287
+
288
+
289
+ def _collect_all_token_usage(result: dict) -> "TokenUsage":
290
+ """Recursively collect all token_usage from a flattened aggregate_metadata result.
291
+
292
+ Args:
293
+ result: The flattened dict from aggregate_metadata (before JSON serialization)
294
+
295
+ Returns:
296
+ Combined TokenUsage from all entries (direct and nested sub-agents)
297
+ """
298
+ total = TokenUsage()
299
+
300
+ for key, value in result.items():
301
+ if key == "token_usage" and isinstance(value, TokenUsage):
302
+ # Direct token_usage at this level
303
+ total = total + value
304
+ elif isinstance(value, dict):
305
+ # This could be a sub-agent's tool dict - check for nested token_usage
306
+ nested_token_usage = value.get("token_usage")
307
+ if isinstance(nested_token_usage, TokenUsage):
308
+ total = total + nested_token_usage
309
+
310
+ return total
311
+
312
+
313
+ def aggregate_metadata(
314
+ metadata_dict: dict[str, list[Any]], prefix: str = "", return_json_serializable: bool = True
315
+ ) -> dict | object:
316
+ """Aggregate metadata lists and flatten sub-agents into a single-level dict with hierarchical keys.
317
+
318
+ For entries with nested run_metadata (e.g., SubAgentMetadata), flattens sub-agents using dot notation.
319
+ Each sub-agent's value is a dict mapping its direct tool names to their aggregated metadata
320
+ (excluding nested sub-agent data, which gets its own top-level key).
321
+
322
+ At the root level, token_usage is rolled up to include all sub-agent token usage.
323
+
324
+ Args:
325
+ metadata_dict: Dict mapping names (tools or agents) to lists of metadata instances
326
+ prefix: Key prefix for nested calls (used internally for recursion)
327
+
328
+ Returns:
329
+ Flat dict with dot-notation keys for sub-agents.
330
+ Example: {
331
+ "token_usage": <combined from all agents>,
332
+ "web_browsing_sub_agent": {"web_search": <aggregated>, "token_usage": <aggregated>},
333
+ "web_browsing_sub_agent.web_fetch_sub_agent": {"fetch_web_page": <aggregated>, "token_usage": <aggregated>}
334
+ }
335
+ """
336
+ result: dict = {}
337
+
338
+ # First pass: aggregate all entries in this level
339
+ aggregated_level: dict = {}
340
+ for name, metadata_list in metadata_dict.items():
341
+ if not metadata_list:
342
+ continue
343
+ aggregated_level[name] = _aggregate_list(metadata_list)
344
+
345
+ # Second pass: separate nested sub-agents from direct tools, and recurse
346
+ direct_tools: dict = {}
347
+ for name, aggregated in aggregated_level.items():
348
+ if hasattr(aggregated, "run_metadata") and isinstance(aggregated.run_metadata, dict):
349
+ # This is a sub-agent - recurse into it
350
+ full_key = f"{prefix}.{name}" if prefix else name
351
+ nested = aggregate_metadata(aggregated.run_metadata, prefix=full_key, return_json_serializable=False)
352
+ result.update(nested)
353
+ else:
354
+ # This is a direct tool/metadata - keep it at this level
355
+ direct_tools[name] = aggregated
356
+
357
+ # Store direct tools under the current prefix
358
+ if prefix:
359
+ result[prefix] = direct_tools
360
+ else:
361
+ # At root level, merge direct tools into result
362
+ result.update(direct_tools)
363
+
364
+ # At root level, roll up all token_usage from sub-agents
365
+ if not prefix:
366
+ total_token_usage = _collect_all_token_usage(result)
367
+ if total_token_usage.total > 0:
368
+ result["token_usage"] = [total_token_usage]
369
+
370
+ if return_json_serializable:
371
+ # Convert all Pydantic models to JSON-serializable dicts
372
+ return to_json_serializable(result)
373
+ return result
374
+
375
+
376
+ # Messages
377
+ class TokenUsage(BaseModel):
378
+ """Token counts for LLM usage (input, output, reasoning tokens)."""
379
+
380
+ input: int = 0
381
+ output: int = 0
382
+ reasoning: int = 0
383
+
384
+ @property
385
+ def total(self) -> int:
386
+ """Total token count across input, output, and reasoning."""
387
+ return self.input + self.output + self.reasoning
388
+
389
+ def __add__(self, other: "TokenUsage") -> "TokenUsage":
390
+ """Add two TokenUsage objects together, summing each field independently."""
391
+ return TokenUsage(
392
+ input=self.input + other.input,
393
+ output=self.output + other.output,
394
+ reasoning=self.reasoning + other.reasoning,
395
+ )
396
+
397
+
398
+ class ToolUseCountMetadata(BaseModel):
399
+ """Generic metadata tracking tool usage count.
400
+
401
+ Implements Addable protocol for aggregation. Use this for tools that only need
402
+ to track how many times they were called.
403
+ """
404
+
405
+ num_uses: int = 1
406
+
407
+ def __add__(self, other: "ToolUseCountMetadata") -> "ToolUseCountMetadata":
408
+ return ToolUseCountMetadata(num_uses=self.num_uses + other.num_uses)
409
+
410
+
411
+ class ToolResult[M](BaseModel):
412
+ """Result from a tool executor with optional metadata.
413
+
414
+ Generic over metadata type M. M should implement Addable protocol for aggregation support,
415
+ but this is not enforced at the class level due to Pydantic schema generation limitations.
416
+ """
417
+
418
+ content: Content
419
+ metadata: M | None = None
420
+
421
+
422
+ class Tool[P: BaseModel, M](BaseModel):
423
+ """Tool definition with name, description, parameter schema, and executor function.
424
+
425
+ Generic over:
426
+ P: Parameter model type (must be a Pydantic BaseModel, or None for parameterless tools)
427
+ M: Metadata type (should implement Addable for aggregation; use None for tools without metadata)
428
+
429
+ Tools are simple, stateless callables. For tools requiring lifecycle management
430
+ (setup/teardown, resource pooling), use a ToolProvider instead.
431
+
432
+ Example with parameters:
433
+ class CalcParams(BaseModel):
434
+ expression: str
435
+
436
+ calc_tool = Tool[CalcParams, None](
437
+ name="calc",
438
+ description="Evaluate math",
439
+ parameters=CalcParams,
440
+ executor=lambda p: ToolResult(content=str(eval(p.expression))),
441
+ )
442
+
443
+ Example without parameters:
444
+ time_tool = Tool[None, None](
445
+ name="time",
446
+ description="Get current time",
447
+ executor=lambda _: ToolResult(content=datetime.now().isoformat()),
448
+ )
449
+ """
450
+
451
+ name: str
452
+ description: str
453
+ parameters: type[P] | None = None
454
+ executor: Callable[[P], ToolResult[M] | Awaitable[ToolResult[M]]]
455
+
456
+
457
+ class ToolProvider(ABC):
458
+ """Abstract base class for tool providers with lifecycle management.
459
+
460
+ ToolProviders manage resources (HTTP clients, sandboxes, server connections)
461
+ and return Tool instances when entering their async context. They implement
462
+ the async context manager protocol.
463
+
464
+ Use ToolProvider for:
465
+ - Tools requiring setup/teardown (connections, temp directories)
466
+ - Tools that return multiple Tool instances (e.g., MCP servers)
467
+ - Tools with shared state across calls (e.g., HTTP client pooling)
468
+
469
+ Example:
470
+ class MyToolProvider(ToolProvider):
471
+ async def __aenter__(self) -> Tool | list[Tool]:
472
+ # Setup resources and return tool(s)
473
+ return self._create_tool()
474
+
475
+ # __aexit__ is optional - default is no-op
476
+
477
+ Agent automatically manages ToolProvider lifecycle via its session() context.
478
+ """
479
+
480
+ @abstractmethod
481
+ async def __aenter__(self) -> "Tool | list[Tool]":
482
+ """Enter async context: setup resources and return tool(s).
483
+
484
+ Returns:
485
+ A single Tool instance, or a list of Tool instances for providers
486
+ that expose multiple tools (e.g., MCP servers).
487
+ """
488
+ ...
489
+
490
+ async def __aexit__( # noqa: B027
491
+ self,
492
+ exc_type: type[BaseException] | None,
493
+ exc_val: BaseException | None,
494
+ exc_tb: TracebackType | None,
495
+ ) -> None:
496
+ """Exit async context: cleanup resources. Default: no-op."""
497
+
498
+
499
+ @runtime_checkable
500
+ class LLMClient(Protocol):
501
+ """Protocol defining the interface for LLM client implementations.
502
+
503
+ Any LLM client must implement this protocol to work with the Agent class.
504
+ Provides text generation with tool support and model capability inspection.
505
+ """
506
+
507
+ @abstractmethod
508
+ async def generate(self, messages: list["ChatMessage"], tools: dict[str, Tool]) -> "AssistantMessage": ...
509
+
510
+ @property
511
+ def model_slug(self) -> str: ...
512
+
513
+ @property
514
+ def max_tokens(self) -> int: ...
515
+
516
+
517
+ class ToolCall(BaseModel):
518
+ """Represents a tool invocation request from the LLM.
519
+
520
+ Attributes:
521
+ name: Name of the tool to invoke
522
+ arguments: JSON string containing tool parameters
523
+ tool_call_id: Unique identifier for tracking this tool call and its result
524
+ """
525
+
526
+ name: str
527
+ arguments: str
528
+ tool_call_id: str | None = None
529
+
530
+
531
+ class SystemMessage(BaseModel):
532
+ """System-level instructions and context for the LLM."""
533
+
534
+ role: Literal["system"] = "system"
535
+ content: Content
536
+
537
+
538
+ class UserMessage(BaseModel):
539
+ """User input message to the LLM."""
540
+
541
+ role: Literal["user"] = "user"
542
+ content: Content
543
+
544
+
545
+ class Reasoning(BaseModel):
546
+ """Extended thinking/reasoning content from models that support chain-of-thought reasoning."""
547
+
548
+ signature: str | None = None
549
+ content: str
550
+
551
+
552
+ class AssistantMessage(BaseModel):
553
+ """LLM response message with optional tool calls and token usage tracking."""
554
+
555
+ role: Literal["assistant"] = "assistant"
556
+ reasoning: Reasoning | None = None
557
+ content: Content
558
+ tool_calls: Annotated[list[ToolCall], Field(default_factory=list)]
559
+ token_usage: Annotated[TokenUsage, Field(default_factory=TokenUsage)]
560
+
561
+
562
+ class ToolMessage(BaseModel):
563
+ """Tool execution result returned to the LLM."""
564
+
565
+ role: Literal["tool"] = "tool"
566
+ content: Content
567
+ tool_call_id: str | None = None
568
+ name: str | None = None
569
+ args_was_valid: bool = True
570
+
571
+
572
+ type ChatMessage = Annotated[SystemMessage | UserMessage | AssistantMessage | ToolMessage, Field(discriminator="role")]
573
+ """Discriminated union of all message types, automatically parsed based on role field."""
574
+
575
+
576
+ class SubAgentMetadata(BaseModel):
577
+ """Metadata from sub-agent execution including token usage, message history, and child run metadata.
578
+
579
+ Implements Addable protocol to support aggregation across multiple subagent calls.
580
+ """
581
+
582
+ message_history: list[list[ChatMessage]]
583
+ run_metadata: Annotated[dict[str, list[Any]], Field(default_factory=dict)]
584
+
585
+ def __add__(self, other: "SubAgentMetadata") -> "SubAgentMetadata":
586
+ """Combine metadata from multiple subagent calls."""
587
+ # Concatenate message histories
588
+ combined_history = self.message_history + other.message_history
589
+ # Merge run metadata (concatenate lists per key)
590
+ combined_meta: dict[str, list[Any]] = dict(self.run_metadata)
591
+ for key, metadata_list in other.run_metadata.items():
592
+ if key in combined_meta:
593
+ combined_meta[key] = combined_meta[key] + metadata_list
594
+ else:
595
+ combined_meta[key] = list(metadata_list)
596
+ return SubAgentMetadata(
597
+ message_history=combined_history,
598
+ run_metadata=combined_meta,
599
+ )
@@ -0,0 +1,22 @@
1
+ """Prompt templates for agent framework.
2
+
3
+ All prompts are loaded at module initialization from the prompts directory.
4
+ Templates can be formatted using .format() with the appropriate variables.
5
+ """
6
+
7
+ from importlib.resources import files
8
+
9
+ prompts_dir = files("stirrup.prompts")
10
+
11
+ # Templates that need .format() with runtime values
12
+ MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE = (prompts_dir / "message_summarizer_bridge.txt").read_text(encoding="utf-8")
13
+ BASE_SYSTEM_PROMPT_TEMPLATE = (prompts_dir / "base_system_prompt.txt").read_text(encoding="utf-8")
14
+
15
+ # Ready-to-use prompts (no formatting needed)
16
+ MESSAGE_SUMMARIZER = (prompts_dir / "message_summarizer.txt").read_text(encoding="utf-8")
17
+
18
+ __all__ = [
19
+ "BASE_SYSTEM_PROMPT_TEMPLATE",
20
+ "MESSAGE_SUMMARIZER",
21
+ "MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE",
22
+ ]
@@ -0,0 +1 @@
1
+ You are an AI agent that will be given a specific task. You are to complete that task using the tools provided in {max_turns} steps. You will need to call the finish tool as your last step, where you will pass your finish reason and paths to any files that you wish to return to the user. You are not able to interact with the user during the task.
@@ -0,0 +1,27 @@
1
+ The context window is approaching its limit. Please create a concise summary of the conversation so far to preserve important information.
2
+
3
+ Your summary should include:
4
+
5
+ 1. **Task Overview**: What is the main goal or objective?
6
+
7
+ 2. **Progress Made**: What has been accomplished so far?
8
+ - Key files created/modified (with paths)
9
+ - Important functions/classes implemented
10
+ - Tools used and their outcomes
11
+
12
+ 3. **Current State**: Where are we now?
13
+ - What is currently working?
14
+ - What has been tested/verified?
15
+
16
+ 4. **Next Steps**: What still needs to be done?
17
+ - Outstanding TODOs (with specific file paths and line numbers if applicable)
18
+ - Known issues or bugs to address
19
+ - Features or functionality not yet implemented
20
+
21
+ 5. **Important Context**: Any critical details that shouldn't be lost
22
+ - Special configurations or setup requirements
23
+ - Important variable names, API endpoints, or data structures
24
+ - Edge cases or constraints to keep in mind
25
+ - Dependencies or relationships between components
26
+
27
+ Keep the summary concise but comprehensive. Do not use any tools. Focus on actionable information that will allow smooth continuation of the work.
@@ -0,0 +1,11 @@
1
+ **Context Continuation**
2
+
3
+ Due to context window limitations, the previous conversation has been summarized. Below is a summary of what happened before:
4
+
5
+ ---
6
+
7
+ {summary}
8
+
9
+ ---
10
+
11
+ You should continue working on this task from where it was left off. All the progress, current state, and next steps are described in the summary above. Proceed with completing any outstanding work.
stirrup/py.typed ADDED
File without changes