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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user