Models
stirrup.core.models
__all__
module-attribute
__all__ = [
"Addable",
"AnyReasoningBlock",
"AssistantBlock",
"AssistantMessage",
"AudioContentBlock",
"BinaryContentBlock",
"ChatMessage",
"Content",
"ContentBlock",
"EmptyParams",
"EncryptedReasoningBlock",
"ImageContentBlock",
"LLMClient",
"OpaqueBlock",
"Reasoning",
"ReasoningBlock",
"ReasoningRefBlock",
"RedactedReasoningBlock",
"SignedReasoningBlock",
"SubAgentMetadata",
"SummaryMessage",
"SystemMessage",
"TextBlock",
"TokenUsage",
"Tool",
"ToolCall",
"ToolMessage",
"ToolProvider",
"ToolResult",
"ToolUseCountMetadata",
"TurnWarningMessage",
"UserMessage",
"VideoContentBlock",
"aggregate_metadata",
"final_text",
"joined_text",
"reasoning_blocks",
"tool_call_blocks",
]
Base64Bytes
module-attribute
Base64Bytes = Annotated[
bytes,
PlainValidator(_b64_to_bytes),
PlainSerializer(_bytes_to_b64, when_used="json"),
]
REASONING_BLOCK_TYPES
module-attribute
REASONING_BLOCK_TYPES = (
ReasoningBlock,
SignedReasoningBlock,
RedactedReasoningBlock,
ReasoningRefBlock,
EncryptedReasoningBlock,
)
Runtime mirror of AnyReasoningBlock for isinstance checks — keep in lockstep.
_CHANNEL_PROJECTION_DEPRECATION
module-attribute
_CHANNEL_PROJECTION_DEPRECATION = "AssistantMessage.{channel} is deprecated; {replacement}. The compatibility projection will be removed in a future release."
ContentBlock
ContentBlock = (
ImageContentBlock
| VideoContentBlock
| AudioContentBlock
| str
)
Union of all content block types (image, video, audio, or text).
Content
Content = list[ContentBlock] | str
Message content: either a plain string or list of mixed content blocks.
AnyReasoningBlock
AnyReasoningBlock = (
ReasoningBlock
| SignedReasoningBlock
| RedactedReasoningBlock
| ReasoningRefBlock
| EncryptedReasoningBlock
)
The reasoning family: one kind per passback mechanism.
AssistantBlock
AssistantBlock = Annotated[
TextBlock
| ReasoningBlock
| SignedReasoningBlock
| RedactedReasoningBlock
| ReasoningRefBlock
| EncryptedReasoningBlock
| OpaqueBlock
| ToolCall
| ImageContentBlock
| VideoContentBlock
| AudioContentBlock,
Field(discriminator=kind),
]
One block of an assistant turn, discriminated on kind.
UserRoleMessage
UserRoleMessage = Annotated[
UserMessage | SummaryMessage | TurnWarningMessage,
Field(discriminator=kind),
BeforeValidator(_reject_untagged_user_message),
]
User-role messages, discriminated on kind — the agent-injected UserMessage
subclasses share role="user", so dumped histories need the nested discriminator
to rehydrate them as their own types (e.g. SummaryMessage.replaced_ids).
ChatMessage
ChatMessage = Annotated[
SystemMessage
| UserRoleMessage
| AssistantMessage
| ToolMessage,
Field(discriminator=role),
]
Discriminated union of all message types, automatically parsed based on role field.
BinaryContentBlock
ImageContentBlock
Bases: BinaryContentBlock
Image content supporting PNG, JPEG, WebP, PSD formats with automatic downscaling.
to_base64_url
to_base64_url(
max_pixels: int | None = RESOLUTION_1MP,
) -> str
Convert image to base64 data URL, optionally resizing to max pixel count.
Source code in src/stirrup/core/models.py
VideoContentBlock
Bases: BinaryContentBlock
MP4 video content with automatic transcoding and resolution downscaling.
to_base64_url
to_base64_url(
max_pixels: int | None = RESOLUTION_480P,
fps: int | None = None,
) -> str
Transcode to MP4 and return base64 data URL.
Source code in src/stirrup/core/models.py
AudioContentBlock
Bases: BinaryContentBlock
Audio content supporting MPEG, WAV, AAC, and other common audio formats.
to_base64_url
Transcode to MP3 and return base64 data URL.
Source code in src/stirrup/core/models.py
Addable
TokenUsage
Bases: BaseModel
Token counts for LLM usage.
Token terminology: output = reasoning + answer.
__add__
__add__(other: TokenUsage) -> TokenUsage
Add two TokenUsage objects together, summing each field independently.
Source code in src/stirrup/core/models.py
ToolUseCountMetadata
Bases: BaseModel
Generic metadata tracking tool usage count.
Implements Addable protocol for aggregation. Use this for tools that only need to track how many times they were called.
Subclasses can override add with their own type thanks to Self typing.
ToolResult
Bases: BaseModel
Result from a tool executor with optional metadata.
Generic over metadata type M. M should implement Addable protocol for aggregation support, but this is not enforced at the class level due to Pydantic schema generation limitations.
Attributes:
| Name | Type | Description |
|---|---|---|
content |
Content
|
The result content (string, list of content blocks, or images) |
success |
bool
|
Whether the tool call was successful. For finish tools, controls if agent terminates. |
metadata |
M | None
|
Optional metadata (e.g., usage stats) that implements Addable for aggregation |
EmptyParams
Bases: BaseModel
Empty parameter model for tools that don't require parameters.
Tool
Bases: BaseModel
Tool definition with name, description, parameter schema, and executor function.
Generic over
P: Parameter model type (Pydantic BaseModel subclass, or EmptyParams for parameterless tools) M: Metadata type (should implement Addable for aggregation; use None for tools without metadata)
Tools are simple, stateless callables. For tools requiring lifecycle management (setup/teardown, resource pooling), use a ToolProvider instead.
Example with parameters
Example without parameters (uses EmptyParams by default):
time_tool = Tool[EmptyParams, None](
name="time",
description="Get current time",
executor=lambda _: ToolResult(content=datetime.now().isoformat()),
)
ToolProvider
Bases: ABC
Abstract base class for tool providers with lifecycle management.
ToolProviders manage resources (HTTP clients, sandboxes, server connections) and return Tool instances when entering their async context. They implement the async context manager protocol.
Use ToolProvider for: - Tools requiring setup/teardown (connections, temp directories) - Tools that return multiple Tool instances (e.g., MCP servers) - Tools with shared state across calls (e.g., HTTP client pooling)
Example
class MyToolProvider(ToolProvider): async def aenter(self) -> Tool | list[Tool]: # Setup resources and return tool(s) return self._create_tool()
# __aexit__ is optional - default is no-op
Agent automatically manages ToolProvider lifecycle via its session() context.
__aenter__
abstractmethod
async
__aexit__
async
__aexit__(
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None
Exit async context: cleanup resources. Default: no-op.
LLMClient
Bases: Protocol
Protocol defining the interface for LLM client implementations.
Any LLM client must implement this protocol to work with the Agent class. Provides text generation with tool support and model capability inspection.
ToolCall
Bases: BaseModel
Represents a tool invocation request from the LLM.
Also a member of the AssistantBlock union: the kind discriminator is
defaulted, so legacy payloads without the key still validate anywhere ToolCall
is used as a plain input, and new dumps always carry it.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Name of the tool to invoke |
arguments |
str
|
JSON string containing tool parameters |
tool_call_id |
str
|
Unique identifier for tracking this tool call and its result |
signature
class-attribute
instance-attribute
signature: str | None = None
Opaque passback state attached to this exact block, e.g. a Google thought signature.
has_provider_tool_call_id
class-attribute
instance-attribute
has_provider_tool_call_id: bool = True
Whether tool_call_id was present on the provider's original block.
A client may synthesize tool_call_id for internal call/result matching
while retaining that it must be omitted from provider-attached passback.
from_provider
classmethod
from_provider(
*,
provider_id: str | None,
name: str,
arguments: str,
signature: str | None = None,
) -> Self
Capture one provider call with a stable internal correlation ID.
Source code in src/stirrup/core/models.py
SystemMessage
Bases: BaseModel
System-level instructions and context for the LLM.
UserMessage
Bases: BaseModel
User input message to the LLM.
SummaryMessage
Bases: UserMessage
Summary message bridging summarized-away conversation context.
Attributes:
| Name | Type | Description |
|---|---|---|
replaced_ids |
list[str]
|
Ids of the AssistantMessages this summary replaced, so consumers of dumped histories can reconstruct lineage offline. |
TurnWarningMessage
Reasoning
Bases: BaseModel
Channel-era reasoning shape accepted only while reading serialized v0.1 messages.
Deprecated as a standalone type: match on the AnyReasoningBlock kinds
(ReasoningBlock / SignedReasoningBlock / RedactedReasoningBlock /
ReasoningRefBlock / EncryptedReasoningBlock) instead.
TextBlock
Bases: BaseModel
One contiguous run of answer text in an assistant turn.
signature carries opaque passback state attached to this exact block,
e.g. a Google thought signature emitted on a visible text part. A client
that cannot re-emit the signature must reject passback rather than silently
stripping it.
ReasoningBlock
Bases: BaseModel
In-band reasoning text with no passback token.
E.g. reasoning_content on Chat Completions-compatible hosts, or
SignedReasoningBlock
Bases: BaseModel
Reasoning bound to an opaque provider signature re-emitted verbatim on passback.
E.g. Anthropic signed thinking blocks.
RedactedReasoningBlock
Bases: BaseModel
Reasoning the provider withheld, replaced by an opaque payload.
E.g. Anthropic redacted_thinking: data must be re-emitted verbatim as a
redacted_thinking block on passback. Carries no readable content.
ReasoningRefBlock
Bases: BaseModel
Reasoning held provider-side and passed back by reference.
This is retained for providers that require an item-level handle on replay.
OpenAI Responses continuation uses AssistantMessage.provider_response_id
instead and does not create this block.
EncryptedReasoningBlock
Bases: BaseModel
Reasoning returned as an opaque encrypted payload for stateless passback.
E.g. OpenAI Responses reasoning items requested with
include: ["reasoning.encrypted_content"] (store=false /
zero-data-retention): the item — id, summary parts, and encrypted payload —
is re-emitted verbatim in position on passback. The payload is opaque and
non-inspectable.
OpaqueBlock
Bases: BaseModel
Provider-native block the framework carries uninterpreted.
For provider-issued marker/control blocks that must round-trip untouched:
data holds the block's raw JSON (self-describing — the provider's own
type field travels inside it). The framework preserves it in position
through history, projections, and serialization so a client that understands
the payload can re-emit it verbatim on passback; other clients fail loudly.
AssistantMessage
Bases: BaseModel
LLM response message: an ordered sequence of assistant blocks.
blocks is the only stored content. The channel-era content and
tool_calls attributes remain deprecated views; reasoning raises because
an ordered reasoning block sequence has no faithful channel-shaped projection.
Serialized v0.1 payloads upgrade to blocks during validation. Channel-shaped
construction is not part of the v0.2 API; new code constructs blocks directly.
Mixing blocks with non-empty legacy channel keys raises.
provider_response_id
class-attribute
instance-attribute
provider_response_id: str | None = None
Provider-attached continuation state, e.g. an OpenAI Responses resp_... id.
This is turn metadata rather than emitted assistant content, so it lives beside
blocks instead of inside their emission order. It is distinct from id
(Stirrup's message identity) and ReasoningRefBlock.id (an emitted reasoning
item handle).
content
property
content: list[AssistantBlock] | str
Bare text for one text block, empty text for no blocks, or the block list.
reasoning
property
reasoning: Reasoning | None
Deprecated channel accessor retained only to fail with migration guidance.
ToolMessage
Bases: BaseModel
Tool execution result returned to the LLM.
Attributes:
| Name | Type | Description |
|---|---|---|
role |
Literal['tool']
|
Always "tool" |
content |
Content
|
The tool result content |
tool_call_id |
str
|
ID linking this result to the corresponding tool call |
name |
str | None
|
Name of the tool that was called |
args_was_valid |
bool
|
Whether the tool arguments were valid |
success |
bool
|
Whether the tool executed successfully (used by finish tool to control termination) |
SubAgentMetadata
Bases: BaseModel
Metadata from sub-agent execution including token usage, message history, and child run metadata.
Implements Addable protocol to support aggregation across multiple subagent calls.
__add__
__add__(other: SubAgentMetadata) -> SubAgentMetadata
Combine metadata from multiple subagent calls.
Source code in src/stirrup/core/models.py
_bytes_to_b64
_b64_to_bytes
downscale_image
Downscale image dimensions to fit within max pixel count while maintaining aspect ratio.
Returns even dimensions with minimum 2x2 size.
Source code in src/stirrup/core/models.py
_merge_dicts
Deep merge two dicts, recursively merging nested dicts and summing numbers.
Source code in src/stirrup/core/models.py
_aggregate_list
_aggregate_list(metadata_list: list[T]) -> T | None
Aggregate a list of metadata using add, with fallback for dicts.
Source code in src/stirrup/core/models.py
to_json_serializable
Source code in src/stirrup/core/models.py
_collect_all_token_usage
_collect_all_token_usage(result: dict) -> TokenUsage
Recursively collect all token_usage from a flattened aggregate_metadata result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
dict
|
The flattened dict from aggregate_metadata (before JSON serialization) |
required |
Returns:
| Type | Description |
|---|---|
TokenUsage
|
Combined TokenUsage from all entries (direct and nested sub-agents) |
Source code in src/stirrup/core/models.py
aggregate_metadata
aggregate_metadata(
metadata_dict: dict[str, list[Any]],
prefix: str = "",
return_json_serializable: bool = True,
) -> dict | object
Aggregate metadata lists and flatten sub-agents into a single-level dict with hierarchical keys.
For entries with nested run_metadata (e.g., SubAgentMetadata), flattens sub-agents using dot notation. Each sub-agent's value is a dict mapping its direct tool names to their aggregated metadata (excluding nested sub-agent data, which gets its own top-level key).
At the root level, token_usage is rolled up to include all sub-agent token usage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata_dict
|
dict[str, list[Any]]
|
Dict mapping names (tools or agents) to lists of metadata instances |
required |
prefix
|
str
|
Key prefix for nested calls (used internally for recursion) |
''
|
Returns:
| Name | Type | Description |
|---|---|---|
dict | object
|
Flat dict with dot-notation keys for sub-agents. |
|
Example |
dict | object
|
{
"token_usage": |
dict | object
|
} |
Source code in src/stirrup/core/models.py
joined_text
joined_text(blocks: Sequence[AssistantBlock]) -> str | None
All answer text across text blocks, directly concatenated; None when absent.
Source code in src/stirrup/core/models.py
final_text
final_text(blocks: Sequence[AssistantBlock]) -> str | None
Text of the last text block — the "answer" in thinking→text→thinking→text turns.
Source code in src/stirrup/core/models.py
tool_call_blocks
tool_call_blocks(
blocks: Sequence[AssistantBlock],
) -> list[ToolCall]
reasoning_blocks
reasoning_blocks(
blocks: Sequence[AssistantBlock],
) -> list[AnyReasoningBlock]
Reasoning blocks (any kind) in emission order.
_reasoning_to_block
Upgrade a flat channel-era Reasoning value to its block equivalent.
A signature means signed passback; bare content is in-band reasoning.
Source code in src/stirrup/core/models.py
_warn_channel_projection
_upgrade_legacy_assistant_message
Read a serialized v0.1 assistant message into canonical blocks.
Source code in src/stirrup/core/models.py
_reject_untagged_user_message
Source code in src/stirrup/core/models.py
_upgrade_legacy_message_sequence
Correlate nullable v0.1 tool-call/result IDs before per-message validation.
v0.1 allowed both sides of tool correlation to omit their ID. A single-message validator cannot recover that relationship, so sequence readers assign a shared ID to each idless call and copy it to the following null-ID result in emission order. Explicit provider IDs are never rewritten.
Source code in src/stirrup/core/models.py
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 | |