refactor(tasks): Align update-tasks with unified AI service and remove obsolete helpers
Completes the refactoring of the AI-interacting task management functions by aligning `update-tasks.js` with the unified service architecture and removing now-unused helper files.
Key Changes:
- **`update-tasks.js` Refactoring:**
- Replaced direct AI client calls and AI-specific config fetching with a call to `generateTextService` from `ai-services-unified.js`.
- Preserved the original system and user prompts requesting a JSON array output.
- Implemented manual JSON parsing (`parseUpdatedTasksFromText`) with Zod validation to handle the text response reliably.
- Updated the core function signature to accept the standard `context` object (`{ session, mcpLog }`).
- Corrected logger implementation to handle both MCP (`mcpLog`) and CLI (`consoleLog`) contexts appropriately.
- **Related Component Updates:**
- Refactored `mcp-server/src/core/direct-functions/update-tasks.js` to use the standard direct function pattern (logger wrapper, silent mode, call core function with context).
- Verified `mcp-server/src/tools/update.js` correctly passes arguments and context.
- Verified `scripts/modules/commands.js` (update command) correctly calls the refactored core function.
- **Obsolete File Cleanup:**
- Removed the now-unused `scripts/modules/task-manager/get-subtasks-from-ai.js` file and its export, as its functionality was integrated into `expand-task.js`.
- Removed the now-unused `scripts/modules/task-manager/generate-subtask-prompt.js` file and its export for the same reason.
- **Task Management:**
- Marked subtasks 61.38, 61.39, and 61.41 as complete.
This commit finalizes the alignment of `updateTasks`, `updateTaskById`, `expandTask`, `expandAllTasks`, `analyzeTaskComplexity`, `addTask`, and `parsePRD` with the unified AI service and configuration management patterns.
This commit is contained in:
@@ -211,7 +211,7 @@ function registerCommands(programInstance) {
|
||||
)
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const fromId = parseInt(options.from, 10);
|
||||
const fromId = parseInt(options.from, 10); // Validation happens here
|
||||
const prompt = options.prompt;
|
||||
const useResearch = options.research || false;
|
||||
|
||||
@@ -260,7 +260,14 @@ function registerCommands(programInstance) {
|
||||
);
|
||||
}
|
||||
|
||||
await updateTasks(tasksPath, fromId, prompt, useResearch);
|
||||
// Call core updateTasks, passing empty context for CLI
|
||||
await updateTasks(
|
||||
tasksPath,
|
||||
fromId,
|
||||
prompt,
|
||||
useResearch,
|
||||
{} // Pass empty context
|
||||
);
|
||||
});
|
||||
|
||||
// update-task command
|
||||
|
||||
@@ -22,8 +22,6 @@ import removeSubtask from './task-manager/remove-subtask.js';
|
||||
import updateSubtaskById from './task-manager/update-subtask-by-id.js';
|
||||
import removeTask from './task-manager/remove-task.js';
|
||||
import taskExists from './task-manager/task-exists.js';
|
||||
import generateSubtaskPrompt from './task-manager/generate-subtask-prompt.js';
|
||||
import getSubtasksFromAI from './task-manager/get-subtasks-from-ai.js';
|
||||
import isTaskDependentOn from './task-manager/is-task-dependent.js';
|
||||
|
||||
// Export task manager functions
|
||||
@@ -47,7 +45,5 @@ export {
|
||||
removeTask,
|
||||
findTaskById,
|
||||
taskExists,
|
||||
generateSubtaskPrompt,
|
||||
getSubtasksFromAI,
|
||||
isTaskDependentOn
|
||||
};
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Generate a prompt for creating subtasks from a task
|
||||
* @param {Object} task - The task to generate subtasks for
|
||||
* @param {number} numSubtasks - Number of subtasks to generate
|
||||
* @param {string} additionalContext - Additional context to include in the prompt
|
||||
* @param {Object} taskAnalysis - Optional complexity analysis for the task
|
||||
* @returns {string} - The generated prompt
|
||||
*/
|
||||
function generateSubtaskPrompt(
|
||||
task,
|
||||
numSubtasks,
|
||||
additionalContext = '',
|
||||
taskAnalysis = null
|
||||
) {
|
||||
// Build the system prompt
|
||||
const basePrompt = `You need to break down the following task into ${numSubtasks} specific subtasks that can be implemented one by one.
|
||||
|
||||
Task ID: ${task.id}
|
||||
Title: ${task.title}
|
||||
Description: ${task.description || 'No description provided'}
|
||||
Current details: ${task.details || 'No details provided'}
|
||||
${additionalContext ? `\nAdditional context to consider: ${additionalContext}` : ''}
|
||||
${taskAnalysis ? `\nComplexity analysis: This task has a complexity score of ${taskAnalysis.complexityScore}/10.` : ''}
|
||||
${taskAnalysis && taskAnalysis.reasoning ? `\nReasoning for complexity: ${taskAnalysis.reasoning}` : ''}
|
||||
|
||||
Subtasks should:
|
||||
1. Be specific and actionable implementation steps
|
||||
2. Follow a logical sequence
|
||||
3. Each handle a distinct part of the parent task
|
||||
4. Include clear guidance on implementation approach
|
||||
5. Have appropriate dependency chains between subtasks
|
||||
6. Collectively cover all aspects of the parent task
|
||||
|
||||
Return exactly ${numSubtasks} subtasks with the following JSON structure:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "First subtask title",
|
||||
"description": "Detailed description",
|
||||
"dependencies": [],
|
||||
"details": "Implementation details"
|
||||
},
|
||||
...more subtasks...
|
||||
]
|
||||
|
||||
Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use an empty array if there are no dependencies.`;
|
||||
|
||||
return basePrompt;
|
||||
}
|
||||
|
||||
export default generateSubtaskPrompt;
|
||||
@@ -19,7 +19,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
// Determine if we're in MCP mode by checking for mcpLog
|
||||
const isMcpMode = !!options?.mcpLog;
|
||||
|
||||
log('info', `Reading tasks from ${tasksPath}...`);
|
||||
log('info', `Preparing to regenerate task files in ${tasksPath}`);
|
||||
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
@@ -31,13 +31,10 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
log('info', `Found ${data.tasks.length} tasks to generate files for.`);
|
||||
log('info', `Found ${data.tasks.length} tasks to regenerate`);
|
||||
|
||||
// Validate and fix dependencies before generating files
|
||||
log(
|
||||
'info',
|
||||
`Validating and fixing dependencies before generating files...`
|
||||
);
|
||||
log('info', `Validating and fixing dependencies`);
|
||||
validateAndFixDependencies(data, tasksPath);
|
||||
|
||||
// Generate task files
|
||||
@@ -120,7 +117,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
|
||||
// Write the file
|
||||
fs.writeFileSync(taskPath, content);
|
||||
log('info', `Generated: task_${task.id.toString().padStart(3, '0')}.txt`);
|
||||
// log('info', `Generated: task_${task.id.toString().padStart(3, '0')}.txt`); // Pollutes the CLI output
|
||||
});
|
||||
|
||||
log(
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { log, isSilentMode } from '../utils.js';
|
||||
|
||||
import {
|
||||
_handleAnthropicStream,
|
||||
getConfiguredAnthropicClient,
|
||||
parseSubtasksFromText
|
||||
} from '../ai-services.js';
|
||||
|
||||
// Import necessary config getters
|
||||
import {
|
||||
getMainModelId,
|
||||
getMainMaxTokens,
|
||||
getMainTemperature,
|
||||
getResearchModelId,
|
||||
getResearchMaxTokens,
|
||||
getResearchTemperature
|
||||
} from '../config-manager.js';
|
||||
|
||||
/**
|
||||
* Call AI to generate subtasks based on a prompt
|
||||
* @param {string} prompt - The prompt to send to the AI
|
||||
* @param {boolean} useResearch - Whether to use Perplexity for research
|
||||
* @param {Object} session - Session object from MCP
|
||||
* @param {Object} mcpLog - MCP logger object
|
||||
* @returns {Object} - Object containing generated subtasks
|
||||
*/
|
||||
async function getSubtasksFromAI(
|
||||
prompt,
|
||||
useResearch = false,
|
||||
session = null,
|
||||
mcpLog = null
|
||||
) {
|
||||
try {
|
||||
// Get the configured client
|
||||
const client = getConfiguredAnthropicClient(session);
|
||||
|
||||
// Prepare API parameters
|
||||
const apiParams = {
|
||||
model: getMainModelId(session),
|
||||
max_tokens: getMainMaxTokens(session),
|
||||
temperature: getMainTemperature(session),
|
||||
system:
|
||||
'You are an AI assistant helping with task breakdown for software development.',
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
};
|
||||
|
||||
if (mcpLog) {
|
||||
mcpLog.info('Calling AI to generate subtasks');
|
||||
}
|
||||
|
||||
let responseText;
|
||||
|
||||
// Call the AI - with research if requested
|
||||
if (useResearch && perplexity) {
|
||||
if (mcpLog) {
|
||||
mcpLog.info('Using Perplexity AI for research-backed subtasks');
|
||||
}
|
||||
|
||||
const perplexityModel = getResearchModelId(session);
|
||||
const result = await perplexity.chat.completions.create({
|
||||
model: perplexityModel,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are an AI assistant helping with task breakdown for software development. Research implementation details and provide comprehensive subtasks.'
|
||||
},
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
temperature: getResearchTemperature(session),
|
||||
max_tokens: getResearchMaxTokens(session)
|
||||
});
|
||||
|
||||
responseText = result.choices[0].message.content;
|
||||
} else {
|
||||
// Use regular Claude
|
||||
if (mcpLog) {
|
||||
mcpLog.info('Using Claude for generating subtasks');
|
||||
}
|
||||
|
||||
// Call the streaming API
|
||||
responseText = await _handleAnthropicStream(
|
||||
client,
|
||||
apiParams,
|
||||
{ mcpLog, silentMode: isSilentMode() },
|
||||
!isSilentMode()
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure we have a valid response
|
||||
if (!responseText) {
|
||||
throw new Error('Empty response from AI');
|
||||
}
|
||||
|
||||
// Try to parse the subtasks
|
||||
try {
|
||||
const parsedSubtasks = parseSubtasksFromText(responseText);
|
||||
if (
|
||||
!parsedSubtasks ||
|
||||
!Array.isArray(parsedSubtasks) ||
|
||||
parsedSubtasks.length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to parse valid subtasks array from AI response'
|
||||
);
|
||||
}
|
||||
return { subtasks: parsedSubtasks };
|
||||
} catch (parseError) {
|
||||
if (mcpLog) {
|
||||
mcpLog.error(`Error parsing subtasks: ${parseError.message}`);
|
||||
mcpLog.error(`Response start: ${responseText.substring(0, 200)}...`);
|
||||
} else {
|
||||
log('error', `Error parsing subtasks: ${parseError.message}`);
|
||||
}
|
||||
// Return error information instead of fallback subtasks
|
||||
return {
|
||||
error: parseError.message,
|
||||
taskId: null, // This will be filled in by the calling function
|
||||
suggestion:
|
||||
'Use \'task-master update-task --id=<id> --prompt="Generate subtasks for this task"\' to manually create subtasks.'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
if (mcpLog) {
|
||||
mcpLog.error(`Error generating subtasks: ${error.message}`);
|
||||
} else {
|
||||
log('error', `Error generating subtasks: ${error.message}`);
|
||||
}
|
||||
// Return error information instead of fallback subtasks
|
||||
return {
|
||||
error: error.message,
|
||||
taskId: null, // This will be filled in by the calling function
|
||||
suggestion:
|
||||
'Use \'task-master update-task --id=<id> --prompt="Generate subtasks for this task"\' to manually create subtasks.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default getSubtasksFromAI;
|
||||
@@ -2,8 +2,15 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod'; // Keep Zod for post-parsing validation
|
||||
|
||||
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
|
||||
import {
|
||||
log as consoleLog,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
truncate,
|
||||
isSilentMode
|
||||
} from '../utils.js';
|
||||
|
||||
import {
|
||||
getStatusWithColor,
|
||||
@@ -21,68 +28,195 @@ import {
|
||||
getMainTemperature
|
||||
} from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
|
||||
// Zod schema for validating the structure of tasks AFTER parsing
|
||||
const updatedTaskSchema = z
|
||||
.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.union([z.number().int(), z.string()])),
|
||||
priority: z.string().optional(),
|
||||
details: z.string().optional(),
|
||||
testStrategy: z.string().optional(),
|
||||
subtasks: z.array(z.any()).optional() // Keep subtasks flexible for now
|
||||
})
|
||||
.strip(); // Allow potential extra fields during parsing if needed, then validate structure
|
||||
const updatedTaskArraySchema = z.array(updatedTaskSchema);
|
||||
|
||||
/**
|
||||
* Update tasks based on new context
|
||||
* Parses an array of task objects from AI's text response.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} expectedCount - Expected number of tasks.
|
||||
* @param {Function | Object} logFn - The logging function (consoleLog) or MCP log object.
|
||||
* @param {boolean} isMCP - Flag indicating if logFn is MCP logger.
|
||||
* @returns {Array} Parsed and validated tasks array.
|
||||
* @throws {Error} If parsing or validation fails.
|
||||
*/
|
||||
function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
|
||||
// Helper for consistent logging inside parser
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logFn[level] === 'function') logFn[level](...args);
|
||||
else logFn.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
// Check silent mode for consoleLog
|
||||
consoleLog(level, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
report(
|
||||
'info',
|
||||
'Attempting to parse updated tasks array from text response...'
|
||||
);
|
||||
if (!text || text.trim() === '')
|
||||
throw new Error('AI response text is empty.');
|
||||
|
||||
let cleanedResponse = text.trim();
|
||||
const originalResponseForDebug = cleanedResponse;
|
||||
|
||||
// Extract from Markdown code block first
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```(?:json)?\s*([\s\S]*?)\s*```/
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
report('info', 'Extracted JSON content from Markdown code block.');
|
||||
} else {
|
||||
// If no code block, find first '[' and last ']' for the array
|
||||
const firstBracket = cleanedResponse.indexOf('[');
|
||||
const lastBracket = cleanedResponse.lastIndexOf(']');
|
||||
if (firstBracket !== -1 && lastBracket > firstBracket) {
|
||||
cleanedResponse = cleanedResponse.substring(
|
||||
firstBracket,
|
||||
lastBracket + 1
|
||||
);
|
||||
report('info', 'Extracted content between first [ and last ].');
|
||||
} else {
|
||||
report(
|
||||
'warn',
|
||||
'Response does not appear to contain a JSON array structure. Parsing raw response.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse the array
|
||||
let parsedTasks;
|
||||
try {
|
||||
parsedTasks = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
report('error', `Failed to parse JSON array: ${parseError.message}`);
|
||||
report(
|
||||
'error',
|
||||
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to parse JSON response array: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate Array structure
|
||||
if (!Array.isArray(parsedTasks)) {
|
||||
report(
|
||||
'error',
|
||||
`Parsed content is not an array. Type: ${typeof parsedTasks}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Parsed content sample: ${JSON.stringify(parsedTasks).substring(0, 200)}`
|
||||
);
|
||||
throw new Error('Parsed AI response is not a valid JSON array.');
|
||||
}
|
||||
|
||||
report('info', `Successfully parsed ${parsedTasks.length} potential tasks.`);
|
||||
if (expectedCount && parsedTasks.length !== expectedCount) {
|
||||
report(
|
||||
'warn',
|
||||
`Expected ${expectedCount} tasks, but parsed ${parsedTasks.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate each task object using Zod
|
||||
const validationResult = updatedTaskArraySchema.safeParse(parsedTasks);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task array failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
report('error', ` - Path '${err.path.join('.')}': ${err.message}`);
|
||||
});
|
||||
throw new Error(
|
||||
`AI response failed task structure validation: ${validationResult.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
report('info', 'Successfully validated task structure.');
|
||||
// Return the validated data, potentially filtering/adjusting length if needed
|
||||
return validationResult.data.slice(
|
||||
0,
|
||||
expectedCount || validationResult.data.length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tasks based on new context using the unified AI service.
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {number} fromId - Task ID to start updating from
|
||||
* @param {string} prompt - Prompt with new context
|
||||
* @param {boolean} useResearch - Whether to use Perplexity AI for research
|
||||
* @param {function} reportProgress - Function to report progress to MCP server (optional)
|
||||
* @param {Object} mcpLog - MCP logger object (optional)
|
||||
* @param {Object} session - Session object from MCP server (optional)
|
||||
* @param {boolean} [useResearch=false] - Whether to use the research AI role.
|
||||
* @param {Object} context - Context object containing session and mcpLog.
|
||||
* @param {Object} [context.session] - Session object from MCP server.
|
||||
* @param {Object} [context.mcpLog] - MCP logger object.
|
||||
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
||||
*/
|
||||
async function updateTasks(
|
||||
tasksPath,
|
||||
fromId,
|
||||
prompt,
|
||||
useResearch = false,
|
||||
{ reportProgress, mcpLog, session } = {}
|
||||
context = {},
|
||||
outputFormat = 'text' // Default to text for CLI
|
||||
) {
|
||||
// Determine output format based on mcpLog presence (simplification)
|
||||
const outputFormat = mcpLog ? 'json' : 'text';
|
||||
const { session, mcpLog } = context;
|
||||
// Use mcpLog if available, otherwise use the imported consoleLog function
|
||||
const logFn = mcpLog || consoleLog;
|
||||
// Flag to easily check which logger type we have
|
||||
const isMCP = !!mcpLog;
|
||||
|
||||
// Create custom reporter that checks for MCP log and silent mode
|
||||
const report = (message, level = 'info') => {
|
||||
if (mcpLog) {
|
||||
mcpLog[level](message);
|
||||
} else if (!isSilentMode() && outputFormat === 'text') {
|
||||
// Only log to console if not in silent mode and outputFormat is 'text'
|
||||
log(level, message);
|
||||
}
|
||||
};
|
||||
if (isMCP)
|
||||
logFn.info(`updateTasks called with context: session=${!!session}`);
|
||||
else logFn('info', `updateTasks called`); // CLI log
|
||||
|
||||
try {
|
||||
report(`Updating tasks from ID ${fromId} with prompt: "${prompt}"`);
|
||||
if (isMCP) logFn.info(`Updating tasks from ID ${fromId}`);
|
||||
else
|
||||
logFn(
|
||||
'info',
|
||||
`Updating tasks from ID ${fromId} with prompt: "${prompt}"`
|
||||
);
|
||||
|
||||
// Read the tasks file
|
||||
// --- Task Loading/Filtering (Unchanged) ---
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
if (!data || !data.tasks)
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Find tasks to update (ID >= fromId and not 'done')
|
||||
const tasksToUpdate = data.tasks.filter(
|
||||
(task) => task.id >= fromId && task.status !== 'done'
|
||||
);
|
||||
if (tasksToUpdate.length === 0) {
|
||||
report(
|
||||
`No tasks to update (all tasks with ID >= ${fromId} are already marked as done)`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// Only show UI elements for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`No tasks to update (all tasks with ID >= ${fromId} are already marked as done)`
|
||||
)
|
||||
);
|
||||
}
|
||||
return;
|
||||
if (isMCP)
|
||||
logFn.info(`No tasks to update (ID >= ${fromId} and not 'done').`);
|
||||
else
|
||||
logFn('info', `No tasks to update (ID >= ${fromId} and not 'done').`);
|
||||
if (outputFormat === 'text') console.log(/* yellow message */);
|
||||
return; // Nothing to do
|
||||
}
|
||||
// --- End Task Loading/Filtering ---
|
||||
|
||||
// Only show UI elements for text output (CLI)
|
||||
// --- Display Tasks to Update (CLI Only - Unchanged) ---
|
||||
if (outputFormat === 'text') {
|
||||
// Show the tasks that will be updated
|
||||
const table = new Table({
|
||||
@@ -139,8 +273,10 @@ async function updateTasks(
|
||||
)
|
||||
);
|
||||
}
|
||||
// --- End Display Tasks ---
|
||||
|
||||
// Build the system prompt
|
||||
// --- Build Prompts (Unchanged Core Logic) ---
|
||||
// Keep the original system prompt logic
|
||||
const systemPrompt = `You are an AI assistant helping to update software development tasks based on new context.
|
||||
You will be given a set of tasks and a prompt describing changes or new implementation details.
|
||||
Your job is to update the tasks to reflect these changes, while preserving their basic structure.
|
||||
@@ -159,331 +295,158 @@ Guidelines:
|
||||
|
||||
The changes described in the prompt should be applied to ALL tasks in the list.`;
|
||||
|
||||
const taskData = JSON.stringify(tasksToUpdate, null, 2);
|
||||
// Keep the original user prompt logic
|
||||
const taskDataString = JSON.stringify(tasksToUpdate, null, 2);
|
||||
const userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated tasks as a valid JSON array.`;
|
||||
// --- End Build Prompts ---
|
||||
|
||||
// Initialize variables for model selection and fallback
|
||||
let updatedTasks;
|
||||
let loadingIndicator = null;
|
||||
let claudeOverloaded = false;
|
||||
let modelAttempts = 0;
|
||||
const maxModelAttempts = 2; // Try up to 2 models before giving up
|
||||
|
||||
// Only create loading indicator for text output (CLI) initially
|
||||
if (outputFormat === 'text') {
|
||||
loadingIndicator = startLoadingIndicator(
|
||||
useResearch
|
||||
? 'Updating tasks with Perplexity AI research...'
|
||||
: 'Updating tasks with Claude AI...'
|
||||
'Calling AI service to update tasks...'
|
||||
);
|
||||
}
|
||||
|
||||
let responseText = '';
|
||||
let updatedTasks;
|
||||
|
||||
try {
|
||||
// Import the getAvailableAIModel function
|
||||
const { getAvailableAIModel } = await import('./ai-services.js');
|
||||
// --- Call Unified AI Service ---
|
||||
const role = useResearch ? 'research' : 'main';
|
||||
if (isMCP) logFn.info(`Using AI service with role: ${role}`);
|
||||
else logFn('info', `Using AI service with role: ${role}`);
|
||||
|
||||
// Try different models with fallback
|
||||
while (modelAttempts < maxModelAttempts && !updatedTasks) {
|
||||
modelAttempts++;
|
||||
const isLastAttempt = modelAttempts >= maxModelAttempts;
|
||||
let modelType = null;
|
||||
|
||||
try {
|
||||
// Get the appropriate model based on current state
|
||||
const result = getAvailableAIModel({
|
||||
claudeOverloaded,
|
||||
requiresResearch: useResearch
|
||||
});
|
||||
modelType = result.type;
|
||||
const client = result.client;
|
||||
|
||||
report(
|
||||
`Attempt ${modelAttempts}/${maxModelAttempts}: Updating tasks using ${modelType}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// Update loading indicator - only for text output
|
||||
if (outputFormat === 'text') {
|
||||
if (loadingIndicator) {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
}
|
||||
loadingIndicator = startLoadingIndicator(
|
||||
`Attempt ${modelAttempts}: Using ${modelType.toUpperCase()}...`
|
||||
);
|
||||
}
|
||||
|
||||
if (modelType === 'perplexity') {
|
||||
// Call Perplexity AI using proper format and getters
|
||||
const result = await client.chat.completions.create({
|
||||
model: getResearchModelId(session),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `${systemPrompt}\n\nAdditionally, please research the latest best practices, implementation details, and considerations when updating these tasks. Use your online search capabilities to gather relevant information. Remember to strictly follow the guidelines about preserving completed subtasks and building upon what has already been done rather than modifying or replacing it.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Here are the tasks to update:\n${taskData}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated tasks as a valid JSON array.`
|
||||
}
|
||||
],
|
||||
temperature: getResearchTemperature(session),
|
||||
max_tokens: getResearchMaxTokens(session)
|
||||
});
|
||||
|
||||
const responseText = result.choices[0].message.content;
|
||||
|
||||
// Extract JSON from response
|
||||
const jsonStart = responseText.indexOf('[');
|
||||
const jsonEnd = responseText.lastIndexOf(']');
|
||||
|
||||
if (jsonStart === -1 || jsonEnd === -1) {
|
||||
throw new Error(
|
||||
`Could not find valid JSON array in ${modelType}'s response`
|
||||
);
|
||||
}
|
||||
|
||||
const jsonText = responseText.substring(jsonStart, jsonEnd + 1);
|
||||
updatedTasks = JSON.parse(jsonText);
|
||||
} else {
|
||||
// Call Claude to update the tasks with streaming
|
||||
let responseText = '';
|
||||
let streamingInterval = null;
|
||||
|
||||
try {
|
||||
// Update loading indicator to show streaming progress - only for text output
|
||||
if (outputFormat === 'text') {
|
||||
let dotCount = 0;
|
||||
const readline = await import('readline');
|
||||
streamingInterval = setInterval(() => {
|
||||
readline.cursorTo(process.stdout, 0);
|
||||
process.stdout.write(
|
||||
`Receiving streaming response from Claude${'.'.repeat(dotCount)}`
|
||||
);
|
||||
dotCount = (dotCount + 1) % 4;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Use streaming API call with getters
|
||||
const stream = await client.messages.create({
|
||||
model: getMainModelId(session),
|
||||
max_tokens: getMainMaxTokens(session),
|
||||
temperature: getMainTemperature(session),
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Here is the task to update:
|
||||
${taskData}
|
||||
|
||||
Please update this task based on the following new context:
|
||||
${prompt}
|
||||
|
||||
IMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.
|
||||
|
||||
Return only the updated task as a valid JSON object.`
|
||||
}
|
||||
],
|
||||
stream: true
|
||||
});
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({
|
||||
progress:
|
||||
(responseText.length / getMainMaxTokens(session)) * 100
|
||||
});
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(
|
||||
`Progress: ${(responseText.length / getMainMaxTokens(session)) * 100}%`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
|
||||
report(
|
||||
`Completed streaming response from ${modelType} API (Attempt ${modelAttempts})`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// Extract JSON from response
|
||||
const jsonStart = responseText.indexOf('[');
|
||||
const jsonEnd = responseText.lastIndexOf(']');
|
||||
|
||||
if (jsonStart === -1 || jsonEnd === -1) {
|
||||
throw new Error(
|
||||
`Could not find valid JSON array in ${modelType}'s response`
|
||||
);
|
||||
}
|
||||
|
||||
const jsonText = responseText.substring(jsonStart, jsonEnd + 1);
|
||||
updatedTasks = JSON.parse(jsonText);
|
||||
} catch (streamError) {
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
|
||||
// Process stream errors explicitly
|
||||
report(`Stream error: ${streamError.message}`, 'error');
|
||||
|
||||
// Check if this is an overload error
|
||||
let isOverload = false;
|
||||
// Check 1: SDK specific property
|
||||
if (streamError.type === 'overloaded_error') {
|
||||
isOverload = true;
|
||||
}
|
||||
// Check 2: Check nested error property
|
||||
else if (streamError.error?.type === 'overloaded_error') {
|
||||
isOverload = true;
|
||||
}
|
||||
// Check 3: Check status code
|
||||
else if (
|
||||
streamError.status === 429 ||
|
||||
streamError.status === 529
|
||||
) {
|
||||
isOverload = true;
|
||||
}
|
||||
// Check 4: Check message string
|
||||
else if (
|
||||
streamError.message?.toLowerCase().includes('overloaded')
|
||||
) {
|
||||
isOverload = true;
|
||||
}
|
||||
|
||||
if (isOverload) {
|
||||
claudeOverloaded = true;
|
||||
report(
|
||||
'Claude overloaded. Will attempt fallback model if available.',
|
||||
'warn'
|
||||
);
|
||||
// Let the loop continue to try the next model
|
||||
throw new Error('Claude overloaded');
|
||||
} else {
|
||||
// Re-throw non-overload errors
|
||||
throw streamError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here successfully, break out of the loop
|
||||
if (updatedTasks) {
|
||||
report(
|
||||
`Successfully updated tasks using ${modelType} on attempt ${modelAttempts}`,
|
||||
'success'
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (modelError) {
|
||||
const failedModel = modelType || 'unknown model';
|
||||
report(
|
||||
`Attempt ${modelAttempts} failed using ${failedModel}: ${modelError.message}`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
// Continue to next attempt if we have more attempts and this was an overload error
|
||||
const wasOverload = modelError.message
|
||||
?.toLowerCase()
|
||||
.includes('overload');
|
||||
|
||||
if (wasOverload && !isLastAttempt) {
|
||||
if (modelType === 'claude') {
|
||||
claudeOverloaded = true;
|
||||
report('Will attempt with Perplexity AI next', 'info');
|
||||
}
|
||||
continue; // Continue to next attempt
|
||||
} else if (isLastAttempt) {
|
||||
report(
|
||||
`Final attempt (${modelAttempts}/${maxModelAttempts}) failed. No fallback possible.`,
|
||||
'error'
|
||||
);
|
||||
throw modelError; // Re-throw on last attempt
|
||||
} else {
|
||||
throw modelError; // Re-throw for non-overload errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have updated tasks after all attempts, throw an error
|
||||
if (!updatedTasks) {
|
||||
throw new Error(
|
||||
'Failed to generate updated tasks after all model attempts'
|
||||
);
|
||||
}
|
||||
|
||||
// Replace the tasks in the original data
|
||||
updatedTasks.forEach((updatedTask) => {
|
||||
const index = data.tasks.findIndex((t) => t.id === updatedTask.id);
|
||||
if (index !== -1) {
|
||||
data.tasks[index] = updatedTask;
|
||||
}
|
||||
responseText = await generateTextService({
|
||||
prompt: userPrompt,
|
||||
systemPrompt: systemPrompt,
|
||||
role,
|
||||
session
|
||||
});
|
||||
|
||||
// Write the updated tasks to the file
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
report(`Successfully updated ${updatedTasks.length} tasks`, 'success');
|
||||
|
||||
// Generate individual task files
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
|
||||
// Only show success box for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(`Successfully updated ${updatedTasks.length} tasks`),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
if (isMCP) logFn.info('Successfully received text response');
|
||||
else
|
||||
logFn('success', 'Successfully received text response via AI service');
|
||||
// --- End AI Service Call ---
|
||||
} catch (error) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
if (isMCP) logFn.error(`Error during AI service call: ${error.message}`);
|
||||
else logFn('error', `Error during AI service call: ${error.message}`);
|
||||
if (error.message.includes('API key')) {
|
||||
if (isMCP)
|
||||
logFn.error(
|
||||
'Please ensure API keys are configured correctly in .env or mcp.json.'
|
||||
);
|
||||
else
|
||||
logFn(
|
||||
'error',
|
||||
'Please ensure API keys are configured correctly in .env or mcp.json.'
|
||||
);
|
||||
}
|
||||
throw error; // Re-throw error
|
||||
} finally {
|
||||
// Stop the loading indicator if it was created
|
||||
if (loadingIndicator) {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
loadingIndicator = null;
|
||||
}
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
}
|
||||
} catch (error) {
|
||||
report(`Error updating tasks: ${error.message}`, 'error');
|
||||
|
||||
// Only show error box for text output (CLI)
|
||||
// --- Parse and Validate Response ---
|
||||
try {
|
||||
updatedTasks = parseUpdatedTasksFromText(
|
||||
responseText,
|
||||
tasksToUpdate.length,
|
||||
logFn,
|
||||
isMCP
|
||||
);
|
||||
} catch (parseError) {
|
||||
if (isMCP)
|
||||
logFn.error(
|
||||
`Failed to parse updated tasks from AI response: ${parseError.message}`
|
||||
);
|
||||
else
|
||||
logFn(
|
||||
'error',
|
||||
`Failed to parse updated tasks from AI response: ${parseError.message}`
|
||||
);
|
||||
if (getDebugFlag(session)) {
|
||||
if (isMCP) logFn.error(`Raw AI Response:\n${responseText}`);
|
||||
else logFn('error', `Raw AI Response:\n${responseText}`);
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to parse valid updated tasks from AI response: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
// --- End Parse/Validate ---
|
||||
|
||||
// --- Update Tasks Data (Unchanged) ---
|
||||
if (!Array.isArray(updatedTasks)) {
|
||||
// Should be caught by parser, but extra check
|
||||
throw new Error('Parsed AI response for updated tasks was not an array.');
|
||||
}
|
||||
if (isMCP)
|
||||
logFn.info(`Received ${updatedTasks.length} updated tasks from AI.`);
|
||||
else
|
||||
logFn('info', `Received ${updatedTasks.length} updated tasks from AI.`);
|
||||
// Create a map for efficient lookup
|
||||
const updatedTasksMap = new Map(
|
||||
updatedTasks.map((task) => [task.id, task])
|
||||
);
|
||||
|
||||
// Iterate through the original data and update based on the map
|
||||
let actualUpdateCount = 0;
|
||||
data.tasks.forEach((task, index) => {
|
||||
if (updatedTasksMap.has(task.id)) {
|
||||
// Only update if the task was part of the set sent to AI
|
||||
data.tasks[index] = updatedTasksMap.get(task.id);
|
||||
actualUpdateCount++;
|
||||
}
|
||||
});
|
||||
if (isMCP)
|
||||
logFn.info(
|
||||
`Applied updates to ${actualUpdateCount} tasks in the dataset.`
|
||||
);
|
||||
else
|
||||
logFn(
|
||||
'info',
|
||||
`Applied updates to ${actualUpdateCount} tasks in the dataset.`
|
||||
);
|
||||
// --- End Update Tasks Data ---
|
||||
|
||||
// --- Write File and Generate (Unchanged) ---
|
||||
writeJSON(tasksPath, data);
|
||||
if (isMCP)
|
||||
logFn.info(
|
||||
`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
|
||||
);
|
||||
else
|
||||
logFn(
|
||||
'success',
|
||||
`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
|
||||
);
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// --- End Write File ---
|
||||
|
||||
// --- Final CLI Output (Unchanged) ---
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(chalk.green(`Successfully updated ${actualUpdateCount} tasks`), {
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round'
|
||||
})
|
||||
);
|
||||
}
|
||||
// --- End Final CLI Output ---
|
||||
} catch (error) {
|
||||
// --- General Error Handling (Unchanged) ---
|
||||
if (isMCP) logFn.error(`Error updating tasks: ${error.message}`);
|
||||
else logFn('error', `Error updating tasks: ${error.message}`);
|
||||
if (outputFormat === 'text') {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
|
||||
// Provide helpful error messages based on error type
|
||||
if (error.message?.includes('ANTHROPIC_API_KEY')) {
|
||||
console.log(
|
||||
chalk.yellow('\nTo fix this issue, set your Anthropic API key:')
|
||||
);
|
||||
console.log(' export ANTHROPIC_API_KEY=your_api_key_here');
|
||||
} else if (error.message?.includes('PERPLEXITY_API_KEY') && useResearch) {
|
||||
console.log(chalk.yellow('\nTo fix this issue:'));
|
||||
console.log(
|
||||
' 1. Set your Perplexity API key: export PERPLEXITY_API_KEY=your_api_key_here'
|
||||
);
|
||||
console.log(
|
||||
' 2. Or run without the research flag: task-master update --from=<id> --prompt="..."'
|
||||
);
|
||||
} else if (error.message?.includes('overloaded')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nAI model overloaded, and fallback failed or was unavailable:'
|
||||
)
|
||||
);
|
||||
console.log(' 1. Try again in a few minutes.');
|
||||
console.log(' 2. Ensure PERPLEXITY_API_KEY is set for fallback.');
|
||||
}
|
||||
|
||||
if (getDebugFlag()) {
|
||||
// Use getter
|
||||
if (getDebugFlag(session)) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} else {
|
||||
throw error; // Re-throw for JSON output
|
||||
throw error; // Re-throw for MCP/programmatic callers
|
||||
}
|
||||
// --- End General Error Handling ---
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user