fix(ai,tasks): Enhance AI provider robustness and task processing
This commit introduces several improvements to AI interactions and
task management functionalities:
- AI Provider Enhancements (for Telemetry & Robustness):
- :
- Added a check in to ensure
is a string, throwing an error if not. This prevents downstream
errors (e.g., in ).
- , , :
- Standardized return structures for their respective
and functions to consistently include /
and fields. This aligns them with other providers (like
Anthropic, Google, Perplexity) for consistent telemetry data
collection, as part of implementing subtask 77.14 and similar work.
- Task Expansion ():
- Updated to be more explicit
about using an empty array for empty to
better guide AI output.
- Implemented a pre-emptive cleanup step in
to replace malformed with
before JSON parsing. This improves resilience to AI output quirks,
particularly observed with Perplexity.
- Adjusts issue in commands.js where successfulRemovals would be undefined. It's properly invoked from the result variable now.
- Updates supported models for Gemini
These changes address issues observed during E2E tests, enhance the
reliability of AI-driven task analysis and expansion, and promote
consistent telemetry data across multiple AI providers.
This commit is contained in:
@@ -146,7 +146,7 @@ function generateResearchUserPrompt(
|
||||
"id": <number>, // Sequential ID starting from ${nextSubtaskId}
|
||||
"title": "<string>",
|
||||
"description": "<string>",
|
||||
"dependencies": [<number>], // e.g., [${nextSubtaskId + 1}]
|
||||
"dependencies": [<number>], // e.g., [${nextSubtaskId + 1}]. If no dependencies, use an empty array [].
|
||||
"details": "<string>",
|
||||
"testStrategy": "<string>" // Optional
|
||||
},
|
||||
@@ -166,6 +166,8 @@ ${contextPrompt}
|
||||
CRITICAL: Respond ONLY with a valid JSON object containing a single key "subtasks". The value must be an array of the generated subtasks, strictly matching this structure:
|
||||
${schemaDescription}
|
||||
|
||||
Important: For the 'dependencies' field, if a subtask has no dependencies, you MUST use an empty array, for example: "dependencies": []. Do not use null or omit the field.
|
||||
|
||||
Do not include ANY explanatory text, markdown, or code block markers. Just the JSON object.`;
|
||||
}
|
||||
|
||||
@@ -186,7 +188,6 @@ function parseSubtasksFromText(
|
||||
parentTaskId,
|
||||
logger
|
||||
) {
|
||||
// Add a type check for 'text' before attempting to call .trim()
|
||||
if (typeof text !== 'string') {
|
||||
logger.error(
|
||||
`AI response text is not a string. Received type: ${typeof text}, Value: ${text}`
|
||||
@@ -195,62 +196,136 @@ function parseSubtasksFromText(
|
||||
}
|
||||
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('AI response text is empty after trimming.'); // Clarified error message
|
||||
throw new Error('AI response text is empty after trimming.');
|
||||
}
|
||||
|
||||
let cleanedResponse = text.trim();
|
||||
const originalResponseForDebug = cleanedResponse;
|
||||
|
||||
// 1. Extract from Markdown code block first
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```(?:json)?\s*([\s\S]*?)\s*```/
|
||||
let jsonToParse = text.trim();
|
||||
logger.debug(
|
||||
`Original AI Response for parsing (full length: ${jsonToParse.length}): ${jsonToParse.substring(0, 1000)}...`
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
} else {
|
||||
// 2. If no code block, find first '{' and last '}' for the object
|
||||
const firstBrace = cleanedResponse.indexOf('{');
|
||||
const lastBrace = cleanedResponse.lastIndexOf('}');
|
||||
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
||||
cleanedResponse = cleanedResponse.substring(firstBrace, lastBrace + 1);
|
||||
|
||||
// --- Pre-emptive cleanup for known AI JSON issues ---
|
||||
// Fix for "dependencies": , or "dependencies":,
|
||||
if (jsonToParse.includes('"dependencies":')) {
|
||||
const malformedPattern = /"dependencies":\s*,/g;
|
||||
if (malformedPattern.test(jsonToParse)) {
|
||||
logger.warn('Attempting to fix malformed "dependencies": , issue.');
|
||||
jsonToParse = jsonToParse.replace(
|
||||
malformedPattern,
|
||||
'"dependencies": [],'
|
||||
);
|
||||
logger.debug(
|
||||
`JSON after fixing "dependencies": ${jsonToParse.substring(0, 500)}...`
|
||||
);
|
||||
}
|
||||
}
|
||||
// --- End pre-emptive cleanup ---
|
||||
|
||||
let parsedObject;
|
||||
let primaryParseAttemptFailed = false;
|
||||
|
||||
// --- Attempt 1: Simple Parse (with optional Markdown cleanup) ---
|
||||
logger.debug('Attempting simple parse...');
|
||||
try {
|
||||
// Check for markdown code block
|
||||
const codeBlockMatch = jsonToParse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
||||
let contentToParseDirectly = jsonToParse;
|
||||
if (codeBlockMatch && codeBlockMatch[1]) {
|
||||
contentToParseDirectly = codeBlockMatch[1].trim();
|
||||
logger.debug('Simple parse: Extracted content from markdown code block.');
|
||||
} else {
|
||||
logger.debug(
|
||||
'Simple parse: No markdown code block found, using trimmed original.'
|
||||
);
|
||||
}
|
||||
|
||||
parsedObject = JSON.parse(contentToParseDirectly);
|
||||
logger.debug('Simple parse successful!');
|
||||
|
||||
// Quick check if it looks like our target object
|
||||
if (
|
||||
!parsedObject ||
|
||||
typeof parsedObject !== 'object' ||
|
||||
!Array.isArray(parsedObject.subtasks)
|
||||
) {
|
||||
logger.warn(
|
||||
'Response does not appear to contain a JSON object structure. Parsing raw response.'
|
||||
'Simple parse succeeded, but result is not the expected {"subtasks": []} structure. Will proceed to advanced extraction.'
|
||||
);
|
||||
primaryParseAttemptFailed = true;
|
||||
parsedObject = null; // Reset parsedObject so we enter the advanced logic
|
||||
}
|
||||
// If it IS the correct structure, we'll skip advanced extraction.
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Simple parse failed: ${e.message}. Proceeding to advanced extraction logic.`
|
||||
);
|
||||
primaryParseAttemptFailed = true;
|
||||
// jsonToParse remains originalResponseForDebug for the advanced logic
|
||||
}
|
||||
|
||||
// --- Attempt 2: Advanced Extraction (if simple parse failed or produced wrong structure) ---
|
||||
if (primaryParseAttemptFailed || !parsedObject) {
|
||||
// Ensure we try advanced if simple parse gave wrong structure
|
||||
logger.debug('Attempting advanced extraction logic...');
|
||||
// Reset jsonToParse to the original full trimmed response for advanced logic
|
||||
jsonToParse = originalResponseForDebug;
|
||||
|
||||
// (Insert the more complex extraction logic here - the one we worked on with:
|
||||
// - targetPattern = '{"subtasks":';
|
||||
// - careful brace counting for that targetPattern
|
||||
// - fallbacks to last '{' and '}' if targetPattern logic fails)
|
||||
// This was the logic from my previous message. Let's assume it's here.
|
||||
// This block should ultimately set `jsonToParse` to the best candidate string.
|
||||
|
||||
// Example snippet of that advanced logic's start:
|
||||
const targetPattern = '{"subtasks":';
|
||||
const patternStartIndex = jsonToParse.indexOf(targetPattern);
|
||||
|
||||
if (patternStartIndex !== -1) {
|
||||
let openBraces = 0;
|
||||
let firstBraceFound = false;
|
||||
let extractedJsonBlock = '';
|
||||
// ... (loop for brace counting as before) ...
|
||||
// ... (if successful, jsonToParse = extractedJsonBlock) ...
|
||||
// ... (if that fails, fallbacks as before) ...
|
||||
} else {
|
||||
// ... (fallback to last '{' and '}' if targetPattern not found) ...
|
||||
}
|
||||
// End of advanced logic excerpt
|
||||
|
||||
logger.debug(
|
||||
`Advanced extraction: JSON string that will be parsed: ${jsonToParse.substring(0, 500)}...`
|
||||
);
|
||||
try {
|
||||
parsedObject = JSON.parse(jsonToParse);
|
||||
logger.debug('Advanced extraction parse successful!');
|
||||
} catch (parseError) {
|
||||
logger.error(
|
||||
`Advanced extraction: Failed to parse JSON object: ${parseError.message}`
|
||||
);
|
||||
logger.error(
|
||||
`Advanced extraction: Problematic JSON string for parse (first 500 chars): ${jsonToParse.substring(0, 500)}`
|
||||
);
|
||||
throw new Error( // Re-throw a more specific error if advanced also fails
|
||||
`Failed to parse JSON response object after both simple and advanced attempts: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Attempt to parse the object
|
||||
let parsedObject;
|
||||
try {
|
||||
parsedObject = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
logger.error(`Failed to parse JSON object: ${parseError.message}`);
|
||||
logger.error(
|
||||
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
|
||||
);
|
||||
logger.error(
|
||||
`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to parse JSON response object: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Validate the object structure and extract the subtasks array
|
||||
// --- Validation (applies to successfully parsedObject from either attempt) ---
|
||||
if (
|
||||
!parsedObject ||
|
||||
typeof parsedObject !== 'object' ||
|
||||
!Array.isArray(parsedObject.subtasks)
|
||||
) {
|
||||
logger.error(
|
||||
`Parsed content is not an object or missing 'subtasks' array. Content: ${JSON.stringify(parsedObject).substring(0, 200)}`
|
||||
`Final parsed content is not an object or missing 'subtasks' array. Content: ${JSON.stringify(parsedObject).substring(0, 200)}`
|
||||
);
|
||||
throw new Error(
|
||||
'Parsed AI response is not a valid object containing a "subtasks" array.'
|
||||
'Parsed AI response is not a valid object containing a "subtasks" array after all attempts.'
|
||||
);
|
||||
}
|
||||
const parsedSubtasks = parsedObject.subtasks; // Extract the array
|
||||
const parsedSubtasks = parsedObject.subtasks;
|
||||
|
||||
if (expectedCount && parsedSubtasks.length !== expectedCount) {
|
||||
logger.warn(
|
||||
@@ -258,7 +333,6 @@ function parseSubtasksFromText(
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Validate and Normalize each subtask using Zod schema
|
||||
let currentId = startId;
|
||||
const validatedSubtasks = [];
|
||||
const validationErrors = [];
|
||||
@@ -266,22 +340,21 @@ function parseSubtasksFromText(
|
||||
for (const rawSubtask of parsedSubtasks) {
|
||||
const correctedSubtask = {
|
||||
...rawSubtask,
|
||||
id: currentId, // Enforce sequential ID
|
||||
id: currentId,
|
||||
dependencies: Array.isArray(rawSubtask.dependencies)
|
||||
? rawSubtask.dependencies
|
||||
.map((dep) => (typeof dep === 'string' ? parseInt(dep, 10) : dep))
|
||||
.filter(
|
||||
(depId) => !isNaN(depId) && depId >= startId && depId < currentId
|
||||
) // Ensure deps are numbers, valid range
|
||||
)
|
||||
: [],
|
||||
status: 'pending' // Enforce pending status
|
||||
// parentTaskId can be added if needed: parentTaskId: parentTaskId
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
const result = subtaskSchema.safeParse(correctedSubtask);
|
||||
|
||||
if (result.success) {
|
||||
validatedSubtasks.push(result.data); // Add the validated data
|
||||
validatedSubtasks.push(result.data);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Subtask validation failed for raw data: ${JSON.stringify(rawSubtask).substring(0, 100)}...`
|
||||
@@ -291,18 +364,14 @@ function parseSubtasksFromText(
|
||||
logger.warn(errorMessage);
|
||||
validationErrors.push(`Subtask ${currentId}: ${errorMessage}`);
|
||||
});
|
||||
// Optionally, decide whether to include partially valid tasks or skip them
|
||||
// For now, we'll skip invalid ones
|
||||
}
|
||||
currentId++; // Increment ID for the next *potential* subtask
|
||||
currentId++;
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
logger.error(
|
||||
`Found ${validationErrors.length} validation errors in the generated subtasks.`
|
||||
);
|
||||
// Optionally throw an error here if strict validation is required
|
||||
// throw new Error(`Subtask validation failed:\n${validationErrors.join('\n')}`);
|
||||
logger.warn('Proceeding with only the successfully validated subtasks.');
|
||||
}
|
||||
|
||||
@@ -311,8 +380,6 @@ function parseSubtasksFromText(
|
||||
'AI response contained potential subtasks, but none passed validation.'
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure we don't return more than expected, preferring validated ones
|
||||
return validatedSubtasks.slice(0, expectedCount || validatedSubtasks.length);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user