返回 DeepSeek-Reasonix
usecapability_inspect.go
根目录 / internal / agent / usecapability_inspect.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "reasonix/internal/capability"
10 "reasonix/internal/plugin"
11 "reasonix/internal/tool"
12 )
13
14 const maxInspectBytes = 16 << 10
15
16 func (t *UseCapabilityTool) resolveDiscovery(ctx context.Context, p useCapabilityArgs, action, id string, base tool.ResolvedCall) (tool.ResolvedCall, error) {
17 switch action {
18 case "list":
19 out, err := t.listCapabilitiesPage(ctx, p.Limit, p.Cursor)
20 if err != nil {
21 if t.audit != nil {
22 t.audit.RecordMCPProxy(true, false, true)
23 }
24 return tool.ResolvedCall{}, err
25 }
26 if t.audit != nil {
27 t.audit.RecordMCPProxy(true, false, false)
28 t.audit.RecordCapabilityDiscovery("list", 0, len(out), false)
29 }
30 base.SkipExecute = true
31 base.Result = out
32 base.ReadOnly = true
33 return base, nil
34 case "search":
35 query := strings.TrimSpace(p.Query)
36 if query == "" {
37 return tool.ResolvedCall{}, capabilityInputErrorf("query is required for action=search")
38 }
39 out, resultCount, err := t.searchCapabilities(ctx, query, p.Limit)
40 if err != nil {
41 return tool.ResolvedCall{}, err
42 }
43 base.SkipExecute = true
44 base.Result = out
45 base.ReadOnly = true
46 if t.audit != nil {
47 t.audit.RecordCapabilityDiscovery("search", resultCount, len(out), false)
48 }
49 return base, nil
50 default:
51 if id == "" {
52 return tool.ResolvedCall{}, capabilityInputErrorf("capability_id is required for action=inspect")
53 }
54 if id == sessionToolResultCapabilityID {
55 out, err := t.inspectSessionToolResult()
56 if err != nil {
57 return tool.ResolvedCall{}, err
58 }
59 base.SkipExecute = true
60 base.Result = out
61 base.ReadOnly = true
62 return base, nil
63 }
64 out, err := t.inspect(ctx, id)
65 if err != nil {
66 if t.audit != nil {
67 t.audit.RecordMCPProxy(true, false, true)
68 }
69 return tool.ResolvedCall{}, err
70 }
71 if t.audit != nil {
72 t.audit.RecordMCPProxy(true, false, false)
73 t.audit.RecordCapabilityDiscovery("inspect", 1, len(out), false)
74 }
75 base.SkipExecute = true
76 base.Result = out
77 base.ReadOnly = true
78 return base, nil
79 }
80 }
81
82 func (t *UseCapabilityTool) inspect(ctx context.Context, id string) (string, error) {
83 cat := t.currentCatalog()
84 e, ok := cat.Lookup(id)
85 if !ok {
86 return "", fmt.Errorf("unknown capability_id %q", id)
87 }
88 e = t.contextualEntry(ctx, e)
89 payload := map[string]any{
90 "id": e.ID,
91 "kind": e.Kind,
92 "name": e.Name,
93 "description": e.Description,
94 "status": e.Status,
95 "read_only": e.ReadOnly,
96 "auto_use": e.AutoUse,
97 "requires": e.Requires,
98 "profiles": e.Profiles,
99 "tool_name": e.ToolName,
100 "auto_start": e.AutoStart,
101 "network_call": false,
102 }
103 if e.Kind == capability.KindTool && e.FailureReason != "" {
104 payload["unavailable_reason"] = e.FailureReason
105 }
106 if e.Kind == capability.KindTool && t.registry != nil {
107 if target, ok := t.registry.Get(e.ToolName); ok {
108 payload["input_schema"] = target.Schema()
109 payload["schema_fingerprint"] = tool.SchemaFingerprint(target.Schema())
110 }
111 }
112 if strings.HasPrefix(id, "skill:") {
113 if contract, ok := capabilityArgumentContract(e); ok {
114 payload["input_schema"] = contract.Schema
115 payload["call_example"] = contract.Example
116 payload["schema_fingerprint"] = tool.SchemaFingerprint(contract.Schema)
117 }
118 }
119 if e.Kind == capability.KindMCPServer || e.Kind == capability.KindMCPTool {
120 t.decorateMCPInspect(payload, e)
121 }
122 return marshalBoundedInspect(payload), nil
123 }
124
125 func (t *UseCapabilityTool) decorateMCPInspect(payload map[string]any, e capability.Entry) {
126 server := e.Source
127 if server == "" {
128 server = e.ConnectName
129 }
130 if server == "" {
131 return
132 }
133 if !t.serverEnabled(server) {
134 payload["note"] = t.serverUnavailableReason(server)
135 return
136 }
137 tools, source := t.localMCPTools(server)
138 payload["source"] = source
139 schemaBytes := 0
140 for _, item := range tools {
141 schemaBytes += len(item.Schema)
142 }
143 if t.capabilityAudit() != nil {
144 t.capabilityAudit().RecordMCPList(source, "inspect", 0, len(tools), schemaBytes)
145 }
146 t.observeMCPList(mcpListObservation{
147 Server: server, Source: source, Trigger: "inspect",
148 ToolCount: len(tools), SchemaBytes: schemaBytes, NetworkCall: false,
149 })
150 if e.Kind == capability.KindMCPTool {
151 _, raw, err := parseMCPCapabilityID(e.ID)
152 if err != nil {
153 return
154 }
155 selected, selectedSource, found := t.localMCPTool(server, raw)
156 if !found {
157 payload["note"] = "Exact schema is not present in the shared-host or disk cache. Call the server capability once to connect, then inspect this exact tool."
158 return
159 }
160 payload["source"] = selectedSource
161 payload["description"] = selected.Description
162 payload["read_only"] = selected.ReadOnly
163 payload["input_schema"] = selected.Schema
164 payload["schema_fingerprint"] = tool.SchemaFingerprint(selected.Schema)
165 payload["call_example"] = map[string]any{
166 "action": "call",
167 "capability_id": e.ID,
168 "arguments": map[string]any{},
169 }
170 return
171 }
172 if len(tools) == 0 {
173 payload["note"] = "Server not connected and no cached tool schema; call action=call on mcp-server:" + server + " to connect after authorization."
174 return
175 }
176 payload["tools"] = compactInspectToolList(server, tools)
177 payload["note"] = "Compact directory only. Inspect one mcp-tool capability_id to load its full input schema."
178 }
179
180 func (t *UseCapabilityTool) capabilityAudit() *capability.Audit {
181 return t.audit
182 }
183
184 type inspectToolInfo struct {
185 ID string `json:"id"`
186 Name string `json:"name"`
187 Description string `json:"description"`
188 ReadOnly bool `json:"read_only"`
189 Fingerprint string `json:"schema_fingerprint,omitempty"`
190 Schema json.RawMessage `json:"input_schema,omitempty"`
191 }
192
193 func compactInspectToolList(server string, tools []plugin.CachedTool) []inspectToolInfo {
194 list := make([]inspectToolInfo, 0, len(tools))
195 for _, candidate := range tools {
196 list = append(list, inspectToolInfo{
197 ID: "mcp-tool:" + server + "/" + candidate.Name,
198 Name: plugin.ModelToolName(server, candidate.Name),
199 Description: truncateSearchDescription(candidate.Description),
200 ReadOnly: candidate.ReadOnly,
201 Fingerprint: tool.SchemaFingerprint(candidate.Schema),
202 })
203 }
204 return list
205 }
206
207 func marshalBoundedInspect(payload map[string]any) string {
208 b, _ := json.MarshalIndent(payload, "", " ")
209 if len(b) <= maxInspectBytes {
210 return string(b)
211 }
212 if tools, ok := payload["tools"].([]inspectToolInfo); ok {
213 for len(tools) > 0 && len(b) > maxInspectBytes {
214 tools = tools[:len(tools)-1]
215 payload["tools"] = tools
216 payload["truncated"] = true
217 b, _ = json.MarshalIndent(payload, "", " ")
218 }
219 }
220 if len(b) > maxInspectBytes {
221 delete(payload, "input_schema")
222 payload["schema_omitted"] = "input schema exceeded the 16KB inspect response limit"
223 b, _ = json.MarshalIndent(payload, "", " ")
224 }
225 if len(b) > maxInspectBytes {
226 // Third-party descriptions and metadata are untrusted and may exceed the
227 // limit even after schemas/tool rows are removed. Preserve only bounded
228 // scalar identity fields; never return oversized or invalid JSON.
229 bounded := map[string]any{
230 "truncated": true,
231 "note": "inspect response exceeded the 16KB limit; narrow the capability_id or page the underlying result",
232 }
233 for _, key := range []string{"id", "kind", "name", "status", "source", "schema_fingerprint"} {
234 if value, ok := payload[key].(string); ok && value != "" {
235 bounded[key] = truncateInspectString(value, 1024)
236 }
237 }
238 for _, key := range []string{"read_only", "network_call", "auto_start"} {
239 if value, ok := payload[key].(bool); ok {
240 bounded[key] = value
241 }
242 }
243 b, _ = json.MarshalIndent(bounded, "", " ")
244 }
245 if len(b) > maxInspectBytes {
246 return `{"truncated":true,"note":"inspect response exceeded the 16KB limit"}`
247 }
248 return string(b)
249 }
250
251 func truncateInspectString(value string, limit int) string {
252 if len(value) <= limit {
253 return value
254 }
255 return value[:limit-len("...")] + "..."
256 }
257
258 // inspectToolListJSON renders the compact directory returned after a server
259 // connection. Exact schemas stay behind inspect(mcp-tool:server/tool).
260 func inspectToolListJSON(server string, tools []tool.Tool) string {
261 var list []inspectToolInfo
262 for _, tl := range tools {
263 raw := ""
264 if m, ok := tl.(tool.MCPMetadata); ok {
265 raw = m.MCPRawToolName()
266 }
267 list = append(list, inspectToolInfo{
268 ID: "mcp-tool:" + server + "/" + raw,
269 Name: tl.Name(),
270 Description: tl.Description(),
271 ReadOnly: tl.ReadOnly(),
272 Fingerprint: tool.SchemaFingerprint(tl.Schema()),
273 })
274 }
275 extra, _ := json.MarshalIndent(list, "", " ")
276 return string(extra)
277 }
278
278 lines GO