fix(cron): show schedule details and run state in _list_jobs() output

_list_jobs() only displayed job name, id, and schedule kind (e.g. "cron"),
omitting the actual timing and run state. The agent couldn't answer
"when does this run?" or "did it run?" even though CronSchedule and
CronJobState had all the data.

Now surfaces:
- Cron expression + timezone for cron jobs
- Human-readable interval for every jobs
- ISO timestamp for one-shot at jobs
- Enabled/disabled status
- Last run time + status (ok/error/skipped) + error message
- Next scheduled run time

Fixes #1496

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
PJ Hoberman
2026-03-16 16:54:38 +00:00
committed by Xubin Ren
parent f72ceb7a3c
commit eb83778f50

View File

@@ -147,7 +147,41 @@ class CronTool(Tool):
jobs = self._cron.list_jobs()
if not jobs:
return "No scheduled jobs."
lines = [f"- {j.name} (id: {j.id}, {j.schedule.kind})" for j in jobs]
lines = []
for j in jobs:
s = j.schedule
if s.kind == "cron":
timing = f"cron: {s.expr}"
if s.tz:
timing += f" ({s.tz})"
elif s.kind == "every" and s.every_ms:
secs = s.every_ms // 1000
if secs >= 3600:
timing = f"every {secs // 3600}h"
elif secs >= 60:
timing = f"every {secs // 60}m"
else:
timing = f"every {secs}s"
elif s.kind == "at" and s.at_ms:
from datetime import datetime, timezone
dt = datetime.fromtimestamp(s.at_ms / 1000, tz=timezone.utc)
timing = f"at {dt.isoformat()}"
else:
timing = s.kind
status = "enabled" if j.enabled else "disabled"
parts = [f"- {j.name} (id: {j.id}, {timing}, {status})"]
if j.state.last_run_at_ms:
from datetime import datetime, timezone
last_dt = datetime.fromtimestamp(j.state.last_run_at_ms / 1000, tz=timezone.utc)
last_info = f" Last run: {last_dt.isoformat()}{j.state.last_status or 'unknown'}"
if j.state.last_error:
last_info += f" ({j.state.last_error})"
parts.append(last_info)
if j.state.next_run_at_ms:
from datetime import datetime, timezone
next_dt = datetime.fromtimestamp(j.state.next_run_at_ms / 1000, tz=timezone.utc)
parts.append(f" Next run: {next_dt.isoformat()}")
lines.append("\n".join(parts))
return "Scheduled jobs:\n" + "\n".join(lines)
def _remove_job(self, job_id: str | None) -> str: