Files
Agent-Native/runtime/tools/glob.go
T
weichen b105e71533 Add glob, multi_edit, regex grep, and line-range reads to the workspace toolset
Why: the five builtin tools could not express file discovery (no glob),
code search (literal-only grep), paged reads (200 KiB silent
truncation), or batch edits, capping the kernel below general coding-
agent parity.

Evidence: go vet ./... clean; go test ./... all ok. New cases cover
numbered line ranges and beyond-EOF rejection, regex/include/
ignore_case grep with invalid-regex errors, double-star glob
(**/*.go, docs/**, no-match), workspace confinement of glob, and
multi_edit atomicity (failed batch leaves the file untouched).

Impact: WorkspaceTools now registers 7 tools (adds glob, multi_edit);
read_file gains optional offset/limit and announces truncation; grep
gains regex/include/ignore_case while keeping literal default
behavior; existing callers see only additive changes.

Refs: docs/parity-matrix.md #3 (tool semantics negatives, Batch2).
2026-10-01 05:29:12 +00:00

170 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
const maxGlobResults = 500
type globTool struct{ root string }
func (t *globTool) Name() string { return "glob" }
func (t *globTool) Description() string {
return "Find workspace files matching a glob pattern (** crosses directories, * within one segment). Returns up to 500 relative paths."
}
func (t *globTool) Parameters() json.RawMessage {
return json.RawMessage(`{"type":"object","required":["pattern"],"properties":{"pattern":{"type":"string","description":"glob such as **/*.go or docs/*.md"}}}`)
}
func (t *globTool) Execute(_ context.Context, args json.RawMessage) (Result, error) {
rel, err := readArg(args, "pattern")
if err != nil {
return Result{}, err
}
if strings.TrimSpace(rel) == "" {
return Result{Content: "pattern is empty", IsError: true}, nil
}
re, err := globRegexp(rel)
if err != nil {
return Result{Content: err.Error(), IsError: true}, nil
}
var matches []string
capped := false
err = filepath.WalkDir(t.root, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return nil
}
r, rerr := filepath.Rel(t.root, path)
if rerr != nil {
return nil
}
r = filepath.ToSlash(r)
if r == "." {
return nil
}
if d.IsDir() {
// 目录命中以 / 结尾,供模型分辨文件与目录。
if re.MatchString(r + "/") {
if len(matches) < maxGlobResults {
matches = append(matches, r+"/")
} else {
capped = true
}
}
return nil
}
if re.MatchString(r) {
if len(matches) < maxGlobResults {
matches = append(matches, r)
} else {
capped = true
}
}
return nil
})
if err != nil {
return Result{Content: err.Error(), IsError: true}, nil
}
if len(matches) == 0 {
return Result{Content: "no matches"}, nil
}
sort.Strings(matches)
out := strings.Join(matches, "\n")
if capped {
out += fmt.Sprintf("\n(showing first %d matches)", maxGlobResults)
}
return Result{Content: out}, nil
}
// globRegexp 把受限 glob(** 跨目录、* 单段、? 单字符,其余按字面)编译为正则。
// 尾段单独的 ** 匹配其下全部内容(文件与目录);中段的 ** 匹配零或多层目录。
// 不支持字符类与分组,避免与正则语义混淆。
func globRegexp(pattern string) (*regexp.Regexp, error) {
var b strings.Builder
b.WriteString("^")
segs := strings.Split(filepath.ToSlash(pattern), "/")
prevStarStar := false
for i, seg := range segs {
// 前一段是独立的 **(非尾段)时,其 (?:[^/]+/)* 已含目录斜杠,
// 零层目录的场景不需要再补一个分隔符。
if i > 0 && !prevStarStar {
b.WriteString("/")
}
prevStarStar = false
if seg == "**" {
if i == len(segs)-1 {
// 尾段 **:匹配其余全部路径(文件与目录)。
b.WriteString(".+")
} else {
b.WriteString("(?:[^/]+/)*")
prevStarStar = true
}
continue
}
for _, part := range splitDoubleStar(seg) {
if part == "**" {
b.WriteString("[^/]*")
continue
}
for _, ch := range part {
switch ch {
case '*':
b.WriteString("[^/]*")
case '?':
b.WriteString("[^/]")
default:
b.WriteString(regexp.QuoteMeta(string(ch)))
}
}
}
}
b.WriteString("$")
return regexp.Compile(b.String())
}
// splitDoubleStar 把段内混用的 ** 与字面部分切开(如 "a**b")。
func splitDoubleStar(seg string) []string {
var out []string
cur := strings.Builder{}
for i := 0; i < len(seg); i++ {
if i+1 < len(seg) && seg[i] == '*' && seg[i+1] == '*' {
if cur.Len() > 0 {
out = append(out, cur.String())
cur.Reset()
}
out = append(out, "**")
i++
continue
}
cur.WriteByte(seg[i])
}
if cur.Len() > 0 {
out = append(out, cur.String())
}
return out
}
// globMatcher 是文件名级 glob(filepath.Match 语义),供 grep include 过滤用。
type globMatcher struct{ pattern string }
func newGlobMatcher(pattern string) (*globMatcher, error) {
if _, err := filepath.Match(pattern, "probe"); err != nil {
return nil, fmt.Errorf("invalid include pattern %q: %w", pattern, err)
}
return &globMatcher{pattern: pattern}, nil
}
func (m *globMatcher) Match(name string) bool {
ok, _ := filepath.Match(m.pattern, name)
return ok
}