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