extractToolStats function
Extract tool usage statistics from session messages.
Processes each message to count tool invocations, detect language usage, track git operations, measure response times, and identify errors.
Implementation
ToolStatsResult extractToolStats(List<Map<String, dynamic>> messages) {
final stats = ToolStatsResult();
String? lastAssistantTimestamp;
for (final msg in messages) {
final type = msg['type'] as String?;
final message = msg['message'] as Map<String, dynamic>?;
final msgTimestamp = msg['timestamp'] as String?;
if (type == 'assistant' && message != null) {
if (msgTimestamp != null) {
lastAssistantTimestamp = msgTimestamp;
}
// Track token usage.
final usage = message['usage'] as Map<String, dynamic>?;
if (usage != null) {
stats.inputTokens += (usage['input_tokens'] as int?) ?? 0;
stats.outputTokens += (usage['output_tokens'] as int?) ?? 0;
}
// Process content blocks.
final content = message['content'];
if (content is List) {
for (final block in content) {
if (block is! Map<String, dynamic>) continue;
if (block['type'] == 'tool_use' && block['name'] != null) {
final toolName = block['name'] as String;
stats.toolCounts[toolName] = (stats.toolCounts[toolName] ?? 0) + 1;
// Check for special tool usage.
if (toolName == 'Task' || toolName == 'dispatch_agent') {
stats.usesTaskAgent = true;
}
if (toolName.startsWith('mcp__')) stats.usesMcp = true;
if (toolName == 'WebSearch') stats.usesWebSearch = true;
if (toolName == 'WebFetch') stats.usesWebFetch = true;
final input = block['input'] as Map<String, dynamic>?;
if (input != null) {
final filePath = input['file_path'] as String? ?? '';
if (filePath.isNotEmpty) {
final lang = getLanguageFromPath(filePath);
if (lang != null) {
stats.languages[lang] = (stats.languages[lang] ?? 0) + 1;
}
// Track files modified by Edit/Write tools.
if (toolName == 'Edit' || toolName == 'Write') {
stats.filesModified.add(filePath);
}
}
// Count lines changed in Edit operations.
if (toolName == 'Edit') {
final oldString = input['old_string'] as String? ?? '';
final newString = input['new_string'] as String? ?? '';
final oldLines = oldString.split('\n').length;
final newLines = newString.split('\n').length;
if (newLines > oldLines) {
stats.linesAdded += newLines - oldLines;
} else {
stats.linesRemoved += oldLines - newLines;
}
}
// Track lines from Write tool (all added).
if (toolName == 'Write') {
final writeContent = input['content'] as String? ?? '';
if (writeContent.isNotEmpty) {
stats.linesAdded += _countChar(writeContent, '\n') + 1;
}
}
// Track git operations.
final command = input['command'] as String? ?? '';
if (command.contains('git commit')) stats.gitCommits++;
if (command.contains('git push')) stats.gitPushes++;
}
}
}
}
}
// Process user messages.
if (type == 'user' && message != null) {
final content = message['content'];
bool isHumanMessage = false;
if (content is String && content.trim().isNotEmpty) {
isHumanMessage = true;
} else if (content is List) {
for (final block in content) {
if (block is Map<String, dynamic> &&
block['type'] == 'text' &&
block['text'] != null) {
isHumanMessage = true;
break;
}
}
}
// Track message hours and response times for actual human messages.
if (isHumanMessage && msgTimestamp != null) {
try {
final msgDate = DateTime.parse(msgTimestamp);
stats.messageHours.add(msgDate.hour);
stats.userMessageTimestamps.add(msgTimestamp);
} catch (_) {
// Skip invalid timestamps.
}
// Calculate response time.
if (lastAssistantTimestamp != null) {
try {
final assistantTime = DateTime.parse(
lastAssistantTimestamp,
).millisecondsSinceEpoch;
final userTime = DateTime.parse(
msgTimestamp,
).millisecondsSinceEpoch;
final responseTimeSec = (userTime - assistantTime) / 1000.0;
// Only count reasonable response times (2s-1 hour).
if (responseTimeSec > 2 && responseTimeSec < 3600) {
stats.userResponseTimes.add(responseTimeSec);
}
} catch (_) {
// Skip invalid timestamps.
}
}
}
// Process tool results for error tracking.
if (content is List) {
for (final block in content) {
if (block is Map<String, dynamic> &&
block['type'] == 'tool_result' &&
block['is_error'] == true) {
stats.toolErrors++;
final resultContent = block['content'] as String? ?? '';
final category = categorizeToolError(resultContent);
stats.toolErrorCategories[category] =
(stats.toolErrorCategories[category] ?? 0) + 1;
}
}
}
// Check for interruptions.
if (content is String &&
content.contains('[Request interrupted by user')) {
stats.userInterruptions++;
} else if (content is List) {
for (final block in content) {
if (block is Map<String, dynamic> &&
block['type'] == 'text' &&
(block['text'] as String? ?? '').contains(
'[Request interrupted by user',
)) {
stats.userInterruptions++;
break;
}
}
}
}
}
return stats;
}