240 lines
7.3 KiB
Python
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())
|