@mono-agent/agent-runtime 0.20.14 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +30 -7
- package/README.md +219 -35
- package/package.json +9 -4
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +104 -5
- package/src/agent/tools/bash.js +10 -2
- package/src/agent/tools/codex-subscription-search.js +122 -28
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/monitor.js +11 -2
- package/src/agent/tools/pi-bridge.js +33 -14
- package/src/agent/tools/shared/monitors.js +22 -3
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +3 -1
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +1 -0
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/pi-native/compaction-driver.js +45 -21
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +40 -2
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +28 -4
- package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
- package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +245 -13
- package/src/ai/providers/pi-native.js +159 -40
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/router.js +30 -11
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +26 -5
- package/src/runtime.js +24 -5
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -1
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +6 -2
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/monitor.d.ts +5 -2
- package/types/agent/tools/pi-bridge.d.ts +6 -4
- package/types/agent/tools/shared/monitors.d.ts +17 -2
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +3 -2
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/index.d.ts +1 -0
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +61 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +3 -1
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +11 -1
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +140 -12
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { Readability } from "@mozilla/readability";
|
|
4
|
+
import { Defuddle as parseDefuddle } from "defuddle/node";
|
|
5
|
+
import { XMLValidator } from "fast-xml-parser";
|
|
6
|
+
import { DOMParser, parseHTML } from "linkedom";
|
|
7
|
+
import TurndownService from "turndown";
|
|
8
|
+
import { extractText as extractPdfText, getDocumentProxy } from "unpdf";
|
|
9
|
+
|
|
10
|
+
export const MAX_STRUCTURED_DOCUMENT_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const MARKDOWN_MIME_TYPES = new Set([
|
|
12
|
+
"application/markdown",
|
|
13
|
+
"application/x-markdown",
|
|
14
|
+
"text/markdown",
|
|
15
|
+
"text/md",
|
|
16
|
+
"text/vnd.daringfireball.markdown",
|
|
17
|
+
"text/x-markdown",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function contentKind(contentType, bytes) {
|
|
21
|
+
const mime = String(contentType || "").split(";", 1)[0].trim().toLowerCase();
|
|
22
|
+
if (mime === "application/pdf" || Buffer.from(bytes.subarray(0, 5)).toString("ascii") === "%PDF-") return "pdf";
|
|
23
|
+
if (mime.includes("json") || mime.endsWith("+json")) return "json";
|
|
24
|
+
if (mime.includes("xml") || mime.includes("rss") || mime.includes("atom") || mime.endsWith("+xml")) return "xml";
|
|
25
|
+
if (mime.includes("html")) return "html";
|
|
26
|
+
if (MARKDOWN_MIME_TYPES.has(mime)) return "markdown";
|
|
27
|
+
if (mime.startsWith("text/") || [
|
|
28
|
+
"application/ecmascript", "application/graphql", "application/javascript", "application/rtf",
|
|
29
|
+
"application/sql", "application/x-httpd-php", "application/x-yaml", "application/yaml",
|
|
30
|
+
].includes(mime)) return "text";
|
|
31
|
+
if (["", "application/octet-stream", "application/binary", "binary/octet-stream"].includes(mime)) {
|
|
32
|
+
if (looksLikeHtml(bytes)) return "html";
|
|
33
|
+
if (looksLikeXml(bytes)) return "xml";
|
|
34
|
+
if (looksLikeJson(bytes)) return "json";
|
|
35
|
+
if (!looksBinary(bytes)) return "text";
|
|
36
|
+
}
|
|
37
|
+
return "binary";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decodeWebBytes(bytes, contentType, kind = contentKind(contentType, bytes)) {
|
|
41
|
+
const bom = detectBom(bytes);
|
|
42
|
+
const header = charsetFromContentType(contentType);
|
|
43
|
+
const declaration = header === undefined && (kind === "html" || kind === "xml")
|
|
44
|
+
? charsetFromDeclaration(bytes, kind)
|
|
45
|
+
: undefined;
|
|
46
|
+
const selected = bom ?? header ?? declaration ?? { charset: "utf-8", source: "default" };
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = new TextDecoder(selected.charset, { fatal: false }).decode(bytes.subarray(selected.offset ?? 0));
|
|
50
|
+
} catch {
|
|
51
|
+
throw extractionError("unsupported_charset", `Unsupported declared charset: ${selected.charset}.`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
text,
|
|
55
|
+
charset: selected.charset,
|
|
56
|
+
charsetSource: selected.source,
|
|
57
|
+
hadDecodingReplacement: text.includes("\uFFFD"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function extractWebDocument(bytes, { contentType, format, url }) {
|
|
62
|
+
const kind = contentKind(contentType, bytes);
|
|
63
|
+
const decoded = decodeWebBytes(bytes, contentType, kind);
|
|
64
|
+
if (format === "raw") return { body: decoded.text, readableText: decoded.text, title: "", extractionStage: "raw", parserFailureCount: 0, parserFailures: [], kind, ...decoded };
|
|
65
|
+
if (["html", "json", "xml"].includes(kind) && bytes.byteLength > MAX_STRUCTURED_DOCUMENT_BYTES) {
|
|
66
|
+
throw extractionError("parser_input_too_large", `Structured document exceeded ${MAX_STRUCTURED_DOCUMENT_BYTES} bytes.`);
|
|
67
|
+
}
|
|
68
|
+
if (kind === "pdf") return { ...(await extractPdf(bytes)), kind, ...decoded };
|
|
69
|
+
if (kind === "markdown") {
|
|
70
|
+
const readableText = markdownToText(decoded.text);
|
|
71
|
+
return {
|
|
72
|
+
body: format === "text" ? readableText : decoded.text,
|
|
73
|
+
readableText,
|
|
74
|
+
title: "",
|
|
75
|
+
extractionStage: "markdown",
|
|
76
|
+
parserFailureCount: 0,
|
|
77
|
+
parserFailures: [],
|
|
78
|
+
kind,
|
|
79
|
+
...decoded,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (kind === "json") {
|
|
83
|
+
let parsed;
|
|
84
|
+
try { parsed = JSON.parse(decoded.text); }
|
|
85
|
+
catch { throw extractionError("invalid_json", "Response declared JSON but was malformed."); }
|
|
86
|
+
const pretty = JSON.stringify(parsed, null, 2);
|
|
87
|
+
return { body: format === "markdown" ? `\`\`\`json\n${pretty}\n\`\`\`` : pretty, readableText: pretty, title: "", extractionStage: "json", parserFailureCount: 0, parserFailures: [], kind, ...decoded };
|
|
88
|
+
}
|
|
89
|
+
if (kind === "xml") return { ...extractXml(decoded.text, format, url), kind, ...decoded };
|
|
90
|
+
if (kind === "html") return { ...(await extractHtml(decoded.text, url, format)), kind, ...decoded };
|
|
91
|
+
const body = collapseDocumentText(decoded.text);
|
|
92
|
+
return { body, readableText: body, title: "", extractionStage: "text", parserFailureCount: 0, parserFailures: [], kind, ...decoded };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function extractPdf(bytes) {
|
|
96
|
+
const pdf = await getDocumentProxy(bytes);
|
|
97
|
+
try {
|
|
98
|
+
const extracted = await extractPdfText(pdf, { mergePages: true });
|
|
99
|
+
const body = String(extracted.text || "").trim();
|
|
100
|
+
if (!body) throw extractionError("extraction_failed", "PDF contained no readable text.");
|
|
101
|
+
return { body, readableText: body, title: "", extractionStage: "pdf", parserFailureCount: 0, parserFailures: [] };
|
|
102
|
+
} finally {
|
|
103
|
+
try { await /** @type {any} */ (pdf).destroy?.(); } catch { /* best effort */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function extractHtml(html, url, format) {
|
|
108
|
+
let title = "";
|
|
109
|
+
let markdown = "";
|
|
110
|
+
const failures = [];
|
|
111
|
+
try {
|
|
112
|
+
let { document } = parseHTML(html);
|
|
113
|
+
if (!document.body?.innerHTML?.trim() && html.trim()) {
|
|
114
|
+
({ document } = parseHTML(`<html><body>${html}</body></html>`));
|
|
115
|
+
}
|
|
116
|
+
sanitizeDocumentLinks(document, url);
|
|
117
|
+
const parsed = await parseDefuddle(/** @type {any} */ (document), url, {
|
|
118
|
+
markdown: true, separateMarkdown: true, useAsync: false,
|
|
119
|
+
});
|
|
120
|
+
title = String(parsed.title || "").trim();
|
|
121
|
+
markdown = String(parsed.contentMarkdown || parsed.content || "").trim();
|
|
122
|
+
if (meaningfulCharacters(markdown) > 0) return finishHtml(markdown, title, format, "defuddle", failures);
|
|
123
|
+
failures.push("defuddle");
|
|
124
|
+
} catch { failures.push("defuddle"); }
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const { document } = parseHTML(html);
|
|
128
|
+
sanitizeDocumentLinks(document, url);
|
|
129
|
+
const article = new Readability(/** @type {any} */ (document)).parse();
|
|
130
|
+
if (article) {
|
|
131
|
+
title ||= String(article.title || "").trim();
|
|
132
|
+
markdown = htmlToMarkdown(article.content || "", url);
|
|
133
|
+
if (meaningfulCharacters(markdown) > 0) return finishHtml(markdown, title, format, "readability", failures);
|
|
134
|
+
}
|
|
135
|
+
failures.push("readability");
|
|
136
|
+
} catch { failures.push("readability"); }
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const { document } = parseHTML(html);
|
|
140
|
+
title ||= collapseWhitespace(document.querySelector("title")?.textContent);
|
|
141
|
+
for (const node of document.querySelectorAll("script,style,noscript,template,nav")) node.remove();
|
|
142
|
+
sanitizeDocumentLinks(document, url);
|
|
143
|
+
markdown = turndown().turndown(document.body?.innerHTML || "").trim();
|
|
144
|
+
if (meaningfulCharacters(markdown) > 0) return finishHtml(markdown, title, format, "body", failures);
|
|
145
|
+
failures.push("body");
|
|
146
|
+
} catch { failures.push("body"); }
|
|
147
|
+
throw extractionError("extraction_failed", `HTML extraction failed after ${failures.join(", ")}.`, { parserFailures: failures });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function finishHtml(markdown, title, format, stage, failures) {
|
|
151
|
+
const readableText = markdownToText(markdown);
|
|
152
|
+
const body = format === "text"
|
|
153
|
+
? readableText
|
|
154
|
+
: title && !markdown.trimStart().startsWith(`# ${title}`) ? `# ${title}\n\n${markdown}` : markdown;
|
|
155
|
+
return { body, readableText, title, extractionStage: stage, parserFailureCount: failures.length, parserFailures: failures };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractXml(xml, format, url) {
|
|
159
|
+
if (XMLValidator.validate(String(xml || "")) !== true) {
|
|
160
|
+
throw extractionError("invalid_xml", "Response declared XML but was malformed.");
|
|
161
|
+
}
|
|
162
|
+
const document = new DOMParser().parseFromString(String(xml || ""), "text/xml");
|
|
163
|
+
if (!document?.documentElement || document.querySelector("parsererror")) {
|
|
164
|
+
throw extractionError("invalid_xml", "Response declared XML but was malformed.");
|
|
165
|
+
}
|
|
166
|
+
const entries = [...document.querySelectorAll("item, entry")].slice(0, 50);
|
|
167
|
+
if (entries.length === 0) {
|
|
168
|
+
const body = collapseDocumentText(document.documentElement.textContent || "");
|
|
169
|
+
if (!body) throw extractionError("extraction_failed", "XML document contained no readable text.");
|
|
170
|
+
return { body, readableText: body, title: "", extractionStage: "xml", parserFailureCount: 0, parserFailures: [] };
|
|
171
|
+
}
|
|
172
|
+
const blocks = entries.map((entry) => {
|
|
173
|
+
const title = collapseWhitespace(entry.querySelector("title")?.textContent) || "Untitled";
|
|
174
|
+
const linkElement = entry.querySelector("link");
|
|
175
|
+
const link = safeUrl(linkElement?.getAttribute("href") || collapseWhitespace(linkElement?.textContent), url);
|
|
176
|
+
const description = collapseWhitespace(entry.querySelector("description, summary, content")?.textContent);
|
|
177
|
+
if (format === "text") return [title, link, description].filter(Boolean).join("\n");
|
|
178
|
+
return [`## ${escapeMarkdownLabel(title)}`, link ? `[${escapeMarkdownLabel(link)}](${link})` : "", description].filter(Boolean).join("\n\n");
|
|
179
|
+
});
|
|
180
|
+
const body = blocks.join("\n\n");
|
|
181
|
+
return { body, readableText: markdownToText(body), title: "", extractionStage: "xml", parserFailureCount: 0, parserFailures: [] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function htmlToMarkdown(value, url) {
|
|
185
|
+
const { document } = parseHTML(String(value || ""));
|
|
186
|
+
sanitizeDocumentLinks(document, url);
|
|
187
|
+
return turndown().turndown(document.body?.innerHTML || "").trim();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function turndown() {
|
|
191
|
+
const service = new TurndownService({ bulletListMarker: "-", codeBlockStyle: "fenced", emDelimiter: "_", strongDelimiter: "**" });
|
|
192
|
+
service.addRule("tablesAsText", {
|
|
193
|
+
filter: ["table"],
|
|
194
|
+
replacement(_content, node) {
|
|
195
|
+
return `\n\n${[...node.querySelectorAll("tr")].map((row) => [...row.querySelectorAll("th,td")].map((cell) => collapseWhitespace(cell.textContent)).join(" | ")).filter(Boolean).join("\n")}\n\n`;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
return service;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sanitizeDocumentLinks(document, baseUrl) {
|
|
202
|
+
for (const node of document.querySelectorAll("a[href],img[src]")) {
|
|
203
|
+
const attribute = node.tagName?.toLowerCase() === "a" ? "href" : "src";
|
|
204
|
+
const safe = safeUrl(node.getAttribute(attribute), baseUrl);
|
|
205
|
+
if (safe) node.setAttribute(attribute, safe);
|
|
206
|
+
else node.removeAttribute(attribute);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function safeUrl(value, base) {
|
|
211
|
+
if (typeof value !== "string" || !value.trim()) return "";
|
|
212
|
+
try {
|
|
213
|
+
const parsed = new URL(value, base);
|
|
214
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) return "";
|
|
215
|
+
return parsed.href;
|
|
216
|
+
} catch { return ""; }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function detectBom(bytes) {
|
|
220
|
+
if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) return { charset: "utf-8", source: "bom", offset: 3 };
|
|
221
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe) return { charset: "utf-16le", source: "bom", offset: 2 };
|
|
222
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff) return { charset: "utf-16be", source: "bom", offset: 2 };
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function charsetFromContentType(value) {
|
|
227
|
+
const match = String(value || "").match(/charset\s*=\s*["']?([^;"'\s]+)/iu);
|
|
228
|
+
return match ? { charset: match[1].toLowerCase(), source: "header" } : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function charsetFromDeclaration(bytes, kind) {
|
|
232
|
+
const sample = Buffer.from(bytes.subarray(0, 4096)).toString("latin1");
|
|
233
|
+
const match = kind === "html"
|
|
234
|
+
? sample.match(/<meta[^>]+charset\s*=\s*["']?([^\s"'/>]+)/iu)
|
|
235
|
+
?? sample.match(/<meta[^>]+content=["'][^"']*charset=([^\s"';]+)/iu)
|
|
236
|
+
: sample.match(/^\s*<\?xml[^>]+encoding=["']([^"']+)["']/iu);
|
|
237
|
+
return match ? { charset: match[1].toLowerCase(), source: kind === "html" ? "html_meta" : "xml_declaration" } : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function looksLikeHtml(bytes) {
|
|
241
|
+
return /^\s*(?:<!doctype html|<html|<head|<body)/iu.test(Buffer.from(bytes.subarray(0, 512)).toString("utf8"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function looksLikeXml(bytes) {
|
|
245
|
+
return /^\s*(?:<\?xml\b|<(?:rss|feed|rdf:RDF)\b)/iu.test(Buffer.from(bytes.subarray(0, 512)).toString("utf8"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function looksLikeJson(bytes) {
|
|
249
|
+
return /^[\s\uFEFF]*[\[{]/u.test(Buffer.from(bytes.subarray(0, 512)).toString("utf8"));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function looksBinary(bytes) {
|
|
253
|
+
const sample = bytes.subarray(0, Math.min(bytes.byteLength, 1024));
|
|
254
|
+
if (sample.byteLength === 0) return false;
|
|
255
|
+
let controls = 0;
|
|
256
|
+
for (const byte of sample) {
|
|
257
|
+
if (byte === 0) return true;
|
|
258
|
+
if (byte < 0x20 && ![0x09, 0x0a, 0x0c, 0x0d].includes(byte)) controls += 1;
|
|
259
|
+
}
|
|
260
|
+
return controls / sample.byteLength > 0.1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function shouldAutoRender(readableText, html) {
|
|
264
|
+
if (meaningfulCharacters(readableText) >= 200) return false;
|
|
265
|
+
const scriptCount = (html.match(/<script\b/giu) || []).length;
|
|
266
|
+
const hasAppRoot = /<(?:div|main)[^>]+(?:id|class)=["'][^"']*(?:app|root|__next|nuxt|svelte)[^"']*["']/iu.test(html);
|
|
267
|
+
const hasSpaAssets = /\b(?:webpack|__NEXT_DATA__|vite|hydration|data-reactroot)\b/iu.test(html);
|
|
268
|
+
return scriptCount >= 2 && (hasAppRoot || hasSpaAssets);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function meaningfulCharacters(value) { return markdownToText(value).replace(/\s/gu, "").length; }
|
|
272
|
+
|
|
273
|
+
export function markdownToText(value) {
|
|
274
|
+
const source = String(value || "").replace(/\r\n?/gu, "\n");
|
|
275
|
+
const { text, codeBlocks } = protectFencedCode(source);
|
|
276
|
+
let output = collapseDocumentText(text
|
|
277
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/gu, "$1")
|
|
278
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
|
|
279
|
+
.replace(/^ {0,3}(?:[*_-][ \t]*){3,}$/gmu, "")
|
|
280
|
+
.replace(/^ {0,3}#{1,6}[ \t]+/gmu, "")
|
|
281
|
+
.replace(/^ {0,3}>[ \t]+/gmu, "")
|
|
282
|
+
.replace(/^ {0,3}[*+-][ \t]+/gmu, "")
|
|
283
|
+
.replace(/~~(?=\S)([^~\n]*?\S)~~/gu, "$1")
|
|
284
|
+
.replace(/\*\*(?=\S)([^*\n]*?\S)\*\*/gu, "$1")
|
|
285
|
+
.replace(/__(?=\S)([^_\n]*?\S)__/gu, "$1")
|
|
286
|
+
.replace(/(?<!\*)\*(?=\S)([^*\n]*?\S)\*(?!\*)/gu, "$1")
|
|
287
|
+
.replace(/(?<![\p{L}\p{N}_])_(?=\S)([^_\n]*?\S)_(?![\p{L}\p{N}_])/gu, "$1")
|
|
288
|
+
.replace(/`([^`\n]+)`/gu, "$1"));
|
|
289
|
+
for (const block of codeBlocks) output = output.replace(block.token, block.body);
|
|
290
|
+
return output;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function protectFencedCode(value) {
|
|
294
|
+
const lines = value.split("\n");
|
|
295
|
+
const output = [];
|
|
296
|
+
const codeBlocks = [];
|
|
297
|
+
let fence;
|
|
298
|
+
let code = [];
|
|
299
|
+
for (const line of lines) {
|
|
300
|
+
if (fence !== undefined) {
|
|
301
|
+
const containerContent = stripFenceContainer(line, fence, fence.containerIndent);
|
|
302
|
+
const closing = containerContent.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/u)?.[1];
|
|
303
|
+
if (closing !== undefined && closing[0] === fence.marker[0] && closing.length >= fence.marker.length) {
|
|
304
|
+
const token = uniqueCodeToken(value, codeBlocks.length);
|
|
305
|
+
codeBlocks.push({ token, body: code.join("\n") });
|
|
306
|
+
output.push(token);
|
|
307
|
+
fence = undefined;
|
|
308
|
+
code = [];
|
|
309
|
+
} else {
|
|
310
|
+
code.push(stripFenceContainer(line, fence, fence.contentIndent));
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const opening = readFenceOpening(line);
|
|
315
|
+
if (opening !== undefined) {
|
|
316
|
+
fence = opening;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
output.push(line);
|
|
320
|
+
}
|
|
321
|
+
if (fence !== undefined) {
|
|
322
|
+
const token = uniqueCodeToken(value, codeBlocks.length);
|
|
323
|
+
codeBlocks.push({ token, body: code.join("\n") });
|
|
324
|
+
output.push(token);
|
|
325
|
+
}
|
|
326
|
+
return { text: output.join("\n"), codeBlocks };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readFenceOpening(line) {
|
|
330
|
+
let candidate = line;
|
|
331
|
+
let quoteDepth = 0;
|
|
332
|
+
for (;;) {
|
|
333
|
+
const quote = candidate.match(/^ {0,3}>[ \t]?/u)?.[0];
|
|
334
|
+
if (quote === undefined) break;
|
|
335
|
+
quoteDepth += 1;
|
|
336
|
+
candidate = candidate.slice(quote.length);
|
|
337
|
+
}
|
|
338
|
+
const list = candidate.match(/^ {0,3}(?:[*+-]|\d{1,9}[.)])[ \t]+/u)?.[0];
|
|
339
|
+
const listIndent = list?.length ?? 0;
|
|
340
|
+
if (list !== undefined) candidate = candidate.slice(list.length);
|
|
341
|
+
const indentation = candidate.match(/^ {0,3}/u)?.[0].length ?? 0;
|
|
342
|
+
candidate = candidate.slice(indentation);
|
|
343
|
+
const match = candidate.match(/^(`{3,}|~{3,})([^\n]*)$/u);
|
|
344
|
+
if (match === null || (match[1][0] === "`" && match[2].includes("`"))) return undefined;
|
|
345
|
+
return {
|
|
346
|
+
marker: match[1],
|
|
347
|
+
quoteDepth,
|
|
348
|
+
containerIndent: listIndent,
|
|
349
|
+
contentIndent: listIndent + indentation,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function stripFenceContainer(line, fence, indentation) {
|
|
354
|
+
let candidate = line;
|
|
355
|
+
for (let depth = 0; depth < fence.quoteDepth; depth += 1) {
|
|
356
|
+
const quote = candidate.match(/^ {0,3}>[ \t]?/u)?.[0];
|
|
357
|
+
if (quote === undefined) return line;
|
|
358
|
+
candidate = candidate.slice(quote.length);
|
|
359
|
+
}
|
|
360
|
+
let remainingIndent = indentation;
|
|
361
|
+
while (remainingIndent > 0 && candidate.startsWith(" ")) {
|
|
362
|
+
candidate = candidate.slice(1);
|
|
363
|
+
remainingIndent -= 1;
|
|
364
|
+
}
|
|
365
|
+
return candidate;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function uniqueCodeToken(source, index) {
|
|
369
|
+
let token = `\u0000MONOAGENTFENCE${index}\u0000`;
|
|
370
|
+
while (source.includes(token)) token = `\u0000${token}\u0000`;
|
|
371
|
+
return token;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function collapseDocumentText(value) {
|
|
375
|
+
return String(value || "").replace(/\r/gu, "").replace(/[ \t]+\n/gu, "\n").replace(/\n{3,}/gu, "\n\n").replace(/[ \t]{2,}/gu, " ").trim();
|
|
376
|
+
}
|
|
377
|
+
function collapseWhitespace(value) { return String(value || "").replace(/\s+/gu, " ").trim(); }
|
|
378
|
+
function escapeMarkdownLabel(value) { return collapseWhitespace(value).replace(/[[\]\\]/gu, "\\$&"); }
|
|
379
|
+
function extractionError(code, message, details = {}) { return Object.assign(new Error(message), { code, ...details }); }
|