返回 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(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(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(_ 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 payload := map[string]any{
89 "id": e.ID,
90 "kind": e.Kind,
91 "name": e.Name,
92 "description": e.Description,
93 "status": e.Status,
94 "read_only": e.ReadOnly,
95 "auto_use": e.AutoUse,
96 "requires": e.Requires,
97 "profiles": e.Profiles,
98 "tool_name": e.ToolName,
99 "auto_start": e.AutoStart,
100 "network_call": false,
101 }
102 if strings.HasPrefix(id, "skill:") {
103 if contract, ok := capabilityArgumentContract(e); ok {
104 payload["input_schema"] = contract.Schema
105 payload["call_example"] = contract.Example
106 payload["schema_fingerprint"] = tool.SchemaFingerprint(contract.Schema)
107 }
108 }
109 if e.Kind == capability.KindMCPServer || e.Kind == capability.KindMCPTool {
110 t.decorateMCPInspect(payload, e)
111 }
112 return marshalBoundedInspect(payload), nil
113 }
114
115 func (t *UseCapabilityTool) decorateMCPInspect(payload map[string]any, e capability.Entry) {
116 server := e.Source
117 if server == "" {
118 server = e.ConnectName
119 }
120 if server == "" {
121 return
122 }
123 if !t.serverEnabled(server) {
124 payload["note"] = t.serverUnavailableReason(server)
125 return
126 }
127 tools, source := t.localMCPTools(server)
128 payload["source"] = source
129 schemaBytes := 0
130 for _, item := range tools {
131 schemaBytes += len(item.Schema)
132 }
133 if t.capabilityAudit() != nil {
134 t.capabilityAudit().RecordMCPList(source, "inspect", 0, len(tools), schemaBytes)
135 }
136 t.observeMCPList(mcpListObservation{
137 Server: server, Source: source, Trigger: "inspect",
138 ToolCount: len(tools), SchemaBytes: schemaBytes, NetworkCall: false,
139 })
140 if e.Kind == capability.KindMCPTool {
141 _, raw, err := parseMCPCapabilityID(e.ID)
142 if err != nil {
143 return
144 }
145 selected, selectedSource, found := t.localMCPTool(server, raw)
146 if !found {
147 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."
148 return
149 }
150 payload["source"] = selectedSource
151 payload["description"] = selected.Description
152 payload["read_only"] = selected.ReadOnly
153 payload["input_schema"] = selected.Schema
154 payload["schema_fingerprint"] = tool.SchemaFingerprint(selected.Schema)
155 payload["call_example"] = map[string]any{
156 "action": "call",
157 "capability_id": e.ID,
158 "arguments": map[string]any{},
159 }
160 return
161 }
162 if len(tools) == 0 {
163 payload["note"] = "Server not connected and no cached tool schema; call action=call on mcp-server:" + server + " to connect after authorization."
164 return
165 }
166 payload["tools"] = compactInspectToolList(server, tools)
167 payload["note"] = "Compact directory only. Inspect one mcp-tool capability_id to load its full input schema."
168 }
169
170 func (t *UseCapabilityTool) capabilityAudit() *capability.Audit {
171 return t.audit
172 }
173
174 type inspectToolInfo struct {
175 ID string `json:"id"`
176 Name string `json:"name"`
177 Description string `json:"description"`
178 ReadOnly bool `json:"read_only"`
179 Fingerprint string `json:"schema_fingerprint,omitempty"`
180 Schema json.RawMessage `json:"input_schema,omitempty"`
181 }
182
183 func compactInspectToolList(server string, tools []plugin.CachedTool) []inspectToolInfo {
184 list := make([]inspectToolInfo, 0, len(tools))
185 for _, candidate := range tools {
186 list = append(list, inspectToolInfo{
187 ID: "mcp-tool:" + server + "/" + candidate.Name,
188 Name: plugin.ModelToolName(server, candidate.Name),
189 Description: truncateSearchDescription(candidate.Description),
190 ReadOnly: candidate.ReadOnly,
191 Fingerprint: tool.SchemaFingerprint(candidate.Schema),
192 })
193 }
194 return list
195 }
196
197 func marshalBoundedInspect(payload map[string]any) string {
198 b, _ := json.MarshalIndent(payload, "", " ")
199 if len(b) <= maxInspectBytes {
200 return string(b)
201 }
202 if tools, ok := payload["tools"].([]inspectToolInfo); ok {
203 for len(tools) > 0 && len(b) > maxInspectBytes {
204 tools = tools[:len(tools)-1]
205 payload["tools"] = tools
206 payload["truncated"] = true
207 b, _ = json.MarshalIndent(payload, "", " ")
208 }
209 }
210 if len(b) > maxInspectBytes {
211 delete(payload, "input_schema")
212 payload["schema_omitted"] = "input schema exceeded the 16KB inspect response limit"
213 b, _ = json.MarshalIndent(payload, "", " ")
214 }
215 if len(b) > maxInspectBytes {
216 // Third-party descriptions and metadata are untrusted and may exceed the
217 // limit even after schemas/tool rows are removed. Preserve only bounded
218 // scalar identity fields; never return oversized or invalid JSON.
219 bounded := map[string]any{
220 "truncated": true,
221 "note": "inspect response exceeded the 16KB limit; narrow the capability_id or page the underlying result",
222 }
223 for _, key := range []string{"id", "kind", "name", "status", "source", "schema_fingerprint"} {
224 if value, ok := payload[key].(string); ok && value != "" {
225 bounded[key] = truncateInspectString(value, 1024)
226 }
227 }
228 for _, key := range []string{"read_only", "network_call", "auto_start"} {
229 if value, ok := payload[key].(bool); ok {
230 bounded[key] = value
231 }
232 }
233 b, _ = json.MarshalIndent(bounded, "", " ")
234 }
235 if len(b) > maxInspectBytes {
236 return `{"truncated":true,"note":"inspect response exceeded the 16KB limit"}`
237 }
238 return string(b)
239 }
240
241 func truncateInspectString(value string, limit int) string {
242 if len(value) <= limit {
243 return value
244 }
245 return value[:limit-len("...")] + "..."
246 }
247
248 // inspectToolListJSON renders the compact directory returned after a server
249 // connection. Exact schemas stay behind inspect(mcp-tool:server/tool).
250 func inspectToolListJSON(server string, tools []tool.Tool) string {
251 var list []inspectToolInfo
252 for _, tl := range tools {
253 raw := ""
254 if m, ok := tl.(tool.MCPMetadata); ok {
255 raw = m.MCPRawToolName()
256 }
257 list = append(list, inspectToolInfo{
258 ID: "mcp-tool:" + server + "/" + raw,
259 Name: tl.Name(),
260 Description: tl.Description(),
261 ReadOnly: tl.ReadOnly(),
262 Fingerprint: tool.SchemaFingerprint(tl.Schema()),
263 })
264 }
265 extra, _ := json.MarshalIndent(list, "", " ")
266 return string(extra)
267 }
268
268 lines GO