Files
blackroad/roadnet/fleet-agents.sh
Alexa Amundson 78fbe80f2a Initial monorepo — everything BlackRoad in one place
bin/       230 CLI tools (ask-*, br-*, agent-*, roadid, carpool)
scripts/   99 automation scripts
fleet/     Node configs and deployment
workers/   Cloudflare Worker sources (roadpay, road-search, squad webhooks)
roadc/     RoadC programming language
roadnet/   Mesh network (5 APs, WireGuard)
operator/  Memory system scripts
config/    System configs
dotfiles/  Shell configs
docs/      Documentation

BlackRoad OS — Pave Tomorrow.

RoadChain-SHA2048: d1a24f55318d338b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
2026-03-14 17:08:41 -05:00

696 lines
25 KiB
Bash
Executable File

#!/bin/bash
# fleet-agents.sh — Agent coordination, handoff, memory, and continuity
# The nervous system of BlackRoad OS
#
# Usage:
# fleet-agents.sh roster # Show all agents, their nodes, purpose
# fleet-agents.sh handoff <from> <to> <task-id> # Transfer task with context
# fleet-agents.sh dispatch <prompt> # Route prompt to best agent
# fleet-agents.sh memory <agent> [query] # Read/search agent memory
# fleet-agents.sh inject <agent> <key> <value> # Write to agent memory
# fleet-agents.sh chain <task-id> # Show task chain (who touched it)
# fleet-agents.sh status # Live fleet agent status
# fleet-agents.sh wake <agent> # Load agent's model into VRAM
# fleet-agents.sh think <agent> <prompt> # Direct inference from agent
set -e
PINK='\033[38;5;205m'
AMBER='\033[38;5;214m'
BLUE='\033[38;5;69m'
VIOLET='\033[38;5;135m'
GREEN='\033[38;5;82m'
RED='\033[38;5;196m'
CYAN='\033[38;5;87m'
DIM='\033[2m'
BOLD='\033[1m'
RESET='\033[0m'
AGENT_DB="$HOME/.blackroad/agent-memory.db"
TASK_DB="$HOME/.blackroad/agent-tasks.db"
HEARTBEAT_DB="$HOME/.blackroad/fleet-heartbeat.db"
MEMORIES_DIR="$HOME/.blackroad/agents/memories"
COMM_DIR="$HOME/.blackroad/agent-comm"
REGISTRY="$HOME/.blackroad/agents/config/agent-registry.json"
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes"
# ── Agent → Node → Model mapping ──
# Every agent has: a name, a node, a model, a purpose, capabilities
# This is the source of truth for who does what
declare -A AGENT_NODE=(
# Cecilia fleet (main AI, 15 models, Hailo-8)
[athena]="cecilia" # Strategy + architecture — qwen3:8b (best reasoning)
[prometheus]="cecilia" # Code generation — qwen2.5-coder:3b
[apollo]="cecilia" # General knowledge — llama3.2:3b
[cece]="cecilia" # BlackRoad custom agent — cece:latest
[hermes]="cecilia" # Fast messenger — tinyllama:latest
# Lucidia fleet (6 models, CI/CD, web)
[lucidia]="lucidia" # Oracle, long-form — lucidia:latest
[morpheus]="lucidia" # Dream/creative — qwen2.5:3b
[chronos]="lucidia" # Quick tasks — llama3.2:1b
# Alice fleet (gateway, 3.7GB RAM)
[gaia]="alice" # Earth/infra monitoring — tinyllama:latest
# Octavia fleet (NVMe, Gitea, Docker Swarm)
[hades]="octavia" # Storage, repos, data — local services
[daedalus]="octavia" # Build/deploy — docker + gitea
)
declare -A AGENT_MODEL=(
[athena]="qwen3:8b"
[prometheus]="qwen2.5-coder:3b"
[apollo]="llama3.2:3b"
[cece]="cece:latest"
[hermes]="tinyllama:latest"
[lucidia]="lucidia:latest"
[morpheus]="qwen2.5:3b"
[chronos]="llama3.2:1b"
[gaia]="tinyllama:latest"
[hades]=""
[daedalus]=""
)
declare -A AGENT_PURPOSE=(
[athena]="Strategy, architecture, code review, system design"
[prometheus]="Code generation, debugging, refactoring, tests"
[apollo]="General Q&A, documentation, explanations"
[cece]="BlackRoad custom agent — fleet knowledge, personality"
[hermes]="Fast dispatch, routing, quick answers, message relay"
[lucidia]="Deep reasoning, long-form analysis, oracle predictions"
[morpheus]="Creative writing, brainstorming, dream logic"
[chronos]="Quick tasks, timestamps, scheduling, cron"
[gaia]="Infrastructure monitoring, DNS, network health"
[hades]="Data storage, repo management, backups, NVMe"
[daedalus]="Build pipelines, Docker, CI/CD, deployment"
)
declare -A AGENT_CAPS=(
[athena]="code,architecture,review,strategy,math"
[prometheus]="code,debug,test,refactor,generate"
[apollo]="chat,explain,docs,general,search"
[cece]="chat,fleet,personality,custom"
[hermes]="chat,fast,route,relay"
[lucidia]="chat,reason,oracle,long-form,analysis"
[morpheus]="chat,creative,brainstorm,write"
[chronos]="chat,fast,schedule,time"
[gaia]="monitor,dns,network,infra"
[hades]="storage,git,backup,data"
[daedalus]="docker,build,deploy,ci"
)
declare -A NODE_SSH=(
[cecilia]="blackroad@192.168.4.96"
[lucidia]="octavia@192.168.4.38"
[alice]="pi@192.168.4.49"
[octavia]="pi@192.168.4.100"
)
declare -A NODE_OLLAMA=(
[cecilia]="http://192.168.4.96:11434"
[lucidia]="http://192.168.4.38:11434"
[alice]="http://192.168.4.49:11434"
)
# ── Helpers ──
header() {
echo -e "\n${PINK}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${PINK} ◆ BlackRoad Agent Mesh${RESET}"
echo -e "${PINK}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
}
log() { echo -e " ${BLUE}${RESET} $1"; }
ok() { echo -e " ${GREEN}${RESET} $1"; }
warn() { echo -e " ${AMBER}${RESET} $1"; }
err() { echo -e " ${RED}${RESET} $1"; }
roadid() {
# Generate a RoadID: <node>-<type>-<time_base36>-<rand>
local node="${1:-mac}"
local type="${2:-task}"
local ts=$(printf '%s' "$(date +%s)" | python3 -c "import sys; n=int(sys.stdin.read()); s=''; d='0123456789abcdefghijklmnopqrstuvwxyz'
while n>0: s=d[n%36]+s; n//=36
print(s)")
local rand=$(head -c 4 /dev/urandom | xxd -p | head -c 4)
echo "${node}-${type}-${ts}-${rand}"
}
# ── ROSTER: Show all agents, their purpose, node, model ──
cmd_roster() {
header
echo -e "${AMBER}Agent Roster${RESET}\n"
printf " ${DIM}%-12s %-10s %-22s %s${RESET}\n" "AGENT" "NODE" "MODEL" "PURPOSE"
echo -e " ${DIM}$(printf '─%.0s' {1..80})${RESET}"
for agent in athena prometheus apollo cece hermes lucidia morpheus chronos gaia hades daedalus; do
local node="${AGENT_NODE[$agent]}"
local model="${AGENT_MODEL[$agent]:-}"
local purpose="${AGENT_PURPOSE[$agent]}"
# Color by node
local color="$RESET"
case "$node" in
cecilia) color="$PINK" ;;
lucidia) color="$VIOLET" ;;
alice) color="$BLUE" ;;
octavia) color="$CYAN" ;;
esac
printf " ${color}%-12s${RESET} %-10s %-22s %s\n" "$agent" "$node" "$model" "$purpose"
done
echo ""
echo -e " ${DIM}Mythological personas (persistent memory):${RESET}"
local count=$(ls "$MEMORIES_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
echo -e " ${DIM}$count agents with memory in $MEMORIES_DIR${RESET}"
echo ""
}
# ── STATUS: Live agent status across fleet ──
cmd_status() {
header
echo -e "${AMBER}Fleet Agent Status${RESET}\n"
for node in cecilia lucidia alice octavia; do
local ssh="${NODE_SSH[$node]}"
echo -e " ${VIOLET}[$node]${RESET} ${DIM}$ssh${RESET}"
if ! ssh $SSH_OPTS "$ssh" "echo ok" &>/dev/null; then
err "unreachable"
echo ""
continue
fi
ok "online"
# Get loaded models (= active agents)
local loaded
loaded=$(ssh $SSH_OPTS "$ssh" "curl -s localhost:11434/api/ps 2>/dev/null" || echo '{}')
local models
models=$(echo "$loaded" | python3 -c "
import sys,json
try:
d=json.load(sys.stdin)
for m in d.get('models',[]):
print(m['name']+'|'+str(round(m.get('size',0)/1e9,1))+'GB')
except: pass
" 2>/dev/null)
if [ -n "$models" ]; then
echo "$models" | while IFS='|' read -r mname msize; do
# Find which agent uses this model
local agent_name="—"
for a in "${!AGENT_MODEL[@]}"; do
if [ "${AGENT_MODEL[$a]}" = "$mname" ] && [ "${AGENT_NODE[$a]}" = "$node" ]; then
agent_name="$a"
break
fi
done
echo -e " ${GREEN}${RESET} $mname ${DIM}($msize)${RESET}${CYAN}$agent_name${RESET}"
done
else
warn "no models loaded"
fi
# Memory
local mem
mem=$(ssh $SSH_OPTS "$ssh" "free -h | awk '/Mem:/{print \$4\"/\"\$2}'" 2>/dev/null)
echo -e " ${DIM}RAM: $mem${RESET}"
echo ""
done
}
# ── DISPATCH: Route a prompt to the best agent ──
cmd_dispatch() {
local prompt="$*"
if [ -z "$prompt" ]; then
err "Usage: fleet-agents.sh dispatch <prompt>"
exit 1
fi
header
echo -e "${AMBER}Dispatching:${RESET} ${DIM}\"$(echo "$prompt" | head -c 60)...\"${RESET}\n"
# Keyword matching → capability → agent
local prompt_lower=$(echo "$prompt" | tr '[:upper:]' '[:lower:]')
local best_agent=""
local best_score=0
for agent in "${!AGENT_CAPS[@]}"; do
local caps="${AGENT_CAPS[$agent]}"
local score=0
# Score by keyword match
IFS=',' read -ra cap_arr <<< "$caps"
for cap in "${cap_arr[@]}"; do
case "$cap" in
code) echo "$prompt_lower" | grep -qE 'code|function|script|debug|bug|error|fix|write.*program|implement|refactor' && ((score+=10)) ;;
chat) ((score+=1)) ;; # base score for all chat agents
fast) echo "$prompt_lower" | grep -qE 'quick|fast|brief|short|simple' && ((score+=5)) ;;
creative) echo "$prompt_lower" | grep -qE 'write|story|creative|brainstorm|idea|dream' && ((score+=8)) ;;
reason) echo "$prompt_lower" | grep -qE 'explain|why|how|analyze|think|reason|complex' && ((score+=8)) ;;
architecture) echo "$prompt_lower" | grep -qE 'architect|design|system|plan|structure|scale' && ((score+=10)) ;;
review) echo "$prompt_lower" | grep -qE 'review|check|audit|quality' && ((score+=8)) ;;
monitor) echo "$prompt_lower" | grep -qE 'monitor|health|status|ping|check.*server|dns|network' && ((score+=10)) ;;
docker) echo "$prompt_lower" | grep -qE 'docker|container|deploy|build|ci|pipeline' && ((score+=10)) ;;
git) echo "$prompt_lower" | grep -qE 'git|repo|commit|branch|merge|pull' && ((score+=10)) ;;
storage) echo "$prompt_lower" | grep -qE 'storage|disk|file|backup|nvme|data' && ((score+=10)) ;;
fleet) echo "$prompt_lower" | grep -qE 'fleet|blackroad|node|pi|raspberry' && ((score+=8)) ;;
math) echo "$prompt_lower" | grep -qE 'math|calcul|equation|algebra|geometry' && ((score+=8)) ;;
schedule) echo "$prompt_lower" | grep -qE 'cron|schedule|timer|every.*minute|recurring' && ((score+=10)) ;;
search) echo "$prompt_lower" | grep -qE 'search|find|look.*up|where' && ((score+=6)) ;;
esac
done
if [ "$score" -gt "$best_score" ]; then
best_score=$score
best_agent=$agent
fi
done
# Default to apollo (general) if no strong match
if [ "$best_score" -lt 3 ]; then
best_agent="apollo"
fi
local node="${AGENT_NODE[$best_agent]}"
local model="${AGENT_MODEL[$best_agent]}"
ok "Routed to ${CYAN}${best_agent}${RESET} on ${VIOLET}${node}${RESET} (score: $best_score)"
log "Model: $model"
log "Purpose: ${AGENT_PURPOSE[$best_agent]}"
# Create task record
local task_id=$(roadid "$node" "task")
sqlite3 "$TASK_DB" "INSERT INTO tasks (id, title, description, assigned_to, status, priority)
VALUES ('$task_id', '$(echo "$prompt" | head -c 40 | sed "s/'/''/g")', '$(echo "$prompt" | sed "s/'/''/g")', '$best_agent', 'dispatched', 5);"
ok "Task: $task_id"
# If agent has a model, run inference
if [ -n "$model" ]; then
local ssh="${NODE_SSH[$node]}"
echo -e "\n ${PINK}$best_agent responds:${RESET}\n"
ssh $SSH_OPTS "$ssh" "curl -s -X POST localhost:11434/api/generate \
-d '{\"model\":\"$model\",\"prompt\":\"$(echo "$prompt" | sed 's/"/\\"/g')\",\"stream\":false,\"keep_alive\":-1,\"options\":{\"num_predict\":512}}' \
--max-time 120 2>/dev/null" | python3 -c "
import sys,json
try:
d=json.load(sys.stdin)
print(d.get('response','(no response)'))
except Exception as e:
print(f'Error: {e}')
" 2>/dev/null
# Mark done
sqlite3 "$TASK_DB" "UPDATE tasks SET status='done', completed_at=strftime('%s','now') WHERE id='$task_id';"
echo -e "\n ${GREEN}${RESET} Task complete: $task_id"
fi
}
# ── HANDOFF: Transfer task from one agent to another with full context ──
cmd_handoff() {
local from="$1" to="$2" task_id="$3"
if [ -z "$from" ] || [ -z "$to" ]; then
err "Usage: fleet-agents.sh handoff <from-agent> <to-agent> [task-id]"
exit 1
fi
header
echo -e "${AMBER}Handoff: ${CYAN}$from${RESET}${CYAN}$to${RESET}\n"
# Validate agents exist
if [ -z "${AGENT_NODE[$from]}" ]; then err "Unknown agent: $from"; exit 1; fi
if [ -z "${AGENT_NODE[$to]}" ]; then err "Unknown agent: $to"; exit 1; fi
# Get task context
if [ -n "$task_id" ]; then
local task_info
task_info=$(sqlite3 "$TASK_DB" "SELECT title, description, result FROM tasks WHERE id='$task_id';" 2>/dev/null)
if [ -z "$task_info" ]; then
warn "Task $task_id not found, creating handoff without prior context"
else
log "Task: $task_id"
log "Context: $(echo "$task_info" | head -c 80)"
fi
fi
# Get from-agent's recent memory/conversation
local from_context
from_context=$(sqlite3 "$AGENT_DB" "
SELECT content FROM messages
WHERE conversation_id IN (
SELECT id FROM conversations WHERE model='${AGENT_MODEL[$from]}' ORDER BY updated_at DESC LIMIT 1
)
ORDER BY created_at DESC LIMIT 5;
" 2>/dev/null)
# Create handoff record
local handoff_id=$(roadid "mac" "task")
local handoff_time=$(date +%s)
# Write to agent inbox
local inbox_file="$COMM_DIR/${to}-handoff.json"
cat > "$inbox_file" << HANDOFF
{
"handoff_id": "$handoff_id",
"from": "$from",
"to": "$to",
"original_task": "$task_id",
"timestamp": $handoff_time,
"from_node": "${AGENT_NODE[$from]}",
"to_node": "${AGENT_NODE[$to]}",
"from_model": "${AGENT_MODEL[$from]}",
"to_model": "${AGENT_MODEL[$to]}",
"context": $(echo "$from_context" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo '""'),
"reason": "Agent handoff: $from capabilities exhausted or $to better suited"
}
HANDOFF
# Log handoff in task DB
sqlite3 "$TASK_DB" "INSERT INTO agent_log (agent, event, detail)
VALUES ('$from', 'handoff_out', 'to=$to task=$task_id handoff=$handoff_id');"
sqlite3 "$TASK_DB" "INSERT INTO agent_log (agent, event, detail)
VALUES ('$to', 'handoff_in', 'from=$from task=$task_id handoff=$handoff_id');"
# Create continuation task
sqlite3 "$TASK_DB" "INSERT INTO tasks (id, title, description, assigned_to, status, priority, chain_to)
VALUES ('$handoff_id', 'Handoff from $from', 'Continue task $task_id', '$to', 'dispatched', 3, '$task_id');"
ok "Handoff complete: $handoff_id"
ok "From: $from (${AGENT_NODE[$from]}) → To: $to (${AGENT_NODE[$to]})"
ok "Context transferred: $(echo "$from_context" | wc -c | tr -d ' ') bytes"
ok "Inbox written: $inbox_file"
# If target agent has a model, notify it
local to_model="${AGENT_MODEL[$to]}"
local to_node="${AGENT_NODE[$to]}"
if [ -n "$to_model" ] && [ -n "${NODE_SSH[$to_node]}" ]; then
log "Waking $to on $to_node..."
ssh $SSH_OPTS "${NODE_SSH[$to_node]}" "curl -s -X POST localhost:11434/api/generate \
-d '{\"model\":\"$to_model\",\"prompt\":\"ping\",\"keep_alive\":-1,\"options\":{\"num_predict\":1}}' \
--max-time 30 >/dev/null 2>&1" &
ok "$to model loaded on $to_node"
fi
}
# ── MEMORY: Read or search agent memory ──
cmd_memory() {
local agent="$1"
local query="$2"
if [ -z "$agent" ]; then
# Show all agents with memory
header
echo -e "${AMBER}Agent Memories${RESET}\n"
for f in "$MEMORIES_DIR"/*.md; do
local name=$(basename "$f" .md)
local sessions=$(grep -c "Session" "$f" 2>/dev/null || echo 0)
local size=$(wc -c < "$f" | tr -d ' ')
printf " ${CYAN}%-14s${RESET} %6s bytes %s sessions\n" "$name" "$size" "$sessions"
done
echo ""
echo -e " ${DIM}Agent state in DB:${RESET}"
sqlite3 "$AGENT_DB" "SELECT agent_id, json_extract(state,'$.status') as status, datetime(updated_at,'unixepoch') as updated FROM agent_state ORDER BY updated_at DESC;" 2>/dev/null | while IFS='|' read -r aid status updated; do
echo -e " ${GREEN}${RESET} $aid$status ($updated)"
done
return
fi
# Show specific agent memory
local mem_file="$MEMORIES_DIR/$(echo "$agent" | tr '[:lower:]' '[:upper:]' | head -c 1)$(echo "$agent" | tail -c +2).md"
if [ ! -f "$mem_file" ]; then
# Try uppercase
mem_file="$MEMORIES_DIR/$(echo "$agent" | tr '[:lower:]' '[:upper:]').md"
fi
header
echo -e "${AMBER}Memory: ${CYAN}$agent${RESET}\n"
if [ -f "$mem_file" ]; then
cat "$mem_file"
else
warn "No memory file for $agent"
fi
# Also show recent conversation history
local model="${AGENT_MODEL[$agent]}"
if [ -n "$model" ]; then
echo -e "\n ${DIM}Recent conversations (model: $model):${RESET}"
sqlite3 "$AGENT_DB" "
SELECT c.title, datetime(c.updated_at,'unixepoch'), COUNT(m.id) as msgs
FROM conversations c
LEFT JOIN messages m ON m.conversation_id = c.id
WHERE c.model = '$model'
GROUP BY c.id
ORDER BY c.updated_at DESC
LIMIT 5;
" 2>/dev/null | while IFS='|' read -r title updated msgs; do
echo -e " ${DIM}$updated${RESET}$title ($msgs msgs)"
done
fi
# Search if query provided
if [ -n "$query" ]; then
echo -e "\n ${AMBER}Search: \"$query\"${RESET}"
sqlite3 "$AGENT_DB" "
SELECT m.content FROM messages m
JOIN conversations c ON m.conversation_id = c.id
WHERE c.model = '$model' AND m.content LIKE '%$query%'
ORDER BY m.created_at DESC LIMIT 5;
" 2>/dev/null | while read -r content; do
echo -e " ${DIM}$(echo "$content" | head -c 120)${RESET}"
done
fi
}
# ── INJECT: Write to agent memory ──
cmd_inject() {
local agent="$1" key="$2" value="$3"
if [ -z "$agent" ] || [ -z "$key" ] || [ -z "$value" ]; then
err "Usage: fleet-agents.sh inject <agent> <key> <value>"
exit 1
fi
# Update agent_state in DB
local state_json=$(sqlite3 "$AGENT_DB" "SELECT state FROM agent_state WHERE agent_id='$agent';" 2>/dev/null)
if [ -z "$state_json" ]; then
state_json='{}'
fi
local new_state
new_state=$(echo "$state_json" | python3 -c "
import sys,json
d=json.loads(sys.stdin.read() or '{}')
d['$key']='$value'
d['updated']='$(date -u +%Y-%m-%dT%H:%M:%SZ)'
print(json.dumps(d))
" 2>/dev/null)
sqlite3 "$AGENT_DB" "INSERT OR REPLACE INTO agent_state (agent_id, state, context, updated_at)
VALUES ('$agent', '$(echo "$new_state" | sed "s/'/''/g")', '$key=$value', strftime('%s','now'));"
# Also append to memory file
local mem_file="$MEMORIES_DIR/$(echo "$agent" | tr '[:lower:]' '[:upper:]').md"
if [ -f "$mem_file" ]; then
echo "" >> "$mem_file"
echo "### Memory Injection ($(date -u +%Y-%m-%d))" >> "$mem_file"
echo "- **$key**: $value" >> "$mem_file"
fi
ok "Injected into $agent: $key = $value"
sqlite3 "$TASK_DB" "INSERT INTO agent_log (agent, event, detail) VALUES ('$agent', 'memory_inject', '$key=$value');"
}
# ── CHAIN: Show task handoff chain ──
cmd_chain() {
local task_id="$1"
if [ -z "$task_id" ]; then
# Show recent task chains
header
echo -e "${AMBER}Recent Task Chains${RESET}\n"
sqlite3 "$TASK_DB" "
SELECT id, assigned_to, status, title, chain_to, datetime(created_at,'unixepoch')
FROM tasks
WHERE chain_to != '' OR id IN (SELECT chain_to FROM tasks WHERE chain_to != '')
ORDER BY created_at DESC LIMIT 20;
" 2>/dev/null | while IFS='|' read -r tid agent status title chain_to created; do
local arrow=""
[ -n "$chain_to" ] && arrow="$chain_to"
echo -e " ${CYAN}$tid${RESET} ${DIM}$created${RESET}"
echo -e " ${GREEN}$agent${RESET} ($status) — $title$arrow"
done
return
fi
header
echo -e "${AMBER}Task Chain: $task_id${RESET}\n"
# Walk the chain forward and backward
local current="$task_id"
local chain=()
# Walk backward (find origin)
while true; do
local info
info=$(sqlite3 "$TASK_DB" "SELECT id, assigned_to, status, title, chain_to FROM tasks WHERE id='$current';" 2>/dev/null)
if [ -z "$info" ]; then break; fi
chain=("$info" "${chain[@]}")
local prev=$(echo "$info" | cut -d'|' -f5)
if [ -z "$prev" ] || [ "$prev" = "$current" ]; then break; fi
current="$prev"
done
# Walk forward (find continuations)
current="$task_id"
while true; do
local next
next=$(sqlite3 "$TASK_DB" "SELECT id, assigned_to, status, title, chain_to FROM tasks WHERE chain_to='$current' LIMIT 1;" 2>/dev/null)
if [ -z "$next" ]; then break; fi
chain+=("$next")
current=$(echo "$next" | cut -d'|' -f1)
done
# Display chain
local i=0
for entry in "${chain[@]}"; do
IFS='|' read -r tid agent status title chain_to <<< "$entry"
local marker=" "
[ "$tid" = "$task_id" ] && marker="→ "
echo -e " ${marker}${CYAN}$tid${RESET}"
echo -e " Agent: ${GREEN}$agent${RESET} Status: $status"
echo -e " ${DIM}$title${RESET}"
if [ $i -lt $((${#chain[@]}-1)) ]; then
echo -e " ${DIM}↓ handoff${RESET}"
fi
((i++))
done
}
# ── WAKE: Load an agent's model into VRAM ──
cmd_wake() {
local agent="$1"
if [ -z "$agent" ]; then
err "Usage: fleet-agents.sh wake <agent>"
echo -e " Available: ${!AGENT_MODEL[*]}"
exit 1
fi
local model="${AGENT_MODEL[$agent]}"
local node="${AGENT_NODE[$agent]}"
if [ -z "$model" ]; then
warn "$agent has no LLM model (service agent)"
return
fi
local ssh="${NODE_SSH[$node]}"
if [ -z "$ssh" ]; then
err "No SSH config for node $node"
return
fi
log "Waking $agent on $node ($model)..."
ssh $SSH_OPTS "$ssh" "curl -s -X POST localhost:11434/api/generate \
-d '{\"model\":\"$model\",\"prompt\":\"ping\",\"keep_alive\":-1,\"options\":{\"num_predict\":1}}' \
--max-time 60 >/dev/null 2>&1"
ok "$agent is awake on $node (model: $model, keep_alive=forever)"
sqlite3 "$TASK_DB" "INSERT INTO agent_log (agent, event, detail) VALUES ('$agent', 'wake', 'model=$model node=$node');"
}
# ── THINK: Direct inference from a specific agent ──
cmd_think() {
local agent="$1"
shift
local prompt="$*"
if [ -z "$agent" ] || [ -z "$prompt" ]; then
err "Usage: fleet-agents.sh think <agent> <prompt>"
exit 1
fi
local model="${AGENT_MODEL[$agent]}"
local node="${AGENT_NODE[$agent]}"
local purpose="${AGENT_PURPOSE[$agent]}"
if [ -z "$model" ]; then
err "$agent has no LLM model"
exit 1
fi
local ssh="${NODE_SSH[$node]}"
echo -e "\n${CYAN}$agent${RESET} ${DIM}($model on $node)${RESET}\n"
# Build system prompt with agent identity
local sys_prompt="You are $agent, a BlackRoad AI agent. Your purpose: $purpose. Be concise and direct."
# Create task
local task_id=$(roadid "$node" "task")
sqlite3 "$TASK_DB" "INSERT INTO tasks (id, title, assigned_to, status, priority)
VALUES ('$task_id', '$(echo "$prompt" | head -c 40 | sed "s/'/''/g")', '$agent', 'running', 5);"
# Run inference
local response
response=$(ssh $SSH_OPTS "$ssh" "curl -s -X POST localhost:11434/api/generate \
-d '{\"model\":\"$model\",\"system\":\"$(echo "$sys_prompt" | sed 's/"/\\"/g')\",\"prompt\":\"$(echo "$prompt" | sed 's/"/\\"/g')\",\"stream\":false,\"keep_alive\":-1,\"options\":{\"num_predict\":-1}}' \
--max-time 300 2>/dev/null" | python3 -c "
import sys,json
try:
d=json.load(sys.stdin)
print(d.get('response','(no response)'))
except Exception as e:
print(f'Error: {e}')
" 2>/dev/null)
echo "$response"
# Store in memory
local conv_id=$(roadid "mac" "sess")
sqlite3 "$AGENT_DB" "INSERT INTO conversations (id, title, model) VALUES ('$conv_id', '$(echo "$prompt" | head -c 40 | sed "s/'/''/g")', '$model');"
sqlite3 "$AGENT_DB" "INSERT INTO messages (id, conversation_id, role, content, model) VALUES ('$(roadid mac msg)', '$conv_id', 'user', '$(echo "$prompt" | sed "s/'/''/g")', '$model');"
sqlite3 "$AGENT_DB" "INSERT INTO messages (id, conversation_id, role, content, model) VALUES ('$(roadid mac msg)', '$conv_id', 'assistant', '$(echo "$response" | sed "s/'/''/g")', '$model');"
# Complete task
sqlite3 "$TASK_DB" "UPDATE tasks SET status='done', result='$(echo "$response" | head -c 200 | sed "s/'/''/g")', completed_at=strftime('%s','now') WHERE id='$task_id';"
echo -e "\n${DIM}task: $task_id${RESET}"
}
# ── Main ──
case "${1:-roster}" in
roster) cmd_roster ;;
status) cmd_status ;;
dispatch) shift; cmd_dispatch "$@" ;;
handoff) shift; cmd_handoff "$@" ;;
memory) shift; cmd_memory "$@" ;;
inject) shift; cmd_inject "$@" ;;
chain) shift; cmd_chain "$@" ;;
wake) shift; cmd_wake "$@" ;;
think) shift; cmd_think "$@" ;;
*)
echo "Usage: fleet-agents.sh <command>"
echo ""
echo " roster All agents, their nodes, purpose"
echo " status Live fleet agent status"
echo " dispatch <prompt> Route prompt to best agent"
echo " handoff <from> <to> [task-id] Transfer task with context"
echo " memory [agent] [query] Read/search agent memory"
echo " inject <agent> <key> <value> Write to agent memory"
echo " chain [task-id] Show task handoff chain"
echo " wake <agent> Load agent model into VRAM"
echo " think <agent> <prompt> Direct inference from agent"
;;
esac