refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/README.md
README.md: 811c8b3439a8962116df0e8b3ab4f6e282ecb3d2
README.zh.md: e328447d5d026c0abc815b269179ba6c4cd9acfc
README.md: fc37d7cdead59138db149b5a86f0a0c031d40037
README.zh.md: 53fe673ddaed235bbb938db4757e9cd240928d61

View File

@@ -10,7 +10,7 @@ This family provides provider-neutral web search and fetch operations plus the m
| [`web-search-exa/`](web-search-exa/README.md) | Provides web search through Exa | registers on `ctx.web` |
| [`web-search-perplexity/`](web-search-perplexity/README.md) | Provides web search through Perplexity | registers on `ctx.web` |
| [`web-search-deepseek/`](web-search-deepseek/README.md) | Provides native DeepSeek web search | registers on `ctx.web` |
| [`web-fetch-local/`](web-fetch-local/README.md) | Fetches public HTTP and HTTPS resources | registers on `ctx.web` |
| [`web-fetch-http/`](web-fetch-http/README.md) | Fetches public HTTP and HTTPS resources | registers on `ctx.web` |
| [`tool-web/`](tool-web/README.md) | Exposes web search and fetch to the model | registers on `ctx.tools` |
The [web capability decision](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) records why search and fetch share one provider-selection service.

View File

@@ -10,7 +10,7 @@
| [`web-search-exa/`](web-search-exa/README.md) | 通过 Exa 提供 web 搜索 | 注册到 `ctx.web` |
| [`web-search-perplexity/`](web-search-perplexity/README.md) | 通过 Perplexity 提供 web 搜索 | 注册到 `ctx.web` |
| [`web-search-deepseek/`](web-search-deepseek/README.md) | 提供 DeepSeek 原生 web 搜索 | 注册到 `ctx.web` |
| [`web-fetch-local/`](web-fetch-local/README.md) | 抓取公共 HTTP 和 HTTPS 资源 | 注册到 `ctx.web` |
| [`web-fetch-http/`](web-fetch-http/README.md) | 抓取公共 HTTP 和 HTTPS 资源 | 注册到 `ctx.web` |
| [`tool-web/`](tool-web/README.md) | 向模型公开 web 搜索和抓取 | 注册到 `ctx.tools` |
[web 能力决策](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)记录了搜索和抓取共用一项提供方选择服务的原因。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/tool-web/README.md
README.md: 6a7ca42773f4a62b75d6077c5cdd892b0ccfdbee
README.zh.md: d8dd51dcc8cf5146c3bc8ae28715ddf08ca7721a
README.md: 81dc4d3f3dbf9788e68b86513bfa09271fb99c84
README.zh.md: 8e976f23f72347e12c6aea35f9b99c5d8ad31d85

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The model-facing web tool suite — `web_search` and `web_fetch` — over the [web capability seam](../web/README.md) (`ctx.web`). It owns model-facing concerns only: tool names, JSON schemas, snake_case argument names, prompt sections, the result-count bound, result formatting, HTML→markdown presentation, and the UI presentation projection — `presentCall`, `presentResult` (a `card: 'web'` result card discriminated by `kind: 'search' | 'fetch'`), and the `output.presentationMeta` that carries the structured search sources or the fetch summary the lossy render text cannot (see the [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md)). All web access goes through `ctx.web`; this package never imports a concrete provider. Neither tool exposes a model-facing timeout — each tool's cooperative tool-call budget is declared here via config (`fetchTimeoutMs`/`searchTimeoutMs`, attached as `ToolDefinition.timeoutMs`) and enforced by [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md) (a `tools/execute` wrapper); each tool just forwards `exec.signal` to the seam.
The model-facing web tool suite — `web_search` and `web_fetch` — over the [web capability seam](../web/README.md) (`ctx.web`). It owns model-facing concerns only: tool names, JSON schemas, snake_case argument names, prompt sections, the result-count bound, result formatting, HTML→markdown presentation, and the UI presentation projection — `presentCall`, `presentResult` (a `card: 'web'` result card discriminated by `kind: 'search' | 'fetch'`), and the `output.presentationMeta` that carries the structured search sources or the fetch summary the lossy render text cannot (see the [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md)). All web access goes through `ctx.web`; this package never imports a concrete provider. Neither tool exposes a model-facing timeout — each tool's cooperative tool-call budget is declared here via config (`fetchTimeoutMs`/`searchTimeoutMs`, attached as `ToolDefinition.timeoutMs`) and enforced by [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) (a `tools/execute` wrapper); each tool just forwards `exec.signal` to the seam.
Each tool is registered independently; a product that wants only one disables the other via config (`{ search: false }` / `{ fetch: false }`). Search guidance mentions `web_fetch` only when fetch is also config-enabled; a search-only composition instead tells the model to use returned snippets and cite their URLs.
@@ -11,7 +11,7 @@ Each tool is registered independently; a product that wants only one disables th
| Tool | Args | Behavior |
|---|---|---|
| `web_search` | `query` (string) | Discovery. Returns an optional answer plus source URLs. `max_results` is **not** model-facing — the tool sets the bound (the `searchMaxResults` config, default 8) and passes it to the seam. |
| `web_fetch` | `url` (string) | Retrieves a specific URL. HTML bodies are rendered to markdown (turndown with GFM tables/strikethrough); text bodies pass through. A non-2xx status is reported, not an error. The tool-call timeout is deployment policy (`dsh-timeout-policy`), not a model argument. |
| `web_fetch` | `url` (string) | Retrieves a specific URL. HTML bodies are rendered to markdown (turndown with GFM tables/strikethrough); text bodies pass through. A non-2xx status is reported, not an error. The tool-call timeout is deployment policy (`dsh-tool-call-timeout-policy`), not a model argument. |
Both tools opt into concurrent scheduling because provider reads return content without mutating parent-agent state.
@@ -28,7 +28,7 @@ The normalized service results are also the canonical tool values: `WebSearchRes
| `searchTimeoutMs` | `30000` | Cooperative tool-call timeout budget (ms) for `web_search`. |
| `fetchMaxOutputChars` | `200000` | Cap on source characters converted synchronously and on one complete `web_fetch` output (header, rendered body, and footer); a cut body gets the truncation notice when it fits. |
`fetchTimeoutMs`/`searchTimeoutMs` declare each tool's cooperative timeout budget (attached as `ToolDefinition.timeoutMs`), enforced by [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md); the model-facing schema exposes no timeout argument. `fetchMaxOutputChars` bounds both synchronous conversion work and the complete rendered result: only that many source characters are converted, and the header, converted prefix, and truncation notice are then capped together. The default leaves headroom above the local provider's 100,000-character body cap, but rendered expansion can still make the final bound truncate the result.
`fetchTimeoutMs`/`searchTimeoutMs` declare each tool's cooperative timeout budget (attached as `ToolDefinition.timeoutMs`), enforced by [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md); the model-facing schema exposes no timeout argument. `fetchMaxOutputChars` bounds both synchronous conversion work and the complete rendered result: only that many source characters are converted, and the header, converted prefix, and truncation notice are then capped together. The default leaves headroom above the local provider's 100,000-character body cap, but rendered expansion can still make the final bound truncate the result.
```yaml
- id: tool-web
@@ -37,7 +37,7 @@ The normalized service results are also the canonical tool values: `WebSearchRes
## Stable registration
Tool registration follows product **enablement**, not backend availability. A tool stays visible even when its selected provider is missing, misconfigured, ambiguous, or temporarily unavailable; the seam resolves the provider at execution time and execution fails with a structured `WebError` (e.g. `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_AMBIGUOUS`), which `ToolRegistry.execute()` turns into an error tool result the model can read and hooks/UI can route on. This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. To remove a web tool entirely, disable it here in config.
Tool registration follows product **enablement**, not backend availability. A tool stays visible even when its selected provider is missing, misconfigured, ambiguous, or temporarily unavailable; the seam resolves the provider at execution time and execution fails with a structured `WebError` (e.g. `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_AMBIGUOUS`), which `ToolRuntime.execute()` turns into an error tool result the model can read and hooks/UI can route on. This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. To remove a web tool entirely, disable it here in config.
The tool never calls a provider's `available()` and never enumerates providers — its only execution path is `ctx.web.search()` / `ctx.web.fetch()`, and provider unavailability reaches it as the structured `WebError` codes selection throws at execution time. Provider selection stays entirely inside the seam, with one owner.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
面向模型的 web 工具套件 `web_search``web_fetch`,构建于 [web 能力 seam](../web/README.md)`ctx.web`之上。它只负责面向模型的事项工具名称、JSON Schema、snake_case 参数名称、提示词区段、结果数量上限、结果格式、HTML→markdown 呈现,以及 UI 呈现投影——`presentCall``presentResult`(以 `kind: 'search' | 'fetch'` 区分的 `card: 'web'` 结果卡片),以及承载有损渲染文本无法携带的结构化搜索来源或抓取摘要的 `output.presentationMeta`(见 [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md))。所有 web 访问都通过 `ctx.web`;该包绝不导入具体提供方。两个工具都不公开面向模型的超时:每个工具的协作式工具调用超时预算通过配置在此声明(`fetchTimeoutMs``searchTimeoutMs`,附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md)`tools/execute` 包装层)强制执行;每个工具只把 `exec.signal` 转发给 seam。
面向模型的 web 工具套件 `web_search``web_fetch`,构建于 [web 能力 seam](../web/README.md)`ctx.web`之上。它只负责面向模型的事项工具名称、JSON Schema、snake_case 参数名称、提示词区段、结果数量上限、结果格式、HTML→markdown 呈现,以及 UI 呈现投影——`presentCall``presentResult`(以 `kind: 'search' | 'fetch'` 区分的 `card: 'web'` 结果卡片),以及承载有损渲染文本无法携带的结构化搜索来源或抓取摘要的 `output.presentationMeta`(见 [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md))。所有 web 访问都通过 `ctx.web`;该包绝不导入具体提供方。两个工具都不公开面向模型的超时:每个工具的协作式工具调用超时预算通过配置在此声明(`fetchTimeoutMs``searchTimeoutMs`,附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md)`tools/execute` 包装层)强制执行;每个工具只把 `exec.signal` 转发给 seam。
每个工具独立注册;只需要其中一个工具的产品可以通过配置禁用另一个(`{ search: false }``{ fetch: false }`)。仅当抓取也通过配置启用时,搜索指引才会提及 `web_fetch`;仅启用搜索的组合则会要求模型使用返回的 snippet 并引用其 URL。
@@ -11,7 +11,7 @@
| 工具 | 参数 | 行为 |
|---|---|---|
| `web_search` | `query`string | 用于发现信息。返回可选答案与来源 URL。`max_results` **不**面向模型:工具设置上限(`searchMaxResults` 配置,默认 8并传给 seam。 |
| `web_fetch` | `url`string | 获取特定 URL。HTML 主体渲染为 markdownturndown带 GFM 表格/删除线);文本主体原样通过。非 2xx 状态会报告,而非报错。工具调用超时是部署策略(`dsh-timeout-policy`),不是模型参数。 |
| `web_fetch` | `url`string | 获取特定 URL。HTML 主体渲染为 markdownturndown带 GFM 表格/删除线);文本主体原样通过。非 2xx 状态会报告,而非报错。工具调用超时是部署策略(`dsh-tool-call-timeout-policy`),不是模型参数。 |
两个工具都选择并发调度,因为提供方读取会返回内容,不会修改父 agent智能体的状态。
@@ -28,7 +28,7 @@
| `searchTimeoutMs` | `30000` | `web_search` 的协作式工具调用超时预算ms。 |
| `fetchMaxOutputChars` | `200000` | 同步转换的源字符数与单次完整 `web_fetch` 输出的上限(状态头、渲染后的主体与页脚合并计算);主体被截断时,在能容纳的情况下附带截断提示。 |
`fetchTimeoutMs``searchTimeoutMs` 声明每个工具的协作式超时预算(附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md) 强制执行;面向模型的 schema 不公开超时参数。`fetchMaxOutputChars` 同时限制同步转换工作量和完整渲染结果:只转换至多该数量的源字符,随后对状态头、转换后的前缀和截断提示合并设限。默认值为本地提供方的 100,000 字符主体上限留出余量,但渲染膨胀仍可能使最终上限截断结果。
`fetchTimeoutMs``searchTimeoutMs` 声明每个工具的协作式超时预算(附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) 强制执行;面向模型的 schema 不公开超时参数。`fetchMaxOutputChars` 同时限制同步转换工作量和完整渲染结果:只转换至多该数量的源字符,随后对状态头、转换后的前缀和截断提示合并设限。默认值为本地提供方的 100,000 字符主体上限留出余量,但渲染膨胀仍可能使最终上限截断结果。
```yaml
- id: tool-web
@@ -37,7 +37,7 @@
## 稳定注册
工具注册遵循产品**启用状态**而非后端可用性。即使选中的提供方缺失、错误配置、存在歧义或暂时不可用工具仍保持可见seam 在执行时解析提供方,执行以结构化 `WebError`(例如 `WEB_PROVIDER_UNAVAILABLE``WEB_PROVIDER_AMBIGUOUS`)失败,`ToolRegistry.execute()` 会把它转为模型可读、钩子UI 可路由的错误工具结果。这样无需把插件加载顺序、凭据状态或 HMR热模块替换时机纳入面向模型约定也能保持模型 schema 稳定。要彻底移除 web 工具,请在此处通过配置将其禁用。
工具注册遵循产品**启用状态**而非后端可用性。即使选中的提供方缺失、错误配置、存在歧义或暂时不可用工具仍保持可见seam 在执行时解析提供方,执行以结构化 `WebError`(例如 `WEB_PROVIDER_UNAVAILABLE``WEB_PROVIDER_AMBIGUOUS`)失败,`ToolRuntime.execute()` 会把它转为模型可读、钩子UI 可路由的错误工具结果。这样无需把插件加载顺序、凭据状态或 HMR热模块替换时机纳入面向模型约定也能保持模型 schema 稳定。要彻底移除 web 工具,请在此处通过配置将其禁用。
工具绝不会调用提供方的 `available()`,也不会枚举提供方;唯一执行路径是 `ctx.web.search()``ctx.web.fetch()`,提供方不可用时,选择机制会在执行阶段抛出结构化 `WebError`,其错误码由工具接收。提供方选择完全留在 seam 内,由单一主体负责。

View File

@@ -53,10 +53,10 @@
"@deepseek-ai/dsh-spill-local": "workspace:^",
"@deepseek-ai/dsh-spill-policy": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/dsh-web-fetch-local": "workspace:^",
"@deepseek-ai/dsh-web-fetch-http": "workspace:^",
"@deepseek-ai/dsh-web-search-exa": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -80,7 +80,7 @@ turndown.addRule('tableRowWithoutSpanExpansion', {
* Validate value constraints the schema DSL can't express: a non-blank `url`.
* Throws a plain `Error` otherwise. No timeout parameter — the tool-call budget
* is deployment policy declared via `fetchTimeoutMs` config and enforced by
* `@deepseek-ai/dsh-timeout-policy`, not a model argument.
* `@deepseek-ai/dsh-tool-call-timeout-policy`, not a model argument.
*
* @param args - the schema-validated `web_fetch` arguments.
* @returns the arguments as the seam's request fields.
@@ -422,7 +422,7 @@ export function presentFetchResult(args: { url: string }, result: ToolResult): W
* @param ctx - context whose `tools` and `systemPrompt` registries receive the
* registrations; both are effect-scoped and unregister on plugin dispose.
* @param timeoutMs - the cooperative tool-call budget (ms) attached as the tool's
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-timeout-policy` to enforce.
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-tool-call-timeout-policy` to enforce.
* @param maxOutputChars - cap on the complete rendered tool output (see
* {@link formatFetchOutput}) and on source characters converted synchronously.
*/

View File

@@ -73,7 +73,7 @@ function assertPositiveInteger(name: string, value: number): void {
* that wants only one disables the other in config. Each tool's cooperative
* timeout budget (`fetchTimeoutMs`/`searchTimeoutMs`, default 30000) is resolved
* here and attached to the tool as `ToolDefinition.timeoutMs` for
* `@deepseek-ai/dsh-timeout-policy` to enforce. The tools' disposers are
* `@deepseek-ai/dsh-tool-call-timeout-policy` to enforce. The tools' disposers are
* fiber-scoped (the effect-based registries clean up on dispose), so no manual
* teardown is needed.
*/

View File

@@ -203,7 +203,7 @@ export function presentSearchResult(args: { query: string }, result: ToolResult)
* @param maxResults - the deployment's source cap, sent as every seam
* request's `maxResults`.
* @param timeoutMs - the cooperative tool-call budget (ms) attached as the tool's
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-timeout-policy` to enforce.
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-tool-call-timeout-policy` to enforce.
* @param fetchEnabled - whether the same composition exposes `web_fetch`, which
* controls whether search guidance may recommend that follow-up tool.
*/

View File

@@ -1,7 +1,7 @@
/**
* Integration: the real fetch backend (`dsh-web-fetch-local`) + a real search provider
* Integration: the real fetch backend (`dsh-web-fetch-http`) + a real search provider
* (`dsh-web-search-exa`) + the real seam (`dsh-web`) + the model tool (`dsh-tool-web`) + the
* tool-call timeout policy (`dsh-timeout-policy`), exercised through `ctx.tools.execute()` —
* tool-call timeout policy (`dsh-tool-call-timeout-policy`), exercised through `ctx.tools.execute()` —
* nothing bypasses the tool registry. Fetch verifies world effects against loopback HTTP; search
* uses the real Exa provider with only its network boundary stubbed.
*/
@@ -12,12 +12,12 @@ import { AddressInfo } from 'node:net'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import WebService from '@deepseek-ai/dsh-web'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
import ToolRuntime, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import WebRuntime from '@deepseek-ai/dsh-web'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-http'
import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
import * as TimeoutPolicy from '@deepseek-ai/dsh-timeout-policy'
import * as TimeoutPolicy from '@deepseek-ai/dsh-tool-call-timeout-policy'
const testToolSignal = new AbortController().signal
@@ -37,8 +37,8 @@ beforeEach(async () => {
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, { searchProvider: WebSearchExa.EXA_PROVIDER_ID, fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, { searchProvider: WebSearchExa.EXA_PROVIDER_ID, fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebFetchLocal, {})
await ctx.plugin(WebSearchExa, { apiKey: 'exa-key', baseURL: 'https://api.exa.test' })
// The shipped deployment shape: the tool-call budget is declared by tool-web
@@ -132,8 +132,8 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc
tctx = new Context()
await tctx.plugin(SystemPrompt)
await tctx.plugin(ToolRegistry)
await tctx.plugin(WebService, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
await tctx.plugin(ToolRuntime)
await tctx.plugin(WebRuntime, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
// Provider backstop well ABOVE the tool-call budget, so the policy wins.
await tctx.plugin(WebFetchLocal, { timeoutMs: 30_000 })
await tctx.plugin(TimeoutPolicy)
@@ -150,7 +150,7 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc
it('returns a structured TOOL_TIMEOUT (not the provider WEB_FETCH_TIMEOUT) when the tool-call budget wins', async () => {
const out = await tctx.tools.execute({ signal: testToolSignal, callId: CallId('slow-1'), name: 'web_fetch', arguments: { url: slowBase } })
expect(out.isError).toBe(true)
// The outer tool-call deadline won: TOOL_TIMEOUT, owned by dsh-timeout-policy,
// The outer tool-call deadline won: TOOL_TIMEOUT, owned by dsh-tool-call-timeout-policy,
// NOT the provider's own WEB_FETCH_TIMEOUT (its 30s backstop never fired).
expect(out.error?.info?.code).toBe('TOOL_TIMEOUT')
const text = out.content.map(b => (b.type === 'text' ? b.text : '')).join('')
@@ -160,7 +160,7 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc
it('the provider backstop still protects a direct provider call (no tool-call policy in that path)', async () => {
// A direct provider caller bypasses tools/execute, so a short configured backstop
// must produce provider-owned WEB_FETCH_TIMEOUT rather than TOOL_TIMEOUT.
const direct = new WebFetchLocal.LocalFetchProvider({
const direct = new WebFetchLocal.HttpFetchProvider({
maxUrlLength: 2048,
maxResponseBytes: 5_000_000,
maxBodyChars: 100_000,

View File

@@ -9,8 +9,8 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import WebService from '@deepseek-ai/dsh-web'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import WebRuntime from '@deepseek-ai/dsh-web'
import * as toolWeb from '@deepseek-ai/dsh-tool-web'
describe('dsh-tool-web real-load-path guard', () => {
@@ -28,8 +28,8 @@ describe('dsh-tool-web real-load-path guard', () => {
it('boots over ctx.web through the unwrapped module without an inject error', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, {})
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, {})
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(toolWeb) as Parameters<Context['plugin']>[0]

View File

@@ -17,12 +17,12 @@ import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
const testToolSignal = new AbortController().signal
import WebService from '@deepseek-ai/dsh-web'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
import WebRuntime from '@deepseek-ai/dsh-web'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-http'
import LocalSpillStore from '@deepseek-ai/dsh-spill-local'
import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
@@ -47,8 +47,8 @@ beforeEach(async () => {
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
// Provider cap generous so the tool returns a large formatted result; the
// policy cap is what triggers the spill (the Agent Note's separation of concerns).
await ctx.plugin(WebFetchLocal, { maxBodyChars: 500_000 })

View File

@@ -3,8 +3,8 @@ import { Context } from '@deepseek-ai/cordis'
import TurndownService from 'turndown'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import WebService from '@deepseek-ai/dsh-web'
import ToolRuntime, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import WebRuntime from '@deepseek-ai/dsh-web'
import type { WebSearchProvider, WebSearchResult } from '@deepseek-ai/dsh-web'
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
import {
@@ -36,14 +36,14 @@ function searchProvider(result: WebSearchResult, isAvailable = available): WebSe
/** Mount the real registry, seam, and tool-web; return an executor helper. */
async function mountTools(opts: {
config?: ToolWeb.Config
webConfig?: ConstructorParameters<typeof WebService>[1]
webConfig?: ConstructorParameters<typeof WebRuntime>[1]
search?: WebSearchProvider
fetchProvider?: import('@deepseek-ai/dsh-web').WebFetchProvider
} = {}): Promise<{ ctx: Context; fiber: Awaited<ReturnType<Context['plugin']>>; call: (name: string, args: unknown) => Promise<ToolExecutionResult> }> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, opts.webConfig ?? {})
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, opts.webConfig ?? {})
if (opts.search) ctx.web.registerSearchProvider(opts.search)
if (opts.fetchProvider) ctx.web.registerFetchProvider(opts.fetchProvider)
const fiber = await ctx.plugin(ToolWeb, opts.config ?? {})
@@ -595,7 +595,7 @@ describe('tool-web execution through the real registry', () => {
truncated: false,
})
// The model schema exposes no timeout: the tool forwards only the url; the
// tool-call budget is owned by dsh-timeout-policy over exec.signal.
// tool-call budget is owned by dsh-tool-call-timeout-policy over exec.signal.
expect(seen.request).toEqual({ url: 'https://a.test' })
expect(seen.signal).toBe(controller.signal)
await fiber.dispose()
@@ -679,8 +679,8 @@ describe('searchMaxResults is plugin config', () => {
])('rejects a %s searchMaxResults at load', async (_label, value) => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, {})
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, {})
await expect(ctx.plugin(ToolWeb, { searchMaxResults: value }))
.rejects.toThrow(/tool-web: searchMaxResults must be a positive integer/)
})
@@ -707,8 +707,8 @@ describe('tool-call timeout budget is plugin config', () => {
])('rejects a non-positive-integer %s at load', async (key, config) => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, {})
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, {})
await expect(ctx.plugin(ToolWeb, config))
.rejects.toThrow(new RegExp(`tool-web: ${key} must be a positive integer`))
})
@@ -739,8 +739,8 @@ describe('fetchMaxOutputChars is plugin config', () => {
it.each([0, -1, 1.5])('rejects an invalid fetchMaxOutputChars value %s at load', async (value) => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, {})
await ctx.plugin(ToolRuntime)
await ctx.plugin(WebRuntime, {})
await expect(ctx.plugin(ToolWeb, { fetchMaxOutputChars: value }))
.rejects.toThrow(/tool-web: fetchMaxOutputChars must be a positive integer/)
})

View File

@@ -31,7 +31,7 @@
"path": "../web"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web-fetch-local/README.md
README.md: 03d6d2a96b04528b33cd063736e8618e66932058
README.zh.md: 339ad3f57c29ec021298aab73504612da5794624
# pnpm run verify-translation-pairing --write packages/web/web-fetch-http/README.md
README.md: 5589a8e8605a64ae9ef5f6d9978a9b63331d5b0d
README.zh.md: e9cf98feb9065947af1321c87562a2ae9ac21315

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-web-fetch-local
# @deepseek-ai/dsh-web-fetch-http
English | [中文](README.zh.md)
@@ -10,7 +10,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i
The provider owns **safe resource retrieval**: URL validation, HTTP transport, redirect policy, a resource-backstop timeout, abort propagation, byte caps, charset decoding, content-type classification, and binary rejection. `@deepseek-ai/dsh-tool-web` owns **presentation** (HTML→markdown, truncation formatting). A non-2xx HTTP response is a *result* (status code + decoded body), not an error; `WebError` is reserved for failures to safely retrieve or represent the resource.
The provider's `timeoutMs` is a resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments, not the model-facing tool-call budget. [`dsh-timeout-policy`](../../guard/timeout-policy/README.md) owns the `web_fetch` tool-call budget by arming `exec.signal`.
The provider's `timeoutMs` is a resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments, not the model-facing tool-call budget. [`dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) owns the `web_fetch` tool-call budget by arming `exec.signal`.
A shipping web-tool deployment sets the provider backstop above the tool budget, so model calls normally return `TOOL_TIMEOUT`. If the outer deadline reaches the provider first, the provider reports `WEB_ABORTED` and the outer policy replaces it with `TOOL_TIMEOUT`. `WEB_FETCH_TIMEOUT` therefore identifies a direct service caller whose provider budget elapsed.
@@ -30,7 +30,7 @@ A shipping web-tool deployment sets the provider backstop above the tool budget,
| `maxUrlLength` | `2048` | Maximum accepted request URL length. |
| `maxResponseBytes` | `5_000_000` | Maximum response body size in bytes. |
| `maxBodyChars` | `100_000` | Maximum decoded body length in characters. |
| `timeoutMs` | `30_000` | Fetch timeout within Node's timer range — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). |
| `timeoutMs` | `30_000` | Fetch timeout within Node's timer range — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-tool-call-timeout-policy`). |
| `maxRedirects` | `5` | Maximum same-origin redirect hops (`0` follows none). |
| `userAgent` | `deepseek-harness/…` | `User-Agent` header. |

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-web-fetch-local
# @deepseek-ai/dsh-web-fetch-http
[English](README.md) | 中文
@@ -10,7 +10,7 @@
提供方拥有**安全资源获取**URL 验证、HTTP 传输、重定向策略、资源兜底超时、中止传播、字节上限、charset 解码、内容类型分类与二进制拒绝。`@deepseek-ai/dsh-tool-web` 拥有**呈现**HTML→markdown、截断格式。非 2xx HTTP 响应是*结果*(状态码 + 解码主体),不是错误;`WebError` 只用于无法安全获取或表示资源的失败。
提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-timeout-policy`](../../guard/timeout-policy/README.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。
提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。
已交付的 web 工具部署会把提供方兜底设为高于工具预算,因此模型调用通常返回 `TOOL_TIMEOUT`。如果外层截止期限先于提供方的兜底超时触发,提供方会报告 `WEB_ABORTED`,外层策略再将其替换为 `TOOL_TIMEOUT`。因此,`WEB_FETCH_TIMEOUT` 表明直接服务调用方的提供方预算已经耗尽。
@@ -30,7 +30,7 @@
| `maxUrlLength` | `2048` | 接受的请求 URL 最大长度。 |
| `maxResponseBytes` | `5_000_000` | 响应主体最大字节数。 |
| `maxBodyChars` | `100_000` | 解码主体最大字符数。 |
| `timeoutMs` | `30_000` | Node 定时器范围内的抓取超时:直接 `ctx.web.fetch()` 调用方的资源兜底,而非面向模型的工具调用预算(后者属于 `dsh-timeout-policy`)。 |
| `timeoutMs` | `30_000` | Node 定时器范围内的抓取超时:直接 `ctx.web.fetch()` 调用方的资源兜底,而非面向模型的工具调用预算(后者属于 `dsh-tool-call-timeout-policy`)。 |
| `maxRedirects` | `5` | 同源重定向最大跳数(`0` 表示完全不跟随)。 |
| `userAgent` | `deepseek-harness/…` | `User-Agent` 标头。 |

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-web-fetch-local",
"name": "@deepseek-ai/dsh-web-fetch-http",
"description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)",
"version": "0.0.1-rc.2",
"publishConfig": {
@@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/web/web-fetch-local"
"directory": "packages/web/web-fetch-http"
},
"type": "module",
"main": "lib/index.js",

View File

@@ -1,31 +1,31 @@
/**
* `@deepseek-ai/dsh-web-fetch-local`: registers an anonymous public HTTP(S)
* `@deepseek-ai/dsh-web-fetch-http`: registers an anonymous public HTTP(S)
* `WebFetchProvider` with `ctx.web`. A function/namespace plugin (NOT a
* default-export service): it registers INTO the seam's fetch registry, like the
* search providers register into the search registry.
*
* @module @deepseek-ai/dsh-web-fetch-local
* @module @deepseek-ai/dsh-web-fetch-http
*/
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-web'
import { LocalFetchProvider } from './provider.ts'
import type { LocalFetchLimits } from './provider.ts'
import { HttpFetchProvider } from './provider.ts'
import type { HttpFetchLimits } from './provider.ts'
const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647
export {
LOCAL_FETCH_PROVIDER_ID,
LocalFetchProvider,
HttpFetchProvider,
} from './provider.ts'
export type { LocalFetchLimits } from './provider.ts'
export type { HttpFetchLimits } from './provider.ts'
/** Default `User-Agent`: an explicit product agent, never a browser disguise. */
export const DEFAULT_USER_AGENT = 'deepseek-harness/0.0.1 (+https://github.com/deepseek-ai)'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'web-fetch-local'
export const name = 'web-fetch-http'
/** The web seam this provider registers into. */
export const inject = ['web']
@@ -61,7 +61,7 @@ type ResolvedConfig = Required<Config>
/** A resource limit (byte/char/length/timeout cap) must be a positive finite number. */
function assertPositiveFinite(name: string, value: number): void {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`web-fetch-local: ${name} must be a positive finite number`)
throw new Error(`web-fetch-http: ${name} must be a positive finite number`)
}
}
@@ -69,14 +69,14 @@ function assertPositiveFinite(name: string, value: number): void {
function assertTimeoutMs(value: number): void {
assertPositiveFinite('timeoutMs', value)
if (value > MAX_NODE_TIMER_DELAY_MS) {
throw new Error(`web-fetch-local: timeoutMs must be no greater than ${MAX_NODE_TIMER_DELAY_MS}`)
throw new Error(`web-fetch-http: timeoutMs must be no greater than ${MAX_NODE_TIMER_DELAY_MS}`)
}
}
/** The redirect hop cap must be a non-negative integer (0 follows no redirects). */
function assertNonNegativeInteger(name: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new Error(`web-fetch-local: ${name} must be a non-negative integer`)
throw new Error(`web-fetch-http: ${name} must be a non-negative integer`)
}
}
@@ -89,7 +89,7 @@ export function apply(ctx: Context, config: Config): void {
assertPositiveFinite('maxBodyChars', resolved.maxBodyChars)
assertTimeoutMs(resolved.timeoutMs)
assertNonNegativeInteger('maxRedirects', resolved.maxRedirects)
const limits: LocalFetchLimits = {
const limits: HttpFetchLimits = {
maxUrlLength: resolved.maxUrlLength,
maxResponseBytes: resolved.maxResponseBytes,
maxBodyChars: resolved.maxBodyChars,
@@ -97,5 +97,5 @@ export function apply(ctx: Context, config: Config): void {
maxRedirects: resolved.maxRedirects,
userAgent: resolved.userAgent,
}
ctx.web.registerFetchProvider(new LocalFetchProvider(limits))
ctx.web.registerFetchProvider(new HttpFetchProvider(limits))
}

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-web-fetch-local`.
* @module @deepseek-ai/dsh-web-fetch-local/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-web-fetch-http`.
* @module @deepseek-ai/dsh-web-fetch-http/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-web-fetch-local'
const PACKAGE_NAME = '@deepseek-ai/dsh-web-fetch-http'
/** Cordis companion plugin name. */
export const name = 'web-fetch-local-invariant'
export const name = 'web-fetch-http-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -3,7 +3,7 @@
* provider the pure, network-free half. The provider's `fetch()` composes
* these with transport (redirect following, byte caps, decoding).
*
* @module @deepseek-ai/dsh-web-fetch-local/policy
* @module @deepseek-ai/dsh-web-fetch-http/policy
*/
import { WebError } from '@deepseek-ai/dsh-web'

View File

@@ -5,7 +5,7 @@
*
* Private-network and SSRF protection is not implemented; do not enable this provider where
* it can reach sensitive internal targets.
* @module @deepseek-ai/dsh-web-fetch-local/provider
* @module @deepseek-ai/dsh-web-fetch-http/provider
*/
import { WebError } from '@deepseek-ai/dsh-web'
@@ -14,7 +14,7 @@ import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
import { classifyContentType, decoderForCharset, isSameOrigin, parseCharset, validateFetchUrl } from './policy.ts'
/** Resolved provider limits (the plugin's schemastery Config supplies defaults). */
export interface LocalFetchLimits {
export interface HttpFetchLimits {
/** Maximum accepted request URL length. */
maxUrlLength: number
/** Maximum response body size in bytes (read is aborted past this). */
@@ -30,13 +30,13 @@ export interface LocalFetchLimits {
}
/** Stable id this provider registers under. */
export const LOCAL_FETCH_PROVIDER_ID = 'local-http'
export const LOCAL_FETCH_PROVIDER_ID = 'http'
/** The anonymous public HTTP(S) fetch provider. */
export class LocalFetchProvider implements WebFetchProvider {
export class HttpFetchProvider implements WebFetchProvider {
readonly id = LOCAL_FETCH_PROVIDER_ID
constructor(private readonly limits: LocalFetchLimits) {}
constructor(private readonly limits: HttpFetchLimits) {}
/** No credentials to check — an anonymous public fetcher is always usable. */
available(): boolean {

View File

@@ -2,13 +2,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
import { AddressInfo } from 'node:net'
import { Context } from '@deepseek-ai/cordis'
import WebService from '@deepseek-ai/dsh-web'
import { LocalFetchProvider, LOCAL_FETCH_PROVIDER_ID } from '@deepseek-ai/dsh-web-fetch-local'
import type { LocalFetchLimits } from '@deepseek-ai/dsh-web-fetch-local'
import * as fetchPlugin from '@deepseek-ai/dsh-web-fetch-local'
import WebRuntime from '@deepseek-ai/dsh-web'
import { HttpFetchProvider, LOCAL_FETCH_PROVIDER_ID } from '@deepseek-ai/dsh-web-fetch-http'
import type { HttpFetchLimits } from '@deepseek-ai/dsh-web-fetch-http'
import * as fetchPlugin from '@deepseek-ai/dsh-web-fetch-http'
import { classifyContentType, decoderForCharset, isSameOrigin, parseCharset, validateFetchUrl } from '../src/policy.ts'
const limits: LocalFetchLimits = {
const limits: HttpFetchLimits = {
maxUrlLength: 2048,
maxResponseBytes: 5_000_000,
maxBodyChars: 100_000,
@@ -36,8 +36,8 @@ afterEach(async () => {
await new Promise<void>(resolve => server.close(() => { resolve() }))
})
function provider(overrides: Partial<LocalFetchLimits> = {}): LocalFetchProvider {
return new LocalFetchProvider({ ...limits, ...overrides })
function provider(overrides: Partial<HttpFetchLimits> = {}): HttpFetchProvider {
return new HttpFetchProvider({ ...limits, ...overrides })
}
describe('policy helpers', () => {
@@ -78,7 +78,7 @@ describe('policy helpers', () => {
})
})
describe('LocalFetchProvider success', () => {
describe('HttpFetchProvider success', () => {
it('fetches a text body', async () => {
handler = (_req, res) => { res.writeHead(200, { 'content-type': 'text/plain' }); res.end('hello world') }
const result = await provider().fetch({ url: base })
@@ -109,7 +109,7 @@ describe('LocalFetchProvider success', () => {
})
})
describe('LocalFetchProvider caps', () => {
describe('HttpFetchProvider caps', () => {
it('rejects an over-cap Content-Length with WEB_FETCH_TOO_LARGE', async () => {
handler = (_req, res) => { res.writeHead(200, { 'content-type': 'text/plain', 'content-length': '999999' }); res.end('x'.repeat(999999)) }
await expect(provider({ maxResponseBytes: 10 }).fetch({ url: base }))
@@ -169,7 +169,7 @@ describe('LocalFetchProvider caps', () => {
})
})
describe('LocalFetchProvider redirects', () => {
describe('HttpFetchProvider redirects', () => {
it('follows a same-origin redirect and reports the final URL', async () => {
handler = (req, res) => {
if (req.url === '/start') { res.writeHead(302, { location: '/end' }); res.end() }
@@ -273,7 +273,7 @@ describe('LocalFetchProvider redirects', () => {
})
})
describe('LocalFetchProvider invalid URLs and abort', () => {
describe('HttpFetchProvider invalid URLs and abort', () => {
it('rejects a non-http scheme before any network access', async () => {
await expect(provider().fetch({ url: 'ftp://example.com' }))
.rejects.toThrow(expect.objectContaining({ code: 'WEB_INVALID_URL' }))
@@ -327,7 +327,7 @@ describe('LocalFetchProvider invalid URLs and abort', () => {
})
describe('LocalFetchProvider body cancellation on error paths', () => {
describe('HttpFetchProvider body cancellation on error paths', () => {
/** A fake Response whose body.cancel is observable. */
type FakeInit = { status: number; headers: Record<string, string>; location?: string }
function fakeResponse(init: FakeInit): { response: Response; cancelled: () => boolean } {
@@ -367,10 +367,10 @@ describe('LocalFetchProvider body cancellation on error paths', () => {
})
})
describe('web-fetch-local plugin registration', () => {
describe('web-fetch-http plugin registration', () => {
it('registers the provider into ctx.web (HMR-safe)', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
const fiber = await ctx.plugin(fetchPlugin, {})
await expect(ctx.web.fetch({ url: `${base}/` }))
.resolves.toMatchObject({ statusCode: 200 })
@@ -385,42 +385,42 @@ describe('web-fetch-local plugin registration', () => {
it('rejects a non-positive resource limit at construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await expect(ctx.plugin(fetchPlugin, { maxResponseBytes: -1 }))
.rejects.toThrow(/maxResponseBytes must be a positive finite number/)
})
it('rejects a zero timeout at construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await expect(ctx.plugin(fetchPlugin, { timeoutMs: 0 }))
.rejects.toThrow(/timeoutMs must be a positive finite number/)
})
it('rejects a timeout beyond Node timer range at construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await expect(ctx.plugin(fetchPlugin, { timeoutMs: 2_147_483_648 }))
.rejects.toThrow(/timeoutMs must be no greater than 2147483647/)
})
it('rejects a fractional redirect cap at construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await expect(ctx.plugin(fetchPlugin, { maxRedirects: 1.5 }))
.rejects.toThrow(/maxRedirects must be a non-negative integer/)
})
it('rejects a negative redirect cap at construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await expect(ctx.plugin(fetchPlugin, { maxRedirects: -1 }))
.rejects.toThrow(/maxRedirects must be a non-negative integer/)
})
it('accepts maxRedirects: 0 (follow no redirects) as valid config', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebRuntime, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
const fiber = await ctx.plugin(fetchPlugin, { maxRedirects: 0 })
await expect(ctx.web.fetch({ url: `${base}/` }))
.resolves.toMatchObject({ statusCode: 200 })

View File

@@ -24,7 +24,7 @@
"path": "../web"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -34,7 +34,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
@@ -48,7 +48,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-credentials-local": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",

View File

@@ -10,7 +10,7 @@ import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-agent'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { environmentOf } from '@deepseek-ai/dsh-environment'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import type {} from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-web'
import {
@@ -103,12 +103,12 @@ function resolveOptions(ctx: Context, config: Config): DeepSeekSearchProviderOpt
const credentials = ctx.get('credentials')
if (credentials !== undefined) return (await credentials.resolve(apiKeyEnv))?.value
// Without the seam the environment is the whole credential plane.
const ambient = environmentOf(ctx).get(apiKeyEnv)
const ambient = launchEnvironmentOf(ctx).get(apiKeyEnv)
return ambient !== undefined && ambient.value.length > 0 ? ambient.value : undefined
},
apiKeyEnv,
baseURL: config.baseURL
?? environmentOf(ctx).get(SEARCH_BASE_URL_ENV)?.value
?? launchEnvironmentOf(ctx).get(SEARCH_BASE_URL_ENV)?.value
?? DEEPSEEK_DEFAULT_BASE_URL,
model: config.model ?? DEEPSEEK_DEFAULT_MODEL,
apiVersion: config.apiVersion ?? DEEPSEEK_DEFAULT_API_VERSION,

View File

@@ -32,7 +32,7 @@ maybe('DeepSeekSearchProvider real API', () => {
maxTokens: DEEPSEEK_DEFAULT_MAX_TOKENS,
maxUses: DEEPSEEK_DEFAULT_MAX_USES,
})
const result = await provider.search({ query: 'What is the DeepSeek Harness SDK?', maxResults: 5 })
const result = await provider.search({ query: 'What is DeepSeek Harness?', maxResults: 5 })
expect(result.sources.length).toBeGreaterThan(0)
for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//)
}, 60_000)

View File

@@ -5,8 +5,8 @@ import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
import WebService from '@deepseek-ai/dsh-web'
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
import WebRuntime from '@deepseek-ai/dsh-web'
import {
DeepSeekSearchProvider,
DEEPSEEK_PROVIDER_ID,
@@ -403,7 +403,7 @@ describe('web-search-deepseek plugin registration', () => {
it('registers the provider into ctx.web (HMR-safe)', async () => {
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse(searchResponse())))
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
const fiber = await ctx.plugin(deepseekPlugin, { apiKey: 'ds-key' })
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ truncated: false })
await fiber.dispose()
@@ -413,21 +413,21 @@ describe('web-search-deepseek plugin registration', () => {
it('rejects maxTokens: 0 at plugin construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
await expect(ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', maxTokens: 0 }))
.rejects.toThrow(/maxTokens expected number >= 1/)
})
it('rejects maxUses: 0 at plugin construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
await expect(ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', maxUses: 0 }))
.rejects.toThrow(/maxUses expected number >= 1/)
})
it('rejects a fractional maxUses at plugin construction', async () => {
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
await expect(ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', maxUses: 1.5 }))
.rejects.toThrow(/maxUses expected number multiple of 1/)
})
@@ -450,7 +450,7 @@ describe('web-search-deepseek plugin registration', () => {
it('boots over ctx.web through the unwrapped module without an inject error', async () => {
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse(searchResponse())))
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(deepseekPlugin) as Parameters<Context['plugin']>[0]
// A collapsed export shape (dropped inject) would throw "without inject" here.
@@ -466,7 +466,7 @@ describe('web-search-deepseek plugin registration', () => {
const fetchMock = vi.fn(async () => jsonResponse(searchResponse()))
vi.stubGlobal('fetch', fetchMock)
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
deepseekPlugin.apply(ctx, {})
await ctx.web.search({ query: 'q' })
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
@@ -488,8 +488,8 @@ describe('web-search-deepseek plugin registration', () => {
vi.stubGlobal('fetch', fetchMock)
const ctx = new Context()
try {
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(CredentialsLocal, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(deepseekPlugin, { baseURL: 'https://api.deepseek.test/anthropic/v1' })
await expect(ctx.web.search({ query: 'missing' }))
@@ -516,7 +516,7 @@ describe('web-search-deepseek plugin registration', () => {
delete process.env.DEEPSEEK_API_KEY
try {
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
await ctx.plugin(deepseekPlugin, {})
let caught: unknown
try {

View File

@@ -3,14 +3,14 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Fiber } from '@deepseek-ai/cordis'
import { Settings } from '@deepseek-ai/dsh-settings'
import { SettingsProvider } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import WebService from '@deepseek-ai/dsh-web'
import WebRuntime from '@deepseek-ai/dsh-web'
import * as deepseekPlugin from '@deepseek-ai/dsh-web-search-deepseek'
import { WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-web-search-deepseek'
/** The smallest real provider: one in-memory document, always writable. */
class MemorySettings extends Settings {
class MemorySettings extends SettingsProvider {
doc: Record<string, unknown> = {}
get writable(): boolean {
@@ -47,7 +47,7 @@ const ONE_RESULT = {
async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; pluginFiber: Fiber }> {
const ctx = new Context()
await ctx.plugin(WebService, {})
await ctx.plugin(WebRuntime, {})
const settingsFiber = ctx.plugin(MemorySettings)
await settingsFiber.await()
const pluginFiber = ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', baseURL: 'https://search.entry.test/v1' })

View File

@@ -9,7 +9,7 @@
],
"references": [
{
"path": "../../util/environment"
"path": "../../util/launch-environment"
},
{
"path": "../../../vendor/cosmokit"
@@ -36,7 +36,7 @@
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -32,7 +32,7 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
@@ -41,7 +41,7 @@
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -9,7 +9,7 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import { environmentOf } from '@deepseek-ai/dsh-environment'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-web'
import {
@@ -61,7 +61,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.web.registerSearchProvider(new ExaSearchProvider({
// Every environment layer may name this key: the product trusts the
// project it is launched in, and the managed store is not involved here.
apiKey: config.apiKey ?? environmentOf(ctx).get('EXA_API_KEY')?.value ?? '',
apiKey: config.apiKey ?? launchEnvironmentOf(ctx).get('EXA_API_KEY')?.value ?? '',
baseURL: config.baseURL ?? EXA_DEFAULT_BASE_URL,
searchType: config.searchType ?? EXA_DEFAULT_SEARCH_TYPE,
highlightsPerResult: config.highlightsPerResult ?? EXA_DEFAULT_HIGHLIGHTS_PER_RESULT,

View File

@@ -16,7 +16,7 @@ maybe('ExaSearchProvider real API', () => {
searchType: EXA_DEFAULT_SEARCH_TYPE,
highlightsPerResult: EXA_DEFAULT_HIGHLIGHTS_PER_RESULT,
})
const result = await provider.search({ query: 'DeepSeek Harness SDK', maxResults: 5 })
const result = await provider.search({ query: 'DeepSeek Harness', maxResults: 5 })
expect(result.sources.length).toBeGreaterThan(0)
for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//)
}, 30_000)

View File

@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import WebService from '@deepseek-ai/dsh-web'
import WebRuntime from '@deepseek-ai/dsh-web'
import { ExaSearchProvider, EXA_PROVIDER_ID } from '@deepseek-ai/dsh-web-search-exa'
import * as exaPlugin from '@deepseek-ai/dsh-web-search-exa'
import { mapExaResponse, mapExaResult } from '../src/provider.ts'
@@ -202,7 +202,7 @@ describe('web-search-exa plugin registration', () => {
it('registers the provider into ctx.web (HMR-safe)', async () => {
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ results: [] })))
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: EXA_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: EXA_PROVIDER_ID })
const fiber = await ctx.plugin(exaPlugin, { apiKey: 'exa-key' })
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ sources: [], truncated: false })
await fiber.dispose()
@@ -218,7 +218,7 @@ describe('web-search-exa plugin registration', () => {
const fetchMock = vi.fn(async () => jsonResponse({ results: [] }))
vi.stubGlobal('fetch', fetchMock)
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: EXA_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: EXA_PROVIDER_ID })
const fiber = await ctx.plugin(exaPlugin, { apiKey: 'exa-key', searchType: 'keyword', highlightsPerResult: 2, numResults: 9 })
await ctx.web.search({ query: 'q' })
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
@@ -233,7 +233,7 @@ describe('web-search-exa plugin registration', () => {
const fetchMock = vi.fn(async () => jsonResponse({ results: [] }))
vi.stubGlobal('fetch', fetchMock)
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: EXA_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: EXA_PROVIDER_ID })
const fiber = await ctx.plugin(exaPlugin, {})
await ctx.web.search({ query: 'q' })
const [url] = fetchMock.mock.calls[0] as unknown as [string]
@@ -250,7 +250,7 @@ describe('web-search-exa plugin registration', () => {
delete process.env.EXA_API_KEY
try {
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: EXA_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: EXA_PROVIDER_ID })
await ctx.plugin(exaPlugin, {})
await expect(ctx.web.search({ query: 'q' }))
.rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_UNAVAILABLE' }))

View File

@@ -9,7 +9,7 @@
],
"references": [
{
"path": "../../util/environment"
"path": "../../util/launch-environment"
},
{
"path": "../../../vendor/cosmokit"
@@ -24,7 +24,7 @@
"path": "../web"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -32,7 +32,7 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
@@ -41,7 +41,7 @@
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -8,7 +8,7 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import { environmentOf } from '@deepseek-ai/dsh-environment'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-web'
import { PerplexitySearchProvider, PERPLEXITY_DEFAULT_BASE_URL, PERPLEXITY_DEFAULT_MAX_TOKENS, PERPLEXITY_DEFAULT_MODEL } from './provider.ts'
@@ -55,7 +55,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.web.registerSearchProvider(new PerplexitySearchProvider({
// Every environment layer may name this key: the product trusts the
// project it is launched in, and the managed store is not involved here.
apiKey: config.apiKey ?? environmentOf(ctx).get('PERPLEXITY_API_KEY')?.value ?? '',
apiKey: config.apiKey ?? launchEnvironmentOf(ctx).get('PERPLEXITY_API_KEY')?.value ?? '',
baseURL: config.baseURL ?? PERPLEXITY_DEFAULT_BASE_URL,
model: config.model ?? PERPLEXITY_DEFAULT_MODEL,
maxTokens: config.maxTokens ?? PERPLEXITY_DEFAULT_MAX_TOKENS,

View File

@@ -16,7 +16,7 @@ maybe('PerplexitySearchProvider real API', () => {
model: process.env.PERPLEXITY_MODEL ?? PERPLEXITY_DEFAULT_MODEL,
maxTokens: PERPLEXITY_DEFAULT_MAX_TOKENS,
})
const result = await provider.search({ query: 'What is the DeepSeek Harness SDK?', maxResults: 5 })
const result = await provider.search({ query: 'What is DeepSeek Harness?', maxResults: 5 })
expect(result.content ?? '').not.toBe('')
for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//)
}, 30_000)

View File

@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import WebService from '@deepseek-ai/dsh-web'
import WebRuntime from '@deepseek-ai/dsh-web'
import {
PerplexitySearchProvider,
PERPLEXITY_PROVIDER_ID,
@@ -183,7 +183,7 @@ describe('web-search-perplexity plugin registration', () => {
it('registers the provider into ctx.web (HMR-safe)', async () => {
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ choices: [{ message: { content: 'a' } }], citations: [] })))
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: PERPLEXITY_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: PERPLEXITY_PROVIDER_ID })
const fiber = await ctx.plugin(perplexityPlugin, { apiKey: 'pplx-key' })
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ content: 'a', sources: [] })
await fiber.dispose()
@@ -199,7 +199,7 @@ describe('web-search-perplexity plugin registration', () => {
const fetchMock = vi.fn(async () => jsonResponse({ choices: [{ message: { content: 'a' } }], citations: [] }))
vi.stubGlobal('fetch', fetchMock)
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: PERPLEXITY_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: PERPLEXITY_PROVIDER_ID })
const fiber = await ctx.plugin(perplexityPlugin, { apiKey: 'pplx-key', maxTokens: 256, searchRecency: 'month' })
await ctx.web.search({ query: 'q' })
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
@@ -214,7 +214,7 @@ describe('web-search-perplexity plugin registration', () => {
const fetchMock = vi.fn(async () => jsonResponse({ choices: [{ message: { content: 'a' } }], citations: [] }))
vi.stubGlobal('fetch', fetchMock)
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: PERPLEXITY_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: PERPLEXITY_PROVIDER_ID })
const fiber = await ctx.plugin(perplexityPlugin, {})
await ctx.web.search({ query: 'q' })
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
@@ -232,7 +232,7 @@ describe('web-search-perplexity plugin registration', () => {
delete process.env.PERPLEXITY_API_KEY
try {
const ctx = new Context()
await ctx.plugin(WebService, { searchProvider: PERPLEXITY_PROVIDER_ID })
await ctx.plugin(WebRuntime, { searchProvider: PERPLEXITY_PROVIDER_ID })
await ctx.plugin(perplexityPlugin, {})
await expect(ctx.web.search({ query: 'q' }))
.rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_UNAVAILABLE' }))

View File

@@ -9,7 +9,7 @@
],
"references": [
{
"path": "../../util/environment"
"path": "../../util/launch-environment"
},
{
"path": "../../../vendor/cosmokit"
@@ -24,7 +24,7 @@
"path": "../web"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web/README.md
README.md: 237c7e28416a1f920042d2713a2b2e67dc882c42
README.zh.md: 18cfa59d60520587467df673b2869109bbfe0890
README.md: 8dfc7f032e25e40207bb3880777b814e67175a20
README.zh.md: 3354037bfcd77ca2b1a07da710b565065baf543b

View File

@@ -2,16 +2,16 @@
English | [中文](README.zh.md)
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.
The **`WebRuntime`** (`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.
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:
This package owns the Service Definition role of the web capability. Unlike shell/fs it spans two operations (search and fetch) on one seam, with potentially multiple providers each:
| Package | Role |
|---|---|
| `@deepseek-ai/dsh-web` (this) | Service Definition: the service, provider registries, selection policy, request/result vocabulary, the `WebError` taxonomy |
| `@deepseek-ai/dsh-web-search-exa` | Search provider: Exa |
| `@deepseek-ai/dsh-web-search-perplexity` | Search provider: Perplexity |
| `@deepseek-ai/dsh-web-fetch-local` | Fetch provider: anonymous public HTTP(S) |
| `@deepseek-ai/dsh-web-fetch-http` | Fetch provider: anonymous public HTTP(S) |
| `@deepseek-ai/dsh-tool-web` | Consumer: the model-facing `web_search` / `web_fetch` tool schemas over `ctx.web` |
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.

View File

@@ -2,16 +2,16 @@
[English](README.md) | 中文
**`WebService`**`ctx.web`)定义 harness 具备哪些 web 访问能力(搜索 web、抓取 URL并通过多个提供方实现不把模型约定绑定到某个厂商的 API 形状。
**`WebRuntime`**`ctx.web`)定义 harness 具备哪些 web 访问能力(搜索 web、抓取 URL并通过多个提供方实现不把模型约定绑定到某个厂商的 API 形状。
本包承担 web 能力的 Service Definition 角色。与 bash/fs 不同,它在一个 seam 上跨越搜索与抓取两种操作,每种操作都可能有多个提供方:
本包承担 web 能力的 Service Definition 角色。与 shell/fs 不同,它在一个 seam 上跨越搜索与抓取两种操作,每种操作都可能有多个提供方:
| 包 | 职责 |
|---|---|
| `@deepseek-ai/dsh-web`(本包) | Service Definition服务、提供方注册表、选择策略、请求结果词汇、`WebError` 分类体系 |
| `@deepseek-ai/dsh-web-search-exa` | 搜索提供方Exa |
| `@deepseek-ai/dsh-web-search-perplexity` | 搜索提供方Perplexity |
| `@deepseek-ai/dsh-web-fetch-local` | 抓取提供方:匿名公共 HTTP(S) |
| `@deepseek-ai/dsh-web-fetch-http` | 抓取提供方:匿名公共 HTTP(S) |
| `@deepseek-ai/dsh-tool-web` | Consumer面向模型的 `web_search``web_fetch` 工具 schema构建于 `ctx.web` 之上 |
搜索与抓取没有共享请求 schema 或业务逻辑,但有意共用一个 seam`ctx.web` 是单一 web 访问中间层,拥有一项提供方选择策略、一套中止/错误词汇和一个面向产品的「该 harness 如何访问 web」配置接口。成对的 `Search``Fetch` 方法保持并行是有意为之。

View File

@@ -34,7 +34,7 @@ export type {
declare module '@deepseek-ai/cordis' {
interface Context {
web: WebService
web: WebRuntime
}
}
@@ -52,7 +52,7 @@ interface Selection<P> {
* provider auto-selects). Operational overrides such as environment variables
* must feed these same fields rather than introduce a hidden priority chain.
*/
export interface WebServiceConfig {
export interface WebRuntimeConfig {
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
readonly searchProvider?: string
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
@@ -71,13 +71,13 @@ export interface WebServiceConfig {
* - No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
* - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
*/
export class WebService extends Service {
export class WebRuntime extends Service {
/**
* Provider selection config. Operational env overrides feed the SAME fields:
* `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
* `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
*/
static Config: z<WebServiceConfig> = z.object({
static Config: z<WebRuntimeConfig> = z.object({
searchProvider: z.string(),
fetchProvider: z.string(),
})
@@ -87,7 +87,7 @@ export class WebService extends Service {
private readonly searchProviderId: string | undefined
private readonly fetchProviderId: string | undefined
constructor(ctx: Context, config: WebServiceConfig = {}) {
constructor(ctx: Context, config: WebRuntimeConfig = {}) {
super(ctx, 'web')
this.searchProviderId = config.searchProvider ?? process.env.DSH_WEB_SEARCH_PROVIDER
this.fetchProviderId = config.fetchProvider ?? process.env.DSH_WEB_FETCH_PROVIDER
@@ -199,4 +199,4 @@ function capSources(result: WebSearchResult, maxResults: number | undefined): We
return { ...result, sources: result.sources.slice(0, maxResults), truncated: true }
}
export default WebService
export default WebRuntime

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import WebService, {
import WebRuntime, {
WebError,
type WebFetchProvider,
type WebFetchResult,
@@ -33,14 +33,14 @@ function fetchResult(marker: string): WebFetchResult {
return { url: 'https://example.com', statusCode: 200, body: { kind: 'text', content: marker }, truncated: false }
}
/** Mount a WebService on a fresh root context with the given config. */
async function mountWeb(config: ConstructorParameters<typeof WebService>[1] = {}): Promise<{ ctx: Context; web: WebService }> {
/** Mount a WebRuntime on a fresh root context with the given config. */
async function mountWeb(config: ConstructorParameters<typeof WebRuntime>[1] = {}): Promise<{ ctx: Context; web: WebRuntime }> {
const ctx = new Context()
await ctx.plugin(WebService, config)
await ctx.plugin(WebRuntime, config)
return { ctx, web: ctx.web }
}
describe('WebService registration', () => {
describe('WebRuntime registration', () => {
it('registers a search provider and unregisters it via the returned disposer', async () => {
const { web } = await mountWeb()
@@ -75,7 +75,7 @@ describe('WebService registration', () => {
})
})
describe('WebService execution resolution', () => {
describe('WebRuntime execution resolution', () => {
it('throws WEB_PROVIDER_UNAVAILABLE when nothing is registered', async () => {
const { web } = await mountWeb()
await expect(web.search({ query: 'q' })).rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_UNAVAILABLE' }))
@@ -156,7 +156,7 @@ describe('WebService execution resolution', () => {
})
})
describe('WebService maxResults enforcement', () => {
describe('WebRuntime maxResults enforcement', () => {
it('truncates sources and sets truncated when a provider over-returns', async () => {
const { web } = await mountWeb()
web.registerSearchProvider(makeSearchProvider('exa', available, () => Promise.resolve(searchResult('exa', {
@@ -188,12 +188,12 @@ describe('WebService maxResults enforcement', () => {
})
})
describe('WebService fetch capability', () => {
describe('WebRuntime fetch capability', () => {
it('resolves and runs the fetch provider independently of search', async () => {
const { web } = await mountWeb()
web.registerFetchProvider(makeFetchProvider('local-http', available, fetchResult('local-http')))
web.registerFetchProvider(makeFetchProvider('http', available, fetchResult('http')))
const result = await web.fetch({ url: 'https://example.com' })
expect(result.body.content).toBe('local-http')
expect(result.body.content).toBe('http')
expect(result.statusCode).toBe(200)
})

View File

@@ -21,7 +21,7 @@
"path": "../../llm/llm"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}