返回 DeepSeek-Reasonix
adapters.go
根目录 / internal / extension / adapters.go
1 package extension
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7
8 "reasonix/internal/command"
9 "reasonix/internal/hook"
10 "reasonix/internal/plugin"
11 "reasonix/internal/pluginpkg"
12 "reasonix/internal/provider"
13 "reasonix/internal/skill"
14 "reasonix/internal/tool"
15 )
16
17 // The adapters wrap existing discovery in the Contributor interface. They do
18 // not reimplement any winner rules: each source package keeps resolving its
19 // own intra-source clashes exactly as today (skill tier priority, command
20 // later-root-wins, hook accumulation), and the kernel resolves only what
21 // crosses contributor boundaries.
22
23 // BuiltinToolsContributor contributes the compile-time built-in tools as
24 // KindTool at the builtin tier. The built-in set is process-global, so the
25 // contributor is a pure function of whatever registered itself via init().
26 func BuiltinToolsContributor() Contributor {
27 return ContributorFunc{
28 ContributorName: "builtin-tools",
29 Fn: func(context.Context) ([]Contribution, error) {
30 tools := tool.Builtins()
31 out := make([]Contribution, 0, len(tools))
32 for _, t := range tools {
33 out = append(out, Contribution{
34 Kind: KindTool,
35 ID: t.Name(),
36 Source: ContributionSource{Scope: ScopeBuiltin, Origin: "builtin"},
37 Payload: t,
38 })
39 }
40 return out, nil
41 },
42 }
43 }
44
45 // skillScope maps a discovered skill onto a kernel scope. Plugin ownership
46 // wins over the discovery scope because a plugin's skill must shadow and
47 // conflict as part of its package, not as the root it happened to load from.
48 // Custom roots have no kernel tier of their own; they map to global (the
49 // store's own project > custom > global ordering already ran inside List, so
50 // no cross-tier information is lost).
51 func skillScope(sk skill.Skill) (Scope, string) {
52 if sk.Plugin != "" {
53 return ScopePlugin, "plugin"
54 }
55 switch sk.Scope {
56 case skill.ScopeProject:
57 return ScopeProject, "project"
58 case skill.ScopeBuiltin:
59 return ScopeBuiltin, "builtin"
60 case skill.ScopeCustom:
61 return ScopeGlobal, "custom"
62 default:
63 return ScopeGlobal, "global"
64 }
65 }
66
67 // SkillContribution maps one discovered skill to its kernel contribution. The
68 // canonical ID is the user-facing slash name, so a plugin skill ("pkg:name")
69 // and a project skill ("name") occupy distinct IDs exactly as they do for the
70 // user. Boot uses this to contribute the skill list its build already
71 // assembled, so scope attribution lives in exactly one place.
72 func SkillContribution(sk skill.Skill) Contribution {
73 scope, origin := skillScope(sk)
74 return Contribution{
75 Kind: KindSkill,
76 ID: sk.SlashName(),
77 Source: ContributionSource{
78 PluginID: sk.Plugin,
79 Scope: scope,
80 Origin: origin,
81 Path: sk.Path,
82 },
83 Payload: sk,
84 }
85 }
86
87 // SkillsContributor contributes a store's model-visible skills as KindSkill.
88 // The canonical ID is the user-facing slash name, so a plugin skill
89 // ("pkg:name") and a project skill ("name") occupy distinct IDs exactly as
90 // they do for the user. Store.List already dedupes bare names by tier, so
91 // what reaches the kernel is the effective set.
92 func SkillsContributor(store *skill.Store) Contributor {
93 return ContributorFunc{
94 ContributorName: "skills",
95 Fn: func(context.Context) ([]Contribution, error) {
96 if store == nil {
97 return nil, nil
98 }
99 skills := store.List()
100 out := make([]Contribution, 0, len(skills))
101 for _, sk := range skills {
102 out = append(out, SkillContribution(sk))
103 }
104 return out, nil
105 },
106 }
107 }
108
109 // CommandContribution maps one resolved command to its kernel contribution.
110 // Plugin commands map to the plugin tier. Non-plugin commands map to the
111 // project tier: command.LoadRoots has already collapsed user-vs-project
112 // ordering into a single winner, so the surviving entry represents the
113 // strongest applicable scope.
114 func CommandContribution(c command.Command) Contribution {
115 source := ContributionSource{
116 PluginID: c.Plugin,
117 Scope: ScopeProject,
118 Origin: "command_root",
119 Path: c.Source,
120 }
121 if c.Plugin != "" {
122 source.Scope = ScopePlugin
123 source.Origin = "plugin"
124 }
125 return Contribution{
126 Kind: KindCommand,
127 ID: c.Name,
128 Source: source,
129 Payload: c,
130 }
131 }
132
133 // CommandsContributor contributes the commands resolved by command.LoadRoots
134 // as KindCommand. LoadRoots' later-root-wins and plugin namespacing run
135 // first; the kernel sees the resolved set. A load error aborts the
136 // contribution — silently dropping a broken file is how a user's command
137 // disappears without a trace.
138 func CommandsContributor(roots ...command.Root) Contributor {
139 return ContributorFunc{
140 ContributorName: "commands",
141 Fn: func(context.Context) ([]Contribution, error) {
142 commands, err := command.LoadRoots(roots...)
143 if err != nil {
144 return nil, fmt.Errorf("extension: commands: %w", err)
145 }
146 out := make([]Contribution, 0, len(commands))
147 for _, c := range commands {
148 out = append(out, CommandContribution(c))
149 }
150 return out, nil
151 },
152 }
153 }
154
155 // HookContribution maps one resolved hook to its additive kernel
156 // contribution. seq is the hook's per-event index in load order, so the
157 // "event#n" ID matches the order hooks fire today (project, then plugin,
158 // then global within an event).
159 func HookContribution(h hook.ResolvedHook, seq int) Contribution {
160 return Contribution{
161 Kind: KindHook,
162 ID: fmt.Sprintf("%s#%d", h.Event, seq),
163 Source: ContributionSource{
164 Scope: hookScope(h.Scope),
165 Origin: h.Source,
166 Path: h.Source,
167 },
168 Payload: h,
169 }
170 }
171
172 // HooksContributor contributes every resolved hook as additive KindHook
173 // entries. IDs are "event#n" with n counted per event in load order, matching
174 // the order the hooks fire today (project, then plugin, then global within an
175 // event). Hooks never shadow, so all of them survive resolution.
176 func HooksContributor(opts hook.LoadOptions) Contributor {
177 return ContributorFunc{
178 ContributorName: "hooks",
179 Fn: func(context.Context) ([]Contribution, error) {
180 hooks := hook.Load(opts)
181 out := make([]Contribution, 0, len(hooks))
182 perEvent := map[hook.Event]int{}
183 for _, h := range hooks {
184 n := perEvent[h.Event]
185 perEvent[h.Event]++
186 out = append(out, HookContribution(h, n))
187 }
188 return out, nil
189 },
190 }
191 }
192
193 // hookScope maps the settings-file scope of a hook. Plugin hooks carry their
194 // package's tier; the plugin identity itself stays in the payload because
195 // hook.ResolvedHook does not expose it separately.
196 func hookScope(s hook.Scope) Scope {
197 switch s {
198 case hook.ScopeProject:
199 return ScopeProject
200 case hook.ScopePlugin:
201 return ScopePlugin
202 default:
203 return ScopeGlobal
204 }
205 }
206
207 // MCPServerContribution maps one MCP server spec to its kernel contribution,
208 // keyed by server name — the same identity that namespaces the server's tools
209 // as mcp__<server>__<tool>.
210 func MCPServerContribution(spec plugin.Spec) Contribution {
211 scope, origin := mcpScope(spec)
212 return Contribution{
213 Kind: KindMCPServer,
214 ID: spec.Name,
215 Source: ContributionSource{
216 PluginID: spec.Package,
217 Scope: scope,
218 Origin: origin,
219 },
220 Payload: spec,
221 }
222 }
223
224 // MCPServersContributor contributes MCP server specs as KindMCPServer keyed
225 // by server name — the same identity that namespaces the server's tools as
226 // mcp__<server>__<tool>. Tier mapping follows the spec's provenance: plugin
227 // packages at the plugin tier, project/workspace config at the project tier,
228 // everything else (user config, host sessions) at the global tier.
229 func MCPServersContributor(specs ...plugin.Spec) Contributor {
230 return ContributorFunc{
231 ContributorName: "mcp-servers",
232 Fn: func(context.Context) ([]Contribution, error) {
233 out := make([]Contribution, 0, len(specs))
234 for _, spec := range specs {
235 out = append(out, MCPServerContribution(spec))
236 }
237 return out, nil
238 },
239 }
240 }
241
242 // mcpScope derives the kernel tier from a spec's provenance. Package is the
243 // strongest signal — it means an installed plugin brought the server along;
244 // ConfigSource distinguishes project/workspace config from user-level config
245 // (see internal/config.MCPConfigSource for the value set).
246 func mcpScope(spec plugin.Spec) (Scope, string) {
247 configSource := strings.TrimSpace(spec.ConfigSource)
248 if spec.Package != "" || configSource == "plugin_package" {
249 if configSource == "" {
250 configSource = "plugin_package"
251 }
252 return ScopePlugin, configSource
253 }
254 if strings.HasPrefix(configSource, "project") || configSource == "workspace_config" {
255 return ScopeProject, configSource
256 }
257 if configSource == "" {
258 configSource = "user_config"
259 }
260 return ScopeGlobal, configSource
261 }
262
263 // PromptContribution maps one plugin package prompt template to its kernel
264 // contribution as KindPrompt, namespaced "plugin:<plugin>:<name>" — the same
265 // namespacing the catalog documents for themes — so prompts from different
266 // packages can never collide. This adapter lives in extension rather than
267 // pluginpkg for the same reason it may import pluginpkg at all: pluginpkg
268 // cannot import extension (extension -> hook -> pluginpkg would become an
269 // import cycle), while extension importing pluginpkg is acyclic.
270 func PromptContribution(pluginID string, ref pluginpkg.PromptRef) Contribution {
271 return Contribution{
272 Kind: KindPrompt,
273 ID: "plugin:" + pluginID + ":" + ref.Name,
274 Source: ContributionSource{
275 PluginID: pluginID,
276 Scope: ScopePlugin,
277 Origin: "plugin",
278 Path: ref.Path,
279 },
280 Payload: ref,
281 }
282 }
283
284 // ThemeContribution maps one plugin package theme file to its kernel
285 // contribution as KindTheme with the stable ID "plugin:<plugin>:<theme>"
286 // named in the catalog contract.
287 func ThemeContribution(pluginID string, ref pluginpkg.ThemeRef) Contribution {
288 return Contribution{
289 Kind: KindTheme,
290 ID: "plugin:" + pluginID + ":" + ref.Name,
291 Source: ContributionSource{
292 PluginID: pluginID,
293 Scope: ScopePlugin,
294 Origin: "plugin",
295 Path: ref.Path,
296 },
297 Payload: ref,
298 }
299 }
300
301 // PluginResourcesContributor contributes one parsed plugin package's prompt
302 // templates (KindPrompt) and theme files (KindTheme). Runtime, interceptor,
303 // and strategy wiring is a later stage; this covers the manifest's static
304 // resource contributions.
305 func PluginResourcesContributor(pkg pluginpkg.Package) Contributor {
306 return ContributorFunc{
307 ContributorName: "plugin-resources-" + pkg.Manifest.Name,
308 Fn: func(context.Context) ([]Contribution, error) {
309 inv := pkg.Inventory()
310 out := make([]Contribution, 0, len(inv.Prompts)+len(inv.Themes))
311 for _, ref := range inv.Prompts {
312 out = append(out, PromptContribution(pkg.Manifest.Name, ref))
313 }
314 for _, ref := range inv.Themes {
315 out = append(out, ThemeContribution(pkg.Manifest.Name, ref))
316 }
317 return out, nil
318 },
319 }
320 }
321
322 // ProviderContribution maps one provider descriptor to its kernel
323 // contribution, keyed by ref ("provider/model") at the builtin tier.
324 func ProviderContribution(desc provider.Descriptor) Contribution {
325 return Contribution{
326 Kind: KindProvider,
327 ID: desc.Ref,
328 Source: ContributionSource{Scope: ScopeBuiltin, Origin: "provider_catalog"},
329 Payload: desc,
330 }
331 }
332
333 // ProvidersContributor contributes provider descriptors as KindProvider keyed
334 // by ref ("provider/model"). Descriptors come from the built-in provider
335 // catalog, so they sit at the builtin tier until a stage-3 source contributes
336 // user-defined providers above them.
337 func ProvidersContributor(descs ...provider.Descriptor) Contributor {
338 return ContributorFunc{
339 ContributorName: "providers",
340 Fn: func(context.Context) ([]Contribution, error) {
341 out := make([]Contribution, 0, len(descs))
342 for _, desc := range descs {
343 out = append(out, ProviderContribution(desc))
344 }
345 return out, nil
346 },
347 }
348 }
349
349 lines GO