/** * Server Factory * * Creates an SSE server that wraps an Gizmo runtime and streams state updates. */ import { Hono } from "hono"; import { cors } from "hono/cors"; import type { Runtime, Action } from "@gizmo-ai/runtime"; import type { Server, ServerConfig } from "./types.ts"; import { createEventsRoute } from "./routes/events.ts"; import { createInvokeRoute } from "./routes/invoke.ts"; import { createAbortRoute } from "./routes/abort.ts"; import { createStateRoute } from "./routes/state.ts"; import { createRunsRoute } from "./routes/runs.ts"; import { createHydrateRoute } from "./routes/hydrate.ts"; import { createManifestRoute } from "./routes/manifest.ts"; import { createContractsRoute } from "./routes/contracts.ts"; import { createStreamStateRoute } from "./routes/stream-state.ts"; import { createStreamActionsRoute } from "./routes/stream-actions.ts"; import { safeSerialize } from "./lib/serialization.ts"; /** * Create an SSE server for an Gizmo runtime * * Usage: * ```typescript * // 1. Create server plugin first * const serverPlugin = createServerPlugin({ slices: ["execution", "agent"] }); * * // 2. Create runtime with plugin * const runtime = createRuntime({ * plugins: [agent, serverPlugin.plugin], * }); * * // 3. Create and start server * const server = createServer({ * runtime, * plugin: serverPlugin, * }); * * server.listen(3000); * ``` */ export function createServer< S extends Record, A extends Action >(config: ServerConfig): Server { const { runtime, plugin, heartbeatInterval = 30000, basePath = "", serializer = safeSerialize, agUIAdapterBus, manifest, contracts = [], } = config; const { bus, slices, resolvedSlices } = plugin; // Check if history is enabled (in-memory or persistence) const historyEnabled = plugin.historySize > 0 || plugin.persistence !== null; // Create Hono app const app = new Hono(); // Add CORS middleware app.use("*", cors()); // Mount routes const prefix = basePath.replace(/\/$/, ""); // Remove trailing slash // Stream routes app.route( `${prefix}/stream/state`, createStreamStateRoute({ runtime, bus, slices, resolvedSlices, heartbeatInterval, serializer, }) ); app.route( `${prefix}/stream/actions`, createStreamActionsRoute({ runtime, bus, heartbeatInterval, serializer, }) ); // Combined events stream (AG-UI compatible) app.route( `${prefix}/events`, createEventsRoute({ runtime, bus, slices, resolvedSlices, heartbeatInterval, serializer, }) ); // Mount AG-UI events route if adapter bus is provided if (agUIAdapterBus) { // Lazy load to avoid requiring @gizmo-ai/ag-ui-adapter-plugin when not used import("./routes/ag-ui-events.ts").then(({ createAGUIEventsRoute }) => { app.route( `${prefix}/events/ag-ui`, createAGUIEventsRoute({ runtime, bus: agUIAdapterBus, slices, heartbeatInterval, serializer, }) ); }).catch((error) => { console.warn("AG-UI adapter route not available:", error.message); }); } app.route(`${prefix}/invoke`, createInvokeRoute(runtime)); app.route(`${prefix}/abort`, createAbortRoute(runtime)); app.route(`${prefix}/state`, createStateRoute(runtime, serializer)); app.route(`${prefix}/runs`, createRunsRoute({ getRunHistory: plugin.getRunHistory, persistence: plugin.persistence, runtime, })); app.route(`${prefix}/hydrate`, createHydrateRoute({ runtime, persistence: plugin.persistence, })); // Health check app.get(`${prefix}/health`, (c) => c.json({ status: "ok" })); // Manifest endpoint app.route( `${prefix}/.well-known/manifest.json`, createManifestRoute({ runtime, manifestConfig: manifest, basePath: prefix, historyEnabled, }) ); // Contracts endpoint (public action contracts) if (contracts.length > 0) { app.route( `${prefix}/contracts`, createContractsRoute({ contracts }) ); } // Server control let server: ReturnType | null = null; const listen = (port: number) => { server = Bun.serve({ port, fetch: app.fetch, idleTimeout: 255, // Max allowed by Bun (4.25 minutes) }); console.log(`Gizmo server listening on http://localhost:${port}`); }; const close = () => { if (server) { server.stop(); server = null; } }; return { app, listen, close, }; }