fetch.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * The model-facing `web_fetch` tool. This module owns its schema, validation, and presentation;
  3. * `ctx.web` owns retrieval. Timeout is deployment policy, not a model argument: config becomes
  4. * `ToolDefinition.timeoutMs`, timeout policy enforces it, and this tool forwards the resulting
  5. * signal. A provider timeout remains a backstop for direct seam callers.
  6. */
  7. import type { Context } from 'cordis'
  8. import { defineTool } from '@deepseek-ai/dsh-tools'
  9. import type { GenericCallView } from '@deepseek-ai/dsh-tools'
  10. import type { WebFetchBody, WebFetchResult } from '@deepseek-ai/dsh-web'
  11. import { assertNever } from '@deepseek-ai/dsh-llm'
  12. import type {} from '@deepseek-ai/dsh-system-prompt'
  13. import { htmlToMarkdown } from './html.ts'
  14. /**
  15. * Validate value constraints the schema DSL can't express: a non-blank `url`.
  16. * Throws a plain `Error` otherwise. No timeout parameter — the tool-call budget
  17. * is deployment policy declared via `fetchTimeoutMs` config and enforced by
  18. * `@deepseek-ai/dsh-timeout-policy`, not a model argument.
  19. *
  20. * @param args - the schema-validated `web_fetch` arguments.
  21. * @returns the arguments as the seam's request fields.
  22. */
  23. export function parseFetchArgs(args: { url: string }): { url: string } {
  24. if (args.url.trim().length === 0) throw new Error('url must be a non-empty string')
  25. return { url: args.url }
  26. }
  27. /**
  28. * Render a fetched body to model-facing markdown text.
  29. *
  30. * @param body - the decoded body; `html` is converted via
  31. * {@link htmlToMarkdown}, `text` passes through verbatim.
  32. * @returns the text for the tool's output block.
  33. */
  34. export function renderBody(body: WebFetchBody): string {
  35. switch (body.kind) {
  36. case 'html':
  37. return htmlToMarkdown(body.content)
  38. case 'text':
  39. return body.content
  40. /* v8 ignore next 2 -- WebFetchBody is a closed union; this arm is unreachable and only makes adding a kind a compile error. */
  41. default:
  42. return assertNever(body, 'unhandled web fetch body kind')
  43. }
  44. }
  45. /**
  46. * Format a fetch result as one model-facing text block.
  47. *
  48. * @param result - the seam's fetch outcome.
  49. * @returns a `Fetched <url> (HTTP <status>)` header, the rendered body, and a
  50. * fetch-something-narrower notice when the provider truncated the content.
  51. */
  52. export function formatFetchOutput(result: WebFetchResult): string {
  53. const header = `Fetched ${result.url} (HTTP ${result.statusCode})`
  54. const footer = result.truncated ? '\n\n(Content truncated. Fetch a more specific URL or section for the full text.)' : ''
  55. return `${header}\n\n${renderBody(result.body)}${footer}`
  56. }
  57. /**
  58. * Pending-call presentation: a fetch card titled by the URL.
  59. *
  60. * @param args - the raw tool arguments; only `url` feeds the view.
  61. * @returns the generic card view (`kind: 'fetch'`) shown while the call runs.
  62. */
  63. export function presentFetchCall(args: { url: string }): GenericCallView {
  64. return { card: 'generic', title: args.url, kind: 'fetch', rawInput: args.url }
  65. }
  66. /**
  67. * Register the `web_fetch` tool and its system-prompt guidance.
  68. *
  69. * @param ctx - context whose `tools` and `systemPrompt` registries receive the
  70. * registrations; both are effect-scoped and unregister on plugin dispose.
  71. * @param timeoutMs - the cooperative tool-call budget (ms) attached as the tool's
  72. * `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-timeout-policy` to enforce.
  73. */
  74. export function applyWebFetchTool(ctx: Context, timeoutMs: number): void {
  75. ctx.systemPrompt.section({
  76. name: 'tool:web_fetch',
  77. order: 111,
  78. text: 'Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL (for example a result from web_search). It returns the page content decoded to text. Cite the URL as a markdown link when you use its content.',
  79. })
  80. ctx.tools.register(defineTool({
  81. name: 'web_fetch',
  82. description: 'Fetch the content of a specific HTTP(S) URL and return it decoded to text.',
  83. parameters: {
  84. url: { type: 'string', required: true, description: 'The HTTP(S) URL to fetch.' },
  85. },
  86. output: {
  87. schema: {
  88. type: 'object',
  89. additionalProperties: false,
  90. properties: {
  91. url: { type: 'string', required: true },
  92. statusCode: { type: 'integer', required: true },
  93. body: {
  94. required: true,
  95. oneOf: [
  96. {
  97. type: 'object',
  98. additionalProperties: false,
  99. properties: {
  100. kind: { type: 'string', required: true, const: 'html' },
  101. content: { type: 'string', required: true },
  102. },
  103. },
  104. {
  105. type: 'object',
  106. additionalProperties: false,
  107. properties: {
  108. kind: { type: 'string', required: true, const: 'text' },
  109. content: { type: 'string', required: true },
  110. },
  111. },
  112. ],
  113. },
  114. truncated: { type: 'boolean', required: true },
  115. },
  116. },
  117. render: (_args, value) => [{ type: 'text', text: formatFetchOutput(value) }],
  118. },
  119. timeoutMs,
  120. // Provider reads do not mutate parent-agent state.
  121. isConcurrencySafe: () => true,
  122. async execute(args, exec) {
  123. const input = parseFetchArgs(args)
  124. const result = await ctx.web.fetch(
  125. { url: input.url },
  126. exec.signal,
  127. )
  128. return {
  129. url: result.url,
  130. statusCode: result.statusCode,
  131. body: { kind: result.body.kind, content: result.body.content },
  132. truncated: result.truncated,
  133. }
  134. },
  135. presentCall: presentFetchCall,
  136. }))
  137. }