返回 DeepSeek-Reasonix
subagent_context.go
根目录 / internal / agent / subagent_context.go
1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "sort"
7 "strings"
8
9 "reasonix/internal/evidence"
10 )
11
12 const defaultReviewMaxSteps = 8
13 const defaultReviewOutputTokens = 2048
14
15 func composeChildTaskPrompt(spec ProfileExecSpec) string {
16 var b strings.Builder
17 b.WriteString("## Task\n")
18 b.WriteString(strings.TrimSpace(spec.Task.Objective))
19 if ctx := spec.Context; ctxHasFacts(ctx) {
20 if len(ctx.Decisions) > 0 {
21 b.WriteString("\n\n## Confirmed decisions\n")
22 for _, dec := range ctx.Decisions {
23 fmt.Fprintf(&b, "- %s (%s): %s\n", dec.Question, dec.ID, dec.Answer)
24 }
25 }
26 if strings.TrimSpace(ctx.EvidenceSummary) != "" {
27 b.WriteString("\n## Evidence summary\n")
28 b.WriteString(strings.TrimSpace(ctx.EvidenceSummary))
29 b.WriteByte('\n')
30 }
31 if len(ctx.FileAnchors) > 0 {
32 b.WriteString("\n## File anchors\n")
33 for _, path := range ctx.FileAnchors {
34 fmt.Fprintf(&b, "- %s\n", path)
35 }
36 }
37 if strings.TrimSpace(ctx.OutputFormat) != "" {
38 b.WriteString("\n## Output format\n")
39 b.WriteString(strings.TrimSpace(ctx.OutputFormat))
40 b.WriteByte('\n')
41 }
42 }
43 b.WriteString("\nDo not copy or reconstruct the parent session. Use only this pack plus tools.")
44 return strings.TrimSpace(b.String())
45 }
46
47 func ctxHasFacts(ctx ContextRequest) bool {
48 return len(ctx.Decisions) > 0 || strings.TrimSpace(ctx.EvidenceSummary) != "" ||
49 len(ctx.FileAnchors) > 0 || strings.TrimSpace(ctx.OutputFormat) != ""
50 }
51
52 func applyReviewBudget(spec *ProfileExecSpec) {
53 if spec == nil {
54 return
55 }
56 switch strings.TrimSpace(spec.Worker.Profile) {
57 case "review", "security-review", "security_review", "team-architect":
58 if spec.Sched.MaxSteps <= 0 {
59 spec.Sched.MaxSteps = defaultReviewMaxSteps
60 }
61 if spec.Sched.MaxOutputTokens <= 0 {
62 spec.Sched.MaxOutputTokens = defaultReviewOutputTokens
63 }
64 if strings.TrimSpace(spec.Context.OutputFormat) == "" {
65 spec.Context.OutputFormat = "Return structured fields only: verdict, blocking_findings, non_blocking, required_changes. Do not restate full files or full test logs."
66 }
67 }
68 }
69
70 // PrepareReviewSubagentContext applies the same bounded review contract used
71 // by task/profile delegation to built-in skill runners. The returned boolean
72 // is false for non-review profiles so their existing budgets remain unchanged.
73 func PrepareReviewSubagentContext(ctx context.Context, profile, objective string) (prompt string, maxSteps, maxOutputTokens int, ok bool) {
74 spec := ProfileExecSpec{
75 Task: TaskSpec{Objective: objective},
76 Worker: WorkerSpec{Profile: profile},
77 }
78 applyReviewBudget(&spec)
79 if spec.Sched.MaxSteps == 0 && spec.Sched.MaxOutputTokens == 0 {
80 return objective, 0, 0, false
81 }
82 fillChildFacts(ctx, &spec)
83 return composeChildTaskPrompt(spec), spec.Sched.MaxSteps, spec.Sched.MaxOutputTokens, true
84 }
85
86 type childOutputBudgetKey struct{}
87
88 func withChildOutputBudget(ctx context.Context, n int) context.Context {
89 if n <= 0 {
90 return ctx
91 }
92 return context.WithValue(ctx, childOutputBudgetKey{}, n)
93 }
94
95 func childOutputBudgetFrom(ctx context.Context) int {
96 n, _ := ctx.Value(childOutputBudgetKey{}).(int)
97 return n
98 }
99
100 func fillChildFacts(ctx context.Context, spec *ProfileExecSpec) {
101 if spec == nil {
102 return
103 }
104 if len(spec.Context.Decisions) == 0 {
105 if turn := turnStateFrom(ctx); turn != nil {
106 spec.Context.Decisions = turn.loop.snapshotDecisions()
107 }
108 }
109 ledger, ok := evidence.FromContext(ctx)
110 if !ok {
111 return
112 }
113 summary, anchors := compactParentFacts(ledger)
114 if spec.Context.EvidenceSummary == "" {
115 spec.Context.EvidenceSummary = summary
116 }
117 if len(spec.Context.FileAnchors) == 0 {
118 spec.Context.FileAnchors = anchors
119 }
120 }
121
122 func compactParentFacts(ledger *evidence.Ledger) (string, []string) {
123 if ledger == nil {
124 return "", nil
125 }
126 receipts := ledger.Receipts()
127 successes, mutations, reads := 0, 0, 0
128 seen := map[string]bool{}
129 var anchors []string
130 for _, rec := range receipts {
131 if !rec.Success {
132 continue
133 }
134 successes++
135 if rec.Mutation || rec.Write {
136 mutations++
137 }
138 if rec.Read {
139 reads++
140 }
141 for _, path := range rec.Paths {
142 if path == "" || seen[path] {
143 continue
144 }
145 seen[path] = true
146 if len(anchors) < 16 {
147 anchors = append(anchors, path)
148 }
149 }
150 }
151 sort.Strings(anchors)
152 if successes == 0 {
153 return "", anchors
154 }
155 var facts []string
156 for i := len(receipts) - 1; i >= 0 && len(facts) < 12; i-- {
157 rec := receipts[i]
158 if !rec.Success {
159 continue
160 }
161 kind := "other"
162 switch {
163 case rec.Mutation || rec.Write:
164 kind = "write"
165 case rec.Read:
166 kind = "read"
167 }
168 line := fmt.Sprintf("- tool=%s kind=%s paths=%d output_bytes=%d", rec.ToolName, kind, len(rec.Paths), rec.OutputBytes)
169 if rec.Verification != "" {
170 line += " verification=" + rec.Verification
171 }
172 if rec.ExitCode != nil {
173 line += fmt.Sprintf(" exit_code=%d", *rec.ExitCode)
174 }
175 if rec.OutputDigest != "" {
176 digest := rec.OutputDigest
177 if len(digest) > 12 {
178 digest = digest[:12]
179 }
180 line += " output_digest=" + digest
181 }
182 facts = append(facts, line)
183 }
184 for left, right := 0, len(facts)-1; left < right; left, right = left+1, right-1 {
185 facts[left], facts[right] = facts[right], facts[left]
186 }
187 return fmt.Sprintf("%d successful receipts (%d mutations, %d reads).\n%s", successes, mutations, reads, strings.Join(facts, "\n")), anchors
188 }
189
189 lines GO