browse: refactor tool responses to match claudetool patterns

This was vibe-coded, but the vibe was wrong.
Make it look like the rest of the code,
which will ease upcoming refactoring work.
Switch from JSON to XML-ish for textual tool outputs.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: se50bf57009dfd97ak
diff --git a/claudetool/browse/browse.go b/claudetool/browse/browse.go
index f600c72..928860d 100644
--- a/claudetool/browse/browse.go
+++ b/claudetool/browse/browse.go
@@ -141,14 +141,6 @@
 	return b.browserCtx, nil
 }
 
-func successResponse() string {
-	return `{"status":"success"}`
-}
-
-func errorResponse(err error) string {
-	return fmt.Sprintf(`{"status":"error","error":"%s"}`, err.Error())
-}
-
 // NavigateTool definition
 type navigateInput struct {
 	URL     string `json:"url"`
@@ -191,16 +183,16 @@
 func (b *BrowseTools) navigateRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input navigateInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	if isPort80(input.URL) {
-		return llm.TextContent(errorResponse(fmt.Errorf("port 80 is not the port you're looking for--it is the main sketch server"))), nil
+		return nil, 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 llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -212,10 +204,10 @@
 		chromedp.WaitReady("body"),
 	)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	return llm.TextContent(successResponse()), nil
+	return llm.TextContent("done"), nil
 }
 
 // ClickTool definition
@@ -255,12 +247,12 @@
 func (b *BrowseTools) clickRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input clickInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -279,10 +271,10 @@
 
 	err = chromedp.Run(timeoutCtx, actions...)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	return llm.TextContent(successResponse()), nil
+	return llm.TextContent("done"), nil
 }
 
 // TypeTool definition
@@ -327,12 +319,12 @@
 func (b *BrowseTools) typeRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input typeInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -352,10 +344,10 @@
 
 	err = chromedp.Run(timeoutCtx, actions...)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	return llm.TextContent(successResponse()), nil
+	return llm.TextContent("done"), nil
 }
 
 // WaitForTool definition
@@ -390,12 +382,12 @@
 func (b *BrowseTools) waitForRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input waitForInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	timeoutCtx, cancel := context.WithTimeout(browserCtx, parseTimeout(input.Timeout))
@@ -403,10 +395,10 @@
 
 	err = chromedp.Run(timeoutCtx, chromedp.WaitReady(input.Selector))
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	return llm.TextContent(successResponse()), nil
+	return llm.TextContent("done"), nil
 }
 
 // GetTextTool definition
@@ -415,15 +407,11 @@
 	Timeout  string `json:"timeout,omitempty"`
 }
 
-type getTextOutput struct {
-	Text string `json:"text"`
-}
-
 // NewGetTextTool creates a tool for getting text from elements
 func (b *BrowseTools) NewGetTextTool() *llm.Tool {
 	return &llm.Tool{
 		Name:        "browser_get_text",
-		Description: "Get the innerText of an element. Can be used to read the web page.",
+		Description: "Get the innerText of an element, returned in innerText tag. Can be used to read the web page.",
 		InputSchema: json.RawMessage(`{
 			"type": "object",
 			"properties": {
@@ -445,12 +433,12 @@
 func (b *BrowseTools) getTextRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input getTextInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -463,16 +451,10 @@
 		chromedp.Text(input.Selector, &text),
 	)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	output := getTextOutput{Text: text}
-	result, err := json.Marshal(output)
-	if err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("failed to marshal response: %w", err))), nil
-	}
-
-	return llm.TextContent(string(result)), nil
+	return llm.TextContent("<innerText>" + text + "</innerText>"), nil
 }
 
 // EvalTool definition
@@ -481,10 +463,6 @@
 	Timeout    string `json:"timeout,omitempty"`
 }
 
-type evalOutput struct {
-	Result any `json:"result"`
-}
-
 // NewEvalTool creates a tool for evaluating JavaScript
 func (b *BrowseTools) NewEvalTool() *llm.Tool {
 	return &llm.Tool{
@@ -511,12 +489,12 @@
 func (b *BrowseTools) evalRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input evalInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -526,16 +504,16 @@
 	var result any
 	err = chromedp.Run(timeoutCtx, chromedp.Evaluate(input.Expression, &result))
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	output := evalOutput{Result: result}
-	response, err := json.Marshal(output)
+	// Return the result as JSON
+	response, err := json.Marshal(result)
 	if err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("failed to marshal response: %w", err))), nil
+		return nil, fmt.Errorf("failed to marshal response: %w", err)
 	}
 
-	return llm.TextContent(string(response)), nil
+	return llm.TextContent("<javascript_result>" + string(response) + "</javascript_result>"), nil
 }
 
 // ScreenshotTool definition
@@ -575,12 +553,12 @@
 func (b *BrowseTools) screenshotRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input screenshotInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -603,13 +581,13 @@
 
 	err = chromedp.Run(timeoutCtx, actions...)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Save the screenshot and get its ID for potential future reference
 	id := b.SaveScreenshot(buf)
 	if id == "" {
-		return llm.TextContent(errorResponse(fmt.Errorf("failed to save screenshot"))), nil
+		return nil, fmt.Errorf("failed to save screenshot")
 	}
 
 	// Get the full path to the screenshot
@@ -664,12 +642,12 @@
 func (b *BrowseTools) scrollIntoViewRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input scrollIntoViewInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -691,14 +669,14 @@
 		chromedp.Evaluate(script, &result),
 	)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	if !result {
-		return llm.TextContent(errorResponse(fmt.Errorf("element not found: %s", input.Selector))), nil
+		return nil, fmt.Errorf("element not found: %s", input.Selector)
 	}
 
-	return llm.TextContent(successResponse()), nil
+	return llm.TextContent("done"), nil
 }
 
 // ResizeTool definition
@@ -738,12 +716,12 @@
 func (b *BrowseTools) resizeRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input resizeInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	browserCtx, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Create a timeout context for this operation
@@ -752,7 +730,7 @@
 
 	// Validate dimensions
 	if input.Width <= 0 || input.Height <= 0 {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid dimensions: width and height must be positive"))), nil
+		return nil, fmt.Errorf("invalid dimensions: width and height must be positive")
 	}
 
 	// Resize the browser window
@@ -760,10 +738,10 @@
 		chromedp.EmulateViewport(int64(input.Width), int64(input.Height)),
 	)
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
-	return llm.TextContent(successResponse()), nil
+	return llm.TextContent("done"), nil
 }
 
 // GetTools returns browser tools, optionally filtering out screenshot-related tools
@@ -847,24 +825,24 @@
 func (b *BrowseTools) readImageRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input readImageInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	// Check if the path exists
 	if _, err := os.Stat(input.Path); os.IsNotExist(err) {
-		return llm.TextContent(errorResponse(fmt.Errorf("image file not found: %s", input.Path))), nil
+		return nil, fmt.Errorf("image file not found: %s", input.Path)
 	}
 
 	// Read the file
 	imageData, err := os.ReadFile(input.Path)
 	if err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("failed to read image file: %w", err))), nil
+		return nil, fmt.Errorf("failed to read image file: %w", err)
 	}
 
 	// Detect the image type
 	imageType := http.DetectContentType(imageData)
 	if !strings.HasPrefix(imageType, "image/") {
-		return llm.TextContent(errorResponse(fmt.Errorf("file is not an image: %s", imageType))), nil
+		return nil, fmt.Errorf("file is not an image: %s", imageType)
 	}
 
 	// Encode the image as base64
@@ -939,13 +917,13 @@
 func (b *BrowseTools) recentConsoleLogsRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input recentConsoleLogsInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	// Ensure browser is initialized
 	_, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Apply limit (default to 100 if not specified)
@@ -967,7 +945,7 @@
 	// Format the logs as JSON
 	logData, err := json.MarshalIndent(logs, "", "  ")
 	if err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("failed to serialize logs: %w", err))), nil
+		return nil, fmt.Errorf("failed to serialize logs: %w", err)
 	}
 
 	// Format the logs
@@ -1000,13 +978,13 @@
 func (b *BrowseTools) clearConsoleLogsRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input clearConsoleLogsInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return llm.TextContent(errorResponse(fmt.Errorf("invalid input: %w", err))), nil
+		return nil, fmt.Errorf("invalid input: %w", err)
 	}
 
 	// Ensure browser is initialized
 	_, err := b.GetBrowserContext()
 	if err != nil {
-		return llm.TextContent(errorResponse(err)), nil
+		return nil, err
 	}
 
 	// Clear console logs with mutex protection
diff --git a/claudetool/browse/browse_test.go b/claudetool/browse/browse_test.go
index 024f685..65be1e9 100644
--- a/claudetool/browse/browse_test.go
+++ b/claudetool/browse/browse_test.go
@@ -183,22 +183,13 @@
 	}
 
 	// Verify the response is successful
-	var response struct {
-		Status string `json:"status"`
-		Error  string `json:"error,omitempty"`
-	}
-
 	resultText := result[0].Text
-	if err := json.Unmarshal([]byte(resultText), &response); err != nil {
-		t.Fatalf("Error unmarshaling response: %v", err)
-	}
-
-	if response.Status != "success" {
+	if !strings.Contains(resultText, "done") {
 		// If browser automation is not available, skip the test
-		if strings.Contains(response.Error, "browser automation not available") {
+		if strings.Contains(resultText, "browser automation not available") {
 			t.Skip("Browser automation not available in this environment")
 		} else {
-			t.Errorf("Expected status 'success', got '%s' with error: %s", response.Status, response.Error)
+			t.Fatalf("Expected done in result text, got: %s", resultText)
 		}
 	}
 
@@ -351,8 +342,8 @@
 	if err != nil {
 		t.Fatalf("Navigation error: %v", err)
 	}
-	if !strings.Contains(content[0].Text, "success") {
-		t.Fatalf("Expected success in navigation response, got: %s", content[0].Text)
+	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
@@ -412,8 +403,8 @@
 		if err != nil {
 			t.Fatalf("Error: %v", err)
 		}
-		if !strings.Contains(content[0].Text, "success") {
-			t.Fatalf("Expected success in response, got: %s", content[0].Text)
+		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
@@ -422,8 +413,8 @@
 		if err != nil {
 			t.Fatalf("Error: %v", err)
 		}
-		if !strings.Contains(content[0].Text, "success") {
-			t.Fatalf("Expected success in response, got: %s", content[0].Text)
+		if !strings.Contains(content[0].Text, "done") {
+			t.Fatalf("Expected done in response, got: %s", content[0].Text)
 		}
 
 		// Check dimensions via JavaScript
diff --git a/loop/testdata/agent_loop.httprr b/loop/testdata/agent_loop.httprr
index 21c3b28..49e76da 100644
--- a/loop/testdata/agent_loop.httprr
+++ b/loop/testdata/agent_loop.httprr
@@ -1,9 +1,9 @@
 httprr trace v1
-20665 2568
+20692 2587
 POST https://api.anthropic.com/v1/messages HTTP/1.1

 Host: api.anthropic.com

 User-Agent: Go-http-client/1.1

-Content-Length: 20467

+Content-Length: 20494

 Anthropic-Version: 2023-06-01

 Content-Type: application/json

 

@@ -440,7 +440,7 @@
   },
   {
    "name": "browser_get_text",
-   "description": "Get the innerText of an element. Can be used to read the web page.",
+   "description": "Get the innerText of an element, returned in innerText tag. Can be used to read the web page.",
    "input_schema": {
     "type": "object",
     "properties": {
@@ -603,24 +603,24 @@
 Anthropic-Organization-Id: 3c473a21-7208-450a-a9f8-80aebda45c1b

 Anthropic-Ratelimit-Input-Tokens-Limit: 200000

 Anthropic-Ratelimit-Input-Tokens-Remaining: 200000

-Anthropic-Ratelimit-Input-Tokens-Reset: 2025-06-18T17:37:58Z

+Anthropic-Ratelimit-Input-Tokens-Reset: 2025-07-02T22:43:53Z

 Anthropic-Ratelimit-Output-Tokens-Limit: 80000

 Anthropic-Ratelimit-Output-Tokens-Remaining: 80000

-Anthropic-Ratelimit-Output-Tokens-Reset: 2025-06-18T17:38:05Z

+Anthropic-Ratelimit-Output-Tokens-Reset: 2025-07-02T22:44:03Z

 Anthropic-Ratelimit-Requests-Limit: 4000

 Anthropic-Ratelimit-Requests-Remaining: 3999

-Anthropic-Ratelimit-Requests-Reset: 2025-06-18T17:37:56Z

+Anthropic-Ratelimit-Requests-Reset: 2025-07-02T22:43:47Z

 Anthropic-Ratelimit-Tokens-Limit: 280000

 Anthropic-Ratelimit-Tokens-Remaining: 280000

-Anthropic-Ratelimit-Tokens-Reset: 2025-06-18T17:37:58Z

+Anthropic-Ratelimit-Tokens-Reset: 2025-07-02T22:43:53Z

 Cf-Cache-Status: DYNAMIC

-Cf-Ray: 951c8eb9ef9fcf1a-SJC

+Cf-Ray: 9591a9fb9af39e58-SJC

 Content-Type: application/json

-Date: Wed, 18 Jun 2025 17:38:05 GMT

-Request-Id: req_011CQG6NuhqCivuiY6qpfgrF

+Date: Wed, 02 Jul 2025 22:44:03 GMT

+Request-Id: req_011CQj12QtwdhXutWELw6Xs3

 Server: cloudflare

 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

 Via: 1.1 google

 X-Robots-Tag: none

 

-{"id":"msg_019AFgFq1PXMS9XCNuWC9mQt","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Here are the tools available to me:\n\n**Code & File Management:**\n- `bash` - Execute shell commands\n- `patch` - Edit files with precise text operations\n- `keyword_search` - Search codebase by keywords/concepts\n\n**Task Management:**\n- `todo_read` / `todo_write` - Track and manage task lists\n- `think` - Record thoughts and plans\n\n**Git & Development:**\n- `commit-message-style` - Get git commit message guidance\n- `codereview` - Run automated code review\n- `done` - Complete tasks with verification checklist\n\n**Browser Automation:**\n- `browser_navigate` - Navigate to URLs\n- `browser_click` - Click elements\n- `browser_type` - Type text into inputs\n- `browser_wait_for` - Wait for elements\n- `browser_get_text` - Read page text\n- `browser_eval` - Execute JavaScript\n- `browser_scroll_into_view` - Scroll to elements\n- `browser_resize` - Resize browser window\n- `browser_take_screenshot` - Capture screenshots\n- `browser_recent_console_logs` / `browser_clear_console_logs` - Manage console logs\n\n**Utilities:**\n- `read_image` - Read and encode image files\n- `multiplechoice` - Present multiple choice questions\n- `about_sketch` - Get information about Sketch environment\n- `set-slug` - Set conversation identifier"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":4734,"cache_read_input_tokens":0,"output_tokens":332,"service_tier":"standard"}}
\ No newline at end of file
+{"id":"msg_01UtTaMqbCD6qFYqz7s8w1Yi","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Here are the tools available to me:\n\n**File & Code Management:**\n- `bash` - Execute shell commands\n- `patch` - Modify files with precise text edits\n- `keyword_search` - Search codebase by keywords\n\n**Task Management:**\n- `todo_read` - Read current todo list\n- `todo_write` - Create and manage task lists\n- `think` - Record thoughts and plans\n\n**Git & Code Quality:**\n- `commit-message-style` - Get git commit message guidance\n- `codereview` - Run automated code review\n- `done` - Complete work with verification checklist\n\n**Browser Automation:**\n- `browser_navigate` - Navigate to URLs\n- `browser_click` - Click elements\n- `browser_type` - Type text into inputs\n- `browser_wait_for` - Wait for elements\n- `browser_get_text` - Read element text\n- `browser_eval` - Execute JavaScript\n- `browser_scroll_into_view` - Scroll to elements\n- `browser_resize` - Resize browser window\n- `browser_take_screenshot` - Capture screenshots\n- `browser_recent_console_logs` - Get console logs\n- `browser_clear_console_logs` - Clear console logs\n\n**Utility:**\n- `set-slug` - Set conversation identifier\n- `about_sketch` - Get Sketch platform help\n- `multiplechoice` - Present multiple choice questions\n- `read_image` - Read and encode image files"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":4740,"cache_read_input_tokens":0,"output_tokens":342,"service_tier":"standard"}}
\ No newline at end of file