1package tools
2
3import (
4 "context"
5 _ "embed"
6 "log/slog"
7 "net/http"
8 "time"
9
10 "charm.land/fantasy"
11)
12
13//go:embed web_search.md
14var webSearchToolDescription []byte
15
16// NewWebSearchTool creates a web search tool for sub-agents (no permissions needed).
17func NewWebSearchTool(client *http.Client) fantasy.AgentTool {
18 if client == nil {
19 transport := http.DefaultTransport.(*http.Transport).Clone()
20 transport.MaxIdleConns = 100
21 transport.MaxIdleConnsPerHost = 10
22 transport.IdleConnTimeout = 90 * time.Second
23
24 client = &http.Client{
25 Timeout: 30 * time.Second,
26 Transport: transport,
27 }
28 }
29
30 return fantasy.NewParallelAgentTool(
31 WebSearchToolName,
32 string(webSearchToolDescription),
33 func(ctx context.Context, params WebSearchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
34 if params.Query == "" {
35 return fantasy.NewTextErrorResponse("query is required"), nil
36 }
37
38 maxResults := params.MaxResults
39 if maxResults <= 0 {
40 maxResults = 10
41 }
42 if maxResults > 20 {
43 maxResults = 20
44 }
45
46 maybeDelaySearch()
47 results, err := searchDuckDuckGo(ctx, client, params.Query, maxResults)
48 slog.Debug("Web search completed", "query", params.Query, "results", len(results), "err", err)
49 if err != nil {
50 return fantasy.NewTextErrorResponse("Failed to search: " + err.Error()), nil
51 }
52
53 return fantasy.NewTextResponse(formatSearchResults(results)), nil
54 })
55}