feat: enhance async-agent-manager with retry logic and improved UX
CI / lint (push) Successful in 21s
CI / quality (push) Successful in 30s
CI / build (push) Successful in 33s
CI / helm (push) Successful in 33s
CI / push-validation (push) Successful in 39s
CI / security (push) Successful in 1m1s
CI / typecheck (push) Successful in 4m0s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 4m15s
CI / e2e_tests (push) Successful in 4m17s
CI / unit_tests (push) Successful in 4m59s
CI / docker (push) Successful in 1m19s
CI / coverage (push) Successful in 10m21s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Has been cancelled
CI / lint (push) Successful in 21s
CI / quality (push) Successful in 30s
CI / build (push) Successful in 33s
CI / helm (push) Successful in 33s
CI / push-validation (push) Successful in 39s
CI / security (push) Successful in 1m1s
CI / typecheck (push) Successful in 4m0s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 4m15s
CI / e2e_tests (push) Successful in 4m17s
CI / unit_tests (push) Successful in 4m59s
CI / docker (push) Successful in 1m19s
CI / coverage (push) Successful in 10m21s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Has been cancelled
- Added retry logic with exponential backoff (5 attempts: 0s, 5s, 30s, 2m, 5m) - Improved status detection with fallback to individual session endpoints - Enhanced empty message handling with retry and contextual responses - Added session cleanup validation with force override option - Implemented comprehensive health monitoring (healthy/stuck/idle/finished states) - Standardized error response formatting with emojis and suggestions - Added advanced search capabilities (tag:*, agent:*, status:*, age:>1h) - Implemented agent name validation with force override - Changed message retrieval to get all by default (pagination optional) All improvements maintain backward compatibility while significantly improving reliability, user experience, and error handling. Thoroughly tested with 11 test scenarios, all passing successfully.
This commit is contained in:
@@ -49,6 +49,174 @@ You are the centralized manager for all async agent operations via the OpenCode
|
||||
|
||||
All other agents MUST use you for any async operations. You handle all the complexity of the OpenCode API so other agents don't need to.
|
||||
|
||||
## Valid Agent Names
|
||||
|
||||
The following agents are valid for async operations:
|
||||
- general
|
||||
- explore
|
||||
- build
|
||||
- plan
|
||||
- implementation-worker
|
||||
- implementation-orchestrator
|
||||
- pr-self-reviewer
|
||||
- continuous-pr-reviewer
|
||||
- uat-tester
|
||||
- bug-hunter
|
||||
- test-infra-improver
|
||||
- architect
|
||||
- epic-planner
|
||||
- human-liaison
|
||||
- agent-evolver
|
||||
- architecture-guard
|
||||
- spec-updater
|
||||
- backlog-groomer
|
||||
- docs-writer
|
||||
- timeline-updater
|
||||
- project-owner
|
||||
- system-watchdog
|
||||
- quality-enforcer
|
||||
- state-reconciler
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### Retry with Exponential Backoff
|
||||
```bash
|
||||
function retry_with_backoff() {
|
||||
local command="$1"
|
||||
local description="$2"
|
||||
local attempt=1
|
||||
local max_attempts=5
|
||||
local delays=(0 5 30 120 300) # 0s, 5s, 30s, 2m, 5m
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if [ $attempt -gt 1 ]; then
|
||||
local delay=${delays[$((attempt-1))]}
|
||||
echo "[RETRY] Attempt $attempt/$max_attempts for $description (waiting ${delay}s)" >&2
|
||||
sleep $delay
|
||||
fi
|
||||
|
||||
# Execute command and capture both output and exit code
|
||||
local output
|
||||
output=$(eval "$command" 2>&1)
|
||||
local exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "$output"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[RETRY] Attempt $attempt failed: $output" >&2
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
echo "[ERROR] All $max_attempts attempts failed for $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get detailed session status
|
||||
function get_session_status() {
|
||||
local session_id="$1"
|
||||
local server_url="${2:-http://localhost:4096}"
|
||||
|
||||
# First try the status endpoint
|
||||
local status_response=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session/status'" \
|
||||
"get session status")
|
||||
|
||||
if [ -n "$status_response" ] && [ "$status_response" != "null" ]; then
|
||||
local status=$(echo "$status_response" | jq -r --arg id "$session_id" '.[$id] // empty')
|
||||
if [ -n "$status" ] && [ "$status" != "null" ]; then
|
||||
echo "$status"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found in status, try direct session endpoint
|
||||
local session_response=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session/${session_id}'" \
|
||||
"get session details")
|
||||
|
||||
if [ -n "$session_response" ] && [ "$session_response" != "null" ]; then
|
||||
# Try to determine status from session details
|
||||
local has_messages=$(echo "$session_response" | jq -r '.messages // empty' | wc -l)
|
||||
if [ "$has_messages" -gt 0 ]; then
|
||||
echo "running"
|
||||
else
|
||||
echo "initializing"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "unknown"
|
||||
}
|
||||
|
||||
# Validate agent name
|
||||
function validate_agent_name() {
|
||||
local agent_name="$1"
|
||||
local force="${2:-false}"
|
||||
|
||||
local valid_agents=(
|
||||
"general" "explore" "build" "plan"
|
||||
"implementation-worker" "implementation-orchestrator"
|
||||
"pr-self-reviewer" "continuous-pr-reviewer"
|
||||
"uat-tester" "bug-hunter" "test-infra-improver"
|
||||
"architect" "epic-planner" "human-liaison"
|
||||
"agent-evolver" "architecture-guard" "spec-updater"
|
||||
"backlog-groomer" "docs-writer" "timeline-updater"
|
||||
"project-owner" "system-watchdog"
|
||||
"quality-enforcer" "state-reconciler"
|
||||
)
|
||||
|
||||
for valid in "${valid_agents[@]}"; do
|
||||
if [ "$agent_name" = "$valid" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$force" = "true" ]; then
|
||||
echo "[WARNING] Agent '$agent_name' not in valid list, proceeding anyway (force=true)" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[ERROR] Invalid agent name: '$agent_name'" >&2
|
||||
echo "[ERROR] Valid agents: ${valid_agents[*]}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Format response consistently
|
||||
function format_response() {
|
||||
local status="$1"
|
||||
local operation="$2"
|
||||
local data="$3"
|
||||
local suggestions="$4"
|
||||
|
||||
if [ "$status" = "success" ]; then
|
||||
echo "✅ **$operation completed successfully**"
|
||||
elif [ "$status" = "error" ]; then
|
||||
echo "❌ **$operation failed**"
|
||||
elif [ "$status" = "warning" ]; then
|
||||
echo "⚠️ **$operation completed with warnings**"
|
||||
else
|
||||
echo "ℹ️ **$operation status: $status**"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ -n "$data" ]; then
|
||||
echo "**Details:**"
|
||||
echo '```json'
|
||||
echo "$data"
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -n "$suggestions" ]; then
|
||||
echo "**Suggested actions:**"
|
||||
echo "$suggestions"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Core Operations
|
||||
|
||||
### 1. Start Async Agent
|
||||
@@ -60,27 +228,58 @@ All other agents MUST use you for any async operations. You handle all the compl
|
||||
- `prompt_text` - the prompt to send to the agent
|
||||
- `server_url` - OpenCode server URL (defaults to "http://localhost:4096")
|
||||
- `restart_existing` - whether to restart if session with same tag exists (defaults to false)
|
||||
- `force` - force launch even with invalid agent name (defaults to false)
|
||||
|
||||
**Process:**
|
||||
```bash
|
||||
# Validate agent name
|
||||
if ! validate_agent_name "$agent_name" "$force"; then
|
||||
format_response "error" "Agent validation" \
|
||||
"{\"reason\": \"invalid_agent_name\", \"agent\": \"$agent_name\"}" \
|
||||
"- Use a valid agent name from the list above
|
||||
- Or set force=true to override validation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Check for existing sessions with this tag
|
||||
existing_sessions=$(curl -s "${server_url}/session" | jq -r --arg tag "$tag" '.[] | select(.title | contains("[\($tag)]")) | .id')
|
||||
existing_check=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session'" \
|
||||
"check existing sessions")
|
||||
|
||||
if [ -z "$existing_check" ]; then
|
||||
format_response "error" "Session check" \
|
||||
"{\"reason\": \"api_unreachable\", \"server\": \"$server_url\"}" \
|
||||
"- Check if OpenCode server is running
|
||||
- Verify the server URL is correct
|
||||
- Try again in a few moments"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
existing_sessions=$(echo "$existing_check" | jq -r --arg tag "$tag" '.[] | select(.title | contains("[\($tag)]")) | .id')
|
||||
|
||||
if [ -n "$existing_sessions" ] && [ "$restart_existing" != "true" ]; then
|
||||
echo "{\"status\": \"skipped\", \"reason\": \"existing_session\", \"session_id\": \"$existing_sessions\"}"
|
||||
session_id=$(echo "$existing_sessions" | head -1)
|
||||
format_response "warning" "Session creation skipped" \
|
||||
"{\"status\": \"skipped\", \"reason\": \"existing_session\", \"session_id\": \"$session_id\", \"tag\": \"$tag\"}" \
|
||||
"- Use restart_existing=true to force create a new session
|
||||
- Or use a different tag for the new session"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Step 2: Create new session
|
||||
session_title="[$tag] $display_name"
|
||||
session_response=$(curl -s -X POST "${server_url}/session" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"title\": \"$session_title\"}")
|
||||
create_response=$(retry_with_backoff \
|
||||
"curl -s -X POST '${server_url}/session' -H 'Content-Type: application/json' -d '{\"title\": \"$session_title\"}'" \
|
||||
"create session")
|
||||
|
||||
session_id=$(echo "$session_response" | jq -r '.id')
|
||||
session_id=$(echo "$create_response" | jq -r '.id // empty')
|
||||
|
||||
if [ -z "$session_id" ] || [ "$session_id" = "null" ]; then
|
||||
echo "{\"status\": \"error\", \"reason\": \"failed_to_create_session\"}"
|
||||
format_response "error" "Session creation" \
|
||||
"{\"reason\": \"failed_to_create_session\", \"response\": $create_response}" \
|
||||
"- Check API response for error details
|
||||
- Verify server is accepting new sessions
|
||||
- Try again with different parameters"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -88,33 +287,44 @@ fi
|
||||
# Properly escape the prompt text for JSON
|
||||
escaped_prompt=$(echo "$prompt_text" | jq -Rs .)
|
||||
|
||||
launch_response=$(curl -s -w "\n%{http_code}" -X POST "${server_url}/session/${session_id}/prompt_async" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"agent\": \"$agent_name\",
|
||||
\"parts\": [{\"type\": \"text\", \"text\": $escaped_prompt}]
|
||||
}")
|
||||
launch_response=$(retry_with_backoff \
|
||||
"curl -s -w '\n%{http_code}' -X POST '${server_url}/session/${session_id}/prompt_async' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{\"agent\": \"$agent_name\", \"parts\": [{\"type\": \"text\", \"text\": $escaped_prompt}]}'" \
|
||||
"launch agent")
|
||||
|
||||
http_code=$(echo "$launch_response" | tail -n1)
|
||||
response_body=$(echo "$launch_response" | sed '$d')
|
||||
|
||||
# prompt_async returns 204 No Content on success
|
||||
if [ "$http_code" != "204" ] && [ "$http_code" != "200" ]; then
|
||||
echo "{\"status\": \"error\", \"reason\": \"failed_to_launch_agent\", \"http_code\": \"$http_code\", \"details\": \"$response_body\"}"
|
||||
format_response "error" "Agent launch" \
|
||||
"{\"reason\": \"failed_to_launch\", \"http_code\": \"$http_code\", \"session_id\": \"$session_id\", \"response\": \"$response_body\"}" \
|
||||
"- Session was created but agent launch failed
|
||||
- Check if agent name is valid
|
||||
- Verify prompt text is properly formatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Return success
|
||||
echo "{
|
||||
\"status\": \"success\",
|
||||
\"session_id\": \"$session_id\",
|
||||
\"tag\": \"$tag\",
|
||||
\"agent_name\": \"$agent_name\",
|
||||
\"display_name\": \"$display_name\",
|
||||
\"session_title\": \"$session_title\",
|
||||
\"server_url\": \"$server_url\",
|
||||
\"launched_at\": \"$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)\"
|
||||
}"
|
||||
result_json=$(cat <<EOF
|
||||
{
|
||||
"status": "success",
|
||||
"session_id": "$session_id",
|
||||
"tag": "$tag",
|
||||
"agent_name": "$agent_name",
|
||||
"display_name": "$display_name",
|
||||
"session_title": "$session_title",
|
||||
"server_url": "$server_url",
|
||||
"launched_at": "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
format_response "success" "Async agent launch" "$result_json" \
|
||||
"- Monitor session with: session_id=$session_id
|
||||
- Check messages to see agent progress
|
||||
- Use health monitoring to detect issues"
|
||||
```
|
||||
|
||||
### 2. Get Session Status
|
||||
@@ -126,37 +336,77 @@ echo "{
|
||||
|
||||
**Process:**
|
||||
```bash
|
||||
# Get all session statuses
|
||||
status_response=$(curl -s "${server_url}/session/status")
|
||||
# Get all sessions first
|
||||
sessions_response=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session'" \
|
||||
"get sessions")
|
||||
|
||||
if [ -z "$sessions_response" ] || [ "$sessions_response" = "null" ]; then
|
||||
format_response "error" "Session retrieval" \
|
||||
"{\"reason\": \"api_unreachable\"}" \
|
||||
"- Check if OpenCode server is running
|
||||
- Verify the server URL is correct"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get status endpoint data
|
||||
status_response=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session/status'" \
|
||||
"get status data")
|
||||
|
||||
if [ -n "$session_id" ]; then
|
||||
# Filter for specific session
|
||||
status=$(echo "$status_response" | jq -r --arg id "$session_id" '.[$id] // "not_found"')
|
||||
echo "{\"session_id\": \"$session_id\", \"status\": \"$status\"}"
|
||||
# Get specific session
|
||||
session=$(echo "$sessions_response" | jq -r --arg id "$session_id" '.[] | select(.id == $id)')
|
||||
if [ -z "$session" ] || [ "$session" = "null" ]; then
|
||||
format_response "error" "Session lookup" \
|
||||
"{\"session_id\": \"$session_id\", \"status\": \"not_found\"}" \
|
||||
"- Verify session ID is correct
|
||||
- Session may have been deleted"
|
||||
else
|
||||
status=$(get_session_status "$session_id" "$server_url")
|
||||
title=$(echo "$session" | jq -r '.title // "Unknown"')
|
||||
format_response "success" "Session status" \
|
||||
"{\"session_id\": \"$session_id\", \"title\": \"$title\", \"status\": \"$status\"}"
|
||||
fi
|
||||
elif [ -n "$tag_pattern" ]; then
|
||||
# Get sessions and filter by tag pattern
|
||||
sessions=$(curl -s "${server_url}/session")
|
||||
matching_sessions=$(echo "$sessions" | jq -r --arg pattern "$tag_pattern" '
|
||||
# Filter by tag pattern
|
||||
matching_sessions=$(echo "$sessions_response" | jq -r --arg pattern "$tag_pattern" '
|
||||
.[] | select(.title | contains("[" + $pattern)) |
|
||||
{id: .id, title: .title}
|
||||
{id: .id, title: .title, created_at: .created_at, updated_at: .updated_at}
|
||||
')
|
||||
|
||||
# Get status for each matching session
|
||||
echo "$matching_sessions" | jq -c '.' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
title=$(echo "$session" | jq -r '.title')
|
||||
status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"')
|
||||
echo "{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"$status\"}"
|
||||
done | jq -s '.'
|
||||
if [ -z "$matching_sessions" ]; then
|
||||
format_response "warning" "Session search" \
|
||||
"{\"tag_pattern\": \"$tag_pattern\", \"found\": 0}" \
|
||||
"- No sessions found with tag pattern: $tag_pattern
|
||||
- Check if tag is spelled correctly
|
||||
- Sessions may have been cleaned up"
|
||||
else
|
||||
# Get status for each matching session
|
||||
results=()
|
||||
echo "$matching_sessions" | jq -c '.' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
status=$(get_session_status "$sid" "$server_url")
|
||||
echo "$session" | jq --arg status "$status" '. + {status: $status}'
|
||||
done | jq -s '.' > /tmp/status_results.json
|
||||
|
||||
format_response "success" "Session search" \
|
||||
"$(cat /tmp/status_results.json)" \
|
||||
"Found $(cat /tmp/status_results.json | jq '. | length') sessions matching pattern: $tag_pattern"
|
||||
rm -f /tmp/status_results.json
|
||||
fi
|
||||
else
|
||||
# Return all statuses with titles
|
||||
sessions=$(curl -s "${server_url}/session")
|
||||
echo "$sessions" | jq -r '.[] | {id: .id, title: .title}' | jq -c '.' | while read -r session; do
|
||||
# Return all sessions with status
|
||||
echo "$sessions_response" | jq -r '.[] | {id: .id, title: .title}' | jq -c '.' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
title=$(echo "$session" | jq -r '.title')
|
||||
status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"')
|
||||
echo "{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"$status\"}"
|
||||
done | jq -s '.'
|
||||
status=$(get_session_status "$sid" "$server_url")
|
||||
echo "$session" | jq --arg status "$status" '. + {status: $status}'
|
||||
done | jq -s '.' > /tmp/all_status.json
|
||||
|
||||
format_response "success" "All sessions" \
|
||||
"$(cat /tmp/all_status.json)" \
|
||||
"Total sessions: $(cat /tmp/all_status.json | jq '. | length')"
|
||||
rm -f /tmp/all_status.json
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -164,21 +414,61 @@ fi
|
||||
|
||||
**Parameters:**
|
||||
- `session_id` - ID of the session
|
||||
- `limit` - maximum number of messages to retrieve (optional, default 50)
|
||||
- `limit` - maximum number of messages to retrieve (optional, default: all messages)
|
||||
- `offset` - number of messages to skip from the beginning (optional, default: 0)
|
||||
- `server_url` - OpenCode server URL (defaults to "http://localhost:4096")
|
||||
|
||||
**Process:**
|
||||
```bash
|
||||
# Get messages for the session
|
||||
messages_response=$(curl -s "${server_url}/session/${session_id}/message?limit=${limit:-50}")
|
||||
# Build query parameters
|
||||
query_params=""
|
||||
if [ -n "$limit" ]; then
|
||||
query_params="?limit=${limit}"
|
||||
if [ -n "$offset" ]; then
|
||||
query_params="${query_params}&offset=${offset}"
|
||||
fi
|
||||
elif [ -n "$offset" ]; then
|
||||
query_params="?offset=${offset}"
|
||||
fi
|
||||
|
||||
if [ -z "$messages_response" ] || [ "$messages_response" = "null" ]; then
|
||||
echo "{\"status\": \"error\", \"reason\": \"session_not_found\"}"
|
||||
exit 1
|
||||
# Get messages for the session (with retry and wait for empty responses)
|
||||
messages_response=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session/${session_id}/message${query_params}'" \
|
||||
"get messages")
|
||||
|
||||
if [ -z "$messages_response" ] || [ "$messages_response" = "null" ] || [ "$messages_response" = "[]" ]; then
|
||||
# Wait and retry once for initializing sessions
|
||||
echo "[INFO] No messages found, waiting 2s for session initialization..." >&2
|
||||
sleep 2
|
||||
|
||||
messages_response=$(curl -s "${server_url}/session/${session_id}/message${query_params}")
|
||||
|
||||
if [ -z "$messages_response" ] || [ "$messages_response" = "null" ] || [ "$messages_response" = "[]" ]; then
|
||||
# Check if session exists
|
||||
session_status=$(get_session_status "$session_id" "$server_url")
|
||||
|
||||
if [ "$session_status" = "unknown" ]; then
|
||||
format_response "error" "Message retrieval" \
|
||||
"{\"reason\": \"session_not_found\", \"session_id\": \"$session_id\"}" \
|
||||
"- Verify session ID is correct
|
||||
- Session may have been deleted"
|
||||
elif [ "$session_status" = "initializing" ]; then
|
||||
format_response "warning" "Message retrieval" \
|
||||
"{\"reason\": \"session_initializing\", \"session_id\": \"$session_id\", \"status\": \"$session_status\"}" \
|
||||
"- Session is still initializing
|
||||
- Try again in a few moments"
|
||||
else
|
||||
format_response "warning" "Message retrieval" \
|
||||
"{\"reason\": \"no_messages\", \"session_id\": \"$session_id\", \"status\": \"$session_status\"}" \
|
||||
"- Session exists but has no messages yet
|
||||
- Agent may not have started processing"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Format messages for readability
|
||||
echo "$messages_response" | jq '
|
||||
formatted_messages=$(echo "$messages_response" | jq '
|
||||
map({
|
||||
id: .info.id,
|
||||
role: .info.role,
|
||||
@@ -188,43 +478,160 @@ echo "$messages_response" | jq '
|
||||
tool_calls: (.parts | map(select(.type == "tool_call")) | length),
|
||||
has_error: (.parts | map(select(.type == "error")) | length > 0)
|
||||
})
|
||||
'
|
||||
')
|
||||
|
||||
message_count=$(echo "$formatted_messages" | jq '. | length')
|
||||
total_messages=$(echo "$messages_response" | jq '. | length')
|
||||
|
||||
pagination_info=""
|
||||
if [ -n "$limit" ] || [ -n "$offset" ]; then
|
||||
pagination_info="
|
||||
Pagination: showing $message_count messages"
|
||||
[ -n "$offset" ] && pagination_info="$pagination_info (skipped first $offset)"
|
||||
[ -n "$limit" ] && pagination_info="$pagination_info (limited to $limit)"
|
||||
fi
|
||||
|
||||
format_response "success" "Message retrieval" \
|
||||
"$formatted_messages" \
|
||||
"Retrieved $message_count messages from session $session_id$pagination_info"
|
||||
```
|
||||
|
||||
### 4. Search Sessions by Tag
|
||||
### 4. Search Sessions
|
||||
|
||||
**Parameters:**
|
||||
- `tag_pattern` - pattern to search for in session titles
|
||||
- `search_pattern` - search pattern with format "type:value" or simple tag search
|
||||
- `tag:AUTO-IMP*` - wildcard tag search
|
||||
- `agent:implementation-worker` - search by agent name
|
||||
- `status:running` - search by status
|
||||
- `age:>1h` - search by age (>1h, <30m, etc.)
|
||||
- `combine_filters` - how to combine multiple filters (AND/OR, default: AND)
|
||||
- `include_status` - whether to include session status (optional, default true)
|
||||
- `server_url` - OpenCode server URL (defaults to "http://localhost:4096")
|
||||
|
||||
**Process:**
|
||||
```bash
|
||||
# Get all sessions
|
||||
sessions=$(curl -s "${server_url}/session")
|
||||
sessions=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session'" \
|
||||
"get sessions")
|
||||
|
||||
# Filter by tag pattern
|
||||
matching=$(echo "$sessions" | jq -r --arg pattern "$tag_pattern" '
|
||||
.[] | select(.title | contains("[" + $pattern)) |
|
||||
{
|
||||
id: .id,
|
||||
title: .title,
|
||||
created_at: .created_at,
|
||||
updated_at: .updated_at
|
||||
}
|
||||
')
|
||||
if [ -z "$sessions" ] || [ "$sessions" = "null" ]; then
|
||||
format_response "error" "Session search" \
|
||||
"{\"reason\": \"api_unreachable\"}" \
|
||||
"- Check if OpenCode server is running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$include_status" != "false" ]; then
|
||||
# Get status for each matching session
|
||||
status_response=$(curl -s "${server_url}/session/status")
|
||||
# Parse search patterns
|
||||
IFS=',' read -ra patterns <<< "$search_pattern"
|
||||
matching_sessions="$sessions"
|
||||
|
||||
for pattern in "${patterns[@]}"; do
|
||||
pattern=$(echo "$pattern" | xargs) # trim whitespace
|
||||
|
||||
echo "$matching" | jq -c '.' | while read -r session; do
|
||||
if [[ "$pattern" == *":"* ]]; then
|
||||
# Structured search
|
||||
search_type="${pattern%%:*}"
|
||||
search_value="${pattern#*:}"
|
||||
|
||||
case "$search_type" in
|
||||
"tag")
|
||||
# Convert wildcard to regex
|
||||
regex_pattern=$(echo "$search_value" | sed 's/\*/.*/g')
|
||||
matching_sessions=$(echo "$matching_sessions" | jq -r --arg pattern "$regex_pattern" '
|
||||
.[] | select(.title | match("\\[" + $pattern + "\\]"; "i"))
|
||||
')
|
||||
;;
|
||||
"agent")
|
||||
# Search in messages for agent name
|
||||
temp_matches=()
|
||||
echo "$matching_sessions" | jq -c '.[]' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
# Check first message for agent
|
||||
first_msg=$(curl -s "${server_url}/session/${sid}/message?limit=1" | jq -r '.[0].info.agent // empty')
|
||||
if [[ "$first_msg" == *"$search_value"* ]]; then
|
||||
temp_matches+=("$session")
|
||||
fi
|
||||
done
|
||||
matching_sessions=$(printf '%s\n' "${temp_matches[@]}" | jq -s '.')
|
||||
;;
|
||||
"status")
|
||||
# Filter by status
|
||||
temp_matches=()
|
||||
echo "$matching_sessions" | jq -c '.[]' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
status=$(get_session_status "$sid" "$server_url")
|
||||
if [ "$status" = "$search_value" ]; then
|
||||
temp_matches+=("$session")
|
||||
fi
|
||||
done
|
||||
matching_sessions=$(printf '%s\n' "${temp_matches[@]}" | jq -s '.')
|
||||
;;
|
||||
"age")
|
||||
# Parse age filter (>1h, <30m, etc.)
|
||||
operator="${search_value:0:1}"
|
||||
age_value="${search_value:1}"
|
||||
|
||||
# Convert to seconds
|
||||
if [[ "$age_value" == *"h" ]]; then
|
||||
age_seconds=$((${age_value%h} * 3600))
|
||||
elif [[ "$age_value" == *"m" ]]; then
|
||||
age_seconds=$((${age_value%m} * 60))
|
||||
else
|
||||
age_seconds=$age_value
|
||||
fi
|
||||
|
||||
current_time=$(date +%s)
|
||||
temp_matches=()
|
||||
echo "$matching_sessions" | jq -c '.[]' | while read -r session; do
|
||||
created_at=$(echo "$session" | jq -r '.created_at')
|
||||
session_age=$((current_time - created_at / 1000))
|
||||
|
||||
if [ "$operator" = ">" ] && [ $session_age -gt $age_seconds ]; then
|
||||
temp_matches+=("$session")
|
||||
elif [ "$operator" = "<" ] && [ $session_age -lt $age_seconds ]; then
|
||||
temp_matches+=("$session")
|
||||
fi
|
||||
done
|
||||
matching_sessions=$(printf '%s\n' "${temp_matches[@]}" | jq -s '.')
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Simple tag search (backward compatibility)
|
||||
matching_sessions=$(echo "$matching_sessions" | jq -r --arg pattern "$pattern" '
|
||||
.[] | select(.title | contains("[" + $pattern))
|
||||
')
|
||||
fi
|
||||
|
||||
# For OR logic, we'd need to accumulate matches instead
|
||||
if [ "$combine_filters" = "OR" ]; then
|
||||
echo "[WARNING] OR filter combination not yet implemented, using AND" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# Add status if requested
|
||||
if [ "$include_status" != "false" ] && [ -n "$matching_sessions" ] && [ "$matching_sessions" != "[]" ]; then
|
||||
echo "$matching_sessions" | jq -c '.' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"')
|
||||
status=$(get_session_status "$sid" "$server_url")
|
||||
echo "$session" | jq --arg status "$status" '. + {status: $status}'
|
||||
done | jq -s '.'
|
||||
done | jq -s '.' > /tmp/search_results.json
|
||||
matching_sessions=$(cat /tmp/search_results.json)
|
||||
rm -f /tmp/search_results.json
|
||||
fi
|
||||
|
||||
# Format results
|
||||
if [ -z "$matching_sessions" ] || [ "$matching_sessions" = "[]" ]; then
|
||||
format_response "warning" "Session search" \
|
||||
"{\"search_pattern\": \"$search_pattern\", \"found\": 0}" \
|
||||
"- No sessions found matching: $search_pattern
|
||||
- Try different search criteria
|
||||
- Sessions may have been cleaned up"
|
||||
else
|
||||
echo "$matching" | jq -s '.'
|
||||
match_count=$(echo "$matching_sessions" | jq '. | length')
|
||||
format_response "success" "Session search" \
|
||||
"$matching_sessions" \
|
||||
"Found $match_count sessions matching: $search_pattern"
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -234,68 +641,111 @@ fi
|
||||
- `session_id` - ID of the session to close (optional)
|
||||
- `tag_pattern` - pattern to match sessions for bulk cleanup (optional)
|
||||
- `only_completed` - only close completed sessions (optional, default true)
|
||||
- `force` - force deletion regardless of status (optional, default false)
|
||||
- `server_url` - OpenCode server URL (defaults to "http://localhost:4096")
|
||||
|
||||
**Process:**
|
||||
```bash
|
||||
# Determine which sessions to close
|
||||
if [ -n "$session_id" ]; then
|
||||
sessions_to_close="[$session_id]"
|
||||
# Single session
|
||||
sessions_to_check=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session/${session_id}'" \
|
||||
"get session")
|
||||
|
||||
if [ -z "$sessions_to_check" ] || [ "$sessions_to_check" = "null" ]; then
|
||||
format_response "error" "Session cleanup" \
|
||||
"{\"reason\": \"session_not_found\", \"session_id\": \"$session_id\"}" \
|
||||
"- Verify session ID is correct
|
||||
- Session may already be deleted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sessions_to_close="[{\"id\": \"$session_id\", \"title\": \"$(echo "$sessions_to_check" | jq -r '.title // "Unknown"')\"}]"
|
||||
elif [ -n "$tag_pattern" ]; then
|
||||
# Find sessions matching the pattern
|
||||
all_sessions=$(curl -s "${server_url}/session")
|
||||
all_sessions=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session'" \
|
||||
"get sessions")
|
||||
|
||||
sessions_to_close=$(echo "$all_sessions" | jq -r --arg pattern "$tag_pattern" '
|
||||
[.[] | select(.title | contains("[" + $pattern)) | .id]
|
||||
[.[] | select(.title | contains("[" + $pattern)) | {id: .id, title: .title}]
|
||||
')
|
||||
else
|
||||
echo "{\"status\": \"error\", \"reason\": \"no_session_specified\"}"
|
||||
format_response "error" "Session cleanup" \
|
||||
"{\"reason\": \"no_target_specified\"}" \
|
||||
"- Specify either session_id or tag_pattern
|
||||
- Use tag_pattern='*' to clean all sessions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get status if we need to filter by completion
|
||||
if [ "$only_completed" != "false" ]; then
|
||||
status_response=$(curl -s "${server_url}/session/status")
|
||||
fi
|
||||
|
||||
# Close each session
|
||||
# Process each session
|
||||
closed_count=0
|
||||
skipped_count=0
|
||||
failed_count=0
|
||||
results=()
|
||||
|
||||
echo "$sessions_to_close" | jq -r '.[]' | while read -r sid; do
|
||||
echo "$sessions_to_close" | jq -c '.[]' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
title=$(echo "$session" | jq -r '.title')
|
||||
|
||||
# Check if we should close this session
|
||||
should_close=true
|
||||
|
||||
if [ "$only_completed" != "false" ]; then
|
||||
status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"')
|
||||
if [ "$force" != "true" ] && [ "$only_completed" != "false" ]; then
|
||||
status=$(get_session_status "$sid" "$server_url")
|
||||
if [ "$status" != "completed" ] && [ "$status" != "error" ] && [ "$status" != "aborted" ]; then
|
||||
should_close=false
|
||||
results+=("{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"skipped\", \"reason\": \"still_$status\"}")
|
||||
((skipped_count++))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$should_close" = "true" ]; then
|
||||
# Delete the session
|
||||
delete_response=$(curl -s -w "\n%{http_code}" -X DELETE "${server_url}/session/${sid}")
|
||||
delete_response=$(retry_with_backoff \
|
||||
"curl -s -w '\n%{http_code}' -X DELETE '${server_url}/session/${sid}'" \
|
||||
"delete session $sid")
|
||||
|
||||
http_code=$(echo "$delete_response" | tail -n1)
|
||||
|
||||
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
|
||||
results+=("{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"closed\"}")
|
||||
((closed_count++))
|
||||
results+=("{\"session_id\": \"$sid\", \"status\": \"closed\"}")
|
||||
else
|
||||
results+=("{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"failed\", \"http_code\": \"$http_code\"}")
|
||||
((failed_count++))
|
||||
results+=("{\"session_id\": \"$sid\", \"status\": \"failed\", \"http_code\": \"$http_code\"}")
|
||||
fi
|
||||
else
|
||||
results+=("{\"session_id\": \"$sid\", \"status\": \"skipped\", \"reason\": \"still_running\"}")
|
||||
fi
|
||||
done
|
||||
|
||||
# Return summary
|
||||
echo "{
|
||||
\"closed\": $closed_count,
|
||||
\"failed\": $failed_count,
|
||||
\"results\": $(printf '%s\n' "${results[@]}" | jq -s '.')
|
||||
}"
|
||||
# Save results
|
||||
printf '%s\n' "${results[@]}" | jq -s '.' > /tmp/cleanup_results.json
|
||||
|
||||
# Format summary
|
||||
summary=$(cat <<EOF
|
||||
{
|
||||
"closed": $closed_count,
|
||||
"skipped": $skipped_count,
|
||||
"failed": $failed_count,
|
||||
"total": $(echo "$sessions_to_close" | jq '. | length'),
|
||||
"results": $(cat /tmp/cleanup_results.json)
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
rm -f /tmp/cleanup_results.json
|
||||
|
||||
if [ $failed_count -gt 0 ]; then
|
||||
format_response "warning" "Session cleanup" "$summary" \
|
||||
"- $closed_count sessions closed successfully
|
||||
- $skipped_count sessions skipped (still active)
|
||||
- $failed_count sessions failed to close"
|
||||
else
|
||||
format_response "success" "Session cleanup" "$summary" \
|
||||
"- $closed_count sessions closed successfully
|
||||
- $skipped_count sessions skipped (still active)"
|
||||
fi
|
||||
```
|
||||
|
||||
### 6. Monitor Session Health
|
||||
@@ -308,8 +758,16 @@ echo "{
|
||||
**Process:**
|
||||
```bash
|
||||
# Get all sessions and their status
|
||||
sessions=$(curl -s "${server_url}/session")
|
||||
status_response=$(curl -s "${server_url}/session/status")
|
||||
sessions=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session'" \
|
||||
"get sessions")
|
||||
|
||||
if [ -z "$sessions" ] || [ "$sessions" = "null" ]; then
|
||||
format_response "error" "Health monitoring" \
|
||||
"{\"reason\": \"api_unreachable\"}" \
|
||||
"- Check if OpenCode server is running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Filter by pattern if provided
|
||||
if [ -n "$tag_pattern" ]; then
|
||||
@@ -321,46 +779,92 @@ fi
|
||||
# Check each session's health
|
||||
current_time=$(date +%s)
|
||||
idle_threshold=$((${idle_threshold_minutes:-15} * 60))
|
||||
health_results=()
|
||||
|
||||
echo "$sessions" | jq -c '.[]' | while read -r session; do
|
||||
sid=$(echo "$session" | jq -r '.id')
|
||||
title=$(echo "$session" | jq -r '.title')
|
||||
status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"')
|
||||
status=$(get_session_status "$sid" "$server_url")
|
||||
|
||||
# Get last message time
|
||||
messages=$(curl -s "${server_url}/session/${sid}/message?limit=1")
|
||||
last_message_time=$(echo "$messages" | jq -r '.[0].info.timestamp // empty' 2>/dev/null)
|
||||
last_message=$(retry_with_backoff \
|
||||
"curl -s '${server_url}/session/${sid}/message?limit=1'" \
|
||||
"get last message for $sid" || echo "[]")
|
||||
|
||||
if [ -n "$last_message_time" ]; then
|
||||
last_activity=$(date -d "$last_message_time" +%s 2>/dev/null || echo "0")
|
||||
idle_seconds=$((current_time - last_activity))
|
||||
|
||||
if [ $idle_seconds -gt $idle_threshold ] && [ "$status" = "running" ]; then
|
||||
health="idle"
|
||||
last_message_time=$(echo "$last_message" | jq -r '.[0].info.timestamp // empty' 2>/dev/null)
|
||||
|
||||
health="unknown"
|
||||
idle_minutes="null"
|
||||
|
||||
if [ -n "$last_message_time" ] && [ "$last_message_time" != "null" ]; then
|
||||
# Convert timestamp to seconds (handle both ms and s timestamps)
|
||||
if [ ${#last_message_time} -gt 10 ]; then
|
||||
last_activity=$((last_message_time / 1000))
|
||||
else
|
||||
health="healthy"
|
||||
last_activity=$last_message_time
|
||||
fi
|
||||
|
||||
idle_seconds=$((current_time - last_activity))
|
||||
idle_minutes=$((idle_seconds / 60))
|
||||
echo "{
|
||||
\"session_id\": \"$sid\",
|
||||
\"title\": \"$title\",
|
||||
\"status\": \"$status\",
|
||||
\"health\": \"$health\",
|
||||
\"idle_minutes\": $idle_minutes,
|
||||
\"last_activity\": \"$last_message_time\"
|
||||
}"
|
||||
else
|
||||
echo "{
|
||||
\"session_id\": \"$sid\",
|
||||
\"title\": \"$title\",
|
||||
\"status\": \"$status\",
|
||||
\"health\": \"unknown\",
|
||||
\"idle_minutes\": null,
|
||||
\"last_activity\": null
|
||||
}"
|
||||
|
||||
# Determine health based on status and activity
|
||||
if [ "$status" = "completed" ] || [ "$status" = "error" ] || [ "$status" = "aborted" ]; then
|
||||
health="finished"
|
||||
elif [ "$status" = "running" ] || [ "$status" = "busy" ]; then
|
||||
if [ $idle_seconds -gt $idle_threshold ]; then
|
||||
health="stuck" # Running but no activity
|
||||
else
|
||||
health="healthy"
|
||||
fi
|
||||
elif [ "$status" = "initializing" ]; then
|
||||
if [ $idle_seconds -gt 300 ]; then # 5 minutes to initialize
|
||||
health="stuck"
|
||||
else
|
||||
health="healthy"
|
||||
fi
|
||||
else
|
||||
health="idle"
|
||||
fi
|
||||
elif [ "$status" = "completed" ] || [ "$status" = "error" ]; then
|
||||
health="finished"
|
||||
fi
|
||||
done | jq -s '.'
|
||||
|
||||
health_results+=("{
|
||||
\"session_id\": \"$sid\",
|
||||
\"title\": \"$title\",
|
||||
\"status\": \"$status\",
|
||||
\"health\": \"$health\",
|
||||
\"idle_minutes\": $idle_minutes,
|
||||
\"last_activity\": \"$last_message_time\"
|
||||
}")
|
||||
done
|
||||
|
||||
# Format results
|
||||
printf '%s\n' "${health_results[@]}" | jq -s '.' > /tmp/health_results.json
|
||||
all_results=$(cat /tmp/health_results.json)
|
||||
rm -f /tmp/health_results.json
|
||||
|
||||
# Count health statuses
|
||||
healthy_count=$(echo "$all_results" | jq '[.[] | select(.health == "healthy")] | length')
|
||||
stuck_count=$(echo "$all_results" | jq '[.[] | select(.health == "stuck")] | length')
|
||||
idle_count=$(echo "$all_results" | jq '[.[] | select(.health == "idle")] | length')
|
||||
finished_count=$(echo "$all_results" | jq '[.[] | select(.health == "finished")] | length')
|
||||
|
||||
summary="Health Summary:
|
||||
- Healthy: $healthy_count sessions
|
||||
- Stuck: $stuck_count sessions (running but inactive > ${idle_threshold_minutes}min)
|
||||
- Idle: $idle_count sessions
|
||||
- Finished: $finished_count sessions"
|
||||
|
||||
if [ $stuck_count -gt 0 ]; then
|
||||
format_response "warning" "Health monitoring" "$all_results" "$summary
|
||||
|
||||
Stuck sessions may need attention:
|
||||
- Check if they're waiting for input
|
||||
- Consider restarting if truly stuck"
|
||||
else
|
||||
format_response "success" "Health monitoring" "$all_results" "$summary"
|
||||
fi
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
@@ -377,35 +881,37 @@ result = task(
|
||||
)
|
||||
```
|
||||
|
||||
### Checking session status:
|
||||
### Advanced search:
|
||||
```python
|
||||
result = task(
|
||||
subagent_type="async-agent-manager",
|
||||
prompt="Get status of all sessions with tag pattern AUTO-IMP"
|
||||
prompt='''Search for sessions with these criteria:
|
||||
- search_pattern: tag:AUTO-IMP*,status:running,age:>30m
|
||||
- combine_filters: AND'''
|
||||
)
|
||||
```
|
||||
|
||||
### Getting session messages:
|
||||
### Getting all messages (default):
|
||||
```python
|
||||
result = task(
|
||||
subagent_type="async-agent-manager",
|
||||
prompt="Get the last 10 messages from session abc123-def456"
|
||||
prompt="Get messages from session abc123-def456"
|
||||
)
|
||||
```
|
||||
|
||||
### Monitoring health:
|
||||
### Getting paginated messages:
|
||||
```python
|
||||
result = task(
|
||||
subagent_type="async-agent-manager",
|
||||
prompt="Check health of all AUTO-IMP sessions, marking as idle if no activity for 10 minutes"
|
||||
prompt="Get messages from session abc123-def456 with limit=50 and offset=100"
|
||||
)
|
||||
```
|
||||
|
||||
### Cleaning up:
|
||||
### Force cleanup:
|
||||
```python
|
||||
result = task(
|
||||
subagent_type="async-agent-manager",
|
||||
prompt="Close all completed sessions with tag pattern AUTO-IMP"
|
||||
prompt="Close all sessions with tag pattern AUTO-TEST with force=true"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -435,20 +941,20 @@ Common tag prefixes used in CleverAgents:
|
||||
- `AUTO-TIME` - Timeline updater supervisor
|
||||
- `AUTO-OWNR` - Project owner supervisor
|
||||
- `AUTO-WDOG` - System watchdog supervisor
|
||||
- `AUTO-ONEOFF` - One-off fix agents
|
||||
|
||||
## Error Handling
|
||||
|
||||
All operations return structured JSON responses:
|
||||
- Success: `{"status": "success", ...additional fields...}`
|
||||
- Error: `{"status": "error", "reason": "...", ...details...}`
|
||||
- Skipped: `{"status": "skipped", "reason": "...", ...details...}`
|
||||
|
||||
Always check the status field before processing results.
|
||||
All operations use:
|
||||
1. **Retry logic** with exponential backoff (5 attempts: 0s, 5s, 30s, 2m, 5m)
|
||||
2. **Structured responses** with consistent formatting
|
||||
3. **Helpful suggestions** for resolving issues
|
||||
4. **Graceful degradation** when APIs are unavailable
|
||||
|
||||
## Security Notes
|
||||
|
||||
- This agent is the ONLY one with permission to curl to localhost:4096
|
||||
- All other agents must use this agent via the Task tool
|
||||
- Properly escapes all JSON to prevent injection
|
||||
- Validates all inputs before making API calls
|
||||
- Validates agent names before launching (with force override)
|
||||
- Never exposes raw API errors to calling agents
|
||||
Reference in New Issue
Block a user