98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Waiting to run
CLI Smoke Test / smoke-test-linux (24) (push) Waiting to run
CLI Smoke Test / smoke-test-windows (20) (push) Waiting to run
CLI Smoke Test / smoke-test-windows (24) (push) Waiting to run
Expo App TypeScript typecheck / typecheck (push) Waiting to run
406 lines
13 KiB
Bash
Executable File
406 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# trace-opencode.sh — reusable harness for tracing OpenCode's protocol
|
|
#
|
|
# Sets up an isolated OpenCode server, runs scripted flows against it,
|
|
# and captures raw request/response/SSE logs.
|
|
#
|
|
# Usage:
|
|
# ./scripts/trace-opencode.sh [flow ...]
|
|
#
|
|
# flows: permission, media, subtask, question, todo, all (default: all)
|
|
#
|
|
# Prerequisites:
|
|
# - OpenCode source checkout at ../happy-adjacent/research/opencode
|
|
# - bun installed
|
|
# - Auth file at ~/.local/share/opencode/auth.json (with at least one provider key)
|
|
#
|
|
# Output:
|
|
# docs/competition/opencode/traces/<timestamp>/
|
|
# setup.json — runtime config used
|
|
# flow-<name>.json — per-flow request/response/events log
|
|
# sse-raw.log — raw SSE stream dump
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ── Config ──────────────────────────────────────────────────
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
OPENCODE_SRC="${OPENCODE_SRC:-$REPO_ROOT/../happy-adjacent/research/opencode}"
|
|
AUTH_SOURCE="${AUTH_SOURCE:-$HOME/.local/share/opencode/auth.json}"
|
|
LAB_RAT_DIR="$REPO_ROOT/environments/lab-rat-todo-project"
|
|
PORT="${OPENCODE_TRACE_PORT:-0}"
|
|
PROVIDER="${OPENCODE_TRACE_PROVIDER:-openai}"
|
|
MODEL="${OPENCODE_TRACE_MODEL:-gpt-4.1-mini}"
|
|
|
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
|
TRACE_DIR="$REPO_ROOT/docs/competition/opencode/traces/$TIMESTAMP"
|
|
TMPROOT=""
|
|
SERVER_PID=""
|
|
SSE_PID=""
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────
|
|
|
|
cleanup() {
|
|
echo ""
|
|
echo "=== cleanup ==="
|
|
[ -n "$SSE_PID" ] && kill "$SSE_PID" 2>/dev/null || true
|
|
[ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true
|
|
[ -n "$TMPROOT" ] && rm -rf "$TMPROOT"
|
|
echo "traces saved to: $TRACE_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
|
|
check_prereqs() {
|
|
[ -d "$OPENCODE_SRC/packages/opencode/src" ] || die "OpenCode source not found at $OPENCODE_SRC"
|
|
command -v bun >/dev/null || die "bun not installed"
|
|
[ -f "$AUTH_SOURCE" ] || die "auth file not found at $AUTH_SOURCE"
|
|
[ -d "$LAB_RAT_DIR" ] || die "lab-rat project not found at $LAB_RAT_DIR"
|
|
command -v jq >/dev/null || die "jq not installed"
|
|
command -v curl >/dev/null || die "curl not installed"
|
|
}
|
|
|
|
# POST/GET with logging
|
|
api() {
|
|
local method="$1" path="$2" flow="$3"
|
|
shift 3
|
|
local url="http://127.0.0.1:$PORT$path"
|
|
local log_file="$TRACE_DIR/flow-${flow}.jsonl"
|
|
|
|
local response
|
|
if [ "$method" = "GET" ]; then
|
|
response=$(curl -sS -w '\n{"_http_code":%{http_code}}' \
|
|
-H "x-opencode-directory: $LAB_RAT_DIR" \
|
|
"$url" "$@" 2>&1)
|
|
else
|
|
response=$(curl -sS -w '\n{"_http_code":%{http_code}}' \
|
|
-X "$method" \
|
|
-H "Content-Type: application/json" \
|
|
-H "x-opencode-directory: $LAB_RAT_DIR" \
|
|
"$url" "$@" 2>&1)
|
|
fi
|
|
|
|
# Log the raw exchange
|
|
local entry
|
|
entry=$(jq -cn \
|
|
--arg method "$method" \
|
|
--arg path "$path" \
|
|
--arg time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
--arg response "$response" \
|
|
'{method: $method, path: $path, time: $time, response: $response}')
|
|
echo "$entry" >> "$log_file"
|
|
|
|
# Return just the response body (strip the http_code line)
|
|
echo "$response" | head -n -1
|
|
}
|
|
|
|
wait_for_server() {
|
|
echo "waiting for server on port $PORT..."
|
|
local tries=0
|
|
while ! curl -s "http://127.0.0.1:$PORT/path" \
|
|
-H "x-opencode-directory: $LAB_RAT_DIR" >/dev/null 2>&1; do
|
|
tries=$((tries + 1))
|
|
[ "$tries" -gt 30 ] && die "server did not start within 30s"
|
|
sleep 1
|
|
done
|
|
echo "server ready"
|
|
}
|
|
|
|
wait_for_idle() {
|
|
local session_id="$1"
|
|
local timeout="${2:-60}"
|
|
local start=$SECONDS
|
|
echo " waiting for session $session_id to go idle..."
|
|
while true; do
|
|
local elapsed=$(( SECONDS - start ))
|
|
[ "$elapsed" -gt "$timeout" ] && die "session did not go idle within ${timeout}s"
|
|
|
|
# Check the SSE log for idle status
|
|
if grep -q "\"session.status\"" "$TRACE_DIR/sse-raw.log" 2>/dev/null; then
|
|
local last_status
|
|
last_status=$(grep "\"session.status\"" "$TRACE_DIR/sse-raw.log" | tail -1 | \
|
|
sed 's/^data: //' | jq -r '.properties.status.type // empty' 2>/dev/null || true)
|
|
if [ "$last_status" = "idle" ]; then
|
|
echo " session idle"
|
|
return 0
|
|
fi
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
}
|
|
|
|
reply_permission() {
|
|
local flow="$1" reply="${2:-once}"
|
|
echo " checking for pending permissions..."
|
|
sleep 1 # give it a moment
|
|
local perms
|
|
perms=$(api GET "/permission" "$flow")
|
|
local perm_id
|
|
perm_id=$(echo "$perms" | jq -r '.[0].id // empty' 2>/dev/null || true)
|
|
if [ -n "$perm_id" ]; then
|
|
echo " replying '$reply' to permission $perm_id"
|
|
api POST "/permission/$perm_id/reply" "$flow" \
|
|
-d "{\"reply\":\"$reply\"}"
|
|
else
|
|
echo " no pending permissions"
|
|
fi
|
|
}
|
|
|
|
reply_question() {
|
|
local flow="$1"
|
|
echo " checking for pending questions..."
|
|
sleep 1
|
|
local questions
|
|
questions=$(api GET "/question" "$flow" 2>/dev/null || echo "[]")
|
|
local q_id
|
|
q_id=$(echo "$questions" | jq -r '.[0].id // empty' 2>/dev/null || true)
|
|
if [ -n "$q_id" ]; then
|
|
# auto-answer with the first option for each question
|
|
local answers
|
|
answers=$(echo "$questions" | jq -c '[.[0].questions[] | [.options[0].label]]')
|
|
echo " replying to question $q_id with: $answers"
|
|
api POST "/question/$q_id/reply" "$flow" \
|
|
-d "{\"answers\":$answers}"
|
|
else
|
|
echo " no pending questions"
|
|
fi
|
|
}
|
|
|
|
# ── Setup ───────────────────────────────────────────────────
|
|
|
|
check_prereqs
|
|
|
|
# Create isolated temp root
|
|
TMPROOT=$(mktemp -d /tmp/opencode-trace.XXXXXX)
|
|
mkdir -p "$TMPROOT"/{share/opencode,cache,config,state,profile}
|
|
cp "$AUTH_SOURCE" "$TMPROOT/share/opencode/auth.json"
|
|
|
|
# Create output dir
|
|
mkdir -p "$TRACE_DIR"
|
|
|
|
echo "=== OpenCode Protocol Tracing ==="
|
|
echo " source: $OPENCODE_SRC"
|
|
echo " lab-rat: $LAB_RAT_DIR"
|
|
echo " tmproot: $TMPROOT"
|
|
echo " output: $TRACE_DIR"
|
|
echo ""
|
|
|
|
# Find a free port if PORT=0
|
|
if [ "$PORT" = "0" ]; then
|
|
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()')
|
|
fi
|
|
|
|
# Save setup info
|
|
jq -n \
|
|
--arg opencode_src "$OPENCODE_SRC" \
|
|
--arg lab_rat "$LAB_RAT_DIR" \
|
|
--arg tmproot "$TMPROOT" \
|
|
--arg port "$PORT" \
|
|
--arg provider "$PROVIDER" \
|
|
--arg model "$MODEL" \
|
|
--arg timestamp "$TIMESTAMP" \
|
|
'{opencode_src: $opencode_src, lab_rat: $lab_rat, tmproot: $tmproot,
|
|
port: ($port | tonumber), provider: $provider, model: $model,
|
|
timestamp: $timestamp}' \
|
|
> "$TRACE_DIR/setup.json"
|
|
|
|
# ── Start server ────────────────────────────────────────────
|
|
|
|
echo "=== starting opencode server on port $PORT ==="
|
|
|
|
XDG_DATA_HOME="$TMPROOT/share" \
|
|
XDG_CACHE_HOME="$TMPROOT/cache" \
|
|
XDG_CONFIG_HOME="$TMPROOT/config" \
|
|
XDG_STATE_HOME="$TMPROOT/state" \
|
|
OPENCODE_CONFIG_DIR="$TMPROOT/profile" \
|
|
OPENCODE_DB="$TMPROOT/share/opencode/opencode.db" \
|
|
bun run --cwd "$OPENCODE_SRC/packages/opencode" --conditions=browser src/index.ts \
|
|
serve --hostname 127.0.0.1 --port "$PORT" --print-logs --log-level DEBUG \
|
|
> "$TRACE_DIR/server.log" 2>&1 &
|
|
SERVER_PID=$!
|
|
|
|
wait_for_server
|
|
|
|
# Start SSE listener
|
|
echo "=== starting SSE listener ==="
|
|
curl -sS -N "http://127.0.0.1:$PORT/event" \
|
|
-H "x-opencode-directory: $LAB_RAT_DIR" \
|
|
> "$TRACE_DIR/sse-raw.log" 2>&1 &
|
|
SSE_PID=$!
|
|
sleep 0.5
|
|
|
|
# ── Parse flow arguments ────────────────────────────────────
|
|
|
|
FLOWS=("$@")
|
|
[ ${#FLOWS[@]} -eq 0 ] && FLOWS=(all)
|
|
|
|
run_flow() {
|
|
local name="$1"
|
|
case "$name" in
|
|
all)
|
|
run_flow permission
|
|
run_flow media
|
|
run_flow subtask
|
|
run_flow todo
|
|
;;
|
|
|
|
permission)
|
|
echo ""
|
|
echo "=== Flow: permission ==="
|
|
echo " creating session with edit ask rule..."
|
|
local session
|
|
session=$(api POST "/session" permission \
|
|
-d "{\"title\":\"trace-permission\",\"permission\":[{\"permission\":\"edit\",\"pattern\":\"*\",\"action\":\"ask\"}]}")
|
|
local sid
|
|
sid=$(echo "$session" | jq -r '.id')
|
|
echo " session: $sid"
|
|
|
|
echo " sending prompt..."
|
|
api POST "/session/$sid/prompt_async" permission \
|
|
-d "{\"agent\":\"build\",\"model\":{\"providerID\":\"$PROVIDER\",\"modelID\":\"$MODEL\"},\"parts\":[{\"type\":\"text\",\"text\":\"Create a file named TRACE_PERM.md with exactly one line: permission trace. Then reply with one short sentence.\"}]}"
|
|
|
|
sleep 2
|
|
reply_permission permission once
|
|
wait_for_idle "$sid" 60
|
|
|
|
echo " fetching messages..."
|
|
api GET "/session/$sid/message" permission
|
|
|
|
echo " fetching children..."
|
|
api GET "/session/$sid/children" permission
|
|
|
|
echo " flow: permission done"
|
|
;;
|
|
|
|
media)
|
|
echo ""
|
|
echo "=== Flow: media ==="
|
|
local session
|
|
session=$(api POST "/session" media \
|
|
-d '{"title":"trace-media"}')
|
|
local sid
|
|
sid=$(echo "$session" | jq -r '.id')
|
|
echo " session: $sid"
|
|
|
|
# Test with a tiny inline PNG (1x1 red pixel)
|
|
local tiny_png="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
|
|
|
echo " sending prompt with inline image..."
|
|
api POST "/session/$sid/prompt_async" media \
|
|
-d "{\"agent\":\"build\",\"model\":{\"providerID\":\"$PROVIDER\",\"modelID\":\"$MODEL\"},\"parts\":[{\"type\":\"text\",\"text\":\"Describe the attached image in one short sentence. Do not use any tools.\"},{\"type\":\"file\",\"mime\":\"image/png\",\"filename\":\"tiny.png\",\"url\":\"data:image/png;base64,$tiny_png\"}]}"
|
|
|
|
wait_for_idle "$sid" 60
|
|
|
|
echo " fetching messages..."
|
|
api GET "/session/$sid/message" media
|
|
|
|
echo " flow: media done"
|
|
;;
|
|
|
|
subtask)
|
|
echo ""
|
|
echo "=== Flow: subtask ==="
|
|
local session
|
|
session=$(api POST "/session" subtask \
|
|
-d '{"title":"trace-subtask"}')
|
|
local sid
|
|
sid=$(echo "$session" | jq -r '.id')
|
|
echo " session: $sid"
|
|
|
|
echo " sending prompt with @explore agent..."
|
|
api POST "/session/$sid/prompt_async" subtask \
|
|
-d "{\"agent\":\"build\",\"model\":{\"providerID\":\"$PROVIDER\",\"modelID\":\"$MODEL\"},\"parts\":[{\"type\":\"text\",\"text\":\"Find the main files in this tiny project and report back briefly.\"},{\"type\":\"agent\",\"name\":\"explore\"}]}"
|
|
|
|
wait_for_idle "$sid" 90
|
|
|
|
echo " fetching messages..."
|
|
api GET "/session/$sid/message" subtask
|
|
|
|
echo " fetching children..."
|
|
api GET "/session/$sid/children" subtask
|
|
|
|
echo " flow: subtask done"
|
|
;;
|
|
|
|
todo)
|
|
echo ""
|
|
echo "=== Flow: todo ==="
|
|
local session
|
|
session=$(api POST "/session" todo \
|
|
-d '{"title":"trace-todo"}')
|
|
local sid
|
|
sid=$(echo "$session" | jq -r '.id')
|
|
echo " session: $sid"
|
|
|
|
echo " sending prompt asking to create todos..."
|
|
api POST "/session/$sid/prompt_async" todo \
|
|
-d "{\"agent\":\"build\",\"model\":{\"providerID\":\"$PROVIDER\",\"modelID\":\"$MODEL\"},\"parts\":[{\"type\":\"text\",\"text\":\"Look at this project and create a todo list with 3 items about improvements. Use the todowrite tool.\"}]}"
|
|
|
|
# todowrite might need permission
|
|
sleep 3
|
|
reply_permission todo once
|
|
wait_for_idle "$sid" 60
|
|
|
|
echo " fetching messages..."
|
|
api GET "/session/$sid/message" todo
|
|
|
|
echo " fetching todos..."
|
|
api GET "/session/$sid/todo" todo
|
|
|
|
echo " flow: todo done"
|
|
;;
|
|
|
|
question)
|
|
echo ""
|
|
echo "=== Flow: question ==="
|
|
local session
|
|
session=$(api POST "/session" question \
|
|
-d '{"title":"trace-question"}')
|
|
local sid
|
|
sid=$(echo "$session" | jq -r '.id')
|
|
echo " session: $sid"
|
|
|
|
echo " sending prompt that should trigger question tool..."
|
|
api POST "/session/$sid/prompt_async" question \
|
|
-d "{\"agent\":\"build\",\"model\":{\"providerID\":\"$PROVIDER\",\"modelID\":\"$MODEL\"},\"parts\":[{\"type\":\"text\",\"text\":\"I want to set up a database for this project. Ask me which database I prefer before proceeding. Use the question tool.\"}]}"
|
|
|
|
sleep 3
|
|
reply_question question
|
|
# might also need permission for the question tool
|
|
reply_permission question once
|
|
wait_for_idle "$sid" 60
|
|
|
|
echo " fetching messages..."
|
|
api GET "/session/$sid/message" question
|
|
|
|
echo " flow: question done"
|
|
;;
|
|
|
|
*)
|
|
die "unknown flow: $name (available: permission, media, subtask, todo, question, all)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Run flows ───────────────────────────────────────────────
|
|
|
|
for flow in "${FLOWS[@]}"; do
|
|
run_flow "$flow"
|
|
done
|
|
|
|
echo ""
|
|
echo "=== all flows complete ==="
|
|
echo ""
|
|
echo "Output:"
|
|
echo " $TRACE_DIR/setup.json — runtime config"
|
|
echo " $TRACE_DIR/flow-*.jsonl — per-flow request/response logs"
|
|
echo " $TRACE_DIR/sse-raw.log — raw SSE event stream"
|
|
echo " $TRACE_DIR/server.log — server stdout/stderr"
|
|
echo ""
|
|
echo "Quick inspection:"
|
|
echo " jq -s '.' $TRACE_DIR/flow-permission.jsonl | less"
|
|
echo " grep 'data:' $TRACE_DIR/sse-raw.log | jq -r '.type' | sort | uniq -c | sort -rn"
|