Files
BlackBean/pi-coop/supervisor.sh
T
2026-08-14 23:41:57 +08:00

208 lines
8.4 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# pi 协作 supervisor(单 agent 版)
#
# 在容器内运行一个 pi headless 进程完成 /blackboard/task.md 中的任务。
# agent 的工作区为 /blackboard/workspace,产物与轮次日志保留在黑板上;
# 任务完成(agent 用 blackboard 工具创建 DONE)后,supervisor 把结果
# 以结构化 JSONresult.json)落盘并打印一行摘要,供外部调度方直接解析。
#
# 与 pigo-coop supervisor 的差异(因 pi CLI 与 pigo 不同):
# 1. cwdpi 用进程 cwd 作工作区(无 -C 参数),supervisor 先 cd 到
# $BB/workspace 再启动 pi。
# 2. 模型接入:pi 用 provider 体系,没有 --base-url/--protocol。
# 由 coop extension-e /extensions/coop.ts)读 BASE_URL/API_KEY/
# PROTOCOL/MODEL 环境变量注册 "coop" providerCLI 用
# --provider coop --model "$MODEL"。
# 3. 输出格式:pi 用 --mode jsonNDJSON),首行是 session header
# {"type":"session","id":"<uuid>",...}supervisor 从首行提取 id
# 供下一轮 --session-id 恢复。
# 4. session 恢复:pi 用 --session-id <id> + --session-dir <dir>
# 替代 pigo 的 --resume <id>。
#
# 环境变量(均由外部调用方传入,与 pigo-coop 完全一致):
# MODEL 模型名,如 deepseek-chat (必填)
# BASE_URL OpenAI/Anthropic 兼容 API base-url (必填)
# API_KEY API key (必填)
# PROTOCOL 协议 openai|anthropic (可选,默认按 model 推断)
# TASK 任务描述 (必填;或挂载 $BB/task.md
# BLACKBOARD 黑板目录 (默认 /blackboard
# PROMPTS 协作 prompt 目录 (默认 /prompts
# EXTENSIONS extension 目录 (默认 /extensions
# ROUND_MAX 最大轮次 (默认 20)
# TIMEOUT 单轮超时秒数,0=不超时 (默认 1800)
# FAIL_MODE agent 失败时:stop=立即退出 | continue=继续下一轮(默认)
set -u
BB="${BLACKBOARD:-/blackboard}"
PROMPTS="${PROMPTS:-/prompts}"
EXTENSIONS="${EXTENSIONS:-/extensions}"
ROUND_MAX="${ROUND_MAX:-20}"
TIMEOUT="${TIMEOUT:-1800}"
FAIL_MODE="${FAIL_MODE:-continue}"
log() { echo "[supervisor] $*"; }
# emit_result 把任务结果以结构化 JSON 写入 $BB/result.json 并打印一行摘要。
# status: solved | unsolved | error | timeout
emit_result() {
local status="$1" summary="$2" code="$3"
local flag="" artifacts_json="[]" summary_json='""' flag_json='""'
# flag 优先从 summary 提取,其次在 workspace 产物中全量检索
if [ -n "$summary" ]; then
flag=$(printf '%s' "$summary" | grep -oE 'flag\{[^}]+\}' | head -1)
fi
if [ -z "$flag" ] && [ -d "$BB/workspace" ]; then
flag=$(grep -rhoE 'flag\{[^}]+\}' "$BB/workspace" 2>/dev/null | head -1)
fi
# 产物清单:workspace 下的全部文件(最多 200 个),经 python3 转义为 JSON 数组
if [ -d "$BB/workspace" ]; then
artifacts_json=$(
cd "$BB/workspace" && find . -type f 2>/dev/null | sed 's|^\./||' | head -200 |
python3 -c 'import json,sys; print(json.dumps([l.rstrip("\n") for l in sys.stdin]))' 2>/dev/null
)
[ -z "$artifacts_json" ] && artifacts_json="[]"
fi
# summary 截断到 8192 字符并经 python3 转义,防止引号/换行破坏 JSON
summary_json=$(printf '%s' "$summary" | head -c 8192 | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
[ -z "$summary_json" ] && summary_json='""'
flag_json=$(printf '%s' "$flag" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
[ -z "$flag_json" ] && flag_json='""'
cat > "$BB/result.json" <<EOF
{
"status": "$status",
"exit_code": $code,
"summary": $summary_json,
"flag": $flag_json,
"artifacts": $artifacts_json
}
EOF
log "===== 协作结束 ====="
log "status=$status exit_code=$code"
[ -n "$flag" ] && log "flag=$flag"
log "结构化结果已写入:$BB/result.json"
}
# ---- 前置校验 ----
if [ -z "${MODEL:-}" ] || [ -z "${BASE_URL:-}" ] || [ -z "${API_KEY:-}" ]; then
echo "错误:必须提供 MODEL / BASE_URL / API_KEY 环境变量" >&2
exit 2
fi
if [ -z "${TASK:-}" ] && [ ! -f "$BB/task.md" ]; then
echo "错误:请通过 TASK 环境变量或挂载 $BB/task.md 提供任务" >&2
exit 2
fi
if [ -f "$BB/DONE" ]; then
log "黑板已有完成标记,如需重新开始请删除 $BB/DONE"
exit 0
fi
# ---- 初始化黑板 ----
mkdir -p "$BB/workspace" "$BB/logs" "$BB/sessions"
if [ -n "${TASK:-}" ]; then
printf '%s\n' "$TASK" > "$BB/task.md"
fi
# 协议注入:agent 的 cwd 是 $BB/workspaceAGENTS.md 注入链从 cwd 起,
# 因此把协议副本放进工作区(勿修改,它是每轮系统提示的一部分)
cp "$PROMPTS/AGENTS.md" "$BB/workspace/AGENTS.md"
log "黑板初始化完成:$BB"
log "任务:$(head -c 200 "$BB/task.md")"
# 从 pi --mode json 输出的首行(session header)提取 session id。
# 首行形如 {"type":"session","version":3,"id":"<uuid>","cwd":...}
extract_session_id() {
local logfile="$1"
head -1 "$logfile" 2>/dev/null | python3 -c '
import json, sys
try:
d = json.loads(sys.stdin.read())
if d.get("type") == "session":
print(d.get("id", ""))
except Exception:
pass
' 2>/dev/null
}
# ---- 单轮运行:一个 pi 进程,--mode json 输出 NDJSON(首行携带 session id----
run_agent() {
local round="$1"
local session_file="$BB/sessions/agent.session"
local task_prompt="轮次 $round 开始。先读取 /blackboard/task.md 中的任务,检查工作区已有产物与 DONE 状态,然后继续推进任务。全部工作完成、交付物完整时,用 blackboard 工具 action=done 创建完成标记(summary 写最终交付总结,含关键结果/flag/提交响应/产物清单)。"
local args=(-p "$task_prompt" -a --mode json \
--provider coop --model "$MODEL" \
--session-dir "$BB/sessions" \
-e "$EXTENSIONS/coop.ts" \
--append-system-prompt "$PROMPTS/agent.md")
if [ -s "$session_file" ]; then
# --session-id:精确匹配,找不到则用该 id 新建(不报错),比 --session 更健壮
args+=(--session-id "$(cat "$session_file")")
fi
export ROUND="$round" NAME="agent" BB="$BB"
local logfile="$BB/logs/round-$round.log"
log "第 $round 轮开始运行 agent(日志:$logfile"
# pi 用进程 cwd 作为工作区,必须先进入 $BB/workspace
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null; then
(cd "$BB/workspace" && timeout "$TIMEOUT" pi "${args[@]}") > "$logfile" 2>&1
else
(cd "$BB/workspace" && pi "${args[@]}") > "$logfile" 2>&1
fi
local rc=$?
# 从 --mode json 首行(session header)提取 session id 供下一轮恢复
local sid
sid=$(extract_session_id "$logfile")
if [ -n "$sid" ]; then
printf '%s' "$sid" > "$session_file"
fi
return $rc
}
# ---- 主循环:逐轮运行 agent,直到 DONE 或达到轮次上限 ----
for round in $(seq 1 "$ROUND_MAX"); do
[ -f "$BB/DONE" ] && break
log "===== 第 $round 轮开始 ====="
run_agent "$round"
rc=$?
if [ "$rc" -eq 0 ]; then
log "第 $round 轮完成"
else
log "第 $round 轮失败(exit=$rc),日志见 $BB/logs/round-$round.log"
if [ "$FAIL_MODE" = "stop" ]; then
# 兜底:agent 虽超时/失败,但黑板已有提交成功证据(workspace 内 *.md 含 correct:true
# → 视为完成正常退出,避免"flag 已提交却因未写 DONE 被强杀(exit 143"。
_done=0
for _f in "$BB"/workspace/*.md "$BB"/messages/*.md; do
[ -f "$_f" ] && grep -q '"correct":true' "$_f" && _done=1 && break
done
if [ "$_done" = "1" ]; then
log "检测到提交成功证据,自动标记完成"
{ echo "Task finished (auto-detected submit success)"; } > "$BB/DONE"
emit_result solved "$(cat "$BB/DONE")" 0
exit 0
fi
if [ "$rc" -eq 124 ]; then
emit_result timeout "" "$rc"
else
emit_result error "agent 退出码 $rc,日志见 logs/round-$round.log" "$rc"
fi
exit "$rc"
fi
fi
done
if [ -f "$BB/DONE" ]; then
log "检测到完成标记,任务结束"
emit_result solved "$(cat "$BB/DONE")" 0
exit 0
fi
log "达到最大轮次 $ROUND_MAX 仍未完成,请检查黑板产物与日志"
emit_result unsolved "" 1
exit 1