- Updated CLI to load configuration from user-specific and project-specific YAML files, prioritizing user settings. - Introduced a new command `/platforms` to display the status of connected messaging platforms (Telegram, Discord, WhatsApp). - Implemented a gateway system for handling messaging interactions, including session management and delivery routing for cron job outputs. - Added support for environment variable configuration and a dedicated gateway configuration file for advanced settings. - Enhanced documentation in README.md and added a new messaging.md file to guide users on platform integrations and setup. - Updated toolsets to include platform-specific capabilities for Telegram, Discord, and WhatsApp, ensuring secure and tailored interactions.
375 lines
13 KiB
Python
375 lines
13 KiB
Python
"""
|
|
Cron job management tools for Hermes Agent.
|
|
|
|
These tools allow the agent to schedule, list, and remove automated tasks.
|
|
Only available when running via CLI (hermes-cli toolset).
|
|
|
|
IMPORTANT: Cronjobs run in isolated sessions with NO prior context.
|
|
The prompt must contain ALL necessary information.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from typing import Optional
|
|
|
|
# Import from cron module (will be available when properly installed)
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from cron.jobs import create_job, get_job, list_jobs, remove_job
|
|
|
|
|
|
# =============================================================================
|
|
# Tool: schedule_cronjob
|
|
# =============================================================================
|
|
|
|
def schedule_cronjob(
|
|
prompt: str,
|
|
schedule: str,
|
|
name: Optional[str] = None,
|
|
repeat: Optional[int] = None,
|
|
deliver: Optional[str] = None,
|
|
task_id: str = None
|
|
) -> str:
|
|
"""
|
|
Schedule an automated task to run the agent on a schedule.
|
|
|
|
IMPORTANT: When the cronjob runs, it starts a COMPLETELY FRESH session.
|
|
The agent will have NO memory of this conversation or any prior context.
|
|
Therefore, the prompt MUST contain ALL necessary information:
|
|
- Full context of what needs to be done
|
|
- Specific file paths, URLs, or identifiers
|
|
- Clear success criteria
|
|
- Any relevant background information
|
|
|
|
BAD prompt: "Check on that server issue"
|
|
GOOD prompt: "SSH into server 192.168.1.100 as user 'deploy', check if nginx
|
|
is running with 'systemctl status nginx', and verify the site
|
|
https://example.com returns HTTP 200. Report any issues found."
|
|
|
|
Args:
|
|
prompt: Complete, self-contained instructions for the future agent.
|
|
Must include ALL context needed - the agent won't remember anything.
|
|
schedule: When to run. Either:
|
|
- Duration for one-shot: "30m", "2h", "1d" (runs once)
|
|
- Interval: "every 30m", "every 2h" (recurring)
|
|
- Cron expression: "0 9 * * *" (daily at 9am)
|
|
- ISO timestamp: "2026-02-03T14:00:00" (one-shot at specific time)
|
|
name: Optional human-friendly name for the job (for listing/management)
|
|
repeat: How many times to run. Omit for default behavior:
|
|
- One-shot schedules default to repeat=1 (run once)
|
|
- Intervals/cron default to forever
|
|
- Set repeat=5 to run 5 times then auto-delete
|
|
deliver: Where to send the output. Options:
|
|
- "origin": Back to where this job was created (default)
|
|
- "local": Save to local files only (~/.hermes/cron/output/)
|
|
- "telegram": Send to Telegram home channel
|
|
- "discord": Send to Discord home channel
|
|
- "telegram:123456": Send to specific chat ID
|
|
|
|
Returns:
|
|
JSON with job_id, next_run time, and confirmation
|
|
"""
|
|
# Get origin info from environment if available
|
|
origin = None
|
|
origin_platform = os.getenv("HERMES_SESSION_PLATFORM")
|
|
origin_chat_id = os.getenv("HERMES_SESSION_CHAT_ID")
|
|
if origin_platform and origin_chat_id:
|
|
origin = {
|
|
"platform": origin_platform,
|
|
"chat_id": origin_chat_id,
|
|
"chat_name": os.getenv("HERMES_SESSION_CHAT_NAME"),
|
|
}
|
|
|
|
try:
|
|
job = create_job(
|
|
prompt=prompt,
|
|
schedule=schedule,
|
|
name=name,
|
|
repeat=repeat,
|
|
deliver=deliver,
|
|
origin=origin
|
|
)
|
|
|
|
# Format repeat info for display
|
|
times = job["repeat"].get("times")
|
|
if times is None:
|
|
repeat_display = "forever"
|
|
elif times == 1:
|
|
repeat_display = "once"
|
|
else:
|
|
repeat_display = f"{times} times"
|
|
|
|
return json.dumps({
|
|
"success": True,
|
|
"job_id": job["id"],
|
|
"name": job["name"],
|
|
"schedule": job["schedule_display"],
|
|
"repeat": repeat_display,
|
|
"deliver": job.get("deliver", "local"),
|
|
"next_run_at": job["next_run_at"],
|
|
"message": f"Cronjob '{job['name']}' created. It will run {repeat_display}, deliver to {job.get('deliver', 'local')}, next at {job['next_run_at']}."
|
|
}, indent=2)
|
|
|
|
except Exception as e:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": str(e)
|
|
}, indent=2)
|
|
|
|
|
|
SCHEDULE_CRONJOB_SCHEMA = {
|
|
"name": "schedule_cronjob",
|
|
"description": """Schedule an automated task to run the agent on a schedule.
|
|
|
|
⚠️ CRITICAL: The cronjob runs in a FRESH SESSION with NO CONTEXT from this conversation.
|
|
The prompt must be COMPLETELY SELF-CONTAINED with ALL necessary information including:
|
|
- Full context and background
|
|
- Specific file paths, URLs, server addresses
|
|
- Clear instructions and success criteria
|
|
- Any credentials or configuration details
|
|
|
|
The future agent will NOT remember anything from the current conversation.
|
|
|
|
SCHEDULE FORMATS:
|
|
- One-shot: "30m", "2h", "1d" (runs once after delay)
|
|
- Interval: "every 30m", "every 2h" (recurring)
|
|
- Cron: "0 9 * * *" (cron expression for precise scheduling)
|
|
- Timestamp: "2026-02-03T14:00:00" (specific date/time)
|
|
|
|
REPEAT BEHAVIOR:
|
|
- One-shot schedules: run once by default
|
|
- Intervals/cron: run forever by default
|
|
- Set repeat=N to run exactly N times then auto-delete
|
|
|
|
DELIVERY OPTIONS (where output goes):
|
|
- "origin": Back to current chat (default if in messaging platform)
|
|
- "local": Save to local files only (default if in CLI)
|
|
- "telegram": Send to Telegram home channel
|
|
- "discord": Send to Discord home channel
|
|
- "telegram:123456": Send to specific chat (if user provides ID)
|
|
|
|
Use for: reminders, periodic checks, scheduled reports, automated maintenance.""",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"prompt": {
|
|
"type": "string",
|
|
"description": "Complete, self-contained instructions. Must include ALL context - the future agent will have NO memory of this conversation."
|
|
},
|
|
"schedule": {
|
|
"type": "string",
|
|
"description": "When to run: '30m' (once in 30min), 'every 30m' (recurring), '0 9 * * *' (cron), or ISO timestamp"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Optional human-friendly name for the job"
|
|
},
|
|
"repeat": {
|
|
"type": "integer",
|
|
"description": "How many times to run. Omit for default (once for one-shot, forever for recurring). Set to N for exactly N runs."
|
|
},
|
|
"deliver": {
|
|
"type": "string",
|
|
"description": "Where to send output: 'origin' (back to this chat), 'local' (files only), 'telegram', 'discord', or 'platform:chat_id'"
|
|
}
|
|
},
|
|
"required": ["prompt", "schedule"]
|
|
}
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Tool: list_cronjobs
|
|
# =============================================================================
|
|
|
|
def list_cronjobs(include_disabled: bool = False, task_id: str = None) -> str:
|
|
"""
|
|
List all scheduled cronjobs.
|
|
|
|
Returns information about each job including:
|
|
- Job ID (needed for removal)
|
|
- Name
|
|
- Schedule (human-readable)
|
|
- Repeat status (completed/total or 'forever')
|
|
- Next scheduled run time
|
|
- Last run time and status (if any)
|
|
|
|
Args:
|
|
include_disabled: Whether to include disabled/completed jobs
|
|
|
|
Returns:
|
|
JSON array of all scheduled jobs
|
|
"""
|
|
try:
|
|
jobs = list_jobs(include_disabled=include_disabled)
|
|
|
|
formatted_jobs = []
|
|
for job in jobs:
|
|
# Format repeat status
|
|
times = job["repeat"].get("times")
|
|
completed = job["repeat"].get("completed", 0)
|
|
if times is None:
|
|
repeat_status = "forever"
|
|
else:
|
|
repeat_status = f"{completed}/{times}"
|
|
|
|
formatted_jobs.append({
|
|
"job_id": job["id"],
|
|
"name": job["name"],
|
|
"prompt_preview": job["prompt"][:100] + "..." if len(job["prompt"]) > 100 else job["prompt"],
|
|
"schedule": job["schedule_display"],
|
|
"repeat": repeat_status,
|
|
"deliver": job.get("deliver", "local"),
|
|
"next_run_at": job.get("next_run_at"),
|
|
"last_run_at": job.get("last_run_at"),
|
|
"last_status": job.get("last_status"),
|
|
"enabled": job.get("enabled", True)
|
|
})
|
|
|
|
return json.dumps({
|
|
"success": True,
|
|
"count": len(formatted_jobs),
|
|
"jobs": formatted_jobs
|
|
}, indent=2)
|
|
|
|
except Exception as e:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": str(e)
|
|
}, indent=2)
|
|
|
|
|
|
LIST_CRONJOBS_SCHEMA = {
|
|
"name": "list_cronjobs",
|
|
"description": """List all scheduled cronjobs with their IDs, schedules, and status.
|
|
|
|
Use this to:
|
|
- See what jobs are currently scheduled
|
|
- Find job IDs for removal with remove_cronjob
|
|
- Check job status and next run times
|
|
|
|
Returns job_id, name, schedule, repeat status, next/last run times.""",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"include_disabled": {
|
|
"type": "boolean",
|
|
"description": "Include disabled/completed jobs in the list (default: false)"
|
|
}
|
|
},
|
|
"required": []
|
|
}
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Tool: remove_cronjob
|
|
# =============================================================================
|
|
|
|
def remove_cronjob(job_id: str, task_id: str = None) -> str:
|
|
"""
|
|
Remove a scheduled cronjob by its ID.
|
|
|
|
Use list_cronjobs first to find the job_id of the job you want to remove.
|
|
|
|
Args:
|
|
job_id: The ID of the job to remove (from list_cronjobs output)
|
|
|
|
Returns:
|
|
JSON confirmation of removal
|
|
"""
|
|
try:
|
|
job = get_job(job_id)
|
|
if not job:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Job with ID '{job_id}' not found. Use list_cronjobs to see available jobs."
|
|
}, indent=2)
|
|
|
|
removed = remove_job(job_id)
|
|
if removed:
|
|
return json.dumps({
|
|
"success": True,
|
|
"message": f"Cronjob '{job['name']}' (ID: {job_id}) has been removed.",
|
|
"removed_job": {
|
|
"id": job_id,
|
|
"name": job["name"],
|
|
"schedule": job["schedule_display"]
|
|
}
|
|
}, indent=2)
|
|
else:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Failed to remove job '{job_id}'"
|
|
}, indent=2)
|
|
|
|
except Exception as e:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": str(e)
|
|
}, indent=2)
|
|
|
|
|
|
REMOVE_CRONJOB_SCHEMA = {
|
|
"name": "remove_cronjob",
|
|
"description": """Remove a scheduled cronjob by its ID.
|
|
|
|
Use list_cronjobs first to find the job_id of the job you want to remove.
|
|
Jobs that have completed their repeat count are auto-removed, but you can
|
|
use this to cancel a job before it completes.""",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"job_id": {
|
|
"type": "string",
|
|
"description": "The ID of the cronjob to remove (from list_cronjobs output)"
|
|
}
|
|
},
|
|
"required": ["job_id"]
|
|
}
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Requirements check
|
|
# =============================================================================
|
|
|
|
def check_cronjob_requirements() -> bool:
|
|
"""
|
|
Check if cronjob tools can be used.
|
|
|
|
Only available in interactive CLI mode (HERMES_INTERACTIVE=1).
|
|
"""
|
|
return os.getenv("HERMES_INTERACTIVE") == "1"
|
|
|
|
|
|
# =============================================================================
|
|
# Exports
|
|
# =============================================================================
|
|
|
|
def get_cronjob_tool_definitions():
|
|
"""Return tool definitions for cronjob management."""
|
|
return [
|
|
SCHEDULE_CRONJOB_SCHEMA,
|
|
LIST_CRONJOBS_SCHEMA,
|
|
REMOVE_CRONJOB_SCHEMA
|
|
]
|
|
|
|
|
|
# For direct testing
|
|
if __name__ == "__main__":
|
|
# Test the tools
|
|
print("Testing schedule_cronjob:")
|
|
result = schedule_cronjob(
|
|
prompt="Test prompt for cron job",
|
|
schedule="5m",
|
|
name="Test Job"
|
|
)
|
|
print(result)
|
|
|
|
print("\nTesting list_cronjobs:")
|
|
result = list_cronjobs()
|
|
print(result)
|