Merge remote-tracking branch 'origin/master' into pr-265
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/rfc/INDEX.md # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/tools/README.md # packages/core/tools/src/index.ts # packages/core/tools/src/schema.ts # packages/ui/acp/src/index.ts # packages/ui/stdio-agent/README.md
This commit is contained in:
@@ -35,3 +35,53 @@ Both tools declare `isConcurrencySafe: () => true` — they are read-only (fetch
|
||||
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.
|
||||
|
||||
The tool never calls a provider's `status()` 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.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### System prompt
|
||||
|
||||
**What the model sees**: Search and fetch contribute the web-search and web-fetch guidance below. A scoped tool restriction does not remove these independently registered sections.
|
||||
|
||||
**Token effect**: Fixed guidance cost per request for each config-enabled tool, even when a restriction hides its schema.
|
||||
|
||||
#### Web search guidance
|
||||
|
||||
```markdown
|
||||
Use the web_search tool to discover current information on the web. It returns an optional answer plus a list of source URLs. Follow up with web_fetch when you need the full content of a specific result, and cite the relevant URLs as markdown links.
|
||||
```
|
||||
|
||||
#### Web fetch guidance
|
||||
|
||||
```markdown
|
||||
Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL (for example a result from web_search). It returns the page content decoded to text. Cite the URL as a markdown link when you use its content.
|
||||
```
|
||||
|
||||
### Tool schemas
|
||||
|
||||
**What the model sees**: The model sees the generated [`web_search` and `web_fetch` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-web). Result-count and timeout budgets are deployment settings, not model arguments.
|
||||
|
||||
**Token effect**: Fixed schema cost per request; config disablement removes both schema and guidance, while a scoped restriction removes only the schema.
|
||||
|
||||
### Search result
|
||||
|
||||
**What the model sees**: The optional provider-owned answer is followed by `Sources:` and data-dependent lines shaped exactly `- [<title-or-url>](<url>)`, optionally suffixed ` — <snippet> (<publishedAt>)`. With neither answer nor sources the result says `No results found.` A capped list adds `(Showing the first <count> sources. Refine the query for more.)`; every result ends `Cite the relevant URLs above as markdown links in your answer.`
|
||||
|
||||
**Token effect**: Data-dependent results are resent until compaction and sources are capped by `searchMaxResults`.
|
||||
|
||||
### Fetch result
|
||||
|
||||
**What the model sees**: A successful fetch is exactly `Fetched <finalUrl> (HTTP <statusCode>)`, a blank line, and the provider-owned decoded body. Truncation adds a blank line and `(Content truncated. Fetch a more specific URL or section for the full text.)`; failures become `Error: <message>`. Queries and URLs remain in call history.
|
||||
|
||||
**Token effect**: Provider caps bound body size; retained call arguments and results are resent until compaction, and timeout policy can replace a late result with a short error.
|
||||
|
||||
### Argument errors
|
||||
|
||||
**What the model sees**: Blank inputs become exactly `Error: query must be a non-empty string` or `Error: url must be a non-empty string`.
|
||||
|
||||
**Token effect**: Only the failing call adds these retained tokens.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **`htmlToMarkdown` is a minimal regex converter, not an HTML parser** — it strips script/style/noscript, keeps headings/bullets/links, and decodes about a dozen named entities; tables, images, and nested formatting are lost.
|
||||
- **The model-facing surface is minimal by design, with promotions deferred** — `max_results` stays a config bound (not a model argument), and `web_fetch` takes only `url` (no `format`/`prompt`/LLM-summarization mode); both are named later steps in [the seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md).
|
||||
- **No web-specific permission policy** — both tools execute without requesting `ctx.approval`; a deployment that needs confirmation must add a `tools/pre-execute` policy, and the package does not define persistent URL/domain grants.
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
/**
|
||||
* The model-facing `web_fetch` tool: retrieve the content of a specific URL.
|
||||
* Execution goes through `ctx.web` — this module owns the model-facing schema,
|
||||
* argument validation, and PRESENTATION (HTML→markdown, truncation formatting),
|
||||
* while the fetch provider owns safe retrieval (transport, redirects, caps).
|
||||
*
|
||||
* The model-facing schema exposes NO timeout knob: the tool-call budget is
|
||||
* deployment policy DECLARED via this package's `fetchTimeoutMs` config (attached
|
||||
* as `ToolDefinition.timeoutMs`) and ENFORCED by `@deepseek-ai/dsh-timeout-policy`
|
||||
* (a `tools/execute` wrapper), matching the reference-agent `WebFetch` shape. This
|
||||
* tool just forwards the (possibly deadline-derived) `exec.signal` to `ctx.web`;
|
||||
* the provider keeps its own timeout only as a resource backstop for direct callers.
|
||||
* The model-facing `web_fetch` tool. This module owns its schema, validation, and presentation;
|
||||
* `ctx.web` owns retrieval. Timeout is deployment policy, not a model argument: config becomes
|
||||
* `ToolDefinition.timeoutMs`, timeout policy enforces it, and this tool forwards the resulting
|
||||
* signal. A provider timeout remains a backstop for direct seam callers.
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/**
|
||||
* Minimal, dependency-free HTML→markdown-ish text conversion for `web_fetch`
|
||||
* presentation. This is intentionally NOT a full HTML parser: it strips
|
||||
* script/style/noscript, drops tags, decodes the common named/numeric entities,
|
||||
* and collapses whitespace into a readable plain-text approximation with a few
|
||||
* markdown affordances (headings, list bullets, links). A heavier converter can
|
||||
* replace this without touching the seam or the tool schema.
|
||||
*
|
||||
* Minimal dependency-free HTML-to-readable-text conversion for `web_fetch`, not a full parser. It
|
||||
* removes non-content elements and tags, decodes common entities, collapses whitespace, and keeps
|
||||
* basic headings, lists, and links. A richer converter can replace it without changing the seam or
|
||||
* tool schema.
|
||||
* @module @deepseek-ai/dsh-tool-web/html
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
/**
|
||||
* The model-facing web tool suite (`web_search`, `web_fetch`) over the `ctx.web`
|
||||
* seam. This root plugin registers the tools the product has ENABLED, composing
|
||||
* the per-tool registration helpers (`applyWebSearchTool`, `applyWebFetchTool`).
|
||||
*
|
||||
* The package owns model-facing concerns only — tool names, JSON schemas,
|
||||
* argument validation, prompt sections, result-cap constants, result formatting,
|
||||
* HTML→markdown presentation. All web access goes through `ctx.web`; this
|
||||
* package never imports a concrete provider package.
|
||||
*
|
||||
* Tool registration follows product/app ENABLEMENT, not backend availability: a
|
||||
* tool stays visible even when its selected provider is missing/misconfigured,
|
||||
* and execution fails with a structured `WebError` (resolved by the seam at call
|
||||
* time). That keeps the model schema stable without making plugin load order,
|
||||
* credential state, or HMR timing part of the model-facing contract.
|
||||
*
|
||||
* Model-facing `web_search` and `web_fetch` tools over `ctx.web`. This package owns schemas,
|
||||
* validation, prompt guidance, limits, and presentation, never concrete providers. Enablement
|
||||
* controls tool registration; an enabled tool remains visible when its provider is unavailable
|
||||
* and fails with a structured error at execution time.
|
||||
* @module @deepseek-ai/dsh-tool-web
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* Integration: the real fetch backend (`dsh-web-fetch-local`) + 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()` — nothing bypasses the tool registry.
|
||||
* Fetch hits a real loopback HTTP server (verifying the WORLD); search runs the
|
||||
* real Exa provider over a stubbed global `fetch` (the network is the one
|
||||
* boundary we mock).
|
||||
* Integration: the real fetch backend (`dsh-web-fetch-local`) + 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()` —
|
||||
* nothing bypasses the tool registry. Fetch verifies world effects against loopback HTTP; search
|
||||
* uses the real Exa provider with only its network boundary stubbed.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -159,10 +157,9 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc
|
||||
})
|
||||
|
||||
it('the provider backstop still protects a DIRECT ctx.web.fetch() call (no tool-call policy in that path)', async () => {
|
||||
// A direct seam caller does not go through tools/execute, so the tool-call
|
||||
// policy never applies; the provider's OWN timeout is the only budget. A
|
||||
// short per-request hint proves the provider backstop is intact and classifies
|
||||
// as WEB_FETCH_TIMEOUT (the provider-owned code), never TOOL_TIMEOUT.
|
||||
// A direct seam caller does not go through tools/execute, so the tool-call policy never
|
||||
// applies; the provider's own timeout is the only budget. A short request hint must therefore
|
||||
// produce provider-owned `WEB_FETCH_TIMEOUT`, never `TOOL_TIMEOUT`.
|
||||
const err = await tctx.web.fetch({ url: slowBase, timeoutMs: 50 }).then(
|
||||
() => undefined,
|
||||
(e: unknown) => e as { code?: string },
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
/**
|
||||
* Real-load-path guard for @deepseek-ai/dsh-tool-web. `tool-web` is a NAMESPACE
|
||||
* plugin with `inject` — so a stray `export default apply` would make the cordis
|
||||
* Loader's `unwrapExports` (`exports.default ?? exports`) collapse the module to
|
||||
* the bare `apply` function, DROPPING `inject`. The plugin would then read
|
||||
* `ctx.web` without having injected it and throw `cannot get property … without
|
||||
* inject` the moment it loads (postmortem 0001).
|
||||
*
|
||||
* A hand-built `ctx.plugin({ apply, inject })` mount CANNOT catch that — it
|
||||
* bypasses `unwrapExports`. So this test unwraps the module through the REAL
|
||||
* `Loader.prototype.unwrapExports` and mounts the result over `ctx.web`,
|
||||
* exercising the exact path the Loader uses. Prove the guard bites: add
|
||||
* `export default apply` to `src/index.ts`, watch this go red, revert.
|
||||
* Real Loader-path guard for an injected namespace plugin. A default export would make
|
||||
* `unwrapExports` collapse the namespace and drop `inject`, causing access to `ctx.web` to fail.
|
||||
* Hand-built mounting bypasses that path, so this test unwraps through the real Loader first; see
|
||||
* postmortem 0001.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -41,7 +33,7 @@ describe('dsh-tool-web real-load-path guard', () => {
|
||||
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(toolWeb) as Parameters<Context['plugin']>[0]
|
||||
// A collapsed export shape (dropped inject) would throw "without inject" here.
|
||||
// Mounting the collapsed shape would throw for missing injection here.
|
||||
const fiber = await ctx.plugin(unwrapped)
|
||||
expect(ctx.tools.schemas().map(s => s.name)).toEqual(expect.arrayContaining(['web_search', 'web_fetch']))
|
||||
await fiber.dispose()
|
||||
|
||||
@@ -8,7 +8,9 @@ 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`/`maxTimeoutMs` is a **resource backstop** for direct `ctx.web.fetch()` callers and misconfigured deployments — it is NOT the model-facing tool-call budget. The tool-call budget for `web_fetch` is deployment policy owned by [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md), which arms a per-call deadline on `exec.signal`. A shipped web-tool deployment sets the provider backstop **above** the `tool-timeout` budget, so the tool-call policy normally wins for model calls (returning `TOOL_TIMEOUT`); when the outer deadline signal reaches this provider first, it classifies as `WEB_ABORTED` and the outer wrapper replaces the result with `TOOL_TIMEOUT`. The provider's own `WEB_FETCH_TIMEOUT` only fires for a direct seam caller whose own budget elapsed.
|
||||
The provider's `timeoutMs`/`maxTimeoutMs` is a resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments, not the model-facing tool-call budget. [`dsh-timeout-policy`](../../timeout/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 seam caller whose provider budget elapsed.
|
||||
|
||||
## Transport hygiene
|
||||
|
||||
@@ -33,6 +35,12 @@ The provider's `timeoutMs`/`maxTimeoutMs` is a **resource backstop** for direct
|
||||
|
||||
The numeric limits are validated at plugin construction: every cap except `maxRedirects` must be a positive finite number, and `maxRedirects` must be a non-negative integer. An invalid value throws rather than silently constructing a provider with nonsensical limits.
|
||||
|
||||
## Security note
|
||||
## Model Experience
|
||||
|
||||
SSRF / private-network protection (blocking private, loopback, link-local, multicast, and otherwise non-public destinations, with DNS-resolve-then-validate and per-hop re-validation) is **deferred** — see the [web capability seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets.
|
||||
Indirectly, through [`dsh-tool-web`](../tool-web/README.md), which places this provider's `maxBodyChars`-bounded decoded text or markdown-shaped HTML under its fetch-result wrapper and retains provider failures while redirects, headers, and transport mechanics remain hidden.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **SSRF / private-network protection is deferred** — no blocking of private, loopback, link-local, multicast, or otherwise non-public destinations, no DNS-resolve-then-validate, no per-hop re-validation (see [the web capability seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets.
|
||||
- **Only textual content decodes** — html/xhtml and `text/*`-plus-JSON/XML families; a missing `Content-Type` or any binary type throws `WEB_UNSUPPORTED_CONTENT_TYPE`, and text-extractable PDF decoding is named deferred work.
|
||||
- **Charset comes only from the `Content-Type` header** (UTF-8 default) — an HTML `<meta charset>` declaration is ignored, and a declared-but-unrecognized charset label throws rather than falling back.
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
/**
|
||||
* `LocalFetchProvider`: a `WebFetchProvider` that retrieves a concrete public
|
||||
* HTTP(S) URL with platform-native `fetch` at the repo's Node floor and returns a status
|
||||
* code plus bounded decoded content. It owns SAFE RESOURCE RETRIEVAL — URL
|
||||
* validation, redirect policy, timeout, abort, byte caps, charset decoding,
|
||||
* content-type classification, binary rejection — but NOT presentation
|
||||
* (HTML→markdown lives in `@deepseek-ai/dsh-tool-web`).
|
||||
*
|
||||
* Redirects are followed manually (`redirect: 'manual'`) so the provider can
|
||||
* enforce a same-origin-only policy: a cross-origin redirect is refused with
|
||||
* `WEB_REDIRECT_BLOCKED`, requiring a fresh tool call (Claude Code's WebFetch
|
||||
* uses the same model). It does NOT carry browser cookies, editor/git
|
||||
* credentials, or implicit access to private services.
|
||||
*
|
||||
* SSRF / private-network protection is DEFERRED (see the package RFC); until it
|
||||
* lands this provider is an SSRF primitive and must not be enabled where it can
|
||||
* reach sensitive internal targets.
|
||||
* Safe HTTP(S) retrieval for `ctx.web`: validates URLs, follows only same-origin redirects,
|
||||
* enforces time and size limits, classifies and decodes text, and leaves presentation to
|
||||
* `@deepseek-ai/dsh-tool-web`. Requests carry no browser cookies or ambient credentials.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -60,11 +49,8 @@ export class LocalFetchProvider implements WebFetchProvider {
|
||||
if (exec?.signal?.aborted) throw new WebError('web fetch aborted', 'WEB_ABORTED')
|
||||
const timeoutMs = clampTimeout(request.timeoutMs, this.limits.timeoutMs, this.limits.maxTimeoutMs)
|
||||
|
||||
// One deadline signal fuses the caller's abort with our own timeout, so the
|
||||
// network request and the streaming read both stop on either. The timeout
|
||||
// abort carries a TimeoutReason we recover afterward to classify the cause
|
||||
// (translateAbortOrNetwork), instead of hand-rolling a controller + timer +
|
||||
// reason-recovery dance.
|
||||
// One signal stops both the request and body read. The deadline's TimeoutReason later
|
||||
// distinguishes this provider's timeout from caller or outer-deadline cancellation.
|
||||
using d = deadline(exec?.signal, timeoutMs, 'WEB_FETCH_TIMEOUT')
|
||||
return await this.followAndRead(request.url, d.signal)
|
||||
}
|
||||
@@ -78,11 +64,7 @@ export class LocalFetchProvider implements WebFetchProvider {
|
||||
const response = await this.requestOnce(currentUrl, signal)
|
||||
|
||||
if (isRedirectStatus(response.status)) {
|
||||
// The redirect budget is enforced BEFORE this hop's target is resolved
|
||||
// or origin-checked, so `maxRedirects: N` follows at most N redirects
|
||||
// exactly: the (N+1)th redirect is refused as "exceeded" regardless of
|
||||
// where it points (a same-origin/cross-origin distinction on a hop we
|
||||
// are not allowed to follow would be the wrong diagnosis).
|
||||
// Enforce the redirect budget before resolving or validating the next hop.
|
||||
if (redirectsFollowed >= this.limits.maxRedirects) {
|
||||
await response.body?.cancel()
|
||||
throw new WebError(`exceeded the maximum of ${this.limits.maxRedirects} redirects`, 'WEB_REDIRECT_BLOCKED')
|
||||
@@ -95,10 +77,9 @@ export class LocalFetchProvider implements WebFetchProvider {
|
||||
throw new WebError(`redirect response (HTTP ${response.status}) without a Location header`, 'WEB_PROVIDER_ERROR')
|
||||
}
|
||||
const target = resolveRedirect(location, currentUrl)
|
||||
// Re-validate the target against the same transport hygiene a direct
|
||||
// request gets: a redirect must not be a back door to a credentialed,
|
||||
// non-http(s), or over-long URL that validateFetchUrl would reject. A
|
||||
// rejection here must still cancel the body first (see below).
|
||||
// Re-validate the target against the same transport hygiene a direct request gets: a
|
||||
// redirect must not be a back door to a credentialed, non-http(s), or over-long URL
|
||||
// that validateFetchUrl would reject.
|
||||
let validatedTarget: URL
|
||||
try {
|
||||
validatedTarget = validateFetchUrl(target.toString(), this.limits.maxUrlLength)
|
||||
|
||||
@@ -33,4 +33,29 @@ It reuses `$DEEPSEEK_API_KEY` (no new secret) but **not** `$DEEPSEEK_BASE_URL`:
|
||||
|
||||
## Mapping
|
||||
|
||||
DeepSeek returns no provider-generated answer surface this provider trusts as `content`, so `content` is omitted. `sources[]` is built from the `web_search_result` items inside `web_search_tool_result` blocks: `url` ← `url`, `title` ← `title`, `publishedAt` ← `page_age`. The per-source `snippet` lives separately in a `text` block's `citations[]` (a `cited_text` keyed by `url`), so the provider joins the two — a result with no citation excerpt simply has no `snippet`. Results are deduped by `url` (a `maxUses > 1` request can surface the same URL across searches). DeepSeek's `web_search` has no result-count knob (only `maxUses`), so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`). Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`.
|
||||
DeepSeek returns no provider-generated answer surface this provider trusts as `content`, so `content` is omitted. `sources[]` comes from `web_search_result` items inside `web_search_tool_result` blocks: `url` ← `url`, `title` ← `title`, and `publishedAt` ← `page_age`. Snippets live separately as URL-keyed `cited_text` entries in a text block's `citations[]`; the provider joins them, leaving `snippet` absent when no excerpt exists.
|
||||
|
||||
Results are deduplicated by URL because one request may surface the same page across searches. DeepSeek exposes `maxUses`, not a result-count knob, so the seam enforces `maxResults` by truncating `sources[]` and setting `truncated`.
|
||||
|
||||
Provider failures become `WEB_PROVIDER_ERROR`; caller cancellation becomes `WEB_ABORTED`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Auxiliary DeepSeek search request
|
||||
|
||||
**What the model sees**: A separate DeepSeek model receives exactly `Perform a web search for the query: <query>` as its user text and one native `web_search` server-tool definition. This request is not part of the conversation model's context.
|
||||
|
||||
**Token effect**: Separate provider input and output tokens are incurred for each search; `maxTokens` caps generated output and `maxUses` caps native search uses.
|
||||
|
||||
### Conversation tool result, indirectly
|
||||
|
||||
**What the model sees**: Through [`dsh-tool-web`](../tool-web/README.md), the conversation model sees deduplicated URLs, titles, dates, and citation snippets from structured search blocks; provider prose is not trusted as an answer. This provider's exact failures are `DeepSeek search aborted`, `DeepSeek search request failed: <error>`, `DeepSeek returned no web_search_tool_result blocks; the request may not have triggered native web search`, and `DeepSeek returned an unprocessable response body: <error>`; HTTP failures preserve the provider message. The consumer owns the error wrapper.
|
||||
|
||||
**Token effect**: Zero direct conversation tokens from registration. Result tokens scale with returned sources and snippets, then the seam enforces the requested source bound.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **One search costs a full Messages model turn** — latency plus generated tokens, with up to `maxUses` server-side searches; DeepSeek exposes no dedicated retrieval endpoint.
|
||||
- **Over-returned sources still cost tokens** — with no result-count knob on the wire, `maxResults` is enforced only post-hoc by seam truncation.
|
||||
- **Uncited results carry no `snippet`** — a source gains one only when a `text` block citation (`cited_text`) matches its URL.
|
||||
- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`.
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
/**
|
||||
* `@deepseek-ai/dsh-web-search-deepseek`: registers a DeepSeek-backed
|
||||
* `WebSearchProvider` with `ctx.web`. A function/namespace plugin (NOT a
|
||||
* default-export service): it registers INTO the seam's provider registry, like
|
||||
* `@deepseek-ai/dsh-llm-deepseek` registers an adapter into `ctx.llm`.
|
||||
*
|
||||
* The provider talks to DeepSeek's Anthropic-compatible Messages API with the
|
||||
* native `web_search_20250305` server tool. It reuses `$DEEPSEEK_API_KEY` (no
|
||||
* new secret) but NOT `$DEEPSEEK_BASE_URL` — the search endpoint is the
|
||||
* Anthropic-compatible base, distinct from the chat-completions base the LLM
|
||||
* adapter uses.
|
||||
*
|
||||
* Register a DeepSeek-backed provider in `ctx.web`. It calls the Anthropic-compatible Messages API
|
||||
* with native `web_search_20250305`. The provider reuses `DEEPSEEK_API_KEY` but not
|
||||
* `DEEPSEEK_BASE_URL`, because search and chat-completions use different bases.
|
||||
* @module @deepseek-ai/dsh-web-search-deepseek
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
/**
|
||||
* `DeepSeekSearchProvider`: a `WebSearchProvider` backed by DeepSeek's
|
||||
* Anthropic-compatible Messages API with the native `web_search_20250305` server
|
||||
* tool enabled.
|
||||
*
|
||||
* Unlike a dedicated search endpoint (Exa's `POST /search`, Perplexity's
|
||||
* `/chat/completions`), this issues a FULL Messages model call carrying a server
|
||||
* tool, so a search costs a complete model turn in latency and tokens. In return
|
||||
* DeepSeek runs the search server-side and returns STRUCTURED
|
||||
* `web_search_tool_result` blocks — this provider parses those blocks and never
|
||||
* scrapes URLs out of model prose. Strict mode: if the response carries no
|
||||
* `web_search_tool_result` block (native search did not trigger), it throws
|
||||
* `WEB_PROVIDER_ERROR` rather than degrading to prose-scraping.
|
||||
*
|
||||
* Network requests use platform-native `fetch` at the repo's Node floor, mirroring
|
||||
* `@deepseek-ai/dsh-llm-deepseek`'s adapter — not a cordis HTTP-client service.
|
||||
* The Anthropic wire shape is a provider-private detail and does NOT make this
|
||||
* provider depend on `ctx.llm`.
|
||||
*
|
||||
* DeepSeek search through an Anthropic-compatible Messages model call with the native
|
||||
* `web_search_20250305` server tool. Each search costs a model turn, but returns structured
|
||||
* result blocks; absence of those blocks is an error rather than a prose-scraping fallback.
|
||||
* The wire format and native `fetch` client are provider-private and do not use `ctx.llm`.
|
||||
* @module @deepseek-ai/dsh-web-search-deepseek/provider
|
||||
*/
|
||||
|
||||
@@ -101,19 +87,16 @@ export function citationSnippets(blocks: readonly ContentBlock[]): Map<string, s
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a DeepSeek Anthropic Messages response to a normalized search result.
|
||||
* Walks `web_search_tool_result` blocks for citeable `web_search_result` items,
|
||||
* joins each to its citation excerpt as `snippet`, and dedupes by `url` (a
|
||||
* `max_uses > 1` request can surface the same URL across searches). The seam
|
||||
* owns the final `maxResults` truncation, so `truncated` is always `false` here.
|
||||
*
|
||||
* Throws `WEB_PROVIDER_ERROR` (strict mode) when no `web_search_tool_result`
|
||||
* block is present — native search did not trigger, and prose-scraping is not a
|
||||
* fallback.
|
||||
* Map a DeepSeek Anthropic Messages response to a normalized search result. Walks
|
||||
* `web_search_tool_result` blocks for citeable `web_search_result` items, joins each to its
|
||||
* citation excerpt as `snippet`, and dedupes by `url` (a `max_uses > 1` request can surface
|
||||
* the same URL across searches). The seam owns the final `maxResults` truncation, so
|
||||
* `truncated` is always `false` here.
|
||||
*
|
||||
* @param query - the original request query, echoed on the result.
|
||||
* @param response - the parsed Messages response body.
|
||||
* @returns the normalized result with deduped, snippet-joined sources.
|
||||
* @throws {@link WebError} when native search produced no result block.
|
||||
*/
|
||||
export function mapAnthropicResponse(query: string, response: AnthropicResponse): WebSearchResult {
|
||||
const blocks = response.content ?? []
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
/**
|
||||
* Wire types for DeepSeek's Anthropic-compatible Messages API
|
||||
* (`POST {baseURL}/messages`) with the native `web_search_20250305` server tool
|
||||
* enabled. Types only — no runtime code.
|
||||
*
|
||||
* DeepSeek returns structured content blocks: `web_search_tool_result` blocks
|
||||
* carry the citeable `web_search_result` items (`url`/`title`/`page_age`), while
|
||||
* the snippet/excerpt for a URL lives separately in a `text` block's
|
||||
* `citations[]` (a `cited_text` keyed by `url`). The provider joins the two.
|
||||
*
|
||||
* The Anthropic wire shape is a provider-private detail; it does not make this
|
||||
* provider depend on `ctx.llm`.
|
||||
*
|
||||
* Provider-private wire types for DeepSeek's Anthropic-compatible Messages API. Citeable
|
||||
* result items and citation excerpts arrive in separate blocks; the provider joins them by
|
||||
* URL. These types do not create a dependency on `ctx.llm`.
|
||||
* @module @deepseek-ai/dsh-web-search-deepseek/types
|
||||
*/
|
||||
|
||||
|
||||
@@ -300,14 +300,8 @@ describe('web-search-deepseek plugin registration', () => {
|
||||
})
|
||||
|
||||
it('survives the real Loader unwrapExports path keeping name/inject/Config', () => {
|
||||
// A stray `export default apply` would make the cordis Loader's
|
||||
// unwrapExports (`exports.default ?? exports`) collapse the module to the
|
||||
// bare `apply` function, DROPPING `inject: ['web']` — the plugin would then
|
||||
// read ctx.web without injecting it and throw "cannot get property … without
|
||||
// inject" the moment it loads. A hand-built ctx.plugin(namespace) mount
|
||||
// bypasses unwrapExports and cannot catch that, so drive the real path.
|
||||
// Prove it bites: add `export default apply` to src/index.ts, watch this go
|
||||
// red, revert.
|
||||
// A default export would make `unwrapExports` collapse the namespace and drop `inject: ['web']`.
|
||||
// Drive the real Loader path because hand-built namespace mounting cannot expose that failure.
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(deepseekPlugin) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(deepseekPlugin)
|
||||
|
||||
@@ -24,3 +24,13 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i
|
||||
## Mapping
|
||||
|
||||
Exa returns a flat `results[]` and no generated answer, so `content` is omitted. Each result maps to a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← the first non-empty `highlights[]` entry (a result with no highlight has no portable snippet and is dropped), `publishedAt` ← `publishedDate`. A request's `maxResults` wins over the configured `numResults` default and is sent as Exa's `numResults` for a cost/latency optimization; the final bound is enforced by the seam. Provider failures (HTTP errors, network failure, unparseable or wrong-shape bodies) surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through [`dsh-tool-web`](../tool-web/README.md), which retains this provider's `maxResults`-bounded URLs, titles, first highlights, and publication dates or its exact `Exa search aborted`, `Exa search request failed: <error>`, and `Exa returned an unprocessable response body: <error>` failures under the consumer's error wrapper while generated answers and provider-private fields remain outside context.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **A result with no non-blank highlight is dropped entirely** — no portable snippet to map, so fewer sources than the requested count can return.
|
||||
- **Only `searchType`/`numResults`/`highlightsPerResult` are exposed** — Exa's other controls (livecrawl, category, domain/date filters, full-text contents) wait on provider-neutral seam fields ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)).
|
||||
- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`.
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
/**
|
||||
* `ExaSearchProvider`: a `WebSearchProvider` backed by the Exa search API
|
||||
* (`POST /search` with highlight contents). Maps Exa's flat `results[]` into the
|
||||
* seam's normalized `WebSearchResult`. Exa returns no provider-generated answer,
|
||||
* so `content` is omitted; each result maps to a `WebSearchSource` with `url`,
|
||||
* `title`, the first highlight as `snippet`, and `publishedDate` as
|
||||
* `publishedAt`.
|
||||
*
|
||||
* Network requests use platform-native `fetch` at the repo's Node floor, mirroring
|
||||
* `@deepseek-ai/dsh-llm-deepseek`'s adapter — not a cordis HTTP-client service.
|
||||
*
|
||||
* `ExaSearchProvider`: a `WebSearchProvider` backed by the Exa search API (`POST /search` with
|
||||
* highlight contents). It maps the first non-blank highlight to `snippet`, maps
|
||||
* `publishedDate` to `publishedAt`, drops entries without a snippet, and omits `content`
|
||||
* because Exa returns no generated answer.
|
||||
* @module @deepseek-ai/dsh-web-search-exa/provider
|
||||
*/
|
||||
|
||||
|
||||
@@ -24,3 +24,24 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i
|
||||
## Mapping
|
||||
|
||||
`content` ← `choices[0].message.content` (the generated answer). `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` ← `date`), falling back to the URL-only `citations[]` array only when `search_results` is absent — those sources carry just a `url`, which is why `title`/`snippet`/`publishedAt` are optional on the seam. Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. Perplexity has no result-count control, so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`).
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Auxiliary Perplexity request
|
||||
|
||||
**What the model sees**: A separate Perplexity model receives `<query>` verbatim as its sole user message through the chat-completions endpoint. This request is not part of the conversation model's context.
|
||||
|
||||
**Token effect**: Separate provider tokens are incurred per search; `maxTokens` caps the generated answer.
|
||||
|
||||
### Conversation tool result, indirectly
|
||||
|
||||
**What the model sees**: Through [`dsh-tool-web`](../tool-web/README.md), the conversation model sees the generated answer plus structured result metadata or URL-only citations. This provider's exact failures are `Perplexity search aborted`, `Perplexity search request failed: <error>`, and `Perplexity returned an unprocessable response body: <error>`; HTTP failures preserve the provider message. The consumer owns the error wrapper.
|
||||
|
||||
**Token effect**: Zero direct conversation tokens from registration. Answer and source tokens are data-dependent, source count is seam-bounded, and the retained result or error is resent until compaction.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Citation-fallback sources are URL-only** — when Perplexity omits structured `search_results[]`, sources carry no `title`/`snippet`/`publishedAt`, so the tool renders bare hostname labels.
|
||||
- **Over-returned sources still cost tokens and latency** — with no result-count control on the wire, `maxResults` is enforced only post-hoc by seam truncation.
|
||||
- **Only `model`/`maxTokens`/`searchRecency` are exposed** — Perplexity's other search controls (domain filters, `web_search_options` context size, images) wait on provider-neutral seam fields ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)).
|
||||
- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`.
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
/**
|
||||
* `PerplexitySearchProvider`: a `WebSearchProvider` backed by the Perplexity
|
||||
* search API (an OpenAI-compatible `POST /chat/completions`). Maps the generated
|
||||
* answer (`choices[0].message.content`) into `content`, and prefers the
|
||||
* structured `search_results[]` for `sources[]`, falling back to the URL-only
|
||||
* `citations[]` when `search_results` is absent.
|
||||
*
|
||||
* Network requests use platform-native `fetch` at the repo's Node floor, mirroring
|
||||
* `@deepseek-ai/dsh-llm-deepseek`'s adapter. The OpenAI-compatible request shape
|
||||
* is a provider-private detail and does NOT make this provider depend on
|
||||
* Perplexity search over its OpenAI-compatible chat-completions endpoint. The generated answer
|
||||
* becomes `content`; sources prefer structured `search_results[]` and fall back to URL-only
|
||||
* `citations[]`. The wire format and native `fetch` client are provider-private and do not use
|
||||
* `ctx.llm`.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-web-search-perplexity/provider
|
||||
*/
|
||||
|
||||
@@ -99,12 +92,16 @@ export class PerplexitySearchProvider implements WebSearchProvider {
|
||||
|
||||
constructor(private readonly options: PerplexitySearchProviderOptions) {}
|
||||
|
||||
// Availability checks stay beside each provider's distinct config contract;
|
||||
// a shared base class would obscure which fields make this backend usable.
|
||||
/* jscpd:ignore-start */
|
||||
status(): WebProviderStatus {
|
||||
if (this.options.apiKey.length === 0) return { available: false, reason: 'missing-credential' }
|
||||
if (!URL.canParse(this.options.baseURL)) return { available: false, reason: 'misconfigured' }
|
||||
if (!isPositiveInteger(this.options.maxTokens)) return { available: false, reason: 'misconfigured' }
|
||||
return { available: true }
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
async search(request: WebSearchRequest, exec?: { readonly signal?: AbortSignal }): Promise<WebSearchResult> {
|
||||
let response: Response
|
||||
@@ -159,6 +156,9 @@ export class PerplexitySearchProvider implements WebSearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// These two predicates are intentionally local: exporting generic internals
|
||||
// from the public web seam would cost more API surface than these pure checks.
|
||||
/* jscpd:ignore-start */
|
||||
/** True for a fetch/`AbortSignal` abort, surfaced as `WEB_ABORTED`. */
|
||||
function isAbortError(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === 'AbortError'
|
||||
@@ -168,3 +168,4 @@ function isAbortError(error: unknown): boolean {
|
||||
function isPositiveInteger(value: number): boolean {
|
||||
return Number.isInteger(value) && value > 0
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
/**
|
||||
* Wire types for the Perplexity search API
|
||||
* (`POST https://api.perplexity.ai/chat/completions`, an OpenAI-compatible chat
|
||||
* shape). Types only — no runtime code. Perplexity returns a generated answer in
|
||||
* `choices[0].message.content` plus citation surfaces: a structured
|
||||
* `search_results[]` (preferred) and a URL-only `citations[]` fallback.
|
||||
*
|
||||
* The OpenAI-compatible wire shape is a provider-private detail; it does not make
|
||||
* this provider depend on `ctx.llm`.
|
||||
*
|
||||
* Wire types for the Perplexity search API (`POST https://api.perplexity.ai/chat/completions`,
|
||||
* an OpenAI-compatible chat shape). Results prefer structured `search_results` and fall back to
|
||||
* URL-only `citations`; the provider-private wire shape does not depend on `ctx.llm`.
|
||||
* @module @deepseek-ai/dsh-web-search-perplexity/types
|
||||
*/
|
||||
|
||||
|
||||
@@ -42,3 +42,14 @@ The failure branches throw `WebError`, whose structured code (plus message detai
|
||||
## Vocabulary
|
||||
|
||||
`WebSearchRequest` (`query`, `maxResults?`) → `WebSearchResult` (`providerId`, `query`, `content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`, `timeoutMs?`) → `WebFetchResult` (`providerId`, final `url`, `statusCode`, `body`, `truncated`); `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.
|
||||
|
||||
## Model Experience
|
||||
|
||||
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.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **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 ([RFC](../../../docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)).
|
||||
- **`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 RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)).
|
||||
- **`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.
|
||||
- **Provider-backed page extraction is out of scope of `fetch()`** — a Firecrawl/Tavily-style `web_extract` capability is deferred rather than widening the fetch seam.
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
/**
|
||||
* The web access seam (`ctx.web`): a provider registry plus a provider-selecting
|
||||
* execution surface for two capabilities — search and fetch. Provider packages
|
||||
* register concrete backends with `registerSearchProvider` /
|
||||
* `registerFetchProvider`; the model-facing consumer
|
||||
* (`@deepseek-ai/dsh-tool-web`) executes through `search()` / `fetch()` and
|
||||
* routes on the structured {@link WebError} codes selection throws.
|
||||
*
|
||||
* The registry half stays close to `LlmService`: a `Map<id, provider>` per
|
||||
* capability kind, register methods that return disposers, duplicate ids that
|
||||
* throw, and execution-time resolution that throws when the selected provider is
|
||||
* absent or unusable — with selection rules that never depend on registration
|
||||
* order.
|
||||
*
|
||||
* The web access seam (`ctx.web`): registries and provider-selecting execution for search and
|
||||
* fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
|
||||
* be usable; without one, exactly one usable provider is required, so selection never depends
|
||||
* on registration order.
|
||||
* @module @deepseek-ai/dsh-web
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
/**
|
||||
* Vocabulary for the web capability seam (`ctx.web`): the search/fetch
|
||||
* request/result shapes providers produce and consumers format, the provider
|
||||
* status discriminant selection reads, the execution-control context, and the
|
||||
* typed error taxonomy.
|
||||
*
|
||||
* These types are shared by every provider backend
|
||||
* (`@deepseek-ai/dsh-web-search-exa`, `@deepseek-ai/dsh-web-search-perplexity`,
|
||||
* `@deepseek-ai/dsh-web-fetch-local`, and future backends) and by the
|
||||
* model-facing consumer (`@deepseek-ai/dsh-tool-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, one abort/error vocabulary, and one product-facing configuration
|
||||
* point. The cost is the parallel `Search`/`Fetch` shapes below; that
|
||||
* parallelism is intentional.
|
||||
*
|
||||
* Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
|
||||
* seam so provider selection, cancellation, errors, and product configuration have one owner,
|
||||
* while retaining separate request and result shapes.
|
||||
* @module @deepseek-ai/dsh-web/types
|
||||
*/
|
||||
|
||||
@@ -162,39 +150,11 @@ export interface WebFetchProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed web error. Extends {@link HarnessError} so it carries a stable,
|
||||
* machine-routable `code` (a `string`, like every other seam's error) and
|
||||
* chains `cause`. `ToolRegistry.execute()` converts a thrown `WebError` into an
|
||||
* error tool result whose structured metadata exposes the code, so callers
|
||||
* (hooks, tests, UI) route on it.
|
||||
*
|
||||
* The `code` is an open `string`, NOT a closed union: a provider may raise its
|
||||
* own codes without editing this package, and a consumer must tolerate an
|
||||
* unknown code (a future provider will introduce ones this file never named).
|
||||
* The codes split by who owns them — seam-neutral codes any provider may see,
|
||||
* versus codes specific to a single implementation:
|
||||
*
|
||||
* Seam-neutral (raised by `WebService` selection and the shared contract):
|
||||
* - `WEB_PROVIDER_UNAVAILABLE`: no provider configured and none usable.
|
||||
* - `WEB_PROVIDER_CONFIGURED_MISSING`: a configured id is not registered.
|
||||
* - `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`: a configured id is registered but its
|
||||
* `status()` reports unavailable.
|
||||
* - `WEB_PROVIDER_AMBIGUOUS`: no id configured and multiple usable providers
|
||||
* exist (selection refuses to pick by registration order).
|
||||
* - `WEB_DUPLICATE_PROVIDER`: a registration-time programming error — an id is
|
||||
* already registered for that capability kind.
|
||||
* - `WEB_ABORTED`: the operation was aborted via `WebExecContext.signal`.
|
||||
* - `WEB_PROVIDER_ERROR`: catch-all for a provider's own failure surfaced
|
||||
* through the seam, including network/transport failure (DNS, connection
|
||||
* refused, TLS).
|
||||
*
|
||||
* Fetch-transport codes (owned by the `dsh-web-fetch-local` implementation; a
|
||||
* different fetch backend need not raise these and may raise its own):
|
||||
* - `WEB_INVALID_URL`: the fetch URL is malformed or not http(s).
|
||||
* - `WEB_BLOCKED_URL`: the fetch URL is rejected by policy (credentials in URL).
|
||||
* - `WEB_REDIRECT_BLOCKED`: a cross-origin redirect was refused.
|
||||
* - `WEB_FETCH_TOO_LARGE`: the response exceeded the byte/character cap.
|
||||
* - `WEB_FETCH_TIMEOUT`: the fetch exceeded its timeout.
|
||||
* - `WEB_UNSUPPORTED_CONTENT_TYPE`: the response content type cannot be decoded.
|
||||
* Typed web error with a machine-routable, open-string `code` and chained `cause`.
|
||||
* Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
|
||||
* missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
|
||||
* the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
|
||||
* size and timeout limits, and unsupported content types. Tool execution exposes the code in
|
||||
* structured error metadata.
|
||||
*/
|
||||
export class WebError extends HarnessError {}
|
||||
|
||||
Reference in New Issue
Block a user