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/about_sketch.go b/claudetool/about_sketch.go
index 15a4059..56b0b28 100644
--- a/claudetool/about_sketch.go
+++ b/claudetool/about_sketch.go
@@ -4,7 +4,6 @@
 	"context"
 	_ "embed"
 	"encoding/json"
-	"fmt"
 	"log/slog"
 	"strings"
 	"text/template"
@@ -43,7 +42,7 @@
 
 var aboutSketchTemplate = template.Must(template.New("sketch").Parse(aboutSketch))
 
-func aboutSketchRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func aboutSketchRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	slog.InfoContext(ctx, "about_sketch called")
 
 	info := conversation.ToolCallInfoFromContext(ctx)
@@ -58,7 +57,7 @@
 	}
 	buf := new(strings.Builder)
 	if err := aboutSketchTemplate.Execute(buf, dot); err != nil {
-		return nil, fmt.Errorf("template execution error: %w", err)
+		return llm.ErrorfToolOut("template execution error: %w", err)
 	}
-	return llm.TextContent(buf.String()), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(buf.String())}
 }
diff --git a/claudetool/bash.go b/claudetool/bash.go
index 59b8e3c..741f1e9 100644
--- a/claudetool/bash.go
+++ b/claudetool/bash.go
@@ -141,22 +141,22 @@
 	}
 }
 
-func (b *BashTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (b *BashTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	var req bashInput
 	if err := json.Unmarshal(m, &req); err != nil {
-		return nil, fmt.Errorf("failed to unmarshal bash command input: %w", err)
+		return llm.ErrorfToolOut("failed to unmarshal bash command input: %w", err)
 	}
 
 	// do a quick permissions check (NOT a security barrier)
 	err := bashkit.Check(req.Command)
 	if err != nil {
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 
 	// Custom permission callback if set
 	if b.CheckPermission != nil {
 		if err := b.CheckPermission(req.Command); err != nil {
-			return nil, err
+			return llm.ErrorToolOut(err)
 		}
 	}
 
@@ -174,23 +174,23 @@
 	if req.Background {
 		result, err := executeBackgroundBash(ctx, req, timeout)
 		if err != nil {
-			return nil, err
+			return llm.ErrorToolOut(err)
 		}
 		// Marshal the result to JSON
 		// TODO: emit XML(-ish) instead?
 		output, err := json.Marshal(result)
 		if err != nil {
-			return nil, fmt.Errorf("failed to marshal background result: %w", err)
+			return llm.ErrorfToolOut("failed to marshal background result: %w", err)
 		}
-		return llm.TextContent(string(output)), nil
+		return llm.ToolOut{LLMContent: llm.TextContent(string(output))}
 	}
 
 	// For foreground commands, use executeBash
 	out, execErr := executeBash(ctx, req, timeout)
 	if execErr != nil {
-		return nil, execErr
+		return llm.ErrorToolOut(execErr)
 	}
-	return llm.TextContent(out), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(out)}
 }
 
 const maxBashOutputLength = 131072
@@ -435,7 +435,7 @@
   },
   "required": ["results"]
 }`),
-		Run: func(ctx context.Context, input json.RawMessage) ([]llm.Content, error) {
+		Run: func(ctx context.Context, input json.RawMessage) llm.ToolOut {
 			type InstallResult struct {
 				CommandName string `json:"command_name"`
 				Installed   bool   `json:"installed"`
@@ -452,7 +452,7 @@
 				slog.InfoContext(ctx, "auto-tool installation complete", "results", results)
 			}
 			done = true
-			return llm.TextContent(""), nil
+			return llm.ToolOut{LLMContent: llm.TextContent("")}
 		},
 	}
 
diff --git a/claudetool/bash_test.go b/claudetool/bash_test.go
index 98410d5..8c57a4b 100644
--- a/claudetool/bash_test.go
+++ b/claudetool/bash_test.go
@@ -17,10 +17,11 @@
 		input := json.RawMessage(`{"command":"echo 'slow test'","slow_ok":true}`)
 
 		bashTool := (&BashTool{}).Tool()
-		result, err := bashTool.Run(context.Background(), input)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := bashTool.Run(context.Background(), input)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		expected := "slow test\n"
 		if len(result) == 0 || result[0].Text != expected {
@@ -33,10 +34,11 @@
 		input := json.RawMessage(`{"command":"echo 'slow background test'","slow_ok":true,"background":true}`)
 
 		bashTool := (&BashTool{}).Tool()
-		result, err := bashTool.Run(context.Background(), input)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := bashTool.Run(context.Background(), input)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		// Should return background result JSON
 		var bgResult BackgroundResult
@@ -64,10 +66,11 @@
 	t.Run("Basic Command", func(t *testing.T) {
 		input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
 
-		result, err := tool.Run(context.Background(), input)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := tool.Run(context.Background(), input)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		expected := "Hello, world!\n"
 		if len(result) == 0 || result[0].Text != expected {
@@ -79,10 +82,11 @@
 	t.Run("Command With Arguments", func(t *testing.T) {
 		input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
 
-		result, err := tool.Run(context.Background(), input)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := tool.Run(context.Background(), input)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		expected := "foobar"
 		if len(result) == 0 || result[0].Text != expected {
@@ -104,10 +108,11 @@
 			t.Fatalf("Failed to marshal input: %v", err)
 		}
 
-		result, err := tool.Run(context.Background(), inputJSON)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := tool.Run(context.Background(), inputJSON)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		expected := "Completed\n"
 		if len(result) == 0 || result[0].Text != expected {
@@ -130,11 +135,11 @@
 
 		input := json.RawMessage(`{"command":"sleep 0.5 && echo 'Should not see this'"}`)
 
-		_, err := tool.Run(context.Background(), input)
-		if err == nil {
+		toolOut := tool.Run(context.Background(), input)
+		if toolOut.Error == nil {
 			t.Errorf("Expected timeout error, got none")
-		} else if !strings.Contains(err.Error(), "timed out") {
-			t.Errorf("Expected timeout error, got: %v", err)
+		} else if !strings.Contains(toolOut.Error.Error(), "timed out") {
+			t.Errorf("Expected timeout error, got: %v", toolOut.Error)
 		}
 	})
 
@@ -142,8 +147,8 @@
 	t.Run("Failed Command", func(t *testing.T) {
 		input := json.RawMessage(`{"command":"exit 1"}`)
 
-		_, err := tool.Run(context.Background(), input)
-		if err == nil {
+		toolOut := tool.Run(context.Background(), input)
+		if toolOut.Error == nil {
 			t.Errorf("Expected error for failed command, got none")
 		}
 	})
@@ -152,8 +157,8 @@
 	t.Run("Invalid JSON Input", func(t *testing.T) {
 		input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
 
-		_, err := tool.Run(context.Background(), input)
-		if err == nil {
+		toolOut := tool.Run(context.Background(), input)
+		if toolOut.Error == nil {
 			t.Errorf("Expected error for invalid input, got none")
 		}
 	})
@@ -268,10 +273,11 @@
 			t.Fatalf("Failed to marshal input: %v", err)
 		}
 
-		result, err := tool.Run(context.Background(), inputJSON)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := tool.Run(context.Background(), inputJSON)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		// Parse the returned JSON
 		var bgResult BackgroundResult
@@ -326,10 +332,11 @@
 			t.Fatalf("Failed to marshal input: %v", err)
 		}
 
-		result, err := tool.Run(context.Background(), inputJSON)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := tool.Run(context.Background(), inputJSON)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		// Parse the returned JSON
 		var bgResult BackgroundResult
@@ -384,10 +391,11 @@
 		}
 
 		// Start the command in the background
-		result, err := tool.Run(context.Background(), inputJSON)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
+		toolOut := tool.Run(context.Background(), inputJSON)
+		if toolOut.Error != nil {
+			t.Fatalf("Unexpected error: %v", toolOut.Error)
 		}
+		result := toolOut.LLMContent
 
 		// Parse the returned JSON
 		var bgResult BackgroundResult
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
diff --git a/claudetool/codereview/codereview_test.go b/claudetool/codereview/codereview_test.go
index f9d240e..4bba851 100644
--- a/claudetool/codereview/codereview_test.go
+++ b/claudetool/codereview/codereview_test.go
@@ -234,10 +234,11 @@
 // runDifferentialTest runs the code review tool on the repository and returns the result.
 func runDifferentialTest(reviewer *CodeReviewer) (string, error) {
 	ctx := context.Background()
-	result, err := reviewer.Run(ctx, nil)
-	if err != nil {
-		return "", fmt.Errorf("error running code review: %w", err)
+	toolOut := reviewer.Run(ctx, nil)
+	if toolOut.Error != nil {
+		return "", fmt.Errorf("error running code review: %w", toolOut.Error)
 	}
+	result := toolOut.LLMContent
 
 	// Normalize paths in the result
 	resultStr := ""
diff --git a/claudetool/codereview/differential.go b/claudetool/codereview/differential.go
index 764f000..6c29d06 100644
--- a/claudetool/codereview/differential.go
+++ b/claudetool/codereview/differential.go
@@ -48,14 +48,14 @@
 	return spec
 }
 
-func (r *CodeReviewer) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func (r *CodeReviewer) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	// Parse input to get timeout
 	var input struct {
 		Timeout string `json:"timeout"`
 	}
 	if len(m) > 0 {
 		if err := json.Unmarshal(m, &input); err != nil {
-			return nil, fmt.Errorf("failed to parse input: %w", err)
+			return llm.ErrorfToolOut("failed to parse input: %w", err)
 		}
 	}
 	if input.Timeout == "" {
@@ -65,7 +65,7 @@
 	// Parse timeout duration
 	timeout, err := time.ParseDuration(input.Timeout)
 	if err != nil {
-		return nil, fmt.Errorf("invalid timeout duration %q: %w", input.Timeout, err)
+		return llm.ErrorfToolOut("invalid timeout duration %q: %w", input.Timeout, err)
 	}
 
 	// Create timeout context
@@ -76,22 +76,22 @@
 	// webui/src/web-components/sketch-tool-card.ts (SketchToolCardCodeReview.getStatusIcon)
 	if err := r.RequireNormalGitState(timeoutCtx); err != nil {
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to check for normal git state", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	if err := r.RequireNoUncommittedChanges(timeoutCtx); err != nil {
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to check for uncommitted changes", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 
 	// Check that the current commit is not the initial commit
 	currentCommit, err := r.CurrentCommit(timeoutCtx)
 	if err != nil {
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to get current commit", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	if r.IsInitialCommit(currentCommit) {
 		slog.DebugContext(ctx, "CodeReviewer.Run: current commit is initial commit, nothing to review")
-		return nil, fmt.Errorf("no new commits have been added, nothing to review")
+		return llm.ErrorToolOut(fmt.Errorf("no new commits have been added, nothing to review"))
 	}
 
 	// No matter what failures happen from here out, we will declare this to have been reviewed.
@@ -101,7 +101,7 @@
 	changedFiles, err := r.changedFiles(timeoutCtx, r.sketchBaseRef, currentCommit)
 	if err != nil {
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to get changed files", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 
 	// Prepare to analyze before/after for the impacted files.
@@ -113,7 +113,7 @@
 	if err != nil {
 		// TODO: log and skip to stuff that doesn't require packages
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to get packages for files", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	allPkgList := slices.Collect(maps.Keys(allPkgs))
 
@@ -152,7 +152,7 @@
 	testMsg, err := r.checkTests(timeoutCtx, allPkgList)
 	if err != nil {
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to check tests", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	if testMsg != "" {
 		errorMessages = append(errorMessages, testMsg)
@@ -161,7 +161,7 @@
 	goplsMsg, err := r.checkGopls(timeoutCtx, changedFiles) // includes vet checks
 	if err != nil {
 		slog.DebugContext(ctx, "CodeReviewer.Run: failed to check gopls", "err", err)
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	if goplsMsg != "" {
 		errorMessages = append(errorMessages, goplsMsg)
@@ -183,7 +183,7 @@
 	if buf.Len() == 0 {
 		buf.WriteString("OK")
 	}
-	return llm.TextContent(buf.String()), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(buf.String())}
 }
 
 func (r *CodeReviewer) initializeInitialCommitWorktree(ctx context.Context) error {
diff --git a/claudetool/keyword.go b/claudetool/keyword.go
index 7f2c492..69b789e 100644
--- a/claudetool/keyword.go
+++ b/claudetool/keyword.go
@@ -83,10 +83,10 @@
 	return strings.TrimSpace(string(out)), nil
 }
 
-func keywordRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func keywordRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	var input keywordInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	wd := WorkingDir(ctx)
 	root, err := FindRepoRoot(wd)
@@ -100,7 +100,7 @@
 	for _, term := range input.SearchTerms {
 		out, err := ripgrep(ctx, wd, []string{term})
 		if err != nil {
-			return nil, err
+			return llm.ErrorToolOut(err)
 		}
 		if len(out) > 64*1024 {
 			slog.InfoContext(ctx, "keyword search result too large", "term", term, "bytes", len(out))
@@ -110,7 +110,7 @@
 	}
 
 	if len(keep) == 0 {
-		return llm.TextContent("each of those search terms yielded too many results"), nil
+		return llm.ToolOut{LLMContent: llm.TextContent("each of those search terms yielded too many results")}
 	}
 
 	// peel off keywords until we get a result that fits in the query window
@@ -119,7 +119,7 @@
 		var err error
 		out, err = ripgrep(ctx, wd, keep)
 		if err != nil {
-			return nil, err
+			return llm.ErrorToolOut(err)
 		}
 		if len(out) < 128*1024 {
 			break
@@ -143,10 +143,10 @@
 
 	resp, err := convo.SendMessage(initialMessage)
 	if err != nil {
-		return nil, fmt.Errorf("failed to send relevance filtering message: %w", err)
+		return llm.ErrorfToolOut("failed to send relevance filtering message: %w", err)
 	}
 	if len(resp.Content) != 1 {
-		return nil, fmt.Errorf("unexpected number of messages in relevance filtering response: %d", len(resp.Content))
+		return llm.ErrorfToolOut("unexpected number of messages in relevance filtering response: %d", len(resp.Content))
 	}
 
 	filtered := resp.Content[0].Text
@@ -159,7 +159,7 @@
 		"filtered", filtered,
 	)
 
-	return llm.TextContent(resp.Content[0].Text), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(resp.Content[0].Text)}
 }
 
 func ripgrep(ctx context.Context, wd string, terms []string) (string, error) {
diff --git a/claudetool/patch.go b/claudetool/patch.go
index 252455a..815cf88 100644
--- a/claudetool/patch.go
+++ b/claudetool/patch.go
@@ -20,9 +20,9 @@
 
 // PatchCallback defines the signature for patch tool callbacks.
 // It runs after the patch tool has executed.
-// It receives the patch input, the tool call results (both content and error),
-// and returns a new, possibly altered tool call result.
-type PatchCallback func(input PatchInput, result []llm.Content, err error) ([]llm.Content, error)
+// It receives the patch input and the tool output,
+// and returns a new, possibly altered tool output.
+type PatchCallback func(input PatchInput, output llm.ToolOut) llm.ToolOut
 
 // Patch creates a patch tool. The callback may be nil.
 func Patch(callback PatchCallback) *llm.Tool {
@@ -30,13 +30,13 @@
 		Name:        PatchName,
 		Description: strings.TrimSpace(PatchDescription),
 		InputSchema: llm.MustSchema(PatchInputSchema),
-		Run: func(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+		Run: func(ctx context.Context, m json.RawMessage) llm.ToolOut {
 			var input PatchInput
-			result, err := patchRun(ctx, m, &input)
+			output := patchRun(ctx, m, &input)
 			if callback != nil {
-				return callback(input, result, err)
+				return callback(input, output)
 			}
-			return result, err
+			return output
 		},
 	}
 }
@@ -112,17 +112,17 @@
 
 // patchRun implements the guts of the patch tool.
 // It populates input from m.
-func patchRun(ctx context.Context, m json.RawMessage, input *PatchInput) ([]llm.Content, error) {
+func patchRun(ctx context.Context, m json.RawMessage, input *PatchInput) llm.ToolOut {
 	if err := json.Unmarshal(m, &input); err != nil {
-		return nil, fmt.Errorf("failed to unmarshal user_patch input: %w", err)
+		return llm.ErrorfToolOut("failed to unmarshal user_patch input: %w", err)
 	}
 
 	// Validate the input
 	if !filepath.IsAbs(input.Path) {
-		return nil, fmt.Errorf("path %q is not absolute", input.Path)
+		return llm.ErrorfToolOut("path %q is not absolute", input.Path)
 	}
 	if len(input.Patches) == 0 {
-		return nil, fmt.Errorf("no patches provided")
+		return llm.ErrorToolOut(fmt.Errorf("no patches provided"))
 	}
 	// TODO: check whether the file is autogenerated, and if so, require a "force" flag to modify it.
 
@@ -135,11 +135,11 @@
 			switch patch.Operation {
 			case "prepend_bof", "append_eof", "overwrite":
 			default:
-				return nil, fmt.Errorf("file %q does not exist", input.Path)
+				return llm.ErrorfToolOut("file %q does not exist", input.Path)
 			}
 		}
 	case err != nil:
-		return nil, fmt.Errorf("failed to read file %q: %w", input.Path, err)
+		return llm.ErrorfToolOut("failed to read file %q: %w", input.Path, err)
 	}
 
 	likelyGoFile := strings.HasSuffix(input.Path, ".go")
@@ -168,7 +168,7 @@
 			buf.Replace(0, len(orig), patch.NewText)
 		case "replace":
 			if patch.OldText == "" {
-				return nil, fmt.Errorf("patch %d: oldText cannot be empty for %s operation", i, patch.Operation)
+				return llm.ErrorfToolOut("patch %d: oldText cannot be empty for %s operation", i, patch.Operation)
 			}
 
 			// Attempt to apply the patch.
@@ -231,23 +231,23 @@
 			patchErr = errors.Join(patchErr, fmt.Errorf("old text not found:\n%s", patch.OldText))
 			continue
 		default:
-			return nil, fmt.Errorf("unrecognized operation %q", patch.Operation)
+			return llm.ErrorfToolOut("unrecognized operation %q", patch.Operation)
 		}
 	}
 
 	if patchErr != nil {
-		return nil, patchErr
+		return llm.ErrorToolOut(patchErr)
 	}
 
 	patched, err := buf.Bytes()
 	if err != nil {
-		return nil, err
+		return llm.ErrorToolOut(err)
 	}
 	if err := os.MkdirAll(filepath.Dir(input.Path), 0o700); err != nil {
-		return nil, fmt.Errorf("failed to create directory %q: %w", filepath.Dir(input.Path), err)
+		return llm.ErrorfToolOut("failed to create directory %q: %w", filepath.Dir(input.Path), err)
 	}
 	if err := os.WriteFile(input.Path, patched, 0o600); err != nil {
-		return nil, fmt.Errorf("failed to write patched contents to file %q: %w", input.Path, err)
+		return llm.ErrorfToolOut("failed to write patched contents to file %q: %w", input.Path, err)
 	}
 
 	response := new(strings.Builder)
@@ -256,7 +256,7 @@
 	if parsed {
 		parseErr := parseGo(patched)
 		if parseErr != nil {
-			return nil, fmt.Errorf("after applying all patches, the file no longer parses:\n%w", parseErr)
+			return llm.ErrorfToolOut("after applying all patches, the file no longer parses:\n%w", parseErr)
 		}
 	}
 
@@ -265,7 +265,7 @@
 	}
 
 	// TODO: maybe report the patch result to the model, i.e. some/all of the new code after the patches and formatting.
-	return llm.TextContent(response.String()), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(response.String())}
 }
 
 func parseGo(buf []byte) error {
diff --git a/claudetool/think.go b/claudetool/think.go
index 9611150..4fe3513 100644
--- a/claudetool/think.go
+++ b/claudetool/think.go
@@ -34,6 +34,6 @@
 `
 )
 
-func thinkRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
-	return llm.TextContent("recorded"), nil
+func thinkRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
+	return llm.ToolOut{LLMContent: llm.TextContent("recorded")}
 }
diff --git a/claudetool/todo.go b/claudetool/todo.go
index 64c7550..5c23457 100644
--- a/claudetool/todo.go
+++ b/claudetool/todo.go
@@ -109,19 +109,19 @@
 	return TodoFilePath(SessionID(ctx))
 }
 
-func todoReadRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func todoReadRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	todoPath := todoFilePathForContext(ctx)
 	content, err := os.ReadFile(todoPath)
 	if os.IsNotExist(err) {
-		return llm.TextContent("No todo list found. Use todo_write to create one."), nil
+		return llm.ToolOut{LLMContent: llm.TextContent("No todo list found. Use todo_write to create one.")}
 	}
 	if err != nil {
-		return nil, fmt.Errorf("failed to read todo file: %w", err)
+		return llm.ErrorfToolOut("failed to read todo file: %w", err)
 	}
 
 	var todoList TodoList
 	if err := json.Unmarshal(content, &todoList); err != nil {
-		return nil, fmt.Errorf("failed to parse todo file: %w", err)
+		return llm.ErrorfToolOut("failed to parse todo file: %w", err)
 	}
 
 	result := fmt.Sprintf(`<todo_list count="%d">%s`, len(todoList.Items), "\n")
@@ -130,13 +130,13 @@
 	}
 	result += "</todo_list>"
 
-	return llm.TextContent(result), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(result)}
 }
 
-func todoWriteRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+func todoWriteRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	var input TodoWriteInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return nil, fmt.Errorf("invalid input: %w", err)
+		return llm.ErrorfToolOut("invalid input: %w", err)
 	}
 
 	// Validate that only one task is in-progress
@@ -148,7 +148,7 @@
 	}
 	switch {
 	case inProgressCount > 1:
-		return nil, fmt.Errorf("only one task can be 'in-progress' at a time, found %d", inProgressCount)
+		return llm.ErrorfToolOut("only one task can be 'in-progress' at a time, found %d", inProgressCount)
 	}
 
 	todoList := TodoList{
@@ -158,19 +158,19 @@
 	todoPath := todoFilePathForContext(ctx)
 	// Ensure directory exists
 	if err := os.MkdirAll(filepath.Dir(todoPath), 0o700); err != nil {
-		return nil, fmt.Errorf("failed to create todo directory: %w", err)
+		return llm.ErrorfToolOut("failed to create todo directory: %w", err)
 	}
 
 	content, err := json.Marshal(todoList)
 	if err != nil {
-		return nil, fmt.Errorf("failed to marshal todo list: %w", err)
+		return llm.ErrorfToolOut("failed to marshal todo list: %w", err)
 	}
 
 	if err := os.WriteFile(todoPath, content, 0o600); err != nil {
-		return nil, fmt.Errorf("failed to write todo file: %w", err)
+		return llm.ErrorfToolOut("failed to write todo file: %w", err)
 	}
 
 	result := fmt.Sprintf("Updated todo list with %d items.", len(input.Tasks))
 
-	return llm.TextContent(result), nil
+	return llm.ToolOut{LLMContent: llm.TextContent(result)}
 }
diff --git a/claudetool/todo_test.go b/claudetool/todo_test.go
index ac36cc2..6f511e8 100644
--- a/claudetool/todo_test.go
+++ b/claudetool/todo_test.go
@@ -16,10 +16,11 @@
 	todoPath := todoFilePathForContext(ctx)
 	os.Remove(todoPath)
 
-	result, err := todoReadRun(ctx, []byte("{}"))
-	if err != nil {
-		t.Fatalf("expected no error, got %v", err)
+	toolOut := todoReadRun(ctx, []byte("{}"))
+	if toolOut.Error != nil {
+		t.Fatalf("expected no error, got %v", toolOut.Error)
 	}
+	result := toolOut.LLMContent
 
 	if len(result) != 1 {
 		t.Fatalf("expected 1 content item, got %d", len(result))
@@ -49,10 +50,11 @@
 	writeInput := TodoWriteInput{Tasks: todos}
 	writeInputJSON, _ := json.Marshal(writeInput)
 
-	result, err := todoWriteRun(ctx, writeInputJSON)
-	if err != nil {
-		t.Fatalf("expected no error, got %v", err)
+	toolOut := todoWriteRun(ctx, writeInputJSON)
+	if toolOut.Error != nil {
+		t.Fatalf("expected no error, got %v", toolOut.Error)
 	}
+	result := toolOut.LLMContent
 
 	if len(result) != 1 {
 		t.Fatalf("expected 1 content item, got %d", len(result))
@@ -64,10 +66,11 @@
 	}
 
 	// Read the todos back
-	result, err = todoReadRun(ctx, []byte("{}"))
-	if err != nil {
-		t.Fatalf("expected no error, got %v", err)
+	toolOut = todoReadRun(ctx, []byte("{}"))
+	if toolOut.Error != nil {
+		t.Fatalf("expected no error, got %v", toolOut.Error)
 	}
+	result = toolOut.LLMContent
 
 	if len(result) != 1 {
 		t.Fatalf("expected 1 content item, got %d", len(result))
@@ -107,14 +110,14 @@
 	writeInput := TodoWriteInput{Tasks: todos}
 	writeInputJSON, _ := json.Marshal(writeInput)
 
-	_, err := todoWriteRun(ctx, writeInputJSON)
-	if err == nil {
+	toolOut := todoWriteRun(ctx, writeInputJSON)
+	if toolOut.Error == nil {
 		t.Fatal("expected error for multiple in_progress tasks, got none")
 	}
 
 	expected := "only one task can be 'in-progress' at a time, found 2"
-	if err.Error() != expected {
-		t.Errorf("expected error %q, got %q", expected, err.Error())
+	if toolOut.Error.Error() != expected {
+		t.Errorf("expected error %q, got %q", expected, toolOut.Error.Error())
 	}
 }