@sema-agent/core
Version:
Stateless, task-oriented AI agent core
117 lines (116 loc) • 3.76 kB
JavaScript
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();
}
}
}