web_fetch.go

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