mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 01:09:46 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b1689a62 | ||
|
|
6a258e1876 | ||
|
|
e3337d06d5 | ||
|
|
3aa190bd7f | ||
|
|
00ad4d4b8d | ||
|
|
a394fd60ee |
No files matched your search
@@ -30,11 +30,13 @@ work.
|
||||
read-before-edit discipline: inspect the target file and nearby callers,
|
||||
tests, or config before you modify behavior. Never edit a file you have not
|
||||
read.
|
||||
2. **Plan.** For multi-step work, call update_plan with an ordered checklist and
|
||||
keep it current at meaningful milestones. Mark completed work and the next
|
||||
in-progress step together when practical; do not spend separate model turns
|
||||
updating the plan after every file or command. Keep at most one item
|
||||
in_progress. Skip the plan for trivial one-step tasks.
|
||||
2. **Plan.** Use update_plan for implementation or investigation that has at
|
||||
least three meaningful dependent steps, then keep it current at milestones.
|
||||
Mark completed work and the next in-progress step together when practical;
|
||||
do not spend separate turns updating it after every file or command. Keep at
|
||||
most one item in_progress. Skip plans for simple lookups, explanations, code
|
||||
navigation, and other short tasks. Never create a plan after the work is
|
||||
already complete merely to describe what you did.
|
||||
3. **Implement.** Make focused changes that match the surrounding code's style,
|
||||
naming, and conventions. Prefer the smallest change that fully solves the
|
||||
problem. Avoid broad refactors, unrelated rewrites, dependency churn, and
|
||||
@@ -49,9 +51,13 @@ work.
|
||||
## Editing discipline
|
||||
|
||||
- Choose the narrowest tool that safely accomplishes the step. Prefer native
|
||||
file tools - read_file, list_directory, glob, grep, write_file, edit_file,
|
||||
apply_patch - over shelling out to cat/sed/awk/python for file operations.
|
||||
file tools - read_file, read_minified_file, list_directory, glob, grep,
|
||||
write_file, edit_file, apply_patch - over shelling out to
|
||||
cat/sed/awk/python for file operations.
|
||||
They are safer, reviewable, and produce clean diffs.
|
||||
- Prefer read_minified_file when initially exploring source code; it preserves
|
||||
code while removing comments and redundant whitespace. Use read_file when
|
||||
exact text, comments, or line numbers are needed.
|
||||
- Keep edits focused and reviewable. A single patch may update several related
|
||||
files when they form one coherent change; do not hide unrelated edits in a
|
||||
bulk shell or script rewrite.
|
||||
|
||||
@@ -71,5 +71,6 @@ const geminiPromptAddendum = `<model_guidance>
|
||||
equivalent shell commands; they are safer and produce cleaner diffs.
|
||||
- Be concise and concrete. When you run a shell command with side effects, state
|
||||
in one short clause why it is needed.
|
||||
- Use update_plan for any multi-step task and keep it current.
|
||||
- Use update_plan only for work with at least three meaningful dependent steps;
|
||||
skip it for simple lookups and never create one after the work is complete.
|
||||
</model_guidance>`
|
||||
@@ -79,8 +79,8 @@ func TestRegisterToolSearchIfEligibleSkipsWhenThresholdZero(t *testing.T) {
|
||||
|
||||
func TestDeferredEligibleCountIncludesOptionalBuiltins(t *testing.T) {
|
||||
registry := newCoreRegistry(t.TempDir())
|
||||
if got := deferredEligibleCount(registry, agent.PermissionModeAuto, nil, nil); got < 3 {
|
||||
t.Fatalf("deferredEligibleCount(core) = %d, want at least 3 optional built-ins", got)
|
||||
if got := deferredEligibleCount(registry, agent.PermissionModeAuto, nil, nil); got < 2 {
|
||||
t.Fatalf("deferredEligibleCount(core) = %d, want at least 2 optional built-ins", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+134
-2
@@ -13,8 +13,10 @@ package minify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -34,8 +36,11 @@ func File(path string, content []byte) Result {
|
||||
if out, ok := minifyGo(content); ok {
|
||||
return Result{Content: out, Language: "go", Applied: true}
|
||||
}
|
||||
// Unparsable Go (a snippet, generics edge, or syntax error): fall through
|
||||
// to the safe generic path rather than risk a partial AST reprint.
|
||||
if out, ok := minifyGoFragment(content); ok {
|
||||
return Result{Content: out, Language: "go-fragment", Applied: true}
|
||||
}
|
||||
// An incomplete fragment that cannot be wrapped safely falls through to
|
||||
// whitespace-only normalization; no source text is guessed or discarded.
|
||||
} else if style, ok := commentStyles[ext]; ok {
|
||||
// Strip comments with a string-aware lexer, then collapse the whitespace the
|
||||
// removed comments left behind. The stripper only handles languages whose
|
||||
@@ -46,6 +51,133 @@ func File(path string, content []byte) Result {
|
||||
return Result{Content: minifyGeneric(content), Language: "text", Applied: false}
|
||||
}
|
||||
|
||||
// Fragment minifies a bounded source range without assuming lexical state from
|
||||
// text outside the range. Go remains parser-validated; other languages use the
|
||||
// conservative whitespace-only transform because a range may begin inside a
|
||||
// multiline string or block comment.
|
||||
func Fragment(path string, content []byte) Result {
|
||||
if strings.EqualFold(filepath.Ext(path), ".go") {
|
||||
if out, ok := minifyGo(content); ok {
|
||||
return Result{Content: out, Language: "go", Applied: true}
|
||||
}
|
||||
if out, ok := minifyGoFragment(content); ok {
|
||||
return Result{Content: out, Language: "go-fragment", Applied: true}
|
||||
}
|
||||
}
|
||||
return Result{Content: minifyGeneric(content), Language: "text", Applied: false}
|
||||
}
|
||||
|
||||
// ContextualFragment minifies a bounded fragment after checking its starting
|
||||
// position against the complete source. A Go range that begins inside a string
|
||||
// or comment must remain verbatim apart from whitespace normalization: parsing
|
||||
// that fragment by itself would mistake literal text for Go syntax.
|
||||
func ContextualFragment(path string, source, content []byte, startOffset int) Result {
|
||||
if strings.EqualFold(filepath.Ext(path), ".go") && goFragmentStartsInsideToken(source, startOffset) {
|
||||
return Result{Content: minifyGeneric(content), Language: "text", Applied: false}
|
||||
}
|
||||
return Fragment(path, content)
|
||||
}
|
||||
|
||||
func goFragmentStartsInsideToken(source []byte, startOffset int) bool {
|
||||
if startOffset <= 0 || startOffset >= len(source) {
|
||||
return false
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(source))
|
||||
var lexer scanner.Scanner
|
||||
lexer.Init(file, source, nil, scanner.ScanComments)
|
||||
for {
|
||||
position, kind, literal := lexer.Scan()
|
||||
if kind == token.EOF {
|
||||
return false
|
||||
}
|
||||
if kind != token.STRING && kind != token.COMMENT {
|
||||
continue
|
||||
}
|
||||
start := file.Offset(position)
|
||||
if startOffset > start && startOffset < start+len(literal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// minifyGoFragment handles bounded reads that do not contain a package clause.
|
||||
// It finds the longest complete declaration or statement prefix that the real
|
||||
// Go parser accepts, prints that prefix without comments, and preserves any
|
||||
// incomplete tail conservatively. Ranges are intentionally capped: repeatedly
|
||||
// parsing a large broken file would otherwise turn a cheap read into O(n²) work.
|
||||
func minifyGoFragment(content []byte) (string, bool) {
|
||||
lines := strings.Split(strings.ReplaceAll(string(content), "\r\n", "\n"), "\n")
|
||||
if len(lines) > 1 && lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
if len(lines) > 512 {
|
||||
return "", false
|
||||
}
|
||||
for end := len(lines); end > 0; end-- {
|
||||
prefix := strings.Join(lines[:end], "\n")
|
||||
if compact, ok := parseGoDeclarations(prefix); ok {
|
||||
return joinCompactPrefix(compact, lines[end:]), true
|
||||
}
|
||||
if compact, ok := parseGoStatements(prefix); ok {
|
||||
return joinCompactPrefix(compact, lines[end:]), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func parseGoDeclarations(fragment string) (string, bool) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "", "package compact\n"+fragment, parser.SkipObjectResolution)
|
||||
if err != nil || len(file.Decls) == 0 {
|
||||
return "", false
|
||||
}
|
||||
nodes := make([]ast.Node, len(file.Decls))
|
||||
for i, declaration := range file.Decls {
|
||||
nodes[i] = declaration
|
||||
}
|
||||
return printGoNodes(fset, nodes)
|
||||
}
|
||||
|
||||
func parseGoStatements(fragment string) (string, bool) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "", "package compact\nfunc _(){\n"+fragment+"\n}", parser.SkipObjectResolution)
|
||||
if err != nil || len(file.Decls) != 1 {
|
||||
return "", false
|
||||
}
|
||||
function, ok := file.Decls[0].(*ast.FuncDecl)
|
||||
if !ok || function.Body == nil || len(function.Body.List) == 0 {
|
||||
return "", false
|
||||
}
|
||||
nodes := make([]ast.Node, len(function.Body.List))
|
||||
for i, statement := range function.Body.List {
|
||||
nodes[i] = statement
|
||||
}
|
||||
return printGoNodes(fset, nodes)
|
||||
}
|
||||
|
||||
func printGoNodes(fset *token.FileSet, nodes []ast.Node) (string, bool) {
|
||||
var buf bytes.Buffer
|
||||
cfg := printer.Config{Mode: printer.TabIndent, Tabwidth: 1}
|
||||
for i, node := range nodes {
|
||||
if i > 0 {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
if err := cfg.Fprint(&buf, fset, node); err != nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(buf.String()), true
|
||||
}
|
||||
|
||||
func joinCompactPrefix(compact string, remainder []string) string {
|
||||
tail := minifyGeneric([]byte(strings.Join(remainder, "\n")))
|
||||
if tail == "" {
|
||||
return compact
|
||||
}
|
||||
return compact + "\n" + tail
|
||||
}
|
||||
|
||||
// minifyGo parses Go WITHOUT comments (omitting parser.ParseComments leaves them
|
||||
// unattached) and reprints the AST, yielding valid, comment-free, gofmt-shaped
|
||||
// source with tab indentation (1 char per level). It returns ok=false on any
|
||||
|
||||
@@ -39,17 +39,58 @@ func Greet(name string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinifyGoFallsBackOnUnparsableGo(t *testing.T) {
|
||||
// A snippet (no package clause) cannot parse as a file -> safe generic path.
|
||||
func TestMinifyGoStatementFragment(t *testing.T) {
|
||||
r := File("snippet.go", []byte("x := 1 \n\n\n// keep me\ny := 2\n"))
|
||||
if r.Applied {
|
||||
t.Fatalf("expected fallback for unparsable Go, got %+v", r)
|
||||
if !r.Applied || r.Language != "go-fragment" {
|
||||
t.Fatalf("expected parsed Go fragment, got %+v", r)
|
||||
}
|
||||
if strings.Contains(r.Content, "\n\n\n") {
|
||||
t.Errorf("generic should collapse blank runs:\n%q", r.Content)
|
||||
t.Errorf("fragment should collapse blank runs:\n%q", r.Content)
|
||||
}
|
||||
if !strings.Contains(r.Content, "// keep me") {
|
||||
t.Errorf("generic must NOT strip comments (unsafe without a parser): %q", r.Content)
|
||||
if strings.Contains(r.Content, "keep me") || !strings.Contains(r.Content, "y := 2") {
|
||||
t.Errorf("fragment was not compacted safely: %q", r.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinifyGoFragmentAccepts512LinesWithFinalNewline(t *testing.T) {
|
||||
src := strings.Repeat("x++\n", 512)
|
||||
r := File("snippet.go", []byte(src))
|
||||
if !r.Applied || r.Language != "go-fragment" {
|
||||
t.Fatalf("expected 512-line fragment to remain eligible, got %+v", r)
|
||||
}
|
||||
if got := strings.Count(r.Content, "x++"); got != 512 {
|
||||
t.Fatalf("compacted statement count = %d, want 512", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinifyGoDeclarationPrefixPreservesIncompleteTail(t *testing.T) {
|
||||
src := `func Handle737(n int) (int, error) {
|
||||
if n%2 == 1 {
|
||||
return 0, errBad
|
||||
}
|
||||
return n + 737, nil
|
||||
}
|
||||
|
||||
// Handle738 begins outside the requested range.
|
||||
func Handle738(n int) (int, error) {
|
||||
if n%2 == 1 {`
|
||||
r := File("range.go", []byte(src))
|
||||
if !r.Applied || r.Language != "go-fragment" {
|
||||
t.Fatalf("expected declaration-prefix compaction, got %+v", r)
|
||||
}
|
||||
if strings.Contains(r.Content, "Handle737 returns") || !strings.Contains(r.Content, "return n + 737") {
|
||||
t.Fatalf("complete declaration was not compacted correctly:\n%s", r.Content)
|
||||
}
|
||||
if !strings.Contains(r.Content, "func Handle738") || !strings.Contains(r.Content, "if n%2 == 1 {") {
|
||||
t.Fatalf("incomplete tail was not preserved:\n%s", r.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinifyGoLargeInvalidInputFallsBackConservatively(t *testing.T) {
|
||||
src := strings.Repeat("x := 1 // keep\n", 513)
|
||||
r := File("broken.go", []byte(src))
|
||||
if r.Applied || !strings.Contains(r.Content, "// keep") {
|
||||
t.Fatalf("large invalid input must retain conservative fallback: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -295,6 +296,40 @@ func TestEditFileAllowsSeenRangeAndRejectsUnseenRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalLimitedReadAuthorizesEdit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
var source strings.Builder
|
||||
for line := 1; line <= 1000; line++ {
|
||||
source.WriteString("line ")
|
||||
source.WriteString(strconv.Itoa(line))
|
||||
source.WriteByte('\n')
|
||||
}
|
||||
path := filepath.Join(dir, "f.txt")
|
||||
if err := os.WriteFile(path, []byte(source.String()), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registry := safetyRegistry(t, dir)
|
||||
tracker := NewFileTracker()
|
||||
opts := grantedOpts(tracker)
|
||||
|
||||
read := registry.RunWithOptions(context.Background(), "read_file", map[string]any{
|
||||
"path": "f.txt", "offset": 500, "limit": 10,
|
||||
}, opts)
|
||||
if read.Status != StatusOK || read.Truncated {
|
||||
t.Fatalf("bounded canonical read must be exact: status=%s truncated=%v meta=%#v", read.Status, read.Truncated, read.Meta)
|
||||
}
|
||||
if read.Meta["seen_lines"] != "500-509" {
|
||||
t.Fatalf("bounded canonical read did not receive exact-read credit: %#v", read.Meta)
|
||||
}
|
||||
|
||||
edit := registry.RunWithOptions(context.Background(), "edit_file", map[string]any{
|
||||
"path": "f.txt", "old_string": "line 505", "new_string": "updated 505",
|
||||
}, opts)
|
||||
if edit.Status != StatusOK {
|
||||
t.Fatalf("edit inside canonical read range was refused: %s", edit.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileReplaceAllRecordsEveryShiftedReplacementSpan(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "f.txt")
|
||||
|
||||
@@ -38,6 +38,108 @@ func TestReadFileToolReadsLineRanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToolReadsCanonicalLineRange(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "notes.txt"), "alpha\nbeta\ngamma\ndelta")
|
||||
|
||||
result := NewScopedReadFileTool(root, nil).Run(context.Background(), map[string]any{
|
||||
"path": "notes.txt",
|
||||
"offset": 2,
|
||||
"limit": 2,
|
||||
})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Fatalf("expected ok status, got %s: %s", result.Status, result.Output)
|
||||
}
|
||||
if result.Truncated {
|
||||
t.Fatalf("canonical limit defines the requested range and must not mark it truncated: %#v", result.Meta)
|
||||
}
|
||||
if !strings.Contains(result.Output, "2 | beta") || !strings.Contains(result.Output, "3 | gamma") {
|
||||
t.Fatalf("canonical range returned the wrong lines: %q", result.Output)
|
||||
}
|
||||
if strings.Contains(result.Output, "alpha") || strings.Contains(result.Output, "delta") {
|
||||
t.Fatalf("canonical range leaked outside requested slice: %q", result.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToolCanonicalOffsetPastEnd(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "notes.txt"), "alpha\nbeta")
|
||||
|
||||
result := NewScopedReadFileTool(root, nil).Run(context.Background(), map[string]any{
|
||||
"path": "notes.txt", "offset": 10,
|
||||
})
|
||||
|
||||
if result.Status != StatusOK || !strings.Contains(result.Output, "offset 10 is past the end") {
|
||||
t.Fatalf("expected canonical out-of-range message, got status=%s output=%q", result.Status, result.Output)
|
||||
}
|
||||
if strings.Contains(result.Output, "start_line") {
|
||||
t.Fatalf("out-of-range message exposed legacy argument: %q", result.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToolMixedLegacyRangesPreferLines(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "notes.txt"), "alpha\nbeta\ngamma\ndelta")
|
||||
|
||||
result := NewScopedReadFileTool(root, nil).Run(context.Background(), map[string]any{
|
||||
"path": "notes.txt",
|
||||
"start_line": 2,
|
||||
"end_line": 3,
|
||||
"max_lines": 2,
|
||||
"byte_offset": 0,
|
||||
"byte_limit": readFileByteChunkMax,
|
||||
})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Fatalf("mixed legacy range should recover, got %s: %s", result.Status, result.Output)
|
||||
}
|
||||
if !strings.Contains(result.Output, "2 | beta") || !strings.Contains(result.Output, "3 | gamma") {
|
||||
t.Fatalf("mixed legacy range did not prefer lines: %q", result.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToolCombinesLegacyStartWithCanonicalLimit(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "notes.txt"), "alpha\nbeta\ngamma\ndelta")
|
||||
|
||||
result := NewScopedReadFileTool(root, nil).Run(context.Background(), map[string]any{
|
||||
"path": "notes.txt", "start_line": 2, "limit": 2,
|
||||
})
|
||||
|
||||
if result.Status != StatusOK || result.Truncated {
|
||||
t.Fatalf("mixed compatible range should be exact, got status=%s truncated=%v: %s", result.Status, result.Truncated, result.Output)
|
||||
}
|
||||
if !strings.Contains(result.Output, "2 | beta") || !strings.Contains(result.Output, "3 | gamma") {
|
||||
t.Fatalf("mixed compatible range returned the wrong lines: %q", result.Output)
|
||||
}
|
||||
if strings.Contains(result.Output, "alpha") || strings.Contains(result.Output, "delta") {
|
||||
t.Fatalf("mixed compatible range leaked outside requested slice: %q", result.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileToolSchemasExposeOnlyCanonicalArguments(t *testing.T) {
|
||||
readProperties := NewScopedReadFileTool(t.TempDir(), nil).Parameters().Properties
|
||||
for _, want := range []string{"path", "offset", "limit", "byte_offset", "byte_limit"} {
|
||||
if _, ok := readProperties[want]; !ok {
|
||||
t.Fatalf("read_file schema missing %q", want)
|
||||
}
|
||||
}
|
||||
for _, legacy := range []string{"start_line", "end_line", "max_lines"} {
|
||||
if _, ok := readProperties[legacy]; ok {
|
||||
t.Fatalf("read_file schema must not expose legacy argument %q", legacy)
|
||||
}
|
||||
}
|
||||
|
||||
grepProperties := NewScopedGrepTool(t.TempDir(), nil).Parameters().Properties
|
||||
if _, ok := grepProperties["case_insensitive"]; !ok {
|
||||
t.Fatal("grep schema missing case_insensitive")
|
||||
}
|
||||
if _, ok := grepProperties["-i"]; ok {
|
||||
t.Fatal("grep schema must not expose the -i alias")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToolMarksTruncation(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "notes.txt"), "a\nb\nc\nd\ne")
|
||||
@@ -52,7 +154,7 @@ func TestReadFileToolMarksTruncation(t *testing.T) {
|
||||
}
|
||||
// The cut must be visible in the rendered output, not just the Truncated flag,
|
||||
// and must tell the model where to continue.
|
||||
if !strings.Contains(result.Output, "[truncated:") || !strings.Contains(result.Output, "start_line=3") {
|
||||
if !strings.Contains(result.Output, "[truncated:") || !strings.Contains(result.Output, "offset=3") {
|
||||
t.Fatalf("expected truncation marker pointing to the next line, got %q", result.Output)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +170,7 @@ func TestReadFileToolAppliesByteBudget(t *testing.T) {
|
||||
if result.Status != StatusOK || !result.Truncated {
|
||||
t.Fatalf("expected ok+truncated, got status=%s truncated=%v", result.Status, result.Truncated)
|
||||
}
|
||||
if !strings.Contains(result.Output, "output exceeded") || !strings.Contains(result.Output, "start_line") {
|
||||
if !strings.Contains(result.Output, "output exceeded") || !strings.Contains(result.Output, "offset/limit") {
|
||||
t.Fatalf("expected byte-budget continuation hint, got %q", result.Output[len(result.Output)-200:])
|
||||
}
|
||||
if result.Meta["raw_bytes"] == "" || result.Meta["emitted_bytes"] == "" {
|
||||
|
||||
@@ -377,5 +377,5 @@ func fileConflictMessage(relativePath string) string {
|
||||
}
|
||||
|
||||
func fileUnseenMessage(relativePath string) string {
|
||||
return "Error writing " + relativePath + ": the intended change depends on content that has not been read exactly in this session. Read the affected lines with read_file, or use byte_offset/byte_limit for an oversized single line, then retry the edit."
|
||||
return "Error writing " + relativePath + ": the intended change depends on content that has not been read exactly in this session. Read the affected lines with read_file using offset/limit (or byte_offset/byte_limit for a very long line), then retry the edit."
|
||||
}
|
||||
@@ -48,7 +48,6 @@ func NewScopedGrepTool(workspaceRoot string, scope PathScope) Tool {
|
||||
"path": {Type: "string", Description: "Directory or file to search. Relative paths stay in the workspace; use an absolute path to search a granted extra root. Defaults to workspace root.", Default: "."},
|
||||
"glob": {Type: "string", Description: `Optional glob filter, for example "**/*.go".`},
|
||||
"output_mode": {Type: "string", Description: "Output mode.", Enum: []string{"content", "files_with_matches", "count"}, Default: "content"},
|
||||
"-i": {Type: "boolean", Description: "Case insensitive search.", Default: false},
|
||||
"case_insensitive": {Type: "boolean", Description: "Case insensitive search.", Default: false},
|
||||
"head_limit": {Type: "integer", Description: "Maximum content results to return.", Default: 50, Minimum: intPtr(1)},
|
||||
},
|
||||
|
||||
+50
-25
@@ -31,16 +31,15 @@ func NewScopedReadFileTool(workspaceRoot string, scope PathScope) Tool {
|
||||
return readFileTool{
|
||||
baseTool: baseTool{
|
||||
name: "read_file",
|
||||
description: "Read a file by 1-based inclusive line range, or by exact byte chunks for oversized single-line files.",
|
||||
description: "Read exact file text with line numbers. Use for comments, formatting, or edits; prefer read_minified_file for initial code understanding. Use offset and limit for a line range.",
|
||||
parameters: Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"path": {Type: "string", Description: "Path of the file to read."},
|
||||
"start_line": {Type: "integer", Description: "1-based inclusive line number to start reading from.", Minimum: intPtr(1)},
|
||||
"end_line": {Type: "integer", Description: "1-based inclusive line number to stop reading at.", Minimum: intPtr(1)},
|
||||
"max_lines": {Type: "integer", Description: "Maximum number of lines to return.", Minimum: intPtr(1)},
|
||||
"byte_offset": {Type: "integer", Description: "Zero-based byte offset for an exact chunk read. Use with byte_limit for oversized single-line files; cannot be combined with line ranges.", Minimum: intPtr(0)},
|
||||
"byte_limit": {Type: "integer", Description: "Maximum source bytes returned in exact byte mode. Defaults to 65536.", Default: readFileByteChunkMax, Minimum: intPtr(1), Maximum: intPtr(readFileByteChunkMax)},
|
||||
"path": {Type: "string", Description: "File path."},
|
||||
"offset": {Type: "integer", Description: "Optional 1-based source line to start from.", Minimum: intPtr(1)},
|
||||
"limit": {Type: "integer", Description: "Optional number of source lines to return.", Minimum: intPtr(1)},
|
||||
"byte_offset": {Type: "integer", Description: "Optional zero-based offset for exact byte reads.", Minimum: intPtr(0)},
|
||||
"byte_limit": {Type: "integer", Description: "Optional byte count for an exact byte read; use with byte_offset.", Minimum: intPtr(1), Maximum: intPtr(readFileByteChunkMax)},
|
||||
},
|
||||
Required: []string{"path"},
|
||||
AdditionalProperties: false,
|
||||
@@ -69,7 +68,27 @@ func (tool readFileTool) run(args map[string]any, options RunOptions, directBudg
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
startLine, err := intArg(args, "start_line", 1, 1, 0)
|
||||
_, hasOffset := args["offset"]
|
||||
_, hasLimit := args["limit"]
|
||||
_, hasStartLine := args["start_line"]
|
||||
_, hasEndLine := args["end_line"]
|
||||
_, hasMaxLines := args["max_lines"]
|
||||
if hasOffset && hasStartLine {
|
||||
return errorResult("Error: Invalid arguments for read_file: use either offset or start_line, not both")
|
||||
}
|
||||
if hasLimit && (hasEndLine || hasMaxLines) {
|
||||
return errorResult("Error: Invalid arguments for read_file: use limit instead of end_line or max_lines, not both")
|
||||
}
|
||||
|
||||
startKey := "offset"
|
||||
if !hasOffset && hasStartLine {
|
||||
startKey = "start_line"
|
||||
}
|
||||
startLine, err := intArg(args, startKey, 1, 1, 0)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
limit, err := intArg(args, "limit", 0, 1, 0)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
@@ -81,19 +100,25 @@ func (tool readFileTool) run(args map[string]any, options RunOptions, directBudg
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
if hasLimit {
|
||||
const maxInt = int(^uint(0) >> 1)
|
||||
endLine = startLine + min(limit-1, maxInt-startLine)
|
||||
}
|
||||
hasLineRange := hasOffset || hasLimit || hasStartLine || hasEndLine || hasMaxLines
|
||||
_, hasByteOffset := args["byte_offset"]
|
||||
_, hasByteLimit := args["byte_limit"]
|
||||
byteMode := hasByteOffset || hasByteLimit
|
||||
byteOffset, err := intArg(args, "byte_offset", 0, 0, 0)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
byteLimit, err := intArg(args, "byte_limit", readFileByteChunkMax, 1, readFileByteChunkMax)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
if byteMode && (args["start_line"] != nil || args["end_line"] != nil || args["max_lines"] != nil) {
|
||||
return errorResult("Error: Invalid arguments for read_file: byte_offset/byte_limit cannot be combined with line ranges")
|
||||
byteMode := (hasByteOffset || hasByteLimit) && !hasLineRange
|
||||
byteOffset := 0
|
||||
byteLimit := readFileByteChunkMax
|
||||
if byteMode {
|
||||
byteOffset, err = intArg(args, "byte_offset", 0, 0, 0)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
byteLimit, err = intArg(args, "byte_limit", readFileByteChunkMax, 1, readFileByteChunkMax)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_file: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
absolutePath, relativePath, err := resolveScopedReadPath(tool.workspaceRoot, tool.scope, requestedPath)
|
||||
@@ -169,7 +194,7 @@ func renderedReadRange(total, start, end, maxLines int) (int, int) {
|
||||
|
||||
func renderReadFileRange(absolutePath string, relativePath string, total int, startLine int, endLine int, maxLines int, maxBytes int) Result {
|
||||
if startLine > total {
|
||||
return okResult(fmt.Sprintf("File: %s\n(start_line %d is past the end of the file, which has %d lines)", relativePath, startLine, total))
|
||||
return okResult(fmt.Sprintf("File: %s\n(offset %d is past the end of the file, which has %d lines)", relativePath, startLine, total))
|
||||
}
|
||||
if endLine == 0 || endLine > total {
|
||||
endLine = total
|
||||
@@ -208,9 +233,9 @@ func renderReadFileRange(absolutePath string, relativePath string, total int, st
|
||||
// Tool.Run calls preserve the legacy prefix-only byte budget.
|
||||
var budgetedOutput *outputBudgetBuilder
|
||||
if maxBytes > 0 {
|
||||
budgetedOutput = newOutputBudgetBuilder(maxBytes, "use start_line/end_line or max_lines for normal files; use byte_offset/byte_limit for an oversized single line")
|
||||
budgetedOutput = newOutputBudgetBuilder(maxBytes, "use offset/limit to request a smaller line range")
|
||||
} else {
|
||||
budgetedOutput = newHeadTailOutputBudgetBuilder(readOutputBudgetBytes, "use start_line/end_line or max_lines for normal files; use byte_offset/byte_limit for an oversized single line")
|
||||
budgetedOutput = newHeadTailOutputBudgetBuilder(readOutputBudgetBytes, "use offset/limit to request a smaller line range")
|
||||
}
|
||||
budgetedOutput.WriteString(header)
|
||||
budgetedOutput.WriteString("\n")
|
||||
@@ -224,9 +249,9 @@ func renderReadFileRange(absolutePath string, relativePath string, total int, st
|
||||
}
|
||||
if truncated {
|
||||
// The Truncated flag alone is invisible to the model in the rendered
|
||||
// output, so it cannot tell a max_lines cut from a complete read. Make the
|
||||
// output, so it cannot tell a limit cut from a complete read. Make the
|
||||
// cut explicit and tell it how to continue.
|
||||
budgetedOutput.WriteString(fmt.Sprintf("\n\n[truncated: %d more line(s) in the requested range not shown; set start_line=%d to continue]", endLine-lastLine, lastLine+1))
|
||||
budgetedOutput.WriteString(fmt.Sprintf("\n\n[truncated: %d more line(s) in the requested range not shown; set offset=%d to continue]", endLine-lastLine, lastLine+1))
|
||||
}
|
||||
|
||||
budgeted := budgetedOutput.Result()
|
||||
@@ -241,7 +266,7 @@ func renderReadFileRange(absolutePath string, relativePath string, total int, st
|
||||
if budgeted.Truncated {
|
||||
meta["truncation_reason"] = "byte_budget"
|
||||
} else {
|
||||
meta["truncation_reason"] = "max_lines"
|
||||
meta["truncation_reason"] = "limit"
|
||||
}
|
||||
}
|
||||
return Result{
|
||||
|
||||
@@ -28,12 +28,13 @@ func NewScopedReadMinifiedFileTool(workspaceRoot string, scope PathScope) Tool {
|
||||
return readMinifiedFileTool{
|
||||
baseTool: baseTool{
|
||||
name: "read_minified_file",
|
||||
description: "Read a source file in a dense, token-cheap form: comments and redundant whitespace removed, no line numbers. Use it to scan or understand code for far fewer tokens than read_file. For exact text, comments, line numbers, or before editing, use read_file instead.",
|
||||
deferred: true,
|
||||
description: "Read source code in a token-efficient, language-aware form. Prefer this for initial understanding; use read_file for exact text or line numbers.",
|
||||
parameters: Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"path": {Type: "string", Description: "Path of the file to read in minified form."},
|
||||
"path": {Type: "string", Description: "Source file path."},
|
||||
"offset": {Type: "integer", Description: "Optional 1-based source line to start from.", Minimum: intPtr(1)},
|
||||
"limit": {Type: "integer", Description: "Optional maximum number of source lines to minify.", Minimum: intPtr(1)},
|
||||
},
|
||||
Required: []string{"path"},
|
||||
AdditionalProperties: false,
|
||||
@@ -59,6 +60,14 @@ func (tool readMinifiedFileTool) run(args map[string]any, options RunOptions, di
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_minified_file: " + err.Error())
|
||||
}
|
||||
offset, err := intArg(args, "offset", 1, 1, 0)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_minified_file: " + err.Error())
|
||||
}
|
||||
limit, err := intArg(args, "limit", 0, 1, 0)
|
||||
if err != nil {
|
||||
return errorResult("Error: Invalid arguments for read_minified_file: " + err.Error())
|
||||
}
|
||||
|
||||
absolutePath, relativePath, err := resolveScopedReadPath(tool.workspaceRoot, tool.scope, requestedPath)
|
||||
if err != nil {
|
||||
@@ -75,11 +84,19 @@ func (tool readMinifiedFileTool) run(args map[string]any, options RunOptions, di
|
||||
info, _ := os.Stat(absolutePath)
|
||||
options.FileTracker.Record(absolutePath, content, info)
|
||||
|
||||
result := minify.File(relativePath, content)
|
||||
rawLines := lineCount(string(content))
|
||||
selected := selectSourceLines(content, offset, limit)
|
||||
if selected.pastEnd {
|
||||
return okResult(fmt.Sprintf("File: %s\n(offset %d is past the end of the file, which has %d lines)", relativePath, offset, selected.totalLines))
|
||||
}
|
||||
ranged := offset > 1 || limit > 0
|
||||
result := minify.File(relativePath, selected.content)
|
||||
if ranged {
|
||||
result = minify.ContextualFragment(relativePath, content, selected.content, selected.startByte)
|
||||
}
|
||||
rawLines := lineCount(string(selected.content))
|
||||
minLines := lineCount(result.Content)
|
||||
pct := 0
|
||||
if rawBytes := len(content); rawBytes > 0 {
|
||||
if rawBytes := len(selected.content); rawBytes > 0 {
|
||||
if saved := rawBytes - len(result.Content); saved > 0 {
|
||||
pct = saved * 100 / rawBytes
|
||||
}
|
||||
@@ -89,12 +106,15 @@ func (tool readMinifiedFileTool) run(args map[string]any, options RunOptions, di
|
||||
if result.Applied {
|
||||
header = fmt.Sprintf("File: %s — minified %s view (comments stripped, no line numbers; %d→%d lines, ~%d%% fewer bytes). For exact text/comments or before editing, use read_file.",
|
||||
relativePath, result.Language, rawLines, minLines, pct)
|
||||
} else if ranged {
|
||||
header = fmt.Sprintf("File: %s — safe ranged view (whitespace normalized, no line numbers; %d→%d lines; context-sensitive stripping disabled because the range may begin inside a multiline construct). For exact text, use read_file.",
|
||||
relativePath, rawLines, minLines)
|
||||
} else {
|
||||
header = fmt.Sprintf("File: %s — whitespace-normalized view (no line numbers; %d→%d lines; full minification not available for this file type). For exact text, use read_file.",
|
||||
relativePath, rawLines, minLines)
|
||||
}
|
||||
|
||||
rawBytes := len(content)
|
||||
rawBytes := len(selected.content)
|
||||
compactBytes := len(result.Content)
|
||||
savedTokens := 0
|
||||
if savedBytes := rawBytes - compactBytes; savedBytes > 0 {
|
||||
@@ -113,11 +133,54 @@ func (tool readMinifiedFileTool) run(args map[string]any, options RunOptions, di
|
||||
meta["estimated_tokens_saved"] = strconv.Itoa(savedTokens)
|
||||
toolResult := Result{Status: StatusOK, Output: output, Meta: meta}
|
||||
if directBudget {
|
||||
return applyLegacyByteBudgetToResult(toolResult, readOutputBudgetBytes, "use read_file with start_line/end_line or max_lines for normal files, or byte_offset/byte_limit for an oversized single line")
|
||||
return applyLegacyByteBudgetToResult(toolResult, readOutputBudgetBytes, "use offset/limit to request a smaller source range, or read_file when exact text is required")
|
||||
}
|
||||
return toolResult
|
||||
}
|
||||
|
||||
type sourceSelection struct {
|
||||
content []byte
|
||||
startByte int
|
||||
totalLines int
|
||||
pastEnd bool
|
||||
}
|
||||
|
||||
func selectSourceLines(content []byte, offset, limit int) sourceSelection {
|
||||
if offset <= 1 && limit == 0 {
|
||||
return sourceSelection{content: content, totalLines: sourceLineCount(content)}
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
totalLines := sourceLineCount(content)
|
||||
start := offset - 1
|
||||
if start >= totalLines {
|
||||
return sourceSelection{startByte: len(content), totalLines: totalLines, pastEnd: true}
|
||||
}
|
||||
startByte := 0
|
||||
for index := 0; index < start; index++ {
|
||||
startByte += len(lines[index]) + 1
|
||||
}
|
||||
end := totalLines
|
||||
if limit > 0 && limit < end-start {
|
||||
end = start + limit
|
||||
}
|
||||
return sourceSelection{
|
||||
content: []byte(strings.Join(lines[start:end], "\n")),
|
||||
startByte: startByte,
|
||||
totalLines: totalLines,
|
||||
}
|
||||
}
|
||||
|
||||
func sourceLineCount(content []byte) int {
|
||||
if len(content) == 0 {
|
||||
return 1
|
||||
}
|
||||
lines := strings.Count(string(content), "\n")
|
||||
if content[len(content)-1] != '\n' {
|
||||
lines++
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// lineCount reports the number of newline-separated lines in s (an empty string
|
||||
// counts as 0 lines, matching how a reader perceives an empty file).
|
||||
func lineCount(s string) int {
|
||||
|
||||
@@ -45,6 +45,123 @@ func TestReadMinifiedFileRejectsTraversal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMinifiedFileSelectsSourceLineRangeBeforeMinifying(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := "package demo\n\nfunc One() int { return 1 }\n\nfunc Two() int { return 2 }\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "f.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res := NewScopedReadMinifiedFileTool(dir, nil).Run(context.Background(), map[string]any{
|
||||
"path": "f.go", "offset": 5, "limit": 1,
|
||||
})
|
||||
if res.Status != StatusOK || !strings.Contains(res.Output, "func Two") || strings.Contains(res.Output, "func One") {
|
||||
t.Fatalf("unexpected ranged compact read: status=%s\n%s", res.Status, res.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMinifiedFileRangesPreserveUnknownLexicalContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
src string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "multiline string",
|
||||
path: "snippet.py",
|
||||
src: "value = \"\"\"\n# literal text\nstill literal\n\"\"\"\nprint(value)\n",
|
||||
want: []string{"# literal text", "still literal"},
|
||||
},
|
||||
{
|
||||
name: "template literal",
|
||||
path: "snippet.js",
|
||||
src: "const value = `\n// literal text\nstill literal\n`;\n",
|
||||
want: []string{"// literal text", "still literal"},
|
||||
},
|
||||
{
|
||||
name: "block comment",
|
||||
path: "snippet.c",
|
||||
src: "/* open\ncomment body\n*/\nint live;\n",
|
||||
want: []string{"comment body", "*/"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, tc.path), []byte(tc.src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res := NewScopedReadMinifiedFileTool(dir, nil).Run(context.Background(), map[string]any{
|
||||
"path": tc.path, "offset": 2, "limit": 2,
|
||||
})
|
||||
if res.Status != StatusOK || res.Meta["compacted"] != "false" {
|
||||
t.Fatalf("ranged read must use conservative normalization: status=%s meta=%#v\n%s", res.Status, res.Meta, res.Output)
|
||||
}
|
||||
for _, want := range tc.want {
|
||||
if !strings.Contains(res.Output, want) {
|
||||
t.Fatalf("ranged read lost lexical content %q:\n%s", want, res.Output)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMinifiedFileRangeInsideGoRawStringIsConservative(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := "package demo\nvar value = `first\nSECRET-MARKER-ONE\n// literal line\nlast`\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "f.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res := NewScopedReadMinifiedFileTool(dir, nil).Run(context.Background(), map[string]any{
|
||||
"path": "f.go", "offset": 3, "limit": 2,
|
||||
})
|
||||
if res.Status != StatusOK || res.Meta["compacted"] != "false" {
|
||||
t.Fatalf("raw-string range must use conservative normalization: status=%s meta=%#v\n%s", res.Status, res.Meta, res.Output)
|
||||
}
|
||||
for _, want := range []string{"SECRET-MARKER-ONE", "// literal line"} {
|
||||
if !strings.Contains(res.Output, want) {
|
||||
t.Fatalf("raw-string range lost or rewrote %q:\n%s", want, res.Output)
|
||||
}
|
||||
}
|
||||
if strings.Contains(res.Output, "SECRET - MARKER - ONE") {
|
||||
t.Fatalf("raw-string contents were parsed as Go code:\n%s", res.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMinifiedFileCanonicalOffsetPastEnd(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "f.go"), []byte("package demo\nvar value = 1\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res := NewScopedReadMinifiedFileTool(dir, nil).Run(context.Background(), map[string]any{
|
||||
"path": "f.go", "offset": 10,
|
||||
})
|
||||
if res.Status != StatusOK || !strings.Contains(res.Output, "offset 10 is past the end") {
|
||||
t.Fatalf("expected canonical out-of-range response, got status=%s output=%q", res.Status, res.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMinifiedFileMaximumLimitDoesNotOverflow(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("alpha\nbeta\ngamma\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const maxInt = int(^uint(0) >> 1)
|
||||
|
||||
res := NewScopedReadMinifiedFileTool(dir, nil).Run(context.Background(), map[string]any{
|
||||
"path": "notes.txt", "offset": 2, "limit": maxInt,
|
||||
})
|
||||
if res.Status != StatusOK {
|
||||
t.Fatalf("maximum limit should return the remaining range without panicking: status=%s output=%q", res.Status, res.Output)
|
||||
}
|
||||
if !strings.Contains(res.Output, "beta") || !strings.Contains(res.Output, "gamma") || strings.Contains(res.Output, "alpha") {
|
||||
t.Fatalf("maximum limit returned the wrong range: %q", res.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMinifiedFileAppliesByteBudget(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "large.txt"), []byte(strings.Repeat("0123456789abcdef\n", 9000)), 0o644); err != nil {
|
||||
|
||||
@@ -297,8 +297,8 @@ func scrubResultSecrets(res Result) Result {
|
||||
|
||||
func CoreReadOnlyToolsScoped(workspaceRoot string, scope PathScope) []Tool {
|
||||
return []Tool{
|
||||
NewScopedReadFileTool(workspaceRoot, scope),
|
||||
NewScopedReadMinifiedFileTool(workspaceRoot, scope),
|
||||
NewScopedReadFileTool(workspaceRoot, scope),
|
||||
// view_image is read-only and shares read_file's path scoping, so it
|
||||
// belongs with the other readers rather than behind a gate.
|
||||
NewScopedViewImageTool(workspaceRoot, scope),
|
||||
|
||||
@@ -523,25 +523,24 @@ func TestIsDeferralEligibleDecouplesFromDeferred(t *testing.T) {
|
||||
|
||||
func TestOptionalBuiltinsUseDeferredDiscovery(t *testing.T) {
|
||||
wantDeferred := map[string]bool{
|
||||
"bash": true,
|
||||
"browser_action": true,
|
||||
"browser_click": true,
|
||||
"browser_connect": true,
|
||||
"browser_install": true,
|
||||
"browser_launch": true,
|
||||
"browser_open": true,
|
||||
"browser_press": true,
|
||||
"browser_snapshot": true,
|
||||
"browser_type": true,
|
||||
"capture_artifact": true,
|
||||
"desktop_action": true,
|
||||
"desktop_snapshot": true,
|
||||
"desktop_windows": true,
|
||||
"lsp_navigate": true,
|
||||
"read_minified_file": true,
|
||||
"terminal_session": true,
|
||||
"web_fetch": true,
|
||||
"web_search": true,
|
||||
"bash": true,
|
||||
"browser_action": true,
|
||||
"browser_click": true,
|
||||
"browser_connect": true,
|
||||
"browser_install": true,
|
||||
"browser_launch": true,
|
||||
"browser_open": true,
|
||||
"browser_press": true,
|
||||
"browser_snapshot": true,
|
||||
"browser_type": true,
|
||||
"capture_artifact": true,
|
||||
"desktop_action": true,
|
||||
"desktop_snapshot": true,
|
||||
"desktop_windows": true,
|
||||
"lsp_navigate": true,
|
||||
"terminal_session": true,
|
||||
"web_fetch": true,
|
||||
"web_search": true,
|
||||
}
|
||||
for _, tool := range BuiltinCatalog(t.TempDir()) {
|
||||
if _, listed := wantDeferred[tool.Name()]; listed {
|
||||
@@ -549,7 +548,7 @@ func TestOptionalBuiltinsUseDeferredDiscovery(t *testing.T) {
|
||||
t.Errorf("%s should be deferred", tool.Name())
|
||||
}
|
||||
delete(wantDeferred, tool.Name())
|
||||
} else if tool.Name() == "read_file" || tool.Name() == "grep" || tool.Name() == ExecCommandToolName || tool.Name() == "edit_file" {
|
||||
} else if tool.Name() == "read_file" || tool.Name() == "read_minified_file" || tool.Name() == "grep" || tool.Name() == ExecCommandToolName || tool.Name() == "edit_file" {
|
||||
if IsDeferred(tool) {
|
||||
t.Errorf("essential tool %s must stay eager", tool.Name())
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func NewUpdatePlanTool() *updatePlanTool {
|
||||
return &updatePlanTool{
|
||||
baseTool: baseTool{
|
||||
name: "update_plan",
|
||||
description: "Create or update the in-memory plan for the current task. " +
|
||||
description: "Create or update the in-memory plan for implementation or investigation with at least three meaningful dependent steps. Do not use it for simple lookups, explanations, or code navigation. " +
|
||||
"Pass the full ordered list of steps each call; it replaces the previous plan. " +
|
||||
"Each item needs a `content` string; `status` defaults to \"pending\" and `id` is " +
|
||||
"auto-numbered, so you only need to supply `content` (and `status` as the task progresses). " +
|
||||
|
||||
Reference in new issue
Block a user