@abot-ai/runtime 1.1.0 → 1.2.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 (268) hide show
  1. package/README.md +6 -3
  2. package/dist/src/capabilities/tool-types.d.ts +1 -1
  3. package/dist/src/model-gateway/providers/ollama/generation-options.d.ts +1 -3
  4. package/dist/src/model-gateway/providers/ollama/generation-options.js +1 -5
  5. package/dist/src/model-gateway/providers/ollama/payload.js +0 -8
  6. package/dist/src/model-gateway/server/request-body.js +1 -0
  7. package/dist/src/plugin-sdk/index.d.ts +2 -0
  8. package/dist/src/plugin-sdk/index.js +2 -0
  9. package/dist/src/plugin-sdk/tool-availability-brief.d.ts +9 -0
  10. package/dist/src/plugin-sdk/tool-availability-brief.js +48 -0
  11. package/dist/src/plugin-sdk/tool-availability-overview.d.ts +9 -0
  12. package/dist/src/plugin-sdk/tool-availability-overview.js +62 -0
  13. package/dist/src/runtime/adapters/registered-tool-payload-plan.d.ts +0 -9
  14. package/dist/src/runtime/adapters/registered-tool-payload-plan.js +0 -67
  15. package/dist/src/runtime/adapters/registered-tool-payload-related-artifacts.d.ts +13 -0
  16. package/dist/src/runtime/adapters/registered-tool-payload-related-artifacts.js +84 -0
  17. package/dist/src/runtime/adapters/registered-tool-worker-capabilities/catalog.js +4 -0
  18. package/dist/src/runtime/adapters/registered-tool-worker-capabilities/execution.js +3 -0
  19. package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-lifecycle.d.ts +1 -0
  20. package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-lifecycle.js +11 -1
  21. package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-stages.d.ts +1 -0
  22. package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-stages.js +9 -1
  23. package/dist/src/runtime/capabilities/request-bound-tool-registry.js +1 -24
  24. package/dist/src/runtime/capabilities/tool-availability.d.ts +6 -0
  25. package/dist/src/runtime/capabilities/tool-availability.js +28 -0
  26. package/dist/src/runtime/context/capability-brief.d.ts +25 -0
  27. package/dist/src/runtime/context/capability-brief.js +106 -0
  28. package/dist/src/runtime/context/request-tool-results-message-binding.d.ts +6 -0
  29. package/dist/src/runtime/context/request-tool-results-message-binding.js +30 -0
  30. package/dist/src/runtime/context/request-tool-results-scope.d.ts +3 -0
  31. package/dist/src/runtime/context/request-tool-results-scope.js +8 -0
  32. package/dist/src/runtime/context/request-tool-results.d.ts +8 -4
  33. package/dist/src/runtime/context/request-tool-results.js +49 -38
  34. package/dist/src/runtime/orchestration/role-calls/candidate-validation.js +8 -1
  35. package/dist/src/runtime/orchestration/role-calls/child-return.d.ts +4 -0
  36. package/dist/src/runtime/orchestration/role-calls/child-return.js +25 -5
  37. package/dist/src/runtime/orchestration/role-calls/command-decoder.js +8 -3
  38. package/dist/src/runtime/orchestration/role-calls/command-dispatch/child-transitions.d.ts +1 -1
  39. package/dist/src/runtime/orchestration/role-calls/command-dispatch/child-transitions.js +17 -2
  40. package/dist/src/runtime/orchestration/role-calls/command-dispatch.d.ts +1 -1
  41. package/dist/src/runtime/orchestration/role-calls/command-dispatch.js +2 -2
  42. package/dist/src/runtime/orchestration/role-calls/contracts.d.ts +5 -2
  43. package/dist/src/runtime/orchestration/role-calls/contracts.js +4 -4
  44. package/dist/src/runtime/orchestration/role-calls/dependency-results.d.ts +1 -1
  45. package/dist/src/runtime/orchestration/role-calls/dependency-results.js +1 -0
  46. package/dist/src/runtime/orchestration/role-calls/index.d.ts +5 -1
  47. package/dist/src/runtime/orchestration/role-calls/index.js +5 -1
  48. package/dist/src/runtime/orchestration/role-calls/ledger.d.ts +3 -1
  49. package/dist/src/runtime/orchestration/role-calls/ledger.js +9 -2
  50. package/dist/src/runtime/orchestration/role-calls/reducer.d.ts +1 -1
  51. package/dist/src/runtime/orchestration/role-calls/reducer.js +2 -2
  52. package/dist/src/runtime/orchestration/role-calls/result-receipt.d.ts +49 -0
  53. package/dist/src/runtime/orchestration/role-calls/result-receipt.js +188 -0
  54. package/dist/src/runtime/orchestration/role-calls/return-result-receipt.d.ts +19 -0
  55. package/dist/src/runtime/orchestration/role-calls/return-result-receipt.js +52 -0
  56. package/dist/src/runtime/orchestration/role-calls/transactions.js +1 -0
  57. package/dist/src/runtime/orchestration/role-calls/work-result-lineage.d.ts +34 -0
  58. package/dist/src/runtime/orchestration/role-calls/work-result-lineage.js +185 -0
  59. package/dist/src/runtime/orchestration/role-calls/work-result-receipt.d.ts +36 -0
  60. package/dist/src/runtime/orchestration/role-calls/work-result-receipt.js +187 -0
  61. package/dist/src/runtime/orchestration/role-executors/activation/result-normalization.js +6 -3
  62. package/dist/src/runtime/orchestration/role-executors/child-invocation/transaction.js +1 -0
  63. package/dist/src/runtime/orchestration/role-executors/contracts.d.ts +2 -1
  64. package/dist/src/runtime/orchestration/worker-capabilities/assignment-provenance.d.ts +36 -0
  65. package/dist/src/runtime/orchestration/worker-capabilities/assignment-provenance.js +171 -0
  66. package/dist/src/runtime/orchestration/worker-capabilities/contracts.d.ts +32 -1
  67. package/dist/src/runtime/orchestration/worker-capabilities/execution/invocation-preparation.d.ts +2 -1
  68. package/dist/src/runtime/orchestration/worker-capabilities/execution/invocation-preparation.js +3 -0
  69. package/dist/src/runtime/orchestration/worker-capabilities/execution.js +42 -19
  70. package/dist/src/runtime/orchestration/worker-capabilities/index.d.ts +2 -0
  71. package/dist/src/runtime/orchestration/worker-capabilities/index.js +2 -0
  72. package/dist/src/runtime/orchestration/worker-capabilities/payload-assignment-validation.d.ts +3 -0
  73. package/dist/src/runtime/orchestration/worker-capabilities/payload-assignment-validation.js +30 -0
  74. package/dist/src/runtime/orchestration/worker-capabilities/payload-author.js +8 -19
  75. package/dist/src/runtime/orchestration/worker-capabilities/payload-contracts.d.ts +5 -1
  76. package/dist/src/runtime/orchestration/worker-capabilities/payload-instructions.js +1 -1
  77. package/dist/src/runtime/orchestration/worker-capabilities/payload-source-provenance.d.ts +14 -0
  78. package/dist/src/runtime/orchestration/worker-capabilities/payload-source-provenance.js +132 -0
  79. package/dist/src/runtime/request/capability-brief.d.ts +5 -0
  80. package/dist/src/runtime/request/capability-brief.js +24 -0
  81. package/dist/src/runtime/request/execution-scope.d.ts +2 -1
  82. package/dist/src/runtime/request/role-executor-composition.js +1 -1
  83. package/dist/src/runtime/request/worker-capability-composition.d.ts +2 -1
  84. package/dist/src/runtime/request/worker-capability-composition.js +6 -1
  85. package/dist/src/runtime/request/worker-capability-payload.js +11 -8
  86. package/dist/src/runtime/steps/execution-agent/prompt.js +4 -3
  87. package/dist/src/runtime/steps/planner-decision/capability-catalog-context.d.ts +14 -0
  88. package/dist/src/runtime/steps/planner-decision/capability-catalog-context.js +87 -0
  89. package/dist/src/runtime/steps/planner-decision/contracts.d.ts +3 -5
  90. package/dist/src/runtime/steps/planner-decision/contracts.js +3 -3
  91. package/dist/src/runtime/steps/planner-decision/format.d.ts +3 -2
  92. package/dist/src/runtime/steps/planner-decision/format.js +73 -76
  93. package/dist/src/runtime/steps/planner-decision/input.d.ts +4 -3
  94. package/dist/src/runtime/steps/planner-decision/input.js +10 -7
  95. package/dist/src/runtime/steps/planner-decision/parser.d.ts +3 -2
  96. package/dist/src/runtime/steps/planner-decision/parser.js +10 -19
  97. package/dist/src/runtime/steps/planner-decision/plan-parser.js +18 -12
  98. package/dist/src/runtime/steps/planner-decision/prompt.js +33 -28
  99. package/dist/src/runtime/steps/planner-decision/resume.d.ts +1 -1
  100. package/dist/src/runtime/steps/planner-decision/resume.js +38 -1
  101. package/dist/src/runtime/steps/planner-decision/run.d.ts +3 -2
  102. package/dist/src/runtime/steps/reviewer-decision/audit-coverage.d.ts +10 -0
  103. package/dist/src/runtime/steps/reviewer-decision/audit-coverage.js +186 -0
  104. package/dist/src/runtime/steps/reviewer-decision/contracts.d.ts +3 -8
  105. package/dist/src/runtime/steps/reviewer-decision/contracts.js +12 -6
  106. package/dist/src/runtime/steps/reviewer-decision/delegated-context.d.ts +24 -0
  107. package/dist/src/runtime/steps/reviewer-decision/delegated-context.js +70 -0
  108. package/dist/src/runtime/steps/reviewer-decision/diagnostics.js +1 -1
  109. package/dist/src/runtime/steps/reviewer-decision/format.js +37 -24
  110. package/dist/src/runtime/steps/reviewer-decision/input.js +1 -1
  111. package/dist/src/runtime/steps/reviewer-decision/model-context.d.ts +4 -3
  112. package/dist/src/runtime/steps/reviewer-decision/model-context.js +92 -10
  113. package/dist/src/runtime/steps/reviewer-decision/parser.js +19 -1
  114. package/dist/src/runtime/steps/reviewer-decision/projection.js +42 -68
  115. package/dist/src/runtime/steps/reviewer-decision/prompt.js +10 -5
  116. package/dist/src/runtime/steps/reviewer-decision/run.js +12 -3
  117. package/dist/src/runtime/steps/reviewer-decision/verification-coverage.d.ts +14 -0
  118. package/dist/src/runtime/steps/reviewer-decision/verification-coverage.js +52 -0
  119. package/dist/src/runtime/steps/supervisor-decision/capability-brief.d.ts +14 -0
  120. package/dist/src/runtime/steps/supervisor-decision/capability-brief.js +38 -0
  121. package/dist/src/runtime/steps/supervisor-decision/contracts.d.ts +7 -3
  122. package/dist/src/runtime/steps/supervisor-decision/diagnostics.d.ts +2 -0
  123. package/dist/src/runtime/steps/supervisor-decision/diagnostics.js +8 -2
  124. package/dist/src/runtime/steps/supervisor-decision/format.js +32 -20
  125. package/dist/src/runtime/steps/supervisor-decision/input.d.ts +4 -1
  126. package/dist/src/runtime/steps/supervisor-decision/input.js +19 -1
  127. package/dist/src/runtime/steps/supervisor-decision/parser.js +62 -59
  128. package/dist/src/runtime/steps/supervisor-decision/prompt.js +6 -7
  129. package/dist/src/runtime/steps/supervisor-decision/resume.js +47 -1
  130. package/dist/src/runtime/steps/supervisor-decision/run.js +10 -0
  131. package/dist/src/runtime/steps/supervisor-decision/working-directory.js +2 -1
  132. package/dist/src/runtime/steps/worker-decision/contracts.d.ts +2 -1
  133. package/dist/src/runtime/steps/worker-decision/input/canonical-state.js +8 -1
  134. package/dist/src/runtime/steps/worker-decision/input/diagnostic-projection.js +2 -2
  135. package/dist/src/runtime/steps/worker-decision/input/prompt-context.d.ts +1 -0
  136. package/dist/src/runtime/steps/worker-decision/input/prompt-context.js +4 -1
  137. package/dist/src/runtime/steps/worker-decision/input/reference-context.js +34 -14
  138. package/dist/src/runtime/steps/worker-decision/input/types.d.ts +2 -1
  139. package/dist/src/runtime/steps/worker-decision/input.d.ts +1 -0
  140. package/dist/src/runtime/steps/worker-decision/input.js +1 -0
  141. package/dist/src/runtime/steps/worker-decision/payload-dependency-results.d.ts +8 -0
  142. package/dist/src/runtime/steps/worker-decision/payload-dependency-results.js +19 -0
  143. package/dist/src/runtime/steps/worker-decision/prompt.js +8 -8
  144. package/dist/src/runtime/steps/worker-decision/result-author.js +18 -11
  145. package/dist/src/runtime/steps/worker-decision/run.js +2 -2
  146. package/dist/src/web-ui/app/ARCHITECTURE.md +19 -6
  147. package/dist/src/web-ui/app/app.js +6 -7
  148. package/dist/src/web-ui/app/components/composer-context-window.d.ts +25 -0
  149. package/dist/src/web-ui/app/components/composer-context-window.js +155 -0
  150. package/dist/src/web-ui/app/components/composer-plan.d.ts +9 -0
  151. package/dist/src/web-ui/app/components/composer-plan.js +128 -0
  152. package/dist/src/web-ui/app/components/conversation-activity.d.ts +5 -6
  153. package/dist/src/web-ui/app/components/conversation-activity.js +64 -157
  154. package/dist/src/web-ui/app/components/conversation-role-cards.d.ts +23 -0
  155. package/dist/src/web-ui/app/components/conversation-role-cards.js +181 -0
  156. package/dist/src/web-ui/app/components/conversation-sidebar-layout.js +12 -0
  157. package/dist/src/web-ui/app/components/conversation-sources.d.ts +7 -0
  158. package/dist/src/web-ui/app/components/conversation-sources.js +182 -0
  159. package/dist/src/web-ui/app/components/conversation-view.js +34 -6
  160. package/dist/src/web-ui/app/components/model-selector.js +4 -1
  161. package/dist/src/web-ui/app/components/operations-section.js +46 -0
  162. package/dist/src/web-ui/app/components/workspace-shell.js +86 -78
  163. package/dist/src/web-ui/app/controllers/realtime-event-controller.d.ts +1 -0
  164. package/dist/src/web-ui/app/controllers/realtime-event-controller.js +55 -90
  165. package/dist/src/web-ui/app/controllers/runtime-selection-controller.js +2 -0
  166. package/dist/src/web-ui/app/controllers/session-controller.js +17 -12
  167. package/dist/src/web-ui/app/index.html +209 -210
  168. package/dist/src/web-ui/app/lib/composer-plan-model.d.ts +32 -0
  169. package/dist/src/web-ui/app/lib/composer-plan-model.js +108 -0
  170. package/dist/src/web-ui/app/lib/conversation-role-model.d.ts +19 -0
  171. package/dist/src/web-ui/app/lib/conversation-role-model.js +193 -0
  172. package/dist/src/web-ui/app/lib/event-presentation.js +30 -9
  173. package/dist/src/web-ui/app/lib/source-url-policy.d.ts +2 -0
  174. package/dist/src/web-ui/app/lib/source-url-policy.js +31 -0
  175. package/dist/src/web-ui/app/lib/task-progress.d.ts +53 -0
  176. package/dist/src/web-ui/app/lib/task-progress.js +233 -0
  177. package/dist/src/web-ui/app/lib/tool-invocation-count.d.ts +5 -0
  178. package/dist/src/web-ui/app/lib/tool-invocation-count.js +21 -0
  179. package/dist/src/web-ui/app/lib/web-source-event.d.ts +8 -0
  180. package/dist/src/web-ui/app/lib/web-source-event.js +21 -0
  181. package/dist/src/web-ui/app/lib/web-sources.d.ts +36 -0
  182. package/dist/src/web-ui/app/lib/web-sources.js +177 -0
  183. package/dist/src/web-ui/app/styles/00-tokens-base.css +15 -3
  184. package/dist/src/web-ui/app/styles/10-shell-sessions.css +24 -34
  185. package/dist/src/web-ui/app/styles/12-conversation-sidebar.css +93 -0
  186. package/dist/src/web-ui/app/styles/20-chat-composer.css +55 -472
  187. package/dist/src/web-ui/app/styles/21-composer-controls.css +368 -0
  188. package/dist/src/web-ui/app/styles/22-composer-context-window.css +90 -0
  189. package/dist/src/web-ui/app/styles/23-composer-plan.css +171 -0
  190. package/dist/src/web-ui/app/styles/24-conversation-role-cards.css +205 -0
  191. package/dist/src/web-ui/app/styles/25-conversation-sources.css +141 -0
  192. package/dist/src/web-ui/app/styles/30-inspector-config.css +21 -209
  193. package/dist/src/web-ui/app/styles/35-memory-onboarding.css +8 -8
  194. package/dist/src/web-ui/app/styles/36-memory-management.css +13 -13
  195. package/dist/src/web-ui/app/styles/37-config-operations.css +197 -0
  196. package/dist/src/web-ui/app/styles/40-config-workspace.css +36 -36
  197. package/dist/src/web-ui/app/styles/90-responsive.css +14 -77
  198. package/dist/src/web-ui/app/styles/foundation.css +2 -2
  199. package/dist/src/web-ui/app/styles.css +7 -0
  200. package/dist/src/web-ui/app/ui-behavior.d.ts +1 -1
  201. package/dist/src/web-ui/app/ui-behavior.js +1 -1
  202. package/docs/configuration.md +15 -8
  203. package/docs/installation.md +7 -4
  204. package/docs/known-limitations.md +21 -0
  205. package/docs/plugins.md +28 -13
  206. package/docs/runtime-invariant-coverage.md +18 -17
  207. package/docs/runtime-prompt-policy-contracts.md +27 -17
  208. package/package.json +4 -3
  209. package/plugins/capability-brief/source/index.ts +5 -48
  210. package/plugins/capability-brief/src/index.cjs +46 -33
  211. package/plugins/system-probe/plugin.json +1 -0
  212. package/plugins/web/README.md +125 -0
  213. package/plugins/web/plugin.json +1 -1
  214. package/plugins/web/skills/web_fetch_skill/SKILL.md +2 -2
  215. package/plugins/web/skills/web_search_skill/SKILL.md +8 -4
  216. package/plugins/web/source/errors.ts +5 -1
  217. package/plugins/web/source/fetch-presentation.ts +61 -0
  218. package/plugins/web/source/fetch-service.ts +2 -31
  219. package/plugins/web/source/light/config.ts +196 -0
  220. package/plugins/web/source/light/crawl/origin-gate.ts +134 -0
  221. package/plugins/web/source/light/crawl/robots-cache.ts +71 -0
  222. package/plugins/web/source/light/crawl/robots-parser.ts +132 -0
  223. package/plugins/web/source/light/crawl/robots-policy.ts +154 -0
  224. package/plugins/web/source/light/crawl/session-budget.ts +141 -0
  225. package/plugins/web/source/light/crawl/transport.ts +216 -0
  226. package/plugins/web/source/light/discovery/candidate-values.ts +63 -0
  227. package/plugins/web/source/light/discovery/feed-document.ts +127 -0
  228. package/plugins/web/source/light/discovery/html-links.ts +80 -0
  229. package/plugins/web/source/light/discovery/html-robots-directives.ts +29 -0
  230. package/plugins/web/source/light/discovery/markup-tree.ts +145 -0
  231. package/plugins/web/source/light/discovery/origin-dates.ts +71 -0
  232. package/plugins/web/source/light/discovery/sitemap-document.ts +63 -0
  233. package/plugins/web/source/light/documents/cache-policy.ts +28 -0
  234. package/plugins/web/source/light/documents/document-cache.ts +99 -0
  235. package/plugins/web/source/light/documents/document-charset.ts +89 -0
  236. package/plugins/web/source/light/documents/document-contract.ts +20 -0
  237. package/plugins/web/source/light/documents/document-reader.ts +75 -0
  238. package/plugins/web/source/light/documents/extract-document.ts +124 -0
  239. package/plugins/web/source/light/documents/http-cache-freshness.ts +68 -0
  240. package/plugins/web/source/light/documents/response-decoding.ts +120 -0
  241. package/plugins/web/source/light/ranking/document-ranking.ts +114 -0
  242. package/plugins/web/source/light/ranking/query-normalization.ts +69 -0
  243. package/plugins/web/source/light/search-collection.ts +136 -0
  244. package/plugins/web/source/light/search-frontier.ts +246 -0
  245. package/plugins/web/source/light/search-metadata.ts +84 -0
  246. package/plugins/web/source/light/search-service.ts +154 -0
  247. package/plugins/web/source/light/search-unavailable.ts +33 -0
  248. package/plugins/web/source/light/sources/catalog.ts +312 -0
  249. package/plugins/web/source/light/sources/source-definition.ts +9 -0
  250. package/plugins/web/source/light/sources/source-selection.ts +35 -0
  251. package/plugins/web/source/plugin.ts +70 -56
  252. package/plugins/web/source/public-http.ts +10 -1
  253. package/plugins/web/source/search-presentation.ts +63 -63
  254. package/plugins/web/source/search-service-selector.ts +31 -0
  255. package/plugins/web/source/search-service.ts +6 -0
  256. package/plugins/web/source/search-source-presentation.ts +76 -0
  257. package/plugins/web/source/search-types.ts +4 -0
  258. package/plugins/web/source/source-output-receipts.ts +267 -0
  259. package/plugins/web/source/source-receipt-budget.ts +51 -0
  260. package/plugins/web/source/source-receipt-contract.ts +18 -0
  261. package/plugins/web/src/index.cjs +3214 -288
  262. package/src/runtime/README.md +30 -9
  263. package/dist/src/model-gateway/structured-output/budget.d.ts +0 -7
  264. package/dist/src/model-gateway/structured-output/budget.js +0 -20
  265. package/dist/src/model-gateway/structured-output/schema-character-estimator.d.ts +0 -1
  266. package/dist/src/model-gateway/structured-output/schema-character-estimator.js +0 -211
  267. package/dist/src/model-gateway/structured-output-budget.d.ts +0 -1
  268. package/dist/src/model-gateway/structured-output-budget.js +0 -1
@@ -0,0 +1,124 @@
1
+ import { extractFetchedPage } from "../../content.js";
2
+ import { WebPluginError } from "../../errors.js";
3
+ import { WEB_LIMITS } from "../../limits.js";
4
+ import type { PublicHttpResponse } from "../../public-http.js";
5
+ import { assertCandidateLimit } from "../discovery/candidate-values.js";
6
+ import { parseFeedDocument } from "../discovery/feed-document.js";
7
+ import { discoverHtmlLinks } from "../discovery/html-links.js";
8
+ import {
9
+ childElements,
10
+ localName,
11
+ parseMarkup,
12
+ } from "../discovery/markup-tree.js";
13
+ import { parseSitemapDocument } from "../discovery/sitemap-document.js";
14
+ import type { LightDocument } from "./document-contract.js";
15
+ import { decodeLightDocumentText } from "./document-charset.js";
16
+
17
+ export type { LightDocument, DiscoveryCandidate } from "./document-contract.js";
18
+
19
+ function mediaType(response: PublicHttpResponse): string {
20
+ const raw = response.headers["content-type"];
21
+ return ((Array.isArray(raw) ? raw[0] : raw) ?? "")
22
+ .split(";", 1)[0]!
23
+ .trim()
24
+ .toLowerCase();
25
+ }
26
+
27
+ function isXmlSource(type: string, text: string): boolean {
28
+ if (
29
+ [
30
+ "application/xml",
31
+ "text/xml",
32
+ "application/rss+xml",
33
+ "application/atom+xml",
34
+ ].includes(type)
35
+ )
36
+ return true;
37
+ if (type.endsWith("+xml") && type !== "application/xhtml+xml") return true;
38
+ return /^\s*(?:<\?xml[^>]*>\s*)?<(?:[\w.-]+:)?(?:rss|feed|rdf|urlset|sitemapindex)\b/iu.test(
39
+ text,
40
+ );
41
+ }
42
+
43
+ function normalizedPageResponse(
44
+ response: PublicHttpResponse,
45
+ type: string,
46
+ text: string,
47
+ ): PublicHttpResponse {
48
+ const needsXmlMediaType =
49
+ type.endsWith("+xml") && type !== "application/xhtml+xml";
50
+ const contentType = needsXmlMediaType ? "application/xml" : type;
51
+ return {
52
+ ...response,
53
+ body: Buffer.from(text, "utf8"),
54
+ headers: {
55
+ ...response.headers,
56
+ "content-type": `${contentType}; charset=utf-8`,
57
+ },
58
+ };
59
+ }
60
+
61
+ export function parseLightDocument(
62
+ response: PublicHttpResponse,
63
+ maxCandidates: number,
64
+ ): LightDocument {
65
+ assertCandidateLimit(maxCandidates);
66
+ if (response.status < 200 || response.status >= 300) {
67
+ throw new WebPluginError(
68
+ "web_fetch_http_error",
69
+ `The source server returned HTTP ${response.status}.`,
70
+ );
71
+ }
72
+ if (response.body.byteLength > WEB_LIMITS.responseBytes) {
73
+ throw new WebPluginError(
74
+ "web_response_invalid",
75
+ "The source exceeds the decoded response limit.",
76
+ );
77
+ }
78
+ const type = mediaType(response);
79
+ const text = decodeLightDocumentText(response);
80
+ const xmlMode = isXmlSource(type, text);
81
+ const isMarkupPage = type === "text/html" || type === "application/xhtml+xml";
82
+ if (xmlMode) {
83
+ const root = parseMarkup(text, true);
84
+ const name = localName(childElements(root)[0] ?? root);
85
+ if (["rss", "feed", "rdf"].includes(name)) {
86
+ const document = parseFeedDocument(
87
+ root,
88
+ response.finalUrl,
89
+ maxCandidates,
90
+ );
91
+ return Object.freeze({
92
+ ...document,
93
+ partial: document.partial || response.partialContent,
94
+ });
95
+ }
96
+ if (["urlset", "sitemapindex"].includes(name)) {
97
+ const document = parseSitemapDocument(
98
+ root,
99
+ response.finalUrl,
100
+ maxCandidates,
101
+ );
102
+ return Object.freeze({
103
+ ...document,
104
+ partial: document.partial || response.partialContent,
105
+ });
106
+ }
107
+ }
108
+ const page = extractFetchedPage(normalizedPageResponse(response, type, text));
109
+ const discovery = isMarkupPage
110
+ ? discoverHtmlLinks(
111
+ parseMarkup(text, false),
112
+ response.finalUrl,
113
+ maxCandidates,
114
+ )
115
+ : { links: Object.freeze([]), partial: false };
116
+ return Object.freeze({
117
+ canonicalUrl: page.finalUrl,
118
+ title: page.title,
119
+ text: page.text,
120
+ kind: "page",
121
+ ...discovery,
122
+ partial: page.partialContent || discovery.partial,
123
+ });
124
+ }
@@ -0,0 +1,68 @@
1
+ import type { PublicHttpResponse } from "../../public-http.js";
2
+
3
+ function header(
4
+ response: PublicHttpResponse,
5
+ name: string,
6
+ ): string | undefined {
7
+ const value = response.headers[name];
8
+ return Array.isArray(value) ? value.join(",") : value;
9
+ }
10
+
11
+ function parseDeltaSeconds(value: string): number {
12
+ if (!/^\d+$/u.test(value)) return Number.NaN;
13
+ const milliseconds = Number(value) * 1_000;
14
+ return Number.isSafeInteger(milliseconds) ? milliseconds : Number.NaN;
15
+ }
16
+
17
+ function explicitFreshnessLifetime(
18
+ response: PublicHttpResponse,
19
+ directives: readonly string[],
20
+ responseDate: number,
21
+ fallback: number,
22
+ ): number {
23
+ const maxAge = directives.filter(
24
+ (value) => value.split("=", 1)[0]!.trim() === "max-age",
25
+ );
26
+ if (maxAge.length > 1) return 0;
27
+ if (maxAge.length === 1) {
28
+ const value = maxAge[0]!.match(/^max-age\s*=\s*(?:"(\d+)"|(\d+))$/u);
29
+ if (!value) return 0;
30
+ return parseDeltaSeconds(value[1] ?? value[2]!);
31
+ }
32
+ const expires = header(response, "expires");
33
+ if (expires === undefined) return fallback;
34
+ return Date.parse(expires) - responseDate;
35
+ }
36
+
37
+ export function httpCacheFreshnessMs(
38
+ response: PublicHttpResponse,
39
+ maximum: number,
40
+ now: number,
41
+ ): number {
42
+ const directives = (header(response, "cache-control") ?? "")
43
+ .toLowerCase()
44
+ .split(",")
45
+ .map((value) => value.trim());
46
+ const names = new Set(
47
+ directives.map((value) => value.split("=", 1)[0]!.trim()),
48
+ );
49
+ if (names.has("no-store")) return 0;
50
+ if (names.has("no-cache")) return 0;
51
+
52
+ const date = header(response, "date");
53
+ const responseDate = date === undefined ? now : Date.parse(date);
54
+ const rawAge = header(response, "age");
55
+ const age = rawAge === undefined ? 0 : parseDeltaSeconds(rawAge.trim());
56
+ const lifetime = explicitFreshnessLifetime(
57
+ response,
58
+ directives,
59
+ responseDate,
60
+ maximum,
61
+ );
62
+ // Invalid or conflicting origin freshness never earns a default cache lifetime.
63
+ if (!Number.isFinite(responseDate)) return 0;
64
+ if (!Number.isFinite(age)) return 0;
65
+ if (!Number.isFinite(lifetime)) return 0;
66
+ const currentAge = Math.max(0, now - responseDate, age);
67
+ return Math.min(maximum, Math.max(0, lifetime - currentAge));
68
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ brotliDecompressSync,
3
+ gunzipSync,
4
+ inflateRawSync,
5
+ inflateSync,
6
+ } from "node:zlib";
7
+
8
+ import { WebPluginError } from "../../errors.js";
9
+ import { WEB_LIMITS } from "../../limits.js";
10
+ import type { PublicHttpResponse } from "../../public-http.js";
11
+
12
+ function responseHeader(response: PublicHttpResponse, name: string): string {
13
+ const value = response.headers[name];
14
+ return (Array.isArray(value) ? value[0] : value) ?? "";
15
+ }
16
+
17
+ function responseEncoding(response: PublicHttpResponse): string {
18
+ const declared = responseHeader(response, "content-encoding")
19
+ .trim()
20
+ .toLowerCase();
21
+ if (declared && declared !== "identity") return declared;
22
+ const hasGzipSignature =
23
+ response.body[0] === 0x1f && response.body[1] === 0x8b;
24
+ return hasGzipSignature ? "gzip" : "identity";
25
+ }
26
+
27
+ function hasZlibWrapperHeader(body: Uint8Array): boolean {
28
+ if (body.byteLength < 2) return false;
29
+ const method = body[0]!;
30
+ if ((method & 0x0f) !== 8) return false;
31
+ if (method >> 4 > 7) return false;
32
+ return ((method << 8) + body[1]!) % 31 === 0;
33
+ }
34
+
35
+ function decodeBody(
36
+ body: Uint8Array,
37
+ encoding: string,
38
+ maxOutputLength: number,
39
+ ): Uint8Array {
40
+ const options = { maxOutputLength };
41
+ if (encoding === "gzip" || encoding === "x-gzip")
42
+ return gunzipSync(body, options);
43
+ if (encoding === "br") return brotliDecompressSync(body, options);
44
+ if (encoding === "deflate") {
45
+ // Select once: a failed wrapper decoder may already have produced output.
46
+ if (hasZlibWrapperHeader(body)) return inflateSync(body, options);
47
+ return inflateRawSync(body, options);
48
+ }
49
+ throw new WebPluginError(
50
+ "web_response_unsupported",
51
+ "The source used an unsupported content encoding.",
52
+ );
53
+ }
54
+
55
+ function decodedContentType(
56
+ response: PublicHttpResponse,
57
+ encoding: string,
58
+ ): string {
59
+ const original = responseHeader(response, "content-type");
60
+ const mediaType = original.split(";", 1)[0]!.trim().toLowerCase();
61
+ if (encoding === "identity") return original;
62
+ if (["application/gzip", "application/x-gzip"].includes(mediaType))
63
+ return "application/xml";
64
+ if (
65
+ mediaType === "application/octet-stream" &&
66
+ /\.xml\.gz(?:[?#]|$)/iu.test(response.finalUrl)
67
+ ) {
68
+ return "application/xml";
69
+ }
70
+ return original;
71
+ }
72
+
73
+ export function decodeLightResponse(
74
+ response: PublicHttpResponse,
75
+ maxBytes: number,
76
+ consumeDecodedBytes?: (count: number) => void,
77
+ ): PublicHttpResponse {
78
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
79
+ throw new RangeError("maxBytes must be a positive safe integer");
80
+ }
81
+ const limit = Math.min(maxBytes, WEB_LIMITS.responseBytes);
82
+ if (response.body.byteLength > WEB_LIMITS.responseBytes) {
83
+ throw new WebPluginError(
84
+ "web_response_invalid",
85
+ "The encoded source exceeds the response limit.",
86
+ );
87
+ }
88
+ const encoding = responseEncoding(response);
89
+ let body: Uint8Array;
90
+ try {
91
+ body =
92
+ encoding === "identity"
93
+ ? response.body.subarray(0, limit)
94
+ : decodeBody(response.body, encoding, limit);
95
+ } catch (error) {
96
+ if (error instanceof WebPluginError) throw error;
97
+ // A failed synchronous decoder does not expose its partial output size.
98
+ // Charge its whole allowance so repeated failures cannot evade the budget.
99
+ consumeDecodedBytes?.(limit);
100
+ throw new WebPluginError(
101
+ "web_response_invalid",
102
+ "The encoded source is invalid or exceeds the decoded response limit.",
103
+ );
104
+ }
105
+ consumeDecodedBytes?.(body.byteLength);
106
+ return Object.freeze({
107
+ ...response,
108
+ headers: Object.freeze({
109
+ ...response.headers,
110
+ "content-encoding": "identity",
111
+ "content-length": String(body.byteLength),
112
+ "content-type": decodedContentType(response, encoding),
113
+ }),
114
+ body,
115
+ bytesRead: body.byteLength,
116
+ partialContent:
117
+ response.partialContent ||
118
+ (encoding === "identity" && response.body.byteLength > limit),
119
+ });
120
+ }
@@ -0,0 +1,114 @@
1
+ import type {
2
+ QueryResult,
3
+ SearchHit,
4
+ SourceFetch,
5
+ } from "../../search-types.js";
6
+ import { WEB_LIMITS } from "../../limits.js";
7
+ import type { LightCachedDocument } from "../documents/document-cache.js";
8
+ import { queryTerms, textTerms } from "./query-normalization.js";
9
+
10
+ type ScoredDocument = Readonly<{ record: LightCachedDocument; score: number }>;
11
+
12
+ function isIndexablePage(record: LightCachedDocument): boolean {
13
+ if (record.document.noIndex) return false;
14
+ return record.document.kind === "page";
15
+ }
16
+
17
+ function scoreDocument(query: string, record: LightCachedDocument): number {
18
+ const terms = queryTerms(query);
19
+ if (terms.length === 0) return 0;
20
+ const titleTerms = queryTerms(record.document.title);
21
+ const title = new Set(titleTerms);
22
+ const body = textTerms(record.document.text);
23
+ const frequencies = new Map<string, number>();
24
+ for (const term of body)
25
+ frequencies.set(term, (frequencies.get(term) ?? 0) + 1);
26
+ let score = 0;
27
+ let matched = 0;
28
+ let titleMatches = 0;
29
+ for (const term of terms) {
30
+ const inTitle = title.has(term);
31
+ const frequency = frequencies.get(term) ?? 0;
32
+ if (!inTitle && frequency === 0) continue;
33
+ matched += 1;
34
+ if (inTitle) titleMatches += 1;
35
+ score +=
36
+ Math.log1p(Math.min(frequency, 3)) / Math.sqrt(1 + body.length / 200);
37
+ }
38
+ if (matched === 0) return 0;
39
+ const titleConcentration = titleMatches / Math.max(1, title.size);
40
+ score += 8 * titleMatches * (0.5 + titleConcentration);
41
+ const hasExactTopicTitle = titleTerms.join(" ") === terms.join(" ");
42
+ if (hasExactTopicTitle) score += 6;
43
+ return (score * matched) / terms.length;
44
+ }
45
+
46
+ function publishedTime(record: LightCachedDocument): number {
47
+ const date = record.document.publishedAt ?? record.document.updatedAt;
48
+ return date ? Date.parse(date) || 0 : 0;
49
+ }
50
+
51
+ function compareDocuments(left: ScoredDocument, right: ScoredDocument): number {
52
+ return (
53
+ right.score - left.score ||
54
+ publishedTime(right.record) - publishedTime(left.record) ||
55
+ left.record.document.canonicalUrl.localeCompare(
56
+ right.record.document.canonicalUrl,
57
+ )
58
+ );
59
+ }
60
+
61
+ function documentSnippet(query: string, text: string): string {
62
+ const terms = queryTerms(query);
63
+ const paragraphs = text
64
+ .split(/\n+/u)
65
+ .map((paragraph, index) => ({
66
+ paragraph,
67
+ index,
68
+ matches: terms.filter((term) => new Set(textTerms(paragraph)).has(term))
69
+ .length,
70
+ }))
71
+ .sort(
72
+ (left, right) => right.matches - left.matches || left.index - right.index,
73
+ );
74
+ return (paragraphs[0]?.paragraph ?? text).slice(0, 800);
75
+ }
76
+
77
+ export function rankLightDocuments(
78
+ queries: readonly string[],
79
+ records: readonly LightCachedDocument[],
80
+ ) {
81
+ const byUrl = new Map(
82
+ records.map((record) => [record.document.canonicalUrl, record]),
83
+ );
84
+ const results: QueryResult[] = queries.map((query) => {
85
+ const ranked = [...byUrl.values()]
86
+ .filter(isIndexablePage)
87
+ .map((record) => ({ record, score: scoreDocument(query, record) }))
88
+ .filter(({ score }) => score > 0)
89
+ .sort(compareDocuments)
90
+ .slice(0, WEB_LIMITS.searchResultsPerQuery);
91
+ const hits: SearchHit[] = ranked.map(({ record }, index) => ({
92
+ query,
93
+ title: record.document.title || record.document.canonicalUrl,
94
+ url: record.document.canonicalUrl,
95
+ domain: new URL(record.document.canonicalUrl).hostname,
96
+ snippet: documentSnippet(query, record.document.text),
97
+ rank: index + 1,
98
+ }));
99
+ return Object.freeze({ query, hits: Object.freeze(hits) });
100
+ });
101
+ const hits = [
102
+ ...new Map(
103
+ results.flatMap((result) => result.hits).map((hit) => [hit.url, hit]),
104
+ ).values(),
105
+ ];
106
+ const sourceFetches: SourceFetch[] = hits
107
+ .slice(0, WEB_LIMITS.searchSourceFetches)
108
+ .map((hit) => ({ hit, page: byUrl.get(hit.url)?.page }));
109
+ return Object.freeze({
110
+ results: Object.freeze(results),
111
+ hits: Object.freeze(hits),
112
+ sourceFetches: Object.freeze(sourceFetches),
113
+ });
114
+ }
@@ -0,0 +1,69 @@
1
+ const QUERY_STOP_WORDS = new Set([
2
+ "a",
3
+ "an",
4
+ "and",
5
+ "are",
6
+ "as",
7
+ "at",
8
+ "be",
9
+ "by",
10
+ "for",
11
+ "from",
12
+ "how",
13
+ "in",
14
+ "is",
15
+ "it",
16
+ "of",
17
+ "on",
18
+ "or",
19
+ "the",
20
+ "to",
21
+ "was",
22
+ "what",
23
+ "when",
24
+ "where",
25
+ "which",
26
+ "who",
27
+ "with",
28
+ "או",
29
+ "איך",
30
+ "אילו",
31
+ "איזה",
32
+ "את",
33
+ "האם",
34
+ "הוא",
35
+ "היא",
36
+ "הם",
37
+ "זה",
38
+ "זו",
39
+ "מה",
40
+ "מי",
41
+ "מתי",
42
+ "של",
43
+ "על",
44
+ "עם",
45
+ ]);
46
+
47
+ export function normalizeSearchText(value: string): string {
48
+ return value
49
+ .normalize("NFKC")
50
+ .toLowerCase()
51
+ .replace(/[\u0591-\u05BD\u05BF-\u05C2\u05C4-\u05C5\u05C7]/gu, "")
52
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
53
+ .trim();
54
+ }
55
+
56
+ export function queryTerms(query: string): readonly string[] {
57
+ const terms = normalizeSearchText(query).split(/\s+/u).filter(Boolean);
58
+ const significant = terms.filter((term) => !QUERY_STOP_WORDS.has(term));
59
+ return Object.freeze([...new Set(significant.length ? significant : terms)]);
60
+ }
61
+
62
+ export function textTerms(text: string): readonly string[] {
63
+ return normalizeSearchText(text).split(/\s+/u).filter(Boolean);
64
+ }
65
+
66
+ export function countTermMatches(query: string, text: string): number {
67
+ const available = new Set(textTerms(text));
68
+ return queryTerms(query).filter((term) => available.has(term)).length;
69
+ }
@@ -0,0 +1,136 @@
1
+ import { isWebPluginError } from "../errors.js";
2
+ import type { LightConfig } from "./config.js";
3
+ import type { LightCrawlSession } from "./crawl/transport.js";
4
+ import type {
5
+ LightCachedDocument,
6
+ LightDocumentCache,
7
+ } from "./documents/document-cache.js";
8
+ import { readLightDocument } from "./documents/document-reader.js";
9
+ import {
10
+ createSearchFrontier,
11
+ type LightCrawlCandidate,
12
+ } from "./search-frontier.js";
13
+ import type { LightSource } from "./sources/source-definition.js";
14
+
15
+ export type LightSourceFailure = Readonly<{
16
+ url: string;
17
+ errorCode: string;
18
+ error: string;
19
+ }>;
20
+
21
+ function isTerminalSearchError(error: unknown): boolean {
22
+ if (!isWebPluginError(error)) return false;
23
+ return error.code === "web_request_aborted";
24
+ }
25
+
26
+ export async function collectLightDocuments(params: {
27
+ queries: readonly string[];
28
+ sources: readonly LightSource[];
29
+ config: LightConfig;
30
+ session: LightCrawlSession;
31
+ cache: LightDocumentCache;
32
+ }) {
33
+ const { session, config, cache, sources } = params;
34
+ const records = new Map(
35
+ cache
36
+ .forSources(new Set(config.sources.map(({ id }) => id)))
37
+ .map((record) => [record.document.canonicalUrl, record]),
38
+ );
39
+ const fetchedUrls = new Set<string>();
40
+ const consultedSources = new Set<string>();
41
+ const failures: LightSourceFailure[] = [];
42
+ const frontier = createSearchFrontier(params.queries, config);
43
+ let cacheHits = 0;
44
+ let cacheMisses = 0;
45
+ let successfulReads = 0;
46
+
47
+ // Round-robin entry admission gives every selected source a first opportunity.
48
+ const entryCount = Math.max(
49
+ ...sources.map(({ entryUrls }) => entryUrls.length),
50
+ );
51
+ for (let index = 0; index < entryCount; index += 1) {
52
+ for (const source of sources) {
53
+ const url = source.entryUrls[index];
54
+ if (url)
55
+ frontier.enqueue(
56
+ { url, title: source.title, kind: "page" },
57
+ source,
58
+ 0,
59
+ true,
60
+ );
61
+ }
62
+ }
63
+
64
+ async function visit(candidate: LightCrawlCandidate): Promise<void> {
65
+ consultedSources.add(candidate.source.id);
66
+ try {
67
+ const { record, fromCache } = await readLightDocument({
68
+ url: candidate.url,
69
+ sourceId: candidate.source.id,
70
+ session,
71
+ config,
72
+ cache,
73
+ });
74
+ successfulReads += 1;
75
+ if (fromCache) cacheHits += 1;
76
+ else {
77
+ cacheMisses += 1;
78
+ fetchedUrls.add(record.document.canonicalUrl);
79
+ }
80
+ records.set(record.document.canonicalUrl, record);
81
+ for (const link of record.document.links) {
82
+ frontier.enqueue(link, candidate.source, candidate.depth + 1);
83
+ }
84
+ for (const sitemap of session.sitemapsFor(
85
+ new URL(candidate.url).origin,
86
+ )) {
87
+ frontier.enqueue(
88
+ { url: sitemap, title: "", kind: "sitemap" },
89
+ candidate.source,
90
+ 0,
91
+ );
92
+ }
93
+ } catch (error) {
94
+ if (isTerminalSearchError(error)) throw error;
95
+ session.assertActive();
96
+ if (failures.length < 8)
97
+ failures.push(
98
+ Object.freeze({
99
+ url: candidate.url,
100
+ errorCode: isWebPluginError(error)
101
+ ? error.code
102
+ : "web_response_invalid",
103
+ error: isWebPluginError(error)
104
+ ? error.message.slice(0, 256)
105
+ : "The Light source could not be read.",
106
+ }),
107
+ );
108
+ }
109
+ }
110
+
111
+ while (frontier.hasPending()) {
112
+ session.assertActive();
113
+ if (!session.canContinue()) break;
114
+ const settled = await Promise.allSettled(frontier.nextBatch().map(visit));
115
+ const rejected = settled.find((result) => result.status === "rejected");
116
+ if (rejected?.status === "rejected") throw rejected.reason;
117
+ }
118
+ session.assertActive();
119
+ const finishedAt = Date.now();
120
+ const currentRecords = [...records.values()].filter(
121
+ (record) =>
122
+ fetchedUrls.has(record.document.canonicalUrl) ||
123
+ record.expiresAt > finishedAt,
124
+ );
125
+ return Object.freeze({
126
+ successfulReads,
127
+ records: Object.freeze(currentRecords) as readonly LightCachedDocument[],
128
+ fetchedUrls,
129
+ consultedSources: Object.freeze([...consultedSources]),
130
+ failures: Object.freeze(failures),
131
+ cacheHits,
132
+ cacheMisses,
133
+ candidatesDiscovered: frontier.discovered(),
134
+ candidatesOmitted: frontier.omitted(),
135
+ });
136
+ }