feat(agent-pod): add agent-hub sidecar for multi-agent session monitoring

Hub relays pi's own --mode json session protocol (session/agent_start/
turn_start/message_*/turn_end/agent_end -- the same event shape Claude
Code sessions use) to agent-console over WebSocket, so concurrent pi
runs inside the pod are observable as real transcripts instead of log
tails. agent-run.js wraps `pi -p --mode json` and relays its stdout
lines to the hub; each invocation gets its own session id, so N
concurrent agents show up as N sessions. Source mounted via ConfigMap
and run with `go run .` (no registry yet, same as the pi container).
This commit is contained in:
Story Crater Bot
2026-08-16 07:55:33 -07:00
parent 0c52dec155
commit 7f8e947958
5 changed files with 319 additions and 0 deletions
@@ -0,0 +1,62 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: agent-run-script
namespace: agent-pod
data:
agent-run.js: |
#!/usr/bin/env node
// Spawns `pi -p --mode json` and relays its session-protocol events to
// agent-hub (localhost:9090, same pod) line by line as they're emitted, so
// agent-console sees the turn live instead of after the process exits.
//
// Usage: node agent-run.js <agent-name> [-- <extra pi args>...] <prompt>
const { spawn } = require("node:child_process");
const readline = require("node:readline");
const crypto = require("node:crypto");
const HUB = process.env.HUB_ADDR || "http://localhost:9090";
async function post(path, body) {
await fetch(`${HUB}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).catch((err) => console.error(`hub post ${path} failed:`, err.message));
}
async function main() {
const args = process.argv.slice(2);
const agent = args.shift();
if (!agent || args.length === 0) {
console.error("usage: agent-run.js <agent-name> [pi args...] <prompt>");
process.exit(1);
}
const id = crypto.randomUUID();
await post("/agent/start", { id, agent });
const child = spawn("pi", ["-p", "--mode", "json", ...args], {
stdio: ["ignore", "pipe", "pipe"],
});
const rl = readline.createInterface({ input: child.stdout });
rl.on("line", (line) => {
if (!line.trim()) return;
try {
JSON.parse(line); // pi emits one JSON object per line; validate before relay
post("/agent/event", { id, event: line });
} catch {
// non-JSON stdout noise, ignore
}
});
child.stderr.on("data", (chunk) => process.stderr.write(chunk));
child.on("close", async (code) => {
await post("/agent/end", { id, status: code === 0 ? "done" : "error" });
process.exit(code ?? 1);
});
}
main();