返回 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 func (c *Controller) withCapabilityRoute(ctx context.Context, composed, routeInput string) string {
14 if c == nil {
15 return composed
16 }
17 routeInput = strings.TrimSpace(agent.StripTransientUserBlocks(routeInput))
18 if routeInput == "" {
19 routeInput = strings.TrimSpace(agent.StripTransientUserBlocks(composed))
20 }
21 if routeInput == "" {
22 return composed
23 }
24 decision := c.routeCapabilities(ctx, routeInput)
25 // Pass structured decision to the agent via ledger — never re-parse the prompt.
26 if c.executor != nil {
27 c.executor.SeedCapabilityRoute(decision)
28 }
29 // Dual-model Planner also consumes the route through the user turn; seed
30 // its ledger when the runner exposes a planner agent.
31 if c.runner != nil {
32 if coord, ok := c.runner.(interface{ PlannerAgent() *agent.Agent }); ok {
33 if p := coord.PlannerAgent(); p != nil {
34 p.SeedCapabilityRoute(decision)
35 }
36 }
37 }
38 block := capability.RenderTransientBlock(decision)
39 if block == "" {
40 return composed
41 }
42 return block + "\n\n" + composed
43 }
44
45 func (c *Controller) routeCapabilities(ctx context.Context, routeInput string) capability.RouteDecision {
46 if ctx == nil {
47 ctx = context.Background()
48 }
49 tools := c.ToolContractEntries()
50 profile := c.runtimeProfile
51 if profile == "" {
52 profile = capability.ProfileBalanced
53 }
54 delivery := profile == capability.ProfileDelivery
55 var proxyTools map[string][]plugin.CachedTool
56 if c.proxyToolsFn != nil {
57 proxyTools = c.proxyToolsFn()
58 }
59 if proxyTools == nil {
60 if reg := c.mcp.registry(); reg != nil {
61 if t, ok := reg.Get("use_capability"); ok {
62 if p, ok := t.(interface {
63 ConnectedProxyTools() map[string][]plugin.CachedTool
64 }); ok {
65 proxyTools = p.ConnectedProxyTools()
66 }
67 }
68 }
69 }
70 opts := capability.CatalogOptions{
71 Tools: tools,
72 Skills: c.Skills(),
73 Profile: profile,
74 }
75 if c.capabilityRuntime != nil {
76 opts.Plugins, opts.CachedTools, opts.CacheKeyOK, opts.Disabled, proxyTools = c.capabilityRuntime.CapabilityCatalogState()
77 } else if c.pluginCfg != nil {
78 opts.Plugins = c.pluginCfg
79 opts.CachedTools = c.capCachedTools
80 opts.CacheKeyOK = c.capCacheKeyOK
81 }
82 // Cached MCP tool schemas (loaded once in WireCapabilityRouting) let
83 // auto_start=false servers contribute concrete mcp-tool candidates to
84 // deterministic and semantic routing before any connection exists.
85 opts.ProxyTools = proxyTools
86 if h := c.Host(); h != nil {
87 opts.Connected = map[string]bool{}
88 for _, n := range h.ServerNames() {
89 opts.Connected[n] = true
90 }
91 opts.Failed = map[string]string{}
92 for _, f := range h.Failures() {
93 opts.Failed[f.Name] = f.Error
94 }
95 }
96 catalog := capability.BuildCatalog(opts)
97 var decision capability.RouteDecision
98 if delivery {
99 decision = capability.RouteDelivery(routeInput, catalog.Entries)
100 } else {
101 decision = capability.Route(routeInput, catalog.Entries)
102 }
103 if c.capabilityProxy {
104 decision.CapabilityProxy = true
105 }
106
107 // Semantic routing only in Delivery when no strong require/prefer match.
108 if delivery && c.semanticRouter != nil {
109 before := len(decision.Candidates)
110 strong := false
111 for _, cand := range decision.Candidates {
112 if cand.Policy == capability.AutoUseRequire || cand.Policy == capability.AutoUsePrefer {
113 strong = true
114 break
115 }
116 }
117 if !strong {
118 decision = c.semanticRouter.RouteSemantic(ctx, routeInput, catalog, decision)
119 if c.capabilityProxy {
120 decision.CapabilityProxy = true
121 }
122 if c.capabilityAudit != nil {
123 fallback := len(decision.Candidates) == before
124 c.capabilityAudit.RecordRoute(true, fallback)
125 }
126 } else if c.capabilityAudit != nil {
127 c.capabilityAudit.RecordRoute(false, false)
128 }
129 } else if c.capabilityAudit != nil {
130 c.capabilityAudit.RecordRoute(false, false)
131 }
132 if c.capabilityAudit != nil {
133 c.capabilityAudit.RecordDecision(decision)
134 }
135 return decision
136 }
137
138 // WireCapabilityRouting attaches hybrid routing helpers. Safe to call with nil
139 // semantic router (deterministic only). specs are the boot-converted plugin
140 // specs; their persisted schema caches are loaded once here so every routing
141 // turn can offer cached tools of not-yet-started servers.
142 func (c *Controller) WireCapabilityRouting(plugins []config.PluginEntry, specs []plugin.Spec, router *capability.SemanticRouter, audit *capability.Audit) {
143 if c == nil {
144 return
145 }
146 c.pluginCfg = append([]config.PluginEntry(nil), plugins...)
147 c.capCachedTools, c.capCacheKeyOK = capability.LoadCachedToolsForSpecs(specs)
148 c.semanticRouter = router
149 c.capabilityAudit = audit
150 }
151
152 // SetCapabilityProxyRouting directs unready MCP route candidates to
153 // use_capability instead of connect_tool_source. Used by Delivery and by
154 // Balanced dual-model Planner boots.
155 func (c *Controller) SetCapabilityProxyRouting(v bool) {
156 if c == nil {
157 return
158 }
159 c.capabilityProxy = v
160 }
161
162 // SetCapabilityProxyTools registers a getter for live tools observed through
163 // use_capability without entering the provider-visible registry.
164 func (c *Controller) SetCapabilityProxyTools(fn func() map[string][]plugin.CachedTool) {
165 if c == nil {
166 return
167 }
168 c.proxyToolsFn = fn
169 }
170
170 lines GO