Cletus plugin kit: rules for coding agents, specification, host API, style guide, worker SDK and cletus-plugin tool
This commit is contained in:
commit
9b19d3a8be
41 files changed
+4121
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user