UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

117 lines (116 loc) • 3.76 kB
import http from "node:http"; import { thrownText } from "../core/thrown-value.js"; const DEFAULT_MAX_BODY = 5 * 1024 * 1024; class HttpError extends Error { status; constructor(status, message) { super(message); this.status = status; this.name = "HttpError"; } } async function readJson(req, maxBytes) { const chunks = []; let size = 0; for await (const chunk of req) { size += chunk.length; if (size > maxBytes) { throw new HttpError(413, "request body too large"); } chunks.push(chunk); } if (chunks.length === 0) { return {}; } try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch (e) { throw new HttpError(400, `invalid JSON request body: ${thrownText(e)}`); } } function setCors(res, origin) { if (!origin) { return; } res.setHeader("access-control-allow-origin", origin); res.setHeader("access-control-allow-methods", "POST, OPTIONS"); res.setHeader("access-control-allow-headers", "content-type, authorization"); } function sendJson(res, status, body) { const data = JSON.stringify(body); res.writeHead(status, { "content-type": "application/json" }); res.end(data); } export function createTaskServer(opts) { const maxBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY; return http.createServer((req, res) => { void handle(req, res).catch((err) => { if (!res.headersSent) { const status = err instanceof HttpError ? err.status : 500; sendJson(res, status, { error: thrownText(err) }); } else { res.end(); } }); }); async function handle(req, res) { setCors(res, opts.corsOrigin); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } if (opts.authorize && !(await opts.authorize(req))) { sendJson(res, 401, { error: "unauthorized" }); return; } if (req.method !== "POST") { sendJson(res, 405, { error: "method not allowed" }); return; } const url = req.url ?? "/"; const streaming = url === "/task/stream" || url.startsWith("/task/stream?"); const oneShot = url === "/task" || url.startsWith("/task?"); if (!streaming && !oneShot) { sendJson(res, 404, { error: "not found" }); return; } const body = (await readJson(req, maxBytes)); if (!body || typeof body.objective !== "string") { sendJson(res, 400, { error: "missing 'objective' string" }); return; } const spec = await opts.resolveSpec(body, req); if (!streaming) { const result = await opts.runner.runTask(spec); sendJson(res, 200, result); return; } res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive", }); const stream = opts.runner.runTaskStream(spec); let clientGone = false; res.on("close", () => { if (!res.writableEnded) { clientGone = true; stream.interrupt().catch(() => { }); void stream.destroy(); } }); for await (const ev of stream) { if (clientGone || res.writableEnded || res.destroyed) { break; } res.write(`data: ${JSON.stringify(ev)}\n\n`); } if (!res.writableEnded) { res.end(); } } }