The coordinator now reaches SynapBus's MCP endpoint directly from inside the Gemini session. wrapper.sh's coordinator branch is a pure pass-through — no more JSON-plan parsing. When the coordinator runs, Gemini connects to /mcp with the coordinator's own Bearer API key and calls `create_goal`, `propose_task_tree`, and `send_message` as native tools. Goal rows, task trees, and DMs all land in the DB in one in-session flow. - start.sh mints a fresh API key for goal-coordinator via `agent revoke-key` and substitutes it into configs/coordinator.json (plus the port) at apply_config time. - coordinator.json declares the synapbus MCP server in mcp_servers; the subprocess harness already writes .gemini/settings.json from that array, so gemini picks it up automatically. - GEMINI.md rewritten to instruct the model to call MCP tools instead of emitting a JSON action blob. Stdout is explicitly discarded; every reply goes through send_message. - wrapper.sh coordinator branch is ~15 lines: invoke gemini, log, exit. Inspector + critic keep the legacy JSON-plan pattern since they're workers with fixed contracts. - SYNAPBUS_KEEP_WORKDIR=1 preserves per-run workdirs for debugging MCP traces, gemini output, and materialized configs. - Reactor checkPendingWork now fires after subprocess run completion (previously only K8s poller hit this path). The synthetic coalesced trigger uses a `__coalesced__` sentinel instead of `system` so it bypasses the FromAgent=="system" dispatch guard. Verified e2e (with rate-limit-induced retries): - TRIVIAL: "what is 2+2?" → coordinator send_message(algis, "4") - INFEASIBLE: "Transfer \$50…" → coordinator send_message(algis, "CANNOT: …") - SINGLE-STEP: 3-node task tree materialized in goal_tasks, TASK JSON forwarded to generic-inspector → critic-auditor chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
145 lines
5.5 KiB
Bash
Executable File
145 lines
5.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# wrapper.sh — harness-agnostic entry point for every agent in the
|
|
# goal-coordinator example. The subprocess harness execs this with
|
|
# cwd = per-run workdir containing GEMINI.md, .gemini/settings.json
|
|
# (MCP config), and message.json.
|
|
#
|
|
# Dispatches by $AGENT_ROLE:
|
|
# coordinator → pass-through: run gemini with MCP tools, let the
|
|
# model call send_message/create_goal/propose_task_tree
|
|
# directly via the synapbus MCP server.
|
|
# inspector → parse task JSON, run, forward result JSON to critic
|
|
# critic → audit, DM owner on FINAL or re-brief inspector on REVISE
|
|
#
|
|
# The inspector and critic still use the old "emit JSON, wrapper
|
|
# dispatches" pattern because they're workers with a fixed contract.
|
|
# Only the coordinator owns real decision-making, and only it needs
|
|
# MCP-native tool calls.
|
|
|
|
set -eu
|
|
|
|
log() { printf '[wrapper %s] %s\n' "${AGENT_NAME:-?}" "$*" >&2; }
|
|
|
|
[ -f message.json ] || { log "no message.json"; exit 2; }
|
|
|
|
BODY=$(jq -r '.body' < message.json)
|
|
FROM=$(jq -r '.from_agent' < message.json)
|
|
|
|
log "role=$AGENT_ROLE from=$FROM body_bytes=$(printf '%s' "$BODY" | wc -c)"
|
|
|
|
# --- build prompt -----------------------------------------------------
|
|
PROMPT="$(cat GEMINI.md)
|
|
|
|
Incoming DM from @${FROM}:
|
|
${BODY}"
|
|
|
|
printf '%s' "$PROMPT" > prompt.txt
|
|
|
|
# --- coordinator: MCP pass-through -----------------------------------
|
|
# The harness materializes .gemini/settings.json from the agent's
|
|
# mcp_servers config, so Gemini picks up the synapbus MCP server on
|
|
# its own. We just run it and let the model drive — every side-effect
|
|
# (send_message, create_goal, propose_task_tree) is an MCP tool call.
|
|
if [ "$AGENT_ROLE" = "coordinator" ]; then
|
|
log "coordinator pass-through: invoking gemini with MCP tools"
|
|
set +e
|
|
gemini -m "$GEMINI_MODEL" --approval-mode yolo -p "$PROMPT" \
|
|
>gemini.stdout.log 2>gemini.stderr.log
|
|
CLI_EXIT=$?
|
|
set -e
|
|
log "coordinator gemini exited=$CLI_EXIT stdout=$(wc -c < gemini.stdout.log 2>/dev/null || echo 0)B"
|
|
if [ "$CLI_EXIT" -ne 0 ]; then
|
|
tail -20 gemini.stderr.log >&2 || true
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# --- inspector + critic: legacy JSON-plan pattern --------------------
|
|
set +e
|
|
RAW=$(gemini -m "$GEMINI_MODEL" --approval-mode yolo -p "$PROMPT" 2>gemini.stderr.log)
|
|
CLI_EXIT=$?
|
|
set -e
|
|
|
|
# Strip the MCP-warning preamble Gemini prepends when its MCP config
|
|
# can't reach a server. (Inspector + critic don't use MCP from inside
|
|
# gemini; their orchestration happens in this wrapper.)
|
|
RAW=$(printf '%s' "$RAW" | sed 's|^MCP issues detected\. Run /mcp list for status\.||')
|
|
printf '%s' "$RAW" > gemini.stdout.raw
|
|
|
|
# --- extract the first JSON object from the response -----------------
|
|
# Models wrap JSON in ```json fences sometimes; strip them.
|
|
RESPONSE=$(printf '%s' "$RAW" \
|
|
| sed -E 's/^```(json)?//' \
|
|
| sed -E 's/```$//' \
|
|
| awk 'BEGIN{d=0;c=0} { for(i=1;i<=length($0);i++){ch=substr($0,i,1); if(c==0 && ch=="{") c=1; if(c){printf "%s",ch; if(ch=="{")d++; else if(ch=="}"){d--; if(d==0){print ""; exit}}}} if(c&&d>0) print ""}')
|
|
|
|
if [ -z "$RESPONSE" ]; then
|
|
log "empty response from $GEMINI_MODEL (exit=$CLI_EXIT); tail of stderr:"
|
|
tail -10 gemini.stderr.log >&2 || true
|
|
exit 3
|
|
fi
|
|
|
|
printf '%s' "$RESPONSE" > response.txt
|
|
log "response bytes=$(printf '%s' "$RESPONSE" | wc -c)"
|
|
|
|
# --- shortcut helper --------------------------------------------------
|
|
send_dm() {
|
|
# $1 = to, $2 = body (stdin)
|
|
"$SYNAPBUS_BIN" --socket "$SYNAPBUS_SOCKET" messages send \
|
|
--from "$AGENT_NAME" \
|
|
--to "$1" \
|
|
--priority 5 >&2 || {
|
|
log "admin socket send failed (to=$1)"
|
|
return 4
|
|
}
|
|
}
|
|
|
|
# --- dispatch by role -------------------------------------------------
|
|
case "$AGENT_ROLE" in
|
|
inspector)
|
|
# Pass the full JSON response forward to the critic — the critic's
|
|
# GEMINI.md is set up to parse it. Also carry the critic_brief
|
|
# from the original task through unchanged.
|
|
CRITIC_BRIEF=$(printf '%s' "$BODY" | jq -r '.critic_brief // empty')
|
|
PAYLOAD=$(printf '%s' "$RESPONSE" | jq -c --arg cb "$CRITIC_BRIEF" '. + {critic_brief:$cb, from_inspector:"generic-inspector"}')
|
|
log "forwarding inspector result to $NEXT_AGENT"
|
|
printf '%s' "$PAYLOAD" | send_dm "$NEXT_AGENT"
|
|
;;
|
|
|
|
critic)
|
|
VERDICT=$(printf '%s' "$RESPONSE" | jq -r '.verdict // "UNKNOWN"')
|
|
case "$VERDICT" in
|
|
FINAL|Final|final)
|
|
FINAL_SUMMARY=$(printf '%s' "$RESPONSE" | jq -r '.final_summary // .reason // "approved"')
|
|
log "verdict=FINAL → $OWNER_AGENT"
|
|
printf 'FINAL: %s' "$FINAL_SUMMARY" | send_dm "$OWNER_AGENT"
|
|
;;
|
|
REVISE|Revise|revise)
|
|
PATCH=$(printf '%s' "$RESPONSE" | jq -r '.patch // .reason // "please revise"')
|
|
TASK_ID=$(printf '%s' "$RESPONSE" | jq -r '.task_id // 0')
|
|
log "verdict=REVISE → $INSPECTOR_AGENT"
|
|
# Re-brief the inspector with the patch.
|
|
REVISE_MSG=$(jq -nc \
|
|
--arg t "$TASK_ID" \
|
|
--arg brief "Revision requested by critic: $PATCH" \
|
|
'{task_id:($t|tonumber), goal_title:"revision", brief:$brief, acceptance_criteria:"address the critic patch"}')
|
|
printf '%s' "$REVISE_MSG" | send_dm "$INSPECTOR_AGENT"
|
|
# Also tell the owner we're iterating.
|
|
printf 'REVISING: %s' "$PATCH" | send_dm "$OWNER_AGENT"
|
|
;;
|
|
*)
|
|
log "critic emitted unknown verdict: $VERDICT"
|
|
printf 'CRITIC_ERROR: %s' "$RESPONSE" | send_dm "$OWNER_AGENT"
|
|
exit 6
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
*)
|
|
log "unknown AGENT_ROLE: $AGENT_ROLE"
|
|
exit 7
|
|
;;
|
|
esac
|
|
|
|
log "done"
|