Files
household-snapshots/scripts/agent_tick_submitter.py

240 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""
Agent Tick Submitter
Each agent runs this monthly to submit their health tick.
Can also be run centrally for all agents.
"""
import os
import sys
import json
import subprocess
import argparse
from datetime import datetime
from pathlib import Path
REPO_DIR = Path("/root/wizards/household-snapshots")
TICKS_DIR = REPO_DIR / "ticks"
REGISTRY_FILE = REPO_DIR / "config" / "agent-registry.json"
GITEA_URL = os.environ.get("CLAW_CODE_GITEA_URL", "http://143.198.27.163:3000")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
def run_cmd(cmd, cwd=None):
"""Run shell command and return output."""
result = subprocess.run(
cmd, shell=True, cwd=cwd, capture_output=True, text=True
)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def load_registry():
"""Load agent registry."""
with open(REGISTRY_FILE) as f:
return json.load(f)
def check_gateway_running(agent_id):
"""Check if agent's gateway is running."""
stdout, _, _ = run_cmd(f"pgrep -f 'hermes gateway.*{agent_id}' || true")
return bool(stdout)
def check_home_accessible(home_dir):
"""Check if home directory is accessible."""
return Path(home_dir).exists()
def check_config_valid(config_path):
"""Basic config validation."""
if not Path(config_path).exists():
return False
try:
with open(config_path) as f:
content = f.read()
return 'model:' in content and 'platforms:' in content
except:
return False
def get_last_interaction(agent_id):
"""Get last user interaction from logs (if available)."""
log_dir = Path(f"/root/wizards/{agent_id}/logs")
if not log_dir.exists():
return None
try:
log_files = sorted(log_dir.glob("*.log"), key=lambda x: x.stat().st_mtime, reverse=True)
if log_files:
mtime = datetime.fromtimestamp(log_files[0].stat().st_mtime)
return mtime.isoformat() + "Z"
except:
pass
return None
def count_work_items(agent_id):
"""Count work items completed this month."""
work_dir = Path(f"/root/wizards/{agent_id}/work")
if not work_dir.exists():
return 0
count = 0
current_month = datetime.utcnow().strftime("%Y-%m")
for item in work_dir.rglob("*"):
if item.is_file():
try:
mtime = datetime.fromtimestamp(item.stat().st_mtime)
if mtime.strftime("%Y-%m") == current_month:
count += 1
except:
pass
return count
def generate_tick(agent_id, registry_data):
"""Generate tick record for an agent."""
agent = None
for a in registry_data["agents"]:
if a["id"] == agent_id:
agent = a
break
if not agent:
raise ValueError(f"Agent {agent_id} not found in registry")
now = datetime.utcnow()
# Check vitals
gateway_running = check_gateway_running(agent_id)
home_accessible = check_home_accessible(agent["home_dir"])
config_valid = check_config_valid(agent["config_path"])
# Determine status
if not gateway_running or not home_accessible:
status = "critical"
elif not config_valid:
status = "degraded"
else:
status = "healthy"
# Check capabilities
capabilities = {}
for cap in agent.get("expected_capabilities", []):
if cap == "telegram":
# Check if telegram is enabled in config
try:
with open(agent["config_path"]) as f:
capabilities[cap] = "enabled: true" in f.read()
except:
capabilities[cap] = False
elif cap == "api_server":
capabilities[cap] = gateway_running
elif cap == "gitea":
capabilities[cap] = bool(GITEA_TOKEN)
else:
capabilities[cap] = True
tick = {
"agent_id": agent_id,
"agent_name": agent["name"],
"role": agent["role"],
"timestamp": now.isoformat() + "Z",
"tick_month": now.strftime("%Y-%m"),
"status": status,
"vitals": {
"gateway_running": gateway_running,
"home_directory_accessible": home_accessible,
"config_valid": config_valid,
"last_user_interaction": get_last_interaction(agent_id),
"work_items_completed_this_month": count_work_items(agent_id)
},
"capabilities": capabilities,
"notes": ""
}
return tick
def save_tick(tick):
"""Save tick to repository."""
month_dir = TICKS_DIR / tick["tick_month"]
month_dir.mkdir(parents=True, exist_ok=True)
tick_file = month_dir / f"{tick['agent_id']}.json"
with open(tick_file, 'w') as f:
json.dump(tick, f, indent=2)
return tick_file
def commit_tick(agent_id, tick_month):
"""Commit tick to Gitea."""
tick_file = TICKS_DIR / tick_month / f"{agent_id}.json"
if not tick_file.exists():
return False, "Tick file not found"
# Git operations
run_cmd("git add -A", cwd=REPO_DIR)
stdout, stderr, code = run_cmd(
f'git commit -m "Monthly tick: {agent_id} for {tick_month}"',
cwd=REPO_DIR
)
if code != 0 and "nothing to commit" not in stderr.lower():
return False, f"Commit failed: {stderr}"
# Push
stdout, stderr, code = run_cmd("git push origin main", cwd=REPO_DIR)
if code != 0:
return False, f"Push failed: {stderr}"
return True, "Tick committed successfully"
def submit_agent_tick(agent_id, registry_data, commit=True):
"""Submit tick for a single agent."""
print(f"Generating tick for {agent_id}...")
tick = generate_tick(agent_id, registry_data)
tick_file = save_tick(tick)
print(f" ✓ Tick saved: {tick_file}")
print(f" Status: {tick['status']}")
if commit:
success, msg = commit_tick(agent_id, tick["tick_month"])
if success:
print(f" ✓ Committed to Gitea")
else:
print(f" ✗ Commit failed: {msg}")
return False
return tick
def main():
parser = argparse.ArgumentParser(description="Agent Tick Submitter")
parser.add_argument("--agent", help="Submit tick for specific agent")
parser.add_argument("--all", action="store_true", help="Submit ticks for all active agents")
parser.add_argument("--no-commit", action="store_true", help="Save locally but don't commit")
args = parser.parse_args()
registry = load_registry()
if args.all:
print(f"=== Submitting ticks for all agents ===")
for agent in registry["agents"]:
if agent["active"]:
submit_agent_tick(agent["id"], registry, commit=not args.no_commit)
print()
elif args.agent:
submit_agent_tick(args.agent, registry, commit=not args.no_commit)
else:
# Auto-detect current agent from hostname or env
hostname = os.uname().nodename
agent_id = hostname if any(a["id"] == hostname for a in registry["agents"]) else None
if agent_id:
submit_agent_tick(agent_id, registry, commit=not args.no_commit)
else:
print("Error: Could not auto-detect agent. Use --agent or --all")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())