AgentEvalTool/backend/agenteval/intelligent_eval/openclaw_client.py
sinohqb 2ff023a65b feat(intelligent-eval): implement cron pool management (ticket 02)
- Add OpenClawClient wrapping CLI commands (create/delete/list crons)
- Implement pool initialization, scale up/down, auto-scaling logic
- Implement cron state sync and stuck cron detection
- Add pool status and manual scaling APIs
- Add 13 unit tests and 5 integration tests

Pool automatically scales between 5-20 crons based on load.
All 778 tests passing.
2026-08-12 09:47:04 +08:00

172 lines
4.4 KiB
Python

"""OpenClaw CLI client for managing cron jobs.
Wraps `openclaw automations` commands to create, delete, and list cron jobs.
"""
import asyncio
import json
import logging
from dataclasses import dataclass
from typing import Optional
_logger = logging.getLogger("agenteval")
@dataclass
class OpenClawCron:
"""OpenClaw cron job info."""
id: str
name: str
schedule: str
enabled: bool
state: Optional[dict] = None
class OpenClawClient:
"""Client for OpenClaw CLI commands."""
def __init__(self, openclaw_bin: str = "openclaw"):
self.openclaw_bin = openclaw_bin
async def _run_command(self, *args: str) -> tuple[int, str, str]:
"""Run an OpenClaw CLI command.
Returns:
(returncode, stdout, stderr)
"""
cmd = [self.openclaw_bin] + list(args)
_logger.debug(f"Running OpenClaw command: {' '.join(cmd)}")
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
return (
process.returncode or 0,
stdout.decode("utf-8"),
stderr.decode("utf-8"),
)
async def create_cron(
self,
*,
name: str,
schedule: str,
skill: str,
state: Optional[dict] = None,
) -> str:
"""Create a cron job.
Args:
name: Cron job name
schedule: Cron schedule expression (e.g., "* * * * *")
skill: Skill to execute
state: Initial state (JSON)
Returns:
Cron job ID
Raises:
RuntimeError: If creation fails
"""
args = [
"automations",
"create",
schedule,
f"--name={name}",
f"--skill={skill}",
]
if state:
args.append(f"--state={json.dumps(state)}")
returncode, stdout, stderr = await self._run_command(*args)
if returncode != 0:
raise RuntimeError(f"Failed to create cron: {stderr}")
# Parse cron ID from output
# Expected output format: "Created automation <id>"
lines = stdout.strip().split("\n")
for line in lines:
if "Created automation" in line:
cron_id = line.split()[-1]
_logger.info(f"Created OpenClaw cron {cron_id}: {name}")
return cron_id
raise RuntimeError(f"Failed to parse cron ID from output: {stdout}")
async def delete_cron(self, cron_id: str) -> None:
"""Delete a cron job.
Args:
cron_id: Cron job ID
Raises:
RuntimeError: If deletion fails
"""
returncode, stdout, stderr = await self._run_command(
"automations",
"remove",
cron_id,
)
if returncode != 0:
raise RuntimeError(f"Failed to delete cron {cron_id}: {stderr}")
_logger.info(f"Deleted OpenClaw cron {cron_id}")
async def list_crons(self) -> list[OpenClawCron]:
"""List all cron jobs.
Returns:
List of cron jobs
Raises:
RuntimeError: If listing fails
"""
returncode, stdout, stderr = await self._run_command(
"automations",
"list",
"--json",
)
if returncode != 0:
raise RuntimeError(f"Failed to list crons: {stderr}")
try:
data = json.loads(stdout)
crons = []
for item in data:
crons.append(
OpenClawCron(
id=item["id"],
name=item.get("name", ""),
schedule=item.get("schedule", ""),
enabled=item.get("enabled", True),
state=item.get("state"),
)
)
return crons
except (json.JSONDecodeError, KeyError) as e:
raise RuntimeError(f"Failed to parse cron list: {e}") from e
async def get_cron_state(self, cron_id: str) -> Optional[dict]:
"""Get cron job state.
Args:
cron_id: Cron job ID
Returns:
State dict, or None if not found
"""
crons = await self.list_crons()
for cron in crons:
if cron.id == cron_id:
return cron.state
return None