/** * 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 } from './search.ts' export { applyWebFetchTool, formatFetchOutput, parseFetchArgs, presentFetchCall } 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 = 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 /** 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) }