Skip to content

Commit be1d5b9

Browse files
authored
Merge pull request #89 from vvoland/post-edit
tools/filesystem: Add post-edit command support
2 parents b926f60 + 3f8b6db commit be1d5b9

5 files changed

Lines changed: 170 additions & 3 deletions

File tree

examples/post_edit.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
agents:
2+
go_developer:
3+
description: A Go developer agent with post-edit formatting
4+
model: gpt-4
5+
instruction: |
6+
You are a Go developer assistant.
7+
toolsets:
8+
- type: filesystem
9+
post_edit:
10+
- path: "*.go"
11+
cmd: "gofmt -w $path"
12+
13+
models:
14+
gpt-4:
15+
provider: openai
16+
model: gpt-4o
17+
temperature: 0.1

pkg/config/v1/types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ type ScriptShellToolConfig struct {
2222
WorkingDir string `json:"working_dir,omitempty" yaml:"working_dir,omitempty"`
2323
}
2424

25+
// PostEditConfig represents a post-edit command configuration
26+
type PostEditConfig struct {
27+
Path string `json:"path" yaml:"path"`
28+
Cmd string `json:"cmd" yaml:"cmd"`
29+
}
30+
2531
// Toolset represents a tool configuration
2632
type Toolset struct {
2733
Type string `json:"type,omitempty" yaml:"type,omitempty"`
@@ -41,6 +47,9 @@ type Toolset struct {
4147

4248
// For the script tool
4349
Shell map[string]ScriptShellToolConfig `json:"shell,omitempty" yaml:"shell,omitempty"`
50+
51+
// For the filesystem tool - post-edit commands
52+
PostEdit []PostEditConfig `json:"post_edit,omitempty" yaml:"post_edit,omitempty"`
4453
}
4554

4655
type Remote struct {

pkg/teamloader/teamloader.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,19 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri
286286
return nil, fmt.Errorf("failed to get working directory: %w", err)
287287
}
288288

289-
t = append(t, builtin.NewFilesystemTool([]string{wd}, builtin.WithAllowedTools(toolset.Tools)))
289+
opts := []builtin.FileSystemOpt{builtin.WithAllowedTools(toolset.Tools)}
290+
if len(toolset.PostEdit) > 0 {
291+
postEditConfigs := make([]builtin.PostEditConfig, len(toolset.PostEdit))
292+
for i, pe := range toolset.PostEdit {
293+
postEditConfigs[i] = builtin.PostEditConfig{
294+
Path: pe.Path,
295+
Cmd: pe.Cmd,
296+
}
297+
}
298+
opts = append(opts, builtin.WithPostEditCommands(postEditConfigs))
299+
}
300+
301+
t = append(t, builtin.NewFilesystemTool([]string{wd}, opts...))
290302

291303
case toolset.Type == "mcp" && toolset.Ref != "":
292304
t = append(t, mcp.NewGatewayToolset(toolset.Ref, toolset.Config, toolset.Tools, envProvider))

pkg/tools/builtin/filesystem.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"encoding/json"
66
"fmt"
77
"io/fs"
8+
"log/slog"
89
"os"
10+
"os/exec"
911
"path/filepath"
1012
"regexp"
1113
"strings"
@@ -14,9 +16,16 @@ import (
1416
"github.com/docker/cagent/pkg/tools"
1517
)
1618

19+
// PostEditConfig represents a post-edit command configuration
20+
type PostEditConfig struct {
21+
Path string // File path pattern (glob-style)
22+
Cmd string // Command to execute (with $path placeholder)
23+
}
24+
1725
type FilesystemTool struct {
1826
allowedDirectories []string
1927
allowedTools []string
28+
postEditCommands []PostEditConfig
2029
}
2130

2231
type FileSystemOpt func(*FilesystemTool)
@@ -27,6 +36,12 @@ func WithAllowedTools(allowedTools []string) FileSystemOpt {
2736
}
2837
}
2938

39+
func WithPostEditCommands(postEditCommands []PostEditConfig) FileSystemOpt {
40+
return func(t *FilesystemTool) {
41+
t.postEditCommands = postEditCommands
42+
}
43+
}
44+
3045
func NewFilesystemTool(allowedDirectories []string, opts ...FileSystemOpt) *FilesystemTool {
3146
t := &FilesystemTool{
3247
allowedDirectories: allowedDirectories,
@@ -439,6 +454,34 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
439454
return allowedTools, nil
440455
}
441456

457+
// executePostEditCommands executes any matching post-edit commands for the given file path
458+
func (t *FilesystemTool) executePostEditCommands(ctx context.Context, filePath string) error {
459+
if len(t.postEditCommands) == 0 {
460+
return nil
461+
}
462+
463+
for _, postEdit := range t.postEditCommands {
464+
matched, err := filepath.Match(postEdit.Path, filepath.Base(filePath))
465+
if err != nil {
466+
slog.WarnContext(ctx, "Invalid post-edit pattern", "pattern", postEdit.Path, "error", err)
467+
continue
468+
}
469+
if !matched {
470+
continue
471+
}
472+
473+
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", postEdit.Cmd)
474+
cmd.Env = cmd.Environ()
475+
cmd.Env = append(cmd.Env, "path="+filePath)
476+
477+
if err := cmd.Run(); err != nil {
478+
return fmt.Errorf("post-edit command failed for %s: %w", filePath, err)
479+
}
480+
481+
}
482+
return nil
483+
}
484+
442485
// Security helper to check if path is allowed
443486
func (t *FilesystemTool) isPathAllowed(path string) error {
444487
absPath, err := filepath.Abs(path)
@@ -554,7 +597,7 @@ func (t *FilesystemTool) buildDirectoryTree(path string, maxDepth *int, currentD
554597
return node, nil
555598
}
556599

557-
func (t *FilesystemTool) handleEditFile(_ context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
600+
func (t *FilesystemTool) handleEditFile(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
558601
var args struct {
559602
Path string `json:"path"`
560603
Edits []struct {
@@ -596,6 +639,11 @@ func (t *FilesystemTool) handleEditFile(_ context.Context, toolCall tools.ToolCa
596639
return &tools.ToolCallResult{Output: fmt.Sprintf("Error writing file: %s", err)}, nil
597640
}
598641

642+
// Execute post-edit commands
643+
if err := t.executePostEditCommands(ctx, args.Path); err != nil {
644+
return &tools.ToolCallResult{Output: fmt.Sprintf("File edited successfully but post-edit command failed: %s", err)}, nil
645+
}
646+
599647
return &tools.ToolCallResult{Output: fmt.Sprintf("File edited successfully. Changes:\n%s", strings.Join(changes, "\n"))}, nil
600648
}
601649

@@ -1010,7 +1058,7 @@ func (t *FilesystemTool) handleSearchFilesContent(_ context.Context, toolCall to
10101058
return &tools.ToolCallResult{Output: strings.Join(results, "\n")}, nil
10111059
}
10121060

1013-
func (t *FilesystemTool) handleWriteFile(_ context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
1061+
func (t *FilesystemTool) handleWriteFile(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
10141062
var args struct {
10151063
Path string `json:"path"`
10161064
Content string `json:"content"`
@@ -1027,6 +1075,11 @@ func (t *FilesystemTool) handleWriteFile(_ context.Context, toolCall tools.ToolC
10271075
return &tools.ToolCallResult{Output: fmt.Sprintf("Error writing file: %s", err)}, nil
10281076
}
10291077

1078+
// Execute post-edit commands
1079+
if err := t.executePostEditCommands(ctx, args.Path); err != nil {
1080+
return &tools.ToolCallResult{Output: fmt.Sprintf("File written successfully but post-edit command failed: %s", err)}, nil
1081+
}
1082+
10301083
return &tools.ToolCallResult{Output: fmt.Sprintf("File written successfully: %s (%d bytes)", args.Path, len(args.Content))}, nil
10311084
}
10321085

pkg/tools/builtin/filesystem_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,82 @@ func TestFilesystemTool_StartStop(t *testing.T) {
625625
require.NoError(t, err)
626626
}
627627

628+
func TestFilesystemTool_PostEditCommands(t *testing.T) {
629+
tmpDir := t.TempDir()
630+
631+
testFile := filepath.Join(tmpDir, "test.go")
632+
testContent := `package main
633+
634+
func main() {
635+
fmt.Println("hello")
636+
}`
637+
638+
postEditConfigs := []PostEditConfig{
639+
{
640+
Path: "*.go",
641+
Cmd: "touch $path.formatted",
642+
},
643+
}
644+
tool := NewFilesystemTool([]string{tmpDir}, WithPostEditCommands(postEditConfigs))
645+
646+
formattedFile := testFile + ".formatted"
647+
t.Run("write_file", func(t *testing.T) {
648+
handler := getToolHandler(t, tool, "write_file")
649+
650+
// Use proper JSON marshaling for the arguments
651+
args := map[string]any{
652+
"path": testFile,
653+
"content": testContent,
654+
}
655+
argsBytes, err := json.Marshal(args)
656+
require.NoError(t, err)
657+
658+
toolCall := tools.ToolCall{
659+
Function: tools.FunctionCall{
660+
Arguments: string(argsBytes),
661+
},
662+
}
663+
664+
result, err := handler(t.Context(), toolCall)
665+
require.NoError(t, err)
666+
assert.Contains(t, result.Output, "File written successfully")
667+
668+
_, err = os.Stat(formattedFile)
669+
require.NoError(t, err, "Post-edit command should have created formatted file")
670+
require.NoError(t, os.Remove(formattedFile))
671+
})
672+
673+
t.Run("edit_file", func(t *testing.T) {
674+
editHandler := getToolHandler(t, tool, "edit_file")
675+
676+
editArgs := map[string]any{
677+
"path": testFile,
678+
"edits": []map[string]any{
679+
{
680+
"oldText": "fmt.Println",
681+
"newText": "fmt.Printf",
682+
},
683+
},
684+
}
685+
editArgsBytes, err := json.Marshal(editArgs)
686+
require.NoError(t, err)
687+
688+
editCall := tools.ToolCall{
689+
Function: tools.FunctionCall{
690+
Arguments: string(editArgsBytes),
691+
},
692+
}
693+
694+
editResult, err := editHandler(t.Context(), editCall)
695+
require.NoError(t, err)
696+
assert.Contains(t, editResult.Output, "File edited successfully")
697+
698+
// Check that post-edit was run again
699+
_, err = os.Stat(formattedFile)
700+
require.NoError(t, err, "Post-edit command should have run after edit")
701+
})
702+
}
703+
628704
// Helper functions
629705

630706
func getToolHandler(t *testing.T, tool *FilesystemTool, toolName string) tools.ToolHandler {

0 commit comments

Comments
 (0)