diff --git a/holmes/core/tool_calling_llm.py b/holmes/core/tool_calling_llm.py index 571669723..fa4474455 100644 --- a/holmes/core/tool_calling_llm.py +++ b/holmes/core/tool_calling_llm.py @@ -383,15 +383,29 @@ def call( # type: ignore perf_timing.measure("pre-tool-calls") with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: futures = [] - for tool_index, t in enumerate(tools_to_call, 1): + non_todo_write_count = 0 + for t in tools_to_call: logging.debug(f"Tool to call: {t}") + # Check if this is a TodoWrite tool + tool_name = ( + t.function.name if hasattr(t, "function") else t.custom.name + ) + is_todo_write = tool_name == "TodoWrite" + + # Only assign a tool number to non-TodoWrite tools + if not is_todo_write: + non_todo_write_count += 1 + tool_num = tool_number_offset + non_todo_write_count + else: + tool_num = None + futures.append( executor.submit( self._invoke_tool, tool_to_call=t, previous_tool_calls=tool_calls, trace_span=trace_span, - tool_number=tool_number_offset + tool_index, + tool_number=tool_num, ) ) @@ -403,8 +417,8 @@ def call( # type: ignore perf_timing.measure(f"tool completed {tool_call_result.tool_name}") - # Update the tool number offset for the next iteration - tool_number_offset += len(tools_to_call) + # Update the tool number offset only for non-TodoWrite tools + tool_number_offset += non_todo_write_count # Add a blank line after all tools in this batch complete if tools_to_call: @@ -692,19 +706,33 @@ def call_stream( perf_timing.measure("pre-tool-calls") with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: futures = [] - for tool_index, t in enumerate(tools_to_call, 1): # type: ignore + non_todo_write_count = 0 + for t in tools_to_call: # type: ignore + # Check if this is a TodoWrite tool + tool_name = ( + t.function.name if hasattr(t, "function") else t.custom.name + ) + is_todo_write = tool_name == "TodoWrite" + + # Only assign a tool number to non-TodoWrite tools + if not is_todo_write: + non_todo_write_count += 1 + tool_num = tool_number_offset + non_todo_write_count + else: + tool_num = None + futures.append( executor.submit( self._invoke_tool, tool_to_call=t, # type: ignore previous_tool_calls=tool_calls, trace_span=DummySpan(), # Streaming mode doesn't support tracing yet - tool_number=tool_number_offset + tool_index, + tool_number=tool_num, ) ) yield StreamMessage( event=StreamEvents.START_TOOL, - data={"tool_name": t.function.name, "id": t.id}, + data={"tool_name": tool_name, "id": t.id}, ) for future in concurrent.futures.as_completed(futures): @@ -720,8 +748,8 @@ def call_stream( data=tool_call_result.as_streaming_tool_result_response(), ) - # Update the tool number offset for the next iteration - tool_number_offset += len(tools_to_call) + # Update the tool number offset only for non-TodoWrite tools + tool_number_offset += non_todo_write_count raise Exception( f"Too many LLM calls - exceeded max_steps: {i}/{self.max_steps}" diff --git a/holmes/interactive.py b/holmes/interactive.py index 4a98f7f01..0dd661cbb 100644 --- a/holmes/interactive.py +++ b/holmes/interactive.py @@ -745,10 +745,23 @@ def handle_last_command( ) return + # Filter out TodoWrite tools from display + non_todo_write_tools = [ + tool_call + for tool_call in last_response.tool_calls + if tool_call.tool_name != "TodoWrite" + ] + + if not non_todo_write_tools: + console.print( + f"[bold {ERROR_COLOR}]No displayable tool calls from the last response (TodoWrite calls are hidden).[/bold {ERROR_COLOR}]" + ) + return + console.print( - f"[bold {TOOLS_COLOR}]Used {len(last_response.tool_calls)} tools[/bold {TOOLS_COLOR}]" + f"[bold {TOOLS_COLOR}]Used {len(non_todo_write_tools)} tools[/bold {TOOLS_COLOR}]" ) - for tool_call in last_response.tool_calls: + for tool_call in non_todo_write_tools: tool_index = find_tool_index_in_history(tool_call, all_tool_calls_history) preview_output = format_tool_call_output(tool_call, tool_index) title = f"{tool_call.result.status.to_emoji()} {tool_call.description} -> returned {tool_call.result.return_code}" @@ -769,22 +782,28 @@ def display_recent_tool_outputs( all_tool_calls_history: List[ToolCallResult], ) -> None: """Display recent tool outputs in rich panels (for auto-display after responses).""" - console.print( - f"[bold {TOOLS_COLOR}]Used {len(tool_calls)} tools[/bold {TOOLS_COLOR}]" - ) - for tool_call in tool_calls: - tool_index = find_tool_index_in_history(tool_call, all_tool_calls_history) - preview_output = format_tool_call_output(tool_call, tool_index) - title = f"{tool_call.result.status.to_emoji()} {tool_call.description} -> returned {tool_call.result.return_code}" + # Filter out TodoWrite tools from display + non_todo_write_tools = [ + tool_call for tool_call in tool_calls if tool_call.tool_name != "TodoWrite" + ] + if non_todo_write_tools: console.print( - Panel( - preview_output, - padding=(1, 2), - border_style=TOOLS_COLOR, - title=title, - ) + f"[bold {TOOLS_COLOR}]Used {len(non_todo_write_tools)} tools[/bold {TOOLS_COLOR}]" ) + for tool_call in non_todo_write_tools: + tool_index = find_tool_index_in_history(tool_call, all_tool_calls_history) + preview_output = format_tool_call_output(tool_call, tool_index) + title = f"{tool_call.result.status.to_emoji()} {tool_call.description} -> returned {tool_call.result.return_code}" + + console.print( + Panel( + preview_output, + padding=(1, 2), + border_style=TOOLS_COLOR, + title=title, + ) + ) def run_interactive_loop( @@ -1032,7 +1051,13 @@ def get_bottom_toolbar(): last_response = response if response.tool_calls: - all_tool_calls_history.extend(response.tool_calls) + # Filter out TodoWrite tools from the history used for /show command + non_todo_write_tools = [ + tool_call + for tool_call in response.tool_calls + if tool_call.tool_name != "TodoWrite" + ] + all_tool_calls_history.extend(non_todo_write_tools) # Update the show completer with the latest tool call history show_completer.update_history(all_tool_calls_history)