返回 DeepSeek-Reasonix
contract.go
根目录 / internal / tool / contract.go
1 package tool
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "sort"
8 "strings"
9
10 "reasonix/internal/provider"
11 )
12
13 // ContractEntry is the provider-visible contract for a tool schema snapshot.
14 type ContractEntry struct {
15 Name string
16 Description string
17 ReadOnly bool
18 Schema json.RawMessage
19 }
20
21 // BuiltinContractEntries returns a stable snapshot of compile-time built-ins.
22 func BuiltinContractEntries() []ContractEntry {
23 return contractEntriesFromTools(Builtins(), nil)
24 }
25
26 func contractEntriesFromTools(tools []Tool, canonical map[string]json.RawMessage) []ContractEntry {
27 entries := make([]ContractEntry, 0, len(tools))
28 for _, t := range tools {
29 schema := provider.CanonicalizeSchema(t.Schema())
30 if canonical != nil {
31 if c := canonical[t.Name()]; len(c) > 0 {
32 schema = append(json.RawMessage(nil), c...)
33 }
34 }
35 entries = append(entries, ContractEntry{
36 Name: t.Name(),
37 Description: strings.TrimSpace(t.Description()),
38 ReadOnly: t.ReadOnly(),
39 Schema: schema,
40 })
41 }
42 sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
43 return entries
44 }
45
46 // ContractEntries returns the registry's provider-visible contract snapshot.
47 // The tool list is captured under the lock, but the per-tool method calls
48 // (Schema/Description/ReadOnly) run AFTER it is released: a lazy MCP
49 // placeholder's ReadOnly takes the spawn mutex, and the spawn's trySwap takes
50 // this registry's write lock — holding the read lock across ReadOnly is an
51 // AB-BA deadlock (boot's snapshot assembly hit it with a live swap in flight).
52 func (r *Registry) ContractEntries() []ContractEntry {
53 return r.contractEntries(true, false)
54 }
55
56 // AllContractEntries returns every registered tool's contract, including tools
57 // hidden from provider and capability discovery. Diagnostics and host APIs use
58 // it when they need the complete registry inventory.
59 func (r *Registry) AllContractEntries() []ContractEntry {
60 return r.contractEntries(false, false)
61 }
62
63 // CapabilityContractEntries returns callable non-retired tools for the
64 // use_capability catalog. Compatibility aliases still resolve from old session
65 // calls but are intentionally absent from discovery.
66 func (r *Registry) CapabilityContractEntries() []ContractEntry {
67 return r.contractEntries(false, true)
68 }
69
70 func (r *Registry) contractEntries(providerVisibleOnly, capabilityCatalog bool) []ContractEntry {
71 r.mu.RLock()
72 tools := make([]Tool, 0, len(r.order))
73 canonical := make(map[string]json.RawMessage, len(r.order))
74 for _, name := range r.order {
75 if providerVisibleOnly && !r.isProviderVisibleLocked(name) {
76 continue
77 }
78 t := r.tools[name]
79 if t == nil {
80 continue
81 }
82 if capabilityCatalog {
83 if hidden, ok := t.(CapabilityCatalogHidden); ok && hidden.HiddenFromCapabilityCatalog() {
84 continue
85 }
86 }
87 tools = append(tools, t)
88 canonical[name] = r.canon[name]
89 }
90 r.mu.RUnlock()
91 return contractEntriesFromTools(tools, canonical)
92 }
93
94 // RenderContractMarkdown renders entries as committed documentation. Tests use
95 // the same entries, so docs drift when tool names, descriptions, read-only
96 // flags, or canonical schemas change.
97 func RenderContractMarkdown(entries []ContractEntry) string {
98 var b strings.Builder
99 b.WriteString("# Tool Contract\n\n")
100 b.WriteString("This document records the provider-visible contract for Reasonix compile-time built-in tools. It is generated from the same canonical schema path used by the runtime registry.\n\n")
101 b.WriteString("| Tool | Read-only | Description |\n")
102 b.WriteString("| --- | --- | --- |\n")
103 for _, e := range entries {
104 fmt.Fprintf(&b, "| `%s` | %t | %s |\n", e.Name, e.ReadOnly, markdownCell(e.Description))
105 }
106 b.WriteString("\n## Schemas\n")
107 for _, e := range entries {
108 fmt.Fprintf(&b, "\n### `%s`\n\n", e.Name)
109 fmt.Fprintf(&b, "- Read-only: `%t`\n", e.ReadOnly)
110 if e.Description != "" {
111 fmt.Fprintf(&b, "- Description: %s\n", e.Description)
112 }
113 b.WriteString("\n```json\n")
114 b.WriteString(prettyJSON(e.Schema))
115 b.WriteString("\n```\n")
116 }
117 return b.String()
118 }
119
120 func markdownCell(s string) string {
121 s = strings.ReplaceAll(s, "\n", " ")
122 s = strings.ReplaceAll(s, "|", `\|`)
123 return strings.Join(strings.Fields(s), " ")
124 }
125
126 func prettyJSON(raw json.RawMessage) string {
127 var out bytes.Buffer
128 if err := json.Indent(&out, raw, "", " "); err != nil {
129 return strings.TrimSpace(string(raw))
130 }
131 return out.String()
132 }
133
133 lines GO