返回 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 // Deterministic routing is first. The semantic router runs only when that
51 // catalog match is itself ambiguous — never as a per-turn classification.
52 var proxyTools map[string][]plugin.CachedTool
53 if c.proxyToolsFn != nil {
54 proxyTools = c.proxyToolsFn()
55 }
56 if proxyTools == nil {
57 if reg := c.mcp.registry(); reg != nil {
58 if t, ok := reg.Get("use_capability"); ok {
59 if p, ok := t.(interface {
60 ConnectedProxyTools() map[string][]plugin.CachedTool
61 }); ok {
62 proxyTools = p.ConnectedProxyTools()
63 }
64 }
65 }
66 }
67 opts := capability.CatalogOptions{
68 Tools: tools,
69 Skills: c.Skills(),
70 }
71 if c.capabilityRuntime != nil {
72 opts.Plugins, opts.CachedTools, opts.CacheKeyOK, opts.Disabled, proxyTools = c.capabilityRuntime.CapabilityCatalogState()
73 } else if c.pluginCfg != nil {
74 opts.Plugins = c.pluginCfg
75 opts.CachedTools = c.capCachedTools
76 opts.CacheKeyOK = c.capCacheKeyOK
77 }
78 // Cached MCP tool schemas (loaded once in WireCapabilityRouting) let
79 // auto_start=false servers contribute concrete mcp-tool candidates to
80 // deterministic and semantic routing before any connection exists.
81 opts.ProxyTools = proxyTools
82 if h := c.Host(); h != nil {
83 opts.Connected = map[string]bool{}
84 for _, n := range h.ServerNames() {
85 opts.Connected[n] = true
86 }
87 opts.Failed = map[string]string{}
88 for _, f := range h.Failures() {
89 opts.Failed[f.Name] = f.Error
90 }
91 }
92 catalog := capability.BuildCatalog(opts)
93 decision := capability.Route(routeInput, catalog.Entries)
94 if c.capabilityProxy {
95 decision.CapabilityProxy = true
96 }
97
98 strong := false
99 for _, cand := range decision.Candidates {
100 if cand.Policy == capability.AutoUseRequire || cand.Policy == capability.AutoUsePrefer {
101 strong = true
102 break
103 }
104 }
105 ambiguous := !strong && len(decision.Candidates) > 1
106 if ambiguous && c.semanticRouter != nil {
107 before := len(decision.Candidates)
108 decision = c.semanticRouter.RouteSemantic(ctx, routeInput, catalog, decision)
109 if c.capabilityProxy {
110 decision.CapabilityProxy = true
111 }
112 if c.capabilityAudit != nil {
113 c.capabilityAudit.RecordRoute(true, len(decision.Candidates) == before)
114 }
115 } else if c.capabilityAudit != nil {
116 c.capabilityAudit.RecordRoute(false, false)
117 }
118 if c.capabilityAudit != nil {
119 c.capabilityAudit.RecordDecision(decision)
120 }
121 return decision
122 }
123
124 // WireCapabilityRouting attaches hybrid routing helpers. Safe to call with nil
125 // semantic router (deterministic only). specs are the boot-converted plugin
126 // specs; their persisted schema caches are loaded once here so every routing
127 // turn can offer cached tools of not-yet-started servers.
128 func (c *Controller) WireCapabilityRouting(plugins []config.PluginEntry, specs []plugin.Spec, router *capability.SemanticRouter, audit *capability.Audit) {
129 if c == nil {
130 return
131 }
132 c.pluginCfg = append([]config.PluginEntry(nil), plugins...)
133 c.capCachedTools, c.capCacheKeyOK = capability.LoadCachedToolsForSpecs(specs, c.mcpHostProfile())
134 c.semanticRouter = router
135 c.capabilityAudit = audit
136 }
137
138 // SetCapabilityProxyRouting directs unready MCP route candidates to
139 // use_capability instead of connect_tool_source. Used by closed-loop routes and
140 // dual-model Planner boots.
141 func (c *Controller) SetCapabilityProxyRouting(v bool) {
142 if c == nil {
143 return
144 }
145 c.capabilityProxy = v
146 }
147
148 // SetCapabilityProxyTools registers a getter for live tools observed through
149 // use_capability without entering the provider-visible registry.
150 func (c *Controller) SetCapabilityProxyTools(fn func() map[string][]plugin.CachedTool) {
151 if c == nil {
152 return
153 }
154 c.proxyToolsFn = fn
155 }
156
156 lines GO