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}