2026-06-25 15:04:12 +08:00
/**
* The model-facing `web_fetch` tool: retrieve the content of a specific URL.
* Execution goes through `ctx.web` — this module owns the model-facing schema,
* argument validation, and PRESENTATION (HTML→markdown, truncation formatting),
* while the fetch provider owns safe retrieval (transport, redirects, caps).
2026-07-08 10:06:07 +08:00
*
* The model-facing schema exposes NO timeout knob: the tool-call budget is
2026-07-08 14:40:14 +08:00
* deployment policy DECLARED via this package's `fetchTimeoutMs` config (attached
* as `ToolDefinition.timeoutMs`) and ENFORCED by `@deepseek-ai/dsh-timeout-policy`
* (a `tools/execute` wrapper), matching the reference-agent `WebFetch` shape. This
* tool just forwards the (possibly deadline-derived) `exec.signal` to `ctx.web`;
* the provider keeps its own timeout only as a resource backstop for direct callers.
2026-06-25 15:04:12 +08:00
*/
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
2026-07-03 22:52:18 +08:00
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
2026-06-25 15:04:12 +08:00
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { WebFetchBody , WebFetchResult } from '@deepseek-ai/dsh-web'
import { assertNever } from '@deepseek-ai/dsh-llm'
import type { } from '@deepseek-ai/dsh-system-prompt'
import { htmlToMarkdown } from './html.ts'
2026-07-06 22:09:30 +08:00
/**
2026-07-08 11:18:27 +08:00
* Validate value constraints the schema DSL can't express: a non-blank `url`.
* Throws a plain `Error` otherwise. No timeout parameter — the tool-call budget
2026-07-08 14:40:14 +08:00
* is deployment policy declared via `fetchTimeoutMs` config and enforced by
* `@deepseek-ai/dsh-timeout-policy`, not a model argument.
2026-07-06 22:09:30 +08:00
*
* @param args - the schema-validated `web_fetch` arguments.
2026-07-08 11:18:27 +08:00
* @returns the arguments as the seam's request fields.
2026-07-06 22:09:30 +08:00
*/
2026-07-08 10:06:07 +08:00
export function parseFetchArgs ( args : { url : string } ) : { url : string } {
2026-06-25 15:04:12 +08:00
if ( args . url . trim ( ) . length === 0 ) throw new Error ( 'url must be a non-empty string' )
2026-07-08 10:06:07 +08:00
return { url : args.url }
2026-06-25 15:04:12 +08:00
}
2026-07-06 22:09:30 +08:00
/**
* Render a fetched body to model-facing markdown text.
*
* @param body - the decoded body; `html` is converted via
* {@link htmlToMarkdown}, `text` passes through verbatim.
* @returns the text for the tool's output block.
*/
2026-06-25 15:04:12 +08:00
export function renderBody ( body : WebFetchBody ) : string {
switch ( body . kind ) {
case 'html' :
return htmlToMarkdown ( body . content )
case 'text' :
return body . content
/* v8 ignore next 2 -- WebFetchBody is a closed union; this arm is unreachable and only makes adding a kind a compile error. */
default :
return assertNever ( body , 'unhandled web fetch body kind' )
}
}
2026-07-06 22:09:30 +08:00
/**
* Format a fetch result as one model-facing text block.
*
* @param result - the seam's fetch outcome.
* @returns a `Fetched <url> (HTTP <status>)` header, the rendered body, and a
* fetch-something-narrower notice when the provider truncated the content.
*/
2026-06-25 15:04:12 +08:00
export function formatFetchOutput ( result : WebFetchResult ) : string {
const header = ` Fetched ${ result . url } (HTTP ${ result . statusCode } ) `
const footer = result . truncated ? '\n\n(Content truncated. Fetch a more specific URL or section for the full text.)' : ''
return ` ${ header } \ n \ n ${ renderBody ( result . body ) } ${ footer } `
}
2026-07-06 22:09:30 +08:00
/**
* Pending-call presentation: a fetch card titled by the URL.
*
* @param args - the raw tool arguments; only `url` feeds the view.
* @returns the generic card view (`kind: 'fetch'`) shown while the call runs.
*/
2026-07-08 10:06:07 +08:00
export function presentFetchCall ( args : { url : string } ) : GenericCallView {
2026-07-03 22:52:18 +08:00
return { card : 'generic' , title : args.url , kind : 'fetch' , rawInput : args.url }
2026-06-25 15:04:12 +08:00
}
2026-07-06 22:09:30 +08:00
/**
* Register the `web_fetch` tool and its system-prompt guidance.
*
* @param ctx - context whose `tools` and `systemPrompt` registries receive the
* registrations; both are effect-scoped and unregister on plugin dispose.
2026-07-08 14:40:14 +08:00
* @param timeoutMs - the cooperative tool-call budget (ms) attached as the tool's
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-timeout-policy` to enforce.
2026-07-06 22:09:30 +08:00
*/
2026-07-08 14:40:14 +08:00
export function applyWebFetchTool ( ctx : Context , timeoutMs : number ) : void {
2026-06-25 15:04:12 +08:00
ctx . systemPrompt . section ( {
name : 'tool:web_fetch' ,
order : 111 ,
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.' ,
} )
ctx . tools . register ( defineTool ( {
name : 'web_fetch' ,
description : 'Fetch the content of a specific HTTP(S) URL and return it decoded to text.' ,
parameters : {
url : { type : 'string' , required : true , description : 'The HTTP(S) URL to fetch.' } ,
} ,
2026-07-08 14:40:14 +08:00
timeoutMs ,
2026-06-25 15:04:12 +08:00
async execute ( args , exec ) : Promise < ContentBlock [ ] > {
const input = parseFetchArgs ( args )
const result = await ctx . web . fetch (
2026-07-08 10:06:07 +08:00
{ url : input.url } ,
2026-06-25 15:04:12 +08:00
exec . signal ? { signal : exec.signal } : undefined ,
)
return [ { type : 'text' , text : formatFetchOutput ( result ) } ]
} ,
presentCall : presentFetchCall ,
} ) )
}