253 lines
8.8 KiB
Bash
Executable File
253 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# real-benchmark.sh — Run a benchmark with real Claude Code agents.
|
|
#
|
|
# This script:
|
|
# 1. Starts AO with tracing enabled
|
|
# 2. Spawns N sessions from open GitHub issues
|
|
# 3. Monitors until sessions reach terminal states or time cap
|
|
# 4. Produces a trace file and summary
|
|
#
|
|
# Usage:
|
|
# ./experiments/real-benchmark.sh \
|
|
# --project-dir /path/to/todo-app \
|
|
# --sessions 5 \
|
|
# --timeout 30m
|
|
#
|
|
# Prerequisites:
|
|
# - AO built (`pnpm build` in the AO repo)
|
|
# - `ao` CLI available in PATH
|
|
# - The project's agent-orchestrator.yaml configured
|
|
# - GitHub issues exist on the repo (use seed-issues.sh first)
|
|
# - `ao start` NOT already running (this script starts it)
|
|
|
|
set -euo pipefail
|
|
|
|
PROJECT_DIR=""
|
|
SESSIONS=5
|
|
TIMEOUT_MIN=30
|
|
AO_REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
LABEL="benchmark"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
|
--sessions) SESSIONS="$2"; shift 2 ;;
|
|
--timeout) TIMEOUT_MIN="${2%m}"; shift 2 ;; # strip trailing 'm' if present
|
|
--label) LABEL="$2"; shift 2 ;;
|
|
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$PROJECT_DIR" ]; then
|
|
echo "Usage: $0 --project-dir /path/to/project [--sessions N] [--timeout Nm]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TIMESTAMP=$(date +%s)
|
|
TRACE_FILE="$AO_REPO_DIR/experiments/out/gh-trace-real-$TIMESTAMP.jsonl"
|
|
SUMMARY_FILE="$AO_REPO_DIR/experiments/out/real-benchmark-$TIMESTAMP.txt"
|
|
|
|
mkdir -p "$AO_REPO_DIR/experiments/out"
|
|
|
|
echo "═══════════════════════════════════════════════════════════"
|
|
echo " Real-Agent Benchmark"
|
|
echo " Project: $PROJECT_DIR"
|
|
echo " Sessions: $SESSIONS"
|
|
echo " Timeout: ${TIMEOUT_MIN}m"
|
|
echo " Trace: $TRACE_FILE"
|
|
echo " Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
echo "═══════════════════════════════════════════════════════════"
|
|
|
|
# --- Step 1: Find issues to assign ---
|
|
echo ""
|
|
echo "[1/5] Finding open issues..."
|
|
|
|
# Extract repo from agent-orchestrator.yaml
|
|
REPO=$(grep -A5 'projects:' "$PROJECT_DIR/agent-orchestrator.yaml" | grep 'repo:' | head -1 | awk '{print $2}')
|
|
if [ -z "$REPO" ]; then
|
|
echo "ERROR: Could not find repo in $PROJECT_DIR/agent-orchestrator.yaml" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Get open issue numbers (prefer labeled ones, fall back to all)
|
|
ISSUE_NUMBERS=$(gh issue list --repo "$REPO" --state open --label "$LABEL" --json number --jq '.[].number' 2>/dev/null | head -"$SESSIONS")
|
|
if [ -z "$ISSUE_NUMBERS" ]; then
|
|
echo " No issues with label '$LABEL', trying all open issues..."
|
|
ISSUE_NUMBERS=$(gh issue list --repo "$REPO" --state open --json number --jq '.[].number' 2>/dev/null | head -"$SESSIONS")
|
|
fi
|
|
|
|
AVAILABLE=$(echo "$ISSUE_NUMBERS" | wc -l | tr -d ' ')
|
|
echo " Found $AVAILABLE open issues (requested $SESSIONS)"
|
|
|
|
if [ "$AVAILABLE" -lt "$SESSIONS" ]; then
|
|
echo " WARNING: Only $AVAILABLE issues available, will spawn $AVAILABLE sessions instead"
|
|
SESSIONS="$AVAILABLE"
|
|
fi
|
|
|
|
if [ "$SESSIONS" -eq 0 ]; then
|
|
echo "ERROR: No issues available to spawn sessions for." >&2
|
|
echo "Run: ./experiments/seed-issues.sh --repo $REPO --count 10" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- Step 2: Capture rate limit before ---
|
|
echo ""
|
|
echo "[2/5] Capturing rate limit snapshot (before)..."
|
|
|
|
RATE_BEFORE=$(gh api rate_limit 2>/dev/null)
|
|
GRAPHQL_BEFORE=$(echo "$RATE_BEFORE" | python3 -c "import sys,json; r=json.load(sys.stdin)['resources']['graphql']; print(f\"remaining={r['remaining']} used={r['used']} limit={r['limit']}\")")
|
|
CORE_BEFORE=$(echo "$RATE_BEFORE" | python3 -c "import sys,json; r=json.load(sys.stdin)['resources']['core']; print(f\"remaining={r['remaining']} used={r['used']} limit={r['limit']}\")")
|
|
echo " GraphQL: $GRAPHQL_BEFORE"
|
|
echo " Core: $CORE_BEFORE"
|
|
|
|
# --- Step 3: Ensure AO is running with tracing, then spawn sessions ---
|
|
echo ""
|
|
echo "[3/5] Spawning $SESSIONS sessions..."
|
|
echo ""
|
|
echo " ┌─────────────────────────────────────────────────────────┐"
|
|
echo " │ IMPORTANT: AO must be running WITH tracing enabled. │"
|
|
echo " │ In another terminal, run: │"
|
|
echo " │ │"
|
|
echo " │ cd $PROJECT_DIR"
|
|
echo " │ AO_GH_TRACE_FILE=$TRACE_FILE ao start"
|
|
echo " │ │"
|
|
echo " │ Press Enter here once AO is running... │"
|
|
echo " └─────────────────────────────────────────────────────────┘"
|
|
read -r
|
|
|
|
SPAWNED_IDS=()
|
|
IDX=0
|
|
for ISSUE in $ISSUE_NUMBERS; do
|
|
IDX=$((IDX + 1))
|
|
if [ "$IDX" -gt "$SESSIONS" ]; then break; fi
|
|
|
|
echo " Spawning session for issue #$ISSUE ($IDX/$SESSIONS)..."
|
|
# Run from the project dir so AO auto-detects the project
|
|
SESSION_LINE=$(cd "$PROJECT_DIR" && ao spawn "$ISSUE" 2>&1 | grep "^SESSION=" || true)
|
|
SESSION_ID="${SESSION_LINE#SESSION=}"
|
|
if [ -n "$SESSION_ID" ]; then
|
|
SPAWNED_IDS+=("$SESSION_ID")
|
|
echo " ✓ $SESSION_ID"
|
|
else
|
|
echo " ✗ Failed to spawn for issue #$ISSUE"
|
|
fi
|
|
sleep 2 # stagger spawns slightly
|
|
done
|
|
|
|
echo ""
|
|
echo " Spawned ${#SPAWNED_IDS[@]} sessions: ${SPAWNED_IDS[*]}"
|
|
|
|
# --- Step 4: Monitor until done or timeout ---
|
|
echo ""
|
|
echo "[4/5] Monitoring sessions (timeout: ${TIMEOUT_MIN}m)..."
|
|
echo " Checking every 30s for terminal states..."
|
|
|
|
START_TIME=$(date +%s)
|
|
END_TIME=$((START_TIME + TIMEOUT_MIN * 60))
|
|
CHECK_INTERVAL=30
|
|
|
|
while true; do
|
|
NOW=$(date +%s)
|
|
ELAPSED=$(( (NOW - START_TIME) / 60 ))
|
|
|
|
if [ "$NOW" -ge "$END_TIME" ]; then
|
|
echo ""
|
|
echo " ⏰ Timeout reached (${TIMEOUT_MIN}m). Stopping monitor."
|
|
break
|
|
fi
|
|
|
|
# Check session statuses
|
|
DONE_COUNT=0
|
|
ACTIVE_COUNT=0
|
|
STATUS_SUMMARY=""
|
|
|
|
for SID in "${SPAWNED_IDS[@]}"; do
|
|
# Read status from session metadata
|
|
STATUS=$(cd "$PROJECT_DIR" && ao status 2>/dev/null | grep "$SID" | awk '{print $NF}' || echo "unknown")
|
|
STATUS_SUMMARY="$STATUS_SUMMARY $SID=$STATUS"
|
|
|
|
case "$STATUS" in
|
|
merged|done|killed|stuck|terminated)
|
|
DONE_COUNT=$((DONE_COUNT + 1))
|
|
;;
|
|
*)
|
|
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
|
;;
|
|
esac
|
|
done
|
|
|
|
echo " [${ELAPSED}m] Active: $ACTIVE_COUNT | Done: $DONE_COUNT / ${#SPAWNED_IDS[@]}"
|
|
|
|
if [ "$DONE_COUNT" -ge "${#SPAWNED_IDS[@]}" ]; then
|
|
echo ""
|
|
echo " ✓ All sessions reached terminal state!"
|
|
break
|
|
fi
|
|
|
|
sleep "$CHECK_INTERVAL"
|
|
done
|
|
|
|
# --- Step 5: Capture rate limit after + produce summary ---
|
|
echo ""
|
|
echo "[5/5] Capturing results..."
|
|
|
|
RATE_AFTER=$(gh api rate_limit 2>/dev/null)
|
|
GRAPHQL_AFTER=$(echo "$RATE_AFTER" | python3 -c "import sys,json; r=json.load(sys.stdin)['resources']['graphql']; print(f\"remaining={r['remaining']} used={r['used']} limit={r['limit']}\")")
|
|
CORE_AFTER=$(echo "$RATE_AFTER" | python3 -c "import sys,json; r=json.load(sys.stdin)['resources']['core']; print(f\"remaining={r['remaining']} used={r['used']} limit={r['limit']}\")")
|
|
|
|
TOTAL_TIME=$(( ($(date +%s) - START_TIME) / 60 ))
|
|
TRACE_ROWS=0
|
|
if [ -f "$TRACE_FILE" ]; then
|
|
TRACE_ROWS=$(wc -l < "$TRACE_FILE" | tr -d ' ')
|
|
fi
|
|
|
|
# Write summary
|
|
cat <<SUMMARY | tee "$SUMMARY_FILE"
|
|
|
|
═══════════════════════════════════════════════════════════
|
|
Real-Agent Benchmark Results
|
|
$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
═══════════════════════════════════════════════════════════
|
|
|
|
Sessions: $SESSIONS (real Claude Code agents)
|
|
Duration: ${TOTAL_TIME}m
|
|
Repo: $REPO
|
|
|
|
Rate Limits:
|
|
GraphQL before: $GRAPHQL_BEFORE
|
|
GraphQL after: $GRAPHQL_AFTER
|
|
Core before: $CORE_BEFORE
|
|
Core after: $CORE_AFTER
|
|
|
|
Trace:
|
|
File: $TRACE_FILE
|
|
Rows: $TRACE_ROWS
|
|
|
|
Session IDs:
|
|
$(for SID in "${SPAWNED_IDS[@]}"; do echo " - $SID"; done)
|
|
|
|
═══════════════════════════════════════════════════════════
|
|
|
|
Next steps:
|
|
1. Analyze the trace:
|
|
node experiments/benchmark.mjs report \\
|
|
--trace $TRACE_FILE \\
|
|
--scenario real-agents.single-repo.$SESSIONS
|
|
|
|
2. Or use Python for detailed analysis:
|
|
python3 -c "
|
|
import json
|
|
rows = [json.loads(l) for l in open('$TRACE_FILE')]
|
|
print(f'Total calls: {len(rows)}')
|
|
ops = {}
|
|
for r in rows:
|
|
op = r.get('operation','?')
|
|
ops[op] = ops.get(op,0) + 1
|
|
for op,n in sorted(ops.items(), key=lambda x:-x[1]):
|
|
print(f' {op}: {n}')
|
|
"
|
|
SUMMARY
|
|
|
|
echo ""
|
|
echo "Done!"
|