| 82 | } |
| 83 | |
| 84 | func EditFiles(options EditFilesOptions) fantasy.AgentTool { |
| 85 | return fantasy.NewAgentTool( |
| 86 | "edit_files", |
| 87 | "Perform edits on one or more files by replacing old_text with"+ |
| 88 | " new_text. Matching is fuzzy (tolerates whitespace and indentation"+ |
| 89 | " differences) and preserves the file's existing indentation and"+ |
| 90 | " line endings. Errors if old_text matches zero locations, or more"+ |
| 91 | " than one unless replace_all is set. All edits in a batch are"+ |
| 92 | " validated before any file is written.", |
| 93 | func(ctx context.Context, args EditFilesArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { |
| 94 | var planPath string |
| 95 | if options.IsPlanTurn && len(args.Files) > 0 { |
| 96 | resolvedPlanPath, err := resolvePlanTurnPath(ctx, options.ResolvePlanPath) |
| 97 | if err != nil { |
| 98 | return fantasy.NewTextErrorResponse(err.Error()), nil |
| 99 | } |
| 100 | for i := range args.Files { |
| 101 | args.Files[i].Path = strings.TrimSpace(args.Files[i].Path) |
| 102 | if args.Files[i].Path != resolvedPlanPath { |
| 103 | return fantasy.NewTextErrorResponse("during plan turns, edit_files is restricted to " + resolvedPlanPath), nil |
| 104 | } |
| 105 | } |
| 106 | planPath = resolvedPlanPath |
| 107 | } |
| 108 | if options.GetWorkspaceConn == nil { |
| 109 | return fantasy.NewTextErrorResponse("workspace connection resolver is not configured"), nil |
| 110 | } |
| 111 | conn, err := options.GetWorkspaceConn(ctx) |
| 112 | if err != nil { |
| 113 | return fantasy.NewTextErrorResponse(err.Error()), nil |
| 114 | } |
| 115 | if planPath != "" { |
| 116 | if err := ensurePlanPathResolvesToItself(ctx, conn, planPath); err != nil { |
| 117 | return fantasy.NewTextErrorResponse(err.Error()), nil |
| 118 | } |
| 119 | } |
| 120 | return executeEditFilesTool(ctx, conn, args, options.ResolvePlanPath) |
| 121 | }, |
| 122 | ) |
| 123 | } |
| 124 | |
| 125 | func executeEditFilesTool( |
| 126 | ctx context.Context, |