返回 DeepSeek-Reasonix
profile_spec.go
根目录 / internal / agent / profile_spec.go
1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7
8 "reasonix/internal/skill"
9 "reasonix/internal/tool"
10 )
11
12 // ProfileDefinition is the delegation-facing narrowing of a stored Skill: what
13 // the worker is, never what one call wants of it. A field belongs here only if
14 // its value follows from the worker's identity; allowed-tools and read-only are
15 // ceilings, not grants. Profile names resolve at call time and must never enter
16 // tool schemas or the parent system prompt (prompt-cache stability).
17 type ProfileDefinition struct {
18 Name string
19 Body string
20 AllowedTools []string
21 Model string
22 Effort string
23 ReadOnly bool
24 // Invocation is "auto" or "manual". Explicit profile= on task/fleet may
25 // call manual profiles; automatic discovery still respects the index.
26 Invocation string
27 // NamedBuiltin is true for the built-in explore/research/review/
28 // security-review profiles. Their body is still the full system prompt
29 // (no implicit concise default), matching custom profiles.
30 NamedBuiltin bool
31 }
32
33 // ProfileLookup resolves a profile by exact skill name. Implementations read
34 // from the live Skill store; a nil lookup means profile= is unavailable.
35 type ProfileLookup func(name string) (ProfileDefinition, bool)
36
37 // ProfileFromSkill narrows a stored Skill to the fields delegation may see.
38 // Routing metadata (triggers, auto-use, cost, freshness) stays behind: it
39 // decides when a worker is chosen, not how that worker thinks, and admitting it
40 // here is the first step from a profile toward a workflow language.
41 func ProfileFromSkill(sk skill.Skill) ProfileDefinition {
42 return ProfileDefinition{
43 Name: sk.Name,
44 Body: sk.Body,
45 AllowedTools: sk.AllowedTools,
46 Model: sk.Model,
47 Effort: sk.Effort,
48 ReadOnly: sk.ReadOnly,
49 Invocation: sk.Invocation,
50 NamedBuiltin: NamedBuiltinProfile(sk.Name),
51 }
52 }
53
54 // ProfileExecSpec is the unified execution specification shared by task,
55 // fleet items, and run_skill profile runs. Call sites build a spec, then hand
56 // it to TaskTool.RunProfileSpec so runners cannot drift. Its members are the
57 // delegation boundary: place a new field in the member that decides its value,
58 // never in whichever one is closest to the call site.
59 type ProfileExecSpec struct {
60 Task TaskSpec
61 Worker WorkerSpec
62 Grant CapabilityGrant
63 Context ContextRequest
64 Sched SchedulerPolicy
65 }
66
67 // TaskSpec is what one delegated run must accomplish. Every field is decided
68 // per call by the delegating parent, never by the worker's identity.
69 type TaskSpec struct {
70 // Objective is the task text handed to the child agent.
71 Objective string
72 // Description is an optional short UI label.
73 Description string
74 }
75
76 // WorkerSpec is who carries the run out: the resolved profile identity and the
77 // provider runtime it thinks with. Fields here follow from the worker chosen,
78 // not from what this particular call asks for.
79 type WorkerSpec struct {
80 // Kind is the transcript kind: "task", "skill", or "fleet".
81 Kind string
82 // Name is the transcript / display name (profile name or "task").
83 Name string
84 // Profile is the optional profile skill name (empty for ordinary task).
85 Profile string
86 // SystemPrompt is the full child system prompt.
87 SystemPrompt string
88 // UseProfilePrompt marks a profile body used verbatim, with no task default.
89 UseProfilePrompt bool
90 // Model/Effort are the already-resolved effective values for this run
91 // (after config override → call params → frontmatter → global → parent).
92 Model string
93 Effort string
94 }
95
96 // CapabilityGrant is what the run may touch. Profile frontmatter supplies a
97 // ceiling and call arguments may only narrow it (see IntersectToolLists), so
98 // the effective grant is always the intersection of the two.
99 type CapabilityGrant struct {
100 // ReadOnly forces the read-only registry even when the profile can write.
101 ReadOnly bool
102 // AllowNoTools lets the parallel-research path run a child with no tools.
103 AllowNoTools bool
104 // CallTools is the optional per-call tools whitelist.
105 CallTools []string
106 // ProfileTools is the profile frontmatter allowed-tools ceiling.
107 ProfileTools []string
108 // WritePaths is the normalized write claim (empty for read-only).
109 WritePaths WritePathSet
110 }
111
112 // ContextRequest is the context a child starts from, as opposed to the task it
113 // is given. Today that is only a prior transcript to resume.
114 type ContextRequest struct {
115 // ContinueFrom / ForkFrom are transcript continuation refs (writer path).
116 ContinueFrom string
117 ForkFrom string
118 // Ephemeral forces a non-persisted transcript for entry points that promise
119 // no durable host side effects, such as read_only_task.
120 Ephemeral bool
121 // Decisions, EvidenceSummary, FileAnchors, and OutputFormat are the only
122 // parent facts a child should start from. The parent transcript is not copied.
123 Decisions []acceptedDecision
124 EvidenceSummary string
125 FileAnchors []string
126 OutputFormat string
127 }
128
129 // SchedulerPolicy is when and how the run executes. It never changes what the
130 // child is asked to do or what it is allowed to touch.
131 type SchedulerPolicy struct {
132 // MaxSteps is the optional per-call step budget (0 = default).
133 MaxSteps int
134 // MaxOutputTokens is an optional child completion cap (0 = inherit).
135 MaxOutputTokens int
136 // RunInBackground starts a jobs.Manager background job.
137 RunInBackground bool
138 // BackgroundWriter marks work already hosted by a parent background job
139 // (for example fleet). It participates in checkpoint writer exclusion
140 // without spawning a second nested job.
141 BackgroundWriter bool
142 // Nested marks nested sub-agent acquires (fail-fast on concurrency limits).
143 Nested bool
144 }
145
146 // ResolveProfileDefinition looks up a profile and enforces the runAs=subagent
147 // contract. Explicit names may invoke invocation=manual profiles.
148 func ResolveProfileDefinition(lookup ProfileLookup, name string) (ProfileDefinition, error) {
149 name = strings.TrimSpace(name)
150 if name == "" {
151 return ProfileDefinition{}, fmt.Errorf("profile name is required")
152 }
153 if lookup == nil {
154 return ProfileDefinition{}, fmt.Errorf("profile resolution is not configured in this session")
155 }
156 def, ok := lookup(name)
157 if !ok {
158 return ProfileDefinition{}, fmt.Errorf("unknown profile %q", name)
159 }
160 if strings.TrimSpace(def.Name) == "" {
161 def.Name = name
162 }
163 return def, nil
164 }
165
166 // IntersectToolLists returns the intersection of profile tools and call tools.
167 // Call parameters may only narrow permissions, never expand them.
168 //
169 // Rules:
170 // - both empty → nil (meaning "all tools allowed by the registry builder")
171 // - profile empty, call set → call list
172 // - call empty, profile set → profile list
173 // - both set → expand patterns against parent, then intersect; empty
174 // intersection is an error
175 func IntersectToolLists(parent *tool.Registry, profileTools, callTools []string) ([]string, error) {
176 profileTools = cleanToolList(profileTools)
177 callTools = cleanToolList(callTools)
178 if len(profileTools) == 0 {
179 return callTools, nil
180 }
181 if len(callTools) == 0 {
182 return profileTools, nil
183 }
184 // Imported profiles support wildcard tool names. Resolve both sides against
185 // the same live registry before comparing them so a profile pattern can be
186 // narrowed by a concrete call tool (and vice versa).
187 if parent != nil {
188 profileTools = expandToolPatterns(parent, profileTools)
189 callTools = expandToolPatterns(parent, callTools)
190 }
191 allowed := map[string]bool{}
192 for _, t := range profileTools {
193 allowed[t] = true
194 }
195 var out []string
196 seen := map[string]bool{}
197 for _, t := range callTools {
198 if !allowed[t] || seen[t] {
199 continue
200 }
201 seen[t] = true
202 out = append(out, t)
203 }
204 if len(out) == 0 {
205 return nil, fmt.Errorf("tools intersection is empty: call tools are not within the profile allowlist")
206 }
207 return out, nil
208 }
209
210 // ResolveModelEffort applies the fixed priority:
211 // profile persistent config → call params → profile frontmatter → global
212 // subagent default. Empty results leave identity resolution to the parent.
213 func ResolveModelEffort(configModel, configEffort, callModel, callEffort, profileModel, profileEffort, globalModel, globalEffort string) (model, effort string) {
214 model = firstNonBlank(
215 strings.TrimSpace(configModel),
216 strings.TrimSpace(callModel),
217 strings.TrimSpace(profileModel),
218 strings.TrimSpace(globalModel),
219 )
220 effort = firstNonBlank(
221 strings.TrimSpace(configEffort),
222 strings.TrimSpace(callEffort),
223 strings.TrimSpace(profileEffort),
224 strings.TrimSpace(globalEffort),
225 )
226 return model, effort
227 }
228
229 func firstNonBlank(values ...string) string {
230 for _, v := range values {
231 if strings.TrimSpace(v) != "" {
232 return strings.TrimSpace(v)
233 }
234 }
235 return ""
236 }
237
238 func cleanToolList(names []string) []string {
239 if len(names) == 0 {
240 return nil
241 }
242 out := make([]string, 0, len(names))
243 seen := map[string]bool{}
244 for _, n := range names {
245 n = strings.TrimSpace(n)
246 if n == "" || seen[n] {
247 continue
248 }
249 seen[n] = true
250 out = append(out, n)
251 }
252 return out
253 }
254
255 // NamedBuiltinProfile reports whether name is a built-in named subagent profile.
256 func NamedBuiltinProfile(name string) bool {
257 switch strings.TrimSpace(name) {
258 case "explore", "research", "review", "security-review", "security_review":
259 return true
260 default:
261 return false
262 }
263 }
264
265 // parentSession returns the owning session, or empty when the caller asked for
266 // an ephemeral run so the store never persists a transcript for it.
267 func (c ContextRequest) parentSession(ctx context.Context) string {
268 if c.Ephemeral {
269 return ""
270 }
271 return ParentSession(ctx)
272 }
273
273 lines GO