@deepseek-ai/dsh-web 0.0.1-rc.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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +61 -0
- package/README.zh.md +61 -0
- package/lib/index.js +143 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +92 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/types.d.ts +125 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/web/web/README.md
|
|
5
|
+
README.md: 2acb5022a83c08c33a26be626a93b7355c07a0ed
|
|
6
|
+
README.zh.md: b77b0a897082dba01e10be3f7b0ca3b3f0260a9f
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-web
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The **`WebService`** (`ctx.web`) defines WHAT web access the harness has — search the web, fetch a URL — over multiple providers, without binding the model contract to one vendor's API shape.
|
|
6
|
+
|
|
7
|
+
This package owns the Service Definition role of the web capability. Unlike bash/fs it spans two operations (search and fetch) on one seam, with potentially multiple providers each:
|
|
8
|
+
|
|
9
|
+
| Package | Role |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `@deepseek-ai/dsh-web` (this) | Service Definition: the service, provider registries, selection policy, request/result vocabulary, the `WebError` taxonomy |
|
|
12
|
+
| `@deepseek-ai/dsh-web-search-exa` | Search provider: Exa |
|
|
13
|
+
| `@deepseek-ai/dsh-web-search-perplexity` | Search provider: Perplexity |
|
|
14
|
+
| `@deepseek-ai/dsh-web-fetch-local` | Fetch provider: anonymous public HTTP(S) |
|
|
15
|
+
| `@deepseek-ai/dsh-tool-web` | Consumer: the model-facing `web_search` / `web_fetch` tool schemas over `ctx.web` |
|
|
16
|
+
|
|
17
|
+
Search and fetch share no request schema and no business logic, but they are deliberately one seam: `ctx.web` is a single web-access middle layer with one provider-selection policy owner, one abort/error vocabulary, and one product-facing "how this harness reaches the web" config surface. The `Search`/`Fetch` method pairs are deliberately parallel.
|
|
18
|
+
|
|
19
|
+
## Service API (`ctx.web`)
|
|
20
|
+
|
|
21
|
+
| Member | Semantics |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `registerSearchProvider(provider)` / `registerFetchProvider(provider)` | Register a backend. Throws `WebError` `WEB_DUPLICATE_PROVIDER` on a duplicate id within that capability kind. Returns a disposer. Disposed with the calling fiber. |
|
|
24
|
+
| `search(request, signal?)` | Resolve the search provider and run one search. Enforces `request.maxResults` on the result (truncates `sources[]`, sets `truncated`). Throws `WebError` when the capability cannot run. |
|
|
25
|
+
| `fetch(request, signal?)` | Resolve the fetch provider and retrieve one URL. A non-2xx response is a result, not a throw. Throws `WebError` for failures to safely retrieve or represent the resource. |
|
|
26
|
+
|
|
27
|
+
Providers register **capabilities**, not tools. `dsh-tool-web` is the only owner of model-facing names, descriptions, prompt guidance, JSON schemas, and presentation.
|
|
28
|
+
|
|
29
|
+
## Selection
|
|
30
|
+
|
|
31
|
+
Selection never depends on registration, config, or HMR order. A capability has an explicit provider id (config `searchProvider`/`fetchProvider`, or env `$DSH_WEB_SEARCH_PROVIDER`/`$DSH_WEB_FETCH_PROVIDER` feeding the same fields), or auto-selects when exactly one usable provider is registered. `search()`/`fetch()` resolve the provider at execution time:
|
|
32
|
+
|
|
33
|
+
| Situation | Execution |
|
|
34
|
+
|---|---|
|
|
35
|
+
| configured id registered and `available()` | runs that provider |
|
|
36
|
+
| configured id not registered | `WEB_PROVIDER_CONFIGURED_MISSING` |
|
|
37
|
+
| configured id registered but unavailable | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
|
|
38
|
+
| no id, exactly one registered usable provider | runs it |
|
|
39
|
+
| no id, no usable provider | `WEB_PROVIDER_UNAVAILABLE` |
|
|
40
|
+
| no id, multiple usable providers | `WEB_PROVIDER_AMBIGUOUS` |
|
|
41
|
+
|
|
42
|
+
The failure branches throw `WebError`, whose structured code (plus message detail — the missing id, the ambiguous candidate set) is the surface callers route on. A provider's own `available()` is a cheap local check (credential presence, parseable config) that feeds this execution-time selection and **must not make network calls**; `dsh-tool-web` never calls it — the tool executes through `ctx.web.search()`/`fetch()` and routes on the thrown codes, so provider selection has one owner.
|
|
43
|
+
|
|
44
|
+
## Vocabulary
|
|
45
|
+
|
|
46
|
+
`WebSearchRequest` (`query`, `maxResults?`) → `WebSearchResult` (`content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`) → `WebFetchResult` (final `url`, `statusCode`, `body`, `truncated`); cancellation is a direct optional `AbortSignal` argument to `search()`/`fetch()`. `WebFetchBody` is a CLOSED discriminated union (`html` | `text`) owned here — consumers `switch` to exhaustiveness so a new kind breaks their compilation until handled. See `src/types.ts` for the full contracts and the `WebError` code taxonomy.
|
|
47
|
+
|
|
48
|
+
## Model Experience
|
|
49
|
+
|
|
50
|
+
Indirectly, through `dsh-tool-web`, which retains bounded normalized provider data or the exact configured-provider, unavailable-provider, no-provider, multiple-provider, and `Error: <message>` failures while this registry contributes no prompt or schema itself.
|
|
51
|
+
|
|
52
|
+
#### KV Cache effect
|
|
53
|
+
|
|
54
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
55
|
+
|
|
56
|
+
## Known Limitations and Deferred Work
|
|
57
|
+
|
|
58
|
+
- **No observation surface** — no provider-change event and no capability-status query; availability is observed only by executing `search()`/`fetch()` and routing the thrown `WebError` codes, and the no-provider failure is the generic `WEB_PROVIDER_UNAVAILABLE` with no per-provider reason enumeration ([Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)).
|
|
59
|
+
- **`WebSearchRequest` carries only `query` + `maxResults`** — provider-neutral controls (recency, domain filters, regional hints, search depth) are deferred until Exa and Perplexity can both honor them honestly ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)).
|
|
60
|
+
- **`WebFetchBody` has no `pdf` arm** — text-extractable PDF support is named deferred work; the closed union makes adding it a compile-enforced change across the three web packages.
|
|
61
|
+
- **Provider-backed page extraction is out of scope of `fetch()`** — a Firecrawl/Tavily-style `web_extract` capability is deferred rather than widening the fetch operation.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-web
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
**`WebService`**(`ctx.web`)定义 harness 具备哪些 web 访问能力(搜索 web、抓取 URL),并通过多个提供方实现,不把模型约定绑定到某个厂商的 API 形状。
|
|
6
|
+
|
|
7
|
+
本包承担 web 能力的 Service Definition 角色。与 bash/fs 不同,它在一个 seam 上跨越搜索与抓取两种操作,每种操作都可能有多个提供方:
|
|
8
|
+
|
|
9
|
+
| 包 | 职责 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `@deepseek-ai/dsh-web`(本包) | Service Definition:服务、提供方注册表、选择策略、请求/结果词汇、`WebError` 分类体系 |
|
|
12
|
+
| `@deepseek-ai/dsh-web-search-exa` | 搜索提供方:Exa |
|
|
13
|
+
| `@deepseek-ai/dsh-web-search-perplexity` | 搜索提供方:Perplexity |
|
|
14
|
+
| `@deepseek-ai/dsh-web-fetch-local` | 抓取提供方:匿名公共 HTTP(S) |
|
|
15
|
+
| `@deepseek-ai/dsh-tool-web` | Consumer:面向模型的 `web_search`/`web_fetch` 工具 schema,构建于 `ctx.web` 之上 |
|
|
16
|
+
|
|
17
|
+
搜索与抓取没有共享请求 schema 或业务逻辑,但有意共用一个 seam:`ctx.web` 是单一 web 访问中间层,拥有一项提供方选择策略、一套中止/错误词汇和一个面向产品的「该 harness 如何访问 web」配置接口。成对的 `Search`/`Fetch` 方法保持并行是有意为之。
|
|
18
|
+
|
|
19
|
+
## 服务 API(`ctx.web`)
|
|
20
|
+
|
|
21
|
+
| 成员 | 语义 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `registerSearchProvider(provider)`/`registerFetchProvider(provider)` | 注册后端。同一能力类型下 id 重复时抛出 `WebError` `WEB_DUPLICATE_PROVIDER`。返回 disposer。随调用 fiber 一并 dispose(资源释放)。 |
|
|
24
|
+
| `search(request, signal?)` | 解析搜索提供方并运行一次搜索。在结果上强制执行 `request.maxResults`(截断 `sources[]`,设置 `truncated`)。能力无法运行时抛出 `WebError`。 |
|
|
25
|
+
| `fetch(request, signal?)` | 解析抓取提供方并获取一个 URL。非 2xx 响应是结果,不会抛出异常。无法安全获取或表示资源时抛出 `WebError`。 |
|
|
26
|
+
|
|
27
|
+
提供方注册的是**能力**而非工具。`dsh-tool-web` 是面向模型的名称、描述、提示词指引、JSON Schema 和呈现的唯一归属方。
|
|
28
|
+
|
|
29
|
+
## 选择
|
|
30
|
+
|
|
31
|
+
选择绝不依赖注册、配置或热模块替换(HMR)顺序。能力要么具有显式提供方 id(配置 `searchProvider`/`fetchProvider`,或由环境变量 `$DSH_WEB_SEARCH_PROVIDER`/`$DSH_WEB_FETCH_PROVIDER` 提供相同字段),要么在恰好只注册一个可用提供方时自动选择。`search()`/`fetch()` 会在执行时解析提供方:
|
|
32
|
+
|
|
33
|
+
| 情况 | 执行 |
|
|
34
|
+
|---|---|
|
|
35
|
+
| 已配置 id 已注册且 `available()` | 运行该提供方 |
|
|
36
|
+
| 已配置 id 未注册 | `WEB_PROVIDER_CONFIGURED_MISSING` |
|
|
37
|
+
| 已配置 id 已注册但不可用 | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
|
|
38
|
+
| 无 id,恰好一个已注册的可用提供方 | 运行该提供方 |
|
|
39
|
+
| 无 id,没有可用提供方 | `WEB_PROVIDER_UNAVAILABLE` |
|
|
40
|
+
| 无 id,多个可用提供方 | `WEB_PROVIDER_AMBIGUOUS` |
|
|
41
|
+
|
|
42
|
+
失败分支会抛出 `WebError`;调用方按其结构化 code(加消息细节:缺失 id、歧义候选集合)路由。提供方自身的 `available()` 是便宜的局部检查(凭据是否存在、配置是否可解析),供执行时选择使用,且**禁止发起网络调用**;`dsh-tool-web` 永远不会调用它。工具通过 `ctx.web.search()`/`fetch()` 执行,并按抛出的 code 路由,因此提供方选择只有一个归属方。
|
|
43
|
+
|
|
44
|
+
## 词汇
|
|
45
|
+
|
|
46
|
+
`WebSearchRequest`(`query`、`maxResults?`)→ `WebSearchResult`(`content?`、`sources[]`、`truncated`);每个 `WebSearchSource` 都有必填 `url` 与可选 `title`/`snippet`/`publishedAt`(Perplexity 引用可能只含 URL)。`WebFetchRequest`(`url`)→ `WebFetchResult`(最终 `url`、`statusCode`、`body`、`truncated`);取消作为可选的直接 `AbortSignal` 参数传给 `search()`/`fetch()`。`WebFetchBody` 是这里拥有的封闭判别联合(`html` | `text`);消费方使用 `switch` 实现穷尽检查,因此新增类型会导致编译失败,直到处理完毕。完整约定见 `src/types.ts`,其中也包含 `WebError` code 分类体系。
|
|
47
|
+
|
|
48
|
+
## 模型体验
|
|
49
|
+
|
|
50
|
+
通过 `dsh-tool-web` 间接影响;该工具会保留有界的规范化提供方数据,或者原样保留以下失败:已配置的提供方缺失、提供方不可用、无提供方、存在多个提供方以及 `Error: <message>`;本注册表自身不贡献提示词或 schema。
|
|
51
|
+
|
|
52
|
+
#### KV Cache 影响
|
|
53
|
+
|
|
54
|
+
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
|
|
55
|
+
|
|
56
|
+
## 已知限制与暂缓事项
|
|
57
|
+
|
|
58
|
+
- **没有观测接口**:没有提供方变更事件或能力状态查询;可用性只能通过执行 `search()`/`fetch()` 并按抛出的 `WebError` code 路由来观测,无提供方失败是通用的 `WEB_PROVIDER_UNAVAILABLE`,不会枚举逐提供方原因(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md))。
|
|
59
|
+
- **`WebSearchRequest` 只携带 `query` + `maxResults`**:提供方无关的控制项(新近程度、域名过滤条件、区域提示、搜索深度)暂缓至 Exa 与 Perplexity 都能诚实支持时(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。
|
|
60
|
+
- **`WebFetchBody` 没有 `pdf` 分支**:可提取文本的 PDF 支持属于明确的暂缓工作;封闭联合会使新增该分支成为三个 web 包中由编译强制执行的变更。
|
|
61
|
+
- **提供方支持的页面提取不属于 `fetch()` 范围**:Firecrawl/Tavily 风格的 `web_extract` 能力暂缓,而不会扩展抓取操作。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
//#region lib/types/types.js
|
|
5
|
+
/**
|
|
6
|
+
* Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
|
|
7
|
+
* seam so provider selection, cancellation, errors, and product configuration have one owner,
|
|
8
|
+
* while retaining separate request and result types.
|
|
9
|
+
* @module @deepseek-ai/dsh-web/types
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Typed web error with a machine-routable, open-string `code` and chained `cause`.
|
|
13
|
+
* Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
|
|
14
|
+
* missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
|
|
15
|
+
* the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
|
|
16
|
+
* size and timeout limits, and unsupported content types. Tool execution exposes the code in
|
|
17
|
+
* structured error metadata.
|
|
18
|
+
*/
|
|
19
|
+
var WebError = class extends HarnessError {};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region lib/types/index.js
|
|
22
|
+
/**
|
|
23
|
+
* Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
|
|
24
|
+
* fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
|
|
25
|
+
* be usable; without one, exactly one usable provider is required, so selection never depends
|
|
26
|
+
* on registration order.
|
|
27
|
+
* @module @deepseek-ai/dsh-web
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The web access service. Registered as `ctx.web` (one instance per context).
|
|
31
|
+
*
|
|
32
|
+
* Selection semantics (resolved at execution time, never order-dependent):
|
|
33
|
+
* - A configured id that is registered and `available()` → that provider.
|
|
34
|
+
* - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
35
|
+
* - A configured id registered but unavailable →
|
|
36
|
+
* `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
37
|
+
* - No id configured, exactly one registered usable provider → that provider.
|
|
38
|
+
* - No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
|
|
39
|
+
* - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
|
|
40
|
+
*/
|
|
41
|
+
var WebService = class extends Service {
|
|
42
|
+
/**
|
|
43
|
+
* Provider selection config. Operational env overrides feed the SAME fields:
|
|
44
|
+
* `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
|
|
45
|
+
* `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
|
|
46
|
+
*/
|
|
47
|
+
static Config = z.object({
|
|
48
|
+
searchProvider: z.string(),
|
|
49
|
+
fetchProvider: z.string()
|
|
50
|
+
});
|
|
51
|
+
searchProviders = /* @__PURE__ */ new Map();
|
|
52
|
+
fetchProviders = /* @__PURE__ */ new Map();
|
|
53
|
+
searchProviderId;
|
|
54
|
+
fetchProviderId;
|
|
55
|
+
constructor(ctx, config = {}) {
|
|
56
|
+
super(ctx, "web");
|
|
57
|
+
this.searchProviderId = config.searchProvider ?? process.env.DSH_WEB_SEARCH_PROVIDER;
|
|
58
|
+
this.fetchProviderId = config.fetchProvider ?? process.env.DSH_WEB_FETCH_PROVIDER;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
62
|
+
* if its id is already registered for search. Returns a disposer; disposed
|
|
63
|
+
* with the calling fiber.
|
|
64
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
65
|
+
* @returns the disposer that unregisters the provider.
|
|
66
|
+
*/
|
|
67
|
+
registerSearchProvider(provider) {
|
|
68
|
+
return this.registerProvider(this.searchProviders, provider);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
72
|
+
* if its id is already registered for fetch. Returns a disposer; disposed
|
|
73
|
+
* with the calling fiber.
|
|
74
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
75
|
+
* @returns the disposer that unregisters the provider.
|
|
76
|
+
*/
|
|
77
|
+
registerFetchProvider(provider) {
|
|
78
|
+
return this.registerProvider(this.fetchProviders, provider);
|
|
79
|
+
}
|
|
80
|
+
registerProvider(store, provider) {
|
|
81
|
+
if (store.has(provider.id)) throw new WebError(`a web provider with id "${provider.id}" is already registered`, "WEB_DUPLICATE_PROVIDER");
|
|
82
|
+
const dispose = this.ctx.effect(function* () {
|
|
83
|
+
store.set(provider.id, provider);
|
|
84
|
+
yield () => store.delete(provider.id);
|
|
85
|
+
}, "web.registerProvider()");
|
|
86
|
+
return () => void dispose();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run one search through the selected provider. Resolves the provider at call
|
|
90
|
+
* time with the selection rules above; throws {@link WebError} when the
|
|
91
|
+
* capability cannot run. The seam enforces `request.maxResults` on the result:
|
|
92
|
+
* if the provider over-returns, `sources[]` is truncated and `truncated` set.
|
|
93
|
+
* @param request - the query and optional result limit.
|
|
94
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
95
|
+
* @returns the provider's results, capped to `request.maxResults`.
|
|
96
|
+
*/
|
|
97
|
+
async search(request, signal) {
|
|
98
|
+
return capSources(await resolveProvider({
|
|
99
|
+
providers: this.searchProviders,
|
|
100
|
+
...this.searchProviderId !== void 0 ? { configuredId: this.searchProviderId } : {}
|
|
101
|
+
}).search(request, signal), request.maxResults);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Retrieve one URL through the selected provider. Resolves the provider at
|
|
105
|
+
* call time with the selection rules above; throws {@link WebError} when the
|
|
106
|
+
* capability cannot run. A non-2xx response is a result, not a throw.
|
|
107
|
+
* @param request - the URL plus retrieval options.
|
|
108
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
109
|
+
* @returns the retrieval outcome; non-2xx responses resolve descriptively.
|
|
110
|
+
*/
|
|
111
|
+
async fetch(request, signal) {
|
|
112
|
+
return resolveProvider({
|
|
113
|
+
providers: this.fetchProviders,
|
|
114
|
+
...this.fetchProviderId !== void 0 ? { configuredId: this.fetchProviderId } : {}
|
|
115
|
+
}).fetch(request, signal);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/** Resolve the selected provider or throw the matching {@link WebError}. */
|
|
119
|
+
function resolveProvider(selection) {
|
|
120
|
+
const { configuredId, providers } = selection;
|
|
121
|
+
if (configuredId !== void 0) {
|
|
122
|
+
const provider = providers.get(configuredId);
|
|
123
|
+
if (!provider) throw new WebError(`configured web provider "${configuredId}" is not registered`, "WEB_PROVIDER_CONFIGURED_MISSING");
|
|
124
|
+
if (!provider.available()) throw new WebError(`configured web provider "${configuredId}" is registered but unavailable`, "WEB_PROVIDER_CONFIGURED_UNAVAILABLE");
|
|
125
|
+
return provider;
|
|
126
|
+
}
|
|
127
|
+
const usable = [...providers.values()].filter((provider) => provider.available());
|
|
128
|
+
const [single] = usable;
|
|
129
|
+
if (single === void 0) throw new WebError("no usable web provider is registered", "WEB_PROVIDER_UNAVAILABLE");
|
|
130
|
+
if (usable.length > 1) throw new WebError(`multiple usable web providers are registered (${usable.map((provider) => provider.id).join(", ")}); configure one explicitly`, "WEB_PROVIDER_AMBIGUOUS");
|
|
131
|
+
return single;
|
|
132
|
+
}
|
|
133
|
+
/** Enforce `maxResults` on a search result: truncate `sources[]` and flag it. */
|
|
134
|
+
function capSources(result, maxResults) {
|
|
135
|
+
if (maxResults === void 0 || result.sources.length <= maxResults) return result;
|
|
136
|
+
return {
|
|
137
|
+
...result,
|
|
138
|
+
sources: result.sources.slice(0, maxResults),
|
|
139
|
+
truncated: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { WebError, WebService, WebService as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-web`.
|
|
4
|
+
* @module @deepseek-ai/dsh-web/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-web";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "web-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: provider maps are private and selection/result caps are enforced on each
|
|
13
|
+
* call; the seam publishes no independent registry or request/result observation stream.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
|
|
3
|
+
* fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
|
|
4
|
+
* be usable; without one, exactly one usable provider is required, so selection never depends
|
|
5
|
+
* on registration order.
|
|
6
|
+
* @module @deepseek-ai/dsh-web
|
|
7
|
+
*/
|
|
8
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
import type { WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult } from './types.ts';
|
|
11
|
+
export { WebError, } from './types.ts';
|
|
12
|
+
export type { WebFetchBody, WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource, } from './types.ts';
|
|
13
|
+
declare module '@deepseek-ai/cordis' {
|
|
14
|
+
interface Context {
|
|
15
|
+
web: WebService;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
|
|
20
|
+
* wins for each capability; both are optional (a single registered usable
|
|
21
|
+
* provider auto-selects). Operational overrides such as environment variables
|
|
22
|
+
* must feed these same fields rather than introduce a hidden priority chain.
|
|
23
|
+
*/
|
|
24
|
+
export interface WebServiceConfig {
|
|
25
|
+
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
|
|
26
|
+
readonly searchProvider?: string;
|
|
27
|
+
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
|
|
28
|
+
readonly fetchProvider?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The web access service. Registered as `ctx.web` (one instance per context).
|
|
32
|
+
*
|
|
33
|
+
* Selection semantics (resolved at execution time, never order-dependent):
|
|
34
|
+
* - A configured id that is registered and `available()` → that provider.
|
|
35
|
+
* - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
36
|
+
* - A configured id registered but unavailable →
|
|
37
|
+
* `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
38
|
+
* - No id configured, exactly one registered usable provider → that provider.
|
|
39
|
+
* - No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
|
|
40
|
+
* - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
|
|
41
|
+
*/
|
|
42
|
+
export declare class WebService extends Service {
|
|
43
|
+
/**
|
|
44
|
+
* Provider selection config. Operational env overrides feed the SAME fields:
|
|
45
|
+
* `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
|
|
46
|
+
* `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
|
|
47
|
+
*/
|
|
48
|
+
static Config: z<WebServiceConfig>;
|
|
49
|
+
private searchProviders;
|
|
50
|
+
private fetchProviders;
|
|
51
|
+
private readonly searchProviderId;
|
|
52
|
+
private readonly fetchProviderId;
|
|
53
|
+
constructor(ctx: Context, config?: WebServiceConfig);
|
|
54
|
+
/**
|
|
55
|
+
* Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
56
|
+
* if its id is already registered for search. Returns a disposer; disposed
|
|
57
|
+
* with the calling fiber.
|
|
58
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
59
|
+
* @returns the disposer that unregisters the provider.
|
|
60
|
+
*/
|
|
61
|
+
registerSearchProvider(provider: WebSearchProvider): () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
64
|
+
* if its id is already registered for fetch. Returns a disposer; disposed
|
|
65
|
+
* with the calling fiber.
|
|
66
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
67
|
+
* @returns the disposer that unregisters the provider.
|
|
68
|
+
*/
|
|
69
|
+
registerFetchProvider(provider: WebFetchProvider): () => void;
|
|
70
|
+
private registerProvider;
|
|
71
|
+
/**
|
|
72
|
+
* Run one search through the selected provider. Resolves the provider at call
|
|
73
|
+
* time with the selection rules above; throws {@link WebError} when the
|
|
74
|
+
* capability cannot run. The seam enforces `request.maxResults` on the result:
|
|
75
|
+
* if the provider over-returns, `sources[]` is truncated and `truncated` set.
|
|
76
|
+
* @param request - the query and optional result limit.
|
|
77
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
78
|
+
* @returns the provider's results, capped to `request.maxResults`.
|
|
79
|
+
*/
|
|
80
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Retrieve one URL through the selected provider. Resolves the provider at
|
|
83
|
+
* call time with the selection rules above; throws {@link WebError} when the
|
|
84
|
+
* capability cannot run. A non-2xx response is a result, not a throw.
|
|
85
|
+
* @param request - the URL plus retrieval options.
|
|
86
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
87
|
+
* @returns the retrieval outcome; non-2xx responses resolve descriptively.
|
|
88
|
+
*/
|
|
89
|
+
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
90
|
+
}
|
|
91
|
+
export default WebService;
|
|
92
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-web`.
|
|
3
|
+
* @module @deepseek-ai/dsh-web/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "web-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
|
|
3
|
+
* seam so provider selection, cancellation, errors, and product configuration have one owner,
|
|
4
|
+
* while retaining separate request and result types.
|
|
5
|
+
* @module @deepseek-ai/dsh-web/types
|
|
6
|
+
*/
|
|
7
|
+
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
/**
|
|
9
|
+
* What one search-capable backend can return. The model-facing argument is just
|
|
10
|
+
* a query; `maxResults` is a `dsh-tool-web`-layer bound passed through unchanged
|
|
11
|
+
* and enforced on the way back by the seam (see {@link WebSearchResult}).
|
|
12
|
+
*/
|
|
13
|
+
export interface WebSearchRequest {
|
|
14
|
+
readonly query: string;
|
|
15
|
+
/**
|
|
16
|
+
* Upper bound on returned sources; the seam truncates to it. Omitted = no
|
|
17
|
+
* bound. `dsh-tool-web` always sets it. A provider whose API supports a
|
|
18
|
+
* result-count control (Exa's `numResults`) should apply it at the request
|
|
19
|
+
* layer as a cost/latency optimization; the seam enforces the bound
|
|
20
|
+
* regardless.
|
|
21
|
+
*/
|
|
22
|
+
readonly maxResults?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalized search outcome. `content` is optional provider-generated answer
|
|
26
|
+
* text or summary (Exa and DeepSeek return none; Perplexity returns a
|
|
27
|
+
* generated answer).
|
|
28
|
+
* `sources[]` is the portable citation surface. `truncated` is set by the seam
|
|
29
|
+
* when it cut `sources[]` down to `maxResults`.
|
|
30
|
+
*/
|
|
31
|
+
export interface WebSearchResult {
|
|
32
|
+
/** Optional provider-generated answer text, search context, or summary. */
|
|
33
|
+
readonly content?: string;
|
|
34
|
+
/** Citeable sources, already truncated to the request's `maxResults`. */
|
|
35
|
+
readonly sources: readonly WebSearchSource[];
|
|
36
|
+
/** True when the seam dropped sources to honor `maxResults`. */
|
|
37
|
+
readonly truncated: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One citeable source. A source always has a URL; `title`, `snippet`, and
|
|
41
|
+
* `publishedAt` are optional because not every provider returns them — forcing
|
|
42
|
+
* adapters to invent them would make the seam lie (Perplexity citations may be
|
|
43
|
+
* URL-only). `dsh-tool-web` renders `title ?? hostname(url)` for display.
|
|
44
|
+
*/
|
|
45
|
+
export interface WebSearchSource {
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly title?: string;
|
|
48
|
+
readonly snippet?: string;
|
|
49
|
+
/** Publication/crawl timestamp as a provider-supplied ISO-8601 string. */
|
|
50
|
+
readonly publishedAt?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What one fetch-capable backend is asked to retrieve. The request deliberately
|
|
54
|
+
* omits timeout, format, prompt, and extraction controls: cancellation is a
|
|
55
|
+
* direct execution argument, while presentation and higher-level LLM concerns
|
|
56
|
+
* belong outside safe retrieval.
|
|
57
|
+
*/
|
|
58
|
+
export interface WebFetchRequest {
|
|
59
|
+
readonly url: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Normalized fetch outcome. A successful network fetch of a non-2xx response is
|
|
63
|
+
* a result, not an error: the status code is part of the fetched resource
|
|
64
|
+
* state. {@link WebError} is reserved for failures to safely retrieve or
|
|
65
|
+
* represent the resource.
|
|
66
|
+
*/
|
|
67
|
+
export interface WebFetchResult {
|
|
68
|
+
/** The final URL after allowed redirects (the request URL is in the request). */
|
|
69
|
+
readonly url: string;
|
|
70
|
+
/** HTTP status code of the fetched response. */
|
|
71
|
+
readonly statusCode: number;
|
|
72
|
+
/** Decoded body, classified by content kind. */
|
|
73
|
+
readonly body: WebFetchBody;
|
|
74
|
+
/** True when the provider capped the decoded body. */
|
|
75
|
+
readonly truncated: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The decoded body of a fetched resource. A CLOSED discriminated union owned by
|
|
79
|
+
* `dsh-web`: the provider decodes the kind and `dsh-tool-web` renders it, so a
|
|
80
|
+
* new kind is a coordinated change across known packages, not a plugin
|
|
81
|
+
* extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
|
|
82
|
+
* so adding a kind breaks compilation at every consumer until handled. Each arm
|
|
83
|
+
* stays its own object literal even where fields coincide, so an arm can gain
|
|
84
|
+
* fields the others lack.
|
|
85
|
+
*/
|
|
86
|
+
export type WebFetchBody = {
|
|
87
|
+
readonly kind: 'html';
|
|
88
|
+
readonly content: string;
|
|
89
|
+
} | {
|
|
90
|
+
readonly kind: 'text';
|
|
91
|
+
readonly content: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A search-capable backend. Registered with `ctx.web.registerSearchProvider`.
|
|
95
|
+
* `id` is a stable string, unique within the search capability kind.
|
|
96
|
+
*/
|
|
97
|
+
export interface WebSearchProvider {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
/** Cheap local usability check; must not make network calls. */
|
|
100
|
+
available(): boolean;
|
|
101
|
+
/** Run one search; honor `signal` for cancellation. */
|
|
102
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A fetch-capable backend. Registered with `ctx.web.registerFetchProvider`.
|
|
106
|
+
* `id` is a stable string, unique within the fetch capability kind.
|
|
107
|
+
*/
|
|
108
|
+
export interface WebFetchProvider {
|
|
109
|
+
readonly id: string;
|
|
110
|
+
/** Cheap local usability check; must not make network calls. */
|
|
111
|
+
available(): boolean;
|
|
112
|
+
/** Retrieve one URL; honor `signal` for cancellation. */
|
|
113
|
+
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Typed web error with a machine-routable, open-string `code` and chained `cause`.
|
|
117
|
+
* Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
|
|
118
|
+
* missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
|
|
119
|
+
* the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
|
|
120
|
+
* size and timeout limits, and unsupported content types. Tool execution exposes the code in
|
|
121
|
+
* structured error metadata.
|
|
122
|
+
*/
|
|
123
|
+
export declare class WebError extends HarnessError {
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-web",
|
|
3
|
+
"description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/web/web"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
45
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
46
|
+
}
|
|
47
|
+
}
|