#!/usr/bin/env bash # pi 协作 supervisor(单 agent 版) # # 在容器内运行一个 pi headless 进程完成 /blackboard/task.md 中的任务。 # agent 的工作区为 /blackboard/workspace,产物与轮次日志保留在黑板上; # 任务完成(agent 用 blackboard 工具创建 DONE)后,supervisor 把结果 # 以结构化 JSON(result.json)落盘并打印一行摘要,供外部调度方直接解析。 # # 与 pigo-coop supervisor 的差异(因 pi CLI 与 pigo 不同): # 1. cwd:pi 用进程 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" provider;CLI 用 # --provider coop --model "$MODEL"。 # 3. 输出格式:pi 用 --mode json(NDJSON),首行是 session header # {"type":"session","id":"",...},supervisor 从首行提取 id # 供下一轮 --session-id 恢复。 # 4. session 恢复:pi 用 --session-id + --session-dir , # 替代 pigo 的 --resume 。 # # 环境变量(均由外部调用方传入,与 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" <&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/workspace,AGENTS.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":"","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