2026-08-16 07:55:33 -07:00
|
|
|
apiVersion: v1
|
2026-08-16 10:24:03 -07:00
|
|
|
data:
|
|
|
|
|
hub.js: |
|
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// agent-hub: lives inside the pi container (not a sidecar) so it can spawn
|
|
|
|
|
// `pi -p --mode json` directly. One persistent in-cluster service --
|
|
|
|
|
// POST /run to trigger an agent, GET /console (WebSocket) to watch every
|
|
|
|
|
// concurrent run live as it happens, relaying pi's own session protocol
|
|
|
|
|
// verbatim (same event shape Claude Code sessions use).
|
|
|
|
|
const http = require("node:http");
|
|
|
|
|
const crypto = require("node:crypto");
|
|
|
|
|
const { spawn } = require("node:child_process");
|
|
|
|
|
const readline = require("node:readline");
|
|
|
|
|
const { WebSocketServer } = require("ws");
|
|
|
|
|
|
|
|
|
|
const PORT = process.env.HUB_PORT || 9090;
|
|
|
|
|
|
|
|
|
|
const sessions = new Map(); // id -> {id, agent, status, events, startedAt, endedAt}
|
|
|
|
|
const viewers = new Set(); // WebSocket connections watching /console
|
|
|
|
|
|
|
|
|
|
function broadcast(type, session) {
|
|
|
|
|
const msg = JSON.stringify({ type, session });
|
|
|
|
|
for (const ws of viewers) {
|
|
|
|
|
if (ws.readyState === ws.OPEN) ws.send(msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startSession(agent) {
|
|
|
|
|
const id = crypto.randomUUID();
|
|
|
|
|
const session = { id, agent, status: "running", events: [], startedAt: new Date().toISOString() };
|
|
|
|
|
sessions.set(id, session);
|
|
|
|
|
broadcast("start", session);
|
|
|
|
|
return session;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addEvent(session, rawLine) {
|
|
|
|
|
session.events.push(JSON.parse(rawLine));
|
|
|
|
|
broadcast("event", session);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function endSession(session, status) {
|
|
|
|
|
session.status = status;
|
|
|
|
|
session.endedAt = new Date().toISOString();
|
|
|
|
|
broadcast("end", session);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spawns `pi -p --mode json <prompt>` and relays it live. Same shape as the
|
|
|
|
|
// old agent-run.js wrapper, just in-process instead of a separate exec.
|
|
|
|
|
function runAgent(agent, prompt, extraArgs = []) {
|
|
|
|
|
const session = startSession(agent);
|
|
|
|
|
const child = spawn("pi", ["-p", "--mode", "json", ...extraArgs, prompt], {
|
|
|
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
|
|
|
});
|
|
|
|
|
const rl = readline.createInterface({ input: child.stdout });
|
|
|
|
|
rl.on("line", (line) => {
|
|
|
|
|
if (!line.trim()) return;
|
|
|
|
|
try {
|
|
|
|
|
addEvent(session, line);
|
|
|
|
|
} catch {
|
|
|
|
|
// non-JSON stdout noise, ignore
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
child.stderr.on("data", (chunk) => process.stderr.write(chunk));
|
|
|
|
|
child.on("close", (code) => endSession(session, code === 0 ? "done" : "error"));
|
|
|
|
|
return session;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const server = http.createServer((req, res) => {
|
|
|
|
|
const url = new URL(req.url, "http://localhost");
|
|
|
|
|
|
|
|
|
|
if (url.pathname === "/healthz") {
|
|
|
|
|
res.writeHead(200).end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === "/sessions" && req.method === "GET") {
|
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
|
|
|
res.end(JSON.stringify([...sessions.values()]));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === "/run" && req.method === "POST") {
|
|
|
|
|
let body = "";
|
|
|
|
|
req.on("data", (chunk) => (body += chunk));
|
|
|
|
|
req.on("end", () => {
|
|
|
|
|
try {
|
|
|
|
|
const { agent, prompt, provider, model } = JSON.parse(body);
|
|
|
|
|
if (!agent || !prompt) throw new Error("agent and prompt are required");
|
|
|
|
|
const extraArgs = [];
|
|
|
|
|
if (provider) extraArgs.push("--provider", provider);
|
|
|
|
|
if (model) extraArgs.push("--model", model);
|
|
|
|
|
const session = runAgent(agent, prompt, extraArgs);
|
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
|
|
|
res.end(JSON.stringify({ id: session.id }));
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
|
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.writeHead(404).end();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wss = new WebSocketServer({ server, path: "/console" });
|
|
|
|
|
wss.on("connection", (ws) => {
|
|
|
|
|
for (const session of sessions.values()) {
|
|
|
|
|
ws.send(JSON.stringify({ type: "snapshot", session }));
|
|
|
|
|
}
|
|
|
|
|
viewers.add(ws);
|
|
|
|
|
ws.on("close", () => viewers.delete(ws));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
server.listen(PORT, () => console.log(`agent-hub listening on :${PORT}`));
|
2026-08-16 07:55:33 -07:00
|
|
|
kind: ConfigMap
|
|
|
|
|
metadata:
|
|
|
|
|
name: hub-src
|
|
|
|
|
namespace: agent-pod
|