summaryrefslogtreecommitdiff
path: root/internal/executor/localtools.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor/localtools.go')
-rw-r--r--internal/executor/localtools.go152
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)
}