From 7a37056d1242ee46798bb84ff86e80613a162c2d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 15 Feb 2026 22:34:35 +0000 Subject: [PATCH] feat: improve control loop Signed-off-by: Ettore Di Giacinto --- options.go | 9 --- plan.go | 4 +- prompt/prompt.go | 47 ---------------- tools.go | 140 +++-------------------------------------------- 4 files changed, 8 insertions(+), 192 deletions(-) diff --git a/options.go b/options.go index e83e1d4..93bfc0c 100644 --- a/options.go +++ b/options.go @@ -18,7 +18,6 @@ type Options struct { tools Tools deepContext bool toolReasoner bool - toolReEvaluator bool autoPlan bool planReEvaluator bool statusCallback, reasoningCallback func(string) @@ -60,7 +59,6 @@ type Option func(*Options) func defaultOptions() *Options { return &Options{ - toolReEvaluator: true, maxIterations: 1, maxAttempts: 1, maxRetries: 5, @@ -94,13 +92,6 @@ var ( o.toolReasoner = true } - // DisableToolReEvaluator disables the re-evaluation of the need to call other tools - // after each tool call. It might yield to better results to the cost of more - // LLM calls. - DisableToolReEvaluator Option = func(o *Options) { - o.toolReEvaluator = false - } - // DisableSinkState disables the use of a sink state // when the LLM decides that no tool is needed DisableSinkState Option = func(o *Options) { diff --git a/plan.go b/plan.go index 6645463..19a3a81 100644 --- a/plan.go +++ b/plan.go @@ -662,9 +662,7 @@ func convertOptionsToFunctions(o *Options) []Option { if o.toolReasoner { opts = append(opts, EnableToolReasoner) } - if !o.toolReEvaluator { - opts = append(opts, DisableToolReEvaluator) - } + if o.deepContext { opts = append(opts, EnableDeepContext) } diff --git a/prompt/prompt.go b/prompt/prompt.go index 94660bb..7a11f02 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -16,7 +16,6 @@ const ( PromptGuidelinesType PromptType = iota PromptGuidelinesExtractionType PromptType = iota PromptPlanDecisionType PromptType = iota - PromptToolReEvaluationType PromptType = iota PromptParameterReasoningType PromptType = iota PromptTODOGenerationType PromptType = iota PromptTODOWorkType PromptType = iota @@ -39,7 +38,6 @@ var ( PromptGuidelinesType: PromptGuidelines, PromptGuidelinesExtractionType: PromptGuidelinesExtraction, PromptPlanDecisionType: DecideIfPlanningIsNeeded, - PromptToolReEvaluationType: PromptToolReEvaluation, PromptParameterReasoningType: PromptParameterReasoning, PromptTODOGenerationType: PromptTODOGeneration, PromptTODOWorkType: PromptTODOWork, @@ -280,51 +278,6 @@ Based on the conversation, context, and available tools, decide if planning and Keep in mind that Planning will later involve in breaking down the problem into a set of subtasks that require running tools in sequence and evaluating their results. If you think planning is needed, reply with yes, otherwise reply with no.`) - PromptToolReEvaluation = NewPrompt(`You are an AI assistant re-evaluating the conversation after tool executions. - -Your task is to: -1. Review all tool execution results -2. Assess if the goal has been achieved -3. Determine if additional actions are needed -4. Decide on the next best course of action - -Context: -{{.Context}} - -{{ if ne .AdditionalContext "" }} -Additional Context: -{{.AdditionalContext}} -{{ end }} - -Previous Tool Executions: -{{if .PreviousTools}} -{{ range $index, $tool := .PreviousTools }} -Tool {{add1 $index}}: {{$tool.Name}} -Arguments: {{$tool.ToolArguments | toJson}} -Result: {{$tool.Result}} -{{ end }} -{{else}} -No previous tool executions. -{{end}} - -{{ range $index, $guideline := .Guidelines }} -Guideline {{add1 $index }}: If {{$guideline.Condition}} then {{$guideline.Action}} ( Suggested Tools to use: {{$guideline.Tools | toJson}} ) -{{ end }} - -Available Tools: -{{ range $index, $tool := .Tools }} -- Tool name: "{{$tool.Name}}" - Tool description: {{$tool.Description}} - Tool parameters: {{$tool.Parameters | toJson}} -{{ end }} - -Based on all tool execution results and current context: -1. Has the goal been achieved, or do we need to take additional actions? -2. If more actions are needed, which tool(s) should we use next and why? -3. If the goal is achieved, we can conclude and provide a final response. - -Analyze the situation considering all previous tool executions and provide your reasoning about what to do next.`) - PromptParameterReasoning = NewPrompt(`You are tasked with generating the optimal parameters for the tool "{{.ToolName}}". The tool requires the following parameters: {{.Parameters}} diff --git a/tools.go b/tools.go index 24d710b..311574f 100644 --- a/tools.go +++ b/tools.go @@ -564,79 +564,6 @@ func ToolReasoner(llm LLM, f Fragment, opts ...Option) (Fragment, error) { return llm.Ask(o.context, fragment) } -// ToolReEvaluator evaluates the conversation after tool executions and determines next steps -// Calls pickAction/toolSelection with reEvaluationTemplate and the conversation that already has tool results -// It evaluates all previous tools, not just the latest one -func ToolReEvaluator(llm LLM, f Fragment, previousTools []ToolStatus, tools Tools, guidelines Guidelines, opts ...Option) ([]*ToolChoice, string, error) { - o := defaultOptions() - o.Apply(opts...) - - prompter := o.prompts.GetPrompt(prompt.PromptToolReEvaluationType) - - additionalContext := "" - if f.ParentFragment != nil && o.deepContext { - additionalContext = f.ParentFragment.AllFragmentsStrings() - } - - // Convert slice of ToolStatus to slice of pointers for the template - previousToolsPtrs := make([]*ToolStatus, len(previousTools)) - for i := range previousTools { - previousToolsPtrs[i] = &previousTools[i] - } - - reEvaluation := struct { - Context string - AdditionalContext string - PreviousTools []*ToolStatus - Tools []*openai.FunctionDefinition - Guidelines GuidelineMetadataList - }{ - Context: f.String(), - AdditionalContext: additionalContext, - PreviousTools: previousToolsPtrs, - Tools: tools.Definitions(), - Guidelines: guidelines.ToMetadata(), - } - - reEvalPrompt, err := prompter.Render(reEvaluation) - if err != nil { - return nil, "", fmt.Errorf("failed to render tool re-evaluation prompt: %w", err) - } - - xlog.Debug("Tool ReEvaluator called - reusing toolSelection", "previous_tools_count", len(previousTools)) - - // Prepare the re-evaluation prompt as tool prompts to inject into toolSelection - reEvalPrompts := []openai.ChatCompletionMessage{ - { - Role: "system", - Content: reEvalPrompt, - }, - } - - // Reuse toolSelection with the re-evaluation prompt - // The conversation (f) already has the tool execution results in it - reasoningFragment, selectedTools, noTool, reasoning, err := toolSelection(llm, f, tools, guidelines, reEvalPrompts, opts...) - if err != nil { - return nil, "", fmt.Errorf("failed to select following tool: %w", err) - } - - if noTool || len(selectedTools) == 0 { - // No tool selected - xlog.Debug("ToolReEvaluator: No more tools needed", "reasoning", reasoning) - return nil, reasoning, nil - } - - // Extract reasoning text from the fragment - if len(reasoningFragment.Messages) > 0 { - reasoning = reasoningFragment.LastMessage().Content - } - - for _, t := range selectedTools { - xlog.Debug("ToolReEvaluator selected tool", "tool", t.Name, "reasoning", t.Reasoning) - } - return selectedTools, reasoning, nil -} - func decideToPlan(llm LLM, f Fragment, tools Tools, opts ...Option) (bool, error) { o := defaultOptions() o.Apply(opts...) @@ -705,10 +632,8 @@ func doPlan(llm LLM, f Fragment, tools Tools, opts ...Option) (Fragment, bool, e if err != nil { return f, false, fmt.Errorf("failed to execute plan: %w", err) } - return f, true, nil } - return f, false, nil } @@ -896,11 +821,11 @@ func ExecuteTools(llm LLM, f Fragment, opts ...Option) (Fragment, error) { o.maxIterations = 1 } - // nextAction stores a tool that was suggested by the ToolReEvaluator - var nextAction []*ToolChoice + // startingActions stores tools for starting + var startingActions []*ToolChoice if len(o.startWithAction) > 0 { - nextAction = o.startWithAction + startingActions = o.startWithAction o.startWithAction = []*ToolChoice{} } @@ -940,14 +865,14 @@ TOOL_LOOP: var reasoning string // If ToolReEvaluator set a next action, use it directly - if len(nextAction) > 0 { - xlog.Debug("Using next action from ToolReEvaluator", "count", len(nextAction)) - for _, t := range nextAction { + if len(startingActions) > 0 { + xlog.Debug("Starting with actions", "count", len(startingActions)) + for _, t := range startingActions { selectedToolResults = append(selectedToolResults, t) // Generate ID before creating the message t.ID = uuid.New().String() } - nextAction = []*ToolChoice{} // Clear it so we don't reuse it + startingActions = []*ToolChoice{} // Clear it so we don't reuse it // Create a fragment with the tool selection selectedToolFragment = NewEmptyFragment() @@ -1305,57 +1230,6 @@ Please provide revised tool call based on this feedback.`, } return f, nil } - - if o.maxIterations > 1 || o.toolReEvaluator { - // Collect all tool statuses from this iteration for re-evaluation - var previousTools []ToolStatus - if len(executionResults) > 0 { - // Use tools from current execution results - for _, execResult := range executionResults { - previousTools = append(previousTools, execResult.status) - } - } else if len(f.Status.ToolResults) > 0 { - // Fallback to all tool results from fragment status - previousTools = f.Status.ToolResults - } - - // Call ToolReEvaluator to determine if another tool should be called - // It evaluates all previous tools from this iteration, not just the latest one - // calls pickAction with re-evaluation template - // which uses the decision API to properly select the next tool - nextToolChoice, reasoning, err := ToolReEvaluator(llm, f, previousTools, tools, guidelines, opts...) - if err != nil { - return f, fmt.Errorf("failed to evaluate next action: %w", err) - } - - if reasoning != "" { - o.statusCallback(reasoning) - } - - // If ToolReEvaluator selected a tool, store it for the next iteration - if len(nextToolChoice) > 0 { - for _, t := range nextToolChoice { - if tools.Find(t.Name) != nil { - nextAction = append(nextAction, t) - } - } - xlog.Debug("ToolReEvaluator selected next tool", "count", len(nextAction), - "totalIterations", totalIterations, "maxIterations", o.maxIterations) - // Continue to next iteration where nextAction will be used (until maxIterations is reached) - continue - } else { - // ToolReEvaluator didn't select a tool - // If guidelines are enabled, continue to next iteration for guidelines selection - // Otherwise, break (e.g., for ContentReview which has its own outer loop) - if len(o.guidelines) > 0 { - xlog.Debug("ToolReEvaluator: No more tools selected, continuing to next iteration (guidelines enabled)") - continue - } - xlog.Debug("ToolReEvaluator: No more tools selected, breaking") - f = f.AddMessage(AssistantMessageRole, reasoning) - break - } - } } if len(f.Status.ToolsCalled) == 0 {