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.
- package/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2212 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +110 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +520 -98
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4788 -2151
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +592 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8826 -2390
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- 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'
|
|
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
|
-
|
|
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:
|
|
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
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
788
|
-
//
|
|
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,
|
|
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
|
|
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,
|
|
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': {
|