@mono-agent/agent-runtime 0.20.11 → 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.
Files changed (148) hide show
  1. package/ARCHITECTURE.md +50 -11
  2. package/MIGRATION.md +288 -26
  3. package/README.md +352 -477
  4. package/package.json +13 -44
  5. package/src/agent/tool-bloat.js +145 -9
  6. package/src/agent/tools/agent-tool.js +108 -9
  7. package/src/agent/tools/bash.js +11 -26
  8. package/src/agent/tools/codex-subscription-search.js +123 -29
  9. package/src/agent/tools/exec.js +10 -2
  10. package/src/agent/tools/index.js +7 -0
  11. package/src/agent/tools/monitor.js +149 -0
  12. package/src/agent/tools/pi-bridge.js +123 -19
  13. package/src/agent/tools/shared/bash-environment.js +31 -0
  14. package/src/agent/tools/shared/monitors.js +293 -0
  15. package/src/agent/tools/shared/path-resolver.js +25 -6
  16. package/src/agent/tools/shared/process-jobs.js +6 -1
  17. package/src/agent/tools/shared/process-runner.js +26 -6
  18. package/src/agent/tools/shared/tool-context.js +8 -0
  19. package/src/agent/tools/web-access-interstitial.js +70 -0
  20. package/src/agent/tools/web-browser-render.js +83 -58
  21. package/src/agent/tools/web-controller.js +112 -21
  22. package/src/agent/tools/web-document-extractor.js +379 -0
  23. package/src/agent/tools/web-fetch.js +271 -243
  24. package/src/agent/tools/web-request.js +65 -0
  25. package/src/agent/tools/web-search-output.js +165 -0
  26. package/src/agent/tools/web-search-state.js +75 -0
  27. package/src/agent/tools/web-search.js +532 -71
  28. package/src/ai/cost.js +13 -68
  29. package/src/ai/failure.js +3 -3
  30. package/src/ai/index.js +5 -17
  31. package/src/ai/observer.js +8 -0
  32. package/src/ai/pi-interop.js +221 -1
  33. package/src/ai/pi-oauth-compat.js +1 -1
  34. package/src/ai/provider-check.js +131 -0
  35. package/src/ai/providers/codex/app-server-client.js +592 -0
  36. package/src/ai/providers/pi-models.js +18 -10
  37. package/src/ai/providers/pi-native/compaction-driver.js +94 -42
  38. package/src/ai/providers/pi-native/compaction-summary.js +140 -0
  39. package/src/ai/providers/pi-native/harness-adapter.js +376 -0
  40. package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
  41. package/src/ai/providers/pi-native/provider-attribution.js +102 -0
  42. package/src/ai/providers/pi-native/result-builder.js +38 -14
  43. package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
  44. package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
  45. package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
  46. package/src/ai/providers/pi-native/turn-runner.js +279 -28
  47. package/src/ai/providers/pi-native.js +206 -61
  48. package/src/ai/runtime/capabilities.js +11 -56
  49. package/src/ai/runtime/live-input-events.js +250 -54
  50. package/src/ai/runtime/model-refs.js +118 -153
  51. package/src/ai/runtime/registry.js +22 -56
  52. package/src/ai/runtime/router.js +76 -417
  53. package/src/ai/runtime/session-liveness.js +3 -4
  54. package/src/ai/runtime/sessions.js +4 -5
  55. package/src/ai/runtime/tool-policy.js +0 -2
  56. package/src/ai/tool-lifecycle.js +32 -18
  57. package/src/ai/types.js +37 -112
  58. package/src/index.js +0 -6
  59. package/src/runtime.js +29 -16
  60. package/types/agent/tool-bloat.d.ts +1 -1
  61. package/types/agent/tools/agent-tool.d.ts +4 -2
  62. package/types/agent/tools/bash.d.ts +5 -3
  63. package/types/agent/tools/codex-subscription-search.d.ts +7 -3
  64. package/types/agent/tools/exec.d.ts +5 -3
  65. package/types/agent/tools/index.d.ts +1 -0
  66. package/types/agent/tools/monitor.d.ts +47 -0
  67. package/types/agent/tools/pi-bridge.d.ts +7 -4
  68. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  69. package/types/agent/tools/shared/monitors.d.ts +98 -0
  70. package/types/agent/tools/shared/process-jobs.d.ts +5 -1
  71. package/types/agent/tools/shared/process-runner.d.ts +14 -4
  72. package/types/agent/tools/shared/tool-context.d.ts +2 -0
  73. package/types/agent/tools/web-access-interstitial.d.ts +23 -0
  74. package/types/agent/tools/web-browser-render.d.ts +4 -1
  75. package/types/agent/tools/web-controller.d.ts +4 -2
  76. package/types/agent/tools/web-document-extractor.d.ts +27 -0
  77. package/types/agent/tools/web-fetch.d.ts +19 -24
  78. package/types/agent/tools/web-request.d.ts +20 -0
  79. package/types/agent/tools/web-search-output.d.ts +31 -0
  80. package/types/agent/tools/web-search-state.d.ts +21 -0
  81. package/types/agent/tools/web-search.d.ts +10 -45
  82. package/types/ai/cost.d.ts +1 -2
  83. package/types/ai/index.d.ts +2 -4
  84. package/types/ai/observer.d.ts +6 -0
  85. package/types/ai/pi-interop.d.ts +81 -0
  86. package/types/ai/provider-check.d.ts +53 -0
  87. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  88. package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
  89. package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
  90. package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
  91. package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
  92. package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
  93. package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
  94. package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
  95. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  96. package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
  97. package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
  98. package/types/ai/providers/pi-native.d.ts +21 -4
  99. package/types/ai/runtime/capabilities.d.ts +21 -70
  100. package/types/ai/runtime/live-input-events.d.ts +32 -8
  101. package/types/ai/runtime/model-refs.d.ts +0 -24
  102. package/types/ai/runtime/router.d.ts +3 -10
  103. package/types/ai/runtime/tool-policy.d.ts +0 -2
  104. package/types/ai/tool-lifecycle.d.ts +4 -3
  105. package/types/ai/types.d.ts +162 -256
  106. package/types/index.d.ts +0 -1
  107. package/src/ai/providers/acp-client.js +0 -1149
  108. package/src/ai/providers/acp-privacy.js +0 -124
  109. package/src/ai/providers/acp-public.js +0 -21
  110. package/src/ai/providers/acp-session-tokens.js +0 -282
  111. package/src/ai/providers/acp-transport.js +0 -356
  112. package/src/ai/providers/acp.js +0 -543
  113. package/src/ai/providers/claude-cli.js +0 -883
  114. package/src/ai/providers/claude-sandbox.js +0 -71
  115. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  116. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  117. package/src/ai/providers/claude-sdk.js +0 -1127
  118. package/src/ai/providers/claude-subagent-activity.js +0 -719
  119. package/src/ai/providers/claude-subagents.js +0 -88
  120. package/src/ai/providers/codex-app.js +0 -2946
  121. package/src/ai/providers/opencode-app.js +0 -1109
  122. package/src/ai/providers/opencode-discovery.js +0 -39
  123. package/src/ai/providers/opencode-server.js +0 -508
  124. package/src/ai/runtime/context-windows.js +0 -46
  125. package/src/ai/runtime/fast-mode.js +0 -8
  126. package/src/ai/streaming/codex-events.js +0 -146
  127. package/src/ai/streaming/opencode-events.js +0 -59
  128. package/types/ai/providers/acp-client.d.ts +0 -227
  129. package/types/ai/providers/acp-privacy.d.ts +0 -25
  130. package/types/ai/providers/acp-public.d.ts +0 -7
  131. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  132. package/types/ai/providers/acp-transport.d.ts +0 -45
  133. package/types/ai/providers/acp.d.ts +0 -93
  134. package/types/ai/providers/claude-cli.d.ts +0 -305
  135. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  136. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  137. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  138. package/types/ai/providers/claude-sdk.d.ts +0 -138
  139. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  140. package/types/ai/providers/claude-subagents.d.ts +0 -18
  141. package/types/ai/providers/codex-app.d.ts +0 -151
  142. package/types/ai/providers/opencode-app.d.ts +0 -96
  143. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  144. package/types/ai/providers/opencode-server.d.ts +0 -20
  145. package/types/ai/runtime/context-windows.d.ts +0 -9
  146. package/types/ai/runtime/fast-mode.d.ts +0 -2
  147. package/types/ai/streaming/codex-events.d.ts +0 -40
  148. package/types/ai/streaming/opencode-events.d.ts +0 -42
@@ -1,10 +1,11 @@
1
1
  // @ts-check
2
2
 
3
- import { randomUUID } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import { readToolRuntime } from "./shared/runtime-context.js";
5
5
  import { resolveSandboxPolicy } from "./shared/tool-context.js";
6
- import { performWebFetch } from "./web-fetch.js";
6
+ import { performWebFetch, formatWebFetchDocument } from "./web-fetch.js";
7
7
  import { performWebSearch } from "./web-search.js";
8
+ import { createWebSearchRunState, webSearchBudgetSnapshot } from "./web-search-state.js";
8
9
 
9
10
  const MAX_CACHE_ENTRIES = 64;
10
11
  const MAX_SHARED_SEARCH_ENTRIES = 256;
@@ -28,10 +29,12 @@ const sharedSearchCache = new Map();
28
29
  * cleanup. Search results are the exception: they live in the process-wide
29
30
  * cache above so sibling subagents and later turns can reuse them.
30
31
  *
31
- * @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any, codexSearch?: any}} [options]
32
+ * @param {{coordinator?: any, searchConfig?: any, searchState?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any, codexSearch?: any}} [options]
32
33
  */
33
34
  export function createWebToolController({
35
+ coordinator,
34
36
  searchConfig,
37
+ searchState: suppliedSearchState,
35
38
  fetchConfig,
36
39
  sandboxPolicy,
37
40
  sandboxEngine,
@@ -46,6 +49,7 @@ export function createWebToolController({
46
49
  const fetchInFlight = new Map();
47
50
  const cleanups = new Set();
48
51
  let closed = false;
52
+ const searchState = createWebSearchRunState(searchConfig, suppliedSearchState);
49
53
 
50
54
  function registerCleanup(cleanup) {
51
55
  if (closed) {
@@ -74,7 +78,7 @@ export function createWebToolController({
74
78
  const result = await task;
75
79
  if (!result.error) {
76
80
  cache.set(key, cloneResult(result));
77
- while (cache.size > MAX_CACHE_ENTRIES) {
81
+ while (cache.size > MAX_CACHE_ENTRIES || [...cache.values()].reduce((n, r) => n + Buffer.byteLength(r.document?.body || "", "utf8"), 0) > 32 * 1024 * 1024) {
78
82
  cache.delete(cache.keys().next().value);
79
83
  }
80
84
  }
@@ -88,12 +92,15 @@ export function createWebToolController({
88
92
  * @param {string} key
89
93
  * @param {() => Promise<any>} execute
90
94
  */
91
- async function cachedSearch(key, execute) {
95
+ async function cachedSearch(key, query, execute) {
92
96
  if (closed) return closedResult();
93
97
  const cached = readSharedSearch(key);
94
- if (cached) return withCacheHit(cached);
98
+ if (cached) return withSearchCacheHit(cached, searchState, query);
95
99
  const active = searchInFlight.get(key);
96
- if (active) return withCacheHit(await active);
100
+ if (active) {
101
+ const result = await active;
102
+ return result.error ? withCacheHit(result) : withSearchCacheHit(result, searchState, query);
103
+ }
97
104
  const task = Promise.resolve().then(execute);
98
105
  searchInFlight.set(key, task);
99
106
  try {
@@ -111,6 +118,7 @@ export function createWebToolController({
111
118
  namespace,
112
119
 
113
120
  async search(params, execution = {}) {
121
+ if (execution.signal?.aborted) return { text: "Error: WebSearch was aborted.", error: true, outcome: { status: "error", code: "aborted" } };
114
122
  // The key must pin the backend, the endpoint AND the network policy the
115
123
  // search actually ran under. A params-only key was safe while the cache
116
124
  // lived and died with one run; process-wide it would let controllers with
@@ -132,30 +140,37 @@ export function createWebToolController({
132
140
  // strict as the key claims.
133
141
  const resolvedCtx = ctx ?? readToolRuntime();
134
142
  const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
135
- const key = stableKey({ params, searchConfig, policy });
136
- return cachedSearch(key, async () => performWebSearch(params, {
143
+ const key = stableKey({ params, searchConfig: safeSearchCacheIdentity(searchConfig), policy, coordination: coordinator?.scope });
144
+ return cachedSearch(key, params.query, async () => performWebSearch(params, {
145
+ coordinator,
137
146
  searchConfig,
138
147
  sandboxPolicy: policy,
139
148
  ctx: resolvedCtx,
140
149
  fetchImpl,
141
150
  codexSearch,
151
+ searchState,
142
152
  signal: execution.signal,
143
153
  }));
144
154
  },
145
155
 
146
156
  async fetch(params, execution = {}) {
147
- const key = stableKey(params);
148
- return cachedRun(fetchCache, fetchInFlight, key, async () => performWebFetch(params, {
149
- fetchConfig,
150
- sandboxPolicy,
151
- sandboxEngine,
152
- ctx,
153
- fetchImpl,
154
- browserRenderer,
155
- signal: execution.signal,
156
- namespace,
157
- registerCleanup,
157
+ if (execution.signal?.aborted) return { text: "Error: WebFetch was aborted.", error: true, outcome: { status: "error", code: "aborted" } };
158
+ const resolvedCtx = ctx ?? readToolRuntime();
159
+ const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
160
+ const { start_line, max_lines, max_output_chars, ...request } = params;
161
+ if ((start_line !== undefined && (!Number.isSafeInteger(start_line) || start_line < 1))
162
+ || (max_lines !== undefined && (!Number.isSafeInteger(max_lines) || max_lines < 1 || max_lines > 10000))) {
163
+ return { text: "Error: Invalid WebFetch line range.", error: true, outcome: { status: "error", code: "invalid_range" } };
164
+ }
165
+ const key = stableKey({ request, fetchConfig, policy, coordination: coordinator?.scope });
166
+ const result = await cachedRun(fetchCache, fetchInFlight, key, async () => performWebFetch(request, {
167
+ documentOnly: true, coordinator, fetchConfig, sandboxPolicy: policy, sandboxEngine,
168
+ ctx: resolvedCtx, fetchImpl, browserRenderer, signal: execution.signal,
169
+ namespace, registerCleanup,
158
170
  }));
171
+ if (result.error || !result.document) return result;
172
+ const sliced = formatWebFetchDocument({ ...result.document, outcome: result.outcome }, params, resolvedCtx);
173
+ return { ...sliced, document: undefined, outcome: { ...sliced.outcome, cacheHit: result.outcome.cacheHit } };
159
174
  },
160
175
 
161
176
  async close() {
@@ -174,6 +189,19 @@ export function createWebToolController({
174
189
  };
175
190
  }
176
191
 
192
+ function safeSearchCacheIdentity(searchConfig) {
193
+ if (!searchConfig || typeof searchConfig !== "object") return searchConfig;
194
+ const { maxRequestsPerRun: _budget, ...identity } = searchConfig;
195
+ if (typeof searchConfig?.ollama?.apiKey !== "string") return identity;
196
+ return {
197
+ ...identity,
198
+ ollama: {
199
+ ...searchConfig.ollama,
200
+ apiKey: `sha256:${createHash("sha256").update(searchConfig.ollama.apiKey).digest("hex")}`,
201
+ },
202
+ };
203
+ }
204
+
177
205
  function readSharedSearch(key) {
178
206
  const entry = sharedSearchCache.get(key);
179
207
  if (!entry) return null;
@@ -218,7 +246,15 @@ function sortValue(value) {
218
246
  function cloneResult(result) {
219
247
  return {
220
248
  ...result,
221
- outcome: result.outcome ? { ...result.outcome } : result.outcome,
249
+ outcome: result.outcome ? {
250
+ ...result.outcome,
251
+ ...(Array.isArray(result.outcome.providerAttempts)
252
+ ? { providerAttempts: result.outcome.providerAttempts.map((entry) => ({ ...entry })) }
253
+ : {}),
254
+ ...(Array.isArray(result.outcome.failureMetadata)
255
+ ? { failureMetadata: result.outcome.failureMetadata.map((entry) => ({ ...entry })) }
256
+ : {}),
257
+ } : result.outcome,
222
258
  };
223
259
  }
224
260
 
@@ -228,10 +264,65 @@ function withCacheHit(result) {
228
264
  outcome: {
229
265
  ...(result.outcome || {}),
230
266
  cacheHit: true,
267
+ attempts: 0, durationMs: 0, queueWaitMs: 0, backendDurationMs: 0,
268
+ cooldownSkipCount: 0, quotaSkipCount: 0,
269
+ ...(Number.isSafeInteger(result.outcome?.requestsThisCall) ? { requestsThisCall: 0 } : {}),
231
270
  },
232
271
  };
233
272
  }
234
273
 
274
+ function withSearchCacheHit(result, searchState, requestedQuery) {
275
+ const cloned = withCacheHit(result);
276
+ const budget = webSearchBudgetSnapshot(searchState, 0);
277
+ const resultCount = Number.isSafeInteger(cloned.outcome?.resultCount) ? cloned.outcome.resultCount : 0;
278
+ const nextAction = resultCount > 0
279
+ ? "fetch_existing_sources"
280
+ : budget.requestsRemaining > 0 ? "refine_query" : "use_available_evidence";
281
+ const action = nextAction === "fetch_existing_sources"
282
+ ? "Use WebFetch on the strongest returned URLs before searching again."
283
+ : nextAction === "refine_query"
284
+ ? "Refine the query only for a material evidence gap."
285
+ : "Do not retry WebSearch in this run; use available evidence and state the limitation.";
286
+ const control = `[Search control: requests=${budget.requestsUsed}/${budget.maxRequestsPerRun}; remaining=${budget.requestsRemaining}; ${action}]`;
287
+ const query = collapseWhitespace(requestedQuery).slice(0, 500);
288
+ const metadata = `[Search metadata: backend=${cloned.outcome?.backend || "unknown"}; attempted=none; actual_query=${JSON.stringify(query)}; fallback=none]`;
289
+ const textWithControl = typeof cloned.text === "string" && cloned.text.startsWith("[Search control:")
290
+ ? cloned.text.replace(/^\[Search control:[^\n]*\]/u, control)
291
+ : `${control}\n${cloned.text}`;
292
+ const text = textWithControl.includes("[Search metadata:")
293
+ ? textWithControl.replace(/^\[Search metadata:[^\n]*\]/mu, metadata)
294
+ : textWithControl.replace("[BEGIN UNTRUSTED WEB SEARCH RESULTS]", `[BEGIN UNTRUSTED WEB SEARCH RESULTS]\n${metadata}`);
295
+ const {
296
+ retryAfterMs: _retryAfterMs,
297
+ retryAt: _retryAt,
298
+ retryAtMs: _retryAtMs,
299
+ ...cachedOutcome
300
+ } = cloned.outcome || {};
301
+ return {
302
+ ...cloned,
303
+ text,
304
+ outcome: {
305
+ ...cachedOutcome,
306
+ ...budget,
307
+ bytes: Buffer.byteLength(text, "utf8"),
308
+ attemptedBackends: [],
309
+ actualQueries: [],
310
+ providerAttempts: [],
311
+ failureMetadata: [],
312
+ providerFailureCount: 0,
313
+ rateLimited: false,
314
+ cooldownBackends: [],
315
+ fallbackUsed: false,
316
+ retryInRun: budget.requestsRemaining > 0,
317
+ nextAction,
318
+ },
319
+ };
320
+ }
321
+
322
+ function collapseWhitespace(value) {
323
+ return typeof value === "string" ? value.replace(/\s+/gu, " ").trim() : "";
324
+ }
325
+
235
326
  function closedResult() {
236
327
  const text = "Error: Web tool controller has already closed.";
237
328
  return {
@@ -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 }); }