Files
deepseek-harness/packages/web/tool-web/src/index.ts
Chinesezjc fb0063de48 fix(tool-web): unexport renderFetchOutput so its memo stays behind the frozen path
renderFetchOutput has no external consumer: only formatFetchOutput and
fetchMetaFromValue call it, both through the registry, which deep-freezes
the result value. Exporting it let a hypothetical caller mutate a cached
input or the returned RenderedFetch and desync the card's truncated flag
from the model text. Drop it from the barrel and document that the memo
needs no defensive copy because every caller is internal and read-only.
2026-07-30 21:28:54 +08:00

90 lines
4.2 KiB
TypeScript

/**
* 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
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import type {} from '@deepseek-ai/dsh-web'
import { applyWebSearchTool, WEB_SEARCH_MAX_RESULTS } from './search.ts'
import { applyWebFetchTool } from './fetch.ts'
export { WEB_SEARCH_MAX_RESULTS, applyWebSearchTool, formatSearchOutput, parseSearchArgs, presentSearchCall, presentSearchResult, searchMetaFromValue, searchMetaFromResult } from './search.ts'
export type { WebSearchMeta } from './search.ts'
export { applyWebFetchTool, formatFetchOutput, parseFetchArgs, presentFetchCall, presentFetchResult, fetchMetaFromValue, fetchMetaFromResult } from './fetch.ts'
export type { WebFetchMeta } from './fetch.ts'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'tool-web'
/** Services required by the web tool suite. */
export const inject = ['tools', 'web', 'systemPrompt']
/** Default cooperative tool-call timeout budget (ms) for the web tools. */
export const DEFAULT_WEB_TOOL_TIMEOUT_MS = 30_000
/**
* Default cap on one `web_fetch` output and on source characters converted
* synchronously. This leaves headroom above the local provider's default
* 100,000-character body cap while bounding custom providers and rendered output.
*/
export const DEFAULT_FETCH_MAX_OUTPUT_CHARS = 200_000
/** Plugin config: which web tools to register, the source cap, per-tool budgets, and the fetch output cap. */
export interface Config {
/** Register `web_search`. Defaults to true. */
search?: boolean
/** Register `web_fetch`. Defaults to true. */
fetch?: boolean
/** Upper bound on sources returned by one `web_search` call. */
searchMaxResults?: number
/** Cooperative timeout budget (ms) for `web_fetch`. Defaults to 30000. */
fetchTimeoutMs?: number
/** Cooperative timeout budget (ms) for `web_search`. Defaults to 30000. */
searchTimeoutMs?: number
/** Cap on source characters converted and complete `web_fetch` output characters. Defaults to 200000. */
fetchMaxOutputChars?: number
}
export const Config: z<Config> = z.object({
search: z.boolean().default(true),
fetch: z.boolean().default(true),
searchMaxResults: z.number().default(WEB_SEARCH_MAX_RESULTS),
fetchTimeoutMs: z.number().default(DEFAULT_WEB_TOOL_TIMEOUT_MS),
searchTimeoutMs: z.number().default(DEFAULT_WEB_TOOL_TIMEOUT_MS),
fetchMaxOutputChars: z.number().default(DEFAULT_FETCH_MAX_OUTPUT_CHARS),
})
/** The shape after schemastery applies its defaults to every field. */
type ResolvedConfig = Required<Config>
/** Configured count, timeout, and character caps must be positive integers. */
function assertPositiveInteger(name: string, value: number): void {
if (!Number.isInteger(value) || value < 1) {
throw new Error(`tool-web: ${name} must be a positive integer`)
}
}
/**
* Register the enabled web tools. `search`/`fetch` default to true; a product
* that wants only one disables the other in config. Each tool's cooperative
* timeout budget (`fetchTimeoutMs`/`searchTimeoutMs`, default 30000) is resolved
* here and attached to the tool as `ToolDefinition.timeoutMs` for
* `@deepseek-ai/dsh-timeout-policy` to enforce. The tools' disposers are
* fiber-scoped (the effect-based registries clean up on dispose), so no manual
* teardown is needed.
*/
export function apply(ctx: Context, config: Config): void {
// schemastery (Config) has already filled every defaulted field.
const resolved = config as ResolvedConfig
assertPositiveInteger('searchMaxResults', resolved.searchMaxResults)
assertPositiveInteger('fetchTimeoutMs', resolved.fetchTimeoutMs)
assertPositiveInteger('searchTimeoutMs', resolved.searchTimeoutMs)
assertPositiveInteger('fetchMaxOutputChars', resolved.fetchMaxOutputChars)
if (resolved.search) applyWebSearchTool(ctx, resolved.searchMaxResults, resolved.searchTimeoutMs)
if (resolved.fetch) applyWebFetchTool(ctx, resolved.fetchTimeoutMs, resolved.fetchMaxOutputChars)
}