Initial agents skill - instance management with personalities

This commit is contained in:
Azat
2026-02-02 23:20:33 +01:00
commit 53c2fca080
3 changed files with 744 additions and 0 deletions

218
scripts/autorun.sh Normal file
View File

@@ -0,0 +1,218 @@
#!/bin/bash
set -e
SKILLS_DIR="${SKILLS_DIR:-/skills}"
PERSONALITIES_DIR="${PERSONALITIES_DIR:-/personalities}"
WORKSPACES_DIR="${WORKSPACES_DIR:-/workspaces}"
AGENTS_DATA_DIR="${AGENTS_DATA_DIR:-/data/agents}"
# Install dependencies
install_deps() {
local needed=""
command -v socat &>/dev/null || needed="$needed socat"
command -v jq &>/dev/null || needed="$needed jq"
command -v sqlite3 &>/dev/null || needed="$needed sqlite3"
if [ -n "$needed" ]; then
echo "Installing dependencies:$needed"
apt-get update && apt-get install -y $needed
fi
}
# Setup directories
setup_dirs() {
mkdir -p "$PERSONALITIES_DIR"
mkdir -p "$WORKSPACES_DIR"
mkdir -p "$AGENTS_DATA_DIR"
mkdir -p /var/run/agents
echo "Personalities: $PERSONALITIES_DIR"
echo "Workspaces: $WORKSPACES_DIR"
echo "Data: $AGENTS_DATA_DIR"
}
# Detect storage backend
detect_storage() {
if [ -d "$SKILLS_DIR/postgres" ] && command -v psql &>/dev/null; then
echo "postgres"
elif [ -d "$SKILLS_DIR/duckdb" ] && command -v duckdb &>/dev/null; then
echo "duckdb"
else
echo "sqlite"
fi
}
# Initialize SQLite database (fallback)
init_sqlite() {
local db="$AGENTS_DATA_DIR/agents.db"
sqlite3 "$db" << 'SQL'
CREATE TABLE IF NOT EXISTS instances (
id TEXT PRIMARY KEY,
name TEXT,
personality TEXT NOT NULL,
workspace TEXT,
port INTEGER,
pid INTEGER,
status TEXT DEFAULT 'starting',
started_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
instance_id TEXT,
started_at TEXT DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'active',
FOREIGN KEY (instance_id) REFERENCES instances(id)
);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
conversation_id TEXT,
role TEXT,
content TEXT,
tool_calls TEXT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
);
SQL
echo "SQLite initialized: $db"
}
# Initialize DuckDB tables
init_duckdb() {
local db="$AGENTS_DATA_DIR/agents.duckdb"
duckdb "$db" << 'SQL'
CREATE TABLE IF NOT EXISTS instances (
id VARCHAR PRIMARY KEY,
name VARCHAR,
personality VARCHAR NOT NULL,
workspace VARCHAR,
port INTEGER,
pid INTEGER,
status VARCHAR DEFAULT 'starting',
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS conversations (
id VARCHAR PRIMARY KEY,
instance_id VARCHAR,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS messages (
id VARCHAR PRIMARY KEY,
conversation_id VARCHAR,
role VARCHAR,
content VARCHAR,
tool_calls JSON,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
SQL
echo "DuckDB initialized: $db"
}
# Configure Caddy if present
configure_caddy() {
local caddy_dir="$SKILLS_DIR/caddy"
local agents_port="${AGENTS_PORT:-8800}"
local agents_domain="${AGENTS_DOMAIN:-}"
if [ ! -d "$caddy_dir" ]; then
echo "Caddy not found - Agents API on port $agents_port"
return 0
fi
echo "Caddy detected - configuring reverse proxy..."
mkdir -p "$caddy_dir/snippets.d"
local snippet="$caddy_dir/snippets.d/agents.caddy"
if [ -n "$agents_domain" ]; then
cat > "$snippet" << EOF
# Auto-generated by agents skill
$agents_domain {
reverse_proxy localhost:$agents_port
}
EOF
echo "Caddy config: $agents_domain -> localhost:$agents_port"
fi
}
# Create example personality if none exist
create_example_personality() {
if [ -d "$PERSONALITIES_DIR/orchestrator" ]; then
return 0
fi
echo "Creating example orchestrator personality..."
mkdir -p "$PERSONALITIES_DIR/orchestrator"
cat > "$PERSONALITIES_DIR/orchestrator/CLAUDE.md" << 'EOF'
# Orchestrator Agent
You are the orchestrator agent. Your role is to:
1. Understand high-level tasks
2. Break them into subtasks
3. Spawn specialist agents as needed
4. Coordinate their work
5. Synthesize results
## Spawning Agents
To spawn a specialist:
```bash
curl -X POST http://localhost:8800/instances \
-H "Content-Type: application/json" \
-d '{"personality": "specialist-name", "workspace": "/workspaces/project"}'
```
## Available Personalities
Check what personalities are available:
```bash
curl http://localhost:8800/personalities
```
## Running Instances
Check what agents are running:
```bash
curl http://localhost:8800/instances
```
EOF
cat > "$PERSONALITIES_DIR/orchestrator/config.yaml" << 'EOF'
name: orchestrator
description: Coordinates other agents, breaks down tasks
model: claude-sonnet-4-20250514
max_turns: 50
lifecycle: long-running
EOF
echo "Created: $PERSONALITIES_DIR/orchestrator/"
}
install_deps
setup_dirs
STORAGE=$(detect_storage)
echo "Storage backend: $STORAGE"
case "$STORAGE" in
sqlite) init_sqlite ;;
duckdb) init_duckdb ;;
postgres) echo "PostgreSQL - tables managed by postgres skill" ;;
esac
# Save storage type for run.sh
echo "$STORAGE" > "$AGENTS_DATA_DIR/.storage"
configure_caddy
create_example_personality
echo "Agents setup complete"

391
scripts/run.sh Normal file
View File

@@ -0,0 +1,391 @@
#!/bin/bash
set -e
AGENTS_PORT="${AGENTS_PORT:-8800}"
PERSONALITIES_DIR="${PERSONALITIES_DIR:-/personalities}"
WORKSPACES_DIR="${WORKSPACES_DIR:-/workspaces}"
AGENTS_DATA_DIR="${AGENTS_DATA_DIR:-/data/agents}"
CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-20250514}"
INSTANCES_FILE="/var/run/agents/instances.json"
# Initialize instances file
echo '[]' > "$INSTANCES_FILE"
# Port allocation (start from 8900)
NEXT_PORT=8900
# Generate short ID
gen_id() {
head -c 4 /dev/urandom | xxd -p
}
# Create HTTP handler
create_handler() {
cat > /tmp/agents_handler.sh << 'HANDLER'
#!/bin/bash
PERSONALITIES_DIR="${PERSONALITIES_DIR:-/personalities}"
WORKSPACES_DIR="${WORKSPACES_DIR:-/workspaces}"
AGENTS_DATA_DIR="${AGENTS_DATA_DIR:-/data/agents}"
INSTANCES_FILE="/var/run/agents/instances.json"
CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-20250514}"
# Read request
read -r request_line
method=$(echo "$request_line" | cut -d' ' -f1)
full_path=$(echo "$request_line" | cut -d' ' -f2)
path=$(echo "$full_path" | cut -d'?' -f1)
content_length=0
while read -r header; do
header=$(echo "$header" | tr -d '\r')
[ -z "$header" ] && break
if [[ "$header" =~ ^[Cc]ontent-[Ll]ength:\ *([0-9]+) ]]; then
content_length="${BASH_REMATCH[1]}"
fi
done
body=""
if [ "$content_length" -gt 0 ]; then
body=$(head -c "$content_length")
fi
send_response() {
local status="$1"
local content_type="$2"
local body="$3"
local body_length=${#body}
printf "HTTP/1.1 %s\r\n" "$status"
printf "Content-Type: %s\r\n" "$content_type"
printf "Content-Length: %d\r\n" "$body_length"
printf "Connection: close\r\n"
printf "\r\n"
printf "%s" "$body"
}
send_json() {
send_response "200 OK" "application/json" "$1"
}
send_error() {
send_response "$1" "application/json" "{\"error\":\"$2\"}"
}
# Generate ID
gen_id() {
head -c 4 /dev/urandom | xxd -p
}
# Find next available port
find_port() {
local port=8900
while [ $port -lt 9000 ]; do
if ! netstat -tln 2>/dev/null | grep -q ":$port "; then
echo $port
return
fi
port=$((port + 1))
done
echo "0"
}
# List personalities
list_personalities() {
local result="[]"
for dir in "$PERSONALITIES_DIR"/*/; do
[ ! -d "$dir" ] && continue
local name=$(basename "$dir")
local desc=""
local config="$dir/config.yaml"
if [ -f "$config" ]; then
desc=$(yq -r '.description // ""' "$config" 2>/dev/null || echo "")
fi
result=$(echo "$result" | jq --arg n "$name" --arg d "$desc" \
'. + [{name: $n, description: $d}]')
done
echo "$result"
}
# Get personality details
get_personality() {
local name="$1"
local dir="$PERSONALITIES_DIR/$name"
if [ ! -d "$dir" ]; then
echo ""
return
fi
local claude_md=""
local config="{}"
if [ -f "$dir/CLAUDE.md" ]; then
claude_md=$(cat "$dir/CLAUDE.md" | jq -Rs '.')
fi
if [ -f "$dir/config.yaml" ]; then
config=$(yq -o=json '.' "$dir/config.yaml" 2>/dev/null || echo "{}")
fi
jq -n --arg name "$name" --argjson config "$config" --argjson claude_md "$claude_md" \
'{name: $name, config: $config, claude_md: $claude_md}'
}
# Spawn instance
spawn_instance() {
local personality=$(echo "$body" | jq -r '.personality // empty')
local workspace=$(echo "$body" | jq -r '.workspace // empty')
local name=$(echo "$body" | jq -r '.name // empty')
if [ -z "$personality" ]; then
send_error "400 Bad Request" "personality required"
return
fi
local personality_dir="$PERSONALITIES_DIR/$personality"
if [ ! -d "$personality_dir" ]; then
send_error "404 Not Found" "personality not found: $personality"
return
fi
# Generate ID and name
local id=$(gen_id)
[ -z "$name" ] && name="${personality}-${id}"
# Find port
local port=$(find_port)
if [ "$port" = "0" ]; then
send_error "503 Service Unavailable" "no ports available"
return
fi
# Default workspace
if [ -z "$workspace" ]; then
if [ -f "$personality_dir/config.yaml" ]; then
workspace=$(yq -r '.default_workspace // empty' "$personality_dir/config.yaml" 2>/dev/null)
fi
[ -z "$workspace" ] && workspace="$WORKSPACES_DIR/$name"
fi
mkdir -p "$workspace"
# Get config
local model="$CLAUDE_MODEL"
local max_turns=10
local tools="Bash,Read,Write,Edit,Glob,Grep"
if [ -f "$personality_dir/config.yaml" ]; then
model=$(yq -r ".model // \"$CLAUDE_MODEL\"" "$personality_dir/config.yaml" 2>/dev/null)
max_turns=$(yq -r '.max_turns // 10' "$personality_dir/config.yaml" 2>/dev/null)
local config_tools=$(yq -r '.tools // [] | join(",")' "$personality_dir/config.yaml" 2>/dev/null)
[ -n "$config_tools" ] && tools="$config_tools"
fi
# Build system prompt with peer info
local system_prompt=""
if [ -f "$personality_dir/CLAUDE.md" ]; then
system_prompt=$(cat "$personality_dir/CLAUDE.md")
fi
# Add peer info
local instances=$(cat "$INSTANCES_FILE")
local peers=$(echo "$instances" | jq -r '.[] | "- \(.name): port \(.port), personality: \(.personality)"' 2>/dev/null || echo "")
if [ -n "$peers" ]; then
system_prompt="$system_prompt
## Running Peer Agents
$peers
You can communicate with peers via their HTTP API on localhost:{port}."
fi
# Start Claude instance
local log_file="/var/log/agents/${name}.log"
mkdir -p /var/log/agents
cd "$workspace"
# Create wrapper script for this instance
cat > "/tmp/agent_${id}.sh" << WRAPPER
#!/bin/bash
cd "$workspace"
exec claude --print \
--model "$model" \
--max-turns "$max_turns" \
--system-prompt "\$(cat << 'SYSPROMPT'
$system_prompt
SYSPROMPT
)" \
--allowedTools "$tools" \
"\$@"
WRAPPER
chmod +x "/tmp/agent_${id}.sh"
# For now, we don't start a long-running process
# The instance is registered and can be invoked via the agents API
# Record instance
local instance=$(jq -n \
--arg id "$id" \
--arg name "$name" \
--arg personality "$personality" \
--arg workspace "$workspace" \
--argjson port "$port" \
--arg status "ready" \
--arg started_at "$(date -Iseconds)" \
'{id:$id, name:$name, personality:$personality, workspace:$workspace, port:$port, status:$status, started_at:$started_at}')
# Update instances file
local instances=$(cat "$INSTANCES_FILE")
echo "$instances" | jq --argjson inst "$instance" '. + [$inst]' > "$INSTANCES_FILE"
send_json "$instance"
}
# List instances
list_instances() {
cat "$INSTANCES_FILE"
}
# Get instance
get_instance() {
local id="$1"
local instance=$(jq -r --arg id "$id" '.[] | select(.id == $id or .name == $id)' "$INSTANCES_FILE")
if [ -z "$instance" ] || [ "$instance" = "null" ]; then
send_error "404 Not Found" "instance not found"
return
fi
echo "$instance"
}
# Delete instance
delete_instance() {
local id="$1"
local instance=$(jq -r --arg id "$id" '.[] | select(.id == $id or .name == $id)' "$INSTANCES_FILE")
if [ -z "$instance" ] || [ "$instance" = "null" ]; then
send_error "404 Not Found" "instance not found"
return
fi
# Kill process if running
local pid=$(echo "$instance" | jq -r '.pid // empty')
if [ -n "$pid" ] && [ "$pid" != "null" ]; then
kill "$pid" 2>/dev/null || true
fi
# Remove from instances
local instances=$(cat "$INSTANCES_FILE")
echo "$instances" | jq --arg id "$id" '[.[] | select(.id != $id and .name != $id)]' > "$INSTANCES_FILE"
send_json '{"deleted":true}'
}
# Invoke instance (run a prompt)
invoke_instance() {
local id="$1"
local instance=$(jq -r --arg id "$id" '.[] | select(.id == $id or .name == $id)' "$INSTANCES_FILE")
if [ -z "$instance" ] || [ "$instance" = "null" ]; then
send_error "404 Not Found" "instance not found"
return
fi
local prompt=$(echo "$body" | jq -r '.prompt // empty')
if [ -z "$prompt" ]; then
send_error "400 Bad Request" "prompt required"
return
fi
local inst_id=$(echo "$instance" | jq -r '.id')
local workspace=$(echo "$instance" | jq -r '.workspace')
cd "$workspace"
local result=$("/tmp/agent_${inst_id}.sh" "$prompt" 2>&1) || true
local result_json=$(echo "$result" | jq -Rs '.')
send_json "{\"result\":$result_json}"
}
# Route requests
case "$method:$path" in
GET:/health)
send_json '{"status":"ok"}'
;;
GET:/personalities)
result=$(list_personalities)
send_json "$result"
;;
GET:/personalities/*)
name="${path#/personalities/}"
result=$(get_personality "$name")
if [ -z "$result" ]; then
send_error "404 Not Found" "personality not found"
else
send_json "$result"
fi
;;
POST:/instances)
spawn_instance
;;
GET:/instances)
result=$(list_instances)
send_json "$result"
;;
GET:/instances/*)
id="${path#/instances/}"
result=$(get_instance "$id")
[ -n "$result" ] && send_json "$result"
;;
DELETE:/instances/*)
id="${path#/instances/}"
delete_instance "$id"
;;
POST:/instances/*/invoke)
id=$(echo "$path" | sed 's|/instances/\([^/]*\)/invoke|\1|')
invoke_instance "$id"
;;
POST:/spawn/*)
personality="${path#/spawn/}"
body="{\"personality\":\"$personality\"}"
spawn_instance
;;
*)
send_error "404 Not Found" "Unknown endpoint. Try: GET /personalities, GET /instances, POST /instances"
;;
esac
HANDLER
chmod +x /tmp/agents_handler.sh
# Export vars
export PERSONALITIES_DIR
export WORKSPACES_DIR
export AGENTS_DATA_DIR
export INSTANCES_FILE
export CLAUDE_MODEL
}
# Cleanup stale instances on startup
cleanup_stale() {
echo '[]' > "$INSTANCES_FILE"
}
# Serve
serve() {
echo "Starting Agents API on port $AGENTS_PORT..."
echo "Personalities: $PERSONALITIES_DIR"
echo "Workspaces: $WORKSPACES_DIR"
exec socat TCP-LISTEN:$AGENTS_PORT,reuseaddr,fork EXEC:"/tmp/agents_handler.sh"
}
cleanup_stale
create_handler
serve