-
Notifications
You must be signed in to change notification settings - Fork 4k
Expand file tree
/
Copy pathbuilder.go
More file actions
353 lines (316 loc) · 11.7 KB
/
builder.go
File metadata and controls
353 lines (316 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
package inventory
import (
"context"
"sort"
"strings"
)
// ToolFilter is a function that determines if a tool should be included.
// Returns true if the tool should be included, false to exclude it.
type ToolFilter func(ctx context.Context, tool *ServerTool) (bool, error)
// Builder builds a Registry with the specified configuration.
// Use NewBuilder to create a builder, chain configuration methods,
// then call Build() to create the final inventory.
//
// Example:
//
// reg := NewBuilder().
// SetTools(tools).
// SetResources(resources).
// SetPrompts(prompts).
// WithDeprecatedAliases(aliases).
// WithReadOnly(true).
// WithToolsets([]string{"repos", "issues"}).
// WithFeatureChecker(checker).
// WithFilter(myFilter).
// Build()
type Builder struct {
tools []ServerTool
resourceTemplates []ServerResourceTemplate
prompts []ServerPrompt
deprecatedAliases map[string]string
// Configuration options (processed at Build time)
readOnly bool
toolsetIDs []string // raw input, processed at Build()
toolsetIDsIsNil bool // tracks if nil was passed (nil = defaults)
additionalTools []string // raw input, processed at Build()
featureChecker FeatureFlagChecker
filters []ToolFilter // filters to apply to all tools
}
// NewBuilder creates a new Builder.
func NewBuilder() *Builder {
return &Builder{
deprecatedAliases: make(map[string]string),
toolsetIDsIsNil: true, // default to nil (use defaults)
}
}
// SetTools sets the tools for the inventory. Returns self for chaining.
func (b *Builder) SetTools(tools []ServerTool) *Builder {
b.tools = tools
return b
}
// SetResources sets the resource templates for the inventory. Returns self for chaining.
func (b *Builder) SetResources(resources []ServerResourceTemplate) *Builder {
b.resourceTemplates = resources
return b
}
// SetPrompts sets the prompts for the inventory. Returns self for chaining.
func (b *Builder) SetPrompts(prompts []ServerPrompt) *Builder {
b.prompts = prompts
return b
}
// WithDeprecatedAliases adds deprecated tool name aliases that map to canonical names.
// Returns self for chaining.
func (b *Builder) WithDeprecatedAliases(aliases map[string]string) *Builder {
for oldName, newName := range aliases {
b.deprecatedAliases[oldName] = newName
}
return b
}
// WithReadOnly sets whether only read-only tools should be available.
// When true, write tools are filtered out. Returns self for chaining.
func (b *Builder) WithReadOnly(readOnly bool) *Builder {
b.readOnly = readOnly
return b
}
// WithToolsets specifies which toolsets should be enabled.
// Special keywords:
// - "all": enables all toolsets
// - "default": expands to toolsets marked with Default: true in their metadata
//
// Input strings are trimmed of whitespace and duplicates are removed.
// Pass nil to use default toolsets. Pass an empty slice to disable all toolsets
// (useful for dynamic toolsets mode where tools are enabled on demand).
// Returns self for chaining.
func (b *Builder) WithToolsets(toolsetIDs []string) *Builder {
b.toolsetIDs = toolsetIDs
b.toolsetIDsIsNil = toolsetIDs == nil
return b
}
// WithTools specifies additional tools that bypass toolset filtering.
// These tools are additive - they will be included even if their toolset is not enabled.
// Read-only filtering still applies to these tools.
// Deprecated tool aliases are automatically resolved to their canonical names during Build().
// Returns self for chaining.
func (b *Builder) WithTools(toolNames []string) *Builder {
b.additionalTools = toolNames
return b
}
// WithFeatureChecker sets the feature flag checker function.
// The checker receives a context (for actor extraction) and feature flag name,
// returns (enabled, error). If error occurs, it will be logged and treated as false.
// If checker is nil, all feature flag checks return false.
// Returns self for chaining.
func (b *Builder) WithFeatureChecker(checker FeatureFlagChecker) *Builder {
b.featureChecker = checker
return b
}
// WithFilter adds a filter function that will be applied to all tools.
// Multiple filters can be added and are evaluated in order.
// If any filter returns false or an error, the tool is excluded.
// Returns self for chaining.
func (b *Builder) WithFilter(filter ToolFilter) *Builder {
b.filters = append(b.filters, filter)
return b
}
// Build creates the final Inventory with all configuration applied.
// This processes toolset filtering, tool name resolution, compiles EnableConditions
// for O(1) evaluation, pre-sorts all items for deterministic output, and sets up
// the inventory for use. The returned Inventory is ready for use with
// AvailableTools(), RegisterAll(), etc.
func (b *Builder) Build() *Inventory {
// Pre-sort tools, resources, and prompts at build time.
// This eliminates sorting overhead on every Available*() call.
// Filtering preserves order, so if input is sorted, output is sorted.
sortedTools := b.preSortTools()
sortedResources := b.preSortResources()
sortedPrompts := b.preSortPrompts()
r := &Inventory{
tools: sortedTools,
resourceTemplates: sortedResources,
prompts: sortedPrompts,
deprecatedAliases: b.deprecatedAliases,
readOnly: b.readOnly,
featureChecker: b.featureChecker,
filters: b.filters,
}
// Process toolsets and pre-compute metadata in a single pass
r.enabledToolsets, r.unrecognizedToolsets, r.toolsetIDs, r.toolsetIDSet, r.defaultToolsetIDs, r.toolsetDescriptions = b.processToolsets()
// Process additional tools (resolve aliases)
if len(b.additionalTools) > 0 {
r.additionalTools = make(map[string]bool, len(b.additionalTools))
for _, name := range b.additionalTools {
// Resolve deprecated aliases to canonical names
if canonical, isAlias := b.deprecatedAliases[name]; isAlias {
r.additionalTools[canonical] = true
} else {
r.additionalTools[name] = true
}
}
}
// Compile EnableConditions for O(1) bitmask evaluation
// Note: compileConditions uses r.tools which is now sortedTools
r.conditionCompiler, r.compiledConditions = b.compileConditions(sortedTools)
return r
}
// preSortTools returns a copy of tools sorted by toolset ID, then tool name.
// This allows filtering to preserve order without re-sorting.
func (b *Builder) preSortTools() []ServerTool {
if len(b.tools) == 0 {
return b.tools
}
sorted := make([]ServerTool, len(b.tools))
copy(sorted, b.tools)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Toolset.ID != sorted[j].Toolset.ID {
return sorted[i].Toolset.ID < sorted[j].Toolset.ID
}
return sorted[i].Tool.Name < sorted[j].Tool.Name
})
return sorted
}
// preSortResources returns a copy of resources sorted by toolset ID, then template name.
func (b *Builder) preSortResources() []ServerResourceTemplate {
if len(b.resourceTemplates) == 0 {
return b.resourceTemplates
}
sorted := make([]ServerResourceTemplate, len(b.resourceTemplates))
copy(sorted, b.resourceTemplates)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Toolset.ID != sorted[j].Toolset.ID {
return sorted[i].Toolset.ID < sorted[j].Toolset.ID
}
return sorted[i].Template.Name < sorted[j].Template.Name
})
return sorted
}
// preSortPrompts returns a copy of prompts sorted by toolset ID, then prompt name.
func (b *Builder) preSortPrompts() []ServerPrompt {
if len(b.prompts) == 0 {
return b.prompts
}
sorted := make([]ServerPrompt, len(b.prompts))
copy(sorted, b.prompts)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Toolset.ID != sorted[j].Toolset.ID {
return sorted[i].Toolset.ID < sorted[j].Toolset.ID
}
return sorted[i].Prompt.Name < sorted[j].Prompt.Name
})
return sorted
}
// compileConditions compiles all EnableConditions into bitmask-based evaluators.
// Returns the compiler (for building request masks) and compiled conditions slice.
// Takes the sorted tools slice to ensure compiled conditions align with sorted order.
func (b *Builder) compileConditions(sortedTools []ServerTool) (*ConditionCompiler, []*CompiledCondition) {
compiler := NewConditionCompiler()
compiled := make([]*CompiledCondition, len(sortedTools))
for i := range sortedTools {
if sortedTools[i].EnableCondition != nil {
compiled[i] = compiler.Compile(sortedTools[i].EnableCondition)
}
// nil means no condition (always enabled from condition perspective)
}
compiler.Freeze()
return compiler, compiled
}
// processToolsets processes the toolsetIDs configuration and returns:
// - enabledToolsets map (nil means all enabled)
// - unrecognizedToolsets list for warnings
// - allToolsetIDs sorted list of all toolset IDs
// - toolsetIDSet map for O(1) HasToolset lookup
// - defaultToolsetIDs sorted list of default toolset IDs
// - toolsetDescriptions map of toolset ID to description
func (b *Builder) processToolsets() (map[ToolsetID]bool, []string, []ToolsetID, map[ToolsetID]bool, []ToolsetID, map[ToolsetID]string) {
// Single pass: collect all toolset metadata together
validIDs := make(map[ToolsetID]bool)
defaultIDs := make(map[ToolsetID]bool)
descriptions := make(map[ToolsetID]string)
for i := range b.tools {
t := &b.tools[i]
validIDs[t.Toolset.ID] = true
if t.Toolset.Default {
defaultIDs[t.Toolset.ID] = true
}
if t.Toolset.Description != "" {
descriptions[t.Toolset.ID] = t.Toolset.Description
}
}
for i := range b.resourceTemplates {
r := &b.resourceTemplates[i]
validIDs[r.Toolset.ID] = true
if r.Toolset.Default {
defaultIDs[r.Toolset.ID] = true
}
if r.Toolset.Description != "" {
descriptions[r.Toolset.ID] = r.Toolset.Description
}
}
for i := range b.prompts {
p := &b.prompts[i]
validIDs[p.Toolset.ID] = true
if p.Toolset.Default {
defaultIDs[p.Toolset.ID] = true
}
if p.Toolset.Description != "" {
descriptions[p.Toolset.ID] = p.Toolset.Description
}
}
// Build sorted slices from the collected maps
allToolsetIDs := make([]ToolsetID, 0, len(validIDs))
for id := range validIDs {
allToolsetIDs = append(allToolsetIDs, id)
}
sort.Slice(allToolsetIDs, func(i, j int) bool { return allToolsetIDs[i] < allToolsetIDs[j] })
defaultToolsetIDList := make([]ToolsetID, 0, len(defaultIDs))
for id := range defaultIDs {
defaultToolsetIDList = append(defaultToolsetIDList, id)
}
sort.Slice(defaultToolsetIDList, func(i, j int) bool { return defaultToolsetIDList[i] < defaultToolsetIDList[j] })
toolsetIDs := b.toolsetIDs
// Check for "all" keyword - enables all toolsets
for _, id := range toolsetIDs {
if strings.TrimSpace(id) == "all" {
return nil, nil, allToolsetIDs, validIDs, defaultToolsetIDList, descriptions // nil means all enabled
}
}
// nil means use defaults, empty slice means no toolsets
if b.toolsetIDsIsNil {
toolsetIDs = []string{"default"}
}
// Expand "default" keyword, trim whitespace, collect other IDs, and track unrecognized
seen := make(map[ToolsetID]bool)
expanded := make([]ToolsetID, 0, len(toolsetIDs))
var unrecognized []string
for _, id := range toolsetIDs {
trimmed := strings.TrimSpace(id)
if trimmed == "" {
continue
}
if trimmed == "default" {
for _, defaultID := range defaultToolsetIDList {
if !seen[defaultID] {
seen[defaultID] = true
expanded = append(expanded, defaultID)
}
}
} else {
tsID := ToolsetID(trimmed)
if !seen[tsID] {
seen[tsID] = true
expanded = append(expanded, tsID)
// Track if this toolset doesn't exist
if !validIDs[tsID] {
unrecognized = append(unrecognized, trimmed)
}
}
}
}
if len(expanded) == 0 {
return make(map[ToolsetID]bool), unrecognized, allToolsetIDs, validIDs, defaultToolsetIDList, descriptions
}
enabledToolsets := make(map[ToolsetID]bool, len(expanded))
for _, id := range expanded {
enabledToolsets[id] = true
}
return enabledToolsets, unrecognized, allToolsetIDs, validIDs, defaultToolsetIDList, descriptions
}