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

Code before:
				Timestamp: time.Now(),
				Action:    "llm-response",
				Payload:   response,
			})

			if len(response.Candidates()) == 0 {
				log.Error(nil, "No candidates in response")
				return fmt.Errorf("no candidates in LLM response")
			}

			candidate := response.Candidates()[0]

			for _, part := range candidate.Parts() {
				// Check if it's a text response
				if text, ok := part.AsText(); ok {
					log.Info("text response", "text", text)
					if agentTextBlock == nil {
						agentTextBlock = ui.NewAgentTextBlock()
						agentTextBlock.SetStreaming(true)
						a.doc.AddBlock(agentTextBlock)
					}
					agentTextBlock.AppendText(text)
				}

				// Only process function calls if not using tool-use shim
				if !a.EnableToolUseShim {
					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)
		}

		// Only process function calls if not using tool-use shim
		if !a.EnableToolUseShim {
			// TODO(droot): Run all function calls 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, errMsg := toolCall.GetTool().IsInteractive(call.Arguments)
				klog.Infof("isInteractive: %t, errMsg: %s", isInteractive, errMsg)


Review comment (suggestion):
So we don't need to condition this. If toolUseShim is enabled, we convert the textual LLM response to a ShimCandidate response that has function calls etc. (line 165 above). Think of it as: client side implementation of tool-use since server side (API) doesnt' support tool-use.

Code after:
				Timestamp: time.Now(),
				Action:    "llm-response",
				Payload:   response,
			})

			if len(response.Candidates()) == 0 {
				log.Error(nil, "No candidates in response")
				return fmt.Errorf("no candidates in LLM response")
			}

			candidate := response.Candidates()[0]

			for _, part := range candidate.Parts() {
				// Check if it's a text response
				if text, ok := part.AsText(); ok {
					log.Info("text response", "text", text)
					if agentTextBlock == nil {
						agentTextBlock = ui.NewAgentTextBlock()
						agentTextBlock.SetStreaming(true)
						a.doc.AddBlock(agentTextBlock)
					}
					agentTextBlock.AppendText(text)
				}

				// 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)