diff --git a/proto/tim-api/tim/api/thread/v1alpha1/thread_service.proto b/proto/tim-api/tim/api/thread/v1alpha1/thread_service.proto index 5080f201f..589595a9c 100644 --- a/proto/tim-api/tim/api/thread/v1alpha1/thread_service.proto +++ b/proto/tim-api/tim/api/thread/v1alpha1/thread_service.proto @@ -255,12 +255,16 @@ message StreamThreadEventsResponse { ContentDeltaEvent content_delta = 2; // A content block has completed ContentStopEvent content_stop = 3; - // A tool call made by the LLM (input may be null if >512KB) - tim.api.tool.v1alpha1.ToolCall tool_call = 4; + // A tool call is about to be executed (tool name known, input may be incomplete) + ToolCallStartEvent tool_call_start = 4; + // A tool call made by the LLM with full input (input may be null if >512KB) + tim.api.tool.v1alpha1.ToolCall tool_call = 5; + // A tool call has completed execution with results + ToolCallCompleteEvent tool_call_complete = 6; // The thread's LLM processing state has changed (IDLE or PROCESSING) - ThreadStateChangeEvent thread_state_change = 5; + ThreadStateChangeEvent thread_state_change = 7; // The LLM stream encountered an error - StreamErrorEvent stream_error = 6; + StreamErrorEvent stream_error = 8; } } diff --git a/proto/tim-api/tim/api/thread/v1alpha1/thread_types.proto b/proto/tim-api/tim/api/thread/v1alpha1/thread_types.proto index 20dfd0255..e44a264cc 100644 --- a/proto/tim-api/tim/api/thread/v1alpha1/thread_types.proto +++ b/proto/tim-api/tim/api/thread/v1alpha1/thread_types.proto @@ -223,6 +223,24 @@ message ContentStopEvent { string id = 1 [(google.api.field_info).format = UUID4]; } +// ToolCallStartEvent signals that a tool call is about to be executed +message ToolCallStartEvent { + // Tool call ID (UUID) + string id = 1 [(google.api.field_info).format = UUID4]; + // Name of the tool being called + string tool_name = 2; +} + +// ToolCallCompleteEvent signals that a tool call has completed execution +message ToolCallCompleteEvent { + // Tool call ID (UUID) + string id = 1 [(google.api.field_info).format = UUID4]; + // Whether the tool call was successful + bool success = 2; + // Brief summary of what the tool did + string summary = 3; +} + // ThreadStateChangeEvent notifies when thread LLM state changes message ThreadStateChangeEvent { // IDLE or PROCESSING diff --git a/proto/tim-api/tim/api/tool/v1alpha1/tool_types.proto b/proto/tim-api/tim/api/tool/v1alpha1/tool_types.proto index 8c54741b0..61c151f82 100644 --- a/proto/tim-api/tim/api/tool/v1alpha1/tool_types.proto +++ b/proto/tim-api/tim/api/tool/v1alpha1/tool_types.proto @@ -35,4 +35,8 @@ message ToolResult { // Whether this tool result should stop the LLM iteration loop // Set to true by tools like query_complete, code_complete to signal conversation completion bool stop_iteration = 4; + + // A brief summary of what the tool did (e.g., "Updated todo to in_progress", "Read 234 lines") + // Used for client notifications without exposing full result + string summary = 5; } diff --git a/shared/llm/types.go b/shared/llm/types.go index bf1eed60f..a34cbb9cb 100644 --- a/shared/llm/types.go +++ b/shared/llm/types.go @@ -82,6 +82,7 @@ func GenerateToolUseID() string { type ToolResultContent struct { ToolUseID string `json:"tool_use_id"` Result string `json:"result"` + Summary string `json:"summary,omitempty"` IsError bool `json:"is_error,omitempty"` StopIteration bool `json:"stop_iteration,omitempty"` // Signal to stop LLM iteration loop (e.g., query_complete, code_complete) } diff --git a/shared/tools/add_todo.go b/shared/tools/add_todo.go index a6034cba6..432f8e5c1 100644 --- a/shared/tools/add_todo.go +++ b/shared/tools/add_todo.go @@ -60,18 +60,18 @@ func (t *AddTodoTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *AddTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *AddTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Backend == nil { - return false, "", fmt.Errorf("add_todo requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("add_todo requires backend (tool cannot run in this context)") } if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for add_todo (not available in current context)") + return nil, fmt.Errorf("thread path required for add_todo (not available in current context)") } // Default priority to medium @@ -86,10 +86,14 @@ func (t *AddTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bo todoPath, err := t.deps.Backend.CreateTodo(ctx, t.deps.ThreadPath, parsed.Title, parsed.Description, priority) if err != nil { - return false, "", fmt.Errorf("failed to create todo: %w", err) + return nil, fmt.Errorf("failed to create todo: %w", err) } t.deps.Logger.Infow("todo created", "todo_path", todoPath) - return false, fmt.Sprintf("Todo added: %s", parsed.Title), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: fmt.Sprintf("Todo added: %s", parsed.Title), + Summary: fmt.Sprintf("Added todo: %s", parsed.Title), + }, nil } diff --git a/shared/tools/clarify.go b/shared/tools/clarify.go index 79f0f4a22..c7b7ebb77 100644 --- a/shared/tools/clarify.go +++ b/shared/tools/clarify.go @@ -53,6 +53,6 @@ func (t *ClarifyTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *ClarifyTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ClarifyTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { panic("TODO: clarify tool requires client-side execution - implement in CLI") } diff --git a/shared/tools/complete_todo.go b/shared/tools/complete_todo.go index 82ae2fd67..637678894 100644 --- a/shared/tools/complete_todo.go +++ b/shared/tools/complete_todo.go @@ -56,18 +56,18 @@ func (t *CompleteTodoTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *CompleteTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *CompleteTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Backend == nil { - return false, "", fmt.Errorf("complete_todo requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("complete_todo requires backend (tool cannot run in this context)") } if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for complete_todo (not available in current context)") + return nil, fmt.Errorf("thread path required for complete_todo (not available in current context)") } // Build todo path from thread path + todo UID @@ -78,10 +78,14 @@ func (t *CompleteTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage // Update progress to completed err = t.deps.Backend.UpdateTodo(ctx, todoPath, "completed", "") if err != nil { - return false, "", fmt.Errorf("failed to complete todo: %w", err) + return nil, fmt.Errorf("failed to complete todo: %w", err) } t.deps.Logger.Infow("todo completed", "todo_uid", parsed.TodoUID, "thread_path", t.deps.ThreadPath) - return false, "Todo marked as completed", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "Todo marked as completed", + Summary: "Completed todo", + }, nil } diff --git a/shared/tools/delegate_work.go b/shared/tools/delegate_work.go index 2a9982852..9c8edd5d2 100644 --- a/shared/tools/delegate_work.go +++ b/shared/tools/delegate_work.go @@ -96,15 +96,15 @@ func (t *DelegateWorkTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *DelegateWorkTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *DelegateWorkTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } // Check if backend is available if t.deps.Backend == nil { - return false, "", fmt.Errorf("delegate_work requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("delegate_work requires backend (tool cannot run in this context)") } t.deps.Logger.Infow("delegating work to sub-agent", @@ -113,19 +113,19 @@ func (t *DelegateWorkTool) ExecuteRaw(ctx context.Context, input json.RawMessage // Check if user path is available if t.deps.UserPath == "" { - return false, "", fmt.Errorf("user path required for delegate_work (not available in current context)") + return nil, fmt.Errorf("user path required for delegate_work (not available in current context)") } // Check if current thread path is available (to set as parent for sub-agent) if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for delegate_work (not available in current context)") + return nil, fmt.Errorf("thread path required for delegate_work (not available in current context)") } // Create a new thread for the sub-agent with current thread as parent threadPath, err := t.deps.Backend.CreateThread(ctx, t.deps.UserPath, t.deps.ThreadPath, parsed.Persona, parsed.Description) if err != nil { t.deps.Logger.Errorw("failed to create sub-agent thread", "error", err) - return false, "", fmt.Errorf("failed to create sub-agent thread: %w", err) + return nil, fmt.Errorf("failed to create sub-agent thread: %w", err) } t.deps.Logger.Infow("created sub-agent thread", "thread_path", threadPath) @@ -134,7 +134,7 @@ func (t *DelegateWorkTool) ExecuteRaw(ctx context.Context, input json.RawMessage err = t.deps.Backend.SubmitUserMessage(ctx, threadPath, parsed.Prompt) if err != nil { t.deps.Logger.Errorw("failed to send prompt to sub-agent", "error", err) - return false, "", fmt.Errorf("failed to send prompt to sub-agent: %w", err) + return nil, fmt.Errorf("failed to send prompt to sub-agent: %w", err) } t.deps.Logger.Infow("sent prompt to sub-agent", "thread_path", threadPath) @@ -142,14 +142,18 @@ func (t *DelegateWorkTool) ExecuteRaw(ctx context.Context, input json.RawMessage // Poll until the thread is idle (sub-agent finished) result, err := t.waitForCompletion(ctx, threadPath) if err != nil { - return false, "", err + return nil, err } t.deps.Logger.Infow("sub-agent completed", "thread_path", threadPath, "result_length", len(result)) - return false, result, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: result, + Summary: "Delegated work to sub-agent", + }, nil } // waitForCompletion polls the thread until it's idle and returns the final result diff --git a/shared/tools/exec_command.go b/shared/tools/exec_command.go index 13f05217b..5eeff8f1f 100644 --- a/shared/tools/exec_command.go +++ b/shared/tools/exec_command.go @@ -65,14 +65,14 @@ func (t *ExecCommandTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *ExecCommandTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ExecCommandTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("exec_command requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("exec_command requires ops provider (tool cannot run in this context)") } processID := uuid.New().String() @@ -93,7 +93,7 @@ func (t *ExecCommandTool) ExecuteRaw(ctx context.Context, input json.RawMessage) // Run command runResult, err := t.deps.Ops.ExecRun(ctx, "", processID, rootDir, parsed.Command, waitForCompletion, timeout, nil) if err != nil { - return false, "", err + return nil, err } // Determine result based on actual process state @@ -109,10 +109,14 @@ func (t *ExecCommandTool) ExecuteRaw(ctx context.Context, input json.RawMessage) } if err != nil { - return false, "", err + return nil, err } - return false, result, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: result, + Summary: fmt.Sprintf("Executed command: %s", parsed.Command), + }, nil } func (t *ExecCommandTool) processStarted(runResult *ops.OpsExecRunResult) (string, error) { diff --git a/shared/tools/execute_connected_app_action.go b/shared/tools/execute_connected_app_action.go index 385fb795d..4ef019a89 100644 --- a/shared/tools/execute_connected_app_action.go +++ b/shared/tools/execute_connected_app_action.go @@ -72,18 +72,18 @@ func (t *ExecuteConnectedAppActionTool) ValidateRawInput(input json.RawMessage) return err } -func (t *ExecuteConnectedAppActionTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ExecuteConnectedAppActionTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.ConnectedAppClient == nil { - return false, "", fmt.Errorf("execute_connected_app_action requires a connected app client (tool cannot run in this context)") + return nil, fmt.Errorf("execute_connected_app_action requires a connected app client (tool cannot run in this context)") } if t.deps.UserPath == "" { - return false, "", fmt.Errorf("execute_connected_app_action requires a user path (not available in current context)") + return nil, fmt.Errorf("execute_connected_app_action requires a user path (not available in current context)") } // Initialize configured_props if nil @@ -93,7 +93,7 @@ func (t *ExecuteConnectedAppActionTool) ExecuteRaw(ctx context.Context, input js userID := extractUserIDFromPath(t.deps.UserPath) if userID == "" { - return false, "", fmt.Errorf("failed to extract user ID from user path") + return nil, fmt.Errorf("failed to extract user ID from user path") } t.deps.Logger.Infow("executing connected app action", @@ -112,7 +112,7 @@ func (t *ExecuteConnectedAppActionTool) ExecuteRaw(ctx context.Context, input js parsed.ConfiguredProps, ) if err != nil { - return false, "", fmt.Errorf("failed to execute action %s: %w", parsed.ActionKey, err) + return nil, fmt.Errorf("failed to execute action %s: %w", parsed.ActionKey, err) } t.deps.Logger.Infow("successfully executed connected app action", @@ -120,7 +120,11 @@ func (t *ExecuteConnectedAppActionTool) ExecuteRaw(ctx context.Context, input js "action_key", parsed.ActionKey) // Format the response - return false, formatActionResult(parsed.ActionKey, result), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: formatActionResult(parsed.ActionKey, result), + Summary: fmt.Sprintf("Executed action: %s", parsed.ActionKey), + }, nil } func formatActionResult(actionKey string, result *ConnectedAppActionResponse) string { diff --git a/shared/tools/executor.go b/shared/tools/executor.go index 9371a38d7..c9839d06e 100644 --- a/shared/tools/executor.go +++ b/shared/tools/executor.go @@ -73,13 +73,14 @@ func (e *Executor) Execute(ctx context.Context, toolUse *llm.ToolUseContent) (*l return &llm.ToolResultContent{ ToolUseID: toolUse.ID, Result: fmt.Sprintf("Failed to load tool: %v", err), + Summary: fmt.Sprintf("Failed to load tool: %v", err), IsError: true, StopIteration: false, }, nil } // Execute the tool - stopIteration, result, err := tool.ExecuteRaw(ctx, toolUse.Input) + execResult, err := tool.ExecuteRaw(ctx, toolUse.Input) if err != nil { e.logger.Errorw("tool execution failed", "tool_name", toolUse.Name, @@ -88,6 +89,7 @@ func (e *Executor) Execute(ctx context.Context, toolUse *llm.ToolUseContent) (*l return &llm.ToolResultContent{ ToolUseID: toolUse.ID, Result: fmt.Sprintf("Tool execution error: %v", err), + Summary: fmt.Sprintf("Error: %v", err), IsError: true, StopIteration: false, // Don't stop on errors }, nil @@ -96,14 +98,16 @@ func (e *Executor) Execute(ctx context.Context, toolUse *llm.ToolUseContent) (*l e.logger.Infow("tool executed successfully", "tool_name", toolUse.Name, "tool_use_id", toolUse.ID, - "stop_iteration", stopIteration, - "result_length", len(result)) + "stop_iteration", execResult.StopIteration, + "result_length", len(execResult.Result), + "summary", execResult.Summary) return &llm.ToolResultContent{ ToolUseID: toolUse.ID, - Result: result, + Result: execResult.Result, + Summary: execResult.Summary, IsError: false, - StopIteration: stopIteration, // Preserve the stop signal + StopIteration: execResult.StopIteration, }, nil } diff --git a/shared/tools/fetch_webpage.go b/shared/tools/fetch_webpage.go index 0e6e24e20..10717373e 100644 --- a/shared/tools/fetch_webpage.go +++ b/shared/tools/fetch_webpage.go @@ -101,10 +101,10 @@ func (t *FetchWebpageTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *FetchWebpageTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *FetchWebpageTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } // Log the fetch webpage (no emitter in shared context) @@ -113,7 +113,7 @@ func (t *FetchWebpageTool) ExecuteRaw(ctx context.Context, input json.RawMessage // Create request with context for cancellation support req, err := http.NewRequestWithContext(ctx, "GET", parsed.URL, nil) if err != nil { - return false, "", fmt.Errorf("failed to create request: %w", err) + return nil, fmt.Errorf("failed to create request: %w", err) } // Set a reasonable User-Agent @@ -123,20 +123,20 @@ func (t *FetchWebpageTool) ExecuteRaw(ctx context.Context, input json.RawMessage // Make the request resp, err := t.httpClient.Do(req) if err != nil { - return false, "", fmt.Errorf("failed to fetch URL: %w", err) + return nil, fmt.Errorf("failed to fetch URL: %w", err) } defer resp.Body.Close() // Check response status if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return false, "", fmt.Errorf("HTTP error: %d %s", resp.StatusCode, resp.Status) + return nil, fmt.Errorf("HTTP error: %d %s", resp.StatusCode, resp.Status) } // Read response body with size limit limitedReader := io.LimitReader(resp.Body, fetchWebpageMaxContentSize) content, err := io.ReadAll(limitedReader) if err != nil { - return false, "", fmt.Errorf("failed to read response: %w", err) + return nil, fmt.Errorf("failed to read response: %w", err) } // Check if content was truncated, assume truncation if content is max size @@ -168,7 +168,11 @@ func (t *FetchWebpageTool) ExecuteRaw(ctx context.Context, input json.RawMessage result += "**Content:**\n```\n" + cleanedContent + "\n```" } - return false, result, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: result, + Summary: fmt.Sprintf("Fetched webpage from %s", parsed.URL), + }, nil } func (t *FetchWebpageTool) cleanContent(content, contentType string) (string, bool) { diff --git a/shared/tools/file_edit.go b/shared/tools/file_edit.go index 56b5cf665..41bbc0a50 100644 --- a/shared/tools/file_edit.go +++ b/shared/tools/file_edit.go @@ -80,29 +80,29 @@ func (t *FileEditTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *FileEditTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *FileEditTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("file_edit requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("file_edit requires ops provider (tool cannot run in this context)") } // Check if file exists exists, err := t.deps.Ops.PathExists(ctx, parsed.Path) if err != nil { - return false, "", err + return nil, err } if !exists { - return false, "", fmt.Errorf("file does not exist: %s", parsed.Path) + return nil, fmt.Errorf("file does not exist: %s", parsed.Path) } // Read the current file content fileContent, err := t.deps.Ops.ReadFile(ctx, parsed.Path) if err != nil { - return false, "", err + return nil, err } // Perform the replacement @@ -113,18 +113,22 @@ func (t *FileEditTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (b // Check if old_string appears exactly once count := strings.Count(fileContent, parsed.OldString) if count == 0 { - return false, "", fmt.Errorf("old_string not found in file") + return nil, fmt.Errorf("old_string not found in file") } if count > 1 { - return false, "", fmt.Errorf("old_string appears %d times in file, use replace_all=true or provide more context to make it unique", count) + return nil, fmt.Errorf("old_string appears %d times in file, use replace_all=true or provide more context to make it unique", count) } newContent = strings.Replace(fileContent, parsed.OldString, parsed.NewString, 1) } err = t.deps.Ops.WriteFile(ctx, parsed.Path, newContent, 0644) if err != nil { - return false, "", err + return nil, err } - return false, "File edited successfully", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "File edited successfully", + Summary: fmt.Sprintf("Edited %s", parsed.Path), + }, nil } diff --git a/shared/tools/file_list.go b/shared/tools/file_list.go index dd649b275..6ba9690a5 100644 --- a/shared/tools/file_list.go +++ b/shared/tools/file_list.go @@ -71,28 +71,28 @@ func (t *FileListTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *FileListTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *FileListTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("file_list requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("file_list requires ops provider (tool cannot run in this context)") } // Check if directory exists exists, err := t.deps.Ops.PathExists(ctx, parsed.Path) if err != nil { - return false, "", err + return nil, err } if !exists { - return false, "", fmt.Errorf("directory does not exist: %s", parsed.Path) + return nil, fmt.Errorf("directory does not exist: %s", parsed.Path) } entries, err := t.deps.Ops.ListDir(ctx, parsed.Path, parsed.Recursive, parsed.ShowHidden) if err != nil { - return false, "", err + return nil, err } var files []string @@ -115,12 +115,20 @@ func (t *FileListTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (b } if len(files) == 0 { - return false, "Directory is empty or contains only hidden files.", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "Directory is empty or contains only hidden files.", + Summary: "Empty directory", + }, nil } // Format output with headers output := "Type\tName\tSize\tModified\n" output += strings.Join(files, "\n") - return false, output + notice, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: output + notice, + Summary: fmt.Sprintf("Listed %d items in %s", len(files), parsed.Path), + }, nil } diff --git a/shared/tools/file_read.go b/shared/tools/file_read.go index 4be5ee39d..306a88cd6 100644 --- a/shared/tools/file_read.go +++ b/shared/tools/file_read.go @@ -69,32 +69,36 @@ func (t *FileReadTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *FileReadTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *FileReadTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("file_read requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("file_read requires ops provider (tool cannot run in this context)") } // Check if file exists exists, err := t.deps.Ops.PathExists(ctx, parsed.Path) if err != nil { - return false, "", err + return nil, err } if !exists { - return false, "", fmt.Errorf("file does not exist: %s", parsed.Path) + return nil, fmt.Errorf("file does not exist: %s", parsed.Path) } content, err := t.deps.Ops.ReadFile(ctx, parsed.Path) if err != nil { - return false, "", err + return nil, err } if content == "" { - return false, "", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "", + Summary: fmt.Sprintf("File %s is empty", parsed.Path), + }, nil } lines := []string{} @@ -134,5 +138,9 @@ func (t *FileReadTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (b notice = "\nTo save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with grep in order to find the line numbers of what you are looking for." } - return false, strings.Join(lines, "\n") + notice, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: strings.Join(lines, "\n") + notice, + Summary: fmt.Sprintf("Read %d lines from %s", len(lines), parsed.Path), + }, nil } diff --git a/shared/tools/file_write.go b/shared/tools/file_write.go index 9534674c7..0ac9fcf57 100644 --- a/shared/tools/file_write.go +++ b/shared/tools/file_write.go @@ -75,20 +75,24 @@ func (t *FileWriteTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *FileWriteTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *FileWriteTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("file_write requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("file_write requires ops provider (tool cannot run in this context)") } err = t.deps.Ops.WriteFile(ctx, parsed.Path, parsed.Content, 0644) if err != nil { - return false, "", err + return nil, err } - return false, "File written successfully", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "File written successfully", + Summary: fmt.Sprintf("Wrote %d bytes to %s", len(parsed.Content), parsed.Path), + }, nil } diff --git a/shared/tools/gather.go b/shared/tools/gather.go index e4fcc0451..55fc33305 100644 --- a/shared/tools/gather.go +++ b/shared/tools/gather.go @@ -56,14 +56,14 @@ func (t *GatherTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *GatherTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *GatherTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("gather requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("gather requires ops provider (tool cannot run in this context)") } processID := uuid.New().String() @@ -76,7 +76,7 @@ func (t *GatherTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (boo // Run command synchronously (wait for completion) with 5-minute timeout runResult, err := t.deps.Ops.ExecRun(ctx, "", processID, rootDir, parsed.Command, true, 300, nil) if err != nil { - return false, "", err + return nil, err } toolResponse := map[string]any{ @@ -91,8 +91,12 @@ func (t *GatherTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (boo jsonResponse, err := json.Marshal(toolResponse) if err != nil { - return false, "", fmt.Errorf("failed to marshal tool result to json: %w", err) + return nil, fmt.Errorf("failed to marshal tool result to json: %w", err) } - return false, string(jsonResponse), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: string(jsonResponse), + Summary: fmt.Sprintf("Ran command (exit code %d)", toolResponse["exit_code"]), + }, nil } diff --git a/shared/tools/get_environment.go b/shared/tools/get_environment.go index 38c5ab116..7fd6ac92c 100644 --- a/shared/tools/get_environment.go +++ b/shared/tools/get_environment.go @@ -60,14 +60,14 @@ func (t *GetEnvironmentTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *GetEnvironmentTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *GetEnvironmentTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { _, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("get_environment requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("get_environment requires ops provider (tool cannot run in this context)") } // Get environment information @@ -93,8 +93,12 @@ func (t *GetEnvironmentTool) ExecuteRaw(ctx context.Context, input json.RawMessa jsonResponse, err := json.Marshal(response) if err != nil { - return false, "", fmt.Errorf("failed to marshal environment info to json: %w", err) + return nil, fmt.Errorf("failed to marshal environment info to json: %w", err) } - return false, string(jsonResponse), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: string(jsonResponse), + Summary: "Retrieved environment information", + }, nil } diff --git a/shared/tools/glob.go b/shared/tools/glob.go index 1a179fd96..cbf98ceb6 100644 --- a/shared/tools/glob.go +++ b/shared/tools/glob.go @@ -71,23 +71,23 @@ func (t *GlobTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *GlobTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *GlobTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("glob requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("glob requires ops provider (tool cannot run in this context)") } // Check if path exists exists, err := t.deps.Ops.PathExists(ctx, parsed.Path) if err != nil { - return false, "", err + return nil, err } if !exists { - return false, "", fmt.Errorf("path does not exist: %s", parsed.Path) + return nil, fmt.Errorf("path does not exist: %s", parsed.Path) } t.deps.Logger.Infow("searching files with glob pattern", @@ -97,7 +97,7 @@ func (t *GlobTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, files, err := t.deps.Ops.Glob(ctx, parsed.Path, parsed.Pattern, parsed.ShowHidden) if err != nil { - return false, "", err + return nil, err } notice := "" @@ -107,8 +107,16 @@ func (t *GlobTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, } if len(files) == 0 { - return false, "No files found matching pattern", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "No files found matching pattern", + Summary: "No matches found", + }, nil } - return false, strings.Join(files, "\n") + notice, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: strings.Join(files, "\n") + notice, + Summary: fmt.Sprintf("Found %d files matching pattern", len(files)), + }, nil } diff --git a/shared/tools/inform_user.go b/shared/tools/inform_user.go index a067d86da..280040323 100644 --- a/shared/tools/inform_user.go +++ b/shared/tools/inform_user.go @@ -44,13 +44,17 @@ func (t *InformUserTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *InformUserTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *InformUserTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := Parse[informUserInput](input) if err != nil { - return false, "", err + return nil, err } t.deps.Logger.Infow("inform_user tool executed", "message", parsed.Message) - return false, "User was informed", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "User was informed", + Summary: "Informed user", + }, nil } diff --git a/shared/tools/kill_process.go b/shared/tools/kill_process.go index 512e106b1..6f5181dca 100644 --- a/shared/tools/kill_process.go +++ b/shared/tools/kill_process.go @@ -53,14 +53,14 @@ func (t *KillProcessTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *KillProcessTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *KillProcessTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("kill_process requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("kill_process requires ops provider (tool cannot run in this context)") } t.deps.Logger.Infow("killing process", @@ -69,13 +69,13 @@ func (t *KillProcessTool) ExecuteRaw(ctx context.Context, input json.RawMessage) // Get process info before killing it runResult, err := t.deps.Ops.ExecStatus(ctx, parsed.ProcessID, false, 0) if err != nil { - return false, "", fmt.Errorf("process %s not found: %w", parsed.ProcessID, err) + return nil, fmt.Errorf("process %s not found: %w", parsed.ProcessID, err) } // Kill the process err = t.deps.Ops.ExecKill(ctx, parsed.ProcessID) if err != nil { - return false, "", err + return nil, err } response := map[string]any{ @@ -86,8 +86,12 @@ func (t *KillProcessTool) ExecuteRaw(ctx context.Context, input json.RawMessage) jsonResponse, err := json.Marshal(response) if err != nil { - return false, "", fmt.Errorf("failed to marshal tool result to json: %w", err) + return nil, fmt.Errorf("failed to marshal tool result to json: %w", err) } - return false, string(jsonResponse), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: string(jsonResponse), + Summary: fmt.Sprintf("Killed process %s", parsed.ProcessID), + }, nil } diff --git a/shared/tools/list_connected_app_actions.go b/shared/tools/list_connected_app_actions.go index 5948a34a3..e91f85afa 100644 --- a/shared/tools/list_connected_app_actions.go +++ b/shared/tools/list_connected_app_actions.go @@ -66,23 +66,23 @@ func (t *ListConnectedAppActionsTool) ValidateRawInput(input json.RawMessage) er return err } -func (t *ListConnectedAppActionsTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ListConnectedAppActionsTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.ConnectedAppClient == nil { - return false, "", fmt.Errorf("list_connected_app_actions requires a connected app client (tool cannot run in this context)") + return nil, fmt.Errorf("list_connected_app_actions requires a connected app client (tool cannot run in this context)") } if t.deps.UserPath == "" { - return false, "", fmt.Errorf("list_connected_app_actions requires a user path (not available in current context)") + return nil, fmt.Errorf("list_connected_app_actions requires a user path (not available in current context)") } userID := extractUserIDFromPath(t.deps.UserPath) if userID == "" { - return false, "", fmt.Errorf("failed to extract user ID from user path") + return nil, fmt.Errorf("failed to extract user ID from user path") } t.deps.Logger.Infow("listing connected app actions", @@ -91,7 +91,7 @@ func (t *ListConnectedAppActionsTool) ExecuteRaw(ctx context.Context, input json actions, err := t.deps.ConnectedAppClient.ListConnectedAppActions(ctx, userID, parsed.AppSlug, parsed.AccountID) if err != nil { - return false, "", fmt.Errorf("failed to list actions for app %s: %w", parsed.AppSlug, err) + return nil, fmt.Errorf("failed to list actions for app %s: %w", parsed.AppSlug, err) } t.deps.Logger.Infow("successfully listed connected app actions", @@ -100,7 +100,11 @@ func (t *ListConnectedAppActionsTool) ExecuteRaw(ctx context.Context, input json "count", len(actions)) // Format the response - return false, formatActions(parsed.AppSlug, actions), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: formatActions(parsed.AppSlug, actions), + Summary: fmt.Sprintf("Listed %d actions for %s", len(actions), parsed.AppSlug), + }, nil } func formatActions(appSlug string, actions []ConnectedAppAction) string { diff --git a/shared/tools/list_connected_apps.go b/shared/tools/list_connected_apps.go index 4b37281d6..9c3a9f90f 100644 --- a/shared/tools/list_connected_apps.go +++ b/shared/tools/list_connected_apps.go @@ -57,23 +57,23 @@ func (t *ListConnectedAppsTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *ListConnectedAppsTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ListConnectedAppsTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := Parse[listConnectedAppsInput](input) if err != nil { - return false, "", err + return nil, err } if t.deps.ConnectedAppClient == nil { - return false, "", fmt.Errorf("list_connected_apps requires a connected app client (tool cannot run in this context)") + return nil, fmt.Errorf("list_connected_apps requires a connected app client (tool cannot run in this context)") } if t.deps.UserPath == "" { - return false, "", fmt.Errorf("list_connected_apps requires a user path (not available in current context)") + return nil, fmt.Errorf("list_connected_apps requires a user path (not available in current context)") } userID := extractUserIDFromPath(t.deps.UserPath) if userID == "" { - return false, "", fmt.Errorf("failed to extract user ID from user path") + return nil, fmt.Errorf("failed to extract user ID from user path") } t.deps.Logger.Infow("listing connected apps", @@ -81,7 +81,7 @@ func (t *ListConnectedAppsTool) ExecuteRaw(ctx context.Context, input json.RawMe apps, err := t.deps.ConnectedAppClient.ListConnectedApps(ctx, userID, parsed.SearchFilter) if err != nil { - return false, "", fmt.Errorf("failed to list connected apps: %w", err) + return nil, fmt.Errorf("failed to list connected apps: %w", err) } t.deps.Logger.Infow("successfully listed connected apps", @@ -89,7 +89,11 @@ func (t *ListConnectedAppsTool) ExecuteRaw(ctx context.Context, input json.RawMe "count", len(apps)) // Format the response - return false, formatConnectedAccounts(apps), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: formatConnectedAccounts(apps), + Summary: fmt.Sprintf("Listed %d connected apps", len(apps)), + }, nil } func formatConnectedAccounts(apps []ConnectedApp) string { diff --git a/shared/tools/list_processes.go b/shared/tools/list_processes.go index 79a6262ed..fb331df91 100644 --- a/shared/tools/list_processes.go +++ b/shared/tools/list_processes.go @@ -54,14 +54,14 @@ func (t *ListProcessesTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *ListProcessesTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ListProcessesTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("list_processes requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("list_processes requires ops provider (tool cannot run in this context)") } status := "all" @@ -77,12 +77,16 @@ func (t *ListProcessesTool) ExecuteRaw(ctx context.Context, input json.RawMessag "process_count", len(processIDs)) if len(processIDs) == 0 { - return false, "No background processes running", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "No background processes running", + Summary: "No processes running", + }, nil } processes, err := t.deps.Ops.ExecList(ctx, processIDs, false, 0) if err != nil { - return false, "", err + return nil, err } // Filter by status if specified @@ -109,7 +113,11 @@ func (t *ListProcessesTool) ExecuteRaw(ctx context.Context, input json.RawMessag } if len(filteredProcesses) == 0 { - return false, fmt.Sprintf("No processes found with status: %s", status), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: fmt.Sprintf("No processes found with status: %s", status), + Summary: "No processes found", + }, nil } result := map[string]any{ @@ -119,8 +127,12 @@ func (t *ListProcessesTool) ExecuteRaw(ctx context.Context, input json.RawMessag jsonResponse, err := json.Marshal(result) if err != nil { - return false, "", fmt.Errorf("failed to marshal tool result to json: %w", err) + return nil, fmt.Errorf("failed to marshal tool result to json: %w", err) } - return false, string(jsonResponse), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: string(jsonResponse), + Summary: fmt.Sprintf("Listed %d processes", len(filteredProcesses)), + }, nil } diff --git a/shared/tools/list_todos.go b/shared/tools/list_todos.go index 42cd1f279..f8a253b86 100644 --- a/shared/tools/list_todos.go +++ b/shared/tools/list_todos.go @@ -52,18 +52,18 @@ func (t *ListTodosTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *ListTodosTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ListTodosTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := Parse[listTodosInput](input) if err != nil { - return false, "", err + return nil, err } if t.deps.Backend == nil { - return false, "", fmt.Errorf("list_todos requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("list_todos requires backend (tool cannot run in this context)") } if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for list_todos (not available in current context)") + return nil, fmt.Errorf("thread path required for list_todos (not available in current context)") } t.deps.Logger.Infow("listing todos", @@ -73,7 +73,7 @@ func (t *ListTodosTool) ExecuteRaw(ctx context.Context, input json.RawMessage) ( todos, err := t.deps.Backend.ListTodos(ctx, t.deps.ThreadPath, parsed.ProgressFilter, parsed.PriorityFilter) if err != nil { - return false, "", fmt.Errorf("failed to list todos: %w", err) + return nil, fmt.Errorf("failed to list todos: %w", err) } if len(todos) == 0 { @@ -85,7 +85,11 @@ func (t *ListTodosTool) ExecuteRaw(ctx context.Context, input json.RawMessage) ( } else if parsed.PriorityFilter != "" { message = fmt.Sprintf("No todos with priority '%s'", parsed.PriorityFilter) } - return false, message, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: message, + Summary: "No todos found", + }, nil } // Format todos for display @@ -110,5 +114,9 @@ func (t *ListTodosTool) ExecuteRaw(ctx context.Context, input json.RawMessage) ( "priority_filter", parsed.PriorityFilter, "todos_count", len(todos)) - return false, output.String(), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: output.String(), + Summary: fmt.Sprintf("Listed %d todo(s)", len(todos)), + }, nil } diff --git a/shared/tools/process_output.go b/shared/tools/process_output.go index c9dd61823..61f8bcbfa 100644 --- a/shared/tools/process_output.go +++ b/shared/tools/process_output.go @@ -56,14 +56,14 @@ func (t *ProcessOutputTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *ProcessOutputTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *ProcessOutputTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Ops == nil { - return false, "", fmt.Errorf("process_output requires ops provider (tool cannot run in this context)") + return nil, fmt.Errorf("process_output requires ops provider (tool cannot run in this context)") } // Default to 100 lines if not specified @@ -78,7 +78,7 @@ func (t *ProcessOutputTool) ExecuteRaw(ctx context.Context, input json.RawMessag runResult, err := t.deps.Ops.ExecStatus(ctx, parsed.ProcessID, true, lines) if err != nil { - return false, "", fmt.Errorf("process %s not found: %w", parsed.ProcessID, err) + return nil, fmt.Errorf("process %s not found: %w", parsed.ProcessID, err) } // Truncate output to prevent too much token usage @@ -99,8 +99,12 @@ func (t *ProcessOutputTool) ExecuteRaw(ctx context.Context, input json.RawMessag jsonResponse, err := json.Marshal(response) if err != nil { - return false, "", fmt.Errorf("failed to marshal tool result to json: %w", err) + return nil, fmt.Errorf("failed to marshal tool result to json: %w", err) } - return false, string(jsonResponse), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: string(jsonResponse), + Summary: fmt.Sprintf("Retrieved output for process %s", parsed.ProcessID), + }, nil } diff --git a/shared/tools/remove_todo.go b/shared/tools/remove_todo.go index db8694a35..3562d9658 100644 --- a/shared/tools/remove_todo.go +++ b/shared/tools/remove_todo.go @@ -59,18 +59,18 @@ func (t *RemoveTodoTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *RemoveTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *RemoveTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Backend == nil { - return false, "", fmt.Errorf("remove_todo requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("remove_todo requires backend (tool cannot run in this context)") } if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for remove_todo (not available in current context)") + return nil, fmt.Errorf("thread path required for remove_todo (not available in current context)") } // Build todo path from thread path + todo UID @@ -80,8 +80,12 @@ func (t *RemoveTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) err = t.deps.Backend.DeleteTodo(ctx, todoPath) if err != nil { - return false, "", fmt.Errorf("failed to remove todo: %w", err) + return nil, fmt.Errorf("failed to remove todo: %w", err) } - return false, "Todo removed", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "Todo removed", + Summary: "Removed todo", + }, nil } diff --git a/shared/tools/start_todo.go b/shared/tools/start_todo.go index 71d2f5ac7..bdc708ac9 100644 --- a/shared/tools/start_todo.go +++ b/shared/tools/start_todo.go @@ -56,18 +56,18 @@ func (t *StartTodoTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *StartTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *StartTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Backend == nil { - return false, "", fmt.Errorf("start_todo requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("start_todo requires backend (tool cannot run in this context)") } if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for start_todo (not available in current context)") + return nil, fmt.Errorf("thread path required for start_todo (not available in current context)") } // Build todo path from thread path + todo UID @@ -78,10 +78,14 @@ func (t *StartTodoTool) ExecuteRaw(ctx context.Context, input json.RawMessage) ( // Update progress to in_progress err = t.deps.Backend.UpdateTodo(ctx, todoPath, "in_progress", "") if err != nil { - return false, "", fmt.Errorf("failed to start todo: %w", err) + return nil, fmt.Errorf("failed to start todo: %w", err) } t.deps.Logger.Infow("todo started", "todo_uid", parsed.TodoUID, "thread_path", t.deps.ThreadPath) - return false, "Todo marked as in progress", nil + return &ToolExecutionResult{ + StopIteration: false, + Result: "Todo marked as in progress", + Summary: "Started todo", + }, nil } diff --git a/shared/tools/tools.go b/shared/tools/tools.go index 07fc1143c..657d6e9a7 100644 --- a/shared/tools/tools.go +++ b/shared/tools/tools.go @@ -21,6 +21,13 @@ const ( // ToolInputSchema represents the schema for tool inputs type ToolInputSchema = llm.ToolInputSchema +// ToolExecutionResult contains the result of a tool execution +type ToolExecutionResult struct { + StopIteration bool // Whether to stop the LLM iteration loop + Result string // The full result text + Summary string // Brief summary for client notifications +} + // Tool interface - same as tim-server but with adjusted dependencies type Tool interface { Name() string @@ -33,7 +40,7 @@ type Tool interface { ValidateRawInput(input json.RawMessage) error // ExecuteRaw executes the tool with raw JSON input // Tools implement this method, typically by calling t.ParseInput() then executing with typed data - ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) + ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) } // ToolDependencies contains the dependencies needed for tool execution diff --git a/shared/tools/update_todo_priority.go b/shared/tools/update_todo_priority.go index 4cbdb3639..63a334206 100644 --- a/shared/tools/update_todo_priority.go +++ b/shared/tools/update_todo_priority.go @@ -67,18 +67,18 @@ func (t *UpdateTodoPriorityTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *UpdateTodoPriorityTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *UpdateTodoPriorityTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } if t.deps.Backend == nil { - return false, "", fmt.Errorf("update_todo_priority requires backend (tool cannot run in this context)") + return nil, fmt.Errorf("update_todo_priority requires backend (tool cannot run in this context)") } if t.deps.ThreadPath == "" { - return false, "", fmt.Errorf("thread path required for update_todo_priority (not available in current context)") + return nil, fmt.Errorf("thread path required for update_todo_priority (not available in current context)") } // Build todo path from thread path + todo UID @@ -92,8 +92,12 @@ func (t *UpdateTodoPriorityTool) ExecuteRaw(ctx context.Context, input json.RawM // Update only priority err = t.deps.Backend.UpdateTodo(ctx, todoPath, "", parsed.Priority) if err != nil { - return false, "", fmt.Errorf("failed to update todo priority: %w", err) + return nil, fmt.Errorf("failed to update todo priority: %w", err) } - return false, fmt.Sprintf("Todo priority updated to '%s'", parsed.Priority), nil + return &ToolExecutionResult{ + StopIteration: false, + Result: fmt.Sprintf("Todo priority updated to '%s'", parsed.Priority), + Summary: fmt.Sprintf("Updated priority to %s", parsed.Priority), + }, nil } diff --git a/shared/tools/web_search.go b/shared/tools/web_search.go index 96ff60884..2e71a61c6 100644 --- a/shared/tools/web_search.go +++ b/shared/tools/web_search.go @@ -67,24 +67,28 @@ func (t *WebSearchTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *WebSearchTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *WebSearchTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := ValidateAndParse(input, t.validate) if err != nil { - return false, "", err + return nil, err } t.deps.Logger.Infow("performing web search", "query", parsed.Query) results, err := t.performGeminiSearch(ctx, parsed.Query) if err != nil { - return false, "", fmt.Errorf("web search failed: %w", err) + return nil, fmt.Errorf("web search failed: %w", err) } t.deps.Logger.Infow("web search completed", "query", parsed.Query, "result_length", len(results)) - return false, results, nil + return &ToolExecutionResult{ + StopIteration: false, + Result: results, + Summary: fmt.Sprintf("Searched for: %s", parsed.Query), + }, nil } func (t *WebSearchTool) performGeminiSearch(ctx context.Context, query string) (string, error) { diff --git a/shared/tools/work_complete.go b/shared/tools/work_complete.go index 31b78d432..2ccb3244e 100644 --- a/shared/tools/work_complete.go +++ b/shared/tools/work_complete.go @@ -89,10 +89,10 @@ func (t *WorkCompleteTool) ValidateRawInput(input json.RawMessage) error { return err } -func (t *WorkCompleteTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) { +func (t *WorkCompleteTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (*ToolExecutionResult, error) { parsed, err := Parse[workCompleteInput](input) if err != nil { - return false, "", err + return nil, err } t.deps.Logger.Infow("work completed", "summary", parsed.Summary) @@ -100,5 +100,9 @@ func (t *WorkCompleteTool) ExecuteRaw(ctx context.Context, input json.RawMessage // Return confirmation rather than echoing the summary // The LLM already knows what summary it sent // Return true to signal stopIteration - conversation is complete - return true, "Work complete", nil + return &ToolExecutionResult{ + StopIteration: true, + Result: "Work complete", + Summary: "Work complete", + }, nil } diff --git a/tim-api/internal/events/thread_events.go b/tim-api/internal/events/thread_events.go index fa713d357..2db95d791 100644 --- a/tim-api/internal/events/thread_events.go +++ b/tim-api/internal/events/thread_events.go @@ -11,7 +11,9 @@ const ( ThreadEventTypeContentStart ThreadEventType = "content_start" ThreadEventTypeContentDelta ThreadEventType = "content_delta" ThreadEventTypeContentStop ThreadEventType = "content_stop" + ThreadEventTypeToolCallStart ThreadEventType = "tool_call_start" ThreadEventTypeToolCall ThreadEventType = "tool_call" + ThreadEventTypeToolCallComplete ThreadEventType = "tool_call_complete" ThreadEventTypeThreadStateChange ThreadEventType = "thread_state_change" ThreadEventTypeStreamError ThreadEventType = "stream_error" ) @@ -61,12 +63,27 @@ type ContentStopPayload struct { ContentUID uuid.UUID `json:"content_uid"` } +// ToolCallStartPayload is the payload for tool_call_start events +type ToolCallStartPayload struct { + ThreadUID uuid.UUID `json:"thread_uid"` + ToolCallID string `json:"tool_call_id"` + ToolName string `json:"tool_name"` +} + // ToolCallPayload is the payload for tool_call events type ToolCallPayload struct { ThreadUID uuid.UUID `json:"thread_uid"` ToolCallID string `json:"tool_call_id"` } +// ToolCallCompletePayload is the payload for tool_call_complete events +type ToolCallCompletePayload struct { + ThreadUID uuid.UUID `json:"thread_uid"` + ToolCallID string `json:"tool_call_id"` + Success bool `json:"success"` + Summary string `json:"summary"` +} + // ThreadStateChangePayload is the payload for thread_state_change events type ThreadStateChangePayload struct { ThreadUID uuid.UUID `json:"thread_uid"` @@ -93,7 +110,9 @@ type ThreadEventEnvelope struct { ContentStart *ContentStartPayload `json:"content_start,omitempty"` ContentDelta *ContentDeltaPayload `json:"content_delta,omitempty"` ContentStop *ContentStopPayload `json:"content_stop,omitempty"` + ToolCallStart *ToolCallStartPayload `json:"tool_call_start,omitempty"` ToolCall *ToolCallPayload `json:"tool_call,omitempty"` + ToolCallComplete *ToolCallCompletePayload `json:"tool_call_complete,omitempty"` ThreadStateChange *ThreadStateChangePayload `json:"thread_state_change,omitempty"` MessageComplete *MessageCompletePayload `json:"message_complete,omitempty"` StreamError *StreamErrorPayload `json:"stream_error,omitempty"` diff --git a/tim-api/internal/natsnotifier/thread_event_notifier.go b/tim-api/internal/natsnotifier/thread_event_notifier.go index 107b0e393..5837b8dee 100644 --- a/tim-api/internal/natsnotifier/thread_event_notifier.go +++ b/tim-api/internal/natsnotifier/thread_event_notifier.go @@ -103,6 +103,42 @@ func (ten *ThreadEventNotifier) NotifyToolCall(toolCallID string) { }) } +// NotifyToolCallStart sends a notification when a tool call is about to be executed +func (ten *ThreadEventNotifier) NotifyToolCallStart(toolCallID, toolName string) { + envelope := events.ThreadEventEnvelope{ + Type: events.ThreadEventTypeToolCallStart, + ToolCallStart: &events.ToolCallStartPayload{ + ThreadUID: ten.threadPath.ThreadUID, + ToolCallID: toolCallID, + ToolName: toolName, + }, + } + + ten.sendNotification(envelope, map[string]any{ + "tool_call_id": toolCallID, + "tool_name": toolName, + }) +} + +// NotifyToolCallComplete sends a notification when a tool call has completed execution +func (ten *ThreadEventNotifier) NotifyToolCallComplete(toolCallID string, success bool, summary string) { + envelope := events.ThreadEventEnvelope{ + Type: events.ThreadEventTypeToolCallComplete, + ToolCallComplete: &events.ToolCallCompletePayload{ + ThreadUID: ten.threadPath.ThreadUID, + ToolCallID: toolCallID, + Success: success, + Summary: summary, + }, + } + + ten.sendNotification(envelope, map[string]any{ + "tool_call_id": toolCallID, + "success": success, + "summary": summary, + }) +} + // NotifyThreadStateChange sends a notification when thread state changes func (ten *ThreadEventNotifier) NotifyThreadStateChange(newState db.ThreadLlmStatus) { var llmState events.ThreadLLMState diff --git a/tim-api/internal/services/llm_response/handlers.go b/tim-api/internal/services/llm_response/handlers.go index 7ba549abd..4a54f2318 100644 --- a/tim-api/internal/services/llm_response/handlers.go +++ b/tim-api/internal/services/llm_response/handlers.go @@ -285,6 +285,7 @@ type contentBlockState struct { buffer string // Accumulates content deltas regardless of type toolName string providerToolUseID string + toolCallID string // Internal tool call ID (generated for tool_use blocks) signature string // Signature for thinking blocks (used to verify integrity) } @@ -388,11 +389,18 @@ func (s *Service) handleContentBlockStart( "tool_name", start.ToolName, ) + // Generate internal tool call ID for tool_use blocks + toolCallID := "" + if contentType == db.LlmMessageContentTypeToolUse { + toolCallID = llm.GenerateToolUseID() + } + blocks[start.Index] = &contentBlockState{ uid: content.UID, contentType: contentType, toolName: start.ToolName, providerToolUseID: start.ToolCallId, + toolCallID: toolCallID, } // Send notification for content start (only for text/thinking) @@ -408,6 +416,12 @@ func (s *Service) handleContentBlockStart( } } + // Send tool_call_start notification for tool_use blocks + // This lets clients show that a tool is being prepared before we have the full input + if contentType == db.LlmMessageContentTypeToolUse && toolCallID != "" { + notifier.NotifyToolCallStart(toolCallID, start.ToolName) + } + return nil } @@ -497,7 +511,7 @@ func (s *Service) handleContentBlockStop( toolInput = "{}" } toolUseContent = llm.ToolUseContent{ - ID: llm.GenerateToolUseID(), + ID: block.toolCallID, ProviderToolUseID: block.providerToolUseID, Name: block.toolName, Input: json.RawMessage(toolInput), diff --git a/tim-api/internal/services/llm_response/system_tool_execution.go b/tim-api/internal/services/llm_response/system_tool_execution.go index 3354882ed..291ddbbf7 100644 --- a/tim-api/internal/services/llm_response/system_tool_execution.go +++ b/tim-api/internal/services/llm_response/system_tool_execution.go @@ -93,6 +93,7 @@ func (s *Service) executeSystemTool(ctx context.Context, threadPath *resourcepat result = &llm.ToolResultContent{ ToolUseID: toolUseContent.ID, Result: fmt.Sprintf("Tool execution error: %v", err), + Summary: fmt.Sprintf("Error: %v", err), IsError: true, StopIteration: false, } @@ -112,6 +113,10 @@ func (s *Service) executeSystemTool(ctx context.Context, threadPath *resourcepat // Store the result tracker.setResult(toolUseContent.ID, result, nil) + + // Emit tool_call_complete event for system tools + notifier := natsnotifier.NewThreadEventNotifier(ctx, s.nats, threadPath, s.logger) + notifier.NotifyToolCallComplete(toolUseContent.ID, !result.IsError, result.Summary) } // writeSystemToolResults writes all system tool results to the database diff --git a/tim-api/internal/services/tool_execution/handlers.go b/tim-api/internal/services/tool_execution/handlers.go index e983231d9..5995f0a47 100644 --- a/tim-api/internal/services/tool_execution/handlers.go +++ b/tim-api/internal/services/tool_execution/handlers.go @@ -259,6 +259,7 @@ func (s *Service) SubmitToolResult( toolResultContent := llm.ToolResultContent{ ToolUseID: req.Msg.ToolResult.ToolCallId, Result: req.Msg.ToolResult.Result, + Summary: req.Msg.ToolResult.Summary, IsError: !req.Msg.ToolResult.Success, StopIteration: req.Msg.ToolResult.StopIteration, } @@ -299,7 +300,18 @@ func (s *Service) SubmitToolResult( "message_uid", message.UID) s.metrics.RecordToolExecution(ctx, toolUseContent.Name, req.Msg.ToolResult.Success) - s.analytics.ToolExecuted(*toolCallPath.Owner().UserUID, toolCallPath.ToolCallID, toolUseContent.Name, toolCallPath.Parent.ThreadUID, req.Msg.ToolResult.Success, content.CreateTime.Time) + s.analytics.ToolExecuted( + *toolCallPath.Owner().UserUID, + toolCallPath.ToolCallID, + toolUseContent.Name, + toolCallPath.Parent.ThreadUID, + req.Msg.ToolResult.Success, + content.CreateTime.Time, + ) + + // Emit tool_call_complete event + notifier := natsnotifier.NewThreadEventNotifier(ctx, s.nats, toolCallPath.Parent, s.logger) + notifier.NotifyToolCallComplete(toolCallPath.ToolCallID.String(), req.Msg.ToolResult.Success, toolResultContent.Summary) // Check if all tool results have been submitted and if we should continue the LLM loop if err := s.checkAndEnqueueLLMRelayIfNeeded(ctx, queries, toolCallPath, assistantMessage, message); err != nil { diff --git a/tim-proto/gen/openapi.yaml b/tim-proto/gen/openapi.yaml index b7de93739..dcab258f2 100644 --- a/tim-proto/gen/openapi.yaml +++ b/tim-proto/gen/openapi.yaml @@ -3296,10 +3296,18 @@ components: allOf: - $ref: '#/components/schemas/ContentStopEvent' description: A content block has completed + toolCallStart: + allOf: + - $ref: '#/components/schemas/ToolCallStartEvent' + description: A tool call is about to be executed (tool name known, input may be incomplete) toolCall: allOf: - $ref: '#/components/schemas/ToolCall' - description: A tool call made by the LLM (input may be null if >512KB) + description: A tool call made by the LLM with full input (input may be null if >512KB) + toolCallComplete: + allOf: + - $ref: '#/components/schemas/ToolCallCompleteEvent' + description: A tool call has completed execution with results threadStateChange: allOf: - $ref: '#/components/schemas/ThreadStateChangeEvent' @@ -3603,6 +3611,29 @@ components: type: string description: Provider tool call ID (the ID assigned by the LLM provider for this tool call) description: ToolCall represents a call to a specific tool with its corresponding input parameters. + ToolCallCompleteEvent: + type: object + properties: + id: + type: string + description: Tool call ID (UUID) + success: + type: boolean + description: Whether the tool call was successful + summary: + type: string + description: Brief summary of what the tool did + description: ToolCallCompleteEvent signals that a tool call has completed execution + ToolCallStartEvent: + type: object + properties: + id: + type: string + description: Tool call ID (UUID) + toolName: + type: string + description: Name of the tool being called + description: ToolCallStartEvent signals that a tool call is about to be executed ToolChoice: type: object properties: @@ -3665,6 +3696,11 @@ components: description: |- Whether this tool result should stop the LLM iteration loop Set to true by tools like query_complete, code_complete to signal conversation completion + summary: + type: string + description: |- + A brief summary of what the tool did (e.g., "Updated todo to in_progress", "Read 234 lines") + Used for client notifications without exposing full result description: The result of a tool call User: type: object diff --git a/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.pb.go b/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.pb.go index 885913123..a84ec65ea 100644 --- a/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.pb.go +++ b/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.pb.go @@ -656,7 +656,9 @@ type StreamThreadEventsResponse struct { // *StreamThreadEventsResponse_ContentStart // *StreamThreadEventsResponse_ContentDelta // *StreamThreadEventsResponse_ContentStop + // *StreamThreadEventsResponse_ToolCallStart // *StreamThreadEventsResponse_ToolCall + // *StreamThreadEventsResponse_ToolCallComplete // *StreamThreadEventsResponse_ThreadStateChange // *StreamThreadEventsResponse_StreamError Event isStreamThreadEventsResponse_Event `protobuf_oneof:"event"` @@ -728,6 +730,15 @@ func (x *StreamThreadEventsResponse) GetContentStop() *ContentStopEvent { return nil } +func (x *StreamThreadEventsResponse) GetToolCallStart() *ToolCallStartEvent { + if x != nil { + if x, ok := x.Event.(*StreamThreadEventsResponse_ToolCallStart); ok { + return x.ToolCallStart + } + } + return nil +} + func (x *StreamThreadEventsResponse) GetToolCall() *v1alpha1.ToolCall { if x != nil { if x, ok := x.Event.(*StreamThreadEventsResponse_ToolCall); ok { @@ -737,6 +748,15 @@ func (x *StreamThreadEventsResponse) GetToolCall() *v1alpha1.ToolCall { return nil } +func (x *StreamThreadEventsResponse) GetToolCallComplete() *ToolCallCompleteEvent { + if x != nil { + if x, ok := x.Event.(*StreamThreadEventsResponse_ToolCallComplete); ok { + return x.ToolCallComplete + } + } + return nil +} + func (x *StreamThreadEventsResponse) GetThreadStateChange() *ThreadStateChangeEvent { if x != nil { if x, ok := x.Event.(*StreamThreadEventsResponse_ThreadStateChange); ok { @@ -774,19 +794,29 @@ type StreamThreadEventsResponse_ContentStop struct { ContentStop *ContentStopEvent `protobuf:"bytes,3,opt,name=content_stop,json=contentStop,proto3,oneof"` } +type StreamThreadEventsResponse_ToolCallStart struct { + // A tool call is about to be executed (tool name known, input may be incomplete) + ToolCallStart *ToolCallStartEvent `protobuf:"bytes,4,opt,name=tool_call_start,json=toolCallStart,proto3,oneof"` +} + type StreamThreadEventsResponse_ToolCall struct { - // A tool call made by the LLM (input may be null if >512KB) - ToolCall *v1alpha1.ToolCall `protobuf:"bytes,4,opt,name=tool_call,json=toolCall,proto3,oneof"` + // A tool call made by the LLM with full input (input may be null if >512KB) + ToolCall *v1alpha1.ToolCall `protobuf:"bytes,5,opt,name=tool_call,json=toolCall,proto3,oneof"` +} + +type StreamThreadEventsResponse_ToolCallComplete struct { + // A tool call has completed execution with results + ToolCallComplete *ToolCallCompleteEvent `protobuf:"bytes,6,opt,name=tool_call_complete,json=toolCallComplete,proto3,oneof"` } type StreamThreadEventsResponse_ThreadStateChange struct { // The thread's LLM processing state has changed (IDLE or PROCESSING) - ThreadStateChange *ThreadStateChangeEvent `protobuf:"bytes,5,opt,name=thread_state_change,json=threadStateChange,proto3,oneof"` + ThreadStateChange *ThreadStateChangeEvent `protobuf:"bytes,7,opt,name=thread_state_change,json=threadStateChange,proto3,oneof"` } type StreamThreadEventsResponse_StreamError struct { // The LLM stream encountered an error - StreamError *StreamErrorEvent `protobuf:"bytes,6,opt,name=stream_error,json=streamError,proto3,oneof"` + StreamError *StreamErrorEvent `protobuf:"bytes,8,opt,name=stream_error,json=streamError,proto3,oneof"` } func (*StreamThreadEventsResponse_ContentStart) isStreamThreadEventsResponse_Event() {} @@ -795,8 +825,12 @@ func (*StreamThreadEventsResponse_ContentDelta) isStreamThreadEventsResponse_Eve func (*StreamThreadEventsResponse_ContentStop) isStreamThreadEventsResponse_Event() {} +func (*StreamThreadEventsResponse_ToolCallStart) isStreamThreadEventsResponse_Event() {} + func (*StreamThreadEventsResponse_ToolCall) isStreamThreadEventsResponse_Event() {} +func (*StreamThreadEventsResponse_ToolCallComplete) isStreamThreadEventsResponse_Event() {} + func (*StreamThreadEventsResponse_ThreadStateChange) isStreamThreadEventsResponse_Event() {} func (*StreamThreadEventsResponse_StreamError) isStreamThreadEventsResponse_Event() {} @@ -950,14 +984,16 @@ const file_tim_api_thread_v1alpha1_thread_service_proto_rawDesc = "" + "total_size\x18\x03 \x01(\x05B\n" + "\xbaH\a\xd8\x01\x01\x1a\x02(\x00R\ttotalSize\"\xa7\x01\n" + "\x19StreamThreadEventsRequest\x12\x89\x01\n" + - "\x06parent\x18\x01 \x01(\tBq\xe0A\x02\xbaHKrI2G^orgs/[a-fA-F0-9-]{36}/users/[a-fA-F0-9-]{36}/threads/[a-fA-F0-9-]{36}$\u0091\x05\x1c\x12\x1atim.settlerlabs.com/threadR\x06parent\"\x8e\x04\n" + + "\x06parent\x18\x01 \x01(\tBq\xe0A\x02\xbaHKrI2G^orgs/[a-fA-F0-9-]{36}/users/[a-fA-F0-9-]{36}/threads/[a-fA-F0-9-]{36}$\u0091\x05\x1c\x12\x1atim.settlerlabs.com/threadR\x06parent\"\xc5\x05\n" + "\x1aStreamThreadEventsResponse\x12Q\n" + "\rcontent_start\x18\x01 \x01(\v2*.tim.api.thread.v1alpha1.ContentStartEventH\x00R\fcontentStart\x12Q\n" + "\rcontent_delta\x18\x02 \x01(\v2*.tim.api.thread.v1alpha1.ContentDeltaEventH\x00R\fcontentDelta\x12N\n" + - "\fcontent_stop\x18\x03 \x01(\v2).tim.api.thread.v1alpha1.ContentStopEventH\x00R\vcontentStop\x12>\n" + - "\ttool_call\x18\x04 \x01(\v2\x1f.tim.api.tool.v1alpha1.ToolCallH\x00R\btoolCall\x12a\n" + - "\x13thread_state_change\x18\x05 \x01(\v2/.tim.api.thread.v1alpha1.ThreadStateChangeEventH\x00R\x11threadStateChange\x12N\n" + - "\fstream_error\x18\x06 \x01(\v2).tim.api.thread.v1alpha1.StreamErrorEventH\x00R\vstreamErrorB\a\n" + + "\fcontent_stop\x18\x03 \x01(\v2).tim.api.thread.v1alpha1.ContentStopEventH\x00R\vcontentStop\x12U\n" + + "\x0ftool_call_start\x18\x04 \x01(\v2+.tim.api.thread.v1alpha1.ToolCallStartEventH\x00R\rtoolCallStart\x12>\n" + + "\ttool_call\x18\x05 \x01(\v2\x1f.tim.api.tool.v1alpha1.ToolCallH\x00R\btoolCall\x12^\n" + + "\x12tool_call_complete\x18\x06 \x01(\v2..tim.api.thread.v1alpha1.ToolCallCompleteEventH\x00R\x10toolCallComplete\x12a\n" + + "\x13thread_state_change\x18\a \x01(\v2/.tim.api.thread.v1alpha1.ThreadStateChangeEventH\x00R\x11threadStateChange\x12N\n" + + "\fstream_error\x18\b \x01(\v2).tim.api.thread.v1alpha1.StreamErrorEventH\x00R\vstreamErrorB\a\n" + "\x05event\"\xfa\x01\n" + "\x18SubmitUserMessageRequest\x12\x89\x01\n" + "\x06parent\x18\x01 \x01(\tBq\xe0A\x02\xbaHKrI2G^orgs/[a-fA-F0-9-]{36}/users/[a-fA-F0-9-]{36}/threads/[a-fA-F0-9-]{36}$\u0091\x05\x1c\x12\x1atim.settlerlabs.com/threadR\x06parent\x12R\n" + @@ -1011,9 +1047,11 @@ var file_tim_api_thread_v1alpha1_thread_service_proto_goTypes = []any{ (*ContentStartEvent)(nil), // 17: tim.api.thread.v1alpha1.ContentStartEvent (*ContentDeltaEvent)(nil), // 18: tim.api.thread.v1alpha1.ContentDeltaEvent (*ContentStopEvent)(nil), // 19: tim.api.thread.v1alpha1.ContentStopEvent - (*v1alpha1.ToolCall)(nil), // 20: tim.api.tool.v1alpha1.ToolCall - (*ThreadStateChangeEvent)(nil), // 21: tim.api.thread.v1alpha1.ThreadStateChangeEvent - (*StreamErrorEvent)(nil), // 22: tim.api.thread.v1alpha1.StreamErrorEvent + (*ToolCallStartEvent)(nil), // 20: tim.api.thread.v1alpha1.ToolCallStartEvent + (*v1alpha1.ToolCall)(nil), // 21: tim.api.tool.v1alpha1.ToolCall + (*ToolCallCompleteEvent)(nil), // 22: tim.api.thread.v1alpha1.ToolCallCompleteEvent + (*ThreadStateChangeEvent)(nil), // 23: tim.api.thread.v1alpha1.ThreadStateChangeEvent + (*StreamErrorEvent)(nil), // 24: tim.api.thread.v1alpha1.StreamErrorEvent } var file_tim_api_thread_v1alpha1_thread_service_proto_depIdxs = []int32{ 14, // 0: tim.api.thread.v1alpha1.ListThreadsResponse.results:type_name -> tim.api.thread.v1alpha1.Thread @@ -1025,33 +1063,35 @@ var file_tim_api_thread_v1alpha1_thread_service_proto_depIdxs = []int32{ 17, // 6: tim.api.thread.v1alpha1.StreamThreadEventsResponse.content_start:type_name -> tim.api.thread.v1alpha1.ContentStartEvent 18, // 7: tim.api.thread.v1alpha1.StreamThreadEventsResponse.content_delta:type_name -> tim.api.thread.v1alpha1.ContentDeltaEvent 19, // 8: tim.api.thread.v1alpha1.StreamThreadEventsResponse.content_stop:type_name -> tim.api.thread.v1alpha1.ContentStopEvent - 20, // 9: tim.api.thread.v1alpha1.StreamThreadEventsResponse.tool_call:type_name -> tim.api.tool.v1alpha1.ToolCall - 21, // 10: tim.api.thread.v1alpha1.StreamThreadEventsResponse.thread_state_change:type_name -> tim.api.thread.v1alpha1.ThreadStateChangeEvent - 22, // 11: tim.api.thread.v1alpha1.StreamThreadEventsResponse.stream_error:type_name -> tim.api.thread.v1alpha1.StreamErrorEvent - 13, // 12: tim.api.thread.v1alpha1.SubmitUserMessageRequest.user_message:type_name -> tim.api.thread.v1alpha1.UserMessage - 0, // 13: tim.api.thread.v1alpha1.ThreadService.GetThread:input_type -> tim.api.thread.v1alpha1.GetThreadRequest - 1, // 14: tim.api.thread.v1alpha1.ThreadService.ListThreads:input_type -> tim.api.thread.v1alpha1.ListThreadsRequest - 3, // 15: tim.api.thread.v1alpha1.ThreadService.CreateThread:input_type -> tim.api.thread.v1alpha1.CreateThreadRequest - 4, // 16: tim.api.thread.v1alpha1.ThreadService.ForkThread:input_type -> tim.api.thread.v1alpha1.ForkThreadRequest - 6, // 17: tim.api.thread.v1alpha1.ThreadService.UpdateThread:input_type -> tim.api.thread.v1alpha1.UpdateThreadRequest - 7, // 18: tim.api.thread.v1alpha1.ThreadService.GetLlmMessage:input_type -> tim.api.thread.v1alpha1.GetLlmMessageRequest - 8, // 19: tim.api.thread.v1alpha1.ThreadService.ListLlmMessages:input_type -> tim.api.thread.v1alpha1.ListLlmMessagesRequest - 10, // 20: tim.api.thread.v1alpha1.ThreadService.StreamThreadEvents:input_type -> tim.api.thread.v1alpha1.StreamThreadEventsRequest - 12, // 21: tim.api.thread.v1alpha1.ThreadService.SubmitUserMessage:input_type -> tim.api.thread.v1alpha1.SubmitUserMessageRequest - 14, // 22: tim.api.thread.v1alpha1.ThreadService.GetThread:output_type -> tim.api.thread.v1alpha1.Thread - 2, // 23: tim.api.thread.v1alpha1.ThreadService.ListThreads:output_type -> tim.api.thread.v1alpha1.ListThreadsResponse - 14, // 24: tim.api.thread.v1alpha1.ThreadService.CreateThread:output_type -> tim.api.thread.v1alpha1.Thread - 5, // 25: tim.api.thread.v1alpha1.ThreadService.ForkThread:output_type -> tim.api.thread.v1alpha1.ForkThreadResponse - 14, // 26: tim.api.thread.v1alpha1.ThreadService.UpdateThread:output_type -> tim.api.thread.v1alpha1.Thread - 16, // 27: tim.api.thread.v1alpha1.ThreadService.GetLlmMessage:output_type -> tim.api.thread.v1alpha1.LlmMessage - 9, // 28: tim.api.thread.v1alpha1.ThreadService.ListLlmMessages:output_type -> tim.api.thread.v1alpha1.ListLlmMessagesResponse - 11, // 29: tim.api.thread.v1alpha1.ThreadService.StreamThreadEvents:output_type -> tim.api.thread.v1alpha1.StreamThreadEventsResponse - 16, // 30: tim.api.thread.v1alpha1.ThreadService.SubmitUserMessage:output_type -> tim.api.thread.v1alpha1.LlmMessage - 22, // [22:31] is the sub-list for method output_type - 13, // [13:22] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 20, // 9: tim.api.thread.v1alpha1.StreamThreadEventsResponse.tool_call_start:type_name -> tim.api.thread.v1alpha1.ToolCallStartEvent + 21, // 10: tim.api.thread.v1alpha1.StreamThreadEventsResponse.tool_call:type_name -> tim.api.tool.v1alpha1.ToolCall + 22, // 11: tim.api.thread.v1alpha1.StreamThreadEventsResponse.tool_call_complete:type_name -> tim.api.thread.v1alpha1.ToolCallCompleteEvent + 23, // 12: tim.api.thread.v1alpha1.StreamThreadEventsResponse.thread_state_change:type_name -> tim.api.thread.v1alpha1.ThreadStateChangeEvent + 24, // 13: tim.api.thread.v1alpha1.StreamThreadEventsResponse.stream_error:type_name -> tim.api.thread.v1alpha1.StreamErrorEvent + 13, // 14: tim.api.thread.v1alpha1.SubmitUserMessageRequest.user_message:type_name -> tim.api.thread.v1alpha1.UserMessage + 0, // 15: tim.api.thread.v1alpha1.ThreadService.GetThread:input_type -> tim.api.thread.v1alpha1.GetThreadRequest + 1, // 16: tim.api.thread.v1alpha1.ThreadService.ListThreads:input_type -> tim.api.thread.v1alpha1.ListThreadsRequest + 3, // 17: tim.api.thread.v1alpha1.ThreadService.CreateThread:input_type -> tim.api.thread.v1alpha1.CreateThreadRequest + 4, // 18: tim.api.thread.v1alpha1.ThreadService.ForkThread:input_type -> tim.api.thread.v1alpha1.ForkThreadRequest + 6, // 19: tim.api.thread.v1alpha1.ThreadService.UpdateThread:input_type -> tim.api.thread.v1alpha1.UpdateThreadRequest + 7, // 20: tim.api.thread.v1alpha1.ThreadService.GetLlmMessage:input_type -> tim.api.thread.v1alpha1.GetLlmMessageRequest + 8, // 21: tim.api.thread.v1alpha1.ThreadService.ListLlmMessages:input_type -> tim.api.thread.v1alpha1.ListLlmMessagesRequest + 10, // 22: tim.api.thread.v1alpha1.ThreadService.StreamThreadEvents:input_type -> tim.api.thread.v1alpha1.StreamThreadEventsRequest + 12, // 23: tim.api.thread.v1alpha1.ThreadService.SubmitUserMessage:input_type -> tim.api.thread.v1alpha1.SubmitUserMessageRequest + 14, // 24: tim.api.thread.v1alpha1.ThreadService.GetThread:output_type -> tim.api.thread.v1alpha1.Thread + 2, // 25: tim.api.thread.v1alpha1.ThreadService.ListThreads:output_type -> tim.api.thread.v1alpha1.ListThreadsResponse + 14, // 26: tim.api.thread.v1alpha1.ThreadService.CreateThread:output_type -> tim.api.thread.v1alpha1.Thread + 5, // 27: tim.api.thread.v1alpha1.ThreadService.ForkThread:output_type -> tim.api.thread.v1alpha1.ForkThreadResponse + 14, // 28: tim.api.thread.v1alpha1.ThreadService.UpdateThread:output_type -> tim.api.thread.v1alpha1.Thread + 16, // 29: tim.api.thread.v1alpha1.ThreadService.GetLlmMessage:output_type -> tim.api.thread.v1alpha1.LlmMessage + 9, // 30: tim.api.thread.v1alpha1.ThreadService.ListLlmMessages:output_type -> tim.api.thread.v1alpha1.ListLlmMessagesResponse + 11, // 31: tim.api.thread.v1alpha1.ThreadService.StreamThreadEvents:output_type -> tim.api.thread.v1alpha1.StreamThreadEventsResponse + 16, // 32: tim.api.thread.v1alpha1.ThreadService.SubmitUserMessage:output_type -> tim.api.thread.v1alpha1.LlmMessage + 24, // [24:33] is the sub-list for method output_type + 15, // [15:24] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_tim_api_thread_v1alpha1_thread_service_proto_init() } @@ -1064,7 +1104,9 @@ func file_tim_api_thread_v1alpha1_thread_service_proto_init() { (*StreamThreadEventsResponse_ContentStart)(nil), (*StreamThreadEventsResponse_ContentDelta)(nil), (*StreamThreadEventsResponse_ContentStop)(nil), + (*StreamThreadEventsResponse_ToolCallStart)(nil), (*StreamThreadEventsResponse_ToolCall)(nil), + (*StreamThreadEventsResponse_ToolCallComplete)(nil), (*StreamThreadEventsResponse_ThreadStateChange)(nil), (*StreamThreadEventsResponse_StreamError)(nil), } diff --git a/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.swagger.json b/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.swagger.json index 0fecfe115..53af95dd2 100644 --- a/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.swagger.json +++ b/tim-proto/gen/tim/api/thread/v1alpha1/thread_service.swagger.json @@ -734,9 +734,17 @@ "$ref": "#/definitions/v1alpha1ContentStopEvent", "title": "A content block has completed" }, + "toolCallStart": { + "$ref": "#/definitions/v1alpha1ToolCallStartEvent", + "title": "A tool call is about to be executed (tool name known, input may be incomplete)" + }, "toolCall": { "$ref": "#/definitions/apitoolv1alpha1ToolCall", - "title": "A tool call made by the LLM (input may be null if \u003e512KB)" + "title": "A tool call made by the LLM with full input (input may be null if \u003e512KB)" + }, + "toolCallComplete": { + "$ref": "#/definitions/v1alpha1ToolCallCompleteEvent", + "title": "A tool call has completed execution with results" }, "threadStateChange": { "$ref": "#/definitions/v1alpha1ThreadStateChangeEvent", @@ -856,6 +864,38 @@ }, "description": "TokenUsage is the usage of tokens for an llm call." }, + "v1alpha1ToolCallCompleteEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Tool call ID (UUID)" + }, + "success": { + "type": "boolean", + "title": "Whether the tool call was successful" + }, + "summary": { + "type": "string", + "title": "Brief summary of what the tool did" + } + }, + "title": "ToolCallCompleteEvent signals that a tool call has completed execution" + }, + "v1alpha1ToolCallStartEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Tool call ID (UUID)" + }, + "toolName": { + "type": "string", + "title": "Name of the tool being called" + } + }, + "title": "ToolCallStartEvent signals that a tool call is about to be executed" + }, "v1alpha1ToolResult": { "type": "object", "properties": { @@ -874,6 +914,10 @@ "stopIteration": { "type": "boolean", "title": "Whether this tool result should stop the LLM iteration loop\nSet to true by tools like query_complete, code_complete to signal conversation completion" + }, + "summary": { + "type": "string", + "title": "A brief summary of what the tool did (e.g., \"Updated todo to in_progress\", \"Read 234 lines\")\nUsed for client notifications without exposing full result" } }, "title": "The result of a tool call" diff --git a/tim-proto/gen/tim/api/thread/v1alpha1/thread_types.pb.go b/tim-proto/gen/tim/api/thread/v1alpha1/thread_types.pb.go index c8106cdaf..20c20dd6c 100644 --- a/tim-proto/gen/tim/api/thread/v1alpha1/thread_types.pb.go +++ b/tim-proto/gen/tim/api/thread/v1alpha1/thread_types.pb.go @@ -795,6 +795,125 @@ func (x *ContentStopEvent) GetId() string { return "" } +// ToolCallStartEvent signals that a tool call is about to be executed +type ToolCallStartEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tool call ID (UUID) + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Name of the tool being called + ToolName string `protobuf:"bytes,2,opt,name=tool_name,json=toolName,proto3" json:"tool_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolCallStartEvent) Reset() { + *x = ToolCallStartEvent{} + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolCallStartEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolCallStartEvent) ProtoMessage() {} + +func (x *ToolCallStartEvent) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ToolCallStartEvent.ProtoReflect.Descriptor instead. +func (*ToolCallStartEvent) Descriptor() ([]byte, []int) { + return file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP(), []int{7} +} + +func (x *ToolCallStartEvent) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ToolCallStartEvent) GetToolName() string { + if x != nil { + return x.ToolName + } + return "" +} + +// ToolCallCompleteEvent signals that a tool call has completed execution +type ToolCallCompleteEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tool call ID (UUID) + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Whether the tool call was successful + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + // Brief summary of what the tool did + Summary string `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolCallCompleteEvent) Reset() { + *x = ToolCallCompleteEvent{} + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolCallCompleteEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolCallCompleteEvent) ProtoMessage() {} + +func (x *ToolCallCompleteEvent) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ToolCallCompleteEvent.ProtoReflect.Descriptor instead. +func (*ToolCallCompleteEvent) Descriptor() ([]byte, []int) { + return file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP(), []int{8} +} + +func (x *ToolCallCompleteEvent) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ToolCallCompleteEvent) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *ToolCallCompleteEvent) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + // ThreadStateChangeEvent notifies when thread LLM state changes type ThreadStateChangeEvent struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -806,7 +925,7 @@ type ThreadStateChangeEvent struct { func (x *ThreadStateChangeEvent) Reset() { *x = ThreadStateChangeEvent{} - mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[7] + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -818,7 +937,7 @@ func (x *ThreadStateChangeEvent) String() string { func (*ThreadStateChangeEvent) ProtoMessage() {} func (x *ThreadStateChangeEvent) ProtoReflect() protoreflect.Message { - mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[7] + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -831,7 +950,7 @@ func (x *ThreadStateChangeEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ThreadStateChangeEvent.ProtoReflect.Descriptor instead. func (*ThreadStateChangeEvent) Descriptor() ([]byte, []int) { - return file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP(), []int{7} + return file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP(), []int{9} } func (x *ThreadStateChangeEvent) GetLlmState() ThreadLLMState { @@ -852,7 +971,7 @@ type StreamErrorEvent struct { func (x *StreamErrorEvent) Reset() { *x = StreamErrorEvent{} - mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[8] + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -864,7 +983,7 @@ func (x *StreamErrorEvent) String() string { func (*StreamErrorEvent) ProtoMessage() {} func (x *StreamErrorEvent) ProtoReflect() protoreflect.Message { - mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[8] + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -877,7 +996,7 @@ func (x *StreamErrorEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamErrorEvent.ProtoReflect.Descriptor instead. func (*StreamErrorEvent) Descriptor() ([]byte, []int) { - return file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP(), []int{8} + return file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP(), []int{10} } func (x *StreamErrorEvent) GetError() string { @@ -902,7 +1021,7 @@ type Thread_ParentThreadId struct { func (x *Thread_ParentThreadId) Reset() { *x = Thread_ParentThreadId{} - mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[9] + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -914,7 +1033,7 @@ func (x *Thread_ParentThreadId) String() string { func (*Thread_ParentThreadId) ProtoMessage() {} func (x *Thread_ParentThreadId) ProtoReflect() protoreflect.Message { - mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[9] + mi := &file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1007,7 +1126,14 @@ const file_tim_api_thread_v1alpha1_thread_types_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\tB\b\xe2\x8c\xcf\xd7\b\x02\b\x01R\x02id\x12\x14\n" + "\x05delta\x18\x02 \x01(\tR\x05delta\",\n" + "\x10ContentStopEvent\x12\x18\n" + - "\x02id\x18\x01 \x01(\tB\b\xe2\x8c\xcf\xd7\b\x02\b\x01R\x02id\"c\n" + + "\x02id\x18\x01 \x01(\tB\b\xe2\x8c\xcf\xd7\b\x02\b\x01R\x02id\"K\n" + + "\x12ToolCallStartEvent\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xe2\x8c\xcf\xd7\b\x02\b\x01R\x02id\x12\x1b\n" + + "\ttool_name\x18\x02 \x01(\tR\btoolName\"e\n" + + "\x15ToolCallCompleteEvent\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xe2\x8c\xcf\xd7\b\x02\b\x01R\x02id\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\x12\x18\n" + + "\asummary\x18\x03 \x01(\tR\asummary\"c\n" + "\x16ThreadStateChangeEvent\x12I\n" + "\tllm_state\x18\x01 \x01(\x0e2'.tim.api.thread.v1alpha1.ThreadLLMStateB\x03\xe0A\x03R\bllmState\"(\n" + "\x10StreamErrorEvent\x12\x14\n" + @@ -1039,7 +1165,7 @@ func file_tim_api_thread_v1alpha1_thread_types_proto_rawDescGZIP() []byte { } var file_tim_api_thread_v1alpha1_thread_types_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_tim_api_thread_v1alpha1_thread_types_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_tim_api_thread_v1alpha1_thread_types_proto_goTypes = []any{ (LlmMessageRole)(0), // 0: tim.api.thread.v1alpha1.LlmMessageRole (ThreadLLMState)(0), // 1: tim.api.thread.v1alpha1.ThreadLLMState @@ -1051,31 +1177,33 @@ var file_tim_api_thread_v1alpha1_thread_types_proto_goTypes = []any{ (*ContentStartEvent)(nil), // 7: tim.api.thread.v1alpha1.ContentStartEvent (*ContentDeltaEvent)(nil), // 8: tim.api.thread.v1alpha1.ContentDeltaEvent (*ContentStopEvent)(nil), // 9: tim.api.thread.v1alpha1.ContentStopEvent - (*ThreadStateChangeEvent)(nil), // 10: tim.api.thread.v1alpha1.ThreadStateChangeEvent - (*StreamErrorEvent)(nil), // 11: tim.api.thread.v1alpha1.StreamErrorEvent - (*Thread_ParentThreadId)(nil), // 12: tim.api.thread.v1alpha1.Thread.ParentThreadId - (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp - (*v1alpha1.TokenUsage)(nil), // 14: tim.api.llm_response.v1alpha1.TokenUsage - (*v1alpha11.ToolCall)(nil), // 15: tim.api.tool.v1alpha1.ToolCall - (*v1alpha11.ToolResult)(nil), // 16: tim.api.tool.v1alpha1.ToolResult + (*ToolCallStartEvent)(nil), // 10: tim.api.thread.v1alpha1.ToolCallStartEvent + (*ToolCallCompleteEvent)(nil), // 11: tim.api.thread.v1alpha1.ToolCallCompleteEvent + (*ThreadStateChangeEvent)(nil), // 12: tim.api.thread.v1alpha1.ThreadStateChangeEvent + (*StreamErrorEvent)(nil), // 13: tim.api.thread.v1alpha1.StreamErrorEvent + (*Thread_ParentThreadId)(nil), // 14: tim.api.thread.v1alpha1.Thread.ParentThreadId + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*v1alpha1.TokenUsage)(nil), // 16: tim.api.llm_response.v1alpha1.TokenUsage + (*v1alpha11.ToolCall)(nil), // 17: tim.api.tool.v1alpha1.ToolCall + (*v1alpha11.ToolResult)(nil), // 18: tim.api.tool.v1alpha1.ToolResult } var file_tim_api_thread_v1alpha1_thread_types_proto_depIdxs = []int32{ - 12, // 0: tim.api.thread.v1alpha1.Thread.parent_thread_id:type_name -> tim.api.thread.v1alpha1.Thread.ParentThreadId - 13, // 1: tim.api.thread.v1alpha1.Thread.create_time:type_name -> google.protobuf.Timestamp - 13, // 2: tim.api.thread.v1alpha1.Thread.update_time:type_name -> google.protobuf.Timestamp + 14, // 0: tim.api.thread.v1alpha1.Thread.parent_thread_id:type_name -> tim.api.thread.v1alpha1.Thread.ParentThreadId + 15, // 1: tim.api.thread.v1alpha1.Thread.create_time:type_name -> google.protobuf.Timestamp + 15, // 2: tim.api.thread.v1alpha1.Thread.update_time:type_name -> google.protobuf.Timestamp 1, // 3: tim.api.thread.v1alpha1.Thread.llm_state:type_name -> tim.api.thread.v1alpha1.ThreadLLMState 0, // 4: tim.api.thread.v1alpha1.LlmMessage.role:type_name -> tim.api.thread.v1alpha1.LlmMessageRole 6, // 5: tim.api.thread.v1alpha1.LlmMessage.contents:type_name -> tim.api.thread.v1alpha1.LlmMessageContent - 13, // 6: tim.api.thread.v1alpha1.LlmMessage.create_time:type_name -> google.protobuf.Timestamp - 14, // 7: tim.api.thread.v1alpha1.LlmMessage.token_usage:type_name -> tim.api.llm_response.v1alpha1.TokenUsage + 15, // 6: tim.api.thread.v1alpha1.LlmMessage.create_time:type_name -> google.protobuf.Timestamp + 16, // 7: tim.api.thread.v1alpha1.LlmMessage.token_usage:type_name -> tim.api.llm_response.v1alpha1.TokenUsage 5, // 8: tim.api.thread.v1alpha1.LlmMessageContent.thinking:type_name -> tim.api.thread.v1alpha1.Thinking - 15, // 9: tim.api.thread.v1alpha1.LlmMessageContent.tool_call:type_name -> tim.api.tool.v1alpha1.ToolCall - 16, // 10: tim.api.thread.v1alpha1.LlmMessageContent.tool_result:type_name -> tim.api.tool.v1alpha1.ToolResult - 13, // 11: tim.api.thread.v1alpha1.LlmMessageContent.create_time:type_name -> google.protobuf.Timestamp + 17, // 9: tim.api.thread.v1alpha1.LlmMessageContent.tool_call:type_name -> tim.api.tool.v1alpha1.ToolCall + 18, // 10: tim.api.thread.v1alpha1.LlmMessageContent.tool_result:type_name -> tim.api.tool.v1alpha1.ToolResult + 15, // 11: tim.api.thread.v1alpha1.LlmMessageContent.create_time:type_name -> google.protobuf.Timestamp 0, // 12: tim.api.thread.v1alpha1.ContentStartEvent.role:type_name -> tim.api.thread.v1alpha1.LlmMessageRole 2, // 13: tim.api.thread.v1alpha1.ContentStartEvent.type:type_name -> tim.api.thread.v1alpha1.ContentType 1, // 14: tim.api.thread.v1alpha1.ThreadStateChangeEvent.llm_state:type_name -> tim.api.thread.v1alpha1.ThreadLLMState - 13, // 15: tim.api.thread.v1alpha1.Thread.ParentThreadId.before_time:type_name -> google.protobuf.Timestamp + 15, // 15: tim.api.thread.v1alpha1.Thread.ParentThreadId.before_time:type_name -> google.protobuf.Timestamp 16, // [16:16] is the sub-list for method output_type 16, // [16:16] is the sub-list for method input_type 16, // [16:16] is the sub-list for extension type_name @@ -1100,7 +1228,7 @@ func file_tim_api_thread_v1alpha1_thread_types_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_tim_api_thread_v1alpha1_thread_types_proto_rawDesc), len(file_tim_api_thread_v1alpha1_thread_types_proto_rawDesc)), NumEnums: 3, - NumMessages: 10, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/tim-proto/gen/tim/api/thread_context/v1alpha1/thread_context_service.swagger.json b/tim-proto/gen/tim/api/thread_context/v1alpha1/thread_context_service.swagger.json index 5ac0932ad..ccd076964 100644 --- a/tim-proto/gen/tim/api/thread_context/v1alpha1/thread_context_service.swagger.json +++ b/tim-proto/gen/tim/api/thread_context/v1alpha1/thread_context_service.swagger.json @@ -386,6 +386,10 @@ "stopIteration": { "type": "boolean", "title": "Whether this tool result should stop the LLM iteration loop\nSet to true by tools like query_complete, code_complete to signal conversation completion" + }, + "summary": { + "type": "string", + "title": "A brief summary of what the tool did (e.g., \"Updated todo to in_progress\", \"Read 234 lines\")\nUsed for client notifications without exposing full result" } }, "title": "The result of a tool call" diff --git a/tim-proto/gen/tim/api/tool/v1alpha1/tool_types.pb.go b/tim-proto/gen/tim/api/tool/v1alpha1/tool_types.pb.go index 4dac83e80..c532adefe 100644 --- a/tim-proto/gen/tim/api/tool/v1alpha1/tool_types.pb.go +++ b/tim-proto/gen/tim/api/tool/v1alpha1/tool_types.pb.go @@ -110,6 +110,9 @@ type ToolResult struct { // Whether this tool result should stop the LLM iteration loop // Set to true by tools like query_complete, code_complete to signal conversation completion StopIteration bool `protobuf:"varint,4,opt,name=stop_iteration,json=stopIteration,proto3" json:"stop_iteration,omitempty"` + // A brief summary of what the tool did (e.g., "Updated todo to in_progress", "Read 234 lines") + // Used for client notifications without exposing full result + Summary string `protobuf:"bytes,5,opt,name=summary,proto3" json:"summary,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -172,6 +175,13 @@ func (x *ToolResult) GetStopIteration() bool { return false } +func (x *ToolResult) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + var File_tim_api_tool_v1alpha1_tool_types_proto protoreflect.FileDescriptor const file_tim_api_tool_v1alpha1_tool_types_proto_rawDesc = "" + @@ -181,14 +191,15 @@ const file_tim_api_tool_v1alpha1_tool_types_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x02id\x12\x1a\n" + "\x04name\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x04name\x125\n" + "\x05input\x18\x03 \x01(\v2\x17.google.protobuf.StructB\x06\xbaH\x03\xc8\x01\x01R\x05input\x121\n" + - "\x15provider_tool_call_id\x18\x04 \x01(\tR\x12providerToolCallId\"\x97\x01\n" + + "\x15provider_tool_call_id\x18\x04 \x01(\tR\x12providerToolCallId\"\xb1\x01\n" + "\n" + "ToolResult\x12(\n" + "\ftool_call_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\n" + "toolCallId\x12\x18\n" + "\asuccess\x18\x02 \x01(\bR\asuccess\x12\x1e\n" + "\x06result\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x06result\x12%\n" + - "\x0estop_iteration\x18\x04 \x01(\bR\rstopIterationB\xf0\x01\n" + + "\x0estop_iteration\x18\x04 \x01(\bR\rstopIteration\x12\x18\n" + + "\asummary\x18\x05 \x01(\tR\asummaryB\xf0\x01\n" + "\x19com.tim.api.tool.v1alpha1B\x0eToolTypesProtoP\x01ZLgithub.com/Greybox-Labs/tim/tim-proto/gen/tim/api/tool/v1alpha1;toolv1alpha1\xa2\x02\x03TAT\xaa\x02\x15Tim.Api.Tool.V1alpha1\xca\x02\x15Tim\\Api\\Tool\\V1alpha1\xe2\x02!Tim\\Api\\Tool\\V1alpha1\\GPBMetadata\xea\x02\x18Tim::Api::Tool::V1alpha1b\x06proto3" var ( diff --git a/tim-proto/gen/tim/api/tool_execution/v1alpha1/tool_execution_service.swagger.json b/tim-proto/gen/tim/api/tool_execution/v1alpha1/tool_execution_service.swagger.json index 85fbe5050..56e256466 100644 --- a/tim-proto/gen/tim/api/tool_execution/v1alpha1/tool_execution_service.swagger.json +++ b/tim-proto/gen/tim/api/tool_execution/v1alpha1/tool_execution_service.swagger.json @@ -211,6 +211,10 @@ "stopIteration": { "type": "boolean", "title": "Whether this tool result should stop the LLM iteration loop\nSet to true by tools like query_complete, code_complete to signal conversation completion" + }, + "summary": { + "type": "string", + "title": "A brief summary of what the tool did (e.g., \"Updated todo to in_progress\", \"Read 234 lines\")\nUsed for client notifications without exposing full result" } }, "title": "The result of a tool call"