in pkg/agent/conversation.go [131:312]
func (a *Conversation) RunOneRound(ctx context.Context, query string) error {
log := klog.FromContext(ctx)
log.Info("Starting chat loop for query:", "query", query)
// currChatContent tracks chat content that needs to be sent
// to the LLM in each iteration of the agentic loop below
var currChatContent []any
// Set the initial message to start the conversation
currChatContent = []any{query}
currentIteration := 0
maxIterations := a.MaxIterations
for currentIteration < maxIterations {
log.Info("Starting iteration", "iteration", currentIteration)
a.Recorder.Write(ctx, &journal.Event{
Timestamp: time.Now(),
Action: "llm-chat",
Payload: []any{currChatContent},
})
stream, err := a.llmChat.SendStreaming(ctx, currChatContent...)
if err != nil {
return err
}
// Clear our "response" now that we sent the last response
currChatContent = nil
if a.EnableToolUseShim {
// convert the candidate response into a gollm.ChatResponse
stream, err = candidateToShimCandidate(stream)
if err != nil {
return err
}
}
// Process each part of the response
// only applicable is not using tooluse shim
var functionCalls []gollm.FunctionCall
var agentTextBlock *ui.AgentTextBlock
for response, err := range stream {
if err != nil {
return fmt.Errorf("reading streaming LLM response: %w", err)
}
if response == nil {
// end of streaming response
break
}
klog.Infof("response: %+v", response)
a.Recorder.Write(ctx, &journal.Event{
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)
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)
}
s := toolCall.PrettyPrint()
a.doc.AddBlock(ui.NewFunctionCallRequestBlock().SetText(fmt.Sprintf(" Running: %s\n", s)))
// Ask for confirmation only if SkipPermissions is false AND the tool modifies resources.
if !a.SkipPermissions && call.Arguments["modifies_resource"] != "no" {
confirmationPrompt := ` Do you want to proceed ?
1) Yes
2) Yes, and don't ask me again
3) No`
optionsBlock := ui.NewInputOptionBlock().SetPrompt(confirmationPrompt)
optionsBlock.SetOptions([]string{"1", "2", "3"})
a.doc.AddBlock(optionsBlock)
selectedChoice, err := optionsBlock.Observable().Wait()
if err != nil {
if err == io.EOF {
// Use hit control-D, or was piping and we reached the end of stdin.
// Not a "big" problem
return nil
}
return fmt.Errorf("reading input: %w", err)
}
switch selectedChoice {
case "1":
// Proceed with the operation
case "2":
a.SkipPermissions = true
case "3":
a.doc.AddBlock(ui.NewAgentTextBlock().SetText("Operation was skipped."))
observation := fmt.Sprintf("User didn't approve running %q.\n", call.Name)
currChatContent = append(currChatContent, observation)
continue
default:
// This case should technically not be reachable due to AskForConfirmation loop
err := fmt.Errorf("invalid confirmation choice: %q", selectedChoice)
log.Error(err, "Invalid choice received from AskForConfirmation")
a.doc.AddBlock(ui.NewErrorBlock().SetText("Invalid choice received. Cancelling operation."))
return err
}
}
ctx := journal.ContextWithRecorder(ctx, a.Recorder)
output, err := toolCall.InvokeTool(ctx, tools.InvokeToolOptions{
Kubeconfig: a.Kubeconfig,
WorkDir: a.workDir,
})
if err != nil {
return fmt.Errorf("executing action: %w", err)
}
if a.EnableToolUseShim {
observation := fmt.Sprintf("Result of running %q:\n%s", call.Name, output)
currChatContent = append(currChatContent, observation)
} else {
result, err := tools.ToolResultToMap(output)
if err != nil {
return err
}
currChatContent = append(currChatContent, gollm.FunctionCallResult{
ID: call.ID,
Name: call.Name,
Result: result,
})
}
}
// If no function calls were made, we're done
if len(functionCalls) == 0 {
log.Info("No function calls were made, so most likely the task is completed, so we're done.")
return nil
}
currentIteration++
}
// If we've reached the maximum number of iterations
log.Info("Max iterations reached", "iterations", maxIterations)
errorBlock := ui.NewErrorBlock().SetText(fmt.Sprintf("Sorry, couldn't complete the task after %d iterations.\n", maxIterations))
a.doc.AddBlock(errorBlock)
return fmt.Errorf("max iterations reached")
}