# Agent 文件操作能力增强实施计划

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** 为 Agent Team 实现类似 opencode 的文件操作工具（glob, grep, read_file, edit_file, write_file, git 等），支持智能搜索、大文件处理、精细编辑

**Architecture:** 在 Room 层为 Agent 注册文件操作工具，通过 Tool Calling 让 LLM 自主调用工具执行文件操作

**Tech Stack:** Go + go-openai (Tool Calling)

---

## Task 1: 创建文件操作工具定义

**Files:**
- Create: `internal/room/tools/definitions.go`

**Step 1: 创建工具定义文件**

```go
package tools

import "github.com/sashabaranov/go-openai"

var FileTools = []openai.Tool{
	{
		Type: openai.ToolTypeFunction,
		Function: openai.FunctionDefinition{
			Name:        "glob",
			Description: "模式匹配查找文件，支持 * ** ? 等通配符",
			Parameters: `{
				"type": "object",
				"properties": {
					"pattern": {
						"type": "string",
						"description": "glob 模式，如 **/*.go, *.ts"
					}
				},
				"required": ["pattern"]
			}`,
		},
	},
	{
		Type: openai.ToolTypeFunction,
		Function: openai.FunctionDefinition{
			Name:        "grep",
			Description: "在文件中搜索内容，支持正则表达式",
			Parameters: `{
				"type": "object",
				"properties": {
					"pattern": {
						"type": "string",
						"description": "正则表达式搜索模式"
					},
					"path": {
						"type": "string",
						"description": "搜索目录路径"
					},
					"include": {
						"type": "string",
						"description": "文件过滤，如 *.go, *.ts"
					}
				},
				"required": ["pattern"]
			}`,
		},
	},
	{
		Type: openai.ToolTypeFunction,
		Function: openai.FunctionDefinition{
			Name:        "read_file",
			Description: "读取文件内容，支持大文件分段读取",
			Parameters: `{
				"type": "object",
				"properties": {
					"filename": {
						"type": "string",
						"description": "文件名（相对于 workspace）"
					},
					"offset": {
						"type": "integer",
						"description": "起始行号（从 0 开始）"
					},
					"limit": {
						"type": "integer",
						"description": "读取行数，默认 100"
					}
				},
				"required": ["filename"]
			}`,
		},
	},
	{
		Type: openai.ToolTypeFunction,
		Function: openai.FunctionDefinition{
			Name:        "edit_file",
			Description: "编辑文件内容，使用字符串替换",
			Parameters: `{
				"type": "object",
				"properties": {
					"filename": {
						"type": "string",
						"description": "文件名（相对于 workspace）"
					},
					"old_string": {
						"type": "string",
						"description": "要替换的原始内容"
					},
					"new_string": {
						"type": "string",
						"description": "替换后的新内容"
					}
				},
				"required": ["filename", "old_string", "new_string"]
			}`,
		},
	},
	{
		Type: openai.ToolTypeFunction,
		Function: openai.FunctionDefinition{
			Name:        "write_file",
			Description: "写入或创建文件",
			Parameters: `{
				"type": "object",
				"properties": {
					"filename": {
						"type": "string",
						"description": "文件名（相对于 workspace）"
					},
					"content": {
						"type": "string",
						"description": "文件内容"
					}
				},
				"required": ["filename", "content"]
			}`,
		},
	},
	{
		Type: openai.ToolTypeFunction,
		Function: openai.FunctionDefinition{
			Name:        "list_workspace",
			Description: "列出工作区的所有文件",
			Parameters: `{
				"type": "object",
				"properties": {}
			}`,
		},
	},
}
```

**Step 2: 提交**

```bash
git add internal/room/tools/definitions.go
git commit -m "feat: 添加文件操作工具定义"
```

---

## Task 2: 实现工具执行器

**Files:**
- Create: `internal/room/tools/executor.go`

**Step 1: 实现工具执行器**

```go
package tools

import (
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/sdaduanbilei/agent-team/internal/llm"
)

type Executor struct {
	workspaceDir string
}

func NewExecutor(workspaceDir string) *Executor {
	return &Executor{workspaceDir: workspaceDir}
}

func (e *Executor) Execute(toolCall llm.ToolCall) (string, error) {
	switch toolCall.Function.Name {
	case "glob":
		return e.glob(toolCall.Function.Arguments)
	case "grep":
		return e.grep(toolCall.Function.Arguments)
	case "read_file":
		return e.readFile(toolCall.Function.Arguments)
	case "edit_file":
		return e.editFile(toolCall.Function.Arguments)
	case "write_file":
		return e.writeFile(toolCall.Function.Arguments)
	case "list_workspace":
		return e.listWorkspace()
	case "git_status":
		return e.gitStatus()
	case "git_diff":
		return e.gitDiff(toolCall.Function.Arguments)
	case "git_commit":
		return e.gitCommit(toolCall.Function.Arguments)
	default:
		return "", nil
	}
}

type GlobArgs struct {
	Pattern string `json:"pattern"`
}

func (e *Executor) glob(args string) (string, error) {
	var a GlobArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	pattern := filepath.Join(e.workspaceDir, a.Pattern)
	files, err := filepath.Glob(pattern)
	if err != nil {
		return "", err
	}
	
	var result []string
	for _, f := range files {
		rel, _ := filepath.Rel(e.workspaceDir, f)
		result = append(result, rel)
	}
	
	if len(result) == 0 {
		return "未找到匹配的文件", nil
	}
	return strings.Join(result, "\n"), nil
}

type GrepArgs struct {
	Pattern string `json:"pattern"`
	Path    string `json:"path"`
	Include string `json:"include"`
}

func (e *Executor) grep(args string) (string, error) {
	var a GrepArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	searchDir := e.workspaceDir
	if a.Path != "" {
		searchDir = filepath.Join(e.workspaceDir, a.Path)
	}
	
	re, err := regexp.Compile(a.Pattern)
	if err != nil {
		return "", err
	}
	
	var results []string
	err = filepath.Walk(searchDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if info.IsDir() {
			return nil
		}
		if a.Include != "" {
			matched, _ := filepath.Match(a.Include, info.Name())
			if !matched {
				return nil
			}
		}
		
		content, err := os.ReadFile(path)
		if err != nil {
			return nil
		}
		
		lines := strings.Split(string(content), "\n")
		for i, line := range lines {
			if re.MatchString(line) {
				rel, _ := filepath.Rel(e.workspaceDir, path)
				results = append(results, fmt.Sprintf("%s:%d: %s", rel, i+1, line))
			}
		}
		return nil
	})
	
	if len(results) == 0 {
		return "未找到匹配的内容", nil
	}
	
	// 限制结果数量
	if len(results) > 100 {
		results = results[:100]
		results = append(results, "... (还有更多结果)")
	}
	return strings.Join(results, "\n"), nil
}

type ReadFileArgs struct {
	Filename string `json:"filename"`
	Offset   int    `json:"offset"`
	Limit    int    `json:"limit"`
}

func (e *Executor) readFile(args string) (string, error) {
	var a ReadFileArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	if a.Limit == 0 {
		a.Limit = 100
	}
	
	fpath := filepath.Join(e.workspaceDir, a.Filename)
	content, err := os.ReadFile(fpath)
	if err != nil {
		return "", err
	}
	
	lines := strings.Split(string(content), "\n")
	if a.Offset >= len(lines) {
		return "起始位置超出文件行数", nil
	}
	
	end := a.Offset + a.Limit
	if end > len(lines) {
		end = len(lines)
	}
	
	result := strings.Join(lines[a.Offset:end], "\n")
	
	if end < len(lines) {
		result += fmt.Sprintf("\n\n... (共 %d 行，当前显示 %d-%d)", len(lines), a.Offset+1, end)
	}
	
	return result, nil
}

type EditFileArgs struct {
	Filename  string `json:"filename"`
	OldString string `json:"old_string"`
	NewString string `json:"new_string"`
}

func (e *Executor) editFile(args string) (string, error) {
	var a EditFileArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	fpath := filepath.Join(e.workspaceDir, a.Filename)
	original, err := os.ReadFile(fpath)
	if err != nil {
		return "", err
	}
	
	if !strings.Contains(string(original), a.OldString) {
		return "", fmt.Errorf("文件中未找到要替换的内容")
	}
	
	updated := strings.Replace(string(original), a.OldString, a.NewString, 1)
	if err := os.WriteFile(fpath, []byte(updated), 0644); err != nil {
		return "", err
	}
	
	return fmt.Sprintf("已更新文件: %s", a.Filename), nil
}

type WriteFileArgs struct {
	Filename string `json:"filename"`
	Content  string `json:"content"`
}

func (e *Executor) writeFile(args string) (string, error) {
	var a WriteFileArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	fpath := filepath.Join(e.workspaceDir, a.Filename)
	os.MkdirAll(filepath.Dir(fpath), 0755)
	
	exists := ""
	if _, err := os.Stat(fpath); err == nil {
		exists = " (已存在，已覆盖)"
	}
	
	if err := os.WriteFile(fpath, []byte(a.Content), 0644); err != nil {
		return "", err
	}
	
	return fmt.Sprintf("已写入文件: %s%s", a.Filename, exists), nil
}

func (e *Executor) listWorkspace() (string, error) {
	var files []string
	err := filepath.Walk(e.workspaceDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if info.IsDir() {
			return nil
		}
		rel, _ := filepath.Rel(e.workspaceDir, path)
		if !strings.HasPrefix(rel, ".") {
			files = append(files, rel)
		}
		return nil
	})
	
	if err != nil {
		return "", err
	}
	
	if len(files) == 0 {
		return "工作区为空", nil
	}
	
	return strings.Join(files, "\n"), nil
}

func (e *Executor) gitStatus() (string, error) {
	cmd := exec.Command("git", "status", "--porcelain")
	cmd.Dir = e.workspaceDir
	out, err := cmd.Output()
	if err != nil {
		return "", err
	}
	if len(out) == 0 {
		return "工作区干净，无待提交更改", nil
	}
	return string(out), nil
}

type GitDiffArgs struct {
	Filename string `json:"filename"`
}

func (e *Executor) gitDiff(args string) (string, error) {
	var a GitDiffArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	var cmd *exec.Cmd
	if a.Filename != "" {
		cmd = exec.Command("git", "diff", a.Filename)
	} else {
		cmd = exec.Command("git", "diff")
	}
	cmd.Dir = e.workspaceDir
	out, err := cmd.Output()
	if err != nil {
		return "", err
	}
	if len(out) == 0 {
		return "无更改", nil
	}
	return string(out), nil
}

type GitCommitArgs struct {
	Message string `json:"message"`
}

func (e *Executor) gitCommit(args string) (string, error) {
	var a GitCommitArgs
	if err := json.Unmarshal([]byte(args), &a); err != nil {
		return "", err
	}
	
	// git add -A
	cmd := exec.Command("git", "add", "-A")
	cmd.Dir = e.workspaceDir
	if _, err := cmd.Output(); err != nil {
		return "", err
	}
	
	// git commit
	cmd = exec.Command("git", "commit", "-m", a.Message)
	out, err := cmd.Output()
	if err != nil {
		return "", err
	}
	
	return string(out), nil
}
```

**Step 2: 提交**

```bash
git add internal/room/tools/executor.go
git commit -m "feat: 实现文件操作工具执行器"
```

---

## Task 3: 集成工具到 Room

**Files:**
- Modify: `internal/room/handle.go`
- Modify: `internal/room/types.go`

**Step 1: 修改 types.go 添加工具执行器字段**

在 Room 结构体中添加:
```go
ToolExecutor *tools.Executor
```

**Step 2: 修改 handle.go 初始化工具执行器**

在 Load 函数中添加:
```go
r.ToolExecutor = tools.NewExecutor(filepath.Join(roomDir, "workspace"))
```

**Step 3: 修改 room 调用逻辑以支持工具调用**

在需要的地方（如 build 模式下 Agent 调用），传入工具定义并处理工具调用结果。

**Step 4: 提交**

```bash
git add internal/room/handle.go internal/room/types.go
git commit -m "feat: 集成文件操作工具到 Room"
```

---

## Task 4: 注册 Git 工具定义

**Files:**
- Modify: `internal/room/tools/definitions.go`

**Step 1: 添加 Git 工具定义**

在 FileTools 切片中添加:
```go
{
	Type: openai.ToolTypeFunction,
	Function: openai.FunctionDefinition{
		Name:        "git_status",
		Description: "查看 Git 工作区状态",
		Parameters: `{"type": "object", "properties": {}}`,
	},
},
{
	Type: openai.ToolTypeFunction,
	Function: openai.FunctionDefinition{
		Name:        "git_diff",
		Description: "查看文件变更",
		Parameters: `{
			"type": "object",
			"properties": {
				"filename": {"type": "string", "description": "文件名"}
			}
		}`,
	},
},
{
	Type: openai.ToolTypeFunction,
	Function: openai.FunctionDefinition{
		Name:        "git_commit",
		Description: "提交更改",
		Parameters: `{
			"type": "object",
			"properties": {
				"message": {"type": "string", "description": "提交信息"}
			},
			"required": ["message"]
		}`,
	},
},
```

**Step 2: 提交**

```bash
git add internal/room/tools/definitions.go
git commit -m "feat: 添加 Git 工具定义"
```

---

## Task 5: 测试文件操作工具

**Files:**
- Create: `internal/room/tools/executor_test.go`

**Step 1: 创建测试文件**

```go
package tools

import (
	"os"
	"path/filepath"
	"testing"
)

func TestGlob(t *testing.T) {
	// 创建临时目录和测试文件
	tmpDir := t.TempDir()
	os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte("package test"), 0644)
	os.WriteFile(filepath.Join(tmpDir, "test.ts"), []byte("const x = 1"), 0644)
	os.MkdirAll(filepath.Join(tmpDir, "sub"), 0755)
	os.WriteFile(filepath.Join(tmpDir, "sub", "nested.go"), []byte("package sub"), 0644)
	
	exec := NewExecutor(tmpDir)
	
	// 测试 *.go
	result, err := exec.glob(`{"pattern": "*.go"}`)
	if err != nil {
		t.Fatal(err)
	}
	if !contains(result, "test.go") {
		t.Errorf("expected test.go in result, got: %s", result)
	}
	
	// 测试 **/*.go
	result, err = exec.glob(`{"pattern": "**/*.go"}`)
	if err != nil {
		t.Fatal(err)
	}
	if !contains(result, "test.go") || !contains(result, "sub/nested.go") {
		t.Errorf("expected test.go and sub/nested.go, got: %s", result)
	}
}

func TestReadFile(t *testing.T) {
	tmpDir := t.TempDir()
	content := "line1\nline2\nline3\nline4\nline5"
	os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte(content), 0644)
	
	exec := NewExecutor(tmpDir)
	
	// 测试基本读取
	result, err := exec.readFile(`{"filename": "test.txt"}`)
	if err != nil {
		t.Fatal(err)
	}
	if !contains(result, "line1") {
		t.Errorf("expected line1 in result, got: %s", result)
	}
	
	// 测试 offset 和 limit
	result, err = exec.readFile(`{"filename": "test.txt", "offset": 1, "limit": 2}`)
	if err != nil {
		t.Fatal(err)
	}
	if !contains(result, "line2") || !contains(result, "line3") {
		t.Errorf("expected line2 and: %s", result)
	}
 line3, got}

func TestEditFile(t *testing.T) {
	tmpDir := t.TempDir()
	os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("hello world"), 0644)
	
	exec := NewExecutor(tmpDir)
	
	result, err := exec.editFile(`{"filename": "test.txt", "old_string": "world", "new_string": "opencode"}`)
	if err != nil {
		t.Fatal(err)
	}
	
	// 验证文件内容
	data, _ := os.ReadFile(filepath.Join(tmpDir, "test.txt"))
	if string(data) != "hello opencode" {
		t.Errorf("expected 'hello opencode', got: %s", string(data))
	}
	
	_ = result // ignore
}

func TestWriteFile(t *testing.T) {
	tmpDir := t.TempDir()
	
	exec := NewExecutor(tmpDir)
	
	// 测试新建文件
	result, err := exec.writeFile(`{"filename": "new.txt", "content": "new content"}`)
	if err != nil {
		t.Fatal(err)
	}
	
	data, _ := os.ReadFile(filepath.Join(tmpDir, "new.txt"))
	if string(data) != "new content" {
		t.Errorf("expected 'new content', got: %s", string(data))
	}
	
	_ = result // ignore
}

func TestListWorkspace(t *testing.T) {
	tmpDir := t.TempDir()
	os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("a"), 0644)
	os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("b"), 0644)
	os.MkdirAll(filepath.Join(tmpDir, "sub"), 0755)
	os.WriteFile(filepath.Join(tmpDir, "sub", "c.txt"), []byte("c"), 0644)
	
	exec := NewExecutor(tmpDir)
	
	result, err := exec.listWorkspace()
	if err != nil {
		t.Fatal(err)
	}
	
	if !contains(result, "a.txt") || !contains(result, "b.txt") || !contains(result, "sub/c.txt") {
		t.Errorf("expected all files, got: %s", result)
	}
}

func contains(s, substr string) bool {
	return len(s) > 0 && strings.Contains(s, substr)
}
```

**Step 2: 运行测试**

```bash
go test ./internal/room/tools/... -v
```

**Step 3: 提交**

```bash
git add internal/room/tools/executor_test.go
git commit -m "test: 添加文件操作工具测试"
```

---

## Plan complete

**Two execution options:**

1. **Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration

2. **Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints

**Which approach?**
