feat(telemetry): Integrate telemetry for expand-all, aggregate results

This commit implements AI usage telemetry for the `expand-all-tasks` command/tool and refactors its CLI output for clarity and consistency.

Key Changes:

1.  **Telemetry Integration for `expand-all-tasks` (Subtask 77.8):**\n    -   The `expandAllTasks` core logic (`scripts/modules/task-manager/expand-all-tasks.js`) now calls the `expandTask` function for each eligible task and collects the individual `telemetryData` returned.\n    -   A new helper function `_aggregateTelemetry` (in `utils.js`) is used to sum up token counts and costs from all individual expansions into a single `telemetryData` object for the entire `expand-all` operation.\n    -   The `expandAllTasksDirect` wrapper (`mcp-server/src/core/direct-functions/expand-all-tasks.js`) now receives and passes this aggregated `telemetryData` in the MCP response.\n    -   For CLI usage, `displayAiUsageSummary` is called once with the aggregated telemetry.

2.  **Improved CLI Output for `expand-all`:**\n    -   The `expandAllTasks` core function now handles displaying a final "Expansion Summary" box (showing Attempted, Expanded, Skipped, Failed counts) directly after the aggregated telemetry summary.\n    -   This consolidates all summary output within the core function for better flow and removes redundant logging from the command action in `scripts/modules/commands.js`.\n    -   The summary box border is green for success and red if any expansions failed.

3.  **Code Refinements:**\n    -   Ensured `chalk` and `boxen` are imported in `expand-all-tasks.js` for the new summary box.\n    -   Minor adjustments to logging messages for clarity.
This commit is contained in:
Eyal Toledano
2025-05-08 18:22:00 -04:00
parent ab84afd036
commit 21c3cb8cda
24 changed files with 1693 additions and 56 deletions

View File

@@ -1129,12 +1129,6 @@ function registerCommands(programInstance) {
{} // Pass empty context for CLI calls
// outputFormat defaults to 'text' in expandAllTasks for CLI
);
// Optional: Display summary from result
console.log(chalk.green(`Expansion Summary:`));
console.log(chalk.green(` - Attempted: ${result.tasksToExpand}`));
console.log(chalk.green(` - Expanded: ${result.expandedCount}`));
console.log(chalk.yellow(` - Skipped: ${result.skippedCount}`));
console.log(chalk.red(` - Failed: ${result.failedCount}`));
} catch (error) {
console.error(
chalk.red(`Error expanding all tasks: ${error.message}`)

View File

@@ -1,7 +1,14 @@
import { log, readJSON, isSilentMode } from '../utils.js';
import { startLoadingIndicator, stopLoadingIndicator } from '../ui.js';
import {
startLoadingIndicator,
stopLoadingIndicator,
displayAiUsageSummary
} from '../ui.js';
import expandTask from './expand-task.js';
import { getDebugFlag } from '../config-manager.js';
import { _aggregateTelemetry } from '../utils.js';
import chalk from 'chalk';
import boxen from 'boxen';
/**
* Expand all eligible pending or in-progress tasks using the expandTask function.
@@ -14,7 +21,7 @@ import { getDebugFlag } from '../config-manager.js';
* @param {Object} [context.session] - Session object from MCP.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [outputFormat='text'] - Output format ('text' or 'json'). MCP calls should use 'json'.
* @returns {Promise<{success: boolean, expandedCount: number, failedCount: number, skippedCount: number, tasksToExpand: number, message?: string}>} - Result summary.
* @returns {Promise<{success: boolean, expandedCount: number, failedCount: number, skippedCount: number, tasksToExpand: number, telemetryData: Array<Object>}>} - Result summary.
*/
async function expandAllTasks(
tasksPath,
@@ -51,8 +58,8 @@ async function expandAllTasks(
let loadingIndicator = null;
let expandedCount = 0;
let failedCount = 0;
// No skipped count needed now as the filter handles it upfront
let tasksToExpandCount = 0; // Renamed for clarity
let tasksToExpandCount = 0;
const allTelemetryData = []; // Still collect individual data first
if (!isMCPCall && outputFormat === 'text') {
loadingIndicator = startLoadingIndicator(
@@ -90,6 +97,7 @@ async function expandAllTasks(
failedCount: 0,
skippedCount: 0,
tasksToExpand: 0,
telemetryData: allTelemetryData,
message: 'No tasks eligible for expansion.'
};
// --- End Fix ---
@@ -97,19 +105,6 @@ async function expandAllTasks(
// Iterate over the already filtered tasks
for (const task of tasksToExpand) {
// --- Remove Redundant Check ---
// The check below is no longer needed as the initial filter handles it
/*
if (task.subtasks && task.subtasks.length > 0 && !force) {
logger.info(
`Skipping task ${task.id}: Already has subtasks. Use --force to overwrite.`
);
skippedCount++;
continue;
}
*/
// --- End Removed Redundant Check ---
// Start indicator for individual task expansion in CLI mode
let taskIndicator = null;
if (!isMCPCall && outputFormat === 'text') {
@@ -117,17 +112,23 @@ async function expandAllTasks(
}
try {
// Call the refactored expandTask function
await expandTask(
// Call the refactored expandTask function AND capture result
const result = await expandTask(
tasksPath,
task.id,
numSubtasks, // Pass numSubtasks, expandTask handles defaults/complexity
numSubtasks,
useResearch,
additionalContext,
context, // Pass the whole context object { session, mcpLog }
force // Pass the force flag down
force
);
expandedCount++;
// Collect individual telemetry data
if (result && result.telemetryData) {
allTelemetryData.push(result.telemetryData);
}
if (taskIndicator) {
stopLoadingIndicator(taskIndicator, `Task ${task.id} expanded.`);
}
@@ -146,18 +147,48 @@ async function expandAllTasks(
}
}
// Log final summary (removed skipped count from message)
// --- AGGREGATION AND DISPLAY ---
logger.info(
`Expansion complete: ${expandedCount} expanded, ${failedCount} failed.`
);
// Return summary (skippedCount is now 0) - Add success: true here as well for consistency
// Aggregate the collected telemetry data
const aggregatedTelemetryData = _aggregateTelemetry(
allTelemetryData,
'expand-all-tasks'
);
if (outputFormat === 'text') {
const summaryContent =
`${chalk.white.bold('Expansion Summary:')}\n\n` +
`${chalk.cyan('-')} Attempted: ${chalk.bold(tasksToExpandCount)}\n` +
`${chalk.green('-')} Expanded: ${chalk.bold(expandedCount)}\n` +
// Skipped count is always 0 now due to pre-filtering
`${chalk.gray('-')} Skipped: ${chalk.bold(0)}\n` +
`${chalk.red('-')} Failed: ${chalk.bold(failedCount)}`;
console.log(
boxen(summaryContent, {
padding: 1,
margin: { top: 1 },
borderColor: failedCount > 0 ? 'red' : 'green', // Red if failures, green otherwise
borderStyle: 'round'
})
);
}
if (outputFormat === 'text' && aggregatedTelemetryData) {
displayAiUsageSummary(aggregatedTelemetryData, 'cli');
}
// Return summary including the AGGREGATED telemetry data
return {
success: true, // Indicate overall success
success: true,
expandedCount,
failedCount,
skippedCount: 0,
tasksToExpand: tasksToExpandCount
tasksToExpand: tasksToExpandCount,
telemetryData: aggregatedTelemetryData
};
} catch (error) {
if (loadingIndicator)

View File

@@ -508,6 +508,61 @@ function detectCamelCaseFlags(args) {
return camelCaseFlags;
}
/**
* Aggregates an array of telemetry objects into a single summary object.
* @param {Array<Object>} telemetryArray - Array of telemetryData objects.
* @param {string} overallCommandName - The name for the aggregated command.
* @returns {Object|null} Aggregated telemetry object or null if input is empty.
*/
function _aggregateTelemetry(telemetryArray, overallCommandName) {
if (!telemetryArray || telemetryArray.length === 0) {
return null;
}
const aggregated = {
timestamp: new Date().toISOString(), // Use current time for aggregation time
userId: telemetryArray[0].userId, // Assume userId is consistent
commandName: overallCommandName,
modelUsed: 'Multiple', // Default if models vary
providerName: 'Multiple', // Default if providers vary
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
totalCost: 0,
currency: telemetryArray[0].currency || 'USD' // Assume consistent currency or default
};
const uniqueModels = new Set();
const uniqueProviders = new Set();
const uniqueCurrencies = new Set();
telemetryArray.forEach((item) => {
aggregated.inputTokens += item.inputTokens || 0;
aggregated.outputTokens += item.outputTokens || 0;
aggregated.totalCost += item.totalCost || 0;
uniqueModels.add(item.modelUsed);
uniqueProviders.add(item.providerName);
uniqueCurrencies.add(item.currency || 'USD');
});
aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens;
aggregated.totalCost = parseFloat(aggregated.totalCost.toFixed(6)); // Fix precision
if (uniqueModels.size === 1) {
aggregated.modelUsed = [...uniqueModels][0];
}
if (uniqueProviders.size === 1) {
aggregated.providerName = [...uniqueProviders][0];
}
if (uniqueCurrencies.size > 1) {
aggregated.currency = 'Multiple'; // Mark if currencies actually differ
} else if (uniqueCurrencies.size === 1) {
aggregated.currency = [...uniqueCurrencies][0];
}
return aggregated;
}
// Export all utility functions and configuration
export {
LOG_LEVELS,
@@ -529,5 +584,6 @@ export {
isSilentMode,
resolveEnvVariable,
getTaskManager,
findProjectRoot
findProjectRoot,
_aggregateTelemetry
};