mixdog 0.8.1 → 0.9.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 (280) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2212 -446
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +110 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -30,34 +30,14 @@ import {
30
30
  import {
31
31
  flushUsageState,
32
32
  loadUsageState,
33
- noteProviderFailure,
34
- classifyProviderError,
35
- noteProviderSuccess,
36
- saveUsageState,
37
33
  updateProviderState,
38
34
  } from './lib/state.mjs'
39
35
  import { assertPublicUrl, crawlSite, getScrapeCapabilities, pinnedFetch, scrapeUrls } from './lib/web-tools.mjs'
40
36
  import { formatResponse } from './lib/formatter.mjs'
41
- import {
42
- cancelBackgroundTask,
43
- getBackgroundTask,
44
- renderBackgroundTask,
45
- renderBackgroundTaskList,
46
- resolveExecutionMode,
47
- startBackgroundTask,
48
- taskIdFromArgs,
49
- } from '../shared/background-tasks.mjs'
50
-
51
-
52
37
  ensureDataDir()
53
38
 
54
39
  const searchArgsSchema = z.object({
55
40
  keywords: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]).describe('Search query string or array of queries.'),
56
- mode: z.enum(['sync', 'async']).optional(),
57
- action: z.enum(['run', 'list', 'status', 'read', 'cancel']).optional(),
58
- task_id: z.string().optional(),
59
- firstResponseTimeoutMs: z.number().int().min(0).optional(),
60
- idleTimeoutMs: z.number().int().min(0).optional(),
61
41
  site: z.string().optional().describe('Restrict results to a specific domain.'),
62
42
  type: z.enum(['web', 'news', 'images']).optional().describe('Search type. Default: web.'),
63
43
  maxResults: z.number().int().min(1).max(20).optional().describe('Maximum number of results to return (1-20).'),
@@ -81,7 +61,7 @@ const searchUrlArgsSchema = z.object({
81
61
  cwd: z.string().optional(),
82
62
  })
83
63
 
84
- const SEARCH_EMPTY_STRING_FIELDS = ['keywords', 'site', 'type', 'locale', 'contextSize', 'mode', 'action', 'task_id', 'firstResponseTimeoutMs', 'idleTimeoutMs']
64
+ const SEARCH_EMPTY_STRING_FIELDS = ['keywords', 'site', 'type', 'locale', 'contextSize']
85
65
 
86
66
  function normalizeSearchArgs(rawArgs) {
87
67
  if (!rawArgs || typeof rawArgs !== 'object' || Array.isArray(rawArgs)) return rawArgs
@@ -125,33 +105,10 @@ function formattedText(tool, payload) {
125
105
  }
126
106
  }
127
107
 
128
- function toolText(response) {
129
- if (typeof response === 'string') return response
130
- const parts = Array.isArray(response?.content) ? response.content : []
131
- const text = parts
132
- .filter(part => part?.type === 'text')
133
- .map(part => part.text)
134
- .join('\n')
135
- return text || JSON.stringify(response, null, 2)
136
- }
137
-
138
108
  function okText(text) {
139
109
  return { content: [{ type: 'text', text }], isError: false }
140
110
  }
141
111
 
142
- function searchTaskControl(action, args = {}, options = {}) {
143
- if (action === 'list') return okText(renderBackgroundTaskList({ surface: 'search', context: options }))
144
- const taskId = taskIdFromArgs(args)
145
- if (!taskId) return { ...okText('Error: task_id is required'), isError: true }
146
- const task = getBackgroundTask(taskId, { surface: 'search', context: options })
147
- if (!task) return { ...okText(`Error: search task not found: ${taskId}`), isError: true }
148
- if (action === 'cancel') {
149
- cancelBackgroundTask(taskId, 'cancelled by search control')
150
- return okText(renderBackgroundTask(task, { includeResult: true }))
151
- }
152
- return okText(renderBackgroundTask(task, { includeResult: action === 'read' }))
153
- }
154
-
155
112
  function isInvalidSearchResult(result) {
156
113
  const title = String(result?.title || '').trim()
157
114
  return /\bpage not found\b|\b404\b.*\bnot found\b/i.test(title)
@@ -197,365 +154,6 @@ function normalizeCacheUrl(url) {
197
154
  }
198
155
  }
199
156
 
200
- const DOC_INDEX_MAX_BYTES = 2 * 1024 * 1024
201
- const DOC_INDEX_MAX_FETCHES = 8
202
- const DOC_INDEX_COMMON_PATHS = ['docs', 'api', 'reference', 'api/reference']
203
- const DOC_INDEX_STOPWORDS = new Set([
204
- 'about', 'after', 'again', 'also', 'and', 'are', 'can', 'com', 'doc', 'docs',
205
- 'documentation', 'for', 'from', 'how', 'http', 'https', 'into', 'official',
206
- 'page', 'pages', 'site', 'the', 'this', 'title', 'url', 'use', 'using', 'what',
207
- 'when', 'where', 'which', 'with', 'www',
208
- ])
209
-
210
- function keywordsText(keywords) {
211
- return Array.isArray(keywords) ? keywords.join(' ') : String(keywords || '')
212
- }
213
-
214
- function queryTokens(keywords) {
215
- const tokens = keywordsText(keywords)
216
- .toLowerCase()
217
- .match(/[\p{L}\p{N}][\p{L}\p{N}._-]{1,}/gu) || []
218
- return [...new Set(tokens
219
- .filter(token => token.length >= 3 && !DOC_INDEX_STOPWORDS.has(token)))]
220
- }
221
-
222
- // Weighted scoring across title/path/url/snippet. Title hit is the strongest
223
- // signal (8) because llms.txt entries are hand-curated; path-segment hits
224
- // (5) and last-segment hits (3..10) catch /api/foo over /blog/foo. Url and
225
- // snippet (2 / 1) act as tiebreakers when title misses. .md penalty -2 so
226
- // raw markdown sources lose to rendered docs when both are listed.
227
- function docLinkScore(link, tokens) {
228
- if (!tokens.length) return 0
229
- const title = String(link.title || '').toLowerCase()
230
- const url = String(link.url || '').toLowerCase()
231
- const snippet = String(link.snippet || '').toLowerCase()
232
- let pathname = ''
233
- try {
234
- pathname = new URL(link.url).pathname.toLowerCase()
235
- } catch {}
236
- const segments = pathname.split('/').filter(Boolean)
237
- let score = 0
238
- for (const token of tokens) {
239
- if (title.includes(token)) score += 8
240
- if (segments.includes(token)) score += 5
241
- if (segments.at(-1) === token) score += 3 + Math.max(0, 7 - segments.length)
242
- if (url.includes(token)) score += 2
243
- if (snippet.includes(token)) score += 1
244
- }
245
- if (/\.md$/i.test(pathname)) score -= 2
246
- return score
247
- }
248
-
249
- function docIndexUrlCandidates(site, keywords) {
250
- if (!site) return []
251
- let parsed
252
- try {
253
- parsed = new URL(/^https?:\/\//i.test(site) ? site : `https://${site}`)
254
- } catch {
255
- return []
256
- }
257
- const candidates = []
258
- const add = (url) => {
259
- try {
260
- const normalized = new URL(url).toString()
261
- if (!candidates.includes(normalized)) candidates.push(normalized)
262
- } catch {}
263
- }
264
- const pathParts = parsed.pathname.split('/').filter(Boolean)
265
- for (let i = pathParts.length; i >= 0; i -= 1) {
266
- const prefix = pathParts.slice(0, i).join('/')
267
- add(`${parsed.origin}${prefix ? `/${prefix}` : ''}/llms.txt`)
268
- }
269
- // When the user asks an api/docs question on a bare-host site, also probe
270
- // the common doc-prefix llms.txt locations the host might publish under.
271
- const docsIntent = /\b(?:api|docs?|documentation|reference)\b/i.test(keywordsText(keywords))
272
- if (docsIntent && pathParts.length === 0) {
273
- for (const prefix of DOC_INDEX_COMMON_PATHS) {
274
- add(`${parsed.origin}/${prefix}/llms.txt`)
275
- }
276
- }
277
- return candidates
278
- }
279
-
280
-
281
- function docIndexAbortSignal(timeoutMs, parentSignal) {
282
- const ms = Math.min(Math.max(Number(timeoutMs) || 10_000, 1000), 10_000)
283
- if (typeof AbortSignal.any === 'function') {
284
- const parts = [AbortSignal.timeout(ms)]
285
- if (parentSignal) parts.push(parentSignal)
286
- return AbortSignal.any(parts)
287
- }
288
- const controller = new AbortController()
289
- let timer
290
- let onParentAbort
291
- const abortWith = reason => {
292
- if (timer !== undefined) {
293
- clearTimeout(timer)
294
- timer = undefined
295
- }
296
- if (parentSignal && onParentAbort) {
297
- parentSignal.removeEventListener('abort', onParentAbort)
298
- onParentAbort = undefined
299
- }
300
- if (!controller.signal.aborted) controller.abort(reason)
301
- }
302
- timer = setTimeout(
303
- () => abortWith(new DOMException('The operation was aborted due to timeout', 'TimeoutError')),
304
- ms,
305
- )
306
- if (parentSignal) {
307
- if (parentSignal.aborted) {
308
- abortWith(parentSignal.reason)
309
- return controller.signal
310
- }
311
- onParentAbort = () => abortWith(parentSignal.reason)
312
- parentSignal.addEventListener('abort', onParentAbort, { once: true })
313
- }
314
- return controller.signal
315
- }
316
-
317
- async function fetchDocIndex(url, timeoutMs, parentSignal) {
318
- // SSRF: reuse the guarded web_fetch path's public-URL/private-IP check so
319
- // docs-index discovery cannot be steered into localhost / link-local /
320
- // cloud-metadata addresses by a hostile site override. Follow redirects
321
- // manually and re-validate each hop so a 30x Location can't steer us to
322
- // a private/loopback address after the initial check.
323
- const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308])
324
- const MAX_REDIRECTS = 5
325
- const signal = docIndexAbortSignal(timeoutMs, parentSignal)
326
- let currentUrl = url
327
- let response
328
- for (let hops = 0; ; hops++) {
329
- assertPublicUrl(currentUrl)
330
- // pinnedFetch resolves+validates the host once and pins the connection
331
- // to the validated IP, closing the validate-then-fetch DNS-rebinding /
332
- // TOCTOU window that bare `fetch` left open.
333
- response = await pinnedFetch(currentUrl, {
334
- headers: { Accept: 'text/markdown,text/plain,text/*,*/*' },
335
- signal,
336
- redirect: 'manual',
337
- })
338
- if (!REDIRECT_STATUSES.has(response.status)) break
339
- try { await response.body?.cancel() } catch {}
340
- if (hops >= MAX_REDIRECTS) {
341
- throw new Error(`docs index too many redirects (max ${MAX_REDIRECTS})`)
342
- }
343
- const location = response.headers.get('location')
344
- if (!location) {
345
- throw new Error(`docs index redirect ${response.status} without Location header`)
346
- }
347
- currentUrl = new URL(location, currentUrl).toString()
348
- }
349
- if (!response.ok) {
350
- try { await response.body?.cancel() } catch {}
351
- throw new Error(`docs index fetch failed: ${response.status}`)
352
- }
353
- const contentLength = Number(response.headers.get('content-length') || 0)
354
- if (contentLength > DOC_INDEX_MAX_BYTES) {
355
- try { await response.body?.cancel() } catch {}
356
- throw new Error(`docs index too large: ${contentLength}`)
357
- }
358
- // Enforce DOC_INDEX_MAX_BYTES while streaming so chunked / missing-length
359
- // responses can't blow past the 2MB cap by deferring the check until after
360
- // the whole body is buffered.
361
- const reader = response.body?.getReader?.()
362
- let text
363
- if (!reader) {
364
- // Buffer as bytes and cap by byte length — string.length counts UTF-16
365
- // code units, which under-counts multi-byte characters and lets the
366
- // body blow past DOC_INDEX_MAX_BYTES.
367
- const buf = new Uint8Array(await response.arrayBuffer())
368
- const capped = buf.byteLength > DOC_INDEX_MAX_BYTES ? buf.subarray(0, DOC_INDEX_MAX_BYTES) : buf
369
- text = new TextDecoder('utf-8', { fatal: false }).decode(capped)
370
- } else {
371
- const chunks = []
372
- let total = 0
373
- let capped = false
374
- try {
375
- while (true) {
376
- const { done, value } = await reader.read()
377
- if (done) break
378
- total += value.byteLength
379
- chunks.push(value)
380
- if (total >= DOC_INDEX_MAX_BYTES) {
381
- capped = true
382
- try { await reader.cancel() } catch {}
383
- break
384
- }
385
- }
386
- } finally {
387
- try { reader.releaseLock() } catch {}
388
- }
389
- const decoder = new TextDecoder('utf-8', { fatal: false })
390
- let buf = ''
391
- for (const chunk of chunks) buf += decoder.decode(chunk, { stream: true })
392
- buf += decoder.decode()
393
- text = capped || buf.length > DOC_INDEX_MAX_BYTES ? buf.slice(0, DOC_INDEX_MAX_BYTES) : buf
394
- }
395
- return {
396
- text,
397
- url: response.url || url,
398
- }
399
- }
400
-
401
- function parseDocIndexLinks(text, sourceUrl) {
402
- const links = []
403
- const seen = new Set()
404
- const add = (title, rawUrl, snippet = '') => {
405
- if (!title || !rawUrl) return
406
- let url
407
- try {
408
- url = new URL(rawUrl, sourceUrl).toString()
409
- } catch {
410
- return
411
- }
412
- if (!/^https?:\/\//i.test(url) || seen.has(url)) return
413
- seen.add(url)
414
- links.push({
415
- title: String(title).trim(),
416
- url,
417
- snippet: String(snippet || '').trim(),
418
- sourceUrl,
419
- })
420
- }
421
-
422
- for (const line of String(text || '').split(/\r?\n/)) {
423
- const item = line.match(/^\s*[-*]\s+\[([^\]]{1,180})\]\(([^)\s]+)\)\s*:?\s*(.*)$/)
424
- if (item) add(item[1], item[2], item[3])
425
- }
426
- const inlineRe = /\[([^\]]{1,180})\]\((https?:\/\/[^)\s]+)\)/g
427
- let match
428
- while ((match = inlineRe.exec(String(text || '')))) {
429
- add(match[1], match[2])
430
- }
431
- return links
432
- }
433
-
434
-
435
- function isDocIndexLink(url) {
436
- try {
437
- return /\/llms(?:-full)?\.txt$/i.test(new URL(url).pathname)
438
- } catch {
439
- return false
440
- }
441
- }
442
-
443
- function hostFromUrl(url) {
444
- try {
445
- return new URL(/^https?:\/\//i.test(url) ? url : `https://${url}`).hostname.toLowerCase()
446
- } catch {
447
- return ''
448
- }
449
- }
450
-
451
- function isBaseHost(host) {
452
- return host.split('.').filter(Boolean).length <= 2
453
- }
454
-
455
- function hostMatchesScope(host, scopedHost) {
456
- if (!host || !scopedHost) return false
457
- if (host === scopedHost) return true
458
- return isBaseHost(scopedHost) && host.endsWith(`.${scopedHost}`)
459
- }
460
-
461
- function sameDocIndexScope(url, site, requestedIndexUrl) {
462
- const linkHost = hostFromUrl(url)
463
- if (!linkHost) return false
464
- // Always require the link to match the original requested site host.
465
- const siteHost = hostFromUrl(site)
466
- if (siteHost && !hostMatchesScope(linkHost, siteHost)) return false
467
- const scopes = [
468
- siteHost,
469
- hostFromUrl(requestedIndexUrl),
470
- ].filter(Boolean)
471
- return scopes.some(scope => hostMatchesScope(linkHost, scope))
472
- }
473
-
474
- async function discoverDocsIndexResults(args, timeoutMs, parentSignal) {
475
- if (!args?.site || (args.type && args.type !== 'web')) return []
476
- const tokens = queryTokens(args.keywords)
477
- if (!tokens.length) return []
478
-
479
- const queue = docIndexUrlCandidates(args.site, args.keywords)
480
- const seenIndexes = new Set()
481
- const candidates = []
482
-
483
- while (queue.length > 0 && seenIndexes.size < DOC_INDEX_MAX_FETCHES) {
484
- if (parentSignal?.aborted) return []
485
- const indexUrl = queue.shift()
486
- if (!indexUrl || seenIndexes.has(indexUrl)) continue
487
- seenIndexes.add(indexUrl)
488
- let index = null
489
- try {
490
- index = await fetchDocIndex(indexUrl, timeoutMs, parentSignal)
491
- } catch {
492
- continue
493
- }
494
- const sourceUrl = index.url || indexUrl
495
- const links = parseDocIndexLinks(index.text, sourceUrl)
496
- for (const link of links) {
497
- if (isDocIndexLink(link.url)) {
498
- if (!seenIndexes.has(link.url) && queue.length + seenIndexes.size < DOC_INDEX_MAX_FETCHES) queue.push(link.url)
499
- continue
500
- }
501
- if (!sameDocIndexScope(link.url, args.site, indexUrl)) continue
502
- const score = docLinkScore(link, tokens)
503
- if (score <= 0) continue
504
- candidates.push({
505
- ...link,
506
- score,
507
- })
508
- }
509
- }
510
-
511
- const seenUrls = new Set()
512
- return candidates
513
- .sort((a, b) => b.score - a.score)
514
- .filter((item) => {
515
- if (seenUrls.has(item.url)) return false
516
- seenUrls.add(item.url)
517
- return true
518
- })
519
- .slice(0, Math.min(Number(args.maxResults) || 5, 5))
520
- .map(item => ({
521
- title: item.title,
522
- url: item.url,
523
- snippet: item.snippet || `Matched docs index: ${item.sourceUrl}`,
524
- source: 'docs-index',
525
- provider: 'docs-index',
526
- publishedDate: null,
527
- meta: { score: item.score, sourceUrl: item.sourceUrl },
528
- }))
529
- }
530
-
531
- async function augmentSearchPayloadWithDocsIndex(payload, args, timeoutMs, parentSignal) {
532
- if (!payload || typeof payload !== 'object') return payload
533
- const response = payload.response
534
- if (!response || typeof response !== 'object' || !Array.isArray(response.results)) return payload
535
- const indexResults = await discoverDocsIndexResults(args, timeoutMs, parentSignal)
536
- if (!indexResults.length) return payload
537
- const seen = new Set()
538
- const results = []
539
- for (const result of [...indexResults, ...response.results]) {
540
- const url = String(result?.url || '')
541
- const key = url || `${result?.title || ''}\n${result?.snippet || ''}`
542
- if (seen.has(key)) continue
543
- seen.add(key)
544
- results.push(result)
545
- }
546
- return {
547
- ...payload,
548
- response: {
549
- ...response,
550
- results: results.slice(0, Math.max(Number(args.maxResults) || results.length, indexResults.length)),
551
- docsIndexAugmented: {
552
- added: indexResults.length,
553
- sources: [...new Set(indexResults.map(item => item.meta?.sourceUrl).filter(Boolean))],
554
- },
555
- },
556
- }
557
- }
558
-
559
157
  async function writeStartupSnapshot() {
560
158
  loadConfig()
561
159
  const usageState = loadUsageState()
@@ -609,10 +207,92 @@ function buildAgentSearchPrompt(args) {
609
207
  return lines.join('\n')
610
208
  }
611
209
 
612
- async function _searchCore(args, { cacheState, agentSearch, signal }) {
210
+ function sourceUrl(source) {
211
+ return String(source?.url || source?.uri || source?.href || source?.source_url || '').trim()
212
+ }
213
+
214
+ function sourceTitle(source, fallbackUrl = '') {
215
+ return String(source?.title || source?.query || source?.name || fallbackUrl || '(untitled)').trim()
216
+ }
217
+
218
+ function sourceSnippet(source) {
219
+ return String(source?.snippet || source?.text || source?.description || '').replace(/\s+/g, ' ').trim()
220
+ }
221
+
222
+ function collectNativeSearchSources(result) {
223
+ const out = []
224
+ const add = (source, fallback = {}) => {
225
+ if (!source || typeof source !== 'object') return
226
+ const url = sourceUrl(source)
227
+ if (!url) return
228
+ out.push({
229
+ title: sourceTitle(source, fallback.title || url),
230
+ url,
231
+ snippet: sourceSnippet(source),
232
+ source: source.source || fallback.source || 'native-web-search',
233
+ provider: source.provider || fallback.provider || 'native-web-search',
234
+ publishedDate: source.publishedDate || source.published_date || null,
235
+ })
236
+ }
237
+ for (const citation of Array.isArray(result?.citations) ? result.citations : []) {
238
+ add(citation, { source: 'citation' })
239
+ }
240
+ for (const call of Array.isArray(result?.webSearchCalls) ? result.webSearchCalls : []) {
241
+ const action = call?.action || {}
242
+ for (const source of Array.isArray(action.sources) ? action.sources : []) {
243
+ add(source, { title: action.query || '', source: 'web_search_call' })
244
+ }
245
+ if (action.url) add({ url: action.url, title: action.query || '' }, { source: 'web_search_call' })
246
+ for (const url of Array.isArray(action.urls) ? action.urls : []) {
247
+ add({ url, title: action.query || '' }, { source: 'web_search_call' })
248
+ }
249
+ }
250
+ const seen = new Set()
251
+ return out.filter((item) => {
252
+ const key = item.url || `${item.title}\n${item.snippet}`
253
+ if (!key || seen.has(key)) return false
254
+ seen.add(key)
255
+ return true
256
+ })
257
+ }
258
+
259
+ function normalizeNativeSearchPayload(result, args, startedAt) {
260
+ if (result && typeof result === 'object' && result.tool === 'search' && result.response) {
261
+ return result
262
+ }
613
263
  const cacheArgs = searchArgsForCacheKey(args)
264
+ const answer = typeof result === 'string' ? result : String(result?.content || result?.answer || '').trim()
265
+ const provider = String(result?.provider || 'native-web-search')
266
+ const results = collectNativeSearchSources(result).slice(0, cacheArgs.maxResults)
267
+ const warnings = []
268
+ if (!results.length && Array.isArray(result?.webSearchCalls) && result.webSearchCalls.length) {
269
+ warnings.push('native web search returned no source URLs')
270
+ }
271
+ return {
272
+ tool: 'search',
273
+ provider,
274
+ response: {
275
+ usedProvider: provider,
276
+ query: Array.isArray(cacheArgs.keywords) ? cacheArgs.keywords.join('\n') : cacheArgs.keywords,
277
+ rawQuery: Array.isArray(cacheArgs.keywords) ? cacheArgs.keywords.join('\n') : cacheArgs.keywords,
278
+ answer,
279
+ model: result?.model || null,
280
+ durationMs: Date.now() - startedAt,
281
+ usage: result?.usage || null,
282
+ results,
283
+ warnings,
284
+ type: cacheArgs.type,
285
+ site: cacheArgs.site,
286
+ locale: cacheArgs.locale,
287
+ },
288
+ }
289
+ }
290
+
291
+ async function _searchCore(args, { cacheState, nativeSearch, signal }) {
292
+ const cacheArgs = searchArgsForCacheKey(args)
293
+ const backend = 'native-web-search'
614
294
  const searchCacheKey = buildCacheKey('search', {
615
- provider: 'web-researcher',
295
+ provider: backend,
616
296
  ...cacheArgs,
617
297
  })
618
298
  const cachedSearch = getCachedEntry(cacheState, searchCacheKey)
@@ -623,37 +303,19 @@ async function _searchCore(args, { cacheState, agentSearch, signal }) {
623
303
 
624
304
  const run = (async () => {
625
305
  if (signal?.aborted) throw signal.reason || new Error('search aborted')
626
- if (typeof agentSearch !== 'function') {
627
- throw new Error('search provider unavailable: Web Researcher agent bridge is not attached')
628
- }
629
- const startedAt = Date.now()
630
- const content = await agentSearch({
631
- ...args,
632
- ...cacheArgs,
633
- prompt: buildAgentSearchPrompt({ ...args, ...cacheArgs }),
634
- })
635
- const answer = String(content || '').trim()
636
- const payload = {
637
- tool: 'search',
638
- provider: 'web-researcher',
639
- response: {
640
- usedProvider: 'web-researcher',
641
- query: Array.isArray(cacheArgs.keywords) ? cacheArgs.keywords.join('\n') : cacheArgs.keywords,
642
- rawQuery: Array.isArray(cacheArgs.keywords) ? cacheArgs.keywords.join('\n') : cacheArgs.keywords,
643
- answer,
644
- model: null,
645
- durationMs: Date.now() - startedAt,
646
- usage: null,
647
- results: [],
648
- warnings: [],
649
- type: cacheArgs.type,
650
- site: cacheArgs.site,
651
- locale: cacheArgs.locale,
652
- },
306
+ if (typeof nativeSearch === 'function') {
307
+ const startedAt = Date.now()
308
+ const result = await nativeSearch({
309
+ ...args,
310
+ ...cacheArgs,
311
+ prompt: buildAgentSearchPrompt({ ...args, ...cacheArgs }),
312
+ })
313
+ const payload = normalizeNativeSearchPayload(result, { ...args, ...cacheArgs }, startedAt)
314
+ const cachedEntry = setCachedEntry(cacheState, searchCacheKey, payload, getSearchCacheTtlMs(cacheArgs.type))
315
+ flushCacheState()
316
+ return { ...payload, cache: buildCacheMeta(cachedEntry, false) }
653
317
  }
654
- const cachedEntry = setCachedEntry(cacheState, searchCacheKey, payload, getSearchCacheTtlMs(cacheArgs.type))
655
- flushCacheState()
656
- return { ...payload, cache: buildCacheMeta(cachedEntry, false) }
318
+ throw new Error('search provider unavailable: open /search to choose a search provider/model')
657
319
  })()
658
320
 
659
321
  run.catch(() => {})
@@ -784,9 +446,8 @@ async function _fetchCore(args, { usageState, cacheState, timeoutMs, signal }) {
784
446
  return { tool: 'web_fetch', results, urlsTruncated: allUrls.length > urls.length ? allUrls.length : 0 }
785
447
  }
786
448
 
787
- // Only `web_fetch` remains in the direct web module. Web search is routed
788
- // through the Web Researcher agent, so there is no standalone search provider
789
- // configuration or search backend selection surface.
449
+ // Web search is supplied by the runtime through the configured native search
450
+ // route. The module owns argument validation, caching, fan-out, and formatting.
790
451
  import { TOOL_DEFS as toolDefinitions } from './tool-defs.mjs'
791
452
 
792
453
  const SEARCH_INSTRUCTIONS = '';
@@ -809,7 +470,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
809
470
  }))
810
471
 
811
472
  async function handleToolCall(name, rawArgs, options = {}) {
812
- const { signal, agentSearch } = options || {}
473
+ const { signal, nativeSearch } = options || {}
813
474
  const config = loadConfig()
814
475
  const usageState = loadUsageState()
815
476
  const cacheState = loadCacheState()
@@ -818,10 +479,6 @@ async function handleToolCall(name, rawArgs, options = {}) {
818
479
  switch (name) {
819
480
  case 'search': {
820
481
  let args
821
- const rawAction = String(rawArgs?.action || 'run').trim().toLowerCase()
822
- if (['list', 'status', 'read', 'cancel'].includes(rawAction)) {
823
- return searchTaskControl(rawAction, rawArgs || {}, options)
824
- }
825
482
  if (rawArgs && rawArgs.pattern !== undefined && rawArgs.query === undefined && rawArgs.keywords === undefined) {
826
483
  return { content: [{ type: 'text', text: 'Error: web search requires query; use glob(pattern=...) for file paths.' }], isError: true }
827
484
  }
@@ -842,14 +499,14 @@ async function handleToolCall(name, rawArgs, options = {}) {
842
499
  const SEARCH_FANOUT_CAP = Math.max(1, Number(process.env.SEARCH_FANOUT_CAP) || 10)
843
500
  const keywords = [...new Set(args.keywords.map(kw => String(kw || '').trim()).filter(Boolean))].slice(0, SEARCH_FANOUT_CAP)
844
501
  const sections = await Promise.all(keywords.map(async (kw) => {
845
- const sub = await handleToolCall('search', { ...rawArgs, keywords: kw, mode: 'sync', action: 'run' }, { signal, agentSearch })
502
+ const sub = await handleToolCall('search', { ...rawArgs, keywords: kw }, { signal, nativeSearch })
846
503
  const text = (sub.content || []).filter(p => p.type === 'text').map(p => p.text).join('\n')
847
504
  return `### Query: ${kw}\n\n${text}`
848
505
  }))
849
506
  return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] }
850
507
  }
851
508
  try {
852
- const result = await _searchCore(args, { cacheState, agentSearch, signal })
509
+ const result = await _searchCore(args, { cacheState, nativeSearch, signal })
853
510
  flushUsageState()
854
511
  return formattedText('search', result)
855
512
  } catch (error) {
@@ -859,27 +516,6 @@ async function handleToolCall(name, rawArgs, options = {}) {
859
516
  return { content: [{ type: 'text', text: `Search failed: ${_cleanErr}` }], isError: true }
860
517
  }
861
518
  }
862
- if (resolveExecutionMode(args, 'sync') === 'async') {
863
- const label = Array.isArray(args.keywords)
864
- ? args.keywords.join(' | ')
865
- : String(args.keywords || '')
866
- const task = startBackgroundTask({
867
- surface: 'search',
868
- operation: 'search',
869
- label: label.replace(/\s+/g, ' ').slice(0, 120),
870
- input: { keywords: args.keywords, site: args.site || null, type: args.type || 'web' },
871
- context: options,
872
- resultType: 'search_task_result',
873
- renderResult: (text) => String(text || ''),
874
- run: async () => {
875
- const response = await runSearchNow()
876
- const text = toolText(response)
877
- if (response?.isError) throw new Error(text)
878
- return text
879
- },
880
- })
881
- return okText(renderBackgroundTask(task))
882
- }
883
519
  return runSearchNow()
884
520
  }
885
521
  case 'web_fetch': {