- 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.
172 lines
4.4 KiB
Python
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
|