mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -4,7 +4,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
5
5
  import { z } from 'zod'
6
6
  import fs from 'fs'
7
- import path from 'path'
8
7
  import {
9
8
  ensureDataDir,
10
9
  getRequestTimeoutMs,
@@ -30,34 +29,14 @@ import {
30
29
  import {
31
30
  flushUsageState,
32
31
  loadUsageState,
33
- noteProviderFailure,
34
- classifyProviderError,
35
- noteProviderSuccess,
36
- saveUsageState,
37
32
  updateProviderState,
38
33
  } from './lib/state.mjs'
39
- import { assertPublicUrl, crawlSite, getScrapeCapabilities, pinnedFetch, scrapeUrls } from './lib/web-tools.mjs'
34
+ import { getScrapeCapabilities, scrapeUrls } from './lib/web-tools.mjs'
40
35
  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
36
  ensureDataDir()
53
37
 
54
38
  const searchArgsSchema = z.object({
55
39
  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
40
  site: z.string().optional().describe('Restrict results to a specific domain.'),
62
41
  type: z.enum(['web', 'news', 'images']).optional().describe('Search type. Default: web.'),
63
42
  maxResults: z.number().int().min(1).max(20).optional().describe('Maximum number of results to return (1-20).'),
@@ -81,7 +60,7 @@ const searchUrlArgsSchema = z.object({
81
60
  cwd: z.string().optional(),
82
61
  })
83
62
 
84
- const SEARCH_EMPTY_STRING_FIELDS = ['keywords', 'site', 'type', 'locale', 'contextSize', 'mode', 'action', 'task_id', 'firstResponseTimeoutMs', 'idleTimeoutMs']
63
+ const SEARCH_EMPTY_STRING_FIELDS = ['keywords', 'site', 'type', 'locale', 'contextSize']
85
64
 
86
65
  function normalizeSearchArgs(rawArgs) {
87
66
  if (!rawArgs || typeof rawArgs !== 'object' || Array.isArray(rawArgs)) return rawArgs
@@ -125,33 +104,6 @@ function formattedText(tool, payload) {
125
104
  }
126
105
  }
127
106
 
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
- function okText(text) {
139
- return { content: [{ type: 'text', text }], isError: false }
140
- }
141
-
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
107
  function isInvalidSearchResult(result) {
156
108
  const title = String(result?.title || '').trim()
157
109
  return /\bpage not found\b|\b404\b.*\bnot found\b/i.test(title)
@@ -197,365 +149,6 @@ function normalizeCacheUrl(url) {
197
149
  }
198
150
  }
199
151
 
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
152
  async function writeStartupSnapshot() {
560
153
  loadConfig()
561
154
  const usageState = loadUsageState()
@@ -609,10 +202,92 @@ function buildAgentSearchPrompt(args) {
609
202
  return lines.join('\n')
610
203
  }
611
204
 
612
- async function _searchCore(args, { cacheState, agentSearch, signal }) {
205
+ function sourceUrl(source) {
206
+ return String(source?.url || source?.uri || source?.href || source?.source_url || '').trim()
207
+ }
208
+
209
+ function sourceTitle(source, fallbackUrl = '') {
210
+ return String(source?.title || source?.query || source?.name || fallbackUrl || '(untitled)').trim()
211
+ }
212
+
213
+ function sourceSnippet(source) {
214
+ return String(source?.snippet || source?.text || source?.description || '').replace(/\s+/g, ' ').trim()
215
+ }
216
+
217
+ function collectNativeSearchSources(result) {
218
+ const out = []
219
+ const add = (source, fallback = {}) => {
220
+ if (!source || typeof source !== 'object') return
221
+ const url = sourceUrl(source)
222
+ if (!url) return
223
+ out.push({
224
+ title: sourceTitle(source, fallback.title || url),
225
+ url,
226
+ snippet: sourceSnippet(source),
227
+ source: source.source || fallback.source || 'native-web-search',
228
+ provider: source.provider || fallback.provider || 'native-web-search',
229
+ publishedDate: source.publishedDate || source.published_date || null,
230
+ })
231
+ }
232
+ for (const citation of Array.isArray(result?.citations) ? result.citations : []) {
233
+ add(citation, { source: 'citation' })
234
+ }
235
+ for (const call of Array.isArray(result?.webSearchCalls) ? result.webSearchCalls : []) {
236
+ const action = call?.action || {}
237
+ for (const source of Array.isArray(action.sources) ? action.sources : []) {
238
+ add(source, { title: action.query || '', source: 'web_search_call' })
239
+ }
240
+ if (action.url) add({ url: action.url, title: action.query || '' }, { source: 'web_search_call' })
241
+ for (const url of Array.isArray(action.urls) ? action.urls : []) {
242
+ add({ url, title: action.query || '' }, { source: 'web_search_call' })
243
+ }
244
+ }
245
+ const seen = new Set()
246
+ return out.filter((item) => {
247
+ const key = item.url || `${item.title}\n${item.snippet}`
248
+ if (!key || seen.has(key)) return false
249
+ seen.add(key)
250
+ return true
251
+ })
252
+ }
253
+
254
+ function normalizeNativeSearchPayload(result, args, startedAt) {
255
+ if (result && typeof result === 'object' && result.tool === 'search' && result.response) {
256
+ return result
257
+ }
613
258
  const cacheArgs = searchArgsForCacheKey(args)
259
+ const answer = typeof result === 'string' ? result : String(result?.content || result?.answer || '').trim()
260
+ const provider = String(result?.provider || 'native-web-search')
261
+ const results = collectNativeSearchSources(result).slice(0, cacheArgs.maxResults)
262
+ const warnings = []
263
+ if (!results.length && Array.isArray(result?.webSearchCalls) && result.webSearchCalls.length) {
264
+ warnings.push('native web search returned no source URLs')
265
+ }
266
+ return {
267
+ tool: 'search',
268
+ provider,
269
+ response: {
270
+ usedProvider: provider,
271
+ query: Array.isArray(cacheArgs.keywords) ? cacheArgs.keywords.join('\n') : cacheArgs.keywords,
272
+ rawQuery: Array.isArray(cacheArgs.keywords) ? cacheArgs.keywords.join('\n') : cacheArgs.keywords,
273
+ answer,
274
+ model: result?.model || null,
275
+ durationMs: Date.now() - startedAt,
276
+ usage: result?.usage || null,
277
+ results,
278
+ warnings,
279
+ type: cacheArgs.type,
280
+ site: cacheArgs.site,
281
+ locale: cacheArgs.locale,
282
+ },
283
+ }
284
+ }
285
+
286
+ async function _searchCore(args, { cacheState, nativeSearch, signal }) {
287
+ const cacheArgs = searchArgsForCacheKey(args)
288
+ const backend = 'native-web-search'
614
289
  const searchCacheKey = buildCacheKey('search', {
615
- provider: 'web-researcher',
290
+ provider: backend,
616
291
  ...cacheArgs,
617
292
  })
618
293
  const cachedSearch = getCachedEntry(cacheState, searchCacheKey)
@@ -623,37 +298,19 @@ async function _searchCore(args, { cacheState, agentSearch, signal }) {
623
298
 
624
299
  const run = (async () => {
625
300
  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
- },
301
+ if (typeof nativeSearch === 'function') {
302
+ const startedAt = Date.now()
303
+ const result = await nativeSearch({
304
+ ...args,
305
+ ...cacheArgs,
306
+ prompt: buildAgentSearchPrompt({ ...args, ...cacheArgs }),
307
+ })
308
+ const payload = normalizeNativeSearchPayload(result, { ...args, ...cacheArgs }, startedAt)
309
+ const cachedEntry = setCachedEntry(cacheState, searchCacheKey, payload, getSearchCacheTtlMs(cacheArgs.type))
310
+ flushCacheState()
311
+ return { ...payload, cache: buildCacheMeta(cachedEntry, false) }
653
312
  }
654
- const cachedEntry = setCachedEntry(cacheState, searchCacheKey, payload, getSearchCacheTtlMs(cacheArgs.type))
655
- flushCacheState()
656
- return { ...payload, cache: buildCacheMeta(cachedEntry, false) }
313
+ throw new Error('search provider unavailable: open /search to choose a search provider/model')
657
314
  })()
658
315
 
659
316
  run.catch(() => {})
@@ -784,9 +441,8 @@ async function _fetchCore(args, { usageState, cacheState, timeoutMs, signal }) {
784
441
  return { tool: 'web_fetch', results, urlsTruncated: allUrls.length > urls.length ? allUrls.length : 0 }
785
442
  }
786
443
 
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.
444
+ // Web search is supplied by the runtime through the configured native search
445
+ // route. The module owns argument validation, caching, fan-out, and formatting.
790
446
  import { TOOL_DEFS as toolDefinitions } from './tool-defs.mjs'
791
447
 
792
448
  const SEARCH_INSTRUCTIONS = '';
@@ -809,7 +465,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
809
465
  }))
810
466
 
811
467
  async function handleToolCall(name, rawArgs, options = {}) {
812
- const { signal, agentSearch } = options || {}
468
+ const { signal, nativeSearch } = options || {}
813
469
  const config = loadConfig()
814
470
  const usageState = loadUsageState()
815
471
  const cacheState = loadCacheState()
@@ -818,10 +474,6 @@ async function handleToolCall(name, rawArgs, options = {}) {
818
474
  switch (name) {
819
475
  case 'search': {
820
476
  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
477
  if (rawArgs && rawArgs.pattern !== undefined && rawArgs.query === undefined && rawArgs.keywords === undefined) {
826
478
  return { content: [{ type: 'text', text: 'Error: web search requires query; use glob(pattern=...) for file paths.' }], isError: true }
827
479
  }
@@ -842,14 +494,14 @@ async function handleToolCall(name, rawArgs, options = {}) {
842
494
  const SEARCH_FANOUT_CAP = Math.max(1, Number(process.env.SEARCH_FANOUT_CAP) || 10)
843
495
  const keywords = [...new Set(args.keywords.map(kw => String(kw || '').trim()).filter(Boolean))].slice(0, SEARCH_FANOUT_CAP)
844
496
  const sections = await Promise.all(keywords.map(async (kw) => {
845
- const sub = await handleToolCall('search', { ...rawArgs, keywords: kw, mode: 'sync', action: 'run' }, { signal, agentSearch })
497
+ const sub = await handleToolCall('search', { ...rawArgs, keywords: kw }, { signal, nativeSearch })
846
498
  const text = (sub.content || []).filter(p => p.type === 'text').map(p => p.text).join('\n')
847
499
  return `### Query: ${kw}\n\n${text}`
848
500
  }))
849
501
  return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] }
850
502
  }
851
503
  try {
852
- const result = await _searchCore(args, { cacheState, agentSearch, signal })
504
+ const result = await _searchCore(args, { cacheState, nativeSearch, signal })
853
505
  flushUsageState()
854
506
  return formattedText('search', result)
855
507
  } catch (error) {
@@ -859,27 +511,6 @@ async function handleToolCall(name, rawArgs, options = {}) {
859
511
  return { content: [{ type: 'text', text: `Search failed: ${_cleanErr}` }], isError: true }
860
512
  }
861
513
  }
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
514
  return runSearchNow()
884
515
  }
885
516
  case 'web_fetch': {
@@ -893,7 +524,7 @@ async function handleToolCall(name, rawArgs, options = {}) {
893
524
  throw e
894
525
  }
895
526
  try {
896
- const result = await _fetchCore(urlArgs, { config, usageState, cacheState, timeoutMs, signal })
527
+ const result = await _fetchCore(urlArgs, { usageState, cacheState, timeoutMs, signal })
897
528
  flushCacheState()
898
529
  flushUsageState()
899
530
  return {
@@ -44,10 +44,6 @@ export function writeJson(filePath, value) {
44
44
  writeJsonAtomicSync(filePath, value, { lock: true, fsyncDir: true })
45
45
  }
46
46
 
47
- export function saveConfig(config) {
48
- return { ...DEFAULT_CONFIG, ...(config || {}) }
49
- }
50
-
51
47
  export function loadConfig() {
52
48
  ensureDataDir()
53
49
  if (Object.keys(readSection('search') || {}).length > 0) {