diff options
Diffstat (limited to 'internal/executor/localtools.go')
| -rw-r--r-- | internal/executor/localtools.go | 152 |
1 files changed, 148 insertions, 4 deletions
diff --git a/internal/executor/localtools.go b/internal/executor/localtools.go index 48b862f..9fd2cfc 100644 --- a/internal/executor/localtools.go +++ b/internal/executor/localtools.go @@ -1,10 +1,14 @@ package executor import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "os" + "os/exec" + "path/filepath" "github.com/thepeterstone/claudomator/internal/llm" ) @@ -61,14 +65,54 @@ func agentToolDefs() []llm.Tool { "required": []string{"message"}, }, }}, + {Type: "function", Function: llm.ToolFunction{ + Name: "read_file", + Description: "Read the contents of a file in the sandbox working directory.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{"path": strProp("path to the file, relative or absolute")}, + "required": []string{"path"}, + }, + }}, + {Type: "function", Function: llm.ToolFunction{ + Name: "write_file", + Description: "Write or overwrite a file in the sandbox working directory.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "path": strProp("path to the file, relative or absolute"), + "content": strProp("content to write"), + }, + "required": []string{"path", "content"}, + }, + }}, + {Type: "function", Function: llm.ToolFunction{ + Name: "run_bash", + Description: "Run a shell command in the sandbox working directory. Returns exit code, stdout, and stderr.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{"command": strProp("shell command to execute")}, + "required": []string{"command"}, + }, + }}, + {Type: "function", Function: llm.ToolFunction{ + Name: "glob", + Description: "List files matching a glob pattern relative to the sandbox working directory.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{"pattern": strProp("glob pattern, e.g. **/*.go")}, + "required": []string{"pattern"}, + }, + }}, } } // dispatchAgentTool invokes one model-requested tool against the AgentChannel. -// It returns the text to feed back to the model as the tool result, and a -// blocked flag set when ask_user could not be answered in-session (the run must -// stop and the task block). -func dispatchAgentTool(ctx context.Context, ch AgentChannel, name, argsJSON string) (result string, blocked bool, err error) { +// workDir is the sandbox working directory; file/bash tools return an error if +// it is empty. It returns the text to feed back to the model as the tool +// result, and a blocked flag set when ask_user could not be answered in-session +// (the run must stop and the task block). +func dispatchAgentTool(ctx context.Context, ch AgentChannel, workDir, name, argsJSON string) (result string, blocked bool, err error) { switch name { case "ask_user": var a struct { @@ -129,6 +173,106 @@ func dispatchAgentTool(ctx context.Context, ch AgentChannel, name, argsJSON stri } return "Noted.", false, nil + case "read_file": + if workDir == "" { + return "", false, fmt.Errorf("read_file: no sandbox working directory") + } + var a struct { + Path string `json:"path"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + p := a.Path + if !filepath.IsAbs(p) { + p = filepath.Join(workDir, p) + } + data, readErr := os.ReadFile(p) + if readErr != nil { + return "", false, readErr + } + return string(data), false, nil + + case "write_file": + if workDir == "" { + return "", false, fmt.Errorf("write_file: no sandbox working directory") + } + var a struct { + Path string `json:"path"` + Content string `json:"content"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + p := a.Path + if !filepath.IsAbs(p) { + p = filepath.Join(workDir, p) + } + if mkErr := os.MkdirAll(filepath.Dir(p), 0o755); mkErr != nil { + return "", false, mkErr + } + if writeErr := os.WriteFile(p, []byte(a.Content), 0o644); writeErr != nil { + return "", false, writeErr + } + return "Written.", false, nil + + case "run_bash": + if workDir == "" { + return "", false, fmt.Errorf("run_bash: no sandbox working directory") + } + var a struct { + Command string `json:"command"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + cmd := exec.CommandContext(ctx, "sh", "-c", a.Command) + cmd.Dir = workDir + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + runErr := cmd.Run() + exitCode := 0 + if runErr != nil { + if exitErr, ok := runErr.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } else { + return "", false, runErr + } + } + const maxOutput = 8 * 1024 + out := stdout.String() + errOut := stderr.String() + if len(out) > maxOutput { + out = out[:maxOutput] + "\n[truncated]" + } + if len(errOut) > maxOutput { + errOut = errOut[:maxOutput] + "\n[truncated]" + } + return fmt.Sprintf("exit_code: %d\nstdout:\n%s\nstderr:\n%s", exitCode, out, errOut), false, nil + + case "glob": + if workDir == "" { + return "", false, fmt.Errorf("glob: no sandbox working directory") + } + var a struct { + Pattern string `json:"pattern"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + pattern := a.Pattern + if !filepath.IsAbs(pattern) { + pattern = filepath.Join(workDir, pattern) + } + matches, globErr := filepath.Glob(pattern) + if globErr != nil { + return "", false, globErr + } + // Return paths relative to workDir for readability. + rel := make([]string, 0, len(matches)) + for _, m := range matches { + r, relErr := filepath.Rel(workDir, m) + if relErr != nil { + r = m + } + rel = append(rel, r) + } + out, _ := json.Marshal(rel) + return string(out), false, nil + default: return "", false, fmt.Errorf("unknown tool %q", name) } |
