// Standalone agent-runtime process (Phase 5b of the pi-parity refactor).//// Why this exists: Next 16's standalone server (`npm run start`) buffers// locally-generated SSE in route handlers — only proxied/pass-through upstream// streams flush. Running the runtime here and letting the Next routes proxy// (`return new Response(upstream.body, …)`) is the pass-through case that// streams. The in-Next path (transpilePackages) remains the default; this// process is opt-in via LOCAL_STUDIO_AGENT_RUNTIME_URL on the Next side.//// Routes mirror the Next paths one-to-one so the proxy is a pathname-preserving// passthrough. Handlers are the same functions the Next routes call in-process// (src/http/handlers.ts, src/http/browser-handlers.ts).//// Security: this is a localhost sidecar. It binds 127.0.0.1 ONLY and performs// no authentication of its own — the Next layer keeps running// requireApiAccess() on the privileged routes before proxying, and nothing// else can reach this port off-box. Do not bind 0.0.0.0 without adding auth.//// Runtime: node + tsc build (dist/). NOT tsx (cannot load @earendil-works/// pi-ai's entrypoint) and NOT bun (node-pty / Chrome-spawn risk).import { serve } from "@hono/node-server";import { Hono } from "hono";import { handleAgentAbort, handleAgentCompact, handleAgentTurn, handleRuntimeEvents, handleRuntimeSessions, handleRuntimeStatus, handleSetupChecks,} from "./http/handlers";import { handleBrowserFetch, handleBrowserFrame, handleBrowserInput, handleBrowserLocalhosts, handleBrowserState, handleBrowserVerb, handleBrowserViewport,} from "./http/browser-handlers";const app = new Hono();app.get("/health", (c) => c.json({ ok: true, service: "local-studio-agent-runtime", pid: process.pid }),);// The 7 runtime endpoints.app.post("/api/agent/turn", (c) => handleAgentTurn(c.req.raw));app.post("/api/agent/abort", (c) => handleAgentAbort(c.req.raw));app.post("/api/agent/compact", (c) => handleAgentCompact(c.req.raw));app.get("/api/agent/runtime/sessions", () => handleRuntimeSessions());app.get("/api/agent/runtime/status", (c) => handleRuntimeStatus(c.req.raw));app.get("/api/agent/runtime/events", (c) => handleRuntimeEvents(c.req.raw));app.get("/api/agent/setup-checks", () => handleSetupChecks());// Browser-host endpoints. The fixed paths must be registered before the// :verb catch-all so e.g. /browser/fetch is not treated as a verb.app.get("/api/agent/browser/fetch", (c) => handleBrowserFetch(c.req.raw));app.get("/api/agent/browser/frame", () => handleBrowserFrame());app.post("/api/agent/browser/input", (c) => handleBrowserInput(c.req.raw));app.get("/api/agent/browser/localhosts", (c) => handleBrowserLocalhosts(c.req.raw));app.get("/api/agent/browser/state", () => handleBrowserState());app.post("/api/agent/browser/viewport", (c) => handleBrowserViewport(c.req.raw));app.post("/api/agent/browser/:verb", (c) => handleBrowserVerb(c.req.raw, c.req.param("verb")));const port = Number(process.env.PORT) > 0 ? Number(process.env.PORT) : 8081;serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => { console.log( `[agent-runtime] listening on http://127.0.0.1:${info.port} (pid ${process.pid}, node ${process.version})`, );});
// Standalone agent-runtime process (Phase 5b of the pi-parity refactor).
//
// Why this exists: Next 16's standalone server (`npm run start`) buffers
// locally-generated SSE in route handlers — only proxied/pass-through upstream
// streams flush. Running the runtime here and letting the Next routes proxy
// (`return new Response(upstream.body, …)`) is the pass-through case that
// streams. The in-Next path (transpilePackages) remains the default; this
// process is opt-in via LOCAL_STUDIO_AGENT_RUNTIME_URL on the Next side.
//
// Routes mirror the Next paths one-to-one so the proxy is a pathname-preserving
// passthrough. Handlers are the same functions the Next routes call in-process
// (src/http/handlers.ts, src/http/browser-handlers.ts).
//
// Security: this is a localhost sidecar. It binds 127.0.0.1 ONLY and performs
// no authentication of its own — the Next layer keeps running
// requireApiAccess() on the privileged routes before proxying, and nothing
// else can reach this port off-box. Do not bind 0.0.0.0 without adding auth.
//
// Runtime: node + tsc build (dist/). NOT tsx (cannot load @earendil-works/
// pi-ai's entrypoint) and NOT bun (node-pty / Chrome-spawn risk).
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import {
handleAgentAbort,
handleAgentCompact,
handleAgentTurn,
handleRuntimeEvents,
handleRuntimeSessions,
handleRuntimeStatus,
handleSetupChecks,
} from "./http/handlers";
import {
handleBrowserFetch,
handleBrowserFrame,
handleBrowserInput,
handleBrowserLocalhosts,
handleBrowserState,
handleBrowserVerb,
handleBrowserViewport,
} from "./http/browser-handlers";
const app = new Hono();
app.get("/health", (c) =>
c.json({ ok: true, service: "local-studio-agent-runtime", pid: process.pid }),
);
// The 7 runtime endpoints.
app.post("/api/agent/turn", (c) => handleAgentTurn(c.req.raw));
app.post("/api/agent/abort", (c) => handleAgentAbort(c.req.raw));
app.post("/api/agent/compact", (c) => handleAgentCompact(c.req.raw));
app.get("/api/agent/runtime/sessions", () => handleRuntimeSessions());
app.get("/api/agent/runtime/status", (c) => handleRuntimeStatus(c.req.raw));
app.get("/api/agent/runtime/events", (c) => handleRuntimeEvents(c.req.raw));
app.get("/api/agent/setup-checks", () => handleSetupChecks());
// Browser-host endpoints. The fixed paths must be registered before the
// :verb catch-all so e.g. /browser/fetch is not treated as a verb.
app.get("/api/agent/browser/fetch", (c) => handleBrowserFetch(c.req.raw));
app.get("/api/agent/browser/frame", () => handleBrowserFrame());
app.post("/api/agent/browser/input", (c) => handleBrowserInput(c.req.raw));
app.get("/api/agent/browser/localhosts", (c) => handleBrowserLocalhosts(c.req.raw));
app.get("/api/agent/browser/state", () => handleBrowserState());
app.post("/api/agent/browser/viewport", (c) => handleBrowserViewport(c.req.raw));
app.post("/api/agent/browser/:verb", (c) => handleBrowserVerb(c.req.raw, c.req.param("verb")));
const port = Number(process.env.PORT) > 0 ? Number(process.env.PORT) : 8081;
serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
console.log(
`[agent-runtime] listening on http://127.0.0.1:${info.port} (pid ${process.pid}, node ${process.version})`,
);
});