返回 DeepSeek-Reasonix
multiedit.go
根目录 / internal / tool / builtin / multiedit.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7
8 "reasonix/internal/tool"
9 )
10
11 func init() { tool.RegisterBuiltin(multiEdit{}) }
12
13 // multiEdit applies a batch of edits to one file. roots confines the target to
14 // the workspace when non-empty (see writeFile); guard rejects Reasonix
15 // session-data targets (see SessionDataGuard); workDir, when non-empty, is the
16 // directory a relative path resolves against (see resolveIn).
17 type multiEdit struct {
18 roots []string
19 guard SessionDataGuard
20 managed ManagedConfigPaths
21 workDir string
22 }
23
24 // editStep is one edit in a multi_edit operation. Mirrors edit_file's args
25 // plus a per-step replace_all toggle so a single call can mix targeted and
26 // sweep replacements (e.g. rename a function with replace_all, then patch
27 // one specific call site with a unique-match edit).
28 type editStep struct {
29 OldString string `json:"old_string"`
30 NewString string `json:"new_string"`
31 ReplaceAll bool `json:"replace_all,omitempty"`
32 }
33
34 func (multiEdit) Name() string { return "multi_edit" }
35
36 func (multiEdit) Description() string {
37 return "Apply a list of edits to a single file atomically: each edit runs against the result of the previous one, all in memory; the file is rewritten only if every edit succeeds. Cheaper and safer than chaining edit_file calls — a failure in step 3 leaves the file untouched instead of half-edited."
38 }
39
40 func (multiEdit) Schema() json.RawMessage {
41 return json.RawMessage(`{
42 "type":"object",
43 "properties":{
44 "path":{"type":"string","description":"File path"},
45 "edits":{
46 "type":"array",
47 "minItems":1,
48 "description":"Ordered edits. Each step sees the file as left by the previous step.",
49 "items":{
50 "type":"object",
51 "properties":{
52 "old_string":{"type":"string","description":"Exact text to find. Without replace_all, must match exactly once."},
53 "new_string":{"type":"string","description":"Replacement text (empty deletes)."},
54 "replace_all":{"type":"boolean","description":"Replace every occurrence instead of requiring uniqueness."}
55 },
56 "required":["old_string","new_string"]
57 }
58 }
59 },
60 "required":["path","edits"]
61 }`)
62 }
63
64 func (multiEdit) ReadOnly() bool { return false }
65
66 func (m multiEdit) Execute(ctx context.Context, args json.RawMessage) (string, error) {
67 var p struct {
68 Path string `json:"path"`
69 Edits []editStep `json:"edits"`
70 }
71 if err := json.Unmarshal(args, &p); err != nil {
72 return "", fmt.Errorf("invalid args: %w", err)
73 }
74 if p.Path == "" {
75 return "", fmt.Errorf("path is required")
76 }
77 if len(p.Edits) == 0 {
78 return "", fmt.Errorf("edits must not be empty")
79 }
80 p.Path = resolveIn(m.workDir, p.Path)
81 if err := confineWrite(ctx, m.roots, m.guard, m.managed, p.Path); err != nil {
82 return "", err
83 }
84
85 content, enc, err := readFileEncoded(p.Path)
86 if err != nil {
87 return "", fmt.Errorf("read %s: %w", p.Path, err)
88 }
89
90 // Apply edits in order against the running in-memory buffer. Any failure
91 // returns before the write, leaving the file untouched — that's the
92 // safety guarantee that makes multi_edit preferable to chained
93 // edit_file calls.
94 applied := 0
95 usedFuzzy := false
96 receipts := make([]editReplacementReceipt, 0, len(p.Edits))
97 for i, step := range p.Edits {
98 if step.OldString == "" {
99 return "", fmt.Errorf("edit %d: old_string is required", i+1)
100 }
101 result := applyOldStringEdit(content, step.OldString, step.NewString, step.ReplaceAll)
102 switch {
103 case result.applied > 0:
104 content = result.updated
105 applied += result.applied
106 usedFuzzy = usedFuzzy || result.fuzzy
107 receipts = append(receipts, result.receipt)
108 case result.matches == 0:
109 return "", fmt.Errorf("edit %d: %w", i+1, oldStringNotFoundError(p.Path, step.OldString, content))
110 default:
111 return "", fmt.Errorf("edit %d: %w", i+1, oldStringNotUniqueError(p.Path, step.OldString, content, result.matches, true))
112 }
113 }
114
115 if err := writeFileEncoded(p.Path, content, enc); err != nil {
116 return "", fmt.Errorf("write %s: %w", p.Path, err)
117 }
118 summary := fmt.Sprintf("multi_edit %s: %d edits applied (%d total replacements)", p.Path, len(p.Edits), applied)
119 if usedFuzzy {
120 summary += " (fuzzy match)"
121 }
122 return withActualPostWriteReceipts(summary, receipts), nil
123 }
124
124 lines GO