@sema-agent/core
Version:
Stateless, task-oriented AI agent core
278 lines (277 loc) • 12.3 kB
JavaScript
import { uriToPath, buildRequest, callHierarchyMethod, parseResult, pathToUri } from "./lsp-protocol.js";
import { lspFailureOf, MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "./lsp.js";
import { thrownText } from "./thrown-value.js";
const UTF8 = new TextDecoder("utf-8", { ignoreBOM: true });
async function readHeldText(held, signal) {
const bytes = await held.readBytes(MAX_LSP_FILE_BYTES, signal);
if (!bytes.ok)
throw bytes.error;
return UTF8.decode(bytes.value);
}
const SUPERSEDED = Symbol("superseded");
const WARMUP_MAX_ATTEMPTS = 3;
export class TransportLspSession {
transport;
languageId;
readText;
log;
warmup;
opened = new Map();
openedUris = new Map();
carried = new Set();
syncGen = new Map();
syncing = new Map();
createdAt = Date.now();
warmupBarrier;
warmupDone = false;
warmupAttempts = 0;
constructor(transport, languageId, readText, log = () => { }, warmup = { windowMs: 10_000, retryMs: 1_000 }, diagnostics) {
this.transport = transport;
this.languageId = languageId;
this.readText = readText;
this.log = log;
this.warmup = warmup;
if (diagnostics && transport.onNotification) {
transport.onNotification((method, params) => {
if (method !== "textDocument/publishDiagnostics")
return;
const p = params;
if (!p || typeof p.uri !== "string" || !Array.isArray(p.diagnostics))
return;
if (typeof p.version === "number") {
const filePath = this.openedUris.get(p.uri) ?? uriToPath(p.uri);
const known = this.opened.get(filePath);
if (known && p.version < known.version) {
this.log("lsp_diagnostics_stale_dropped", { uri: p.uri, version: p.version, current: known.version });
return;
}
}
diagnostics.publish(p.uri, p.diagnostics);
});
}
}
get inFlight() {
return this.activeRequests;
}
activeRequests = 0;
async request(op, params, signal, gate) {
this.activeRequests += 1;
try {
return await this.requestOnce(op, params, signal, gate);
}
finally {
this.activeRequests -= 1;
}
}
async requestOnce(op, params, signal, gate) {
const built = buildRequest(op, params);
if (!built)
return { kind: "none", reason: "unsupported_operation" };
const sync = await this.syncOpenedFiles(params.filePath, signal, gate);
if (!sync.ok) {
const reason = signal?.aborted ? "cancelled" : (sync.reason ?? "file_sync_failed");
this.log("lsp_op_degraded", { op, file: params.filePath, reason, err: sync.error });
return { kind: "none", reason, ...(reason === "file_sync_failed" ? { detail: sync.error } : {}) };
}
if (this.warmup.windowMs > 0 && !this.warmupDone) {
if (op === "goToDefinition")
this.warmupDone = true;
else
await this.warmupBarrierOnce(params.filePath, signal);
}
for (let attempt = 0;; attempt++) {
let raw;
let noHierarchyItem = false;
try {
raw = await this.transport.request(built.method, built.params, signal);
if (op === "incomingCalls" || op === "outgoingCalls") {
const items = Array.isArray(raw) ? raw : raw != null ? [raw] : [];
noHierarchyItem = items.length === 0;
raw = noHierarchyItem ? null : await this.transport.request(callHierarchyMethod(op), { item: items[0] }, signal);
}
}
catch (e) {
const f = lspFailureOf(e) ?? TransportLspSession.classifyUnbranded(signal, this.transport);
this.log("lsp_op_degraded", { op, file: params.filePath, reason: f.reason, err: thrownText(e) });
return { kind: "none", reason: f.reason, ...(f.detail !== undefined ? { detail: f.detail } : {}) };
}
const result = noHierarchyItem
? { kind: "none", reason: "no_call_hierarchy_item" }
: parseResult(op, raw, pathToUri(params.filePath));
if (result.kind === "none" && this.warmup.windowMs > 0 && Date.now() - this.createdAt < this.warmup.windowMs && !signal?.aborted) {
await new Promise((r) => setTimeout(r, this.warmup.retryMs));
continue;
}
this.log("lsp_op", {
op,
file: params.filePath,
...(params.line !== undefined ? { line: params.line } : {}),
...(params.character !== undefined ? { character: params.character } : {}),
result: result.kind,
n: result.kind === "locations" ? result.locations.length : result.kind === "symbols" ? result.symbols.length : undefined,
rawNull: raw == null,
...(attempt > 0 ? { warmupRetries: attempt } : {}),
});
return result;
}
}
async warmupBarrierOnce(filePath, signal) {
let entry = this.warmupBarrier;
if (entry && entry.scope.signal.aborted)
entry = undefined;
if (!entry) {
if (this.warmupAttempts >= WARMUP_MAX_ATTEMPTS)
return;
this.warmupAttempts += 1;
const scope = new SharedAbortScope();
const started = {};
const job = this.transport
.request("textDocument/definition", { textDocument: { uri: pathToUri(filePath) }, position: { line: 0, character: 0 } }, scope.signal)
.then(() => {
this.warmupDone = true;
})
.catch(() => {
if (this.warmupBarrier === started.self)
this.warmupBarrier = undefined;
});
entry = { job, scope };
started.self = entry;
this.warmupBarrier = entry;
}
entry.scope.join(signal);
await settleOnAbort(entry.job, signal, () => undefined);
}
async syncOpenedFiles(queried, signal, gate) {
const files = new Set([...this.opened.keys(), ...this.carried]);
files.add(queried);
const outcomes = await Promise.all([...files].map(async (fp) => [fp, await this.syncGated(fp, signal, gate)]));
return outcomes.find(([fp]) => fp === queried)?.[1] ?? { ok: true };
}
async syncGated(filePath, signal, gate) {
const outcome = await this.syncInTurn(filePath, signal, gate);
if (outcome.ok || signal?.aborted !== true)
this.carried.delete(filePath);
return outcome;
}
syncInTurn(filePath, signal, gate) {
const aborted = () => ({ ok: false, error: "aborted by the caller" });
const newest = this.syncing.get(filePath);
if (gate === undefined && newest?.byName === true && !newest.scope.signal.aborted) {
newest.scope.join(signal);
return settleOnAbort(newest.job, signal, aborted);
}
const gen = (this.syncGen.get(filePath) ?? 0) + 1;
this.syncGen.set(filePath, gen);
const isNewest = () => this.syncGen.get(filePath) === gen;
const scope = new SharedAbortScope();
scope.join(signal);
const entry = { scope, byName: gate === undefined, job: Promise.resolve({ ok: true }) };
entry.job = (async () => {
let published;
if (gate === undefined)
published = await this.publishRead(filePath, () => this.readText(filePath, scope.signal), isNewest);
else {
const admission = await gate(filePath, scope.signal).catch((e) => ({ ok: false, reason: `the read boundary could not judge this file (${thrownText(e)})` }));
if (!admission.ok) {
if (isNewest() && !scope.signal.aborted)
this.closeRefused(filePath);
return { ok: false, error: admission.reason };
}
const held = admission.object;
published = await this.publishRead(filePath, () => readHeldText(held, scope.signal).finally(() => held.close().catch(() => undefined)), isNewest);
}
if (published !== SUPERSEDED)
return published;
if (scope.signal.aborted)
return aborted();
const successor = this.syncing.get(filePath);
if (successor !== undefined && successor !== entry && !successor.scope.signal.aborted) {
successor.scope.join(scope.signal);
return successor.job;
}
return this.syncInTurn(filePath, scope.signal, gate);
})();
this.syncing.set(filePath, entry);
const settled = entry.job.finally(() => {
if (this.syncing.get(filePath) === entry)
this.syncing.delete(filePath);
});
return settleOnAbort(settled, signal, aborted);
}
closeRefused(filePath) {
this.carried.delete(filePath);
if (!this.opened.has(filePath))
return;
const uri = pathToUri(filePath);
this.opened.delete(filePath);
this.openedUris.delete(uri);
this.transport.notify("textDocument/didClose", { textDocument: { uri } });
this.log("lsp_doc_closed_refused", { file: filePath });
}
async publishRead(filePath, read, isNewest) {
let text;
try {
text = await read();
}
catch (e) {
const error = thrownText(e);
this.log("lsp_sync_skipped", { file: filePath, error, opened: this.opened.has(filePath) });
return { ok: false, error };
}
if (!isNewest())
return SUPERSEDED;
const cur = this.opened.get(filePath);
const uri = pathToUri(filePath);
if (!cur) {
this.transport.notify("textDocument/didOpen", {
textDocument: { uri, languageId: this.languageId, version: 1, text },
});
if (this.transport.closed === true) {
this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didOpen" });
return { ok: false, error: "didOpen was not delivered (transport closed)", reason: "server_terminated" };
}
this.opened.set(filePath, { version: 1, text });
this.openedUris.set(uri, filePath);
}
else if (cur.text !== text) {
const version = cur.version + 1;
this.transport.notify("textDocument/didChange", { textDocument: { uri, version }, contentChanges: [{ text }] });
if (this.transport.closed === true) {
this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didChange", version });
return { ok: false, error: "didChange was not delivered (transport closed)", reason: "server_terminated" };
}
this.opened.set(filePath, { version, text });
this.log("lsp_file_resync", { file: filePath, version });
}
return { ok: true };
}
static classifyUnbranded(signal, transport) {
if (signal?.aborted)
return { reason: "cancelled" };
if (transport.closed === true)
return { reason: "server_terminated" };
return { reason: "server_error" };
}
get closed() {
return this.transport.closed === true;
}
openedFiles() {
return [...new Set([...this.opened.keys(), ...this.carried])];
}
async warmOpen(filePaths, _signal) {
let deferred = 0;
for (const fp of filePaths) {
if (this.opened.has(fp) || this.carried.has(fp))
continue;
this.carried.add(fp);
deferred += 1;
}
return { opened: 0, failed: 0, deferred };
}
notifyFileChanged(filePath, signal, gate) {
return this.syncGated(filePath, signal, gate).then(() => undefined);
}
close() {
return this.transport.close();
}
}