index.ts 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. /**
  2. * The model-facing web tool suite (`web_search`, `web_fetch`) over the `ctx.web`
  3. * seam. This root plugin registers the tools the product has ENABLED, composing
  4. * the per-tool registration helpers (`applyWebSearchTool`, `applyWebFetchTool`).
  5. *
  6. * The package owns model-facing concerns only — tool names, JSON schemas,
  7. * argument validation, prompt sections, result-cap constants, result formatting,
  8. * HTML→markdown presentation. All web access goes through `ctx.web`; this
  9. * package never imports a concrete provider package.
  10. *
  11. * Tool registration follows product/app ENABLEMENT, not backend availability: a
  12. * tool stays visible even when its selected provider is missing/misconfigured,
  13. * and execution fails with a structured `WebError` (resolved by the seam at call
  14. * time). That keeps the model schema stable without making plugin load order,
  15. * credential state, or HMR timing part of the model-facing contract.
  16. *
  17. * @module @deepseek-ai/dsh-tool-web
  18. */
  19. import type { Context } from 'cordis'
  20. import z from 'schemastery'
  21. import type {} from '@deepseek-ai/dsh-web'
  22. import { applyWebSearchTool, WEB_SEARCH_MAX_RESULTS } from './search.ts'
  23. import { applyWebFetchTool } from './fetch.ts'
  24. export { WEB_SEARCH_MAX_RESULTS, applyWebSearchTool, formatSearchOutput, parseSearchArgs, presentSearchCall } from './search.ts'
  25. export { applyWebFetchTool, formatFetchOutput, parseFetchArgs, presentFetchCall, renderBody } from './fetch.ts'
  26. export { htmlToMarkdown } from './html.ts'
  27. /** Cordis plugin name used by loader diagnostics. */
  28. export const name = 'tool-web'
  29. /** Services required by the web tool suite. */
  30. export const inject = ['tools', 'web', 'systemPrompt']
  31. /** Default cooperative tool-call timeout budget (ms) for the web tools. */
  32. export const DEFAULT_WEB_TOOL_TIMEOUT_MS = 30_000
  33. /** Plugin config: which web tools to register, the source cap, and per-tool budgets. */
  34. export interface Config {
  35. /** Register `web_search`. Defaults to true. */
  36. search?: boolean
  37. /** Register `web_fetch`. Defaults to true. */
  38. fetch?: boolean
  39. /** Upper bound on sources returned by one `web_search` call. */
  40. searchMaxResults?: number
  41. /** Cooperative timeout budget (ms) for `web_fetch`. Defaults to 30000. */
  42. fetchTimeoutMs?: number
  43. /** Cooperative timeout budget (ms) for `web_search`. Defaults to 30000. */
  44. searchTimeoutMs?: number
  45. }
  46. export const Config: z<Config> = z.object({
  47. search: z.boolean().default(true),
  48. fetch: z.boolean().default(true),
  49. searchMaxResults: z.number().default(WEB_SEARCH_MAX_RESULTS),
  50. fetchTimeoutMs: z.number().default(DEFAULT_WEB_TOOL_TIMEOUT_MS),
  51. searchTimeoutMs: z.number().default(DEFAULT_WEB_TOOL_TIMEOUT_MS),
  52. })
  53. /** The shape after schemastery applies its defaults to every field. */
  54. type ResolvedConfig = Required<Config>
  55. /** The result cap must be a positive integer (it bounds a provider's source list). */
  56. function assertPositiveInteger(name: string, value: number): void {
  57. if (!Number.isInteger(value) || value < 1) {
  58. throw new Error(`tool-web: ${name} must be a positive integer`)
  59. }
  60. }
  61. /**
  62. * Register the enabled web tools. `search`/`fetch` default to true; a product
  63. * that wants only one disables the other in config. Each tool's cooperative
  64. * timeout budget (`fetchTimeoutMs`/`searchTimeoutMs`, default 30000) is resolved
  65. * here and attached to the tool as `ToolDefinition.timeoutMs` for
  66. * `@deepseek-ai/dsh-timeout-policy` to enforce. The tools' disposers are
  67. * fiber-scoped (the effect-based registries clean up on dispose), so no manual
  68. * teardown is needed.
  69. */
  70. export function apply(ctx: Context, config: Config): void {
  71. // schemastery (Config) has already filled every defaulted field.
  72. const resolved = config as ResolvedConfig
  73. assertPositiveInteger('searchMaxResults', resolved.searchMaxResults)
  74. assertPositiveInteger('fetchTimeoutMs', resolved.fetchTimeoutMs)
  75. assertPositiveInteger('searchTimeoutMs', resolved.searchTimeoutMs)
  76. if (resolved.search) applyWebSearchTool(ctx, resolved.searchMaxResults, resolved.searchTimeoutMs)
  77. if (resolved.fetch) applyWebFetchTool(ctx, resolved.fetchTimeoutMs)
  78. }