From c6d867cde7d87696feaf3073f097ca15b0145273 Mon Sep 17 00:00:00 2001 From: weichen Date: Fri, 2 Oct 2026 02:11:43 +0000 Subject: [PATCH] Let length continuations and Anthropic output run unbounded Why: A fixed continuation cap and a 4096-token Anthropic fallback stopped long replies before the agent finished. Evidence: go test ./runtime/ ./runtime/llm/ Impact: MaxLengthContinuations of 0 keeps the old cap of 3; a negative value removes it. Unset Anthropic max_tokens defaults to 128000. Refs: feature/stream-display-overhaul Co-authored-by: Cursor --- runtime/llm/providers.go | 4 +++- runtime/loop.go | 13 +++++++++--- runtime/loop_harness_test.go | 40 ++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/runtime/llm/providers.go b/runtime/llm/providers.go index 0e8dc9b..5121175 100644 --- a/runtime/llm/providers.go +++ b/runtime/llm/providers.go @@ -258,8 +258,10 @@ func BuildBody(protocol Protocol, model string, messages []Message, tools []Tool "max_tokens": opts.MaxTokens, "stream": true, } + // Anthropic 要求 max_tokens。0 表示调用方不限制:用足够大的默认值, + // 避免 4096 把长回复截断。真正的上限由调用方 StreamOptions.MaxTokens 决定。 if body["max_tokens"] == 0 { - body["max_tokens"] = 4096 + body["max_tokens"] = 128000 } if sys != "" { body["system"] = sys diff --git a/runtime/loop.go b/runtime/loop.go index 5c560a8..529dc70 100644 --- a/runtime/loop.go +++ b/runtime/loop.go @@ -84,9 +84,12 @@ type Config struct { // RepeatGuard 为真时,同名且参数按键排序后相同的调用连续 3 次,注入一条提醒。 // 每个 run 最多 3 条,不中断循环。 RepeatGuard bool - // ContinueOnLength 为真时,FinishReason=length 且没有工具调用就续写,最多 3 次。 - // 续写提示不写进 Session。第 4 次截断返回错误。 + // ContinueOnLength 为真时,FinishReason=length 且没有工具调用就续写。 + // 续写提示不写进 Session。次数由 MaxLengthContinuations 决定。 ContinueOnLength bool + // MaxLengthContinuations 是 length 续写的次数上限。 + // 0 表示默认 3 次;负数表示不限制。 + MaxLengthContinuations int } // StopInfo 是 Stop 钩子看到的本 run 进度。 @@ -219,7 +222,11 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) { } emit(cfg, Event{Type: "turn_end", Message: last}) if cfg.ContinueOnLength && end.FinishReason == "length" && len(last.ToolCalls) == 0 { - if lengthContinues >= maxLengthContinuations { + lengthCap := cfg.MaxLengthContinuations + if lengthCap == 0 { + lengthCap = maxLengthContinuations + } + if lengthCap > 0 && lengthContinues >= lengthCap { errCap := errString("length continuation exceeded") cfg.logger().Error("runtime length continuation exceeded", errCap) return Result{Text: last.Content, Messages: messages, StopReason: llm.StopError, Usage: usage, Err: errCap}, nil diff --git a/runtime/loop_harness_test.go b/runtime/loop_harness_test.go index 6c32c2d..3683693 100644 --- a/runtime/loop_harness_test.go +++ b/runtime/loop_harness_test.go @@ -248,6 +248,46 @@ func TestLoopContinueOnLengthCaps(t *testing.T) { } } +func TestLoopContinueOnLengthUnlimited(t *testing.T) { + var n int + stream := func(ctx context.Context, model string, messages []llm.Message, decls []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event { + n++ + ch := make(chan llm.Event, 1) + finish := "length" + text := "part" + if n >= 5 { + finish = "stop" + text = "done" + } + ch <- llm.Event{ + Kind: llm.KindMessageEnd, + FinishReason: finish, + Message: llm.Message{Role: llm.RoleAssistant, Content: text, StopReason: llm.StopEnd}, + } + close(ch) + return ch + } + res, err := Run(context.Background(), Config{ + Model: "fake", + MaxTurns: 8, + ContinueOnLength: true, + MaxLengthContinuations: -1, + Stream: stream, + }, llm.Message{Role: llm.RoleUser, Content: "go"}) + if err != nil { + t.Fatal(err) + } + if res.Err != nil { + t.Fatalf("err = %v", res.Err) + } + if n != 5 { + t.Fatalf("model calls = %d, want 5", n) + } + if res.Text != "done" { + t.Fatalf("text = %q", res.Text) + } +} + func TestLoopHistorySkipsGeneratedSystem(t *testing.T) { var first string _, err := Run(context.Background(), Config{ -- 2.54.0