llm: make Tool.Run return ToolOut
This is preliminary work towards
allowing tools to add additional information.
No functional changes (at least, that's the intent).
diff --git a/claudetool/browse/browse.go b/claudetool/browse/browse.go
index e96a7f9..99f6ea7 100644
--- a/claudetool/browse/browse.go
+++ b/claudetool/browse/browse.go
@@ -182,19 +182,19 @@
}
}
-func (b *BrowseTools) navigateRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) navigateRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input navigateInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
if isPort80(input.URL) {
- return nil, fmt.Errorf("port 80 is not the port you're looking for--port 80 is the main sketch server")
+ return llm.ErrorToolOut(fmt.Errorf("port 80 is not the port you're looking for--port 80 is the main sketch server"))
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -206,10 +206,10 @@
chromedp.WaitReady("body"),
)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
- return llm.TextContent("done"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("done")}
}
// ClickTool definition
@@ -246,15 +246,15 @@
}
}
-func (b *BrowseTools) clickRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) clickRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input clickInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -273,10 +273,10 @@
err = chromedp.Run(timeoutCtx, actions...)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
- return llm.TextContent("done"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("done")}
}
// TypeTool definition
@@ -318,15 +318,15 @@
}
}
-func (b *BrowseTools) typeRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) typeRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input typeInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -346,10 +346,10 @@
err = chromedp.Run(timeoutCtx, actions...)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
- return llm.TextContent("done"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("done")}
}
// WaitForTool definition
@@ -381,15 +381,15 @@
}
}
-func (b *BrowseTools) waitForRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) waitForRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input waitForInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
timeoutCtx, cancel := context.WithTimeout(browserCtx, parseTimeout(input.Timeout))
@@ -397,10 +397,10 @@
err = chromedp.Run(timeoutCtx, chromedp.WaitReady(input.Selector))
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
- return llm.TextContent("done"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("done")}
}
// GetTextTool definition
@@ -432,15 +432,15 @@
}
}
-func (b *BrowseTools) getTextRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) getTextRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input getTextInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -453,10 +453,10 @@
chromedp.Text(input.Selector, &text),
)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
- return llm.TextContent("<innerText>" + text + "</innerText>"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("<innerText>" + text + "</innerText>")}
}
// EvalTool definition
@@ -488,15 +488,15 @@
}
}
-func (b *BrowseTools) evalRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) evalRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input evalInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -506,16 +506,16 @@
var result any
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(input.Expression, &result))
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Return the result as JSON
response, err := json.Marshal(result)
if err != nil {
- return nil, fmt.Errorf("failed to marshal response: %w", err)
+ return llm.ErrorfToolOut("failed to marshal response: %w", err)
}
- return llm.TextContent("<javascript_result>" + string(response) + "</javascript_result>"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("<javascript_result>" + string(response) + "</javascript_result>")}
}
// ScreenshotTool definition
@@ -552,15 +552,15 @@
}
}
-func (b *BrowseTools) screenshotRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) screenshotRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input screenshotInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -583,13 +583,13 @@
err = chromedp.Run(timeoutCtx, actions...)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Save the screenshot and get its ID for potential future reference
id := b.SaveScreenshot(buf)
if id == "" {
- return nil, fmt.Errorf("failed to save screenshot")
+ return llm.ErrorToolOut(fmt.Errorf("failed to save screenshot"))
}
// Get the full path to the screenshot
@@ -599,7 +599,7 @@
base64Data := base64.StdEncoding.EncodeToString(buf)
// Return the screenshot directly to the LLM
- return []llm.Content{
+ return llm.ToolOut{LLMContent: []llm.Content{
{
Type: llm.ContentTypeText,
Text: fmt.Sprintf("Screenshot taken (saved as %s)", screenshotPath),
@@ -609,7 +609,7 @@
MediaType: "image/png",
Data: base64Data,
},
- }, nil
+ }}
}
// ScrollIntoViewTool definition
@@ -641,15 +641,15 @@
}
}
-func (b *BrowseTools) scrollIntoViewRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) scrollIntoViewRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input scrollIntoViewInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -671,14 +671,14 @@
chromedp.Evaluate(script, &result),
)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
if !result {
- return nil, fmt.Errorf("element not found: %s", input.Selector)
+ return llm.ErrorToolOut(fmt.Errorf("element not found: %s", input.Selector))
}
- return llm.TextContent("done"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("done")}
}
// ResizeTool definition
@@ -715,15 +715,15 @@
}
}
-func (b *BrowseTools) resizeRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) resizeRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input resizeInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
browserCtx, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Create a timeout context for this operation
@@ -732,7 +732,7 @@
// Validate dimensions
if input.Width <= 0 || input.Height <= 0 {
- return nil, fmt.Errorf("invalid dimensions: width and height must be positive")
+ return llm.ErrorToolOut(fmt.Errorf("invalid dimensions: width and height must be positive"))
}
// Resize the browser window
@@ -740,10 +740,10 @@
chromedp.EmulateViewport(int64(input.Width), int64(input.Height)),
)
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
- return llm.TextContent("done"), nil
+ return llm.ToolOut{LLMContent: llm.TextContent("done")}
}
// GetTools returns browser tools, optionally filtering out screenshot-related tools
@@ -824,34 +824,34 @@
}
}
-func (b *BrowseTools) readImageRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) readImageRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input readImageInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
// Check if the path exists
if _, err := os.Stat(input.Path); os.IsNotExist(err) {
- return nil, fmt.Errorf("image file not found: %s", input.Path)
+ return llm.ErrorfToolOut("image file not found: %s", input.Path)
}
// Read the file
imageData, err := os.ReadFile(input.Path)
if err != nil {
- return nil, fmt.Errorf("failed to read image file: %w", err)
+ return llm.ErrorfToolOut("failed to read image file: %w", err)
}
// Detect the image type
imageType := http.DetectContentType(imageData)
if !strings.HasPrefix(imageType, "image/") {
- return nil, fmt.Errorf("file is not an image: %s", imageType)
+ return llm.ErrorfToolOut("file is not an image: %s", imageType)
}
// Encode the image as base64
base64Data := base64.StdEncoding.EncodeToString(imageData)
// Create a Content object that includes both text and the image
- return []llm.Content{
+ return llm.ToolOut{LLMContent: []llm.Content{
{
Type: llm.ContentTypeText,
Text: fmt.Sprintf("Image from %s (type: %s)", input.Path, imageType),
@@ -861,7 +861,7 @@
MediaType: imageType,
Data: base64Data,
},
- }, nil
+ }}
}
// parseTimeout parses a timeout string and returns a time.Duration
@@ -916,16 +916,16 @@
}
}
-func (b *BrowseTools) recentConsoleLogsRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) recentConsoleLogsRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input recentConsoleLogsInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
// Ensure browser is initialized
_, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Apply limit (default to 100 if not specified)
@@ -947,7 +947,7 @@
// Format the logs as JSON
logData, err := json.MarshalIndent(logs, "", " ")
if err != nil {
- return nil, fmt.Errorf("failed to serialize logs: %w", err)
+ return llm.ErrorfToolOut("failed to serialize logs: %w", err)
}
// Format the logs
@@ -961,7 +961,7 @@
sb.WriteString(string(logData))
}
- return llm.TextContent(sb.String()), nil
+ return llm.ToolOut{LLMContent: llm.TextContent(sb.String())}
}
// ClearConsoleLogsTool definition
@@ -977,16 +977,16 @@
}
}
-func (b *BrowseTools) clearConsoleLogsRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BrowseTools) clearConsoleLogsRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input clearConsoleLogsInput
if err := json.Unmarshal(m, &input); err != nil {
- return nil, fmt.Errorf("invalid input: %w", err)
+ return llm.ErrorfToolOut("invalid input: %w", err)
}
// Ensure browser is initialized
_, err := b.GetBrowserContext()
if err != nil {
- return nil, err
+ return llm.ErrorToolOut(err)
}
// Clear console logs with mutex protection
@@ -995,5 +995,5 @@
b.consoleLogs = make([]*runtime.EventConsoleAPICalled, 0)
b.consoleLogsMutex.Unlock()
- return llm.TextContent(fmt.Sprintf("Cleared %d console log entries.", logCount)), nil
+ return llm.ToolOut{LLMContent: llm.TextContent(fmt.Sprintf("Cleared %d console log entries.", logCount))}
}
diff --git a/claudetool/browse/browse_test.go b/claudetool/browse/browse_test.go
index ff17055..d1b1eec 100644
--- a/claudetool/browse/browse_test.go
+++ b/claudetool/browse/browse_test.go
@@ -177,10 +177,11 @@
inputJSON, _ := json.Marshal(input)
// Call the tool
- result, err := navTool.Run(ctx, json.RawMessage(inputJSON))
- if err != nil {
- t.Fatalf("Error running navigate tool: %v", err)
+ toolOut := navTool.Run(ctx, json.RawMessage(inputJSON))
+ if toolOut.Error != nil {
+ t.Fatalf("Error running navigate tool: %v", toolOut.Error)
}
+ result := toolOut.LLMContent
// Verify the response is successful
resultText := result[0].Text
@@ -288,10 +289,11 @@
input := fmt.Sprintf(`{"path": "%s"}`, testImagePath)
// Run the tool
- result, err := readImageTool.Run(ctx, json.RawMessage(input))
- if err != nil {
- t.Fatalf("Read image tool failed: %v", err)
+ toolOut := readImageTool.Run(ctx, json.RawMessage(input))
+ if toolOut.Error != nil {
+ t.Fatalf("Read image tool failed: %v", toolOut.Error)
}
+ result := toolOut.LLMContent
// In the updated code, result is already a []llm.Content
contents := result
@@ -338,20 +340,22 @@
// Navigate to a simple page to ensure the browser is ready
navInput := json.RawMessage(`{"url": "about:blank"}`)
- content, err := tools.NewNavigateTool().Run(ctx, navInput)
- if err != nil {
- t.Fatalf("Navigation error: %v", err)
+ toolOut := tools.NewNavigateTool().Run(ctx, navInput)
+ if toolOut.Error != nil {
+ t.Fatalf("Navigation error: %v", toolOut.Error)
}
+ content := toolOut.LLMContent
if !strings.Contains(content[0].Text, "done") {
t.Fatalf("Expected done in navigation response, got: %s", content[0].Text)
}
// Check default viewport dimensions via JavaScript
evalInput := json.RawMessage(`{"expression": "({width: window.innerWidth, height: window.innerHeight})"}`)
- content, err = tools.NewEvalTool().Run(ctx, evalInput)
- if err != nil {
- t.Fatalf("Evaluation error: %v", err)
+ toolOut = tools.NewEvalTool().Run(ctx, evalInput)
+ if toolOut.Error != nil {
+ t.Fatalf("Evaluation error: %v", toolOut.Error)
}
+ content = toolOut.LLMContent
// Parse the result to verify dimensions
var response struct {
@@ -398,30 +402,33 @@
// Resize to mobile dimensions
resizeTool := tools.NewResizeTool()
input := json.RawMessage(`{"width": 375, "height": 667}`)
- content, err := resizeTool.Run(ctx, input)
- if err != nil {
- t.Fatalf("Error: %v", err)
+ toolOut := resizeTool.Run(ctx, input)
+ if toolOut.Error != nil {
+ t.Fatalf("Error: %v", toolOut.Error)
}
+ content := toolOut.LLMContent
if !strings.Contains(content[0].Text, "done") {
t.Fatalf("Expected done in response, got: %s", content[0].Text)
}
// Navigate to a test page and verify using JavaScript to get window dimensions
navInput := json.RawMessage(`{"url": "https://example.com"}`)
- content, err = tools.NewNavigateTool().Run(ctx, navInput)
- if err != nil {
- t.Fatalf("Error: %v", err)
+ toolOut = tools.NewNavigateTool().Run(ctx, navInput)
+ if toolOut.Error != nil {
+ t.Fatalf("Error: %v", toolOut.Error)
}
+ content = toolOut.LLMContent
if !strings.Contains(content[0].Text, "done") {
t.Fatalf("Expected done in response, got: %s", content[0].Text)
}
// Check dimensions via JavaScript
evalInput := json.RawMessage(`{"expression": "({width: window.innerWidth, height: window.innerHeight})"}`)
- content, err = tools.NewEvalTool().Run(ctx, evalInput)
- if err != nil {
- t.Fatalf("Error: %v", err)
+ toolOut = tools.NewEvalTool().Run(ctx, evalInput)
+ if toolOut.Error != nil {
+ t.Fatalf("Error: %v", toolOut.Error)
}
+ content = toolOut.LLMContent
// The dimensions might not be exactly what we set (browser chrome, etc.)
// but they should be close