UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

110 lines (109 loc) • 4.38 kB
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "./untrusted-text.js"; const MAX_PER_FILE = 10; const MAX_TOTAL = 30; const MAX_SUMMARY_CHARS = 4000; function severitySymbol(severity) { switch (severity) { case 1: return "✖"; case 2: return "⚠"; case 3: return "ℹ"; case 4: return "★"; default: return "●"; } } function diagnosticKey(uri, d) { const r = d.range ? `${d.range.start.line}:${d.range.start.character}-${d.range.end?.line ?? ""}:${d.range.end?.character ?? ""}` : ""; return `${uri}|${d.message}|${d.severity ?? ""}|${d.source ?? ""}|${d.code ?? ""}|${r}`; } export class LspDiagnosticsRegistry { pending = new Map(); delivered = new Map(); publish(uri, diagnostics) { if (diagnostics.length === 0) this.pending.delete(uri); else this.pending.set(uri, diagnostics); } fileEdited(runIdent, uri) { const forRun = this.delivered.get(runIdent); if (forRun === undefined) return; const prefix = `${uri}|`; for (const key of forRun) { if (key.startsWith(prefix)) forRun.delete(key); } if (forRun.size === 0) this.delivered.delete(runIdent); } releaseRun(runIdent) { this.delivered.delete(runIdent); } isEmpty() { return this.pending.size === 0; } drain(runIdent) { const out = []; const requeued = new Map(); let delivered = this.delivered.get(runIdent); let total = 0; for (const [uri, all] of this.pending) { const fresh = all .filter((d) => delivered?.has(diagnosticKey(uri, d)) !== true) .sort((a, b) => (a.severity ?? 99) - (b.severity ?? 99)); if (fresh.length === 0) continue; const room = Math.max(0, Math.min(MAX_PER_FILE, MAX_TOTAL - total)); const take = fresh.slice(0, room); if (fresh.length > take.length) requeued.set(uri, fresh.slice(take.length)); if (take.length === 0) continue; if (delivered === undefined) { delivered = new Set(); this.delivered.set(runIdent, delivered); } for (const d of take) delivered.add(diagnosticKey(uri, d)); total += take.length; out.push({ uri, diagnostics: take }); } this.pending.clear(); for (const [uri, rest] of requeued) this.pending.set(uri, rest); return out; } } export async function withholdListedDiagnostics(files, withheldOf) { if (files.length === 0) return files; const withheld = await withheldOf(files.map((f) => f.uri)).catch(() => undefined); return withheld === undefined ? [] : files.filter((f) => !withheld.has(f.uri)); } function basenameOfUri(uri) { const path = uri.replace(/^file:\/\//, ""); const idx = path.lastIndexOf("/"); return idx >= 0 ? path.slice(idx + 1) : path; } export function formatDiagnosticsSummary(files) { const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS); const lines = []; for (const f of files) { lines.push(`${safe(basenameOfUri(f.uri))}:`); for (const d of f.diagnostics) { const startLine = d.range?.start?.line; const startChar = d.range?.start?.character; const pos = typeof startLine === "number" && Number.isFinite(startLine) && typeof startChar === "number" && Number.isFinite(startChar) ? `[Line ${Math.trunc(startLine) + 1}:${Math.trunc(startChar) + 1}] ` : ""; const code = d.code !== undefined ? ` [${safe(String(d.code))}]` : ""; const source = d.source ? ` (${safe(String(d.source))})` : ""; lines.push(` ${severitySymbol(d.severity)} ${pos}${safe(String(d.message))}${code}${source}`); } } const summary = lines.join("\n"); return summary.length > MAX_SUMMARY_CHARS ? `${summary.slice(0, MAX_SUMMARY_CHARS)}…[truncated]` : summary; } export function formatDiagnosticsBlock(files) { return `<new-diagnostics>The following new diagnostic issues were detected:\n\n${formatDiagnosticsSummary(files)}</new-diagnostics>`; }