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>
171 lines
4.3 KiB
Go
171 lines
4.3 KiB
Go
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
|
||
}
|