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.
- package/README.md +47 -23
- package/package.json +34 -28
- 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 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -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 +108 -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 +77 -29
- 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 +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -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 +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- 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 +289 -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 +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- 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 +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- 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/llm-worker-host.mjs +0 -4
- 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 -2
- 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 +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -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 +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -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 +139 -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 +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- 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 +428 -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 +597 -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 +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- 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 +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -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 +756 -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 +1 -620
- 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/ink.js +54 -8
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- 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/channels/lib/hook-pipe-server.mjs +0 -671
- 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
|
@@ -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 {
|
|
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'
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
//
|
|
788
|
-
//
|
|
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,
|
|
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
|
|
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,
|
|
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, {
|
|
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) {
|