Files
homelab/k8s/apps/agent-pod/hub-configmap.yaml
T

120 lines
4.2 KiB
YAML
Raw Normal View History

apiVersion: v1
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}`));
kind: ConfigMap
metadata:
name: hub-src
namespace: agent-pod