@sema-agent/core
Version:
Stateless, task-oriented AI agent core
379 lines (378 loc) • 16.5 kB
JavaScript
export function summarizeRedactions(findings) {
if (findings.length === 0)
return "no redactions";
const counts = new Map();
for (const f of findings) {
const label = `${f.kind} (${f.confidence})`;
counts.set(label, (counts.get(label) ?? 0) + 1);
}
const parts = [...counts].map(([label, n]) => `${label} x${n}`);
return `${findings.length} redaction${findings.length === 1 ? "" : "s"}: ${parts.join(", ")}`;
}
function passZeroMapper(batches) {
const cursors = batches.map(() => ({ i: 0, delta: 0 }));
return (pos) => {
let p = pos;
for (let b = batches.length - 1; b >= 0; b--) {
const edits = batches[b];
const c = cursors[b];
let mapped;
while (mapped === undefined && c.i < edits.length) {
const e = edits[c.i];
const outStart = e.at + c.delta;
if (p < outStart)
mapped = p - c.delta;
else if (p < outStart + e.insertedLen)
mapped = e.at;
else {
c.delta += e.insertedLen - e.removedLen;
c.i++;
}
}
p = mapped ?? p - c.delta;
}
return p;
};
}
const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g;
const FORMAT_CHAR_RE = /\p{Cf}/u;
const MAX_FORMAT_SLOT_SCANS = 64;
const REDACTED_WHOLE = "[redacted]";
function finding(pass, span, family) {
const f = { kind: pass.kind, confidence: pass.confidence, span, marker: pass.marker, source: pass.source };
if (family !== undefined)
f.family = family;
return f;
}
function insertedFor(pass, match, groups) {
return typeof pass.replace === "string"
? pass.replace.replace(/\$(\d)/g, (_m, d) => groups[Number(d) - 1] ?? "")
: pass.replace(match, ...groups);
}
function removedRegion(pass, match, groups, inserted) {
if (typeof pass.replace === "string") {
const parts = pass.replace.split(/(\$\d)/).filter((p) => p !== "");
const isRef = (p) => /^\$\d$/.test(p);
const refText = (p) => groups[Number(p[1]) - 1] ?? "";
let head = "";
for (const p of parts) {
if (!isRef(p))
break;
head += refText(p);
}
let tail = "";
for (let i = parts.length - 1; i >= 0 && isRef(parts[i]); i--)
tail = refText(parts[i]) + tail;
if (parts.every(isRef))
return [0, match.length];
if (!match.startsWith(head))
head = "";
if (!match.endsWith(tail) || head.length + tail.length > match.length)
tail = "";
return [head.length, match.length - tail.length];
}
const markerAt = pass.marker === "" ? -1 : inserted.lastIndexOf(pass.marker);
const headStop = markerAt < 0 ? inserted.length : markerAt;
const tailStop = markerAt < 0 ? inserted.length : inserted.length - markerAt - pass.marker.length;
let head = 0;
while (head < match.length && head < headStop && match[head] === inserted[head])
head++;
let tail = 0;
while (tail < match.length - head && tail < inserted.length - head && tail < tailStop && match[match.length - 1 - tail] === inserted[inserted.length - 1 - tail])
tail++;
return [head, match.length - tail];
}
const SOLO_SCAN_WORK_PER_CHAR = 8;
const SOLO_SCAN_WORK_FLOOR = 1 << 17;
function soloHits(input, passes) {
const hits = [];
passes.forEach((pass, i) => {
const re = new RegExp(pass.re.source, pass.re.flags.includes("g") ? pass.re.flags : `${pass.re.flags}g`);
const rescans = pass.family !== undefined;
let work = Math.max(SOLO_SCAN_WORK_PER_CHAR * input.length, SOLO_SCAN_WORK_FLOOR);
for (let m = re.exec(input); m !== null; m = re.exec(input)) {
const at = m.index;
const familyAt = () => (typeof pass.family === "function" ? pass.family(input, at) : pass.family);
if (rescans) {
work -= m[0].length;
if (work < 0) {
hits.push({ start: at, end: input.length, marker: pass.marker === "" ? REDACTED_WHOLE : pass.marker, pass: i, family: familyAt(), matchStart: at, matchEnd: input.length });
break;
}
re.lastIndex = at + 1;
}
else
re.lastIndex = at + Math.max(m[0].length, 1);
const inserted = insertedFor(pass, m[0], m.slice(1));
if (inserted === m[0])
continue;
const [a, b] = removedRegion(pass, m[0], m.slice(1), inserted);
if (b <= a)
continue;
const family = familyAt();
hits.push({ start: at + a, end: at + b, marker: inserted.slice(a, inserted.length - (m[0].length - b)), pass: i, family, matchStart: at, matchEnd: at + m[0].length });
}
});
return hits;
}
function unremovedHits(hits, removals) {
const gone = [];
for (const r of [...removals].filter((x) => x.end > x.start).sort((x, y) => x.start - y.start || y.end - x.end)) {
const last = gone[gone.length - 1];
if (last !== undefined && r.start <= last[1])
last[1] = Math.max(last[1], r.end);
else
gone.push([r.start, r.end]);
}
return hits.filter((h) => {
let lo = 0;
let hi = gone.length - 1;
let at = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (gone[mid][0] <= h.start) {
at = mid;
lo = mid + 1;
}
else
hi = mid - 1;
}
return at === -1 || gone[at][1] < h.end;
});
}
function sequentialRemovals(batches) {
const removals = [];
for (let j = 0; j < batches.length; j++) {
const toPassZero = passZeroMapper(batches.slice(0, j));
for (const e of batches[j]) {
const start = toPassZero(e.at + e.head);
const end = toPassZero(e.at + e.removedLen - e.tail);
removals.push({ start, end: Math.max(start, end), marker: e.inserted.slice(e.head, e.insertedLen - e.tail), pass: e.pass, family: e.family });
}
}
return removals;
}
function renderRemovals(input, removals) {
const sorted = removals.filter((r) => r.end > r.start || r.marker !== "").sort((x, y) => x.start - y.start || y.end - x.end);
const edits = [];
let out = "";
let cursor = 0;
for (let i = 0; i < sorted.length;) {
const start = sorted[i].start;
let end = sorted[i].end;
let widest;
for (; i < sorted.length && (sorted[i].start < end || sorted[i].start === start || (sorted[i].marker === "" && sorted[i].start === end)); i++) {
const r = sorted[i];
if (r.end > end)
end = r.end;
if (r.marker !== "" && (widest === undefined || r.end - r.start > widest.end - widest.start))
widest = r;
}
const marker = widest?.marker ?? REDACTED_WHOLE;
out += input.slice(cursor, start) + marker;
cursor = end;
edits.push({ at: start, removedLen: end - start, insertedLen: marker.length, inserted: marker, family: widest?.family, head: 0, tail: 0, pass: widest?.pass ?? sorted[i - 1].pass });
}
return { out: out + input.slice(cursor), edits };
}
function runPasses(input, passes, findings, toOriginal) {
const batches = [];
let cur = input;
passes.forEach((pass, passIndex) => {
const edits = [];
const passInput = cur;
const toPassZero = passZeroMapper(batches.slice());
cur = cur.replace(pass.re, (...args) => {
const match = args[0];
const offset = args[args.length - 2];
const groups = args.slice(1, -2);
const inserted = insertedFor(pass, match, groups);
if (inserted === match)
return inserted;
const family = typeof pass.family === "function" ? pass.family(passInput, offset) : pass.family;
if (findings !== undefined)
findings.push({ pass: passIndex, f: finding(pass, [toOriginal(toPassZero(offset)), toOriginal(toPassZero(offset + match.length))], family) });
const [a, b] = removedRegion(pass, match, groups, inserted);
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted, family, head: a, tail: match.length - b, pass: passIndex });
return inserted;
});
batches.push(edits);
});
return { out: cur, batches };
}
function applyPasses(input, passes, report, toOriginal, disjoint = false) {
const found = report === undefined ? undefined : [];
const plain = runPasses(input, passes, found, toOriginal);
const solo = soloHits(input, passes);
const sequential = solo.length === 0 ? [] : sequentialRemovals(plain.batches);
const outer = unremovedHits(solo, sequential);
const mappable = !disjoint || replacedSpans(plain.batches) !== undefined;
const whole = outer.length === 0 && mappable ? undefined : renderRemovals(input, [...sequential, ...outer]);
if (whole === undefined || (whole.out === plain.out && mappable)) {
if (report !== undefined)
report.findings.push(...found.map((x) => x.f));
return plain;
}
if (report !== undefined) {
const seen = new Set(found.map((x) => `${x.pass}:${x.f.span[0]}:${x.f.span[1]}`));
for (const h of outer) {
const span = [toOriginal(h.matchStart), toOriginal(h.matchEnd)];
const key = `${h.pass}:${span[0]}:${span[1]}`;
if (seen.has(key))
continue;
seen.add(key);
found.push({ pass: h.pass, f: finding(passes[h.pass], span, h.family) });
}
found.sort((x, y) => x.pass - y.pass || x.f.span[0] - y.f.span[0]);
report.findings.push(...found.map((x) => x.f));
}
return { out: whole.out, batches: [whole.edits] };
}
function replacedSpans(batches) {
const spans = [];
for (let j = 0; j < batches.length; j++) {
const toPassZero = passZeroMapper(batches.slice(0, j));
for (const e of batches[j]) {
const s0 = toPassZero(e.at);
const e0 = toPassZero(e.at + e.removedLen);
if (e0 - s0 !== e.removedLen)
return undefined;
spans.push({ start: s0, end: e0, inserted: e.inserted, pass: e.pass, family: e.family });
}
}
spans.sort((a, b) => a.start - b.start);
for (let i = 1; i < spans.length; i++)
if (spans[i].start < spans[i - 1].end)
return undefined;
return spans;
}
export function runRedactionPasses(input, passes, report) {
if (report !== undefined && report.preexistingMarkers === undefined) {
report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0;
}
if (!FORMAT_CHAR_RE.test(input))
return applyPasses(input, passes, report, (pos) => pos).out;
const viewChars = [];
const viewToOriginal = [];
const formatAtSlot = [];
const charStartOffsets = [];
{
let orig = 0;
let viewOff = 0;
let slotBuf = "";
for (const ch of input) {
if (FORMAT_CHAR_RE.test(ch)) {
slotBuf += ch;
}
else {
formatAtSlot.push(slotBuf);
slotBuf = "";
viewToOriginal.push(orig);
charStartOffsets.push(viewOff);
viewChars.push(ch);
viewOff += ch.length;
}
orig += ch.length;
}
formatAtSlot.push(slotBuf);
viewToOriginal.push(input.length);
charStartOffsets.push(viewOff);
}
const view = viewChars.join("");
const offsetToCharIndex = new Map();
charStartOffsets.forEach((off, idx) => offsetToCharIndex.set(off, idx));
const toOriginal = (pos) => {
const idx = offsetToCharIndex.get(pos);
return idx === undefined ? input.length : viewToOriginal[idx];
};
const { batches } = applyPasses(view, passes, report, toOriginal, true);
const viewSpans = replacedSpans(batches);
if (viewSpans === undefined)
return REDACTED_WHOLE;
const spans = viewSpans.map((sp) => {
const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length;
const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length;
const lastIdx = endIdx - 1;
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
return { start: viewToOriginal[startIdx], end: endOrig, inserted: sp.inserted };
});
const viewOffsetToOriginal = (viewStart, viewEnd) => {
const startIdx = offsetToCharIndex.get(viewStart) ?? viewChars.length;
const endIdx = offsetToCharIndex.get(viewEnd) ?? viewChars.length;
const lastIdx = endIdx - 1;
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
return { start: viewToOriginal[startIdx], end: endOrig };
};
const cutOffsets = [];
for (let k = 1; k < viewChars.length; k++)
if (formatAtSlot[k] !== "")
cutOffsets.push(charStartOffsets[k]);
if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS)
return REDACTED_WHOLE;
const extra = [];
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos, true).batches);
if (rawSpans !== undefined)
extra.push(...rawSpans);
for (const cut of cutOffsets) {
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos, true).batches);
if (cutSpans === undefined)
continue;
for (const c of cutSpans) {
const o = viewOffsetToOriginal(c.start + cut, c.end + cut);
extra.push({ ...c, start: o.start, end: o.end });
}
}
extra.sort((a, b) => a.start - b.start);
const firstEndingAfter = (pos) => {
let lo = 0;
let hi = spans.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (spans[mid].end > pos)
hi = mid;
else
lo = mid + 1;
}
return lo;
};
for (const r of extra) {
const at = firstEndingAfter(r.start);
const hit = spans[at];
if (hit !== undefined && hit.start < r.end) {
if (r.start < hit.start)
hit.start = r.start;
if (r.end > hit.end)
hit.end = r.end;
let next = at + 1;
while (next < spans.length && spans[next].start < hit.end)
hit.end = Math.max(hit.end, spans[next++].end);
if (next > at + 1)
spans.splice(at + 1, next - at - 1);
continue;
}
spans.splice(at, 0, { start: r.start, end: r.end, inserted: r.inserted });
if (report !== undefined)
report.findings.push(finding(passes[r.pass], [r.start, r.end], r.family));
}
let result = "";
let cursor = 0;
for (const sp of spans) {
result += input.slice(cursor, sp.start) + sp.inserted;
cursor = sp.end;
}
result += input.slice(cursor);
return result;
}
const BRACKET_MARKER = { "url-credentials": "[redacted-credentials]" };
const bracketMarker = (kind) => BRACKET_MARKER[kind] ?? "[redacted]";
const capturesLabel = (re) => new RegExp(`${re.source}|`).exec("").length > 1;
export function egressPasses(groups, replacers = {}) {
return groups.map((rows) => {
const head = rows[0];
const marker = bracketMarker(head.kind);
const re = rows.length === 1 ? head.match : new RegExp(rows.map((r) => `(?:${r.match.source})`).join("|"), head.match.flags);
const sticky = rows.map((r) => ({ id: r.id, re: new RegExp(r.match.source, r.match.flags.replace("g", "") + "y") }));
const family = rows.length === 1 ? head.id : (input, at) => sticky.find((s) => ((s.re.lastIndex = at), s.re.test(input)))?.id ?? head.id;
return { kind: head.kind, confidence: head.confidence, marker, source: head.source, re, replace: replacers[head.kind] ?? (capturesLabel(head.match) ? `$1${marker}` : marker), family };
});
}