返回 DeepSeek-Reasonix
capability_gate.go
根目录 / internal / agent / capability_gate.go
1 package agent
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "strings"
8
9 "reasonix/internal/capability"
10 "reasonix/internal/skill"
11 )
12
13 // capabilityGateState is one user turn's gate memory, scoped to the same turn
14 // as the ledger it reads: whether the prefer reminder has already been spent,
15 // and which kind of miss was reported, so a later clean gate is audited as a
16 // recovery instead of a first pass. Zeroing the struct is the turn reset.
17 type capabilityGateState struct {
18 }
19
20 // SeedCapabilityRoute installs the turn's route decision into the capability ledger.
21 func (a *Agent) SeedCapabilityRoute(decision capability.RouteDecision) {
22 if a == nil {
23 return
24 }
25 if a.capabilityLedger == nil {
26 a.capabilityLedger = capability.NewLedger()
27 }
28 a.capabilityLedger.Reset()
29 a.capabilityLedger.SeedCandidates(decision)
30 a.capabilityGate = capabilityGateState{}
31 }
32
33 // CapabilityLedger returns the turn-scoped capability ledger (may be nil).
34 func (a *Agent) CapabilityLedger() *capability.Ledger {
35 if a == nil {
36 return nil
37 }
38 return a.capabilityLedger
39 }
40
41 // CapabilityAudit returns the non-persisted capability metrics sink (may be nil).
42 func (a *Agent) CapabilityAudit() *capability.Audit {
43 if a == nil {
44 return nil
45 }
46 return a.capabilityAudit
47 }
48
49 func (a *Agent) noteCapabilityInvocation(toolName string, args json.RawMessage, callErr error) {
50 if a == nil || a.capabilityLedger == nil {
51 return
52 }
53 // Successful/failed proxied MCP calls execute the resolved target
54 // directly, so this is the single audit point for action=call (inspect,
55 // decline, and resolve-time unavailability are counted in ResolveCall,
56 // which returns before this runs).
57 if toolName == "use_capability" && a.capabilityAudit != nil {
58 var p struct {
59 Action string `json:"action"`
60 }
61 _ = json.Unmarshal(args, &p)
62 if strings.EqualFold(strings.TrimSpace(p.Action), "call") {
63 a.capabilityAudit.RecordMCPProxy(false, true, callErr != nil)
64 }
65 }
66 id := capabilityIDFromToolCall(toolName, args)
67 if id == "" {
68 return
69 }
70 if callErr != nil {
71 a.capabilityLedger.MarkFailed(id, callErr.Error())
72 if a.capabilityAudit != nil && strings.HasPrefix(id, "skill:") {
73 a.capabilityAudit.RecordSkill(true, errors.Is(callErr, skill.ErrInvocationUnavailable))
74 }
75 return
76 }
77 a.capabilityLedger.MarkSucceeded(id)
78 if a.capabilityAudit != nil && strings.HasPrefix(id, "skill:") {
79 a.capabilityAudit.RecordSkill(false, false)
80 }
81 }
82
83 func capabilityIDFromToolCall(toolName string, args json.RawMessage) string {
84 switch toolName {
85 case "run_skill", "read_skill", "read_only_skill", "explore", "research", "review", "security_review":
86 var p struct {
87 Name string `json:"name"`
88 }
89 _ = json.Unmarshal(args, &p)
90 name := strings.TrimSpace(p.Name)
91 if name == "" {
92 // Dedicated wrappers use the tool name as the skill name.
93 switch toolName {
94 case "explore", "research", "review", "security_review":
95 name = toolName
96 }
97 }
98 if name == "security_review" {
99 name = "security-review"
100 }
101 if name == "" {
102 return ""
103 }
104 return "skill:" + name
105 case "use_capability":
106 var p struct {
107 CapabilityID string `json:"capability_id"`
108 }
109 _ = json.Unmarshal(args, &p)
110 return strings.TrimSpace(p.CapabilityID)
111 default:
112 if server, raw, ok := splitMCP(toolName); ok {
113 return "mcp-tool:" + server + "/" + raw
114 }
115 }
116 return ""
117 }
118
119 func splitMCP(name string) (server, raw string, ok bool) {
120 const prefix = "mcp__"
121 if !strings.HasPrefix(name, prefix) {
122 return "", "", false
123 }
124 rest := name[len(prefix):]
125 parts := strings.SplitN(rest, "__", 2)
126 if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
127 return "", "", false
128 }
129 return parts[0], parts[1], true
130 }
131
132 // ReviewWarnings returns warn-level review findings collected this turn.
133 func (a *Agent) ReviewWarnings() []string {
134 if a == nil {
135 return nil
136 }
137 return append([]string(nil), a.turn.reviewWarnings...)
138 }
139
140 // FormatReviewWarningsForSummary builds a short appendix for the final answer.
141 func FormatReviewWarningsForSummary(warnings []string) string {
142 if len(warnings) == 0 {
143 return ""
144 }
145 return "Review warnings:\n- " + strings.Join(warnings, "\n- ")
146 }
147
148 // ensure string used
149 var _ = fmt.Sprintf
150
150 lines GO