Files
zeekay ff7ef7114f feat: merge agents control plane + SDKs into unified agent repo
Consolidate all Hanzo agent infrastructure into one repo:
- control-plane/: Go control plane server (was agents-new)
- sdk/: Go, Python, TypeScript SDKs
- deployments/: Docker, Kubernetes, Helm configs
- Dockerfile.sdk: Python SDK image (ghcr.io/hanzoai/agent:latest)
- CI builds both SDK + control-plane images

No more agentfield — everything is hanzoai/agent.
2026-02-13 12:24:31 -08:00

466 lines
15 KiB
Python

"""
Execution state management for async executions.
This module provides dataclasses and enums for tracking the state of individual
async executions throughout their lifecycle.
"""
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, Optional, List
import time
class ExecutionStatus(Enum):
"""Enumeration of possible execution statuses."""
PENDING = "pending"
QUEUED = "queued"
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
CANCELLED = "cancelled"
TIMEOUT = "timeout"
UNKNOWN = "unknown"
class ExecutionPriority(Enum):
"""Enumeration of execution priorities for queue management."""
LOW = "low"
NORMAL = "normal"
HIGH = "high"
URGENT = "urgent"
@dataclass
class ExecutionMetrics:
"""Metrics and performance data for an execution."""
# Timing metrics
submit_time: float = field(default_factory=time.time)
start_time: Optional[float] = None
end_time: Optional[float] = None
# Polling metrics
poll_count: int = 0
total_poll_time: float = 0.0
last_poll_time: Optional[float] = None
# Network metrics
network_requests: int = 0
network_errors: int = 0
retry_count: int = 0
# Resource metrics
result_size_bytes: Optional[int] = None
memory_usage_mb: Optional[float] = None
@property
def total_duration(self) -> Optional[float]:
"""Total execution duration in seconds."""
if self.submit_time and self.end_time:
return self.end_time - self.submit_time
return None
@property
def execution_duration(self) -> Optional[float]:
"""Actual execution duration (excluding queue time)."""
if self.start_time and self.end_time:
return self.end_time - self.start_time
return None
@property
def queue_duration(self) -> Optional[float]:
"""Time spent in queue before execution started."""
if self.submit_time and self.start_time:
return self.start_time - self.submit_time
return None
@property
def average_poll_interval(self) -> Optional[float]:
"""Average time between polls."""
if self.poll_count > 1 and self.total_poll_time > 0:
return self.total_poll_time / (self.poll_count - 1)
return None
def add_poll(self, poll_duration: float) -> None:
"""Record a polling operation."""
self.poll_count += 1
self.total_poll_time += poll_duration
self.last_poll_time = time.time()
self.network_requests += 1
def add_network_error(self) -> None:
"""Record a network error."""
self.network_errors += 1
def add_retry(self) -> None:
"""Record a retry attempt."""
self.retry_count += 1
@dataclass
class ExecutionState:
"""
Complete state information for an async execution.
This class tracks all aspects of an execution from submission to completion,
including status, results, errors, metrics, and polling information.
"""
# Core identification
execution_id: str
target: str
input_data: Dict[str, Any]
# Status and lifecycle
status: ExecutionStatus = ExecutionStatus.QUEUED
priority: ExecutionPriority = ExecutionPriority.NORMAL
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
# Results and errors
result: Optional[Any] = None
error_message: Optional[str] = None
error_details: Optional[Dict[str, Any]] = None
# Execution context
workflow_id: Optional[str] = None
parent_execution_id: Optional[str] = None
session_id: Optional[str] = None
actor_id: Optional[str] = None
# Webhook metadata
webhook_registered: bool = False
webhook_error: Optional[str] = None
# Configuration
timeout: Optional[float] = None
max_retries: int = 3
# Polling state
next_poll_time: float = field(default_factory=time.time)
current_poll_interval: float = 0.05 # Start with 50ms
consecutive_failures: int = 0
# Metrics and monitoring
metrics: ExecutionMetrics = field(default_factory=ExecutionMetrics)
# Internal state
_is_cancelled: bool = field(default=False, init=False)
_cancellation_reason: Optional[str] = field(default=None, init=False)
_capacity_released: bool = field(default=False, init=False, repr=False)
def __post_init__(self):
"""Post-initialization setup."""
# Ensure metrics are initialized
if not hasattr(self, "metrics") or self.metrics is None:
self.metrics = ExecutionMetrics()
# Set initial poll time
if self.next_poll_time == 0:
self.next_poll_time = time.time() + self.current_poll_interval
@property
def age(self) -> float:
"""Age of the execution in seconds since creation."""
return time.time() - self.metrics.submit_time
@property
def is_terminal(self) -> bool:
"""Whether the execution is in a terminal state."""
return self.status in {
ExecutionStatus.SUCCEEDED,
ExecutionStatus.FAILED,
ExecutionStatus.CANCELLED,
ExecutionStatus.TIMEOUT,
}
@property
def is_active(self) -> bool:
"""Whether the execution is actively running or queued."""
return self.status in {
ExecutionStatus.PENDING,
ExecutionStatus.QUEUED,
ExecutionStatus.RUNNING,
}
@property
def is_successful(self) -> bool:
"""Whether the execution completed successfully."""
return self.status == ExecutionStatus.SUCCEEDED and self.result is not None
@property
def is_cancelled(self) -> bool:
"""Whether the execution has been cancelled."""
return self._is_cancelled or self.status == ExecutionStatus.CANCELLED
@property
def should_poll(self) -> bool:
"""Whether this execution should be polled now."""
return (
self.is_active
and not self.is_cancelled
and time.time() >= self.next_poll_time
)
@property
def is_overdue(self) -> bool:
"""Whether this execution has exceeded its timeout."""
if self.timeout is None:
return False
return self.age > self.timeout
def update_status(
self, status: ExecutionStatus, error_message: Optional[str] = None
) -> None:
"""
Update the execution status and timestamp.
Args:
status: New execution status
error_message: Optional error message for failed executions
"""
old_status = self.status
self.status = status
self.updated_at = datetime.now(timezone.utc)
# Update metrics based on status change
current_time = time.time()
if old_status == ExecutionStatus.QUEUED and status == ExecutionStatus.RUNNING:
self.metrics.start_time = current_time
elif status in {
ExecutionStatus.SUCCEEDED,
ExecutionStatus.FAILED,
ExecutionStatus.CANCELLED,
ExecutionStatus.TIMEOUT,
}:
self.metrics.end_time = current_time
# Handle error cases
if status == ExecutionStatus.FAILED and error_message:
self.error_message = error_message
def set_result(self, result: Any) -> None:
"""
Set the execution result and mark as completed.
Args:
result: The execution result
"""
self.result = result
self.update_status(ExecutionStatus.SUCCEEDED)
# Calculate result size if possible
try:
import sys
self.metrics.result_size_bytes = sys.getsizeof(result)
except Exception:
pass # Size calculation is optional
# Clear input_data to free memory after completion
self.input_data = {}
def set_error(
self, error_message: str, error_details: Optional[Dict[str, Any]] = None
) -> None:
"""
Set execution error and mark as failed.
Args:
error_message: Human-readable error message
error_details: Optional detailed error information
"""
self.error_message = error_message
self.error_details = error_details
self.update_status(ExecutionStatus.FAILED)
# Clear input_data to free memory after failure
self.input_data = {}
def cancel(self, reason: Optional[str] = None) -> None:
"""
Cancel the execution.
Args:
reason: Optional cancellation reason
"""
self._is_cancelled = True
self._cancellation_reason = reason
self.update_status(ExecutionStatus.CANCELLED)
# Clear input_data to free memory after cancellation
self.input_data = {}
def timeout_execution(self) -> None:
"""Mark the execution as timed out."""
self.update_status(
ExecutionStatus.TIMEOUT, f"Execution timed out after {self.timeout} seconds"
)
# Clear input_data to free memory after timeout
self.input_data = {}
def update_poll_interval(self, new_interval: float) -> None:
"""
Update the polling interval and next poll time.
Args:
new_interval: New polling interval in seconds
"""
self.current_poll_interval = new_interval
self.next_poll_time = time.time() + new_interval
def record_poll_attempt(self, success: bool, duration: float = 0.0) -> None:
"""
Record a polling attempt.
Args:
success: Whether the poll was successful
duration: Duration of the poll request
"""
self.metrics.add_poll(duration)
if success:
self.consecutive_failures = 0
else:
self.consecutive_failures += 1
self.metrics.add_network_error()
def record_retry(self) -> None:
"""Record a retry attempt."""
self.metrics.add_retry()
def to_dict(self) -> Dict[str, Any]:
"""
Convert execution state to dictionary representation.
Returns:
Dictionary representation of the execution state
"""
return {
"execution_id": self.execution_id,
"target": self.target,
"status": self.status.value,
"priority": self.priority.value,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"age": self.age,
"result": self.result,
"error_message": self.error_message,
"error_details": self.error_details,
"workflow_id": self.workflow_id,
"parent_execution_id": self.parent_execution_id,
"session_id": self.session_id,
"actor_id": self.actor_id,
"timeout": self.timeout,
"is_terminal": self.is_terminal,
"is_active": self.is_active,
"is_successful": self.is_successful,
"is_cancelled": self.is_cancelled,
"metrics": {
"total_duration": self.metrics.total_duration,
"execution_duration": self.metrics.execution_duration,
"queue_duration": self.metrics.queue_duration,
"poll_count": self.metrics.poll_count,
"network_requests": self.metrics.network_requests,
"network_errors": self.metrics.network_errors,
"retry_count": self.metrics.retry_count,
"result_size_bytes": self.metrics.result_size_bytes,
"average_poll_interval": self.metrics.average_poll_interval,
},
}
def __str__(self) -> str:
"""String representation of the execution state."""
return (
f"ExecutionState(id={self.execution_id[:8]}..., "
f"target={self.target}, status={self.status.value}, "
f"age={self.age:.1f}s, polls={self.metrics.poll_count})"
)
def __repr__(self) -> str:
"""Detailed string representation."""
return (
f"ExecutionState("
f"execution_id='{self.execution_id}', "
f"target='{self.target}', "
f"status={self.status}, "
f"age={self.age:.2f}, "
f"polls={self.metrics.poll_count}, "
f"interval={self.current_poll_interval}"
f")"
)
@dataclass
class ExecutionBatch:
"""
Represents a batch of executions for efficient batch processing.
"""
executions: List[ExecutionState] = field(default_factory=list)
batch_id: str = field(default_factory=lambda: f"batch_{int(time.time() * 1000)}")
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@property
def size(self) -> int:
"""Number of executions in the batch."""
return len(self.executions)
@property
def execution_ids(self) -> List[str]:
"""List of execution IDs in the batch."""
return [exec_state.execution_id for exec_state in self.executions]
@property
def active_executions(self) -> List[ExecutionState]:
"""List of active (non-terminal) executions in the batch."""
return [exec_state for exec_state in self.executions if exec_state.is_active]
@property
def completed_executions(self) -> List[ExecutionState]:
"""List of completed executions in the batch."""
return [exec_state for exec_state in self.executions if exec_state.is_terminal]
def add_execution(self, execution: ExecutionState) -> None:
"""Add an execution to the batch."""
if execution not in self.executions:
self.executions.append(execution)
def remove_execution(self, execution_id: str) -> Optional[ExecutionState]:
"""Remove and return an execution from the batch."""
for i, execution in enumerate(self.executions):
if execution.execution_id == execution_id:
return self.executions.pop(i)
return None
def get_execution(self, execution_id: str) -> Optional[ExecutionState]:
"""Get an execution by ID."""
for execution in self.executions:
if execution.execution_id == execution_id:
return execution
return None
def clear_completed(self) -> List[ExecutionState]:
"""Remove and return all completed executions."""
completed = self.completed_executions
self.executions = self.active_executions
return completed
def __len__(self) -> int:
"""Number of executions in the batch."""
return len(self.executions)
def __iter__(self):
"""Iterate over executions in the batch."""
return iter(self.executions)
def __str__(self) -> str:
"""String representation of the batch."""
return f"ExecutionBatch(id={self.batch_id}, size={self.size}, active={len(self.active_executions)})"