Files
Agent-Native/runtime/tools/glob.go
T
weichenandCursor fb2b3b796c
ci / vet-and-test (pull_request) Successful in 46s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Record tool intent before execution and add file checkpoints
Why: a crash after a tool was about to run left the call unpaired, and workspace writes could not be rewound.
Evidence: GOWORK=off go test ./runtime/ ./runtime/llm/ ./runtime/checkpoint/ ./runtime/telemetry/ ./runtime/tools/ ./runtime/session/ ./adapters/native/
Impact: pending calls rerun only when both the stored and current replay declarations are safe; otherwise the result is an error. Empty fallback lists and a nil checkpoint log keep the previous behavior.
Refs: feature/durable-rewind
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:27:04 +00:00

171 lines
4.3 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) Replay() string { return ReplaySafe }
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
}