web_fetch.go

 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	)
80}