webSearchTool function

AgentTool webSearchTool({
  1. required WebSearchConfig config,
})

Creates the web_search tool.

Parameters:

  • query (string, required): the search query.
  • count (integer, optional): max results (default WebSearchConfig.maxResults, clamped to 1..20).
  • site (string, optional): restrict results to one domain (appended as a site: filter, which all ported providers support).

Implementation

AgentTool webSearchTool({required WebSearchConfig config}) {
  return AgentTool(
    name: 'web_search',
    label: 'web_search',
    tier: ApprovalTier.read,
    description:
        'Search the web and return ranked results with [n] citation markers, '
        'titles, URLs, and snippets. Runs a provider chain (DuckDuckGo first, '
        'keyed providers when configured) and falls through on failure. '
        'Follow up with web_fetch on a result URL to read the full page.',
    parameters: const {
      'type': 'object',
      'properties': {
        'query': {'type': 'string', 'description': 'The search query'},
        'count': {
          'type': 'integer',
          'description':
              'Maximum number of results to return (default: 10, max: 20)',
        },
        'site': {
          'type': 'string',
          'description':
              "Restrict results to a single domain, e.g. 'pub.flutter-io.cn' "
              '(appended as a site: filter)',
        },
      },
      'required': ['query'],
    },
    execute: (arguments, cancelToken, onUpdate) async {
      cancelToken?.throwIfCancelled();
      final query = (arguments['query'] as String).trim();
      if (query.isEmpty) throw StateError('query must not be empty');
      final count = clampWebSearchCount(
        (arguments['count'] as num?)?.toInt() ?? config.maxResults,
      );
      final site = (arguments['site'] as String?)?.trim();
      final effectiveQuery = site != null && site.isNotEmpty
          ? '$query site:$site'
          : query;

      final secrets = await config.secrets?.readAll() ?? const {};
      final resolved = resolveWebSearchChain(
        config.providers,
        secrets: secrets,
      );
      if (resolved.isEmpty) {
        throw StateError(
          'No web search provider configured. DuckDuckGo needs no key; set '
          'BRAVE_API_KEY or TAVILY_API_KEY for the keyed providers.',
        );
      }

      // Q1 (issue #682): search endpoints are gated like any model-invoked
      // egress. Only providers whose endpoint the live policy allows run;
      // all denied → the clean `fa_cube[<name>]:` note as a normal tool
      // result, zero client calls.
      final gate = config.networkGate;
      final chain = gate == null
          ? resolved
          : resolved
                .where((provider) => gate.allows(provider.endpoint))
                .toList();
      if (chain.isEmpty) {
        return ToolExecutionResult.text(
          gate!.denialFor(resolved.first.endpoint)!,
        );
      }

      final base = config.httpClient ?? http.Client();
      final client = gate == null ? base : GatedHttpClient(base, gate);
      try {
        final request = WebSearchRequest(
          query: effectiveQuery,
          count: count,
          client: client,
          timeout: config.timeout,
          secrets: secrets,
        );
        return await _executeChain(chain, request, query, cancelToken);
      } finally {
        if (config.httpClient == null) base.close();
      }
    },
  );
}