Source code for agenter.data_models.types
"""Core types for Agenter.
This module contains the primary request/response types, status enums,
and event types used throughout the SDK.
"""
from __future__ import annotations
import time
from enum import Enum
from pathlib import Path # noqa: TC003 - required at runtime by Pydantic
import structlog
from pydantic import BaseModel, Field
from ..config import DEFAULT_MAX_ITERATIONS
from .events import EventData # noqa: TC001 - required at runtime by Pydantic
from .messages import BackendMessage # noqa: TC001 - required at runtime by Pydantic
logger = structlog.get_logger(__name__)
[docs]
class CodingStatus(str, Enum):
"""Status of a coding session.
Status semantics:
COMPLETED: Task succeeded within all budget constraints.
COMPLETED_WITH_LIMIT_EXCEEDED: Task succeeded (validation passed) but
exceeded one or more budget limits. The work is valid but consumed
more resources than configured.
BUDGET_EXCEEDED: Task was stopped because budget limits were reached
before the task could complete successfully.
REFUSED: LLM explicitly refused the request due to safety, policy,
or capability limitations. Use result.refusal_reason for details.
FAILED: True failure (exception, unrecoverable error, not budget-related).
"""
COMPLETED = "completed"
COMPLETED_WITH_LIMIT_EXCEEDED = "completed_with_limit_exceeded"
FAILED = "failed"
BUDGET_EXCEEDED = "budget_exceeded"
REFUSED = "refused"
[docs]
class Verbosity(str, Enum):
"""Verbosity level for console output."""
QUIET = "quiet" # Errors only
NORMAL = "normal" # Progress indicators (default)
VERBOSE = "verbose" # Full details
[docs]
class CodingEventType(str, Enum):
"""Types of events emitted during coding session.
Event lifecycle:
SESSION_START → ITERATION_START → BACKEND_MESSAGE* →
VALIDATION_START → VALIDATION_RESULT* → ITERATION_END →
(repeat iterations) → COMPLETED/FAILED/REFUSED → SESSION_END
"""
SESSION_START = "session_start" # Session initialized with config
ITERATION_START = "iteration_start" # Beginning iteration N
BACKEND_MESSAGE = "backend_message" # Message from backend (text, tool_use, etc.)
VALIDATION_START = "validation_start" # About to run validators
VALIDATION_RESULT = "validation_result" # Result from a single validator
ITERATION_END = "iteration_end" # Iteration complete with metrics
SESSION_END = "session_end" # Session finished (always emitted)
COMPLETED = "completed" # Task completed successfully
FAILED = "failed" # Task failed (budget exceeded, max iterations, etc.)
REFUSED = "refused" # LLM explicitly refused the request
class ContentModifiedFiles(BaseModel):
"""Files modified with full content available.
Used by backends that track file contents (e.g., AnthropicSDKBackend).
Example:
changes = ContentModifiedFiles(files={"main.py": "print('hi')"})
text = changes.content("main.py") # Returns "print('hi')"
"""
files: dict[str, str] = Field(default_factory=dict)
@property
def paths_only(self) -> bool:
return False
def __len__(self) -> int:
return len(self.files)
def __contains__(self, path: object) -> bool:
return path in self.files
def content(self, path: str) -> str | None:
return self.files.get(path)
def paths(self) -> list[str]:
return list(self.files.keys())
class PathsModifiedFiles(BaseModel):
"""Files modified with only paths tracked (no content).
Used by backends that don't track content (e.g., ClaudeCodeBackend).
Content must be read from disk if needed.
Example:
changes = PathsModifiedFiles(file_paths=["main.py", "utils.py"])
changes.content("main.py") # Returns None (must read from disk)
"""
file_paths: list[str] = Field(default_factory=list)
@property
def paths_only(self) -> bool:
return True
@property
def files(self) -> dict[str, str]:
"""Empty content dict for compatibility with ContentModifiedFiles interface."""
return dict.fromkeys(self.file_paths, "")
def __len__(self) -> int:
return len(self.file_paths)
def __contains__(self, path: object) -> bool:
return path in self.file_paths
def content(self, path: str) -> str | None:
"""Always returns None - content must be read from disk."""
logger.debug("content_paths_only_mode", path=path)
return None
def paths(self) -> list[str]:
return list(self.file_paths)
# Union type for backend return values
ModifiedFiles = ContentModifiedFiles | PathsModifiedFiles
[docs]
class Budget(BaseModel):
"""Budget limits for a coding session."""
max_tokens: int | None = None
max_cost_usd: float | None = None
max_time_seconds: float | None = None
max_iterations: int = DEFAULT_MAX_ITERATIONS
[docs]
class CodingRequest(BaseModel):
"""Request for a coding task.
Attributes:
prompt: The task prompt for the agent.
cwd: Working directory for file operations.
system_prompt: Custom system prompt for the LLM. If None, uses the
backend's default system prompt. Use this to provide domain-specific
instructions (e.g., APE's RED TEAM OPERATOR context).
max_iterations: Max validation/refinement iterations (default: 5).
budget: Optional full budget control (tokens, cost, time).
allowed_write_paths: Glob patterns restricting file writes.
output_type: Optional Pydantic model for typed output. When set,
the agent returns structured output matching this schema.
"""
model_config = {"arbitrary_types_allowed": True}
prompt: str
cwd: str
system_prompt: str | None = None # Custom system prompt (overrides default)
max_iterations: int = DEFAULT_MAX_ITERATIONS
budget: Budget | None = None # Optional full budget control
allowed_write_paths: list[str] | None = None # Glob patterns restricting writes
output_type: type[BaseModel] | None = None # Typed structured output
[docs]
class CodingResult(BaseModel):
"""Result of a coding session.
Attributes:
status: Final status of the coding session.
files: Mapping of file path to content for modified files.
summary: Human-readable summary of what was done.
iterations: Number of agent iterations executed.
total_tokens: Total tokens consumed (input + output).
total_cost_usd: Estimated total cost in USD.
total_duration_seconds: Wall clock time for the session.
exceeded_limit: Which budget limit was exceeded (if status is BUDGET_EXCEEDED).
One of: "iterations", "tokens", "cost", "time", or None.
exceeded_values: Details about the exceeded limit (if status is BUDGET_EXCEEDED).
Contains "limit_value" (configured max) and "actual_value" (value that exceeded).
output: Typed structured output when output_type was specified in request.
Contains the Pydantic model instance with all fields including code.
"""
model_config = {"arbitrary_types_allowed": True}
status: CodingStatus
files: dict[str, str] # path -> content
summary: str
iterations: int
total_tokens: int = 0
total_input_tokens: int = 0
total_output_tokens: int = 0
total_cost_usd: float = 0.0
total_duration_seconds: float = 0.0
exceeded_limit: str | None = None # Which limit was exceeded (max_iterations, max_tokens, etc.)
exceeded_values: dict[str, float | int] | None = None # {limit_value: X, actual_value: Y}
output: BaseModel | None = None # Typed structured output
trace_dir: Path | None = None # Directory where traces were saved (if tracer was used)
def _repr_markdown_(self) -> str:
"""Jupyter notebook rich display."""
parts = [f"**Status:** {self.status.value}", f"**Summary:** {self.summary}"]
for path, content in self.files.items():
ext = path.rsplit(".", 1)[-1] if "." in path else ""
parts.append(f"\n**{path}**\n```{ext}\n{content}\n```")
return "\n\n".join(parts)
[docs]
class CodingEvent(BaseModel):
"""Event emitted during coding session for observability."""
model_config = {"arbitrary_types_allowed": True}
type: CodingEventType
data: EventData | None = None
message: BackendMessage | None = None # Typed message for BACKEND_MESSAGE events
result: CodingResult | None = None # Final result for terminal events (COMPLETED, FAILED)
timestamp: float = Field(default_factory=time.time)
[docs]
class ValidationResult(BaseModel):
"""Result of running validators on modified files."""
passed: bool
errors: list[str] = Field(default_factory=list)