1package tools
2
3import (
4 "context"
5 _ "embed"
6 "fmt"
7 "html/template"
8 "net/http"
9 "os"
10 "strings"
11 "time"
12
13 "charm.land/fantasy"
14)
15
16//go:embed web_fetch.md.tpl
17var webFetchDescriptionTmpl []byte
18
19var webFetchDescriptionTpl = template.Must(
20 template.New("webFetchDescription").
21 Parse(string(webFetchDescriptionTmpl)),
22)
23
24// NewWebFetchTool creates a simple web fetch tool for sub-agents (no permissions needed).
25func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
26 if client == nil {
27 transport := http.DefaultTransport.(*http.Transport).Clone()
28 transport.MaxIdleConns = 100
29 transport.MaxIdleConnsPerHost = 10
30 transport.IdleConnTimeout = 90 * time.Second
31
32 client = &http.Client{
33 Timeout: 30 * time.Second,
34 Transport: transport,
35 }
36 }
37
38 return fantasy.NewParallelAgentTool(
39 WebFetchToolName,
40 renderToolDescription(webFetchDescriptionTpl),
41 func(ctx context.Context, params WebFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
42 if params.URL == "" {
43 return fantasy.NewTextErrorResponse("url is required"), nil
44 }
45
46 content, err := FetchURLAndConvert(ctx, client, params.URL)
47 if err != nil {
48 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to fetch URL: %s", err)), nil
49 }
50
51 hasLargeContent := len(content) > LargeContentThreshold
52 var result strings.Builder
53
54 if hasLargeContent {
55 tempFile, err := os.CreateTemp(workingDir, "page-*.md")
56 if err != nil {
57 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary file: %s", err)), nil
58 }
59 tempFilePath := tempFile.Name()
60
61 if _, err := tempFile.WriteString(content); err != nil {
62 _ = tempFile.Close() // Best effort close
63 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to write content to file: %s", err)), nil
64 }
65 if err := tempFile.Close(); err != nil {
66 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to close temporary file: %s", err)), nil
67 }
68
69 fmt.Fprintf(&result, "Fetched content from %s (large page)\n\n", params.URL)
70 fmt.Fprintf(&result, "Content saved to: %s\n\n", tempFilePath)
71 result.WriteString("Use the view and grep tools to analyze this file.")
72 } else {
73 fmt.Fprintf(&result, "Fetched content from %s:\n\n", params.URL)
74 result.WriteString(content)
75 }
76
77 return fantasy.NewTextResponse(result.String()), nil
78 })
79}