createEditTool function

Tool<JsonMap, String> createEditTool(
  1. Genkit ai, {
  2. required ToolRuntime runtime,
})

Builds the tool that edits a file through exact string replacement.

Implementation

Tool<JsonMap, String> createEditTool(
  Genkit ai, {
  required ToolRuntime runtime,
}) {
  return defineTextTool(
    ai,
    runtime: runtime,
    name: 'edit',
    description: _description,
    properties: <String, Object?>{
      'file_path': stringProperty('The absolute path of the file to modify.'),
      'old_string': stringProperty('The exact text to replace.'),
      'new_string': stringProperty('The text to replace it with. Must differ from old_string.'),
      'replace_all': booleanProperty('Replace every occurrence of old_string. Defaults to false.'),
    },
    required: <String>['file_path', 'old_string', 'new_string'],
    run: (input) async {
      final path = runtime.resolvePath(requireString(input, 'file_path'));
      final oldString = readString(input, 'old_string') ?? '';
      final newString = readString(input, 'new_string') ?? '';
      final replaceAll = readBool(input, 'replace_all') ?? false;

      if (!runtime.isWithinWorkspace(path)) {
        throw ToolFailure('$path is outside the workspace ${runtime.workspaceRoot}.');
      }
      if (oldString.isEmpty) {
        throw const ToolFailure('`old_string` must not be empty. Use the write tool to create a file.');
      }
      if (oldString == newString) {
        throw const ToolFailure('`old_string` and `new_string` are identical; there is nothing to change.');
      }
      final tracked = runtime.trackedFile(path);
      if (tracked == null) {
        throw const ToolFailure('The file has not been read yet. Read it with the read tool before editing.');
      }
      if (!await runtime.fileMatchesTrackedState(path)) {
        throw const ToolFailure('The file changed since it was last read. Read it again before editing.');
      }

      final content = tracked.content;
      final occurrences = _countOccurrences(content, oldString);
      if (occurrences == 0) {
        throw const ToolFailure(
          '`old_string` was not found in the file. Check whitespace and indentation, and make sure '
          'you did not include line-number prefixes from the read output.',
        );
      }
      if (occurrences > 1 && !replaceAll) {
        throw ToolFailure(
          '`old_string` occurs $occurrences times. Add surrounding context to make it unique, '
          'or set `replace_all` to true to change every occurrence.',
        );
      }

      runtime.startTool('edit', runtime.displayPath(path));
      final firstIndex = content.indexOf(oldString);
      final updated = replaceAll
          ? content.replaceAll(oldString, newString)
          : content.replaceFirst(oldString, newString);

      final file = File(path);
      await file.writeAsString(updated);
      runtime.recordFileRead(path, updated, await file.stat());

      if (replaceAll) {
        return 'Replaced $occurrences occurrences in $path.';
      }
      return 'Edited $path. Snippet of the result:\n${_snippet(updated, firstIndex, newString)}';
    },
  );
}