Repository: GoogleCloudPlatform/kubectl-ai
File: pkg/agent/conversation.go (Go)

Code before:
				}

				// Check if it's a function call
				if calls, ok := part.AsFunctionCalls(); ok && len(calls) > 0 {
					log.Info("function calls", "calls", calls)
					functionCalls = append(functionCalls, calls...)
				}
			}
		}

		if agentTextBlock != nil {
			agentTextBlock.SetStreaming(false)
		}

		// TODO(droot): Run all function calls in parallel
		// (may have to specify in the prompt to make these function calls independent)
		for _, call := range functionCalls {
			toolCall, err := a.Tools.ParseToolInvocation(ctx, call.Name, call.Arguments)
			if err != nil {
				return fmt.Errorf("building tool call: %w", err)
			}

			// Check if this is a kubectl or bash command that might be interactive
			isInteractive := false
			var errMsg string
			if call.Name == "kubectl" || call.Name == "bash" {
				if command, ok := call.Arguments["command"].(string); ok {
					isInteractive, errMsg = tools.IsInteractiveCommand(command)
				}
			}

			// If interactive, show error and skip to next iteration
			if isInteractive {
				// Use ErrorBlock for interactive command errors
				errorBlock := ui.NewErrorBlock().SetText(fmt.Sprintf("  %s\n", errMsg))
				a.doc.AddBlock(errorBlock)

				currChatContent = append(currChatContent, gollm.FunctionCallResult{
					ID:     call.ID,
					Name:   call.Name,
					Result: map[string]any{"error": errMsg},
				})
				continue
			}

			// Only show "Running" message and proceed with execution for non-interactive commands
			s := toolCall.PrettyPrint()
			a.doc.AddBlock(ui.NewFunctionCallRequestBlock().SetText(fmt.Sprintf("  Running: %s\n", s)))

			var result any
			var invokeErr error

Review comment (suggestion):
I wonder if we can introduce a method in the `Tool` interface called `IsInteractive(args) bool` to delegate this logic to individual tools to keep the core agentic loop tool agnostic.

Code after:

				// Check if it's a function call
				if calls, ok := part.AsFunctionCalls(); ok && len(calls) > 0 {
					log.Info("function calls", "calls", calls)
					functionCalls = append(functionCalls, calls...)
				}
			}
		}

		if agentTextBlock != nil {
			agentTextBlock.SetStreaming(false)
		}

		// TODO(droot): Run all function calls in parallel
		// (may have to specify in the prompt to make these function calls independent)
		// NOTE: Currently, function calls are executed sequentially.
		// Suggestion: Use goroutines and sync.WaitGroup to parallelize execution if tool calls are independent.
		// Be careful with shared state and UI updates if running in parallel.

		for _, call := range functionCalls {
			toolCall, err := a.Tools.ParseToolInvocation(ctx, call.Name, call.Arguments)
			if err != nil {
				return fmt.Errorf("building tool call: %w", err)
			}

			// Check if the command is interactive using the tool's implementation
			isInteractive, err := toolCall.GetTool().IsInteractive(call.Arguments)
			klog.Infof("isInteractive: %t, err: %v, CallArguments: %+v", isInteractive, err, call.Arguments)

			// If interactive, handle based on whether we're using tool-use shim
			if isInteractive {
				// Show error block for both shim enabled and disabled modes
				errorBlock := ui.NewErrorBlock().SetText(fmt.Sprintf("  %s\n", err.Error()))
				a.doc.AddBlock(errorBlock)

				if a.EnableToolUseShim {
					// Add the error as an observation
					observation := fmt.Sprintf("Result of running %q:\n%s", call.Name, err.Error())
					currChatContent = append(currChatContent, observation)
				} else {
					// For models with tool-use support (shim disabled), use proper FunctionCallResult
					// Note: This assumes the model supports sending FunctionCallResult
					currChatContent = append(currChatContent, gollm.FunctionCallResult{
						ID:     call.ID,
						Name:   call.Name,
						Result: map[string]any{"error": err.Error()},
					})
				}
				continue // Skip execution for interactive commands
			}