UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

181 lines (180 loc) • 6.04 kB
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); }