返回 DeepSeek-Reasonix
capability.go
根目录 / internal / control / capability.go
1 package control
2
3 import (
4 "context"
5 "strings"
6
7 "reasonix/internal/agent"
8 "reasonix/internal/capability"
9 "reasonix/internal/config"
10 "reasonix/internal/plugin"
11 )
12
13 type invokedSkillsKey struct{}
14
15 func withInvokedSkills(ctx context.Context, names []string) context.Context {
16 if len(names) == 0 {
17 return ctx
18 }
19 invoked := make(map[string]bool, len(names))
20 for _, name := range names {
21 invoked[strings.ToLower(name)] = true
22 }
23 return context.WithValue(ctx, invokedSkillsKey{}, invoked)
24 }
25
26 func invokedSkills(ctx context.Context) map[string]bool {
27 invoked, _ := ctx.Value(invokedSkillsKey{}).(map[string]bool)
28 return invoked
29 }
30
31 func (c *Controller) withCapabilityRoute(ctx context.Context, composed, routeInput string) string {
32 if c == nil {
33 return composed
34 }
35 routeInput = strings.TrimSpace(agent.StripTransientUserBlocks(routeInput))
36 // A resolved inline invocation already supplies the skill body. With no
37 // typed task, routing on that body would invent a second skill request.
38 if routeInput == "" && len(invokedSkills(ctx)) == 0 {
39 routeInput = strings.TrimSpace(agent.StripTransientUserBlocks(composed))
40 }
41 if routeInput == "" && len(invokedSkills(ctx)) == 0 {
42 return composed
43 }
44 decision := c.routeCapabilities(ctx, routeInput)
45 // Pass structured decision to the agent via ledger — never re-parse the prompt.
46 if c.executor != nil {
47 c.executor.SeedCapabilityRoute(decision)
48 }
49 // Dual-model Planner also consumes the route through the user turn; seed
50 // its ledger when the runner exposes a planner agent.
51 if c.runner != nil {
52 if coord, ok := c.runner.(interface{ PlannerAgent() *agent.Agent }); ok {
53 if p := coord.PlannerAgent(); p != nil {
54 p.SeedCapabilityRoute(decision)
55 }
56 }
57 }
58 block := capability.RenderTransientBlock(decision)
59 if block == "" {
60 return composed
61 }
62 return block + "\n\n" + composed
63 }
64
65 func (c *Controller) routeCapabilities(ctx context.Context, routeInput string) capability.RouteDecision {
66 if ctx == nil {
67 ctx = context.Background()
68 }
69 // The catalog must reflect every registered tool, not just the provider-visible
70 // surface: optional tools (skills, subagents, …) stay off that surface but are
71 // reachable through use_capability, so their readiness decides the route.
72 tools := c.AllToolContractEntries()
73 // Deterministic routing is first. The semantic router runs only when that
74 // catalog match is itself ambiguous — never as a per-turn classification.
75 var proxyTools map[string][]plugin.CachedTool
76 if c.proxyToolsFn != nil {
77 proxyTools = c.proxyToolsFn()
78 }
79 if proxyTools == nil {
80 if reg := c.mcp.registry(); reg != nil {
81 if t, ok := reg.Get("use_capability"); ok {
82 if p, ok := t.(interface {
83 ConnectedProxyTools() map[string][]plugin.CachedTool
84 }); ok {
85 proxyTools = p.ConnectedProxyTools()
86 }
87 }
88 }
89 }
90 opts := capability.CatalogOptions{
91 Tools: tools,
92 Skills: c.Skills(),
93 }
94 if c.capabilityRuntime != nil {
95 opts.Plugins, opts.CachedTools, opts.CacheKeyOK, opts.Disabled, proxyTools = c.capabilityRuntime.CapabilityCatalogState()
96 } else if c.pluginCfg != nil {
97 opts.Plugins = c.pluginCfg
98 opts.CachedTools = c.capCachedTools
99 opts.CacheKeyOK = c.capCacheKeyOK
100 }
101 // Cached MCP tool schemas (loaded once in WireCapabilityRouting) let
102 // auto_start=false servers contribute concrete mcp-tool candidates to
103 // deterministic and semantic routing before any connection exists.
104 opts.ProxyTools = proxyTools
105 if h := c.Host(); h != nil {
106 opts.Connected = map[string]bool{}
107 for _, n := range h.ServerNames() {
108 opts.Connected[n] = true
109 }
110 opts.Failed = map[string]string{}
111 for _, f := range h.Failures() {
112 opts.Failed[f.Name] = f.Error
113 }
114 }
115 catalog := capability.BuildCatalog(opts)
116 if invoked := invokedSkills(ctx); len(invoked) > 0 {
117 entries := make([]capability.Entry, 0, len(catalog.Entries))
118 for _, entry := range catalog.Entries {
119 if entry.Kind != capability.KindSkill || !invoked[strings.ToLower(entry.Name)] {
120 entries = append(entries, entry)
121 }
122 }
123 catalog.Entries = entries
124 }
125 decision := capability.Route(routeInput, catalog.Entries)
126 if c.capabilityProxy {
127 decision.CapabilityProxy = true
128 }
129
130 strong := false
131 for _, cand := range decision.Candidates {
132 if cand.Policy == capability.AutoUseRequire || cand.Policy == capability.AutoUsePrefer {
133 strong = true
134 break
135 }
136 }
137 ambiguous := !strong && len(decision.Candidates) > 1
138 if ambiguous && c.semanticRouter != nil {
139 before := len(decision.Candidates)
140 decision = c.semanticRouter.RouteSemantic(ctx, routeInput, catalog, decision)
141 if c.capabilityProxy {
142 decision.CapabilityProxy = true
143 }
144 if c.capabilityAudit != nil {
145 c.capabilityAudit.RecordRoute(true, len(decision.Candidates) == before)
146 }
147 } else if c.capabilityAudit != nil {
148 c.capabilityAudit.RecordRoute(false, false)
149 }
150 if c.capabilityAudit != nil {
151 c.capabilityAudit.RecordDecision(decision)
152 }
153 return decision
154 }
155
156 // WireCapabilityRouting attaches hybrid routing helpers. Safe to call with nil
157 // semantic router (deterministic only). specs are the boot-converted plugin
158 // specs; their persisted schema caches are loaded once here so every routing
159 // turn can offer cached tools of not-yet-started servers.
160 func (c *Controller) WireCapabilityRouting(plugins []config.PluginEntry, specs []plugin.Spec, router *capability.SemanticRouter, audit *capability.Audit) {
161 if c == nil {
162 return
163 }
164 c.pluginCfg = append([]config.PluginEntry(nil), plugins...)
165 c.capCachedTools, c.capCacheKeyOK = capability.LoadCachedToolsForSpecs(specs, c.mcpHostProfile())
166 c.semanticRouter = router
167 c.capabilityAudit = audit
168 }
169
170 // SetCapabilityProxyRouting directs unready MCP route candidates to
171 // use_capability instead of connect_tool_source. Used by closed-loop routes and
172 // dual-model Planner boots.
173 func (c *Controller) SetCapabilityProxyRouting(v bool) {
174 if c == nil {
175 return
176 }
177 c.capabilityProxy = v
178 }
179
180 // SetCapabilityProxyTools registers a getter for live tools observed through
181 // use_capability without entering the provider-visible registry.
182 func (c *Controller) SetCapabilityProxyTools(fn func() map[string][]plugin.CachedTool) {
183 if c == nil {
184 return
185 }
186 c.proxyToolsFn = fn
187 }
188
188 lines GO