readFileTool function
AgentTool
readFileTool(
- ExecutionEnv env, {
- HashlineSnapshotStore? snapshots,
- Model? model()?,
- SqliteEngine? sqlite,
- String? builtinText(
- 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,
);
},
);
}