返回 DeepSeek-Reasonix
goal_run_ceiling_test.go
根目录 / internal / control / goal_run_ceiling_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "sync/atomic"
8 "testing"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/event"
12 "reasonix/internal/provider"
13 )
14
15 // workingGoalTool is a writer, so every round counts as acting on the
16 // investigation rather than only looking at it.
17 type workingGoalTool struct{ name string }
18
19 func (t workingGoalTool) Name() string { return t.name }
20 func (workingGoalTool) Description() string { return "apply an edit" }
21 func (workingGoalTool) ReadOnly() bool { return false }
22 func (workingGoalTool) Schema() json.RawMessage {
23 return json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"}}}`)
24 }
25 func (workingGoalTool) Execute(context.Context, json.RawMessage) (string, error) {
26 return "applied", nil
27 }
28
29 // steadyWorkProvider makes real progress every round: a distinct mutation, so
30 // neither the zero-evidence ladder nor the exploration decay ever escalates.
31 type steadyWorkProvider struct {
32 calls atomic.Int32
33 max int32
34 }
35
36 func (p *steadyWorkProvider) Name() string { return "steady-work" }
37
38 func (p *steadyWorkProvider) Stream(context.Context, provider.Request) (<-chan provider.Chunk, error) {
39 round := p.calls.Add(1)
40 ch := make(chan provider.Chunk, 4)
41 if round > p.max {
42 if round == p.max+1 {
43 ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: "finished", Name: "update_goal", Arguments: `{"status":"complete","reason":"All edits applied"}`}}
44 }
45 ch <- provider.Chunk{Type: provider.ChunkText, Text: "All done."}
46 ch <- provider.Chunk{Type: provider.ChunkDone}
47 close(ch)
48 return ch, nil
49 }
50 ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{
51 ID: fmt.Sprintf("edit-%d", round),
52 Name: "apply_edit",
53 Arguments: fmt.Sprintf(`{"path":"pkg%d/file.go"}`, round),
54 }}
55 ch <- provider.Chunk{Type: provider.ChunkDone}
56 close(ch)
57 return ch, nil
58 }
59
60 // A Goal turn used to be cut at 16 model rounds. Rounds were the wrong unit
61 // there for the same reason they were wrong for chat: a turn that reaches a
62 // high count while still producing new work is the case least worth
63 // interrupting. Goal now ends on its own terms — completion, a block, or a
64 // structural no-progress loop — none of which is a round count.
65 func TestGoalTurnRunsPastTheOldRoundCeiling(t *testing.T) {
66 prov := &steadyWorkProvider{max: 24}
67 reg := goalRegistry()
68 reg.Add(workingGoalTool{name: "apply_edit"})
69 exec := agent.New(prov, reg, agent.NewSession("sys"), agent.Options{}, event.Discard)
70 c, done := newChatBudgetController(t, exec)
71
72 c.SetGoal("apply every pending edit")
73 c.Submit("start")
74 <-done
75
76 if got := prov.calls.Load(); got <= 16 {
77 t.Fatalf("provider rounds = %d, want productive work to run past the retired 16-round ceiling", got)
78 }
79 }
80
80 lines GO