142 lines
4.7 KiB
Bash
Executable File
142 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# u/cascade — script-driven hub-and-spoke cascading orchestrator.
|
|
#
|
|
# An expensive model decomposes a task into mechanical subtasks, then cheap
|
|
# models execute them in parallel. The script (not an agent) coordinates.
|
|
#
|
|
# Usage: cascade [flags] [prompt...]
|
|
# echo "prompt" | cascade [flags]
|
|
#
|
|
# Flags:
|
|
# -pb BACKEND planner backend (default: $OLLIE_CASCADE_PLAN_BACKEND or anthropic)
|
|
# -pm MODEL planner model (default: $OLLIE_CASCADE_PLAN_MODEL or claude-sonnet-4-20250514)
|
|
# -wb BACKEND worker backend (default: $OLLIE_CASCADE_BACKEND or ollama)
|
|
# -no-synthesize skip the final planner synthesis step; just print worker results
|
|
# -json-output request structured JSON from the synthesis step
|
|
# schema: {summary, answer, worker_results[], errors[]}
|
|
set -euo pipefail
|
|
|
|
OLLIE="${OLLIE:-$HOME/mnt/ollie}"
|
|
B="$OLLIE/s/b"
|
|
|
|
PB="${OLLIE_CASCADE_PLAN_BACKEND:-anthropic}"
|
|
PM="${OLLIE_CASCADE_PLAN_MODEL:-claude-sonnet-4-20250514}"
|
|
WB="${OLLIE_CASCADE_BACKEND:-ollama}"
|
|
WM="${OLLIE_CASCADE_MODEL:-qwen2.5-coder:14b}"
|
|
|
|
die() { printf 'cascade: %s\n' "$*" >&2; exit 1; }
|
|
|
|
synthesize=1
|
|
json_output=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
-pb) PB=$2; shift ;;
|
|
-pm) PM=$2; shift ;;
|
|
-wb) WB=$2; shift ;;
|
|
-wm) WM=$2; shift ;;
|
|
-no-synthesize) synthesize=0 ;;
|
|
-json-output) json_output=1 ;;
|
|
--) shift; break ;;
|
|
-*) die "unknown flag: $1" ;;
|
|
*) break ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ $# -gt 0 ]; then
|
|
task="$*"
|
|
elif ! [ -t 0 ]; then
|
|
task=$(cat)
|
|
else
|
|
die "prompt required"
|
|
fi
|
|
[ -z "$task" ] && die "empty prompt"
|
|
|
|
# Step 1: Ask the planner to decompose into subtasks (JSON array).
|
|
plan=$("$B" -backend "$PB" -model "$PM" -json \
|
|
"Decompose this task into independent, mechanical subtasks that a weak model can execute without judgment. Each subtask must be fully self-contained (include all needed context). Output a JSON array of objects with \"id\" (short string) and \"instruction\" (complete task description). Keep it minimal — only decompose what benefits from parallelism.
|
|
|
|
TASK:
|
|
$task")
|
|
|
|
# Validate we got JSON with correct schema.
|
|
printf '%s' "$plan" | jq empty 2>/dev/null || die "planner did not return valid JSON: $plan"
|
|
printf '%s' "$plan" | jq -e '[.[] | select(.id and .instruction)] | length > 0' >/dev/null \
|
|
|| die "plan missing required fields (need .id and .instruction per entry)"
|
|
|
|
# Step 2: Spawn workers in parallel, collect session paths.
|
|
count=$(printf '%s' "$plan" | jq 'length')
|
|
[ "$count" -eq 0 ] && die "planner returned empty plan"
|
|
|
|
pids=()
|
|
workers=()
|
|
base="cascade-$(date +%s%N)"
|
|
tmpdir=$(mktemp -d)
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
|
|
for i in $(seq 0 $((count - 1))); do
|
|
id=$(printf '%s' "$plan" | jq -r ".[$i].id")
|
|
instruction=$(printf '%s' "$plan" | jq -r ".[$i].instruction")
|
|
sid="${base}-${id}"
|
|
workers+=("$sid")
|
|
|
|
(
|
|
"$B" -name "$sid" -backend "$WB" -model "$WM" -keep \
|
|
"You are a worker agent. Execute the following task exactly. Output only the result.
|
|
|
|
$instruction" > "${tmpdir}/${sid}.out" 2>&1 && touch "${tmpdir}/${sid}.ok"
|
|
) &
|
|
pids+=($!)
|
|
done
|
|
|
|
# Step 3: Wait for all workers.
|
|
for pid in "${pids[@]}"; do wait "$pid" || true; done
|
|
|
|
# Step 4: Gather results with status annotations.
|
|
results=""
|
|
failures=0
|
|
for sid in "${workers[@]}"; do
|
|
if [ -f "${tmpdir}/${sid}.ok" ]; then
|
|
status="done"
|
|
else
|
|
status="failed"
|
|
failures=$((failures + 1))
|
|
fi
|
|
out=$(cat "${tmpdir}/${sid}.out" 2>/dev/null || echo "(no output)")
|
|
results="${results}
|
|
--- ${sid} [STATUS: ${status}] ---
|
|
${out}
|
|
"
|
|
done
|
|
|
|
# Step 5: Synthesize or just print results.
|
|
if [ "$synthesize" -eq 0 ]; then
|
|
printf '%s\n' "$results"
|
|
[ "$failures" -gt 0 ] && exit 1
|
|
else
|
|
synth_flags=(-backend "$PB" -model "$PM")
|
|
if [ "$json_output" -eq 1 ]; then
|
|
synth_flags+=(-json)
|
|
synth_prompt="Synthesize these worker results into a structured JSON response for the original task. Note: workers marked [STATUS: failed] did not complete successfully — include them in errors.
|
|
|
|
Output schema: {\"summary\": \"<one-line summary>\", \"answer\": \"<full synthesized answer>\", \"worker_results\": [{\"id\": \"<worker-id>\", \"status\": \"done|failed\", \"output\": \"<worker output>\"}], \"errors\": [\"<description of any failures>\"]}
|
|
|
|
ORIGINAL TASK:
|
|
$task
|
|
|
|
WORKER RESULTS:
|
|
$results"
|
|
else
|
|
synth_prompt="Synthesize these worker results into a final, coherent answer to the original task. Note: workers marked [STATUS: failed] did not complete successfully — account for this in your synthesis.
|
|
|
|
ORIGINAL TASK:
|
|
$task
|
|
|
|
WORKER RESULTS:
|
|
$results"
|
|
fi
|
|
"$B" "${synth_flags[@]}" "$synth_prompt"
|
|
[ "$failures" -gt 0 ] && exit 1
|
|
fi
|