Makes the Web UI reflect what agents are actually doing: reactions
on DMs that trigger a subprocess run, a per-run detail page that
shows the exact prompt the model received and the raw response, and
cross-linked reactive_runs ↔ harness_runs data for a single composite
API call.
Migration 020 (internal/storage/schema/020_harness_run_detail.sql):
ALTER TABLE harness_runs ADD COLUMN reactive_run_id INTEGER;
ALTER TABLE harness_runs ADD COLUMN prompt TEXT;
ALTER TABLE harness_runs ADD COLUMN response TEXT;
CREATE INDEX idx_harness_runs_reactive ON harness_runs(reactive_run_id);
internal/harness:
* ExecRequest.ReactiveRunID — reactor pins the reactive_runs row id
so the observer can JOIN the two tables.
* ExecResult.Prompt / Response — the subprocess harness reads
prompt.txt / response.txt that wrappers write into the workdir,
and runs.Store persists them (capped at 32 KiB each).
* runs.Run struct now has JSON tags — previously the API returned
PascalCase field names that didn't match the Web UI's snake_case
TypeScript types.
* New runs.Store.GetByReactiveRunID for the composite API endpoint.
* Test schema updated to include the new columns.
internal/reactor:
* New ReactionNotifier interface + SetReactionNotifier.
* dispatchHarness now reacts `in_progress` on the triggering DM
before spawning the goroutine.
* runHarness reacts `done` on success, `reject` on failure. The
existing reactionPriority ordering means the terminal reaction
wins for badge display — no need to remove in_progress first.
* dispatchHarness sets ExecRequest.ReactiveRunID.
cmd/synapbus/main.go:
* reactorReactionAdapter: adapts reactions.Service.Toggle to the
reactor's one-shot AddReaction signature.
* HarnessRunsStore wired into the API router config.
internal/api/runs_handler.go — GetRun composite endpoint:
The GET /api/runs/{id} response now returns everything the Web UI
needs to render the run detail page in one call:
{
"run": <reactive_runs row>,
"harness_run": <linked harness_runs row with prompt/response>,
"agent": <current agent snapshot with harness_config_json>,
"trigger_message": <DM that started the run>,
"outgoing_message": <first DM the agent produced after startedAt>
}
The outgoing-message lookup wraps both sides of the created_at
comparison in datetime() so SQLite parses the stored 'YYYY-MM-DD
HH:MM:SS' and the Go-emitted RFC3339 into the same canonical form
before comparing — a raw string compare was silently returning no
rows.
internal/api/router.go: HarnessRunsStore field in RouterConfig, wired
through to NewRunsHandler.
examples/cold-topic-explainer/wrapper.sh:
Writes prompt.txt and response.txt alongside gemini.stdout.raw so
the subprocess harness can capture "what the model saw" and "what
the model said" post-hoc.
web/src/lib/components/MessageList.svelte:
New ReactionPills render below each message body when the message
carries a `reactions` array (already populated by
EnrichMessages/ReactionEnricher on the server side). Makes the
👀 in_progress / ✔ done / ❌ reject lifecycle visible in every DM
view and conversation.
web/src/routes/runs/[id]/+page.svelte (NEW):
New run detail page at /runs/:id with sections:
1. Header strip — agent, status pill, backend badge, trigger
info, duration, tokens in/out, cost, exit code, trace id.
2. Triggering message — body + sender.
3. What the model saw — GEMINI.md / CLAUDE.md from agent snapshot
+ the captured rendered prompt (byte count on each summary
bar, collapsible details).
4. What the model said — captured response, falling back to
logs_excerpt or error_log when unavailable.
5. Outgoing message — body + recipient + status.
6. Metadata — reactive_run.id, harness_run.run_id, backend,
session_id, tokens_cached, k8s_job, agent trigger config.
Styled against the existing dark tailwind system — no design
overhaul, fits the current aesthetic (editorial sectioning,
monospace for code-like content, accent-blue for links,
accent-purple for system-instructions, accent-green for model
output, accent-red for errors).
web/src/routes/runs/+page.svelte: the inline expand panel now has
a "View full details →" link next to the Retry button.
E2E VERIFIED on a live subprocess run:
* Topic: "why does the subprocess harness materialise GEMINI.md
alongside .gemini/settings.json in the per-run workdir?"
* 3 subprocess runs + 3 reactive_runs + 3 harness_runs, all linked.
* message_reactions: 6 rows — in_progress + done for each hop.
* GET /api/runs/1 returns a composite with
harness_run.prompt=883 bytes, harness_run.response=550 bytes,
reactive_run_id=1, trigger_message populated, outgoing_message
populated (decomposer-pro → writer-flash), agent.gemini_md=747
bytes. All keys are snake_case as the Svelte types expect.
Full go test ./... green. `vite build` green. Demo instance still
running on port 18088 for browser verification.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.5 KiB
Bash
Executable File
104 lines
3.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# Subprocess-harness wrapper for cold-topic-explainer Gemini agents.
|
|
#
|
|
# The subprocess harness execs this with cwd = per-run workdir. The
|
|
# workdir already contains GEMINI.md and message.json, written by
|
|
# MaterialiseAgentConfig and the harness itself. Required env vars are
|
|
# supplied by the agent's harness_config_json.env block (see
|
|
# configs/*.json):
|
|
#
|
|
# AGENT_ROLE decomposer | writer | critic
|
|
# AGENT_NAME this agent's synapbus name
|
|
# GEMINI_MODEL e.g. gemini-2.5-pro
|
|
# NEXT_AGENT the agent to DM on the happy path
|
|
# REVISE_AGENT (critic only) the agent to DM when asking for fixes
|
|
# OWNER_AGENT (critic only) the agent to DM with FINAL: results
|
|
# SYNAPBUS_SOCKET full path to the synapbus admin unix socket
|
|
# SYNAPBUS_BIN path to the synapbus CLI (used to send messages)
|
|
#
|
|
# All agents then: read the DM body, call gemini headless with GEMINI.md
|
|
# + the body, post-process, and hand off via `synapbus messages send`
|
|
# over the admin socket.
|
|
|
|
set -eu
|
|
|
|
log() {
|
|
printf '[wrapper %s] %s\n' "${AGENT_NAME:-?}" "$*" >&2
|
|
}
|
|
|
|
# --- read the triggering DM -------------------------------------------
|
|
if [ ! -f message.json ]; then
|
|
log "no message.json in workdir; refusing to fabricate a task"
|
|
exit 2
|
|
fi
|
|
BODY=$(jq -r '.body' < message.json)
|
|
FROM=$(jq -r '.from_agent' < message.json)
|
|
|
|
log "received from=$FROM bytes=$(printf '%s' "$BODY" | wc -c)"
|
|
|
|
# --- call gemini ------------------------------------------------------
|
|
# -y / --approval-mode yolo means "don't prompt" — safe because we're
|
|
# not giving gemini any tools to call in this workflow.
|
|
PROMPT="$(cat GEMINI.md)
|
|
|
|
Incoming DM from @${FROM}:
|
|
${BODY}"
|
|
|
|
# Preserve the exact prompt the model received — the subprocess
|
|
# harness reads prompt.txt after the run completes and stores it in
|
|
# harness_runs.prompt so the Web UI can show "what the model saw".
|
|
printf '%s' "$PROMPT" > prompt.txt
|
|
|
|
set +e
|
|
RAW=$(gemini -m "$GEMINI_MODEL" --approval-mode yolo -p "$PROMPT" 2>gemini.stderr.log)
|
|
GEMINI_EXIT=$?
|
|
set -e
|
|
|
|
# Gemini prepends "MCP issues detected. Run /mcp list for status." to
|
|
# stdout when its MCP config can't reach a server. Strip it.
|
|
RESPONSE=$(printf '%s' "$RAW" | sed 's|^MCP issues detected\. Run /mcp list for status\.||')
|
|
|
|
# Save both the raw and the cleaned response. `response.txt` is the
|
|
# one the harness persists into harness_runs.response.
|
|
printf '%s' "$RAW" > gemini.stdout.raw
|
|
printf '%s' "$RESPONSE" > response.txt
|
|
|
|
if [ -z "$RESPONSE" ]; then
|
|
log "empty gemini response (exit=$GEMINI_EXIT); last stderr:"
|
|
tail -20 gemini.stderr.log >&2 || true
|
|
exit 3
|
|
fi
|
|
|
|
log "gemini response bytes=$(printf '%s' "$RESPONSE" | wc -c)"
|
|
|
|
# Save full response for forensics.
|
|
printf '%s' "$RESPONSE" > result.md
|
|
printf '%s\n' "$RESPONSE"
|
|
|
|
# --- decide who to DM next --------------------------------------------
|
|
TO="$NEXT_AGENT"
|
|
if [ "$AGENT_ROLE" = "critic" ]; then
|
|
# Critic's prompt tells gemini to prefix FINAL: or REVISE:.
|
|
case "$RESPONSE" in
|
|
FINAL:*|*"FINAL:"*|Final:*|*"Final:"*)
|
|
TO="$OWNER_AGENT"
|
|
log "verdict=FINAL → $TO"
|
|
;;
|
|
*)
|
|
TO="$REVISE_AGENT"
|
|
log "verdict=REVISE → $TO"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# --- hand off ----------------------------------------------------------
|
|
printf '%s' "$RESPONSE" | "$SYNAPBUS_BIN" --socket "$SYNAPBUS_SOCKET" messages send \
|
|
--from "$AGENT_NAME" \
|
|
--to "$TO" \
|
|
--priority 5 >&2 || {
|
|
log "admin socket send failed — check $SYNAPBUS_SOCKET"
|
|
exit 4
|
|
}
|
|
|
|
log "handed off to $TO"
|