@sema-agent/core
Version:
Stateless, task-oriented AI agent core
181 lines (180 loc) • 6.04 kB
JavaScript
import { encodeFrame, createFrameDecoder } from "./frame-decoder.js";
import { killProcessTree } from "../execution-env/kill-tree.js";
import { brandLspFailure } from "../../core/lsp.js";
import { asThrownError } from "../../core/thrown-value.js";
export class StdioLspTransport {
child;
requestTimeoutMs;
nextId = 0;
pending = new Map();
isClosed = false;
decode = createFrameDecoder();
notificationHandlers = new Set();
constructor(child, requestTimeoutMs = 30_000) {
this.child = child;
this.requestTimeoutMs = requestTimeoutMs;
child.stdout.on("data", (chunk) => {
for (const frame of this.decode(chunk))
this.onMessage(frame);
});
child.stdin.on("error", (e) => this.failAll(toError(e)));
const stdout = child.stdout;
if (typeof stdout.removeListener === "function" && typeof stdout.once === "function") {
for (const event of ["end", "close", "error"]) {
stdout.on(event, (arg) => {
if (Buffer.isBuffer(arg))
return;
this.failAll(new Error(`LSP server stdout ${event}`));
});
}
}
child.on("exit", () => this.failAll(new Error("LSP server exited")));
child.on("error", (e) => this.failAll(toError(e)));
}
get closed() {
return this.isClosed;
}
onMessage(raw) {
let msg;
try {
msg = JSON.parse(raw);
}
catch {
return;
}
if (msg.method != null && msg.id != null) {
if (!this.isClosed) {
try {
this.send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Method not found: ${String(msg.method)}` } });
}
catch (e) {
this.failAll(toError(e));
}
}
return;
}
if (typeof msg.id === "number") {
const p = this.pending.get(msg.id);
if (p) {
this.pending.delete(msg.id);
if (msg.error) {
const message = msg.error.message ?? "LSP error";
p.reject(brandLspFailure(new Error(message), jsonRpcReason(msg.error.code), message));
}
else
p.resolve(msg.result);
}
return;
}
if (typeof msg.method === "string" && this.notificationHandlers.size > 0) {
const params = msg.params;
for (const handler of this.notificationHandlers) {
try {
handler(msg.method, params);
}
catch {
}
}
}
}
onNotification(handler) {
this.notificationHandlers.add(handler);
return () => this.notificationHandlers.delete(handler);
}
failAll(err) {
this.isClosed = true;
brandLspFailure(err, "server_terminated");
for (const p of this.pending.values())
p.reject(err);
this.pending.clear();
}
send(msg) {
this.child.stdin.write(encodeFrame(JSON.stringify(msg)));
}
request(method, params, signal) {
if (this.isClosed)
return Promise.reject(brandLspFailure(new Error("LSP transport closed"), "server_terminated"));
const id = ++this.nextId;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (this.pending.delete(id)) {
signal?.removeEventListener("abort", onAbort);
reject(brandLspFailure(new Error(`LSP ${method} timed out after ${this.requestTimeoutMs}ms`), "timeout"));
}
}, this.requestTimeoutMs);
timer.unref?.();
const cleanup = () => {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
};
const onAbort = () => {
if (this.pending.delete(id)) {
cleanup();
this.notify("$/cancelRequest", { id });
reject(brandLspFailure(new Error(`LSP ${method} aborted`), "cancelled"));
}
};
this.pending.set(id, {
resolve: (v) => {
cleanup();
resolve(v);
},
reject: (e) => {
cleanup();
reject(e);
},
});
if (signal) {
if (signal.aborted)
return onAbort();
signal.addEventListener("abort", onAbort, { once: true });
}
try {
this.send({ jsonrpc: "2.0", id, method, params });
}
catch (e) {
this.failAll(brandLspFailure(asThrownError(e), "server_terminated"));
}
});
}
notify(method, params) {
if (this.isClosed)
return;
try {
this.send({ jsonrpc: "2.0", method, params });
}
catch (e) {
this.failAll(toError(e));
}
}
async close() {
this.failAll(new Error("LSP transport closed"));
try {
if (process.platform === "win32" && this.child.pid)
killProcessTree(this.child.pid, { force: true });
else
this.child.kill();
}
catch {
}
}
}
function jsonRpcReason(code) {
if (code === -32601)
return "unsupported_operation";
if (code === -32800 || code === -32802)
return "cancelled";
return "server_error";
}
function toError(e) {
try {
if (e instanceof Error)
return e;
const m = e?.message;
if (typeof m === "string" && m)
return new Error(m);
}
catch {
}
return asThrownError(e);
}