createEditTool function

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

Builds the tool that edits an existing file via exact string replacements.

Implementation

Tool<JsonMap, String> createEditTool(
  Genkit ai, {
  required ToolRuntime runtime,
}) {
  const toolName = 'edit';
  return ai.defineTool(
    name: toolName,
    description: 'Edits an existing file using exact string replacements.',
    inputSchema: createJsonSchema(
      <String, dynamic>{
        'type': 'object',
        'description': 'Input for the edit tool.',
        'properties': <String, dynamic>{
          'file_path': <String, dynamic>{'type': 'string'},
          'edits': <String, dynamic>{
            'type': 'array',
            'items': <String, dynamic>{
              'type': 'object',
              'properties': <String, dynamic>{
                'old_text': <String, dynamic>{'type': 'string'},
                'new_text': <String, dynamic>{'type': 'string'},
                'replace_all': <String, dynamic>{'type': 'boolean'},
              },
              'required': <String>['old_text', 'new_text'],
              'additionalProperties': false,
            },
          },
        },
        'required': <String>['file_path', 'edits'],
        'additionalProperties': false,
      },
    ),
    outputSchema: stringOutputSchema,
    fn: (input, _) async {
      final filePath = readString(input, 'file_path')?.trim();
      final editsInput = readList(input, 'edits');

      if (filePath == null || filePath.isEmpty) {
        const error = 'Error: `file_path` is required.';
        runtime.endTool(toolName, error);
        return error;
      }
      if (!_isAbsolutePath(filePath)) {
        const error = 'Error: `file_path` must be an absolute path.';
        runtime.endTool(toolName, error);
        return error;
      }
      if (!runtime.isWithinWorkspace(filePath)) {
        const error = 'Error: edit operations must stay inside the workspace.';
        runtime.endTool(toolName, error);
        return error;
      }
      if (editsInput == null || editsInput.isEmpty) {
        const error = 'Error: `edits` must contain at least one edit.';
        runtime.endTool(toolName, error);
        return error;
      }

      final tracked = runtime.trackedFile(filePath);
      if (tracked == null) {
        const error = 'Error: file must be read with the read tool before editing.';
        runtime.endTool(toolName, error);
        return error;
      }
      if (!await runtime.fileMatchesTrackedState(filePath)) {
        const error = 'Error: file has changed since it was last read. Read it again before editing.';
        runtime.endTool(toolName, error);
        return error;
      }

      runtime.startTool(toolName, filePath);
      var content = tracked.content;
      var replacementsMade = 0;

      for (final rawEdit in editsInput) {
        if (rawEdit is! Map) {
          const error = 'Error: each edit must be an object.';
          runtime.endTool(toolName, error);
          return error;
        }
        final edit = rawEdit.map((key, value) => MapEntry('$key', value));
        final oldText = readString(edit, 'old_text');
        final newText = readString(edit, 'new_text') ?? '';
        final replaceAll = readBool(edit, 'replace_all') ?? false;

        if (oldText == null || oldText.isEmpty) {
          const error = 'Error: every edit must include non-empty `old_text`.';
          runtime.endTool(toolName, error);
          return error;
        }
        if (!content.contains(oldText)) {
          final error = 'Error: could not find text to replace: $oldText';
          runtime.endTool(toolName, error);
          return error;
        }

        if (replaceAll) {
          final matches = RegExp(RegExp.escape(oldText)).allMatches(content).length;
          content = content.replaceAll(oldText, newText);
          replacementsMade += matches;
        } else {
          content = content.replaceFirst(oldText, newText);
          replacementsMade += 1;
        }
      }

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

      final output = 'Updated $filePath with $replacementsMade replacement(s).';
      runtime.endTool(toolName, output);
      return output;
    },
  );
}