Cletus plugin kit: rules for coding agents, specification, host API, style guide, worker SDK and cletus-plugin tool

This commit is contained in:
Eidonix committed 2026-11-10 19:50:09 -05:00
commit 9b19d3a8be
41 files changed
+4121

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
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
}