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