Initial agents skill - instance management with personalities
This commit is contained in:
135
SKILL.md
Normal file
135
SKILL.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
name: agents
|
||||||
|
description: Agent instance management with personalities
|
||||||
|
metadata:
|
||||||
|
version: "1.0.0"
|
||||||
|
vibestack:
|
||||||
|
main: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agents Skill
|
||||||
|
|
||||||
|
Manages Claude agent instances with different personalities.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- **Personality**: Template (CLAUDE.md + context) defining an agent's "soul"
|
||||||
|
- **Instance**: Running Claude process with a personality
|
||||||
|
- **Workspace**: Directory where an instance does its work
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/personalities/ # Soul templates
|
||||||
|
├── orchestrator/
|
||||||
|
│ ├── CLAUDE.md # System prompt / instructions
|
||||||
|
│ ├── config.yaml # Optional: defaults
|
||||||
|
│ └── context/ # Reference docs
|
||||||
|
├── frontend-dev/
|
||||||
|
└── backend-dev/
|
||||||
|
|
||||||
|
/workspaces/ # Where work happens
|
||||||
|
├── project-a/
|
||||||
|
└── project-b/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `AGENTS_PORT` | API port | `8800` |
|
||||||
|
| `PERSONALITIES_DIR` | Personalities location | `/personalities` |
|
||||||
|
| `WORKSPACES_DIR` | Workspaces location | `/workspaces` |
|
||||||
|
| `AGENTS_DOMAIN` | Domain for Caddy | (none) |
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Personalities
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all personalities
|
||||||
|
GET /personalities
|
||||||
|
|
||||||
|
# Get personality details
|
||||||
|
GET /personalities/{name}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instances
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Spawn new instance
|
||||||
|
POST /instances
|
||||||
|
{
|
||||||
|
"personality": "frontend-dev",
|
||||||
|
"workspace": "/workspaces/my-app", # optional
|
||||||
|
"name": "frontend-1" # optional, auto-generated
|
||||||
|
}
|
||||||
|
|
||||||
|
# List running instances
|
||||||
|
GET /instances
|
||||||
|
|
||||||
|
# Get instance details
|
||||||
|
GET /instances/{id}
|
||||||
|
|
||||||
|
# Kill instance
|
||||||
|
DELETE /instances/{id}
|
||||||
|
|
||||||
|
# Quick spawn with defaults
|
||||||
|
POST /spawn/{personality}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conversations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List conversations
|
||||||
|
GET /conversations
|
||||||
|
|
||||||
|
# Get conversation messages
|
||||||
|
GET /conversations/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Personality Config
|
||||||
|
|
||||||
|
Optional `config.yaml` in personality folder:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: frontend-dev
|
||||||
|
description: React/TypeScript specialist
|
||||||
|
default_workspace: /workspaces/frontend
|
||||||
|
model: claude-sonnet-4-20250514
|
||||||
|
max_turns: 20
|
||||||
|
max_instances: 3
|
||||||
|
lifecycle: on-demand # or long-running
|
||||||
|
tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Edit
|
||||||
|
- Glob
|
||||||
|
- Grep
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Auto-detects available storage:
|
||||||
|
1. PostgreSQL (if postgres skill present)
|
||||||
|
2. DuckDB (if duckdb skill present)
|
||||||
|
3. SQLite (fallback)
|
||||||
|
|
||||||
|
## Instance Discovery
|
||||||
|
|
||||||
|
Instances can discover each other:
|
||||||
|
```bash
|
||||||
|
# From any instance
|
||||||
|
curl http://localhost:8800/instances
|
||||||
|
```
|
||||||
|
|
||||||
|
System prompt includes peer info at spawn time.
|
||||||
|
|
||||||
|
## Example: Spawning from Claude
|
||||||
|
|
||||||
|
An orchestrator agent can spawn specialists:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8800/instances \
|
||||||
|
-d '{"personality":"frontend-dev","workspace":"/workspaces/app"}'
|
||||||
|
```
|
||||||
218
scripts/autorun.sh
Normal file
218
scripts/autorun.sh
Normal 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
391
scripts/run.sh
Normal 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
|
||||||
Reference in New Issue
Block a user