返回 DeepSeek-Reasonix
token_profile.go
根目录 / internal / boot / token_profile.go
1 package boot
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "sort"
8 "strings"
9 "sync"
10
11 "reasonix/internal/tool"
12 )
13
14 const (
15 TokenModeFull = "full"
16 TokenModeEconomy = "economy"
17 TokenModeDelivery = "delivery"
18 )
19
20 const tokenEconomyPrompt = `Economy mode is on. Keep work direct and use connect_tool_source only when the task needs a capability absent from the core file and shell tools.`
21
22 const tokenDeliveryPrompt = `<delivery-profile>
23 Prioritize a verified, complete result over minimizing model calls or tokens.
24 For action requests: establish acceptance criteria; reproduce bugs when practical;
25 inspect the relevant code and project rules; fix the root cause; run focused
26 verification; review the resulting diff and adjacent behavior; and continue until
27 the request is complete or a genuine blocker remains. Do not claim success without
28 evidence. State any unverified result or assumption explicitly.
29 </delivery-profile>`
30
31 var tokenEconomyCoreBuiltins = []string{
32 "bash",
33 "bash_output",
34 "edit_file",
35 "kill_shell",
36 "read_file",
37 "update_goal",
38 "wait",
39 "write_file",
40 }
41
42 func NormalizeTokenMode(mode string) string {
43 switch strings.ToLower(strings.TrimSpace(mode)) {
44 case TokenModeEconomy, "eco", "save", "saving", "low", "lite", "minimal":
45 return TokenModeEconomy
46 case TokenModeDelivery, "deliver", "quality", "performance":
47 return TokenModeDelivery
48 default:
49 return TokenModeFull
50 }
51 }
52
53 func tokenEconomyBuiltins(configured []string) []string {
54 if len(configured) == 0 {
55 return append([]string(nil), tokenEconomyCoreBuiltins...)
56 }
57 core := map[string]bool{}
58 for _, name := range tokenEconomyCoreBuiltins {
59 core[name] = true
60 }
61 out := make([]string, 0, len(configured))
62 seen := map[string]bool{}
63 for _, name := range configured {
64 name = strings.TrimSpace(name)
65 if name == "" || !core[name] || seen[name] {
66 continue
67 }
68 seen[name] = true
69 out = append(out, name)
70 }
71 return out
72 }
73
74 type toolSourceConnector struct {
75 mu sync.Mutex
76
77 skills func(context.Context) (string, error)
78 readOnlySkill func(context.Context) (string, error)
79 task func(context.Context) (string, error)
80 readOnlyTask func(context.Context) (string, error)
81 install func(context.Context) (string, error)
82 webFetch func(context.Context) (string, error)
83 lsp func(context.Context) (string, error)
84 docs func(context.Context) (string, error)
85 sessions func(context.Context) (string, error)
86 memory func(context.Context) (string, error)
87 commands func(context.Context) (string, error)
88 search func(context.Context) (string, error)
89 files func(context.Context) (string, error)
90 workflow func(context.Context) (string, error)
91 mcp func(context.Context, string) (string, error)
92 mcpNames []string
93 }
94
95 func (*toolSourceConnector) Name() string { return "connect_tool_source" }
96
97 func (*toolSourceConnector) Description() string {
98 return "Economy mode only: enable optional tools for the current task, including embedded Reasonix docs. For mcp, pass a configured server name or omit it to list servers. Enabled tools are available on the next model request."
99 }
100
101 func (*toolSourceConnector) ReadOnly() bool { return true }
102
103 func (*toolSourceConnector) Schema() json.RawMessage {
104 return json.RawMessage(`{
105 "type":"object",
106 "properties":{
107 "source":{"type":"string","description":"Tool source to enable: docs, search, files, workflow, sessions, memory, commands, skills, read_only_skill, mcp, lsp, web_fetch, install_source, task, or read_only_task."},
108 "name":{"type":"string","description":"For source=mcp, the configured server name. Omit to list configured MCP servers without connecting them."}
109 },
110 "required":["source"]
111 }`)
112 }
113
114 func (t *toolSourceConnector) Execute(ctx context.Context, args json.RawMessage) (string, error) {
115 var p struct {
116 Source string `json:"source"`
117 Name string `json:"name"`
118 }
119 if err := json.Unmarshal(args, &p); err != nil {
120 return "", fmt.Errorf("invalid args: %w", err)
121 }
122 source := normalizeToolSource(p.Source)
123 if source == "" {
124 return "", fmt.Errorf("unknown tool source %q; available: %s", p.Source, strings.Join(t.availableSources(), ", "))
125 }
126 name := strings.TrimSpace(p.Name)
127
128 out, mcpConnect, err := t.executeLocked(ctx, source, name, p.Source)
129 if mcpConnect == nil {
130 return out, err
131 }
132 // Connecting an MCP server spawns its subprocess and blocks until the
133 // handshake finishes (seconds, or until ctx expires), so it runs outside
134 // t.mu: concurrent connect_tool_source calls for fast sources must not
135 // queue behind it. No re-locking is needed afterwards: the callback itself
136 // merges the server's tools into the registry (which has its own lock),
137 // and Execute keeps no per-server state. Concurrent connects racing on the
138 // same server are deduplicated inside the callback via the plugin host
139 // (ErrServerAlreadyConnected / ErrSpawningInFlight fall back to the
140 // already-connected server's tools), so the loser still idempotently
141 // reports the enabled tools instead of failing.
142 return mcpConnect(ctx, name)
143 }
144
145 // executeLocked dispatches a connect_tool_source call under t.mu. Fast sources
146 // (registry-only mutations) run to completion while the lock is held. For an
147 // MCP connect with a server name it performs only the quick pre-checks
148 // (callback availability and source arguments) and returns the connect callback as
149 // mcpConnect; the caller invokes it after releasing t.mu. When mcpConnect is
150 // nil, out/err are the final result.
151 func (t *toolSourceConnector) executeLocked(ctx context.Context, source, name, rawSource string) (out string, mcpConnect func(context.Context, string) (string, error), err error) {
152 t.mu.Lock()
153 defer t.mu.Unlock()
154
155 switch source {
156 case "skills":
157 out, err = runSourceInstaller(ctx, "skills", t.skills)
158 case "read_only_skill":
159 out, err = runSourceInstaller(ctx, "read_only_skill", t.readOnlySkill)
160 case "task":
161 out, err = runSourceInstaller(ctx, "task", t.task)
162 case "read_only_task":
163 out, err = runSourceInstaller(ctx, "read_only_task", t.readOnlyTask)
164 case "install_source":
165 out, err = runSourceInstaller(ctx, "install_source", t.install)
166 case "web_fetch":
167 out, err = runSourceInstaller(ctx, "web_fetch", t.webFetch)
168 case "lsp":
169 out, err = runSourceInstaller(ctx, "lsp", t.lsp)
170 case "docs":
171 out, err = runSourceInstaller(ctx, "docs", t.docs)
172 case "sessions":
173 out, err = runSourceInstaller(ctx, "sessions", t.sessions)
174 case "memory":
175 out, err = runSourceInstaller(ctx, "memory", t.memory)
176 case "commands":
177 out, err = runSourceInstaller(ctx, "commands", t.commands)
178 case "search":
179 out, err = runSourceInstaller(ctx, "search", t.search)
180 case "files":
181 out, err = runSourceInstaller(ctx, "files", t.files)
182 case "workflow":
183 out, err = runSourceInstaller(ctx, "workflow", t.workflow)
184 case "mcp":
185 if name == "" {
186 if len(t.mcpNames) == 0 {
187 return "No configured MCP servers are available in this session.", nil, nil
188 }
189 names := append([]string(nil), t.mcpNames...)
190 sort.Strings(names)
191 return "Configured MCP servers: " + strings.Join(names, ", ") + ". Call connect_tool_source again with source=\"mcp\" and name set to connect one server.", nil, nil
192 }
193 if t.mcp == nil {
194 return "", nil, fmt.Errorf("MCP source is unavailable in this session")
195 }
196 return "", t.mcp, nil
197 default:
198 return "", nil, fmt.Errorf("unknown tool source %q", rawSource)
199 }
200 return out, nil, err
201 }
202
203 func normalizeToolSource(source string) string {
204 switch strings.ToLower(strings.TrimSpace(source)) {
205 case "skill", "skills":
206 return "skills"
207 case "read_only_skill", "readonly_skill", "read-only-skill", "read_only_skills", "readonly_skills", "read-only-skills":
208 return "read_only_skill"
209 case "mcp", "plugin", "plugins", "server", "servers":
210 return "mcp"
211 case "lsp", "language_server", "language-servers":
212 return "lsp"
213 case "web", "web_fetch", "webfetch", "fetch":
214 return "web_fetch"
215 case "install", "install_source", "installer":
216 return "install_source"
217 case "session", "sessions", "history", "conversation", "conversations":
218 return "sessions"
219 case "doc", "docs", "documentation", "help", "manual":
220 return "docs"
221 case "memory", "memories", "remember":
222 return "memory"
223 case "command", "commands", "slash", "slash_command", "slash-command":
224 return "commands"
225 case "search", "searches", "find", "grep":
226 return "search"
227 case "file", "files", "file_ops", "file-ops", "file_operations", "file-operations":
228 return "files"
229 case "workflow", "workflows", "todo", "todos":
230 return "workflow"
231 case "read_only_task", "readonly_task", "read-only-task", "read_only_subagent", "readonly_subagent", "read-only-subagent", "research_task", "research-subagent":
232 return "read_only_task"
233 case "task", "subagent", "subagents":
234 return "task"
235 default:
236 return ""
237 }
238 }
239
240 func (t *toolSourceConnector) availableSources() []string {
241 var out []string
242 if t.skills != nil {
243 out = append(out, "skills")
244 }
245 if t.readOnlySkill != nil {
246 out = append(out, "read_only_skill")
247 }
248 if t.mcp != nil || len(t.mcpNames) > 0 {
249 out = append(out, "mcp")
250 }
251 if t.lsp != nil {
252 out = append(out, "lsp")
253 }
254 if t.docs != nil {
255 out = append(out, "docs")
256 }
257 if t.sessions != nil {
258 out = append(out, "sessions")
259 }
260 if t.memory != nil {
261 out = append(out, "memory")
262 }
263 if t.commands != nil {
264 out = append(out, "commands")
265 }
266 if t.search != nil {
267 out = append(out, "search")
268 }
269 if t.files != nil {
270 out = append(out, "files")
271 }
272 if t.workflow != nil {
273 out = append(out, "workflow")
274 }
275 if t.webFetch != nil {
276 out = append(out, "web_fetch")
277 }
278 if t.install != nil {
279 out = append(out, "install_source")
280 }
281 if t.task != nil {
282 out = append(out, "task")
283 }
284 if t.readOnlyTask != nil {
285 out = append(out, "read_only_task")
286 }
287 sort.Strings(out)
288 return out
289 }
290
291 func runSourceInstaller(ctx context.Context, name string, fn func(context.Context) (string, error)) (string, error) {
292 if fn == nil {
293 return "", fmt.Errorf("%s source is unavailable in this session", name)
294 }
295 return fn(ctx)
296 }
297
298 func addTools(reg *tool.Registry, tools []tool.Tool) []string {
299 names := make([]string, 0, len(tools))
300 for _, t := range tools {
301 reg.Add(t)
302 names = append(names, t.Name())
303 }
304 sort.Strings(names)
305 return names
306 }
307
307 lines GO