feat(tool-web): declare web tool timeout budgets via config

fetchTimeoutMs/searchTimeoutMs (default 30000) resolve to each tool's
ToolDefinition.timeoutMs, moving the budget's declaration home onto the
owning tool plugin and preserving per-tool deployment override without a
mistypable central tool-name map.
This commit is contained in:
Dudu-0223
2026-07-08 14:40:14 +08:00
parent 534b1dc6d0
commit 7a822ee402
6 changed files with 74 additions and 17 deletions
+11 -6
View File
@@ -5,10 +5,11 @@
* while the fetch provider owns safe retrieval (transport, redirects, caps).
*
* The model-facing schema exposes NO timeout knob: the tool-call budget is
* deployment policy owned 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.
* 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.
*/
import type { Context } from 'cordis'
@@ -23,7 +24,8 @@ import { htmlToMarkdown } from './html.ts'
/**
* 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
* is deployment policy (`@deepseek-ai/dsh-timeout-policy`), not a model argument.
* is deployment policy declared via `fetchTimeoutMs` config and enforced by
* `@deepseek-ai/dsh-timeout-policy`, not a model argument.
*
* @param args - the schema-validated `web_fetch` arguments.
* @returns the arguments as the seam's request fields.
@@ -80,8 +82,10 @@ export function presentFetchCall(args: { url: string }): GenericCallView {
*
* @param ctx - context whose `tools` and `systemPrompt` registries receive the
* registrations; both are effect-scoped and unregister on plugin dispose.
* @param timeoutMs - the cooperative tool-call budget (ms) attached as the tool's
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-timeout-policy` to enforce.
*/
export function applyWebFetchTool(ctx: Context): void {
export function applyWebFetchTool(ctx: Context, timeoutMs: number): void {
ctx.systemPrompt.section({
name: 'tool:web_fetch',
order: 111,
@@ -94,6 +98,7 @@ export function applyWebFetchTool(ctx: Context): void {
parameters: {
url: { type: 'string', required: true, description: 'The HTTP(S) URL to fetch.' },
},
timeoutMs,
async execute(args, exec): Promise<ContentBlock[]> {
const input = parseFetchArgs(args)
const result = await ctx.web.fetch(
+18 -4
View File
@@ -33,7 +33,10 @@ export const name = 'tool-web'
/** Services required by the web tool suite. */
export const inject = ['tools', 'web', 'systemPrompt']
/** Plugin config: which web tools to register, and the `web_search` source cap. */
/** Default cooperative tool-call timeout budget (ms) for the web tools. */
export const DEFAULT_WEB_TOOL_TIMEOUT_MS = 30_000
/** Plugin config: which web tools to register, the source cap, and per-tool budgets. */
export interface Config {
/** Register `web_search`. Defaults to true. */
search?: boolean
@@ -41,12 +44,18 @@ export interface Config {
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
}
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),
})
/** The shape after schemastery applies its defaults to every field. */
@@ -61,7 +70,10 @@ function assertPositiveInteger(name: string, value: number): void {
/**
* Register the enabled web tools. `search`/`fetch` default to true; a product
* that wants only one disables the other in config. The tools' disposers are
* 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.
*/
@@ -69,6 +81,8 @@ 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)
if (resolved.search) applyWebSearchTool(ctx, resolved.searchMaxResults)
if (resolved.fetch) applyWebFetchTool(ctx)
assertPositiveInteger('fetchTimeoutMs', resolved.fetchTimeoutMs)
assertPositiveInteger('searchTimeoutMs', resolved.searchTimeoutMs)
if (resolved.search) applyWebSearchTool(ctx, resolved.searchMaxResults, resolved.searchTimeoutMs)
if (resolved.fetch) applyWebFetchTool(ctx, resolved.fetchTimeoutMs)
}
+4 -1
View File
@@ -92,8 +92,10 @@ export function presentSearchCall(args: { query: string }): GenericCallView {
* registrations; both are effect-scoped and unregister on plugin dispose.
* @param maxResults - the deployment's source cap, sent as every seam
* request's `maxResults`.
* @param timeoutMs - the cooperative tool-call budget (ms) attached as the tool's
* `ToolDefinition.timeoutMs` for `@deepseek-ai/dsh-timeout-policy` to enforce.
*/
export function applyWebSearchTool(ctx: Context, maxResults: number): void {
export function applyWebSearchTool(ctx: Context, maxResults: number, timeoutMs: number): void {
ctx.systemPrompt.section({
name: 'tool:web_search',
order: 110,
@@ -106,6 +108,7 @@ export function applyWebSearchTool(ctx: Context, maxResults: number): void {
parameters: {
query: { type: 'string', required: true, description: 'The search query.' },
},
timeoutMs,
async execute(args, exec): Promise<ContentBlock[]> {
const input = parseSearchArgs(args)
const result = await ctx.web.search(