@@ -114,15 +114,21 @@ export class RPGOrchestrator {
114114 */
115115 async generateRPGPlan ( prompt , context = { } ) {
116116 const existingFiles = context . existingFiles || 'none' ;
117+ const maxRetries = 3 ;
117118
118- const planningPrompt = `
119- You are an expert software architect using the RPG (Recursive Planning Graph) methodology for systematic code generation.
119+ for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
120+ try {
121+ logger . info ( `RPG planning attempt ${ attempt } /${ maxRetries } ` ) ;
122+
123+ const planningPrompt = `You are an expert software architect using the RPG (Recursive Planning Graph) methodology for systematic code generation.
124+
125+ CRITICAL: Respond with ONLY a valid JSON object. Do NOT include any explanations, comments, or text before or after the JSON. The response must be parseable by JSON.parse().
120126
121127For the user request: "${ prompt } "
122128
123129${ existingFiles !== 'none' ? `Existing project files: ${ JSON . stringify ( existingFiles , null , 2 ) } ` : '' }
124130
125- Create a comprehensive RPG plan following this structure:
131+ Create a comprehensive RPG plan following this EXACT structure:
126132
127133{
128134 "features": ["Feature 1", "Feature 2", "Feature 3"],
@@ -159,51 +165,86 @@ Guidelines:
159165- Be specific about technologies and architecture patterns
160166- Consider the existing codebase when planning modifications
161167
162- Respond with valid JSON only. `;
168+ IMPORTANT: Your response must be ONLY the JSON object, nothing else. Start with { and end with }. ${ attempt > 1 ? `\n\nPrevious attempts failed due to JSON parsing errors. Please ensure your response is valid JSON.` : '' } `;
163169
164- logger . debug ( 'Making RPG planning request' , {
165- promptLength : planningPrompt . length ,
166- model : this . model ,
167- } ) ;
170+ logger . debug ( 'Making RPG planning request' , {
171+ promptLength : planningPrompt . length ,
172+ model : this . model ,
173+ attempt,
174+ } ) ;
168175
169- const response = await this . client . chat . completions . create ( {
170- model : this . model ,
171- messages : [
172- {
173- role : 'system' ,
174- content :
175- 'You are a precise RPG planner. Respond with valid JSON only. No explanations or markdown.' ,
176- } ,
177- { role : 'user' , content : planningPrompt } ,
178- ] ,
179- max_tokens : 4096 ,
180- temperature : 0.3 ,
181- } ) ;
176+ const response = await this . client . chat . completions . create ( {
177+ model : this . model ,
178+ messages : [
179+ {
180+ role : 'system' ,
181+ content :
182+ 'You are a precise RPG planner. You must respond with ONLY a valid JSON object. Do NOT include any explanations, comments, markdown, or additional text. The response must start with { and end with } and be parseable by JSON.parse(). Failure to follow this instruction will result in parsing errors .' ,
183+ } ,
184+ { role : 'user' , content : planningPrompt } ,
185+ ] ,
186+ max_tokens : 4096 ,
187+ temperature : Math . max ( 0.1 , 0.3 - ( attempt - 1 ) * 0.1 ) , // Decrease temperature on retries
188+ } ) ;
182189
183- const rawResponse = response . choices [ 0 ] . message . content . trim ( ) ;
184- logger . debug ( 'Received RPG planning response' , {
185- responseLength : rawResponse . length ,
186- } ) ;
190+ const rawResponse = response . choices [ 0 ] . message . content . trim ( ) ;
191+ logger . debug ( 'Received RPG planning response' , {
192+ responseLength : rawResponse . length ,
193+ attempt,
194+ } ) ;
187195
188- try {
189- const plan = JSON . parse ( rawResponse ) ;
196+ // Extract JSON from response - handle cases where AI returns extra text
197+ let jsonString = rawResponse ;
190198
191- // Validate plan structure
192- this . validatePlanStructure ( plan ) ;
199+ // Try to find JSON object in the response
200+ const jsonStart = rawResponse . indexOf ( '{' ) ;
201+ const jsonEnd = rawResponse . lastIndexOf ( '}' ) ;
193202
194- logger . info ( 'Successfully parsed RPG plan' , {
195- features : plan . features ?. length || 0 ,
196- files : Object . keys ( plan . files || { } ) . length ,
197- flows : plan . flows ?. length || 0 ,
198- deps : plan . deps ?. length || 0 ,
199- } ) ;
203+ if ( jsonStart !== - 1 && jsonEnd !== - 1 && jsonEnd > jsonStart ) {
204+ jsonString = rawResponse . substring ( jsonStart , jsonEnd + 1 ) ;
205+ }
200206
201- return { plan, rawResponse } ;
202- } catch ( parseError ) {
203- logger . error ( 'Failed to parse RPG planning response' , parseError , {
204- rawResponse : rawResponse . substring ( 0 , 500 ) ,
205- } ) ;
206- throw new Error ( `Invalid RPG plan format: ${ parseError . message } ` ) ;
207+ // Clean up common issues
208+ jsonString = jsonString
209+ . replace ( / [ \u0000 - \u001F \u007F - \u009F ] / g, '' ) // Remove control characters
210+ . replace ( / , ( \s * [ } \] ] ) / g, '$1' ) // Remove trailing commas
211+ . trim ( ) ;
212+
213+ const plan = JSON . parse ( jsonString ) ;
214+
215+ // Validate plan structure
216+ this . validatePlanStructure ( plan ) ;
217+
218+ logger . info ( 'Successfully parsed RPG plan' , {
219+ features : plan . features ?. length || 0 ,
220+ files : Object . keys ( plan . files || { } ) . length ,
221+ flows : plan . flows ?. length || 0 ,
222+ deps : plan . deps ?. length || 0 ,
223+ attempt,
224+ } ) ;
225+
226+ return { plan, rawResponse } ;
227+
228+ } catch ( parseError ) {
229+ logger . error ( `RPG planning attempt ${ attempt } failed` , parseError , {
230+ errorMessage : parseError . message ,
231+ attempt,
232+ } ) ;
233+
234+ // If this is the last attempt, provide detailed error information
235+ if ( attempt === maxRetries ) {
236+ const errorDetails = this . analyzeJsonError (
237+ parseError . response || 'No response available' ,
238+ parseError
239+ ) ;
240+ throw new Error ( `Invalid RPG plan format after ${ maxRetries } attempts: ${ parseError . message } . ${ errorDetails } ` ) ;
241+ }
242+
243+ // Wait before retrying (exponential backoff)
244+ const waitTime = Math . pow ( 2 , attempt ) * 1000 ; // 2s, 4s, 8s
245+ logger . info ( `Retrying RPG planning in ${ waitTime } ms...` ) ;
246+ await new Promise ( resolve => setTimeout ( resolve , waitTime ) ) ;
247+ }
207248 }
208249 }
209250
@@ -480,6 +521,53 @@ Respond with valid JSON only.`;
480521 }
481522 }
482523
524+ /**
525+ * Analyze JSON parsing errors to provide helpful debugging information
526+ * @param {string } rawResponse - The raw AI response
527+ * @param {Error } parseError - The JSON parsing error
528+ * @returns {string } Error analysis details
529+ */
530+ analyzeJsonError ( rawResponse , parseError ) {
531+ const errorMessage = parseError . message ;
532+ let details = '' ;
533+
534+ // Check for common JSON issues
535+ if ( errorMessage . includes ( 'Unterminated string' ) ) {
536+ details = 'The JSON contains an unterminated string literal. Check for missing quotes around string values.' ;
537+ } else if ( errorMessage . includes ( 'Unexpected token' ) ) {
538+ details = 'The JSON contains unexpected characters. The response may include extra text before or after the JSON.' ;
539+ } else if ( errorMessage . includes ( 'Unexpected end of JSON input' ) ) {
540+ details = 'The JSON appears to be truncated. The AI response may have been cut off.' ;
541+ } else if ( errorMessage . includes ( 'Expected property name' ) ) {
542+ details = 'Missing quotes around property names in the JSON object.' ;
543+ }
544+
545+ // Check if response contains JSON markers
546+ const hasJsonStart = rawResponse . includes ( '{' ) ;
547+ const hasJsonEnd = rawResponse . includes ( '}' ) ;
548+
549+ if ( ! hasJsonStart || ! hasJsonEnd ) {
550+ details += ' No JSON object markers found in response.' ;
551+ } else {
552+ const jsonStart = rawResponse . indexOf ( '{' ) ;
553+ const jsonEnd = rawResponse . lastIndexOf ( '}' ) ;
554+ const jsonLength = jsonEnd - jsonStart + 1 ;
555+ details += ` Found JSON-like content (${ jsonLength } chars) in response.` ;
556+ }
557+
558+ // Show a snippet around the error position
559+ const positionMatch = errorMessage . match ( / p o s i t i o n ( \d + ) / ) ;
560+ if ( positionMatch ) {
561+ const position = parseInt ( positionMatch [ 1 ] ) ;
562+ const start = Math . max ( 0 , position - 50 ) ;
563+ const end = Math . min ( rawResponse . length , position + 50 ) ;
564+ const snippet = rawResponse . substring ( start , end ) ;
565+ details += ` Snippet around error: "...${ snippet } ..."` ;
566+ }
567+
568+ return details ;
569+ }
570+
483571 // Helper methods
484572
485573 validatePlanStructure ( plan ) {
0 commit comments