listDirTool function
Creates the ls tool: lists directory entries sorted alphabetically
(case-insensitive), directories suffixed with /, capped at limit
entries (default defaultLsEntryLimit) and defaultToolMaxBytes bytes.
Implementation
AgentTool listDirTool(ExecutionEnv env) {
return AgentTool(
name: 'ls',
label: 'ls',
tier: ApprovalTier.read,
description:
'List directory contents. Returns entries sorted alphabetically, '
"with '/' suffix for directories. Output is truncated to "
'$defaultLsEntryLimit entries or ${defaultToolMaxBytes ~/ 1024}KB '
'(whichever is hit first).',
parameters: const {
'type': 'object',
'properties': {
'path': {
'type': 'string',
'description': 'Directory to list (default: current directory)',
},
'limit': {
'type': 'integer',
'description': 'Maximum number of entries to return (default: 500)',
},
},
},
execute: (arguments, cancelToken, onUpdate) async {
cancelToken?.throwIfCancelled();
final path = (arguments['path'] as String?) ?? '.';
final limit =
(arguments['limit'] as num?)?.toInt() ?? defaultLsEntryLimit;
// POSIX ls accepts a file path and prints the file name. Check the
// target kind first so we don't fail with notDirectory for a file.
final info = await env.fileInfo(path);
if (info.isErr) {
// Fall back to listing if we can't stat (e.g. path with a trailing
// slash or a backend where fileInfo is unsupported).
if (info.errorOrNull!.code != FileErrorCode.notSupported) {
throw StateError('${info.errorOrNull}');
}
} else if (info.valueOrNull!.kind == FileKind.file) {
return ToolExecutionResult.text(info.valueOrNull!.name);
}
final listed = await env.listDir(path);
if (listed.isErr) throw StateError('${listed.errorOrNull}');
cancelToken?.throwIfCancelled();
final entries = listed.valueOrNull!.toList()
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
final results = <String>[];
var entryLimitReached = false;
for (final entry in entries) {
if (results.length >= limit) {
entryLimitReached = true;
break;
}
final suffix = entry.kind == FileKind.directory ? '/' : '';
results.add('${entry.name}$suffix');
}
if (results.isEmpty && !entryLimitReached) {
return ToolExecutionResult.text('(empty directory)');
}
// Byte truncation only: the entry count is already capped above.
final truncation = _truncateHead(results.join('\n'), maxLines: 1 << 62);
var output = truncation.content;
final notices = <String>[];
if (entryLimitReached) {
notices.add(
'$limit entries limit reached. Use limit=${limit * 2} for more',
);
}
if (truncation.truncated) {
notices.add('${formatToolSize(defaultToolMaxBytes)} limit reached');
}
if (notices.isNotEmpty) output += '\n\n[${notices.join('. ')}]';
return ToolExecutionResult.text(output);
},
);
}