Initial commit
This commit is contained in:
172
skills/adw-bootstrap/reference/scaled/adw_modules/state.py
Normal file
172
skills/adw-bootstrap/reference/scaled/adw_modules/state.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""State management for ADW composable architecture.
|
||||
|
||||
Provides persistent state management via file storage and
|
||||
transient state passing between scripts via stdin/stdout.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from adw_modules.data_types import ADWStateData
|
||||
|
||||
|
||||
class ADWState:
|
||||
"""Container for ADW workflow state with file persistence."""
|
||||
|
||||
STATE_FILENAME = "adw_state.json"
|
||||
|
||||
def __init__(self, adw_id: str):
|
||||
"""Initialize ADWState with a required ADW ID.
|
||||
|
||||
Args:
|
||||
adw_id: The ADW ID for this state (required)
|
||||
"""
|
||||
if not adw_id:
|
||||
raise ValueError("adw_id is required for ADWState")
|
||||
|
||||
self.adw_id = adw_id
|
||||
# Start with minimal state
|
||||
self.data: Dict[str, Any] = {"adw_id": self.adw_id}
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Update state with new key-value pairs."""
|
||||
# Filter to only our core fields
|
||||
core_fields = {"adw_id", "issue_number", "branch_name", "plan_file", "issue_class", "worktree_path", "backend_port", "frontend_port", "model_set", "all_adws"}
|
||||
for key, value in kwargs.items():
|
||||
if key in core_fields:
|
||||
self.data[key] = value
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
"""Get value from state by key."""
|
||||
return self.data.get(key, default)
|
||||
|
||||
def append_adw_id(self, adw_id: str):
|
||||
"""Append an ADW ID to the all_adws list if not already present."""
|
||||
all_adws = self.data.get("all_adws", [])
|
||||
if adw_id not in all_adws:
|
||||
all_adws.append(adw_id)
|
||||
self.data["all_adws"] = all_adws
|
||||
|
||||
def get_working_directory(self) -> str:
|
||||
"""Get the working directory for this ADW instance.
|
||||
|
||||
Returns worktree_path if set (for isolated workflows),
|
||||
otherwise returns the main repo path.
|
||||
"""
|
||||
worktree_path = self.data.get("worktree_path")
|
||||
if worktree_path:
|
||||
return worktree_path
|
||||
|
||||
# Return main repo path (parent of adws directory)
|
||||
return os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
def get_state_path(self) -> str:
|
||||
"""Get path to state file."""
|
||||
project_root = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
return os.path.join(project_root, "agents", self.adw_id, self.STATE_FILENAME)
|
||||
|
||||
def save(self, workflow_step: Optional[str] = None) -> None:
|
||||
"""Save state to file in agents/{adw_id}/adw_state.json."""
|
||||
state_path = self.get_state_path()
|
||||
os.makedirs(os.path.dirname(state_path), exist_ok=True)
|
||||
|
||||
# Create ADWStateData for validation
|
||||
state_data = ADWStateData(
|
||||
adw_id=self.data.get("adw_id"),
|
||||
issue_number=self.data.get("issue_number"),
|
||||
branch_name=self.data.get("branch_name"),
|
||||
plan_file=self.data.get("plan_file"),
|
||||
issue_class=self.data.get("issue_class"),
|
||||
worktree_path=self.data.get("worktree_path"),
|
||||
backend_port=self.data.get("backend_port"),
|
||||
frontend_port=self.data.get("frontend_port"),
|
||||
model_set=self.data.get("model_set", "base"),
|
||||
all_adws=self.data.get("all_adws", []),
|
||||
)
|
||||
|
||||
# Save as JSON
|
||||
with open(state_path, "w") as f:
|
||||
json.dump(state_data.model_dump(), f, indent=2)
|
||||
|
||||
self.logger.info(f"Saved state to {state_path}")
|
||||
if workflow_step:
|
||||
self.logger.info(f"State updated by: {workflow_step}")
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls, adw_id: str, logger: Optional[logging.Logger] = None
|
||||
) -> Optional["ADWState"]:
|
||||
"""Load state from file if it exists."""
|
||||
project_root = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
state_path = os.path.join(project_root, "agents", adw_id, cls.STATE_FILENAME)
|
||||
|
||||
if not os.path.exists(state_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(state_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Validate with ADWStateData
|
||||
state_data = ADWStateData(**data)
|
||||
|
||||
# Create ADWState instance
|
||||
state = cls(state_data.adw_id)
|
||||
state.data = state_data.model_dump()
|
||||
|
||||
if logger:
|
||||
logger.info(f"🔍 Found existing state from {state_path}")
|
||||
logger.info(f"State: {json.dumps(state_data.model_dump(), indent=2)}")
|
||||
|
||||
return state
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to load state from {state_path}: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_stdin(cls) -> Optional["ADWState"]:
|
||||
"""Read state from stdin if available (for piped input).
|
||||
|
||||
Returns None if no piped input is available (stdin is a tty).
|
||||
"""
|
||||
if sys.stdin.isatty():
|
||||
return None
|
||||
try:
|
||||
input_data = sys.stdin.read()
|
||||
if not input_data.strip():
|
||||
return None
|
||||
data = json.loads(input_data)
|
||||
adw_id = data.get("adw_id")
|
||||
if not adw_id:
|
||||
return None # No valid state without adw_id
|
||||
state = cls(adw_id)
|
||||
state.data = data
|
||||
return state
|
||||
except (json.JSONDecodeError, EOFError):
|
||||
return None
|
||||
|
||||
def to_stdout(self):
|
||||
"""Write state to stdout as JSON (for piping to next script)."""
|
||||
# Only output core fields
|
||||
output_data = {
|
||||
"adw_id": self.data.get("adw_id"),
|
||||
"issue_number": self.data.get("issue_number"),
|
||||
"branch_name": self.data.get("branch_name"),
|
||||
"plan_file": self.data.get("plan_file"),
|
||||
"issue_class": self.data.get("issue_class"),
|
||||
"worktree_path": self.data.get("worktree_path"),
|
||||
"backend_port": self.data.get("backend_port"),
|
||||
"frontend_port": self.data.get("frontend_port"),
|
||||
"all_adws": self.data.get("all_adws", []),
|
||||
}
|
||||
print(json.dumps(output_data, indent=2))
|
||||
Reference in New Issue
Block a user