Skip to content

Commit dd7bcc7

Browse files
committed
* tools: raise orchestration limit to 20 turns and force finalization on limit;
1 parent 33a873e commit dd7bcc7

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

internal/app/tools/run.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import (
1818
)
1919

2020
const (
21-
toolsMaxTurns = 10
21+
toolsMaxTurns = 20
2222
toolsMaxConsecutiveNoProgress = 2
2323
toolsMaxConsecutiveDuplicates = 2
2424
emptyFinalAnswerRetryLimit = 1
2525
stalledToolRetryLimit = 1
26+
maxTurnFinalizationRetries = 1
2627
)
2728

2829
type Options struct{}
@@ -214,6 +215,7 @@ func Run(ctx context.Context, client llm.ChatRequester, filePath, prompt string,
214215
var toolChoiceToSend interface{} = "auto"
215216
emptyFinalAnswerRetries := 0
216217
stalledToolRetries := 0
218+
haveToolResults := false
217219

218220
logStop := func(reason string, turns int) {
219221
slog.Debug("tools processing finished",
@@ -308,6 +310,7 @@ func Run(ctx context.Context, client llm.ChatRequester, filePath, prompt string,
308310
ToolCallID: choice.Message.ToolCalls[i].ID,
309311
})
310312
}
313+
haveToolResults = haveToolResults || len(results) > 0
311314
if err != nil {
312315
var orchErr orchestrationError
313316
if errorsAsOrchestration(err, &orchErr) && shouldRetryWithoutTools(orchErr) && stalledToolRetries < stalledToolRetryLimit {
@@ -357,6 +360,18 @@ func Run(ctx context.Context, client llm.ChatRequester, filePath, prompt string,
357360
return choice.Message.Content, nil
358361
}
359362

363+
if haveToolResults {
364+
finalMeter.Start(localize.T("progress.tools.text_answer", localize.Data{"Turn": toolsMaxTurns + 1}))
365+
result, err := requestFinalAnswerWithoutTools(ctx, client, messages, toolsConfig.tools, maxTurnFinalizationRetries)
366+
if err == nil {
367+
finalMeter.Done(localize.T("progress.tools.final_done", localize.Data{"Turn": toolsMaxTurns + 1}))
368+
logStop("forced_final_answer", toolsMaxTurns)
369+
return result, nil
370+
}
371+
logStop("orchestration_limit_finalization_failed", toolsMaxTurns)
372+
return "", err
373+
}
374+
360375
logStop("orchestration_limit", toolsMaxTurns)
361376
return "", orchestrationError{
362377
Code: "orchestration_limit",
@@ -735,3 +750,38 @@ func shouldRetryWithoutTools(err orchestrationError) bool {
735750
return false
736751
}
737752
}
753+
754+
func requestFinalAnswerWithoutTools(ctx context.Context, client llm.ChatRequester, messages []openai.ChatCompletionMessage, tools []openai.Tool, retries int) (string, error) {
755+
attemptMessages := append([]openai.ChatCompletionMessage{}, messages...)
756+
757+
for attempt := 0; attempt <= retries; attempt++ {
758+
attemptMessages = append(attemptMessages, openai.ChatCompletionMessage{
759+
Role: "user",
760+
Content: localize.T("tools.prompt.stop_calls"),
761+
})
762+
763+
resp, _, err := client.SendRequestWithMetrics(ctx, openai.ChatCompletionRequest{
764+
Messages: attemptMessages,
765+
Tools: tools,
766+
ToolChoice: "none",
767+
})
768+
if err != nil {
769+
return "", fmt.Errorf("failed to send forced finalization request: %w", err)
770+
}
771+
if len(resp.Choices) == 0 {
772+
return "", fmt.Errorf("forced finalization returned no choices")
773+
}
774+
775+
choice := resp.Choices[0]
776+
attemptMessages = append(attemptMessages, choice.Message)
777+
if strings.TrimSpace(choice.Message.Content) != "" {
778+
return choice.Message.Content, nil
779+
}
780+
}
781+
782+
return "", orchestrationError{
783+
Code: "orchestration_limit",
784+
Message: fmt.Sprintf("tools orchestration exceeded %d turns and forced finalization returned no answer", toolsMaxTurns),
785+
Details: map[string]any{"max_turns": toolsMaxTurns},
786+
}
787+
}

internal/app/tools/run_test.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,52 @@ func TestRunTools_MultiStepToolLoop(t *testing.T) {
199199
}
200200
}
201201

202-
func TestRunTools_StopsAfterMaxTurns(t *testing.T) {
202+
func TestRunTools_FinalizesWithoutToolsAfterMaxTurns(t *testing.T) {
203203
responses := make([]*openai.ChatCompletionResponse, 0, toolsMaxTurns)
204204
for i := 0; i < toolsMaxTurns; i++ {
205205
responses = append(responses, chatResponse(message("assistant", "", []openai.ToolCall{
206206
toolCall("loop-call", "search_file", fmt.Sprintf(`{"query":"x","limit":1,"offset":%d}`, i)),
207207
})))
208208
}
209+
responses = append(responses, chatResponse(message("assistant", "Итог после принудительной финализации", nil)))
209210

210211
client := &scriptedRequester{responses: responses}
211-
filePath := writeTempFile(t, "x1\nx2\nx3\nx4\nx5\nx6\nx7\nx8\nx9\nx10\n")
212+
filePath := writeTempFile(t, strings.Join(buildNumberedLines("x", toolsMaxTurns+5), "\n")+"\n")
213+
214+
result, err := Run(context.Background(), client, filePath, "loop?", nil)
215+
if err != nil {
216+
t.Fatalf("RunTools() error = %v", err)
217+
}
218+
if result != "Итог после принудительной финализации" {
219+
t.Fatalf("RunTools() result = %q, want forced finalization answer", result)
220+
}
221+
if len(client.requests) != toolsMaxTurns+1 {
222+
t.Fatalf("requests = %d, want %d", len(client.requests), toolsMaxTurns+1)
223+
}
224+
lastReq := client.requests[len(client.requests)-1]
225+
if lastReq.ToolChoice != "none" {
226+
t.Fatalf("ToolChoice = %#v, want none", lastReq.ToolChoice)
227+
}
228+
lastMsg := lastReq.Messages[len(lastReq.Messages)-1]
229+
if !strings.Contains(lastMsg.Content, "Останови вызовы инструментов") {
230+
t.Fatalf("finalization message = %q, want stop-tools instruction", lastMsg.Content)
231+
}
232+
}
233+
234+
func TestRunTools_ReturnsOrchestrationLimitWhenForcedFinalizationStillEmpty(t *testing.T) {
235+
responses := make([]*openai.ChatCompletionResponse, 0, toolsMaxTurns+2)
236+
for i := 0; i < toolsMaxTurns; i++ {
237+
responses = append(responses, chatResponse(message("assistant", "", []openai.ToolCall{
238+
toolCall("loop-call", "search_file", fmt.Sprintf(`{"query":"x","limit":1,"offset":%d}`, i)),
239+
})))
240+
}
241+
responses = append(responses,
242+
chatResponse(message("assistant", "", nil)),
243+
chatResponse(message("assistant", "", nil)),
244+
)
245+
246+
client := &scriptedRequester{responses: responses}
247+
filePath := writeTempFile(t, strings.Join(buildNumberedLines("x", toolsMaxTurns+5), "\n")+"\n")
212248

213249
_, err := Run(context.Background(), client, filePath, "loop?", nil)
214250
var orchErr orchestrationError
@@ -525,6 +561,14 @@ func toolCall(id string, name string, arguments string) openai.ToolCall {
525561
}
526562
}
527563

564+
func buildNumberedLines(prefix string, count int) []string {
565+
lines := make([]string, 0, count)
566+
for i := 1; i <= count; i++ {
567+
lines = append(lines, fmt.Sprintf("%s%d", prefix, i))
568+
}
569+
return lines
570+
}
571+
528572
func writeTempFile(t *testing.T, content string) string {
529573
t.Helper()
530574

0 commit comments

Comments
 (0)