@xopcai/xopc 0.0.385 → 0.0.386

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 (243) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Cb9eaXxR.js → agent-avatar-dicebear-D-86VOlb.js} +1 -1
  4. package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-DEV-xtgm.js → agent-defaults-settings-panel-SmeUVHY8.js} +1 -1
  5. package/dist/gateway/static/root/assets/{agent-plugin-dialog-DXKc3Luj.js → agent-plugin-dialog-BVT3wXq5.js} +1 -1
  6. package/dist/gateway/static/root/assets/{agents-DcNxwpJQ.js → agents-D7rLQFhX.js} +3 -3
  7. package/dist/gateway/static/root/assets/{app-management-settings-panel-X5i0Tz7_.js → app-management-settings-panel-CUR7n5iP.js} +1 -1
  8. package/dist/gateway/static/root/assets/{appearance-settings-CmHHzquT.js → appearance-settings-BCotSGep.js} +1 -1
  9. package/dist/gateway/static/root/assets/{apps-page-ChFA4Zp0.js → apps-page-hk0bo9LT.js} +1 -1
  10. package/dist/gateway/static/root/assets/{automations-page-BFqsIqOI.js → automations-page-CqxuUwox.js} +1 -1
  11. package/dist/gateway/static/root/assets/{automations-workspace-Blfak-fT.js → automations-workspace-11_Z69pr.js} +1 -1
  12. package/dist/gateway/static/root/assets/{block-editor-CpOLG3Co.js → block-editor-eldB84Mj.js} +1 -1
  13. package/dist/gateway/static/root/assets/{browser-automations-page-BSbTJ9j2.js → browser-automations-page-D4TaUPYT.js} +1 -1
  14. package/dist/gateway/static/root/assets/{browser-settings-page-BUlILJrB.js → browser-settings-page-BXRlxeGV.js} +1 -1
  15. package/dist/gateway/static/root/assets/{capabilities-XFwwUNU3.js → capabilities-BkKgImWi.js} +1 -1
  16. package/dist/gateway/static/root/assets/{capabilities-2Do3eKMM.js → capabilities-DVj9n65C.js} +1 -1
  17. package/dist/gateway/static/root/assets/{capabilities-page-CeRdkwRf.js → capabilities-page-CyYlueEd.js} +2 -2
  18. package/dist/gateway/static/root/assets/{capabilities-settings-panel-S-cKsGja.js → capabilities-settings-panel-uVEgfvo0.js} +1 -1
  19. package/dist/gateway/static/root/assets/{capability-header-actions-6w8pQhnn.js → capability-header-actions-BXv0xu56.js} +1 -1
  20. package/dist/gateway/static/root/assets/{channels-DfNTGBmp.js → channels-fNb3SOzm.js} +1 -1
  21. package/dist/gateway/static/root/assets/{chat-terminal-dock-DnoQHpDc.js → chat-terminal-dock-CnLtparQ.js} +1 -1
  22. package/dist/gateway/static/root/assets/{computer-settings-page-7yliE7sG.js → computer-settings-page-xM0AnOlP.js} +1 -1
  23. package/dist/gateway/static/root/assets/{connector-service-page-DOLAUFSW.js → connector-service-page-CdG2WUb6.js} +1 -1
  24. package/dist/gateway/static/root/assets/{connectors-page-DAsn97XG.js → connectors-page-BRd08GH6.js} +1 -1
  25. package/dist/gateway/static/root/assets/constants-CpWu0oHS.js +1 -0
  26. package/dist/gateway/static/root/assets/{date-picker-DNDqr4Fk.js → date-picker-DIBbE-O8.js} +1 -1
  27. package/dist/gateway/static/root/assets/{dependency-picker-B5oG7RgE.js → dependency-picker-DDD8zokD.js} +1 -1
  28. package/dist/gateway/static/root/assets/{desktop-pet-K7kUCX1O.js → desktop-pet-KlF390Xn.js} +1 -1
  29. package/dist/gateway/static/root/assets/{desktop-pet-settings-KgOAuqSh.js → desktop-pet-settings-CgqQMKDC.js} +1 -1
  30. package/dist/gateway/static/root/assets/{device-pairing-wizard-DvqniZDm.js → device-pairing-wizard-BR6isfG2.js} +1 -1
  31. package/dist/gateway/static/root/assets/{directory-picker-path-field-BmOguLDl.js → directory-picker-path-field-vPQsW05P.js} +1 -1
  32. package/dist/gateway/static/root/assets/{extension-debug-page-DtHNy4TE.js → extension-debug-page-ZHLPrLbH.js} +1 -1
  33. package/dist/gateway/static/root/assets/{extension-page-DSSuMQxp.js → extension-page-DJ42KFRN.js} +1 -1
  34. package/dist/gateway/static/root/assets/{extension-provider-nbgU7xBS.js → extension-provider-6IcsyNaV.js} +1 -1
  35. package/dist/gateway/static/root/assets/{extension-settings-page-DSd__mNd.js → extension-settings-page-D7-5v0V4.js} +1 -1
  36. package/dist/gateway/static/root/assets/{fallback-models-D7bjbiwZ.js → fallback-models-DnYn7qOV.js} +1 -1
  37. package/dist/gateway/static/root/assets/{gateway-settings-hSXGWumL.js → gateway-settings-Bwe3JWX-.js} +1 -1
  38. package/dist/gateway/static/root/assets/{highlighted-body-KQOG7T2V-C7xJwE61.js → highlighted-body-KQOG7T2V-DefTxF3q.js} +1 -1
  39. package/dist/gateway/static/root/assets/{home-page-DJNmlMfH.js → home-page-BtSy2hRV.js} +1 -1
  40. package/dist/gateway/static/root/assets/{imports-page-BufZPzGE.js → imports-page-CTdBYZib.js} +1 -1
  41. package/dist/gateway/static/root/assets/{index-K8SXDtxr.js → index-B08iR7fT.js} +9 -9
  42. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-B8pPP5RF.js → keyboard-shortcuts-settings-CXKp57O_.js} +1 -1
  43. package/dist/gateway/static/root/assets/{local-app-workbench-page-zEtvE7pJ.js → local-app-workbench-page-DzD_OzGY.js} +1 -1
  44. package/dist/gateway/static/root/assets/{local-apps-page-Bli7e_eg.js → local-apps-page-B0E0vttg.js} +1 -1
  45. package/dist/gateway/static/root/assets/{logs-page-BimZk3R4.js → logs-page-eL4fYcMl.js} +1 -1
  46. package/dist/gateway/static/root/assets/{management-settings-5Y4COyU9.js → management-settings-DSpjodMC.js} +1 -1
  47. package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-BTrk2wWv.js +1 -0
  48. package/dist/gateway/static/root/assets/{note-detail-panel-KLDPLVg7.js → note-detail-panel-CsY5tQp5.js} +3 -3
  49. package/dist/gateway/static/root/assets/{notes-page-DST0oPXD.js → notes-page-D09LjNoj.js} +1 -1
  50. package/dist/gateway/static/root/assets/{page-context-capture-button-Bmed83-6.js → page-context-capture-button-DO-aP3D9.js} +1 -1
  51. package/dist/gateway/static/root/assets/{personal-page-BG4Ystrm.js → personal-page-E8F2aGNB.js} +1 -1
  52. package/dist/gateway/static/root/assets/{product-open-page-CXkYP9xE.js → product-open-page-aUoA-uPw.js} +1 -1
  53. package/dist/gateway/static/root/assets/{product-preview-content-DjmupQSq.js → product-preview-content-B-cz-pIV.js} +1 -1
  54. package/dist/gateway/static/root/assets/{project-detail-page-CzBmV0P6.js → project-detail-page-WhefAh8E.js} +1 -1
  55. package/dist/gateway/static/root/assets/{projects-page-DByqe5Em.js → projects-page-CyqAs0tO.js} +1 -1
  56. package/dist/gateway/static/root/assets/{remote-access-hub-DTjj-odF.js → remote-access-hub-BwhRGL6m.js} +1 -1
  57. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-B_ptn3T8.js → runtime-tools-settings-panel-o26dmPNq.js} +1 -1
  58. package/dist/gateway/static/root/assets/{scenes-page-wkua4l5y.js → scenes-page-D_e19kv_.js} +1 -1
  59. package/dist/gateway/static/root/assets/{schema-form-VvOSM84Y.js → schema-form-DLLdqUC3.js} +1 -1
  60. package/dist/gateway/static/root/assets/{send-session-input-BCAF-Ikt.js → send-session-input-2I-nFJcE.js} +1 -1
  61. package/dist/gateway/static/root/assets/session-manager-Co5ozD0l.js +16 -0
  62. package/dist/gateway/static/root/assets/{sessions-page-D06XwYAT.js → sessions-page-DX6INsiS.js} +1 -1
  63. package/dist/gateway/static/root/assets/settings-advanced-gate-C4_gW1vw.js +1 -0
  64. package/dist/gateway/static/root/assets/{settings-page-C-6BJdb1.js → settings-page-Bc_zxxRu.js} +1 -1
  65. package/dist/gateway/static/root/assets/{setup-status-panel-L0JSG0K-.js → setup-status-panel-FpF_cPPr.js} +1 -1
  66. package/dist/gateway/static/root/assets/{share-preview-page-nJ2FWAEP.js → share-preview-page-BOCcPBqQ.js} +1 -1
  67. package/dist/gateway/static/root/assets/{shares-settings-BbgAUM3v.js → shares-settings-CInxjxgJ.js} +1 -1
  68. package/dist/gateway/static/root/assets/{skill-api-BFU1tPyw.js → skill-api-Bjd8WAA4.js} +1 -1
  69. package/dist/gateway/static/root/assets/{skills-page-DZw6el4o.js → skills-page-8CKS3Oo2.js} +1 -1
  70. package/dist/gateway/static/root/assets/{system-settings-panel-DshSGWNg.js → system-settings-panel-BgQdsrNw.js} +1 -1
  71. package/dist/gateway/static/root/assets/task-detail-page-BnG5ehZ6.js +2 -0
  72. package/dist/gateway/static/root/assets/task-expandable-content-BCuc70Vc.js +1 -0
  73. package/dist/gateway/static/root/assets/{tasks-DJIeIk9C.js → tasks-PuIUgF9L.js} +1 -1
  74. package/dist/gateway/static/root/assets/{tunnel-api-DFDXwWnU.js → tunnel-api-CY7MoDGf.js} +1 -1
  75. package/dist/gateway/static/root/assets/{usage-page-2RJLg54s.js → usage-page-s8Sv_PIG.js} +1 -1
  76. package/dist/gateway/static/root/assets/{use-composer-attachments-BFP3k6oC.js → use-composer-attachments-D0S2iZF4.js} +1 -1
  77. package/dist/gateway/static/root/assets/{use-file-preview-expanded-B1BiTQNS.js → use-file-preview-expanded-BElqELib.js} +1 -1
  78. package/dist/gateway/static/root/assets/{user-model-page-y1gjGYPI.js → user-model-page-CyWrEtHY.js} +1 -1
  79. package/dist/gateway/static/root/assets/{work-discovery-overlay-Ch-KfpCF.js → work-discovery-overlay-DmZXLs1v.js} +1 -1
  80. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-CTu297Hc.js → workflow-run-setup-panel-BdLOzHm4.js} +1 -1
  81. package/dist/gateway/static/root/assets/{workflows-page-CRZXG_rm.js → workflows-page-Cx3I3AV5.js} +1 -1
  82. package/dist/gateway/static/root/assets/{workspace-api-Xr3wz8T3.js → workspace-api-Du7-xJ0q.js} +2 -2
  83. package/dist/gateway/static/root/assets/{xopc-cloud-account-card-hEfjVjnc.js → xopc-cloud-account-card-D8B136vW.js} +1 -1
  84. package/dist/gateway/static/root/index.html +11 -11
  85. package/dist/package.js +1 -1
  86. package/dist/packages/gateway-contract/src/index.js +2 -0
  87. package/dist/packages/gateway-contract/src/task-research-result.js +89 -0
  88. package/dist/packages/gateway-contract/src/tasks.js +7 -1
  89. package/dist/packages/gateway-contract/src/turn-outcome.js +3 -0
  90. package/dist/src/agent/child-agent-factory.d.ts +4 -1
  91. package/dist/src/agent/child-agent-factory.js +86 -12
  92. package/dist/src/agent/context/connection-objective-context.d.ts +1 -0
  93. package/dist/src/agent/context/connection-objective-context.js +33 -0
  94. package/dist/src/agent/data-acquisition/service.js +41 -6
  95. package/dist/src/agent/data-acquisition/types.d.ts +2 -0
  96. package/dist/src/agent/embedded/agent-profile-context.d.ts +7 -0
  97. package/dist/src/agent/embedded/agent-profile-context.js +92 -0
  98. package/dist/src/agent/embedded/run-for-session.js +5 -2
  99. package/dist/src/agent/embedded/run-turn.js +19 -6
  100. package/dist/src/agent/embedded/runs.d.ts +9 -2
  101. package/dist/src/agent/embedded/runs.js +30 -7
  102. package/dist/src/agent/embedded/session-runner.js +5 -2
  103. package/dist/src/agent/embedded/types.d.ts +4 -0
  104. package/dist/src/agent/external-tools/gateway-tools.js +7 -3
  105. package/dist/src/agent/orchestration/agent-run-supervisor.js +2 -1
  106. package/dist/src/agent/orchestration/agent-turn-policy.js +5 -1
  107. package/dist/src/agent/orchestration/delegation-budget.d.ts +7 -1
  108. package/dist/src/agent/orchestration/delegation-budget.js +17 -9
  109. package/dist/src/agent/orchestration/research-checkpoint.d.ts +56 -0
  110. package/dist/src/agent/orchestration/research-checkpoint.js +64 -0
  111. package/dist/src/agent/orchestration/run-agent-turn-with-timeout.d.ts +3 -1
  112. package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +15 -2
  113. package/dist/src/agent/orchestration/run-termination.d.ts +12 -0
  114. package/dist/src/agent/orchestration/run-termination.js +24 -0
  115. package/dist/src/agent/orchestration/turn-recovery.d.ts +4 -1
  116. package/dist/src/agent/orchestration/turn-recovery.js +19 -3
  117. package/dist/src/agent/prompt/sections/agent-identity.js +12 -7
  118. package/dist/src/agent/prompt/sections/messaging-runtime.js +1 -1
  119. package/dist/src/agent/prompt/sections/project-context.js +1 -1
  120. package/dist/src/agent/prompt/sections/response-language.d.ts +1 -1
  121. package/dist/src/agent/prompt/sections/response-language.js +6 -2
  122. package/dist/src/agent/prompt/subagent-context.js +1 -1
  123. package/dist/src/agent/prompt/system-prompt.js +4 -6
  124. package/dist/src/agent/tasks/task-judge-service.js +3 -1
  125. package/dist/src/agent/tools/apply-patch.js +1 -1
  126. package/dist/src/agent/tools/delegate-tool.js +216 -121
  127. package/dist/src/agent/tools/delegation-policy.js +13 -1
  128. package/dist/src/agent/tools/factory.js +2 -0
  129. package/dist/src/agent/tools/index.d.ts +1 -0
  130. package/dist/src/agent/tools/index.js +2 -1
  131. package/dist/src/agent/tools/language-diagnostics.js +1 -1
  132. package/dist/src/agent/tools/personal-request-tool.js +8 -13
  133. package/dist/src/agent/tools/personal-task-tool.d.ts +1 -2
  134. package/dist/src/agent/tools/personal-task-tool.js +9 -8
  135. package/dist/src/agent/tools/publish-artifacts.js +1 -1
  136. package/dist/src/agent/tools/research-checkpoint-tool.d.ts +3 -0
  137. package/dist/src/agent/tools/research-checkpoint-tool.js +28 -0
  138. package/dist/src/agent/tools/review-workspace.js +1 -1
  139. package/dist/src/agent/tools/search/cache.d.ts +20 -0
  140. package/dist/src/agent/tools/search/cache.js +19 -0
  141. package/dist/src/agent/tools/search/errors.d.ts +19 -0
  142. package/dist/src/agent/tools/search/errors.js +69 -0
  143. package/dist/src/agent/tools/search/providers/bing-api.js +2 -1
  144. package/dist/src/agent/tools/search/providers/bing-html.js +2 -1
  145. package/dist/src/agent/tools/search/providers/brave.js +2 -1
  146. package/dist/src/agent/tools/search/providers/duckduckgo-html.js +2 -1
  147. package/dist/src/agent/tools/search/providers/searxng.js +2 -1
  148. package/dist/src/agent/tools/search/providers/tavily.js +2 -1
  149. package/dist/src/agent/tools/search/providers/xopc-cloud.d.ts +1 -0
  150. package/dist/src/agent/tools/search/providers/xopc-cloud.js +7 -2
  151. package/dist/src/agent/tools/search/registry.d.ts +21 -5
  152. package/dist/src/agent/tools/search/registry.js +252 -25
  153. package/dist/src/agent/tools/search/types.d.ts +25 -0
  154. package/dist/src/agent/tools/task-result-tool.d.ts +2 -0
  155. package/dist/src/agent/tools/task-result-tool.js +73 -0
  156. package/dist/src/agent/tools/web.d.ts +16 -2
  157. package/dist/src/agent/tools/web.js +74 -29
  158. package/dist/src/agent/tools/xopc-use-tool.js +2 -2
  159. package/dist/src/automations/capabilities/write.d.ts +4 -4
  160. package/dist/src/automations/domain/validation.d.ts +8 -8
  161. package/dist/src/automations/draft/automation-draft-validator.d.ts +2 -2
  162. package/dist/src/browser/automations/schema.d.ts +2 -2
  163. package/dist/src/browser/automations/service.d.ts +2 -2
  164. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  165. package/dist/src/config/index.js +2 -2
  166. package/dist/src/config/schema.d.ts +1 -0
  167. package/dist/src/config/schema.js +4 -1
  168. package/dist/src/connectors/connection-recovery-service.d.ts +3 -1
  169. package/dist/src/connectors/connection-recovery-service.js +66 -5
  170. package/dist/src/discussions/action-tasks.d.ts +2 -2
  171. package/dist/src/gateway/agent-builtin-tools.d.ts +1 -1
  172. package/dist/src/gateway/hono/routes/projects.js +1 -1
  173. package/dist/src/gateway/hono/routes/session-input-handler.d.ts +16 -0
  174. package/dist/src/gateway/hono/routes/tasks.js +1 -1
  175. package/dist/src/gateway/service/agent-runner.d.ts +2 -0
  176. package/dist/src/gateway/service/agent-runner.js +30 -17
  177. package/dist/src/gateway/service/run-gateway-agent.js +12 -1
  178. package/dist/src/gateway/service/session-input-coordinator.d.ts +10 -1
  179. package/dist/src/gateway/service/session-input-coordinator.js +213 -26
  180. package/dist/src/gateway/service.d.ts +1 -0
  181. package/dist/src/gateway/service.js +9 -7
  182. package/dist/src/gateway/session-context-summary.js +1 -1
  183. package/dist/src/personal-agent/prompts.js +4 -1
  184. package/dist/src/personal-agent/reply-composer.d.ts +1 -0
  185. package/dist/src/personal-agent/reply-composer.js +70 -12
  186. package/dist/src/personal-agent/reply-style.d.ts +1 -1
  187. package/dist/src/personal-agent/reply-style.js +1 -0
  188. package/dist/src/personal-agent/repository.d.ts +1 -1
  189. package/dist/src/personal-agent/repository.js +1 -1
  190. package/dist/src/personal-agent/request-repository.d.ts +2 -0
  191. package/dist/src/personal-agent/request-service.d.ts +4 -0
  192. package/dist/src/personal-agent/request-service.js +90 -19
  193. package/dist/src/personal-agent/result-file-links.d.ts +3 -0
  194. package/dist/src/personal-agent/result-file-links.js +21 -0
  195. package/dist/src/personal-agent/task-cancel.js +85 -35
  196. package/dist/src/scenes/taskFollowUp/service.d.ts +15 -5
  197. package/dist/src/session/session-context-for-llm.js +2 -2
  198. package/dist/src/session/store.d.ts +3 -1
  199. package/dist/src/session/store.js +13 -3
  200. package/dist/src/storage/sqlite/connection-wait-repository.d.ts +5 -2
  201. package/dist/src/storage/sqlite/connection-wait-repository.js +42 -17
  202. package/dist/src/storage/sqlite/index.d.ts +1 -0
  203. package/dist/src/storage/sqlite/index.js +3 -1
  204. package/dist/src/storage/sqlite/migrations/250_research_checkpoints.sql +15 -0
  205. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  206. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  207. package/dist/src/storage/sqlite/research-checkpoint-repository.d.ts +24 -0
  208. package/dist/src/storage/sqlite/research-checkpoint-repository.js +91 -0
  209. package/dist/src/storage/sqlite/schema.js +1 -1
  210. package/dist/src/storage/sqlite/session-creation-repository.js +3 -2
  211. package/dist/src/storage/sqlite/session-input-repository.d.ts +16 -0
  212. package/dist/src/storage/sqlite/session-input-repository.js +41 -7
  213. package/dist/src/tasks/capabilities/management.d.ts +2 -2
  214. package/dist/src/tasks/capabilities/relations.d.ts +7 -7
  215. package/dist/src/tasks/capabilities/write.d.ts +2 -2
  216. package/dist/src/tasks/index.js +1 -1
  217. package/dist/src/tasks/task-allocation-service.d.ts +2 -0
  218. package/dist/src/tasks/task-allocation-service.js +43 -2
  219. package/dist/src/tasks/task-application-service.js +2 -2
  220. package/dist/src/tasks/task-context-assembler.js +6 -1
  221. package/dist/src/tasks/task-continuation.d.ts +1 -0
  222. package/dist/src/tasks/task-continuation.js +1 -0
  223. package/dist/src/tasks/task-contract-planner.d.ts +1 -0
  224. package/dist/src/tasks/task-contract-planner.js +22 -15
  225. package/dist/src/tasks/task-model-call.d.ts +7 -0
  226. package/dist/src/tasks/task-model-call.js +35 -0
  227. package/dist/src/tasks/task-research-repair.d.ts +19 -0
  228. package/dist/src/tasks/task-research-repair.js +75 -0
  229. package/dist/src/tasks/task-research-result.d.ts +85 -0
  230. package/dist/src/tasks/task-research-result.js +394 -0
  231. package/dist/src/tasks/task-result-delivery-repository.js +13 -2
  232. package/dist/src/tasks/task-result-delivery-service.js +1 -1
  233. package/dist/src/tasks/task-run-coordinator.js +69 -15
  234. package/dist/src/tasks/task-run-dispatcher.js +1 -0
  235. package/dist/src/tasks/task-run-repository.d.ts +5 -0
  236. package/dist/src/tasks/task-run-repository.js +88 -1
  237. package/package.json +9 -5
  238. package/dist/gateway/static/root/assets/constants-Dy8Gw0sF.js +0 -1
  239. package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-DBRj4-1-.js +0 -1
  240. package/dist/gateway/static/root/assets/session-manager-_yy4bxhT.js +0 -16
  241. package/dist/gateway/static/root/assets/settings-advanced-gate-DoAHy1wj.js +0 -1
  242. package/dist/gateway/static/root/assets/task-detail-page-CfZhq4XZ.js +0 -2
  243. package/dist/gateway/static/root/assets/task-expandable-content-CF6Bj-Qa.js +0 -1
@@ -0,0 +1,28 @@
1
+ import { ResearchCheckpointUpdateSchema, init_research_checkpoint } from "../orchestration/research-checkpoint.js";
2
+ //#region src/agent/tools/research-checkpoint-tool.ts
3
+ init_research_checkpoint();
4
+ function createResearchCheckpointTool(sink) {
5
+ return {
6
+ name: "research_checkpoint",
7
+ label: "Save research checkpoint",
8
+ description: "Persist findings immediately after verifying each source. Use stable finding ids to update fields; omitted findings are retained. Include public source URLs for confirmed claims. not_disclosed is a valid result; never invent missing prices or limits. nextSteps replaces the unfinished-work list; set it to [] only when all requested fields are addressed, including explicitly undisclosed fields. This writes only this delegated research checkpoint.",
9
+ parameters: ResearchCheckpointUpdateSchema,
10
+ async execute(_id, update, signal) {
11
+ signal?.throwIfAborted();
12
+ const checkpoint = sink.save(update);
13
+ return {
14
+ content: [{
15
+ type: "text",
16
+ text: JSON.stringify({
17
+ saved: true,
18
+ findingCount: checkpoint.findings.length,
19
+ nextSteps: checkpoint.nextSteps
20
+ })
21
+ }],
22
+ details: {}
23
+ };
24
+ }
25
+ };
26
+ }
27
+ //#endregion
28
+ export { createResearchCheckpointTool };
@@ -5,8 +5,8 @@ import { checkedFilePath, readWorkspaceFile } from "../sandbox/fileAccess.js";
5
5
  import { resolve } from "node:path";
6
6
  import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
- import { lstat, readlink, realpath } from "node:fs/promises";
9
8
  import { Type } from "@sinclair/typebox";
9
+ import { lstat, readlink, realpath } from "node:fs/promises";
10
10
  //#region src/agent/tools/review-workspace.ts
11
11
  const exec = promisify(execFile);
12
12
  function createReviewWorkspaceTool(workspace) {
@@ -0,0 +1,20 @@
1
+ import type { SearchResult } from './types.js';
2
+ export declare const SEARCH_CACHE_TTL_MS: number;
3
+ export interface SearchCacheEntry {
4
+ key: string;
5
+ query: string;
6
+ count: number;
7
+ results: SearchResult[];
8
+ provider: string;
9
+ capturedAt: number;
10
+ }
11
+ export interface SearchCache {
12
+ get(key: string): SearchCacheEntry | undefined;
13
+ set(entry: SearchCacheEntry): void;
14
+ }
15
+ /** Research owns this scope. No account-wide or cross-conversation cache exists. */
16
+ export declare function withSearchCache<T>(cache: SearchCache, run: () => T): T;
17
+ export declare function currentSearchCache(): SearchCache | undefined;
18
+ export declare class SearchCacheWriteError extends Error {
19
+ constructor(cause: unknown);
20
+ }
@@ -0,0 +1,19 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/agent/tools/search/cache.ts
3
+ const SEARCH_CACHE_TTL_MS = 10 * 6e4;
4
+ const searchCache = new AsyncLocalStorage();
5
+ /** Research owns this scope. No account-wide or cross-conversation cache exists. */
6
+ function withSearchCache(cache, run) {
7
+ return searchCache.run(cache, run);
8
+ }
9
+ function currentSearchCache() {
10
+ return searchCache.getStore();
11
+ }
12
+ var SearchCacheWriteError = class extends Error {
13
+ constructor(cause) {
14
+ super("Search completed but its checkpoint could not be saved", { cause });
15
+ this.name = "SearchCacheWriteError";
16
+ }
17
+ };
18
+ //#endregion
19
+ export { SEARCH_CACHE_TTL_MS, SearchCacheWriteError, currentSearchCache, withSearchCache };
@@ -0,0 +1,19 @@
1
+ export type SearchFailureKind = 'permission_denied' | 'rate_limited' | 'timeout' | 'cancelled' | 'unsupported_batch' | 'provider_error';
2
+ export interface SearchFailureInfo {
3
+ kind: SearchFailureKind;
4
+ scope: 'request' | 'batch' | 'provider';
5
+ message: string;
6
+ status?: number;
7
+ retryAfterMs?: number;
8
+ }
9
+ /** Only a host adapter may explicitly identify a refusal as batch-only. */
10
+ export declare class SearchError extends Error implements SearchFailureInfo {
11
+ readonly kind: SearchFailureKind;
12
+ readonly scope: 'request' | 'batch' | 'provider';
13
+ readonly status?: number;
14
+ readonly retryAfterMs?: number;
15
+ constructor(info: SearchFailureInfo);
16
+ }
17
+ export declare function classifySearchError(error: unknown, signal?: AbortSignal): SearchError;
18
+ export declare function searchFailureInfo(error: SearchError): SearchFailureInfo;
19
+ export declare function searchHttpError(response: Response, message?: string): SearchError;
@@ -0,0 +1,69 @@
1
+ //#region src/agent/tools/search/errors.ts
2
+ /** Only a host adapter may explicitly identify a refusal as batch-only. */
3
+ var SearchError = class extends Error {
4
+ kind;
5
+ scope;
6
+ status;
7
+ retryAfterMs;
8
+ constructor(info) {
9
+ super(info.message);
10
+ this.name = "SearchError";
11
+ this.kind = info.kind;
12
+ this.scope = info.scope;
13
+ this.status = info.status;
14
+ this.retryAfterMs = info.retryAfterMs;
15
+ }
16
+ };
17
+ function classifySearchError(error, signal) {
18
+ if (signal?.aborted) {
19
+ const reason = signal.reason;
20
+ const timedOut = reason?.name === "TimeoutError" || reason?.code === "deadline_exceeded";
21
+ return new SearchError({
22
+ kind: timedOut ? "timeout" : "cancelled",
23
+ scope: "request",
24
+ message: timedOut ? "Search caller deadline exceeded" : "Search cancelled by its caller"
25
+ });
26
+ }
27
+ if (error instanceof SearchError) return error;
28
+ const value = error;
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ const legacyStatus = /^HTTP (\d{3})\b/.exec(message)?.[1];
31
+ const status = typeof value?.status === "number" ? value.status : typeof value?.statusCode === "number" ? value.statusCode : legacyStatus ? Number(legacyStatus) : void 0;
32
+ const denied = [
33
+ "EACCES",
34
+ "EPERM",
35
+ "PERMISSION_DENIED",
36
+ "permission_denied",
37
+ "ACCESS_DENIED"
38
+ ].includes(String(value?.code)) || /\b(permission denied|access denied|forbidden|unauthorized|not authorized)\b/i.test(message);
39
+ const kind = status === 401 || status === 403 || denied ? "permission_denied" : status === 429 ? "rate_limited" : status === 408 || status === 504 || value?.name === "TimeoutError" || value?.code === "ETIMEDOUT" ? "timeout" : value?.name === "AbortError" ? "cancelled" : "provider_error";
40
+ return new SearchError({
41
+ kind,
42
+ scope: kind === "permission_denied" ? "provider" : "request",
43
+ status,
44
+ message: message.slice(0, 500)
45
+ });
46
+ }
47
+ function searchFailureInfo(error) {
48
+ return {
49
+ kind: error.kind,
50
+ scope: error.scope,
51
+ message: error.message.slice(0, 500),
52
+ status: error.status,
53
+ retryAfterMs: error.retryAfterMs
54
+ };
55
+ }
56
+ function searchHttpError(response, message = `Search request failed (${response.status})`) {
57
+ const retryAfter = response.headers.get("retry-after");
58
+ const seconds = retryAfter === null ? NaN : Number(retryAfter);
59
+ const retryAfterMs = retryAfter === null ? void 0 : Number.isFinite(seconds) ? Math.max(0, seconds * 1e3) : Math.max(0, Date.parse(retryAfter) - Date.now());
60
+ return new SearchError({
61
+ kind: response.status === 401 || response.status === 403 ? "permission_denied" : response.status === 429 ? "rate_limited" : response.status === 408 || response.status === 504 ? "timeout" : "provider_error",
62
+ scope: response.status === 401 || response.status === 403 ? "provider" : "request",
63
+ message: message.slice(0, 500),
64
+ status: response.status,
65
+ retryAfterMs: retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) ? retryAfterMs : void 0
66
+ });
67
+ }
68
+ //#endregion
69
+ export { SearchError, classifySearchError, searchFailureInfo, searchHttpError };
@@ -1,3 +1,4 @@
1
+ import { searchHttpError } from "../errors.js";
1
2
  //#region src/agent/tools/search/providers/bing-api.ts
2
3
  /** Azure Bing Web Search API (global endpoint; works without a proxy from many regions). */
3
4
  var BingApiProvider = class {
@@ -17,7 +18,7 @@ var BingApiProvider = class {
17
18
  headers: { "Ocp-Apim-Subscription-Key": this.apiKey },
18
19
  signal
19
20
  });
20
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
21
+ if (!response.ok) throw searchHttpError(response);
21
22
  return ((await response.json()).webPages?.value ?? []).map((r) => ({
22
23
  title: r.name ?? "",
23
24
  url: r.url ?? "",
@@ -1,3 +1,4 @@
1
+ import { searchHttpError } from "../errors.js";
1
2
  import { DEFAULT_HTML_SEARCH_TIMEOUT_MS, HTML_SEARCH_USER_AGENT, mergeAbortWithTimeout } from "../http.js";
2
3
  //#region src/agent/tools/search/providers/bing-html.ts
3
4
  const BING_CN_SEARCH = "https://cn.bing.com/search";
@@ -22,7 +23,7 @@ var BingHtmlProvider = class {
22
23
  "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
23
24
  }
24
25
  });
25
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
26
+ if (!response.ok) throw searchHttpError(response);
26
27
  return parseBingCnHtml(await response.text(), count);
27
28
  }
28
29
  };
@@ -1,3 +1,4 @@
1
+ import { searchHttpError } from "../errors.js";
1
2
  //#region src/agent/tools/search/providers/brave.ts
2
3
  var BraveProvider = class {
3
4
  name = "brave";
@@ -17,7 +18,7 @@ var BraveProvider = class {
17
18
  },
18
19
  signal
19
20
  });
20
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
21
+ if (!response.ok) throw searchHttpError(response);
21
22
  return ((await response.json()).web?.results ?? []).map((r) => ({
22
23
  title: r.title ?? "",
23
24
  url: r.url ?? "",
@@ -1,3 +1,4 @@
1
+ import { searchHttpError } from "../errors.js";
1
2
  import { DEFAULT_HTML_SEARCH_TIMEOUT_MS, HTML_SEARCH_USER_AGENT, mergeAbortWithTimeout } from "../http.js";
2
3
  //#region src/agent/tools/search/providers/duckduckgo-html.ts
3
4
  const DDG_HTML = "https://html.duckduckgo.com/html/";
@@ -23,7 +24,7 @@ var DuckDuckGoHtmlProvider = class {
23
24
  },
24
25
  body: body.toString()
25
26
  });
26
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
27
+ if (!response.ok) throw searchHttpError(response);
27
28
  return parseDdgHtml(await response.text(), count);
28
29
  }
29
30
  };
@@ -1,3 +1,4 @@
1
+ import { searchHttpError } from "../errors.js";
1
2
  //#region src/agent/tools/search/providers/searxng.ts
2
3
  var SearXNGProvider = class {
3
4
  name = "searxng";
@@ -14,7 +15,7 @@ var SearXNGProvider = class {
14
15
  url.searchParams.set("format", "json");
15
16
  url.searchParams.set("categories", "general");
16
17
  const response = await fetch(url, { signal });
17
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
18
+ if (!response.ok) throw searchHttpError(response);
18
19
  return ((await response.json()).results ?? []).slice(0, count).map((r) => ({
19
20
  title: r.title ?? "",
20
21
  url: r.url ?? "",
@@ -1,3 +1,4 @@
1
+ import { searchHttpError } from "../errors.js";
1
2
  //#region src/agent/tools/search/providers/tavily.ts
2
3
  var TavilyProvider = class {
3
4
  name = "tavily";
@@ -20,7 +21,7 @@ var TavilyProvider = class {
20
21
  }),
21
22
  signal
22
23
  });
23
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
24
+ if (!response.ok) throw searchHttpError(response);
24
25
  return ((await response.json()).results ?? []).map((r) => ({
25
26
  title: r.title ?? "",
26
27
  url: r.url ?? "",
@@ -14,6 +14,7 @@ export declare class XopcCloudSearchProvider implements SearchProvider {
14
14
  private readonly catalogStore;
15
15
  private readonly credentials;
16
16
  constructor(options: XopcCloudSearchProviderOptions);
17
+ cacheIdentity(): string;
17
18
  isAvailable(): boolean;
18
19
  search(query: string, count: number, signal?: AbortSignal): Promise<SearchResult[]>;
19
20
  }
@@ -1,5 +1,6 @@
1
1
  import { getModelCatalogStore, init_model_catalog_store } from "../../../../providers/model-catalog-store.js";
2
2
  import { getProviderAuthService, init_provider_auth_service } from "../../../../providers/provider-auth-service.js";
3
+ import { searchHttpError } from "../errors.js";
3
4
  //#region src/agent/tools/search/providers/xopc-cloud.ts
4
5
  init_model_catalog_store();
5
6
  init_provider_auth_service();
@@ -14,6 +15,10 @@ var XopcCloudSearchProvider = class {
14
15
  this.catalogStore = options.catalogStore ?? getModelCatalogStore();
15
16
  this.credentials = options.credentials ?? getProviderAuthService();
16
17
  }
18
+ cacheIdentity() {
19
+ const source = this.catalogStore.getSource("xopc-cloud");
20
+ return JSON.stringify([source?.baseUrl, source?.search]);
21
+ }
17
22
  isAvailable() {
18
23
  return Boolean(this.catalogStore.getSource("xopc-cloud")?.search);
19
24
  }
@@ -22,7 +27,7 @@ var XopcCloudSearchProvider = class {
22
27
  if (!source?.search) throw new Error("XOPC Cloud search is not available");
23
28
  const requestSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4);
24
29
  const accessToken = await this.credentials.resolveApiKey("xopc-cloud", requestSignal);
25
- if (!accessToken) throw new Error("XOPC Cloud is not connected");
30
+ if (!accessToken) throw searchHttpError(new Response(null, { status: 401 }), "XOPC Cloud is not connected");
26
31
  const endpoint = `${source.baseUrl.replace(/\/+$/, "")}/${source.search.endpoint.replace(/^\/+/, "")}`;
27
32
  const response = await this.fetchImpl(endpoint, {
28
33
  method: "POST",
@@ -39,7 +44,7 @@ var XopcCloudSearchProvider = class {
39
44
  signal: requestSignal
40
45
  });
41
46
  const body = await response.json().catch(() => null);
42
- if (!response.ok) throw new Error(typeof body?.error?.message === "string" ? body.error.message : `XOPC Cloud search failed (${response.status})`);
47
+ if (!response.ok) throw searchHttpError(response, typeof body?.error?.message === "string" ? body.error.message : `XOPC Cloud search failed (${response.status})`);
43
48
  if (!Array.isArray(body?.results)) throw new Error("XOPC Cloud search returned an invalid response");
44
49
  return body.results.flatMap((result) => {
45
50
  if (typeof result.title !== "string" || typeof result.url !== "string" || typeof result.description !== "string") return [];
@@ -1,14 +1,30 @@
1
- import type { ResolvedWebSearchConfig, SearchProvider, SearchResult } from './types.js';
1
+ import { type SearchCache } from './cache.js';
2
+ import type { ResolvedWebSearchConfig, SearchProvider, SearchResponse, SearchRequest, SearchBatchResponse } from './types.js';
2
3
  export declare class SearchProviderRegistry {
3
4
  private readonly providers;
4
5
  private readonly fallbackProvider;
6
+ private readonly cache;
7
+ private readonly cachePrefix;
8
+ private readonly requestTimeoutMs;
9
+ private readonly retryDelayMs;
10
+ sourceRequests: number;
11
+ private readonly cooldowns;
5
12
  constructor(config: ResolvedWebSearchConfig, options?: {
6
13
  cloudProvider?: SearchProvider;
14
+ providers?: SearchProvider[];
15
+ fallbackProvider?: SearchProvider | null;
16
+ cache?: SearchCache;
17
+ requestTimeoutMs?: number;
18
+ retryDelayMs?: number;
7
19
  });
8
20
  private buildProviders;
9
21
  hasConfiguredApiProvider(): boolean;
10
- search(query: string, count: number, signal?: AbortSignal): Promise<{
11
- results: SearchResult[];
12
- provider: string;
13
- }>;
22
+ private key;
23
+ private cached;
24
+ private remember;
25
+ private attempt;
26
+ private backoff;
27
+ search(query: string, count: number, signal?: AbortSignal, authorize?: () => void): Promise<SearchResponse>;
28
+ /** Only unfinished, non-denied items degrade to sequential authorized searches. */
29
+ searchBatch(requests: SearchRequest[], signal?: AbortSignal, authorize?: () => void): Promise<SearchBatchResponse[]>;
14
30
  }
@@ -1,5 +1,8 @@
1
1
  import { createLogger } from "../../../utils/logger/index.js";
2
2
  import { init_logger } from "../../../utils/logger.js";
3
+ import { executeWithTimeout } from "../../lifecycle/timeout-wrapper.js";
4
+ import { SearchError, classifySearchError, searchFailureInfo } from "./errors.js";
5
+ import { SearchCacheWriteError, currentSearchCache } from "./cache.js";
3
6
  import { BraveProvider } from "./providers/brave.js";
4
7
  import { TavilyProvider } from "./providers/tavily.js";
5
8
  import { BingApiProvider } from "./providers/bing-api.js";
@@ -7,15 +10,40 @@ import { BingHtmlProvider } from "./providers/bing-html.js";
7
10
  import { DuckDuckGoHtmlProvider } from "./providers/duckduckgo-html.js";
8
11
  import { SearXNGProvider } from "./providers/searxng.js";
9
12
  import { XopcCloudSearchProvider } from "./providers/xopc-cloud.js";
13
+ import { createHash } from "node:crypto";
14
+ import { setTimeout } from "node:timers/promises";
10
15
  //#region src/agent/tools/search/registry.ts
11
16
  init_logger();
12
17
  const log = createLogger("Agent:WebSearch");
13
18
  var SearchProviderRegistry = class {
14
19
  providers;
15
20
  fallbackProvider;
21
+ cache;
22
+ cachePrefix;
23
+ requestTimeoutMs;
24
+ retryDelayMs;
25
+ sourceRequests = 0;
26
+ cooldowns = /* @__PURE__ */ new Map();
16
27
  constructor(config, options = {}) {
17
- this.providers = this.buildProviders(config, options.cloudProvider);
18
- this.fallbackProvider = config.region === "cn" ? new BingHtmlProvider() : new DuckDuckGoHtmlProvider();
28
+ this.providers = options.providers ?? this.buildProviders(config, options.cloudProvider);
29
+ this.fallbackProvider = options.fallbackProvider === void 0 ? config.region === "cn" ? new BingHtmlProvider() : new DuckDuckGoHtmlProvider() : options.fallbackProvider;
30
+ const cache = /* @__PURE__ */ new Map();
31
+ this.cache = options.cache ?? currentSearchCache() ?? {
32
+ get: (key) => cache.get(key),
33
+ set: (entry) => {
34
+ if (JSON.stringify(entry).length > 2e5) return;
35
+ cache.delete(entry.key);
36
+ cache.set(entry.key, entry);
37
+ while (cache.size > 20) cache.delete(cache.keys().next().value);
38
+ }
39
+ };
40
+ this.cachePrefix = createHash("sha256").update(JSON.stringify([
41
+ config,
42
+ this.providers.map((provider) => [provider.name, provider.cacheIdentity?.()]),
43
+ this.fallbackProvider?.name
44
+ ])).digest("hex");
45
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
46
+ this.retryDelayMs = options.retryDelayMs ?? 300;
19
47
  log.debug({
20
48
  region: config.region,
21
49
  configuredCount: this.providers.filter((p) => p.isAvailable()).length
@@ -50,33 +78,232 @@ var SearchProviderRegistry = class {
50
78
  hasConfiguredApiProvider() {
51
79
  return this.providers.some((p) => p.isAvailable());
52
80
  }
53
- async search(query, count, signal) {
54
- for (const provider of this.providers) {
81
+ key(query, count) {
82
+ return createHash("sha256").update(JSON.stringify([
83
+ this.cachePrefix,
84
+ query.trim(),
85
+ count
86
+ ])).digest("hex");
87
+ }
88
+ cached(query, count) {
89
+ const cached = this.cache.get(this.key(query, count));
90
+ return cached && Date.now() >= cached.capturedAt && Date.now() - cached.capturedAt < 6e5 ? {
91
+ results: structuredClone(cached.results),
92
+ provider: cached.provider,
93
+ cached: true,
94
+ capturedAt: cached.capturedAt
95
+ } : void 0;
96
+ }
97
+ remember(query, count, response) {
98
+ try {
99
+ this.cache.set({
100
+ key: this.key(query, count),
101
+ query: query.trim(),
102
+ count,
103
+ results: structuredClone(response.results),
104
+ provider: response.provider,
105
+ capturedAt: response.capturedAt ?? Date.now()
106
+ });
107
+ } catch (error) {
108
+ throw new SearchCacheWriteError(error);
109
+ }
110
+ }
111
+ async attempt(name, signal, execute) {
112
+ signal?.throwIfAborted();
113
+ this.sourceRequests++;
114
+ const controller = new AbortController();
115
+ const abort = () => controller.abort(signal?.reason);
116
+ signal?.addEventListener("abort", abort, { once: true });
117
+ try {
118
+ return await executeWithTimeout(() => execute(controller.signal), {
119
+ toolName: `web_search:${name}`,
120
+ timeoutMs: this.requestTimeoutMs,
121
+ signal,
122
+ onTimeout: (error) => controller.abort(error)
123
+ });
124
+ } catch (error) {
125
+ throw classifySearchError(error, signal);
126
+ } finally {
127
+ signal?.removeEventListener("abort", abort);
128
+ }
129
+ }
130
+ async backoff(error, signal) {
131
+ const waitMs = error.retryAfterMs ?? this.retryDelayMs;
132
+ if (waitMs > 2e3) return false;
133
+ await setTimeout(Math.max(0, waitMs), void 0, { signal });
134
+ return true;
135
+ }
136
+ async search(query, count, signal, authorize = () => {}) {
137
+ signal?.throwIfAborted();
138
+ authorize();
139
+ const hit = this.cached(query, count);
140
+ if (hit) return hit;
141
+ let lastError;
142
+ for (const provider of [...this.providers, ...this.fallbackProvider ? [this.fallbackProvider] : []]) {
143
+ signal?.throwIfAborted();
144
+ authorize();
55
145
  if (!provider.isAvailable()) continue;
56
- try {
57
- return {
58
- results: await provider.search(query, count, signal),
59
- provider: provider.name
60
- };
61
- } catch (err) {
62
- log.debug({
63
- provider: provider.name,
64
- err
65
- }, "Search provider failed, trying next");
146
+ const cooldown = this.cooldowns.get(provider.name);
147
+ if (cooldown && cooldown.until > Date.now()) {
148
+ lastError = cooldown.error;
149
+ continue;
150
+ }
151
+ for (let attempt = 0; attempt < 2; attempt++) {
152
+ signal?.throwIfAborted();
153
+ authorize();
154
+ try {
155
+ const results = await this.attempt(provider.name, signal, (requestSignal) => provider.search(query, count, requestSignal));
156
+ signal?.throwIfAborted();
157
+ authorize();
158
+ const response = {
159
+ results,
160
+ provider: provider.name,
161
+ capturedAt: Date.now()
162
+ };
163
+ this.remember(query, count, response);
164
+ return response;
165
+ } catch (error) {
166
+ if (error instanceof SearchCacheWriteError) throw error;
167
+ const failure = classifySearchError(error, signal);
168
+ if (signal?.aborted || failure.kind === "permission_denied" || failure.kind === "cancelled") throw failure;
169
+ lastError = failure;
170
+ if (failure.kind === "rate_limited") this.cooldowns.set(provider.name, {
171
+ until: Date.now() + (failure.retryAfterMs ?? this.retryDelayMs),
172
+ error: failure
173
+ });
174
+ log.debug({
175
+ provider: provider.name,
176
+ kind: failure.kind,
177
+ attempt
178
+ }, "Search provider attempt failed");
179
+ if (attempt || !["rate_limited", "timeout"].includes(failure.kind) || !await this.backoff(failure, signal)) break;
180
+ }
66
181
  }
67
182
  }
68
- try {
69
- return {
70
- results: await this.fallbackProvider.search(query, count, signal),
71
- provider: this.fallbackProvider.name
72
- };
73
- } catch (err) {
74
- log.warn({ err }, "HTML fallback search failed");
75
- return {
76
- results: [],
77
- provider: "none"
78
- };
183
+ throw lastError ?? new SearchError({
184
+ kind: "provider_error",
185
+ scope: "request",
186
+ message: "No search provider is available"
187
+ });
188
+ }
189
+ /** Only unfinished, non-denied items degrade to sequential authorized searches. */
190
+ async searchBatch(requests, signal, authorize = () => {}) {
191
+ if (!requests.length || requests.length > 8 || new Set(requests.map((request) => request.id)).size !== requests.length) throw new Error("Invalid search batch");
192
+ signal?.throwIfAborted();
193
+ authorize();
194
+ const outputs = /* @__PURE__ */ new Map();
195
+ const unique = /* @__PURE__ */ new Map();
196
+ for (const request of requests) {
197
+ const hit = this.cached(request.query, request.count);
198
+ if (hit) outputs.set(this.key(request.query, request.count), {
199
+ ...request,
200
+ response: hit
201
+ });
202
+ else unique.set(this.key(request.query, request.count), request);
203
+ }
204
+ const pending = [...unique.values()];
205
+ const provider = this.providers.find((provider) => provider.isAvailable() && (this.cooldowns.get(provider.name)?.until ?? 0) <= Date.now());
206
+ const itemFailures = /* @__PURE__ */ new Map();
207
+ if (pending.length > 1 && provider?.searchBatch) try {
208
+ authorize();
209
+ const items = await this.attempt(provider.name, signal, (requestSignal) => provider.searchBatch(pending, requestSignal));
210
+ const ids = new Set(pending.map((request) => request.id));
211
+ if (items.some((item) => !ids.has(item.id)) || new Set(items.map((item) => item.id)).size !== items.length) throw new Error("Invalid batch search response ids");
212
+ signal?.throwIfAborted();
213
+ authorize();
214
+ let providerRefusal;
215
+ for (const request of pending) {
216
+ const item = items.find((item) => item.id === request.id);
217
+ if (item?.results && !item.error) {
218
+ const response = {
219
+ results: item.results,
220
+ provider: provider.name,
221
+ capturedAt: Date.now()
222
+ };
223
+ this.remember(request.query, request.count, response);
224
+ outputs.set(this.key(request.query, request.count), {
225
+ ...request,
226
+ response
227
+ });
228
+ } else if (item?.error) {
229
+ if (item.error.kind === "permission_denied" && item.error.scope === "provider") providerRefusal = new SearchError(item.error);
230
+ if (["permission_denied", "cancelled"].includes(item.error.kind) && item.error.scope !== "batch") outputs.set(this.key(request.query, request.count), {
231
+ ...request,
232
+ error: item.error
233
+ });
234
+ else {
235
+ const failure = new SearchError(item.error);
236
+ itemFailures.set(this.key(request.query, request.count), failure);
237
+ if (failure.kind === "rate_limited") this.cooldowns.set(provider.name, {
238
+ until: Date.now() + (failure.retryAfterMs ?? this.retryDelayMs),
239
+ error: failure
240
+ });
241
+ }
242
+ }
243
+ }
244
+ if (providerRefusal) for (const request of pending) {
245
+ const key = this.key(request.query, request.count);
246
+ if (!outputs.has(key)) outputs.set(key, {
247
+ ...request,
248
+ error: searchFailureInfo(providerRefusal)
249
+ });
250
+ }
251
+ } catch (error) {
252
+ if (error instanceof SearchCacheWriteError) throw error;
253
+ const batchFailure = classifySearchError(error, signal);
254
+ if (signal?.aborted || batchFailure.kind === "permission_denied" && batchFailure.scope !== "batch" || batchFailure.kind === "cancelled") for (const request of pending) outputs.set(this.key(request.query, request.count), {
255
+ ...request,
256
+ error: searchFailureInfo(batchFailure)
257
+ });
258
+ else if (batchFailure.kind === "rate_limited") {
259
+ this.cooldowns.set(provider.name, {
260
+ until: Date.now() + (batchFailure.retryAfterMs ?? this.retryDelayMs),
261
+ error: batchFailure
262
+ });
263
+ if (!await this.backoff(batchFailure, signal)) for (const request of pending) outputs.set(this.key(request.query, request.count), {
264
+ ...request,
265
+ error: searchFailureInfo(batchFailure)
266
+ });
267
+ }
268
+ }
269
+ for (const request of pending) {
270
+ const key = this.key(request.query, request.count);
271
+ if (outputs.has(key)) continue;
272
+ try {
273
+ const itemFailure = itemFailures.get(key);
274
+ if (itemFailure?.kind === "rate_limited" && !await this.backoff(itemFailure, signal)) {
275
+ outputs.set(key, {
276
+ ...request,
277
+ error: searchFailureInfo(itemFailure)
278
+ });
279
+ continue;
280
+ }
281
+ const response = await this.search(request.query, request.count, signal, authorize);
282
+ outputs.set(key, {
283
+ ...request,
284
+ response,
285
+ ...provider?.searchBatch ? { fallback: "individual" } : {}
286
+ });
287
+ } catch (error) {
288
+ const failure = classifySearchError(error, signal);
289
+ outputs.set(key, {
290
+ ...request,
291
+ error: searchFailureInfo(failure),
292
+ ...provider?.searchBatch ? { fallback: "individual" } : {}
293
+ });
294
+ if (failure.kind === "permission_denied" && failure.scope === "provider") for (const remaining of pending) {
295
+ const remainingKey = this.key(remaining.query, remaining.count);
296
+ if (!outputs.has(remainingKey)) outputs.set(remainingKey, {
297
+ ...remaining,
298
+ error: searchFailureInfo(failure)
299
+ });
300
+ }
301
+ }
79
302
  }
303
+ return requests.map((request) => ({
304
+ ...outputs.get(this.key(request.query, request.count)),
305
+ ...request
306
+ }));
80
307
  }
81
308
  };
82
309
  //#endregion