返回 DeepSeek-Reasonix
editsource.go
根目录 / internal / tool / builtin / editsource.go
1 package builtin
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8
9 "reasonix/internal/fileops"
10 fileenc "reasonix/internal/fileutil/encoding"
11 "reasonix/internal/tool"
12 )
13
14 // editSource is the file state a read-modify-write tool works against, plus the
15 // route its write must take back. Read and write must stay paired: content read
16 // from the host's unsaved buffer has to return there, and content read from disk
17 // has to return to disk. Mixing the two silently drops one side's changes.
18 type editSource struct {
19 content string
20 enc fileenc.Kind
21 overlay bool
22 id fileIdentity
23 }
24
25 func overlayObservationTarget(overlay FileOverlay, path string) fileops.Target {
26 if identified, ok := overlay.(FileOverlayIdentity); ok {
27 return fileops.OverlayTargetWithIdentity(path, identified.FileOverlayIdentity())
28 }
29 return fileops.OverlayTarget(path)
30 }
31
32 // readEditSource resolves path the way Execute and Preview must both see it:
33 // the host's unsaved editor buffer when the overlay can serve it, otherwise the
34 // decoded disk content. A non-UTF-8 file always stays on the disk route — the
35 // overlay contract is text-only, so routing GBK or UTF-16 through it would
36 // rewrite the file as UTF-8.
37 func readEditSource(ctx context.Context, overlay FileOverlay, path string) (source editSource, readErr error) {
38 data, id, err := readDiskIdentity(path)
39 if err != nil {
40 return editSource{}, err
41 }
42 if !id.existed {
43 if overlay != nil && filepath.IsAbs(path) {
44 if buffered, ok := overlay.ReadTextFile(ctx, path); ok {
45 return editSource{content: buffered, enc: fileenc.UTF8, overlay: true, id: overlayIdentity(buffered)}, nil
46 }
47 }
48 return editSource{enc: fileenc.UTF8, id: id}, &os.PathError{Op: "read", Path: path, Err: os.ErrNotExist}
49 }
50 enc, _ := fileenc.Detect(data)
51 content := string(fileenc.Decode(data, enc))
52 if overlay != nil && enc == fileenc.UTF8 && filepath.IsAbs(path) {
53 if buffered, ok := overlay.ReadTextFile(ctx, path); ok {
54 return editSource{content: buffered, enc: enc, overlay: true, id: overlayIdentity(buffered)}, nil
55 }
56 }
57 return editSource{content: content, enc: enc, id: id}, nil
58 }
59
60 // lockMutationPath serializes all structured mutations of one real target in
61 // this process. Existing hard-link aliases share the native file identity.
62 func lockMutationPath(path string) func() {
63 info, _ := os.Stat(path)
64 target := fileops.DiskTarget(path, info)
65 target.Route = "mutation"
66 return fileops.Lock(target)
67 }
68
69 func (s editSource) observation(overlay FileOverlay, path string) (fileops.Target, fileops.Version, error) {
70 if s.overlay {
71 return overlayObservationTarget(overlay, path), fileops.OverlayVersion(s.content), nil
72 }
73 return s.id.target, s.id.version, nil
74 }
75
76 func (s editSource) requireObserved(ctx context.Context, overlay FileOverlay, path string) error {
77 store := fileops.FromContext(ctx)
78 if store == nil { // Direct package-level tool calls remain usable in tests and embeddings.
79 return nil
80 }
81 target, version, err := s.observation(overlay, path)
82 if err != nil {
83 return &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.FSNotFound, Path: path, Recovery: "read the file, then retry the edit"}, Cause: err}
84 }
85 observed := store.Get(target)
86 if observed.Kind != fileops.Present {
87 return &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.FSNotObserved, Path: path, Recovery: "read any current window of this file, then retry"}, Cause: fmt.Errorf("file has not been observed in this agent session")}
88 }
89 if observed.Version != version {
90 return &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.FSStaleVersion, Path: path, ExpectedSnapshot: string(observed.Version), ActualSnapshot: string(version), Recovery: "the file changed after it was read; read it again, then retry"}, Cause: ErrFileChanged}
91 }
92 return nil
93 }
94
95 func (s editSource) commitObservation(ctx context.Context, overlay FileOverlay, path, content string) {
96 store := fileops.FromContext(ctx)
97 if store == nil {
98 return
99 }
100 if s.overlay {
101 store.ObservePresent(overlayObservationTarget(overlay, path), fileops.OverlayVersion(content))
102 return
103 }
104 if info, err := os.Stat(path); err == nil {
105 target, version := fileops.DiskSnapshot(path, info)
106 store.ObservePresent(target, version)
107 } else {
108 store.Forget(fileops.DiskTarget(path, nil))
109 }
110 }
111
112 func (s editSource) readSnapshot(path string) string {
113 kind, prefix := tool.ReadSourceDisk, "raw-sha256:"
114 if s.overlay {
115 kind, prefix = tool.ReadSourceOverlay, "overlay:"
116 }
117 return tool.SourceSnapshot(kind, path, fmt.Sprintf("%s%x", prefix, s.id.sum))
118 }
119
120 // write persists content on the same route the source was read from. An overlay
121 // that declines a managed write leaves its outcome unknown. It never falls
122 // back to disk because that would update a different source than the one read.
123 func (s editSource) write(ctx context.Context, overlay FileOverlay, path, content string) error {
124 if err := s.assertUnchanged(ctx, overlay, path); err != nil {
125 return err
126 }
127 if s.overlay && overlay != nil {
128 if err := s.recordWrite(ctx, path, content, "overlay", overlay); err != nil {
129 return err
130 }
131 if err := s.assertUnchanged(ctx, overlay, path); err != nil {
132 return err
133 }
134 if ok, err := overlay.WriteTextFile(ctx, path, content); ok {
135 if err != nil {
136 fileops.FromContext(ctx).Forget(overlayObservationTarget(overlay, path))
137 return fmt.Errorf("write outcome unknown: %w", err)
138 }
139 if err == nil {
140 s.commitObservation(ctx, overlay, path, content)
141 }
142 return err
143 }
144 fileops.FromContext(ctx).Forget(overlayObservationTarget(overlay, path))
145 return fmt.Errorf("write outcome unknown: original overlay did not confirm the write")
146 }
147 if err := s.recordWrite(ctx, path, content, "disk", overlay); err != nil {
148 return err
149 }
150 if err := s.assertUnchanged(ctx, overlay, path); err != nil {
151 return err
152 }
153 if err := writeFileEncoded(path, content, s.enc); err != nil {
154 return err
155 }
156 s.commitObservation(ctx, overlay, path, content)
157 return nil
158 }
159
159 lines GO