package pluginsdk import ( "context" "encoding/json" "fmt" "sort" ) // Tool is a chat tool: a function Cletus's AI can call. Name, Mutating and // RequiresApproval must match the plugin's entry in plugin.json "tools". type Tool struct { Name string Description string // Parameters is the JSON schema of the arguments (an object schema). Parameters map[string]any // Mutating is true if the tool writes anything anywhere. Mutating bool // RequiresApproval makes Cletus ask the user before every call. RequiresApproval bool Run func(ctx context.Context, h *Host, call ToolCall) ToolResult } // ToolCall is one call of a chat tool. type ToolCall struct { // Args is the JSON object of arguments the AI sent. Args json.RawMessage // WorkingDir is the user's working folder ("" when none is open). WorkingDir string // EditMode is false when the user has Cletus read-only: a mutating tool // must check Host.CanWrite before each write and refuse when it's false. EditMode bool } // Decode decodes the arguments into v. func (c ToolCall) Decode(v any) error { if len(c.Args) == 0 { return json.Unmarshal([]byte("{}"), v) } if err := json.Unmarshal(c.Args, v); err != nil { return fmt.Errorf("the arguments don't match the tool's parameters: %w", err) } return nil } // ToolResult is what the AI reads back. Set Text on success, Error on // failure (a plain sentence with the next step). Images are optional // (PNG or JPEG bytes). type ToolResult struct { Text string `json:"Text,omitempty"` Error string `json:"Error,omitempty"` Images [][]byte `json:"Images,omitempty"` } // Text returns a successful result. func Text(format string, args ...any) ToolResult { return ToolResult{Text: fmt.Sprintf(format, args...)} } // Fail returns a failed result. func Fail(format string, args ...any) ToolResult { return ToolResult{Error: fmt.Sprintf(format, args...)} } type toolDescriptor struct { Def toolDef `json:"def"` Mutating bool `json:"mutating"` RequiresApproval bool `json:"requires_approval"` } type toolDef struct { Type string `json:"type"` Function functionDef `json:"function"` } type functionDef struct { Name string `json:"name"` Description string `json:"description"` Parameters map[string]any `json:"parameters"` } func (s *server) listTools() []toolDescriptor { out := make([]toolDescriptor, 0, len(s.tools)) for _, t := range s.tools { params := t.Parameters if params == nil { params = map[string]any{"type": "object", "properties": map[string]any{}} } out = append(out, toolDescriptor{ Def: toolDef{Type: "function", Function: functionDef{Name: t.Name, Description: t.Description, Parameters: params}}, Mutating: t.Mutating, RequiresApproval: t.RequiresApproval, }) } sort.Slice(out, func(i, j int) bool { return out[i].Def.Function.Name < out[j].Def.Function.Name }) return out } type toolInvocation struct { Name string `json:"name"` Args string `json:"args"` WorkingDir string `json:"working_dir"` EditMode bool `json:"edit_mode"` } func (s *server) invokeTool(ctx context.Context, h *Host, params json.RawMessage) (ToolResult, error) { var inv toolInvocation if err := json.Unmarshal(params, &inv); err != nil { return ToolResult{}, fmt.Errorf("invalid tool call: %w", err) } t, ok := s.tools[inv.Name] if !ok || t.Run == nil { return Fail("This plugin has no tool named %q.", inv.Name), nil } return t.Run(ctx, h, ToolCall{Args: json.RawMessage(inv.Args), WorkingDir: inv.WorkingDir, EditMode: inv.EditMode}), nil }