2026-04-17 05:44:31 +00:00
package toolshared
2026-02-12 11:28:56 +00:00
2026-03-22 11:05:28 +00:00
import (
"encoding/json"
"strings"
2026-03-19 02:15:00 +00:00
"github.com/sipeed/picoclaw/pkg/providers"
2026-03-22 11:05:28 +00:00
)
const (
2026-04-17 06:31:43 +00:00
HandledToolLLMNote = "The requested output has already been delivered to the user in the current chat. Do not call send_file or any other delivery tool again. If you reply, provide only a brief confirmation."
ArtifactPathsLLMNote = "Use `send_file` with one of these paths to send it to the user, or use file/exec tools to save it inside the workspace if requested."
2026-03-22 11:05:28 +00:00
)
2026-02-12 11:28:56 +00:00
// ToolResult represents the structured return value from tool execution.
// It provides clear semantics for different types of results and supports
// async operations, user-facing messages, and error handling.
type ToolResult struct {
// ForLLM is the content sent to the LLM for context.
// Required for all results.
ForLLM string ` json:"for_llm" `
// ForUser is the content sent directly to the user.
// If empty, no user message is sent.
// Silent=true overrides this field.
ForUser string ` json:"for_user,omitempty" `
// Silent suppresses sending any message to the user.
// When true, ForUser is ignored even if set.
Silent bool ` json:"silent" `
// IsError indicates whether the tool execution failed.
// When true, the result should be treated as an error.
IsError bool ` json:"is_error" `
// Async indicates whether the tool is running asynchronously.
// When true, the tool will complete later and notify via callback.
Async bool ` json:"async" `
// Err is the underlying error (not JSON serialized).
// Used for internal error handling and logging.
Err error ` json:"-" `
2026-02-22 19:10:57 +00:00
// Media contains media store refs produced by this tool.
// When non-empty, the agent will publish these as OutboundMediaMessage.
Media [ ] string ` json:"media,omitempty" `
2026-03-22 11:05:28 +00:00
2026-03-19 02:15:00 +00:00
// Messages holds the ephemeral session history after execution.
// Only populated by SubTurn executions; used by evaluator_optimizer
// to carry stateful worker context across evaluation iterations.
Messages [ ] providers . Message ` json:"-" `
2026-03-22 22:08:27 +00:00
2026-03-22 11:05:28 +00:00
// ArtifactTags exposes local artifact paths back to the LLM in a structured
// form, e.g. "[file:/tmp/example.png]". This is used when a tool produced a
// reusable local artifact but did not deliver it to the user yet.
ArtifactTags [ ] string ` json:"artifact_tags,omitempty" `
// ResponseHandled indicates that this tool execution already satisfied the
// user's request at the channel/output level, so the agent loop can stop
// without a follow-up assistant response.
ResponseHandled bool ` json:"response_handled,omitempty" `
}
// ContentForLLM returns the normalized textual content to append to the
// conversation after a tool call. Errors fall back to Err when ForLLM is empty.
func ( tr * ToolResult ) ContentForLLM ( ) string {
if tr == nil {
return ""
}
content := tr . ForLLM
if content == "" && tr . Err != nil {
content = tr . Err . Error ( )
}
if tr . ResponseHandled {
if content == "" {
2026-04-17 06:31:43 +00:00
return HandledToolLLMNote
2026-03-22 11:05:28 +00:00
}
2026-04-17 06:31:43 +00:00
if ! strings . Contains ( content , HandledToolLLMNote ) {
content += "\n" + HandledToolLLMNote
2026-03-22 11:05:28 +00:00
}
}
if len ( tr . ArtifactTags ) > 0 {
2026-04-17 06:31:43 +00:00
artifactNote := "Local artifact paths: " + strings . Join ( tr . ArtifactTags , " " ) + "\n" + ArtifactPathsLLMNote
2026-03-22 11:05:28 +00:00
if content == "" {
content = artifactNote
} else if ! strings . Contains ( content , artifactNote ) {
content += "\n" + artifactNote
}
}
if content != "" {
return content
}
return ""
2026-02-12 11:28:56 +00:00
}
// NewToolResult creates a basic ToolResult with content for the LLM.
// Use this when you need a simple result with default behavior.
//
// Example:
//
// result := NewToolResult("File updated successfully")
func NewToolResult ( forLLM string ) * ToolResult {
return & ToolResult {
ForLLM : forLLM ,
}
}
// SilentResult creates a ToolResult that is silent (no user message).
// The content is only sent to the LLM for context.
//
// Use this for operations that should not spam the user, such as:
// - File reads/writes
// - Status updates
// - Background operations
//
// Example:
//
// result := SilentResult("Config file saved")
func SilentResult ( forLLM string ) * ToolResult {
return & ToolResult {
ForLLM : forLLM ,
Silent : true ,
IsError : false ,
Async : false ,
}
}
// AsyncResult creates a ToolResult for async operations.
// The task will run in the background and complete later.
//
// Use this for long-running operations like:
// - Subagent spawns
// - Background processing
// - External API calls with callbacks
//
// Example:
//
// result := AsyncResult("Subagent spawned, will report back")
func AsyncResult ( forLLM string ) * ToolResult {
return & ToolResult {
ForLLM : forLLM ,
Silent : false ,
IsError : false ,
Async : true ,
}
}
// ErrorResult creates a ToolResult representing an error.
// Sets IsError=true and includes the error message.
//
// Example:
//
// result := ErrorResult("Failed to connect to database: connection refused")
func ErrorResult ( message string ) * ToolResult {
return & ToolResult {
ForLLM : message ,
Silent : false ,
IsError : true ,
Async : false ,
}
}
// UserResult creates a ToolResult with content for both LLM and user.
// Both ForLLM and ForUser are set to the same content.
//
// Use this when the user needs to see the result directly:
// - Command execution output
// - Fetched web content
// - Query results
//
// Example:
//
// result := UserResult("Total files found: 42")
func UserResult ( content string ) * ToolResult {
return & ToolResult {
ForLLM : content ,
ForUser : content ,
Silent : false ,
IsError : false ,
Async : false ,
}
}
2026-02-22 19:10:57 +00:00
// MediaResult creates a ToolResult with media refs for the user.
// The agent will publish these refs as OutboundMediaMessage.
//
// Example:
//
// result := MediaResult("Image generated successfully", []string{"media://abc123"})
func MediaResult ( forLLM string , mediaRefs [ ] string ) * ToolResult {
return & ToolResult {
ForLLM : forLLM ,
Media : mediaRefs ,
}
}
2026-02-12 11:28:56 +00:00
// MarshalJSON implements custom JSON serialization.
// The Err field is excluded from JSON output via the json:"-" tag.
func ( tr * ToolResult ) MarshalJSON ( ) ( [ ] byte , error ) {
type Alias ToolResult
return json . Marshal ( & struct {
* Alias
} {
Alias : ( * Alias ) ( tr ) ,
} )
}
// WithError sets the Err field and returns the result for chaining.
// This preserves the error for logging while keeping it out of JSON.
//
// Example:
//
// result := ErrorResult("Operation failed").WithError(err)
func ( tr * ToolResult ) WithError ( err error ) * ToolResult {
tr . Err = err
return tr
}
2026-03-22 11:05:28 +00:00
// WithResponseHandled marks the tool result as already delivered to the user.
func ( tr * ToolResult ) WithResponseHandled ( ) * ToolResult {
tr . ResponseHandled = true
return tr
}