package tools import ( "context" "encoding/json" "fmt" "log/slog" "sort" "time" coretypes "github.com/enmanuel/agents/pkg/llm" "github.com/enmanuel/agents/shell/logger" ) // Registry holds available tools keyed by name. type Registry struct { tools map[string]Tool logger *slog.Logger rateLimiter *RateLimiter // nil when rate limiting is disabled } // NewRegistry creates an empty registry. func NewRegistry(log *slog.Logger) *Registry { return &Registry{ tools: make(map[string]Tool), logger: log.With(logger.FieldComponent, "tools"), } } // Register adds a tool to the registry. func (r *Registry) Register(t Tool) { r.tools[t.Def.Name] = t r.logger.Debug("tool_registered", "name", t.Def.Name) } // Get looks up a tool by name. func (r *Registry) Get(name string) (Tool, bool) { t, ok := r.tools[name] return t, ok } // Names returns all registered tool names in sorted order. func (r *Registry) Names() []string { names := make([]string, 0, len(r.tools)) for k := range r.tools { names = append(names, k) } sort.Strings(names) return names } // Len returns the number of registered tools. func (r *Registry) Len() int { return len(r.tools) } // SetRateLimiter attaches a rate limiter to the registry. // When set, ExecuteForRoom checks the limit before running the tool. func (r *Registry) SetRateLimiter(rl *RateLimiter) { r.rateLimiter = rl } // ExecuteForRoom is like Execute but checks the per-room rate limit first. // If the rate limit is exceeded, it returns an error result without executing. func (r *Registry) ExecuteForRoom(ctx context.Context, name, argsJSON, roomID string) Result { if r.rateLimiter != nil && roomID != "" { if !r.rateLimiter.Allow(roomID) { r.logger.Warn("tool_rate_limited", "tool", name, "room", roomID) return Result{Err: fmt.Errorf("rate limit exceeded for room %s: too many tool calls per minute", roomID)} } } return r.Execute(ctx, name, argsJSON) } // Execute looks up a tool by name and runs it. Returns an error result if not found. func (r *Registry) Execute(ctx context.Context, name string, argsJSON string) Result { t, ok := r.tools[name] if !ok { r.logger.Warn("tool_not_found", "tool", name) return Result{Err: fmt.Errorf("tool %q not found", name)} } var args map[string]any if argsJSON != "" { if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { r.logger.Warn("tool_args_invalid", "tool", name, "err", err) return Result{Err: fmt.Errorf("parse args for %q: %w", name, err)} } } r.logger.Info("tool_exec_start", "tool", name) start := time.Now() result := t.Exec(ctx, args) ms := time.Since(start).Milliseconds() if result.Err != nil { r.logger.Warn("tool_exec_error", "tool", name, "err", result.Err, logger.FieldDurationMS, ms) } else { r.logger.Info("tool_exec_end", "tool", name, logger.FieldDurationMS, ms) } return result } // ToLLMSpecs converts all registered tools to the LLM-compatible ToolSpec format. // This is a pure transformation — no side effects. func (r *Registry) ToLLMSpecs() []coretypes.ToolSpec { specs := make([]coretypes.ToolSpec, 0, len(r.tools)) for _, name := range r.Names() { t := r.tools[name] specs = append(specs, defToLLMSpec(t.Def)) } return specs } // defToLLMSpec converts a pure Def to an LLM ToolSpec with JSON Schema. func defToLLMSpec(d Def) coretypes.ToolSpec { properties := make(map[string]any, len(d.Parameters)) required := make([]string, 0) for _, p := range d.Parameters { properties[p.Name] = map[string]any{ "type": p.Type, "description": p.Description, } if p.Required { required = append(required, p.Name) } } schema := map[string]any{ "type": "object", "properties": properties, } if len(required) > 0 { schema["required"] = required } return coretypes.ToolSpec{ Name: d.Name, Description: d.Description, InputSchema: schema, } }