readFileTool function

AgentTool readFileTool(
  1. ExecutionEnv env, {
  2. HashlineSnapshotStore? snapshots,
  3. Model? model()?,
  4. SqliteEngine? sqlite,
  5. String? builtinText(
    1. String path
    )?,
})

Implementation

AgentTool readFileTool(
  ExecutionEnv env, {
  HashlineSnapshotStore? snapshots,
  Model? Function()? model,
  SqliteEngine? sqlite,
  String? Function(String path)? builtinText,
}) {
  final store = snapshots ?? HashlineSnapshotStore();
  return AgentTool(
    name: 'read',
    label: 'read',
    tier: ApprovalTier.read,
    description: _readDescription(sqlite: sqlite),
    parameters: const {
      'type': 'object',
      'properties': {
        'path': {
          'type': 'string',
          'description':
              'Path to the file to read (relative or absolute). May end '
              'with a trailing selector such as :50-100 or :raw, address an '
              'archive member (archive.zip:inner/file), or address a SQLite '
              'database (data.db:table?limit=20)',
        },
        'offset': {
          'type': 'integer',
          'description': 'Line number to start reading from (1-indexed)',
        },
        'limit': {
          'type': 'integer',
          'description': 'Maximum number of lines to read',
        },
        'hashline': {
          'type': 'boolean',
          'description':
              'Prefix each line with its line number and prepend a '
              '[path#TAG] content-hash header for anchoring hashline edit '
              'patches (default: false)',
        },
      },
      'required': ['path'],
    },
    execute: (arguments, cancelToken, onUpdate) async {
      cancelToken?.throwIfCancelled();
      final rawPath = arguments['path'] as String;
      final offset = (arguments['offset'] as num?)?.toInt();
      final limit = (arguments['limit'] as num?)?.toInt();
      final hashlineMode = (arguments['hashline'] as bool?) ?? false;

      // Peel a trailing selector off the path (omp's grammar). A literal file
      // whose name ends in a selector-shaped tail (`test:1-2`) wins over the
      // selector interpretation.
      final split = await splitPathAndSelPreferringLiteral(rawPath, env);
      final parsed = parseSel(split.sel);
      // Issue #862: a selector already pins the window, so offset/limit are
      // ignored with a notice instead of hard-rejecting the call.
      final windowNotice = readWindowCoercionNotice(
        hasSelector: parsed is! ReadSelectorNone,
        offset: offset,
        limit: limit,
      );

      final extended = await _readExtendedTarget(
        env,
        rawPath,
        split,
        sqlite,
        cancelToken,
      );
      if (extended != null) return _withNotice(extended, windowNotice);

      final path = split.path;
      // Built-in skills (issue #1151): builtin:// paths resolve from the
      // embedded copy before any filesystem access.
      final embedded = _resolveBuiltinText(path, builtinText);
      if (embedded == null) {
        final binaryRead = await env.readBinaryFile(path);
        if (binaryRead.isErr) {
          // gh-1444 AC1: a skill pointer sibling (<path>.pointer) resolves
          // the read onto the compiled-in builtin body — the seeded
          // `.fah/skills/<name>/SKILL.md.pointer` files become transparent.
          // A present-but-invalid pointer is refused loudly (E1); no
          // pointer keeps the original error.
          final followed = await followSkillPointer(
            path,
            (pointerPath) async =>
                (await env.readTextFile(pointerPath)).valueOrNull,
          );
          switch (followed) {
            case SkillPointerResolved(:final text):
              return _withNotice(
                await _readTextContent(
                  env,
                  store,
                  path,
                  parsed,
                  offset,
                  limit,
                  text,
                  hashlineMode,
                  cancelToken,
                ),
                windowNotice,
              );
            case SkillPointerRefused(:final reason):
              throw StateError('read $path: skill pointer refused: $reason');
            case SkillPointerAbsent():
              throw StateError('${binaryRead.errorOrNull}');
          }
        } else {
          final bytes = binaryRead.valueOrNull!;
          cancelToken?.throwIfCancelled();

          final imageResult = _readImageResult(path, bytes, parsed, model);
          if (imageResult != null) {
            return _withNotice(imageResult, windowNotice);
          }
        }
      }

      return _withNotice(
        await _readTextContent(
          env,
          store,
          path,
          parsed,
          offset,
          limit,
          embedded,
          hashlineMode,
          cancelToken,
        ),
        windowNotice,
      );
    },
  );
}