dsh-search-enhance 0.2.0 → 0.2.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 +6 -7
- package/README.zh.md +6 -7
- package/client/client.js +85 -3
- package/client/types/client/locales.d.ts +5 -1
- package/client/types/web-config/contracts.d.ts +9 -0
- package/lib/documentation/service.d.ts +1 -1
- package/lib/documentation/service.js +16 -6
- package/lib/orchestration/orchestrator.js +3 -1
- package/lib/orchestration/types.d.ts +1 -1
- package/lib/orchestration/types.js +2 -0
- package/lib/presentation/render.d.ts +3 -0
- package/lib/presentation/render.js +60 -34
- package/lib/presentation/web-card.js +16 -6
- package/lib/prompt/tool-discovery.js +7 -11
- package/lib/providers/context7.js +11 -1
- package/lib/tools/docs-search.js +3 -2
- package/lib/tools/schemas.d.ts +6 -4
- package/lib/tools/schemas.js +7 -3
- package/lib/tools/web-search.js +6 -5
- package/lib/web-config/contracts.d.ts +9 -0
- package/lib/web-config/contracts.js +3 -0
- package/lib/web-config/host.js +11 -1
- package/package.json +28 -10
package/README.md
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
`dsh-search-enhance` is a search extension for DeepSeek Harness. It uses a Grok-compatible Search API for primary web answers and can optionally use Context7, Exa, Tavily, and
|
|
5
|
+
`dsh-search-enhance` is a search extension for DeepSeek Harness. It uses a Grok-compatible Search API for primary web answers and can optionally use Context7, Exa, Tavily, Firecrawl, and Parallel for documentation lookup, supplementary discovery, page extraction, and site mapping.
|
|
6
6
|
|
|
7
7
|
The plugin presents search results, sources, and page content separately. Searches return an answer or documentation snippets with source links; you can ask for more sources when needed. To check important details, ask it to read the original page. Search snippets remain distinct from fetched page content.
|
|
8
8
|
|
|
9
|
-
> Bring your own endpoints and credentials. The plugin ships no API keys. A Grok-compatible endpoint is required for `web_search`; Context7, Exa, Tavily, and
|
|
10
|
-
|
|
11
|
-

|
|
9
|
+
> Bring your own endpoints and credentials. The plugin ships no API keys. A Grok-compatible endpoint is required for `web_search`; Context7, Exa, Tavily, Firecrawl, and Parallel are optional.
|
|
12
10
|
|
|
13
11
|
## Key characteristics
|
|
14
12
|
|
|
15
|
-
- `web_search` uses the Grok-compatible endpoint for the main answer and runs all user-selected supplemental Providers (Exa, Tavily, Firecrawl) in parallel, with an independent per-Provider source limit.
|
|
13
|
+
- `web_search` uses the Grok-compatible endpoint for the main answer and runs all user-selected supplemental Providers (Exa, Tavily, Firecrawl, Parallel) in parallel, with an independent per-Provider source limit.
|
|
16
14
|
- Sources are normalized, de-duplicated, and reordered using source category, requested version, and publication-time signals before they are shown.
|
|
17
15
|
- `source_ref` keeps the complete source record in private durable storage, allowing the Agent to paginate beyond the links included in the initial result.
|
|
18
16
|
- `docs_search` uses Context7 only with an explicit `library_name` or `library_id`; requests without a library identity use Exa discovery.
|
|
19
|
-
- `web_extract` requires one explicit `provider` from the user-enabled set: `tavily_extract`, `firecrawl_scrape`, `smart_direct`, or `direct`. It reports the route, evidence level, and available page metadata; failures never switch Providers.
|
|
17
|
+
- `web_extract` requires one explicit `provider` from the user-enabled set: `tavily_extract`, `firecrawl_scrape`, `parallel_extract`, `smart_direct`, or `direct`. It reports the route, evidence level, and available page metadata; failures never switch Providers.
|
|
20
18
|
- Ask for more sources or related pages on a specific website when needed; no manual tool loading is required.
|
|
21
19
|
- Supplemental search defaults to no Providers selected. Unselected Providers are never requested; missing credentials and failures of selected Providers remain visible as warnings.
|
|
22
20
|
|
|
@@ -86,10 +84,11 @@ Configure only the routes you need.
|
|
|
86
84
|
| Exa | Broad documentation and supplementary discovery | `EXA_API_KEY` | No |
|
|
87
85
|
| Tavily | Supplementary search, page extraction, and site mapping | `TAVILY_API_KEY` | No |
|
|
88
86
|
| Firecrawl | Supplementary search and page extraction | `FIRECRAWL_API_KEY` | No |
|
|
87
|
+
| Parallel | Supplementary search and Markdown page extraction | `PARALLEL_API_KEY` | No |
|
|
89
88
|
|
|
90
89
|
In the plugin settings, select any combination of **supplemental search Providers**. Each selected Provider runs alongside the main search, using `supplementalSearch.maxSourcesPerProvider` (default `5`, maximum `100`). This selection applies to all search profiles.
|
|
91
90
|
|
|
92
|
-
Separately, select the **extraction Providers** available to the Agent. Each `web_extract` call must choose exactly one enabled Provider, for example `{ "url": "https://example.com/", "provider": "direct", "format": "markdown" }`. Disabled, unconfigured, unsupported, or failed Providers do not trigger automatic fallback.
|
|
91
|
+
Separately, select the **extraction Providers** available to the Agent. Each `web_extract` call must choose exactly one enabled Provider, for example `{ "url": "https://example.com/", "provider": "direct", "format": "markdown" }`. Disabled, unconfigured, unsupported, or failed Providers do not trigger automatic fallback. Parallel extraction supports Markdown only and is disabled by default; enable it explicitly. Other extraction Providers are enabled by default; disable any you do not want the Agent to use.
|
|
93
92
|
|
|
94
93
|
For `docs_search`, Context7 requires an explicit `library_name` or `library_id`. Without one, `provider: "auto"` uses Exa instead of guessing a package name from the full question.
|
|
95
94
|
|
package/README.zh.md
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
`dsh-search-enhance` 是 DeepSeek Harness 的搜索增强插件。它使用 Grok-compatible Search API 生成普通网页搜索的主要回答,并可选用 Context7、Exa、Tavily 和
|
|
5
|
+
`dsh-search-enhance` 是 DeepSeek Harness 的搜索增强插件。它使用 Grok-compatible Search API 生成普通网页搜索的主要回答,并可选用 Context7、Exa、Tavily、Firecrawl 和 Parallel 完成文档检索、补充来源、网页正文提取和站点页面发现。
|
|
6
6
|
|
|
7
7
|
插件将搜索结果、参考来源和网页正文分开呈现。搜索时先给出回答或文档片段及来源链接,需要更多来源时可以继续追问;核对重要内容时,再要求读取原网页。搜索摘要与实际读取的正文会明确区分。
|
|
8
8
|
|
|
9
|
-
> 你需要自行提供所选服务的端点和凭据,插件不内置任何 API Key。`web_search` 需要 Grok-compatible 端点;Context7、Exa、Tavily 和
|
|
10
|
-
|
|
11
|
-

|
|
9
|
+
> 你需要自行提供所选服务的端点和凭据,插件不内置任何 API Key。`web_search` 需要 Grok-compatible 端点;Context7、Exa、Tavily、Firecrawl 和 Parallel 均为可选 Provider。
|
|
12
10
|
|
|
13
11
|
## 主要特点
|
|
14
12
|
|
|
15
|
-
- `web_search` 使用 Grok-compatible 端点生成主要回答,并行调用用户勾选的全部补充搜索 Provider(Exa、Tavily、Firecrawl),每家使用独立的来源数量上限。
|
|
13
|
+
- `web_search` 使用 Grok-compatible 端点生成主要回答,并行调用用户勾选的全部补充搜索 Provider(Exa、Tavily、Firecrawl、Parallel),每家使用独立的来源数量上限。
|
|
16
14
|
- 来源在展示前会经过 URL 标准化、去重,并根据来源类别、目标版本和发布时间信号重新排序。
|
|
17
15
|
- `source_ref` 将完整来源记录保存在插件私有持久存储中,Agent 可以继续分页读取首次结果未展示的来源。
|
|
18
16
|
- `docs_search` 只在提供明确 `library_name` 或 `library_id` 时使用 Context7;没有库身份的请求使用 Exa 发现。
|
|
19
|
-
- `web_extract` 每次必须通过 `provider` 指定一家用户已启用的服务:`tavily_extract`、`firecrawl_scrape`、`smart_direct` 或 `direct`。结果报告提取路径、证据等级和页面元数据,失败不自动换服务。
|
|
17
|
+
- `web_extract` 每次必须通过 `provider` 指定一家用户已启用的服务:`tavily_extract`、`firecrawl_scrape`、`parallel_extract`、`smart_direct` 或 `direct`。结果报告提取路径、证据等级和页面元数据,失败不自动换服务。
|
|
20
18
|
- 需要时可继续查看更多来源,或查找某个网站下的相关页面,无需手动加载工具。
|
|
21
19
|
- 补充搜索默认不选择任何 Provider。未选中的服务不会被请求;选中服务缺少凭据或执行失败会在结果中显示警告。
|
|
22
20
|
|
|
@@ -86,10 +84,11 @@ dsh web
|
|
|
86
84
|
| Exa | 广泛文档发现和补充发现 | `EXA_API_KEY` | 否 |
|
|
87
85
|
| Tavily | 补充搜索、网页提取和站点映射 | `TAVILY_API_KEY` | 否 |
|
|
88
86
|
| Firecrawl | 补充搜索和网页提取 | `FIRECRAWL_API_KEY` | 否 |
|
|
87
|
+
| Parallel | 补充搜索和 Markdown 网页提取 | `PARALLEL_API_KEY` | 否 |
|
|
89
88
|
|
|
90
89
|
在插件设置中,可以多选 **补充搜索 Provider**。选中项与主搜索并行执行,每家使用 `supplementalSearch.maxSourcesPerProvider` 限制来源数量(默认 `5`,最大 `100`)。选择对所有搜索 profile 生效。
|
|
91
90
|
|
|
92
|
-
另外,多选 **网页提取 Provider** 可以限制 Agent 可用的服务范围。每次 `web_extract` 必须明确选择一家已启用服务,例如 `{ "url": "https://example.com/", "provider": "direct", "format": "markdown" }
|
|
91
|
+
另外,多选 **网页提取 Provider** 可以限制 Agent 可用的服务范围。每次 `web_extract` 必须明确选择一家已启用服务,例如 `{ "url": "https://example.com/", "provider": "direct", "format": "markdown" }`。服务被禁用、缺少凭据、不支持格式或执行失败时,直接报告错误,不自动回退。Parallel 提取仅支持 Markdown,默认关闭,需要手动启用;其他提取服务默认启用,可取消勾选不希望使用的服务。
|
|
93
92
|
|
|
94
93
|
对于 `docs_search`,Context7 需要明确的 `library_name` 或 `library_id`。两者都未提供时,`provider: "auto"` 使用 Exa,不会根据完整问题猜测包名。
|
|
95
94
|
|
package/client/client.js
CHANGED
|
@@ -38,6 +38,24 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
["defaultProfile"],
|
|
39
39
|
["defaultDepth"],
|
|
40
40
|
["toolTimeoutMs"],
|
|
41
|
+
["retention", "docsSearchMaxResults"],
|
|
42
|
+
...[
|
|
43
|
+
"auto",
|
|
44
|
+
"coding_docs",
|
|
45
|
+
"code_examples",
|
|
46
|
+
"project_research",
|
|
47
|
+
"academic",
|
|
48
|
+
"fact_check"
|
|
49
|
+
].flatMap((profile) => [
|
|
50
|
+
"compact",
|
|
51
|
+
"normal",
|
|
52
|
+
"deep"
|
|
53
|
+
].map((depth) => [
|
|
54
|
+
"budgets",
|
|
55
|
+
profile,
|
|
56
|
+
depth,
|
|
57
|
+
"maxVisibleSources"
|
|
58
|
+
])),
|
|
41
59
|
["toolDiscovery", "mode"],
|
|
42
60
|
...WEB_SUPPLEMENTAL_SEARCH_PROVIDERS.map((provider) => ["supplementalSearch", provider]),
|
|
43
61
|
["supplementalSearch", "maxSourcesPerProvider"],
|
|
@@ -432,6 +450,8 @@ window.__ModuleLoader__.load({
|
|
|
432
450
|
defaultProfile: config.defaultProfile,
|
|
433
451
|
defaultDepth: config.defaultDepth,
|
|
434
452
|
toolTimeoutMs: config.toolTimeoutMs,
|
|
453
|
+
retention: { ...config.retention },
|
|
454
|
+
budgets: Object.fromEntries(Object.entries(config.budgets).map(([profile, depths]) => [profile, Object.fromEntries(Object.entries(depths).map(([depth, budget]) => [depth, { ...budget }]))])),
|
|
435
455
|
toolDiscovery: { ...config.toolDiscovery },
|
|
436
456
|
supplementalSearch: { ...config.supplementalSearch },
|
|
437
457
|
searchApi: { ...config.searchApi },
|
|
@@ -694,7 +714,7 @@ window.__ModuleLoader__.load({
|
|
|
694
714
|
]);
|
|
695
715
|
const mutations = (0, react.useMemo)(() => snapshot === void 0 || draft === void 0 ? [] : mutationsFor(snapshot.value, draft), [draft, snapshot]);
|
|
696
716
|
const dirty = mutations.length > 0;
|
|
697
|
-
const valid = draft !== void 0 && snapshot !== void 0 && validTimeouts(draft) && validSupplementalSearch(draft, snapshot.options.supplementalSearchMaxSources);
|
|
717
|
+
const valid = draft !== void 0 && snapshot !== void 0 && validTimeouts(draft) && validSupplementalSearch(draft, snapshot.options.supplementalSearchMaxSources) && Number.isInteger(draft.retention.docsSearchMaxResults) && draft.retention.docsSearchMaxResults >= snapshot.options.docsSearchMinResults && draft.retention.docsSearchMaxResults <= snapshot.options.docsSearchMaxResults && Object.values(draft.budgets).every((depths) => Object.values(depths).every((budget) => Number.isInteger(budget.maxVisibleSources) && budget.maxVisibleSources >= 0 && budget.maxVisibleSources <= snapshot.options.visibleSourcesMax));
|
|
698
718
|
const editable = phase === "ready" && snapshot?.writable === true && !saving;
|
|
699
719
|
const updateProvider = (provider, value) => {
|
|
700
720
|
setDraft((current) => current === void 0 ? current : provider === "searchApi" ? {
|
|
@@ -1016,6 +1036,60 @@ window.__ModuleLoader__.load({
|
|
|
1016
1036
|
}
|
|
1017
1037
|
})
|
|
1018
1038
|
}),
|
|
1039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1040
|
+
label: t("maxVisibleSources"),
|
|
1041
|
+
overridden: isOverridden(snapshot.user, [
|
|
1042
|
+
"budgets",
|
|
1043
|
+
draft.defaultProfile,
|
|
1044
|
+
draft.defaultDepth,
|
|
1045
|
+
"maxVisibleSources"
|
|
1046
|
+
]),
|
|
1047
|
+
t,
|
|
1048
|
+
hint: t("maxVisibleSourcesHint"),
|
|
1049
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
|
|
1050
|
+
label: t("maxVisibleSources"),
|
|
1051
|
+
value: draft.budgets[draft.defaultProfile][draft.defaultDepth].maxVisibleSources,
|
|
1052
|
+
min: 0,
|
|
1053
|
+
max: snapshot.options.visibleSourcesMax,
|
|
1054
|
+
disabled: !editable,
|
|
1055
|
+
onChange: (maxVisibleSources) => {
|
|
1056
|
+
setDraft((current) => current === void 0 ? current : {
|
|
1057
|
+
...current,
|
|
1058
|
+
budgets: {
|
|
1059
|
+
...current.budgets,
|
|
1060
|
+
[current.defaultProfile]: {
|
|
1061
|
+
...current.budgets[current.defaultProfile],
|
|
1062
|
+
[current.defaultDepth]: { maxVisibleSources }
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
setFeedback("idle");
|
|
1067
|
+
}
|
|
1068
|
+
})
|
|
1069
|
+
}),
|
|
1070
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1071
|
+
label: t("docsSearchMaxResults"),
|
|
1072
|
+
overridden: isOverridden(snapshot.user, ["retention", "docsSearchMaxResults"]),
|
|
1073
|
+
t,
|
|
1074
|
+
hint: t("docsSearchMaxResultsHint"),
|
|
1075
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
|
|
1076
|
+
label: t("docsSearchMaxResults"),
|
|
1077
|
+
value: draft.retention.docsSearchMaxResults,
|
|
1078
|
+
min: snapshot.options.docsSearchMinResults,
|
|
1079
|
+
max: snapshot.options.docsSearchMaxResults,
|
|
1080
|
+
disabled: !editable,
|
|
1081
|
+
onChange: (docsSearchMaxResults) => {
|
|
1082
|
+
setDraft((current) => current === void 0 ? current : {
|
|
1083
|
+
...current,
|
|
1084
|
+
retention: {
|
|
1085
|
+
...current.retention,
|
|
1086
|
+
docsSearchMaxResults
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
setFeedback("idle");
|
|
1090
|
+
}
|
|
1091
|
+
})
|
|
1092
|
+
}),
|
|
1019
1093
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1020
1094
|
label: t("toolTimeoutMs"),
|
|
1021
1095
|
overridden: isOverridden(snapshot.user, ["toolTimeoutMs"]),
|
|
@@ -1441,6 +1515,10 @@ window.__ModuleLoader__.load({
|
|
|
1441
1515
|
strategyIntro: "Defaults used when a tool call does not choose its own profile or depth.",
|
|
1442
1516
|
defaultProfile: "Default profile",
|
|
1443
1517
|
defaultDepth: "Default depth",
|
|
1518
|
+
maxVisibleSources: "Initial web_search source limit",
|
|
1519
|
+
maxVisibleSourcesHint: "Applies to the profile/depth selected above, not Provider collection or storage. Use source_ref for retained sources; model-text byte limits still apply.",
|
|
1520
|
+
docsSearchMaxResults: "docs_search request ceiling",
|
|
1521
|
+
docsSearchMaxResultsHint: "Allows max_results values from 1 up to this ceiling (default call: 6). Does not override an explicit smaller value. Collect more by increasing max_results and rerunning; pagination reads only retained records.",
|
|
1444
1522
|
toolDiscoveryMode: "Tool discovery",
|
|
1445
1523
|
toolTimeoutMs: "Whole tool timeout (ms)",
|
|
1446
1524
|
extraSourcesHeading: "Supplemental search providers",
|
|
@@ -1496,7 +1574,7 @@ window.__ModuleLoader__.load({
|
|
|
1496
1574
|
conflict: "The configuration changed elsewhere. Reload before saving again.",
|
|
1497
1575
|
saveFailed: "The configuration could not be saved.",
|
|
1498
1576
|
readOnly: "The Settings document is read-only.",
|
|
1499
|
-
invalidNumber: "Timeouts must be positive whole numbers
|
|
1577
|
+
invalidNumber: "Timeouts must be positive whole numbers; source and request limits must be whole numbers within their allowed range.",
|
|
1500
1578
|
overridden: "User override",
|
|
1501
1579
|
inherited: "Inherited"
|
|
1502
1580
|
};
|
|
@@ -1522,6 +1600,10 @@ window.__ModuleLoader__.load({
|
|
|
1522
1600
|
strategyIntro: "工具调用未显式选择 profile 或 depth 时使用这些默认值。",
|
|
1523
1601
|
defaultProfile: "默认 Profile",
|
|
1524
1602
|
defaultDepth: "默认深度",
|
|
1603
|
+
maxVisibleSources: "web_search 首次展示来源上限",
|
|
1604
|
+
maxVisibleSourcesHint: "只影响上方所选 Profile / 深度的首次展示,不限制服务收集或存储;已保留来源可通过 source_ref 分页,模型文本仍受字节上限约束。",
|
|
1605
|
+
docsSearchMaxResults: "docs_search 请求数量最高值",
|
|
1606
|
+
docsSearchMaxResultsHint: "允许 max_results 从 1 到此最高值(调用默认 6 条),不会覆盖模型明确传入的较小值。需要更多结果时提高 max_results 后重搜;分页只能读取已保留记录。",
|
|
1525
1607
|
toolDiscoveryMode: "工具披露模式",
|
|
1526
1608
|
toolTimeoutMs: "工具总超时(毫秒)",
|
|
1527
1609
|
extraSourcesHeading: "补充搜索服务",
|
|
@@ -1577,7 +1659,7 @@ window.__ModuleLoader__.load({
|
|
|
1577
1659
|
conflict: "配置已在其他位置发生变化。请重新读取后再保存。",
|
|
1578
1660
|
saveFailed: "无法保存配置。",
|
|
1579
1661
|
readOnly: "Settings 文档为只读。",
|
|
1580
|
-
invalidNumber: "
|
|
1662
|
+
invalidNumber: "超时必须是正整数;来源与请求数量上限必须是允许范围内的整数。",
|
|
1581
1663
|
overridden: "用户覆盖",
|
|
1582
1664
|
inherited: "继承值"
|
|
1583
1665
|
};
|
|
@@ -20,6 +20,10 @@ export declare const en: {
|
|
|
20
20
|
readonly strategyIntro: "Defaults used when a tool call does not choose its own profile or depth.";
|
|
21
21
|
readonly defaultProfile: "Default profile";
|
|
22
22
|
readonly defaultDepth: "Default depth";
|
|
23
|
+
readonly maxVisibleSources: "Initial web_search source limit";
|
|
24
|
+
readonly maxVisibleSourcesHint: "Applies to the profile/depth selected above, not Provider collection or storage. Use source_ref for retained sources; model-text byte limits still apply.";
|
|
25
|
+
readonly docsSearchMaxResults: "docs_search request ceiling";
|
|
26
|
+
readonly docsSearchMaxResultsHint: "Allows max_results values from 1 up to this ceiling (default call: 6). Does not override an explicit smaller value. Collect more by increasing max_results and rerunning; pagination reads only retained records.";
|
|
23
27
|
readonly toolDiscoveryMode: "Tool discovery";
|
|
24
28
|
readonly toolTimeoutMs: "Whole tool timeout (ms)";
|
|
25
29
|
readonly extraSourcesHeading: "Supplemental search providers";
|
|
@@ -75,7 +79,7 @@ export declare const en: {
|
|
|
75
79
|
readonly conflict: "The configuration changed elsewhere. Reload before saving again.";
|
|
76
80
|
readonly saveFailed: "The configuration could not be saved.";
|
|
77
81
|
readonly readOnly: "The Settings document is read-only.";
|
|
78
|
-
readonly invalidNumber: "Timeouts must be positive whole numbers
|
|
82
|
+
readonly invalidNumber: "Timeouts must be positive whole numbers; source and request limits must be whole numbers within their allowed range.";
|
|
79
83
|
readonly overridden: "User override";
|
|
80
84
|
readonly inherited: "Inherited";
|
|
81
85
|
};
|
|
@@ -30,6 +30,12 @@ export interface WebEditableConfig {
|
|
|
30
30
|
defaultProfile: string;
|
|
31
31
|
defaultDepth: string;
|
|
32
32
|
toolTimeoutMs: number;
|
|
33
|
+
retention: {
|
|
34
|
+
docsSearchMaxResults: number;
|
|
35
|
+
};
|
|
36
|
+
budgets: Record<string, Record<string, {
|
|
37
|
+
maxVisibleSources: number;
|
|
38
|
+
}>>;
|
|
33
39
|
toolDiscovery: {
|
|
34
40
|
mode: string;
|
|
35
41
|
};
|
|
@@ -76,6 +82,9 @@ export interface WebConfigOptions {
|
|
|
76
82
|
parallelSearchModes: readonly string[];
|
|
77
83
|
proxyUrlMaxCharacters: number;
|
|
78
84
|
supplementalSearchMaxSources: number;
|
|
85
|
+
docsSearchMinResults: number;
|
|
86
|
+
docsSearchMaxResults: number;
|
|
87
|
+
visibleSourcesMax: number;
|
|
79
88
|
}
|
|
80
89
|
export interface WebCredentialState {
|
|
81
90
|
ref: string;
|
|
@@ -18,7 +18,7 @@ export interface DocumentationProviderStatus {
|
|
|
18
18
|
readonly provider: DocumentationResultProvider;
|
|
19
19
|
readonly state: DocumentationProviderState;
|
|
20
20
|
}
|
|
21
|
-
export declare const DOCUMENTATION_WARNING_CODES: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "no_results"];
|
|
21
|
+
export declare const DOCUMENTATION_WARNING_CODES: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "library_not_found", "collection_limit_reached", "visible_sources_truncated", "no_results"];
|
|
22
22
|
export type DocumentationWarningCode = (typeof DOCUMENTATION_WARNING_CODES)[number];
|
|
23
23
|
export declare const DOCUMENTATION_CACHE_PATH_STATES: readonly ["hit", "miss", "refresh", "stale", "skipped", "failed"];
|
|
24
24
|
export type DocumentationCachePathState = (typeof DOCUMENTATION_CACHE_PATH_STATES)[number];
|
|
@@ -15,6 +15,9 @@ export const DOCUMENTATION_WARNING_CODES = [
|
|
|
15
15
|
'cache_stale',
|
|
16
16
|
'cache_evicted',
|
|
17
17
|
'provider_result_truncated',
|
|
18
|
+
'library_not_found',
|
|
19
|
+
'collection_limit_reached',
|
|
20
|
+
'visible_sources_truncated',
|
|
18
21
|
'no_results',
|
|
19
22
|
];
|
|
20
23
|
export const DOCUMENTATION_CACHE_PATH_STATES = [
|
|
@@ -592,12 +595,18 @@ export class DocumentationSearchService extends Service {
|
|
|
592
595
|
const qualitySources = applySourceQuality(validated.query, compatibleSources);
|
|
593
596
|
const sources = Object.freeze(qualitySources.slice(0, input.maxResults));
|
|
594
597
|
const snippets = Object.freeze(contextOutcome.snippets.slice(0, input.maxResults));
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
+
const providerTruncated = contextOutcome.truncated || exaOutcome.truncated;
|
|
599
|
+
const visibleSourcesTruncated = sources.length < qualitySources.length;
|
|
600
|
+
const truncated = providerTruncated
|
|
601
|
+
|| visibleSourcesTruncated
|
|
598
602
|
|| snippets.length < contextOutcome.snippets.length;
|
|
599
|
-
if (
|
|
603
|
+
if (providerTruncated)
|
|
600
604
|
warnings.push(fixedWarning('provider_result_truncated'));
|
|
605
|
+
if (visibleSourcesTruncated)
|
|
606
|
+
warnings.push(fixedWarning('visible_sources_truncated'));
|
|
607
|
+
if (exaOutcome.state === 'complete' && exaOutcome.sources.length >= input.maxResults) {
|
|
608
|
+
warnings.push(fixedWarning('collection_limit_reached', { provider: 'exa', count: input.maxResults }));
|
|
609
|
+
}
|
|
601
610
|
const completedValidPath = contextOutcome.completedValidPath || exaOutcome.completedValidPath;
|
|
602
611
|
if (sources.length === 0 && snippets.length === 0 && !completedValidPath) {
|
|
603
612
|
const cause = contextOutcome.error ?? exaOutcome.error ?? new ProviderError({
|
|
@@ -613,11 +622,11 @@ export class DocumentationSearchService extends Service {
|
|
|
613
622
|
}
|
|
614
623
|
const cache = contextOutcome.cache ?? cacheReport(cachePath('skipped', 0, 'provider_not_selected'), cachePath('skipped', 0, 'provider_not_selected'));
|
|
615
624
|
const persistence = Object.freeze({
|
|
616
|
-
collectionTruncated: truncated,
|
|
625
|
+
collectionTruncated: exaOutcome.truncated,
|
|
617
626
|
depth: 'compact',
|
|
618
627
|
profile: 'coding_docs',
|
|
619
628
|
query: validated.query,
|
|
620
|
-
sources,
|
|
629
|
+
sources: Object.freeze(qualitySources),
|
|
621
630
|
});
|
|
622
631
|
return Object.freeze({
|
|
623
632
|
attempts,
|
|
@@ -712,6 +721,7 @@ export class DocumentationSearchService extends Service {
|
|
|
712
721
|
}
|
|
713
722
|
if (selectedLibrary?.id === undefined || !isContext7LibraryId(selectedLibrary.id)) {
|
|
714
723
|
docsReport = cachePath('skipped', 0, 'library_not_found');
|
|
724
|
+
warnings.push(fixedWarning('library_not_found', { provider: 'context7', path: 'resolve' }));
|
|
715
725
|
attempts.push(skippedAttempt('context7-docs'));
|
|
716
726
|
return frozenPathOutcome({
|
|
717
727
|
attempts,
|
|
@@ -562,8 +562,10 @@ export class SearchOrchestrator {
|
|
|
562
562
|
const collectionTruncated = providerTruncated || mainSourcesTruncated;
|
|
563
563
|
const visibleSourcesTruncated = visibleSources.length < prioritizedSources.length;
|
|
564
564
|
const sourcesTruncated = collectionTruncated || visibleSourcesTruncated;
|
|
565
|
-
if (
|
|
565
|
+
if (collectionTruncated)
|
|
566
566
|
warnings.push(warning('sources_truncated'));
|
|
567
|
+
if (visibleSourcesTruncated)
|
|
568
|
+
warnings.push(warning('visible_sources_truncated'));
|
|
567
569
|
if (mainSucceeded && rawAnswer.trim().length === 0 && prioritizedSources.length === 0) {
|
|
568
570
|
warnings.push(warning('no_results'));
|
|
569
571
|
}
|
|
@@ -3,7 +3,7 @@ import type { CanonicalSource, SourceRecordCandidate } from '../contracts/index.
|
|
|
3
3
|
import type { ProviderAttemptRecord, ProviderCapability, ProviderErrorKind } from '../provider-runtime/index.js';
|
|
4
4
|
import type { SearchApiResolvedSearchInput, SearchApiSearchResult } from '../providers/search-api.js';
|
|
5
5
|
import type { BoundedSourceProvider } from '../providers/types.js';
|
|
6
|
-
export declare const SEARCH_WARNING_CODES: readonly ["main_search_failed", "native_search_unconfirmed", "provider_failed", "provider_not_configured", "provider_result_truncated", "cache_stale", "cache_evicted", "answer_truncated", "sources_truncated", "canonical_output_truncated", "no_results"];
|
|
6
|
+
export declare const SEARCH_WARNING_CODES: readonly ["main_search_failed", "native_search_unconfirmed", "provider_failed", "provider_not_configured", "provider_result_truncated", "cache_stale", "cache_evicted", "answer_truncated", "sources_truncated", "visible_sources_truncated", "source_retention_truncated", "canonical_output_truncated", "no_results"];
|
|
7
7
|
export type SearchWarningCode = (typeof SEARCH_WARNING_CODES)[number];
|
|
8
8
|
/** Stable, secret-free warning; it deliberately has no arbitrary message field. */
|
|
9
9
|
export interface SearchWarning {
|
|
@@ -11,8 +11,11 @@ export declare function renderWebSearchLimitations(value: WebSearchOutput): stri
|
|
|
11
11
|
* Settings, caches, the clock, or network state.
|
|
12
12
|
*/
|
|
13
13
|
export declare function renderWebSearchText(value: WebSearchOutput): string;
|
|
14
|
+
export declare function isWebSearchModelTextTruncated(value: WebSearchOutput): boolean;
|
|
15
|
+
export declare function renderDocsSearchLimitations(value: DocsSearchOutput): string | undefined;
|
|
14
16
|
/** Pure Native projection for docs_search; it never reads Settings, cache, storage, or the network. */
|
|
15
17
|
export declare function renderDocsSearchText(value: DocsSearchOutput): string;
|
|
18
|
+
export declare function isDocsSearchModelTextTruncated(value: DocsSearchOutput): boolean;
|
|
16
19
|
/** Pure model-text projection for one private-storage source page. */
|
|
17
20
|
export declare function renderSearchSourcesText(value: SearchSourcesOutput): string;
|
|
18
21
|
/**
|
|
@@ -9,14 +9,19 @@ export function sourceDisplayLabel(source) {
|
|
|
9
9
|
return new URL(source.url).hostname;
|
|
10
10
|
}
|
|
11
11
|
function renderWithBoundedNotice(text, notice, maximumBytes) {
|
|
12
|
-
if (notice === undefined)
|
|
13
|
-
return truncateUtf8(text, maximumBytes).text;
|
|
14
12
|
const separator = '\n\n';
|
|
15
|
-
const
|
|
13
|
+
const complete = notice === undefined ? text : `${text}${separator}${notice}`;
|
|
14
|
+
if (utf8ByteLength(complete) <= maximumBytes)
|
|
15
|
+
return complete;
|
|
16
|
+
const marker = '[Model text truncated by model_text_max_bytes.]';
|
|
17
|
+
const suffix = notice === undefined
|
|
18
|
+
? marker
|
|
19
|
+
: `${marker}\nCounts below describe the canonical result, not fully displayed text.${separator}${notice}`;
|
|
20
|
+
const prefixBudget = maximumBytes - utf8ByteLength(suffix) - utf8ByteLength(separator);
|
|
16
21
|
if (prefixBudget <= 0)
|
|
17
|
-
return truncateUtf8(
|
|
22
|
+
return truncateUtf8(suffix, maximumBytes).text;
|
|
18
23
|
const prefix = truncateUtf8(text, prefixBudget).text;
|
|
19
|
-
return prefix.length === 0 ?
|
|
24
|
+
return prefix.length === 0 ? suffix : `${prefix}${separator}${suffix}`;
|
|
20
25
|
}
|
|
21
26
|
function sourceLoadingNotice(sourceRef) {
|
|
22
27
|
if (sourceRef === undefined)
|
|
@@ -45,7 +50,11 @@ function warningText(warning) {
|
|
|
45
50
|
case 'answer_truncated':
|
|
46
51
|
return 'The generated answer was truncated.';
|
|
47
52
|
case 'sources_truncated':
|
|
48
|
-
return '
|
|
53
|
+
return 'Source collection reached a Provider limit; uncollected sources cannot be recovered by pagination. Rerun with a larger collection limit where supported.';
|
|
54
|
+
case 'visible_sources_truncated':
|
|
55
|
+
return 'Only an inline source prefix is shown; use source_ref to page retained sources.';
|
|
56
|
+
case 'source_retention_truncated':
|
|
57
|
+
return 'Source storage dropped some collected sources at its count/byte limit; pagination cannot recover those entries.';
|
|
49
58
|
case 'canonical_output_truncated':
|
|
50
59
|
return 'The canonical search value was truncated to its configured limit.';
|
|
51
60
|
case 'no_results':
|
|
@@ -87,9 +96,8 @@ function sourceSummary(value) {
|
|
|
87
96
|
/** Shared model/card notice; Provider failures precede generic truncation information. */
|
|
88
97
|
export function renderWebSearchLimitations(value) {
|
|
89
98
|
const lines = value.warnings.map(warningText);
|
|
90
|
-
if (value.truncated)
|
|
91
|
-
lines.push('The
|
|
92
|
-
}
|
|
99
|
+
if (value.truncated && lines.length === 0)
|
|
100
|
+
lines.push('The canonical search result was bounded.');
|
|
93
101
|
if (lines.length === 0)
|
|
94
102
|
return undefined;
|
|
95
103
|
return `Limitations\n${lines.map(line => `- ${line}`).join('\n')}`;
|
|
@@ -104,15 +112,20 @@ export function renderWebSearchText(value) {
|
|
|
104
112
|
renderWebSearchLimitations(value),
|
|
105
113
|
answerSection(value),
|
|
106
114
|
sourceSection(value),
|
|
107
|
-
sourceSummary(value),
|
|
108
|
-
DISCOVERY_NOTICE,
|
|
109
115
|
].filter((section) => section !== undefined);
|
|
110
116
|
const complete = sections.join('\n\n');
|
|
111
117
|
const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes)
|
|
112
118
|
&& value.model_text_max_bytes > 0
|
|
113
119
|
? value.model_text_max_bytes
|
|
114
120
|
: 0;
|
|
115
|
-
return renderWithBoundedNotice(complete, sourceLoadingNotice(value.source_ref)
|
|
121
|
+
return renderWithBoundedNotice(complete, [sourceSummary(value), DISCOVERY_NOTICE, sourceLoadingNotice(value.source_ref)]
|
|
122
|
+
.filter((section) => section !== undefined).join('\n\n'), maximumBytes);
|
|
123
|
+
}
|
|
124
|
+
export function isWebSearchModelTextTruncated(value) {
|
|
125
|
+
const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes) && value.model_text_max_bytes > 0
|
|
126
|
+
? value.model_text_max_bytes : 0;
|
|
127
|
+
return utf8ByteLength(renderWebSearchText({ ...value, model_text_max_bytes: Number.MAX_SAFE_INTEGER }))
|
|
128
|
+
> maximumBytes;
|
|
116
129
|
}
|
|
117
130
|
function docsWarningText(warning) {
|
|
118
131
|
const subject = [warning.provider, warning.path].filter(Boolean).join('/');
|
|
@@ -131,8 +144,14 @@ function docsWarningText(warning) {
|
|
|
131
144
|
return `A documentation Provider bounded its discovery result${detail}.`;
|
|
132
145
|
case 'no_results':
|
|
133
146
|
return 'Documentation search completed without matching snippets or sources.';
|
|
134
|
-
case '
|
|
135
|
-
return '
|
|
147
|
+
case 'library_not_found':
|
|
148
|
+
return 'No Context7 library matched library_name. No unrelated library was selected; check the name or supply an exact library_id.';
|
|
149
|
+
case 'collection_limit_reached':
|
|
150
|
+
return `The Provider returned its requested limit (${warning.count ?? 'max_results'}). Counts cover this request, not all available matches. Increase max_results and rerun to collect more; pagination only reads retained records.`;
|
|
151
|
+
case 'visible_sources_truncated':
|
|
152
|
+
return 'Only an inline source prefix is shown; use source_ref to page retained sources.';
|
|
153
|
+
case 'source_retention_truncated':
|
|
154
|
+
return 'Source storage dropped some collected sources at its count/byte limit; pagination cannot recover those entries.';
|
|
136
155
|
case 'canonical_output_truncated':
|
|
137
156
|
return 'The documentation result was shortened to its configured canonical JSON limit.';
|
|
138
157
|
}
|
|
@@ -191,6 +210,12 @@ function docsSourceSection(value) {
|
|
|
191
210
|
}
|
|
192
211
|
return lines.join('\n');
|
|
193
212
|
}
|
|
213
|
+
export function renderDocsSearchLimitations(value) {
|
|
214
|
+
const lines = value.warnings.map(docsWarningText);
|
|
215
|
+
if (value.truncated && lines.length === 0)
|
|
216
|
+
lines.push('The canonical documentation result was bounded.');
|
|
217
|
+
return lines.length === 0 ? undefined : `Limitations\n${lines.map(line => `- ${line}`).join('\n')}`;
|
|
218
|
+
}
|
|
194
219
|
/** Pure Native projection for docs_search; it never reads Settings, cache, storage, or the network. */
|
|
195
220
|
export function renderDocsSearchText(value) {
|
|
196
221
|
const status = value.state === 'partial' ? 'partial' : 'complete';
|
|
@@ -201,35 +226,34 @@ export function renderDocsSearchText(value) {
|
|
|
201
226
|
: 'Documentation discovery completed; bounded discovery results follow.';
|
|
202
227
|
const sections = [
|
|
203
228
|
`Documentation search (${status})\n${explanation}`,
|
|
229
|
+
renderDocsSearchLimitations(value),
|
|
204
230
|
docsLibrarySection(value),
|
|
205
231
|
docsSnippetSection(value),
|
|
206
232
|
docsSourceSection(value),
|
|
207
|
-
[
|
|
208
|
-
`Sources shown: ${value.returned_sources}/${value.total_sources}`,
|
|
209
|
-
`Snippets shown: ${value.returned_snippets}/${value.total_snippets}`,
|
|
210
|
-
].join('\n'),
|
|
211
233
|
[
|
|
212
234
|
docsCachePathText('Context7 resolve cache', value.cache.resolve),
|
|
213
235
|
docsCachePathText('Context7 docs cache', value.cache.docs),
|
|
214
236
|
docsProviderSection(value),
|
|
215
237
|
].join('\n'),
|
|
216
|
-
value.truncated || value.warnings.length > 0
|
|
217
|
-
? [
|
|
218
|
-
'Limitations',
|
|
219
|
-
...(value.truncated
|
|
220
|
-
? ['- Documentation snippets, visible sources, Provider collection, or retained sources were bounded.']
|
|
221
|
-
: []),
|
|
222
|
-
...value.warnings.map(warning => `- ${docsWarningText(warning)}`),
|
|
223
|
-
].join('\n')
|
|
224
|
-
: undefined,
|
|
225
|
-
'Evidence level: discovery. Documentation snippets and source summaries are discovery metadata, not verified fetched page-body evidence; inspect an exact source page before relying on a claim.',
|
|
226
238
|
].filter((section) => section !== undefined);
|
|
227
|
-
const
|
|
239
|
+
const notice = [
|
|
240
|
+
`Sources shown: ${value.returned_sources}/${value.total_sources}`,
|
|
241
|
+
`Snippets shown: ${value.returned_snippets}/${value.total_snippets}`,
|
|
242
|
+
'Counts cover collected results for this request, not all available matches.',
|
|
243
|
+
'Evidence level: discovery. Documentation snippets and source summaries are not verified fetched page-body evidence.',
|
|
244
|
+
sourceLoadingNotice(value.source_ref),
|
|
245
|
+
].filter((section) => section !== undefined).join('\n\n');
|
|
228
246
|
const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes)
|
|
229
247
|
&& value.model_text_max_bytes > 0
|
|
230
248
|
? value.model_text_max_bytes
|
|
231
249
|
: 0;
|
|
232
|
-
return renderWithBoundedNotice(
|
|
250
|
+
return renderWithBoundedNotice(sections.join('\n\n'), notice, maximumBytes);
|
|
251
|
+
}
|
|
252
|
+
export function isDocsSearchModelTextTruncated(value) {
|
|
253
|
+
const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes) && value.model_text_max_bytes > 0
|
|
254
|
+
? value.model_text_max_bytes : 0;
|
|
255
|
+
return utf8ByteLength(renderDocsSearchText({ ...value, model_text_max_bytes: Number.MAX_SAFE_INTEGER }))
|
|
256
|
+
> maximumBytes;
|
|
233
257
|
}
|
|
234
258
|
function renderPageSources(page) {
|
|
235
259
|
if (page.sources.length === 0)
|
|
@@ -261,13 +285,15 @@ export function renderSearchSourcesText(value) {
|
|
|
261
285
|
const sections = [
|
|
262
286
|
renderPageSources(value),
|
|
263
287
|
[
|
|
264
|
-
`Page returned: ${value.returned}; retained total: ${value.total};
|
|
288
|
+
`Page returned: ${value.returned}; retained total: ${value.total}; collected total before storage: ${value.total_before_retention}.`,
|
|
265
289
|
value.has_more && value.next_offset !== undefined
|
|
266
290
|
? `More sources are available at offset ${value.next_offset}.`
|
|
267
291
|
: 'No later retained source page is available.',
|
|
268
|
-
value.
|
|
269
|
-
? '
|
|
270
|
-
:
|
|
292
|
+
value.total < value.total_before_retention
|
|
293
|
+
? 'Source storage dropped collected entries; pagination cannot recover them.'
|
|
294
|
+
: value.truncated
|
|
295
|
+
? 'Upstream source collection was bounded; pagination cannot recover uncollected entries.'
|
|
296
|
+
: undefined,
|
|
271
297
|
value.page_byte_limited
|
|
272
298
|
? 'This page was shortened by its canonical JSON byte limit.'
|
|
273
299
|
: undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, } from '../documentation/index.js';
|
|
2
|
-
import { isWebExtractModelTextTruncated, isWebMapModelTextTruncated, renderWebSearchLimitations, } from './render.js';
|
|
2
|
+
import { isDocsSearchModelTextTruncated, isWebSearchModelTextTruncated, renderDocsSearchLimitations, isWebExtractModelTextTruncated, isWebMapModelTextTruncated, renderWebSearchLimitations, } from './render.js';
|
|
3
3
|
import { WEB_EXTRACT_EVIDENCE_LEVELS, WEB_EXTRACT_ROUTES, } from '../web-extract/types.js';
|
|
4
4
|
function isRecord(value) {
|
|
5
5
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -49,7 +49,7 @@ export function webSearchPresentationMeta(_args, value) {
|
|
|
49
49
|
source_produced: value.source_ref !== undefined,
|
|
50
50
|
...(answer === undefined ? {} : { answer }),
|
|
51
51
|
sources: value.sources.map(source => ({ ...source })),
|
|
52
|
-
truncated: value.truncated,
|
|
52
|
+
truncated: value.truncated || isWebSearchModelTextTruncated(value),
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
/** Generic pending web-search intent whose title is exactly the query. */
|
|
@@ -99,17 +99,19 @@ export function presentWebSearchResult(args, result) {
|
|
|
99
99
|
* durable disclosure recovery; cache paths, credentials, and source_ref stay out.
|
|
100
100
|
*/
|
|
101
101
|
export function docsSearchPresentationMeta(_args, value) {
|
|
102
|
+
const limitations = renderDocsSearchLimitations(value);
|
|
102
103
|
return {
|
|
103
104
|
version: 1,
|
|
104
105
|
type: 'docs_search',
|
|
105
106
|
source_produced: value.source_ref !== undefined,
|
|
107
|
+
...(limitations === undefined ? {} : { limitations }),
|
|
106
108
|
sources: value.sources.map(source => ({ ...source })),
|
|
107
109
|
snippets: value.snippets.map(snippet => ({ ...snippet })),
|
|
108
110
|
cache: {
|
|
109
111
|
resolve: { ...value.cache.resolve },
|
|
110
112
|
docs: { ...value.cache.docs },
|
|
111
113
|
},
|
|
112
|
-
truncated: value.truncated,
|
|
114
|
+
truncated: value.truncated || isDocsSearchModelTextTruncated(value),
|
|
113
115
|
};
|
|
114
116
|
}
|
|
115
117
|
/** Pending documentation discovery intent; snippets are not presented as fetched pages. */
|
|
@@ -155,6 +157,7 @@ function parseDocsSearchMeta(value) {
|
|
|
155
157
|
|| value.version !== 1
|
|
156
158
|
|| value.type !== 'docs_search'
|
|
157
159
|
|| typeof value.truncated !== 'boolean'
|
|
160
|
+
|| !isOptionalString(value.limitations)
|
|
158
161
|
|| !Array.isArray(value.snippets)
|
|
159
162
|
|| !isRecord(value.cache))
|
|
160
163
|
return undefined;
|
|
@@ -173,6 +176,7 @@ function parseDocsSearchMeta(value) {
|
|
|
173
176
|
return {
|
|
174
177
|
version: 1,
|
|
175
178
|
type: 'docs_search',
|
|
179
|
+
...(value.limitations === undefined ? {} : { limitations: value.limitations }),
|
|
176
180
|
sources,
|
|
177
181
|
snippets,
|
|
178
182
|
cache: { resolve, docs },
|
|
@@ -193,8 +197,11 @@ function docsCacheLabel(meta) {
|
|
|
193
197
|
}
|
|
194
198
|
function docsCardAnswer(meta) {
|
|
195
199
|
if (meta.snippets.length === 0)
|
|
196
|
-
return
|
|
197
|
-
const lines = [
|
|
200
|
+
return meta.limitations;
|
|
201
|
+
const lines = [
|
|
202
|
+
...(meta.limitations === undefined ? [] : [meta.limitations, '']),
|
|
203
|
+
'Documentation snippets (discovery metadata; not fetched page bodies):',
|
|
204
|
+
];
|
|
198
205
|
for (let index = 0; index < meta.snippets.length; index += 1) {
|
|
199
206
|
const snippet = meta.snippets[index];
|
|
200
207
|
if (snippet === undefined)
|
|
@@ -216,7 +223,10 @@ export function presentDocsSearchResult(args, result) {
|
|
|
216
223
|
const label = docsCacheLabel(meta);
|
|
217
224
|
const title = `Docs${label === undefined ? '' : ` (${label})`}: ${args.query}`;
|
|
218
225
|
if (meta.sources.length === 0 && meta.snippets.length === 0) {
|
|
219
|
-
|
|
226
|
+
const reason = meta.cache.docs.reason === 'library_not_found'
|
|
227
|
+
? 'No matching Context7 library; check library_name or supply library_id'
|
|
228
|
+
: 'No documentation results';
|
|
229
|
+
return { card: 'generic', title: `${reason}: ${args.query}` };
|
|
220
230
|
}
|
|
221
231
|
const answer = docsCardAnswer(meta);
|
|
222
232
|
return {
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
export const TOOL_DISCOVERY_GUIDANCE = [
|
|
2
|
-
'
|
|
3
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'Use HTTPS URLs directly when available. HTTP 200 with only a title or an app shell is not usable page-body evidence; choose a suitable enabled extraction Provider explicitly rather than repeatedly fetching the same shell. Do not bypass URL safety checks.',
|
|
7
|
-
'Use search_tools({ capabilities: ["sources"] }) only when a source_ref needs pagination, or search_tools({ capabilities: ["site_map"] }) to discover URLs under a known website. Do not load optional tools preemptively.',
|
|
8
|
-
'Newly loaded tools become available on the next model step. Call search_sources or web_map directly using the tool definitions supplied by the host; already loaded tools stay available.',
|
|
9
|
-
'Search source metadata and mapped URLs are discovery, not verified webpage-body evidence.',
|
|
2
|
+
'For docs_search, use known library identity for library-specific queries; omit it for broad/cross-project or unknown-library discovery (auto uses Exa). Never guess a Context7 library.',
|
|
3
|
+
'Read webpage bodies with web_extract, choosing exactly one enabled Provider; use host web_fetch (if available) for structured responses such as raw JSON APIs or XML/Atom.',
|
|
4
|
+
'Prefer HTTPS. A title/app shell alone is not usable page-body evidence; choose another suitable enabled extraction Provider instead of refetching the same shell. Never bypass URL safety checks.',
|
|
5
|
+
'Load sources via search_tools for source_ref pagination, or site_map for URLs under a known site. Call newly loaded search_sources/web_map directly on the next model step; reuse loaded tools and do not load preemptively.',
|
|
10
6
|
].join('\n');
|
|
11
7
|
export const EVIDENCE_DISCIPLINE_GUIDANCE = [
|
|
12
|
-
'For current
|
|
13
|
-
'
|
|
14
|
-
'Before
|
|
8
|
+
'For current/external facts, start with one focused web_search (docs_search for SDK/API docs); do not inspect local files/settings/sessions/credentials unless the user explicitly asks about local state.',
|
|
9
|
+
'Search answers/snippets, source metadata and mapped URLs are discovery, not claim-level evidence.',
|
|
10
|
+
'Before decisive factual/causal conclusions, inspect authoritative URLs with web_extract; label unestablished mechanisms as inference or unconfirmed, never source-stated fact.',
|
|
15
11
|
].join('\n');
|
|
16
12
|
/** Register deterministic guidance that never reads Agent state or tool visibility. */
|
|
17
13
|
export function registerToolDiscoveryGuidance(ctx) {
|
|
@@ -185,7 +185,17 @@ export function selectContext7Library(libraries, libraryName, query = libraryNam
|
|
|
185
185
|
library,
|
|
186
186
|
score: context7LibraryScore(library, libraryName, queryTerms),
|
|
187
187
|
}))
|
|
188
|
-
.filter(
|
|
188
|
+
.filter(({ library }) => {
|
|
189
|
+
if (!isContext7LibraryId(library.id))
|
|
190
|
+
return false;
|
|
191
|
+
const wanted = normalizedMatchText(libraryName);
|
|
192
|
+
if (wanted.length === 0)
|
|
193
|
+
return false;
|
|
194
|
+
const segments = library.id.split('/').filter(Boolean).map(normalizedMatchText);
|
|
195
|
+
return normalizedMatchText(library.title) === wanted
|
|
196
|
+
|| segments[1] === wanted
|
|
197
|
+
|| segments.slice(0, 2).join('') === wanted;
|
|
198
|
+
})
|
|
189
199
|
.sort((left, right) => right.score - left.score || left.index - right.index)[0]?.library;
|
|
190
200
|
}
|
|
191
201
|
/** Parse JSON or plain-text Context7 v2 documentation bodies exactly once and bound every snippet. */
|
package/lib/tools/docs-search.js
CHANGED
|
@@ -58,8 +58,9 @@ function ensureWarning(warnings, code) {
|
|
|
58
58
|
export function projectDocsSearchOutput(result, config, commit) {
|
|
59
59
|
const warnings = result.warnings.map(outputWarning);
|
|
60
60
|
const storageTruncated = commit?.record.truncated === true;
|
|
61
|
-
if (
|
|
62
|
-
ensureWarning(warnings, '
|
|
61
|
+
if (commit !== undefined && commit.record.sources.length < commit.record.totalBeforeRetention) {
|
|
62
|
+
ensureWarning(warnings, 'source_retention_truncated');
|
|
63
|
+
}
|
|
63
64
|
const sources = result.sources.map(source => ({
|
|
64
65
|
url: source.url,
|
|
65
66
|
...(source.title === undefined ? {} : { title: source.title }),
|
package/lib/tools/schemas.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export declare const WEB_SEARCH_OUTPUT_SCHEMA: {
|
|
|
98
98
|
readonly truncated: {
|
|
99
99
|
readonly type: "boolean";
|
|
100
100
|
readonly required: true;
|
|
101
|
+
readonly description: "Canonical result/storage truncation. Native model-text byte clipping is separately marked in rendered text and presentation metadata.";
|
|
101
102
|
};
|
|
102
103
|
readonly evidence_level: {
|
|
103
104
|
readonly type: "string";
|
|
@@ -111,7 +112,7 @@ export declare const WEB_SEARCH_OUTPUT_SCHEMA: {
|
|
|
111
112
|
readonly properties: {
|
|
112
113
|
readonly code: {
|
|
113
114
|
readonly type: "string";
|
|
114
|
-
readonly enum: readonly ["main_search_failed", "native_search_unconfirmed", "provider_failed", "provider_not_configured", "provider_result_truncated", "cache_stale", "cache_evicted", "answer_truncated", "sources_truncated", "canonical_output_truncated", "no_results"];
|
|
115
|
+
readonly enum: readonly ["main_search_failed", "native_search_unconfirmed", "provider_failed", "provider_not_configured", "provider_result_truncated", "cache_stale", "cache_evicted", "answer_truncated", "sources_truncated", "visible_sources_truncated", "source_retention_truncated", "canonical_output_truncated", "no_results"];
|
|
115
116
|
readonly required: true;
|
|
116
117
|
readonly description: "Stable warning category.";
|
|
117
118
|
};
|
|
@@ -167,14 +168,14 @@ export declare const DOCS_SEARCH_PARAMETERS: {
|
|
|
167
168
|
readonly type: "integer";
|
|
168
169
|
readonly enum: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
|
|
169
170
|
readonly default: 6;
|
|
170
|
-
readonly description: "
|
|
171
|
+
readonly description: "Per-Provider collection and inline limit (1-20; default 6, capped by Settings). Not an exhaustive total: pagination only reads collected/retained sources. Increase this and rerun to collect more.";
|
|
171
172
|
};
|
|
172
173
|
readonly force_refresh: {
|
|
173
174
|
readonly type: "boolean";
|
|
174
175
|
readonly description: "Bypass fresh Context7 cache entries and request current Provider data.";
|
|
175
176
|
};
|
|
176
177
|
};
|
|
177
|
-
export declare const DOCS_SEARCH_WARNING_CODES: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "no_results", "
|
|
178
|
+
export declare const DOCS_SEARCH_WARNING_CODES: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "library_not_found", "collection_limit_reached", "visible_sources_truncated", "no_results", "source_retention_truncated", "canonical_output_truncated"];
|
|
178
179
|
/** Canonical high-level documentation value shared exactly by Native and Code Mode. */
|
|
179
180
|
export declare const DOCS_SEARCH_OUTPUT_SCHEMA: {
|
|
180
181
|
readonly type: "object";
|
|
@@ -347,6 +348,7 @@ export declare const DOCS_SEARCH_OUTPUT_SCHEMA: {
|
|
|
347
348
|
readonly truncated: {
|
|
348
349
|
readonly type: "boolean";
|
|
349
350
|
readonly required: true;
|
|
351
|
+
readonly description: "Canonical result/storage truncation. Native model-text byte clipping is separately marked in rendered text and presentation metadata.";
|
|
350
352
|
};
|
|
351
353
|
readonly evidence_level: {
|
|
352
354
|
readonly type: "string";
|
|
@@ -360,7 +362,7 @@ export declare const DOCS_SEARCH_OUTPUT_SCHEMA: {
|
|
|
360
362
|
readonly properties: {
|
|
361
363
|
readonly code: {
|
|
362
364
|
readonly type: "string";
|
|
363
|
-
readonly enum: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "no_results", "
|
|
365
|
+
readonly enum: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "library_not_found", "collection_limit_reached", "visible_sources_truncated", "no_results", "source_retention_truncated", "canonical_output_truncated"];
|
|
364
366
|
readonly required: true;
|
|
365
367
|
};
|
|
366
368
|
readonly provider: {
|
package/lib/tools/schemas.js
CHANGED
|
@@ -107,6 +107,7 @@ export const WEB_SEARCH_OUTPUT_SCHEMA = {
|
|
|
107
107
|
truncated: {
|
|
108
108
|
type: 'boolean',
|
|
109
109
|
required: true,
|
|
110
|
+
description: 'Canonical result/storage truncation. Native model-text byte clipping is separately marked in rendered text and presentation metadata.',
|
|
110
111
|
},
|
|
111
112
|
evidence_level: {
|
|
112
113
|
type: 'string',
|
|
@@ -157,7 +158,7 @@ export const DOCS_SEARCH_PARAMETERS = {
|
|
|
157
158
|
type: 'integer',
|
|
158
159
|
enum: DOCS_SEARCH_MAX_RESULTS_VALUES,
|
|
159
160
|
default: DOCS_SEARCH_DEFAULT_MAX_RESULTS,
|
|
160
|
-
description: '
|
|
161
|
+
description: 'Per-Provider collection and inline limit (1-20; default 6, capped by Settings). Not an exhaustive total: pagination only reads collected/retained sources. Increase this and rerun to collect more.',
|
|
161
162
|
},
|
|
162
163
|
force_refresh: {
|
|
163
164
|
type: 'boolean',
|
|
@@ -225,7 +226,7 @@ const documentationCacheSchema = {
|
|
|
225
226
|
};
|
|
226
227
|
export const DOCS_SEARCH_WARNING_CODES = [
|
|
227
228
|
...DOCUMENTATION_WARNING_CODES,
|
|
228
|
-
'
|
|
229
|
+
'source_retention_truncated',
|
|
229
230
|
'canonical_output_truncated',
|
|
230
231
|
];
|
|
231
232
|
const docsSearchWarningSchema = {
|
|
@@ -289,7 +290,10 @@ export const DOCS_SEARCH_OUTPUT_SCHEMA = {
|
|
|
289
290
|
returned_sources: { type: 'integer', required: true },
|
|
290
291
|
total_snippets: { type: 'integer', required: true },
|
|
291
292
|
returned_snippets: { type: 'integer', required: true },
|
|
292
|
-
truncated: {
|
|
293
|
+
truncated: {
|
|
294
|
+
type: 'boolean', required: true,
|
|
295
|
+
description: 'Canonical result/storage truncation. Native model-text byte clipping is separately marked in rendered text and presentation metadata.',
|
|
296
|
+
},
|
|
293
297
|
evidence_level: { type: 'string', const: 'discovery', required: true },
|
|
294
298
|
warnings: {
|
|
295
299
|
type: 'array',
|
package/lib/tools/web-search.js
CHANGED
|
@@ -19,17 +19,18 @@ function outputWarning(warning) {
|
|
|
19
19
|
...(warning.errorKind === undefined ? {} : { error_kind: warning.errorKind }),
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
if (!warnings.some(warning => warning.code === '
|
|
24
|
-
warnings.push({ code: '
|
|
22
|
+
function ensureRetentionWarning(warnings) {
|
|
23
|
+
if (!warnings.some(warning => warning.code === 'source_retention_truncated')) {
|
|
24
|
+
warnings.push({ code: 'source_retention_truncated' });
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
/** Project only ordinary visible fields; Provider/category routing stays internal. */
|
|
28
28
|
export function projectWebSearchOutput(result, config, commit) {
|
|
29
29
|
const warnings = result.canonical.warnings.map(outputWarning);
|
|
30
30
|
const storageTruncated = commit?.record.truncated === true;
|
|
31
|
-
if (
|
|
32
|
-
|
|
31
|
+
if (commit !== undefined && commit.record.sources.length < commit.record.totalBeforeRetention) {
|
|
32
|
+
ensureRetentionWarning(warnings);
|
|
33
|
+
}
|
|
33
34
|
const sources = result.canonical.sources.map(source => ({
|
|
34
35
|
url: source.url,
|
|
35
36
|
...(source.title === undefined ? {} : { title: source.title }),
|
|
@@ -30,6 +30,12 @@ export interface WebEditableConfig {
|
|
|
30
30
|
defaultProfile: string;
|
|
31
31
|
defaultDepth: string;
|
|
32
32
|
toolTimeoutMs: number;
|
|
33
|
+
retention: {
|
|
34
|
+
docsSearchMaxResults: number;
|
|
35
|
+
};
|
|
36
|
+
budgets: Record<string, Record<string, {
|
|
37
|
+
maxVisibleSources: number;
|
|
38
|
+
}>>;
|
|
33
39
|
toolDiscovery: {
|
|
34
40
|
mode: string;
|
|
35
41
|
};
|
|
@@ -76,6 +82,9 @@ export interface WebConfigOptions {
|
|
|
76
82
|
parallelSearchModes: readonly string[];
|
|
77
83
|
proxyUrlMaxCharacters: number;
|
|
78
84
|
supplementalSearchMaxSources: number;
|
|
85
|
+
docsSearchMinResults: number;
|
|
86
|
+
docsSearchMaxResults: number;
|
|
87
|
+
visibleSourcesMax: number;
|
|
79
88
|
}
|
|
80
89
|
export interface WebCredentialState {
|
|
81
90
|
ref: string;
|
|
@@ -18,6 +18,9 @@ export const WEB_EDITABLE_PATHS = [
|
|
|
18
18
|
['defaultProfile'],
|
|
19
19
|
['defaultDepth'],
|
|
20
20
|
['toolTimeoutMs'],
|
|
21
|
+
['retention', 'docsSearchMaxResults'],
|
|
22
|
+
...['auto', 'coding_docs', 'code_examples', 'project_research', 'academic', 'fact_check']
|
|
23
|
+
.flatMap(profile => ['compact', 'normal', 'deep'].map(depth => ['budgets', profile, depth, 'maxVisibleSources'])),
|
|
21
24
|
['toolDiscovery', 'mode'],
|
|
22
25
|
...WEB_SUPPLEMENTAL_SEARCH_PROVIDERS.map(provider => ['supplementalSearch', provider]),
|
|
23
26
|
['supplementalSearch', 'maxSourcesPerProvider'],
|
package/lib/web-config/host.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
-
import { Config as SearchEnhanceConfig, PARALLEL_SEARCH_MODES, SUPPLEMENTAL_SEARCH_MAX_SOURCES, SEARCH_API_PROTOCOLS, SEARCH_DEPTHS, SEARCH_PROFILES, THINKING_LEVELS, TOOL_DISCOVERY_MODES, WEB_EXTRACT_PROXY_URL_MAX_CHARACTERS, validateWebExtractProxyUrl, } from '../config.js';
|
|
2
|
+
import { Config as SearchEnhanceConfig, DOCS_SEARCH_DEFAULT_MAX_RESULTS, DOCS_SEARCH_MAX_RESULTS_LIMIT, PARALLEL_SEARCH_MODES, SUPPLEMENTAL_SEARCH_MAX_SOURCES, SEARCH_API_PROTOCOLS, SEARCH_DEPTHS, SEARCH_PROFILES, THINKING_LEVELS, TOOL_DISCOVERY_MODES, WEB_EXTRACT_PROXY_URL_MAX_CHARACTERS, validateWebExtractProxyUrl, } from '../config.js';
|
|
3
3
|
import { inspectDiagnosticStatus } from '../diagnostics/status.js';
|
|
4
4
|
import { WEB_BASE_URL_MAX_CHARACTERS, WEB_CONFIG_PATH, WEB_CREDENTIAL_REF_MAX_CHARACTERS, WEB_CREDENTIAL_SLOTS, WEB_CREDENTIAL_VALUE_MAX_CHARACTERS, WEB_CREDENTIALS_PATH, WEB_EDITABLE_PATHS, WEB_MODEL_MAX_CHARACTERS, } from './contracts.js';
|
|
5
5
|
export const WEB_CONFIG_REQUEST_MAX_BYTES = 64 * 1024;
|
|
@@ -59,6 +59,13 @@ function projectConfig(config) {
|
|
|
59
59
|
defaultProfile: config.defaultProfile,
|
|
60
60
|
defaultDepth: config.defaultDepth,
|
|
61
61
|
toolTimeoutMs: config.toolTimeoutMs,
|
|
62
|
+
retention: { docsSearchMaxResults: config.retention.docsSearchMaxResults },
|
|
63
|
+
budgets: Object.fromEntries(Object.entries(config.budgets).map(([profile, depths]) => [
|
|
64
|
+
profile,
|
|
65
|
+
Object.fromEntries(Object.entries(depths).map(([depth, budget]) => [
|
|
66
|
+
depth, { maxVisibleSources: budget.maxVisibleSources },
|
|
67
|
+
])),
|
|
68
|
+
])),
|
|
62
69
|
toolDiscovery: { mode: config.toolDiscovery.mode },
|
|
63
70
|
supplementalSearch: { ...config.supplementalSearch },
|
|
64
71
|
searchApi: {
|
|
@@ -232,6 +239,9 @@ export async function readWebConfigSnapshot(ctx, signal) {
|
|
|
232
239
|
parallelSearchModes: [...PARALLEL_SEARCH_MODES],
|
|
233
240
|
proxyUrlMaxCharacters: WEB_EXTRACT_PROXY_URL_MAX_CHARACTERS,
|
|
234
241
|
supplementalSearchMaxSources: SUPPLEMENTAL_SEARCH_MAX_SOURCES,
|
|
242
|
+
docsSearchMinResults: DOCS_SEARCH_DEFAULT_MAX_RESULTS,
|
|
243
|
+
docsSearchMaxResults: DOCS_SEARCH_MAX_RESULTS_LIMIT,
|
|
244
|
+
visibleSourcesMax: 1000,
|
|
235
245
|
},
|
|
236
246
|
credentials: Object.fromEntries(credentialEntries),
|
|
237
247
|
diagnostics: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-search-enhance",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Bounded search, documentation, extraction, and on-demand source pagination and site mapping for DeepSeek Harness",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -76,7 +76,9 @@
|
|
|
76
76
|
"cordis.patch.yml",
|
|
77
77
|
"README.md"
|
|
78
78
|
],
|
|
79
|
-
"sideEffects": [
|
|
79
|
+
"sideEffects": [
|
|
80
|
+
"./client/client.js"
|
|
81
|
+
],
|
|
80
82
|
"scripts": {
|
|
81
83
|
"clean": "node -e \"const { rmSync } = require('node:fs'); for (const path of ['lib', 'client']) rmSync(path, { recursive: true, force: true })\"",
|
|
82
84
|
"build": "npm run clean && tsc -p tsconfig.build.json && tsdown && tsc -p tsconfig.client.build.json",
|
|
@@ -122,14 +124,30 @@
|
|
|
122
124
|
"react": "^18.2.0"
|
|
123
125
|
},
|
|
124
126
|
"peerDependenciesMeta": {
|
|
125
|
-
"@deepseek-ai/dsh-client-locale": {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"@deepseek-ai/dsh-
|
|
132
|
-
|
|
127
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
128
|
+
"optional": true
|
|
129
|
+
},
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-primitives": {
|
|
131
|
+
"optional": true
|
|
132
|
+
},
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-settings": {
|
|
134
|
+
"optional": true
|
|
135
|
+
},
|
|
136
|
+
"@deepseek-ai/dsh-client-ui-renderer": {
|
|
137
|
+
"optional": true
|
|
138
|
+
},
|
|
139
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": {
|
|
140
|
+
"optional": true
|
|
141
|
+
},
|
|
142
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
143
|
+
"optional": true
|
|
144
|
+
},
|
|
145
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
146
|
+
"optional": true
|
|
147
|
+
},
|
|
148
|
+
"react": {
|
|
149
|
+
"optional": true
|
|
150
|
+
}
|
|
133
151
|
},
|
|
134
152
|
"dependencies": {
|
|
135
153
|
"@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
|