Merge remote-tracking branch 'origin/master' into codex/simp-prune-web-seam-fields
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/core-data-structures/web.md # docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md # packages/web/tool-web/tests/integration.spec.ts # packages/web/web-fetch-local/README.md # packages/web/web-fetch-local/src/provider.ts # packages/web/web/src/types.ts
This commit is contained in:
@@ -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,8 @@ 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 does not go through tools/execute, so the tool-call
|
||||
// policy never applies; the provider's OWN timeout is the only budget. A
|
||||
// A second direct provider with a short configured backstop proves the
|
||||
// provider-owned deadline remains intact and distinct from TOOL_TIMEOUT.
|
||||
// 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({
|
||||
maxUrlLength: 2048,
|
||||
maxResponseBytes: 5_000_000,
|
||||
|
||||
@@ -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 configured `timeoutMs` 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` fires when its configured backstop elapses.
|
||||
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`](../../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
|
||||
|
||||
@@ -34,7 +36,7 @@ The numeric limits are validated at plugin construction: every cap except `maxRe
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-tool-web`, which renders this provider's `maxBodyChars`-bounded decoded text or markdown-shaped HTML under the exact fetch header and its stable failures under `Error: <message>` into retained tool history while redirects, headers, and transport mechanics remain hidden.
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -57,11 +46,8 @@ export class LocalFetchProvider implements WebFetchProvider {
|
||||
async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> {
|
||||
if (signal?.aborted) throw new WebError('web fetch aborted', 'WEB_ABORTED')
|
||||
|
||||
// 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(signal, this.limits.timeoutMs, 'WEB_FETCH_TIMEOUT')
|
||||
return await this.followAndRead(request.url, d.signal)
|
||||
}
|
||||
@@ -75,11 +61,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')
|
||||
@@ -92,10 +74,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,7 +33,11 @@ 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
|
||||
|
||||
@@ -45,7 +49,7 @@ DeepSeek returns no provider-generated answer surface this provider trusts as `c
|
||||
|
||||
### Conversation tool result, indirectly
|
||||
|
||||
**What the model sees**: Through `dsh-tool-web`, the conversation model sees deduplicated URLs, titles, dates, and citation snippets from structured search blocks; provider prose is not trusted as an answer. Provider failures become `Error: DeepSeek search aborted`, `Error: DeepSeek search request failed: <error>`, `Error: DeepSeek returned no web_search_tool_result blocks; the request may not have triggered native web search`, or `Error: DeepSeek returned an unprocessable response body: <error>`; HTTP failures pass through their provider message after `Error:`.
|
||||
**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.
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -100,18 +86,15 @@ 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 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(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
|
||||
*/
|
||||
|
||||
|
||||
@@ -293,14 +293,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)
|
||||
|
||||
@@ -27,7 +27,7 @@ Exa returns a flat `results[]` and no generated answer, so `content` is omitted.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-tool-web`, which retains this provider's `maxResults`-bounded URLs, titles, first highlights, and publication dates or exact `Error: Exa search aborted`, `Error: Exa search request failed: <error>`, and `Error: Exa returned an unprocessable response body: <error>` failures while generated answers and provider-private fields remain outside context.
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i
|
||||
|
||||
### Conversation tool result, indirectly
|
||||
|
||||
**What the model sees**: Through `dsh-tool-web`, the conversation model sees the generated answer plus structured result metadata or URL-only citations. Failures become `Error: Perplexity search aborted`, `Error: Perplexity search request failed: <error>`, or `Error: Perplexity returned an unprocessable response body: <error>`; HTTP failures pass through their provider message after `Error:`.
|
||||
**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.
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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,18 +1,7 @@
|
||||
/**
|
||||
* Vocabulary for the web capability seam (`ctx.web`): the search/fetch
|
||||
* request/result shapes providers produce and consumers format, provider
|
||||
* availability, direct cancellation control, 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
|
||||
*/
|
||||
|
||||
@@ -129,39 +118,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
|
||||
* `available()` returns false.
|
||||
* - `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 its optional 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