feat(templates): add isolated agentTurn and systemEvent cron templates
This commit is contained in:
parent
195fcc2517
commit
51371b18ce
3 changed files with 236 additions and 0 deletions
51
scripts/templates/cron/README.md
Normal file
51
scripts/templates/cron/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Cron Execution Templates
|
||||
|
||||
Two execution types for scheduled agent work, inspired by OpenClaw's cron service.
|
||||
|
||||
## agentTurn (agent-turn.sh)
|
||||
|
||||
Full background autonomy. Creates/resumes its own named session.
|
||||
|
||||
**Use for:**
|
||||
- Complex multi-step work
|
||||
- Tasks that need full context and tool access
|
||||
- Background autonomous operation (proactive agent cycles)
|
||||
|
||||
**Features:**
|
||||
- Named sessions via `--name` for resume capability
|
||||
- Orphan detection (checks PID before starting new run)
|
||||
- Session rollover: fresh session after 200 turns or 72 hours
|
||||
- Context loading from SESSION-STATE.md and HEARTBEAT.md
|
||||
- Dated log files per agent per day
|
||||
|
||||
**Session naming (VERIFIED):**
|
||||
Uses `--name "agent:<name>:turn"` with `--resume` for continuity.
|
||||
Note: `--session-id` requires valid UUIDs. Named sessions are the
|
||||
correct approach for human-readable, resumable agent sessions.
|
||||
|
||||
## systemEvent (system-event.sh)
|
||||
|
||||
Injects text into an existing session. No new session created.
|
||||
|
||||
**Use for:**
|
||||
- Notifications ("new data available")
|
||||
- Simple status checks
|
||||
- Triggering a specific action in a running session
|
||||
|
||||
**Limitations:**
|
||||
- Requires an active named session to resume
|
||||
- Limited to 3 turns (quick action, not full autonomy)
|
||||
- If the session doesn't exist, the command fails gracefully
|
||||
|
||||
## Scheduling examples
|
||||
|
||||
```bash
|
||||
# agentTurn: run full agent turn every hour
|
||||
0 * * * * /path/to/agent-turn.sh >> /tmp/agent-turn.log 2>&1
|
||||
|
||||
# systemEvent: notify agent of new data every 30 min
|
||||
*/30 * * * * /path/to/system-event.sh "agent:processor:turn" "Check /data/inbox for new files"
|
||||
|
||||
# agentTurn with catchup on reboot
|
||||
@reboot /path/to/agent-turn.sh >> /tmp/agent-turn.log 2>&1
|
||||
```
|
||||
143
scripts/templates/cron/agent-turn.sh
Normal file
143
scripts/templates/cron/agent-turn.sh
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/bash
|
||||
# Agent Turn: Full background autonomy for Claude Code agents.
|
||||
# Fires a complete agent turn with its own named session.
|
||||
#
|
||||
# Bash 3.2 compatible. Uses python3 for JSON/date operations.
|
||||
#
|
||||
# Placeholders:
|
||||
# {{AGENT_NAME}} - name of the agent
|
||||
# {{WORKING_DIR}} - absolute path to project directory
|
||||
# {{MAX_TURNS}} - max turns per agent turn (default: 15)
|
||||
|
||||
AGENT_NAME="{{AGENT_NAME}}"
|
||||
WORKING_DIR="{{WORKING_DIR}}"
|
||||
MAX_TURNS="${MAX_TURNS:-15}"
|
||||
SESSION_NAME="agent:${AGENT_NAME}:turn"
|
||||
PID_FILE="$WORKING_DIR/.agent-turn-${AGENT_NAME}.pid"
|
||||
LOG_DIR="$WORKING_DIR/logs"
|
||||
STATE_FILE="$WORKING_DIR/.agent-turn-state.json"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# --- Orphan detection ---
|
||||
# Check if a previous agent turn is still running
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
OLD_PID=$(cat "$PID_FILE" 2>/dev/null)
|
||||
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | agent-turn | SKIP: previous run still active (PID $OLD_PID)" >> "$LOG_DIR/agent-turn.log"
|
||||
exit 0
|
||||
else
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | agent-turn | Cleaned orphan PID file (PID $OLD_PID)" >> "$LOG_DIR/agent-turn.log"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Session rollover check ---
|
||||
# After 200 turns or 72 hours, start a fresh session
|
||||
NEEDS_FRESH=$(python3 -c "
|
||||
import json, time, os
|
||||
state_file = '$STATE_FILE'
|
||||
agent = '$AGENT_NAME'
|
||||
try:
|
||||
state = json.load(open(state_file))
|
||||
agent_state = state.get(agent, {})
|
||||
turns = agent_state.get('turn_count', 0)
|
||||
started = agent_state.get('session_started', 0)
|
||||
age_hours = (time.time() - started) / 3600 if started else 999
|
||||
if turns >= 200 or age_hours >= 72:
|
||||
print('true')
|
||||
else:
|
||||
print('false')
|
||||
except:
|
||||
print('true')
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ "$NEEDS_FRESH" = "true" ]; then
|
||||
# Use a new session name with timestamp for rollover
|
||||
SESSION_NAME="agent:${AGENT_NAME}:turn:$(date +%s)"
|
||||
# Reset state
|
||||
python3 -c "
|
||||
import json, time, os
|
||||
state_file = '$STATE_FILE'
|
||||
agent = '$AGENT_NAME'
|
||||
try:
|
||||
state = json.load(open(state_file))
|
||||
except:
|
||||
state = {}
|
||||
state[agent] = {'turn_count': 0, 'session_started': time.time(), 'session_name': '$SESSION_NAME'}
|
||||
with open(state_file, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
"
|
||||
fi
|
||||
|
||||
# --- Load context ---
|
||||
# Build prompt from HEARTBEAT.md and SESSION-STATE.md
|
||||
PROMPT=$(python3 -c "
|
||||
import os
|
||||
working_dir = '$WORKING_DIR'
|
||||
parts = []
|
||||
|
||||
# Read SESSION-STATE.md for current context
|
||||
ss_memory = os.path.join(working_dir, 'memory', 'SESSION-STATE.md')
|
||||
ss_root = os.path.join(working_dir, 'SESSION-STATE.md')
|
||||
if os.path.exists(ss_memory):
|
||||
parts.append('## Current session state:\n' + open(ss_memory).read()[:2000])
|
||||
elif os.path.exists(ss_root):
|
||||
parts.append('## Current session state:\n' + open(ss_root).read()[:2000])
|
||||
|
||||
# Read HEARTBEAT.md for tasks
|
||||
hb = os.path.join(working_dir, 'HEARTBEAT.md')
|
||||
if os.path.exists(hb):
|
||||
parts.append('## Heartbeat tasks:\n' + open(hb).read()[:2000])
|
||||
|
||||
if parts:
|
||||
print('\n\n'.join(parts))
|
||||
else:
|
||||
print('No context files found. Check the working directory and proceed with any pending tasks.')
|
||||
" 2>/dev/null)
|
||||
|
||||
# --- Run agent turn ---
|
||||
echo $$ > "$PID_FILE"
|
||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
echo "$TIMESTAMP | agent-turn | START: $AGENT_NAME (session: $SESSION_NAME)" >> "$LOG_DIR/agent-turn.log"
|
||||
|
||||
cd "$WORKING_DIR"
|
||||
|
||||
OUTPUT=$(claude -p "$PROMPT" \
|
||||
--name "$SESSION_NAME" \
|
||||
--output-format text \
|
||||
--max-turns "$MAX_TURNS" 2>&1)
|
||||
|
||||
EXIT_CODE=$?
|
||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Save output to dated log
|
||||
echo "--- $TIMESTAMP ---" >> "$LOG_DIR/agent-turn-${AGENT_NAME}-$(date +%Y-%m-%d).log"
|
||||
echo "$OUTPUT" >> "$LOG_DIR/agent-turn-${AGENT_NAME}-$(date +%Y-%m-%d).log"
|
||||
echo "" >> "$LOG_DIR/agent-turn-${AGENT_NAME}-$(date +%Y-%m-%d).log"
|
||||
|
||||
# Update turn count
|
||||
python3 -c "
|
||||
import json, time
|
||||
state_file = '$STATE_FILE'
|
||||
agent = '$AGENT_NAME'
|
||||
try:
|
||||
state = json.load(open(state_file))
|
||||
except:
|
||||
state = {}
|
||||
if agent not in state:
|
||||
state[agent] = {'turn_count': 0, 'session_started': time.time(), 'session_name': '$SESSION_NAME'}
|
||||
state[agent]['turn_count'] = state[agent].get('turn_count', 0) + 1
|
||||
state[agent]['last_run'] = time.time()
|
||||
with open(state_file, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
"
|
||||
|
||||
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||
echo "$TIMESTAMP | agent-turn | COMPLETE: $AGENT_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/agent-turn.log"
|
||||
else
|
||||
echo "$TIMESTAMP | agent-turn | ERROR: $AGENT_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/agent-turn.log"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$PID_FILE"
|
||||
42
scripts/templates/cron/system-event.sh
Normal file
42
scripts/templates/cron/system-event.sh
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/bash
|
||||
# System Event: Inject a text event into an existing Claude Code session.
|
||||
# Lighter than agentTurn — does not create a new session.
|
||||
#
|
||||
# Bash 3.2 compatible.
|
||||
#
|
||||
# Usage: ./system-event.sh "session-name" "Event text to inject"
|
||||
#
|
||||
# Placeholders:
|
||||
# {{WORKING_DIR}} - absolute path to project directory
|
||||
|
||||
WORKING_DIR="{{WORKING_DIR}}"
|
||||
SESSION_NAME="${1:-}"
|
||||
EVENT_TEXT="${2:-}"
|
||||
|
||||
if [ -z "$SESSION_NAME" ] || [ -z "$EVENT_TEXT" ]; then
|
||||
echo "Usage: $0 <session-name> <event-text>"
|
||||
echo "Example: $0 'agent:researcher:turn' 'New data available in /data/inbox'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_DIR="$WORKING_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
echo "$TIMESTAMP | system-event | INJECT: $SESSION_NAME -- $EVENT_TEXT" >> "$LOG_DIR/system-event.log"
|
||||
|
||||
cd "$WORKING_DIR"
|
||||
|
||||
# Resume the named session and inject the event
|
||||
OUTPUT=$(claude --resume "$SESSION_NAME" -p "$EVENT_TEXT" \
|
||||
--output-format text \
|
||||
--max-turns 3 2>&1)
|
||||
|
||||
EXIT_CODE=$?
|
||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||
echo "$TIMESTAMP | system-event | DELIVERED: $SESSION_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/system-event.log"
|
||||
else
|
||||
echo "$TIMESTAMP | system-event | FAILED: $SESSION_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/system-event.log"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue