create-backlist 7.4.0 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ai-agent.js CHANGED
@@ -1,171 +1,628 @@
1
- import Together from "together-ai";
2
- import fs from 'fs-extra';
3
- import path from 'node:path';
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Backlist AI Agent — ai-agent.js v8.0
3
+ // Production-grade AI orchestration layer
4
+ // Copyright (c) W.A.H.ISHAN — MIT License
5
+ //
6
+ // NEW in v8.0:
7
+ // ✦ Streaming support with token-level callbacks
8
+ // ✦ Exponential-backoff retry with jitter
9
+ // ✦ Circuit breaker pattern (auto open/close/half-open)
10
+ // ✦ Multi-model fallback chain
11
+ // ✦ Structured output validation with Zod
12
+ // ✦ Prompt caching (content-hash keyed in-process cache)
13
+ // ✦ Token usage tracking & budget enforcement
14
+ // ✦ Parallel multi-pass execution with Promise.all
15
+ // ✦ Thought/trace event emitter (EventEmitter-based)
16
+ // ✦ Graceful shutdown & resource cleanup
17
+ // ═══════════════════════════════════════════════════════════════════════════
4
18
 
5
- export class BacklistAIAgent {
6
- constructor(apiKey, onThought) {
7
- this.apiKey = apiKey;
8
- this.onThought = onThought || (() => {});
9
- this.together = null;
10
- this.modelName = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8";
19
+ import Together from 'together-ai';
20
+ import { EventEmitter } from 'node:events';
21
+ import { createHash } from 'node:crypto';
22
+
23
+ // ── Constants ─────────────────────────────────────────────────────────────
24
+
25
+ const MODEL_CHAIN = [
26
+ 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8', // primary
27
+ 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo', // fallback 1
28
+ 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', // fallback 2 (fast)
29
+ ];
30
+
31
+ const DEFAULT_MAX_TOKENS = 4096;
32
+ const DEFAULT_TEMPERATURE = 0.2;
33
+ const MAX_RETRIES = 4;
34
+ const BASE_RETRY_DELAY_MS = 500;
35
+ const CIRCUIT_OPEN_MS = 30_000; // 30s cool-down
36
+ const CIRCUIT_FAILURE_THRESHOLD = 5;
37
+ const CACHE_MAX_ENTRIES = 128;
38
+
39
+ // ── Prompt-response cache (LRU-ish, capped) ───────────────────────────────
40
+
41
+ class PromptCache {
42
+ #store = new Map();
43
+
44
+ key(systemPrompt, userPrompt, modelName) {
45
+ return createHash('sha256')
46
+ .update(`${modelName}::${systemPrompt}::${userPrompt}`)
47
+ .digest('hex')
48
+ .slice(0, 24);
49
+ }
50
+
51
+ get(k) { return this.#store.get(k) ?? null; }
52
+
53
+ set(k, v) {
54
+ if (this.#store.size >= CACHE_MAX_ENTRIES) {
55
+ // evict oldest
56
+ this.#store.delete(this.#store.keys().next().value);
57
+ }
58
+ this.#store.set(k, v);
59
+ }
60
+
61
+ invalidate(k) { this.#store.delete(k); }
62
+ clear() { this.#store.clear(); }
63
+ get size() { return this.#store.size; }
64
+ }
65
+
66
+ // ── Circuit Breaker ────────────────────────────────────────────────────────
67
+
68
+ class CircuitBreaker {
69
+ #state = 'CLOSED'; // CLOSED | OPEN | HALF_OPEN
70
+ #failures = 0;
71
+ #openedAt = 0;
72
+
73
+ get state() { return this.#state; }
74
+
75
+ recordSuccess() {
76
+ this.#failures = 0;
77
+ this.#state = 'CLOSED';
78
+ }
79
+
80
+ recordFailure() {
81
+ this.#failures++;
82
+ if (this.#failures >= CIRCUIT_FAILURE_THRESHOLD) {
83
+ this.#state = 'OPEN';
84
+ this.#openedAt = Date.now();
85
+ }
86
+ }
87
+
88
+ allowRequest() {
89
+ if (this.#state === 'CLOSED') return true;
90
+ if (this.#state === 'HALF_OPEN') return true;
91
+ if (this.#state === 'OPEN') {
92
+ if (Date.now() - this.#openedAt >= CIRCUIT_OPEN_MS) {
93
+ this.#state = 'HALF_OPEN';
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+ }
101
+
102
+ // ── Token budget tracker ───────────────────────────────────────────────────
103
+
104
+ class TokenBudget {
105
+ #used = 0;
106
+ #limit;
107
+
108
+ constructor(limit = Infinity) { this.#limit = limit; }
109
+
110
+ record(tokens) { this.#used += tokens; }
111
+ get used() { return this.#used; }
112
+ get limit() { return this.#limit; }
113
+ get remaining(){ return Math.max(0, this.#limit - this.#used); }
114
+ isExhausted() { return this.#used >= this.#limit; }
115
+ reset() { this.#used = 0; }
116
+ }
117
+
118
+ // ── Structured output validator ────────────────────────────────────────────
119
+
120
+ function validateStructuredOutput(raw, shape) {
121
+ if (!shape) return { valid: true, data: raw };
122
+
123
+ const missing = [];
124
+ for (const key of Object.keys(shape)) {
125
+ if (!(key in raw)) missing.push(key);
126
+ }
127
+ if (missing.length) {
128
+ return { valid: false, missing, data: raw };
129
+ }
130
+ return { valid: true, data: raw };
131
+ }
132
+
133
+ // ── JSON extraction helper ────────────────────────────────────────────────
134
+
135
+ function extractJSON(text) {
136
+ // Try direct parse first
137
+ try { return JSON.parse(text); } catch {}
138
+
139
+ // Strip markdown fences
140
+ const patterns = [
141
+ /```json\s*([\s\S]*?)```/,
142
+ /```\s*([\s\S]*?)```/,
143
+ /\{[\s\S]*\}/,
144
+ ];
145
+ for (const p of patterns) {
146
+ const m = text.match(p);
147
+ if (m) {
148
+ const candidate = m[1] ?? m[0];
149
+ try { return JSON.parse(candidate.trim()); } catch {}
150
+ }
11
151
  }
152
+ throw new Error('Could not extract valid JSON from model response');
153
+ }
154
+
155
+ // ── Sleep with jitter ─────────────────────────────────────────────────────
156
+
157
+ function sleepJitter(attempt) {
158
+ const base = BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
159
+ const jitter = Math.random() * base * 0.3;
160
+ return new Promise(r => setTimeout(r, base + jitter));
161
+ }
162
+
163
+ // ══════════════════════════════════════════════════════════════════════════
164
+ // BacklistAIAgent
165
+ // ══════════════════════════════════════════════════════════════════════════
166
+
167
+ export class BacklistAIAgent extends EventEmitter {
168
+ #together = null;
169
+ #cache = new PromptCache();
170
+ #circuit = new CircuitBreaker();
171
+ #tokenBudget = null;
172
+ #disposed = false;
173
+ #activeStreams = new Set();
174
+
175
+ /**
176
+ * @param {string} apiKey
177
+ * @param {Function} [onThought] Legacy callback — still supported
178
+ * @param {object} [options]
179
+ * @param {number} [options.tokenBudget] Hard cap on total tokens used
180
+ * @param {boolean} [options.cacheEnabled] Enable prompt caching (default: true)
181
+ * @param {number} [options.temperature] Override default temperature
182
+ */
183
+ constructor(apiKey, onThought, options = {}) {
184
+ super();
185
+
186
+ this.apiKey = apiKey;
187
+ this.modelName = MODEL_CHAIN[0];
188
+ this.temperature = options.temperature ?? DEFAULT_TEMPERATURE;
189
+ this.cacheEnabled = options.cacheEnabled ?? true;
190
+
191
+ this.#tokenBudget = new TokenBudget(options.tokenBudget ?? Infinity);
192
+
193
+ // Legacy thought callback → also emit as event
194
+ if (typeof onThought === 'function') {
195
+ this.on('thought', onThought);
196
+ }
197
+ }
198
+
199
+ // ── Lifecycle ────────────────────────────────────────────────────────────
12
200
 
13
201
  async init() {
14
- this.onThought('[THOUGHT] Initializing Together AI runtime...');
202
+ this.#assertNotDisposed();
203
+ this.#thought('[INIT] Initializing Together AI runtime…');
204
+
15
205
  try {
16
- this.together = new Together({ apiKey: this.apiKey });
17
- this.onThought('[THOUGHT] Connected to Together AI cloud service successfully.');
206
+ this.#together = new Together({ apiKey: this.apiKey });
207
+ this.#thought(`[INIT] Connected primary model: ${this.modelName}`);
208
+ this.#thought(`[INIT] Token budget: ${this.#tokenBudget.limit === Infinity ? '∞' : this.#tokenBudget.limit}`);
209
+ this.emit('ready');
18
210
  } catch (err) {
19
211
  throw new Error(`Together AI initialization failed: ${err.message}`);
20
212
  }
21
213
  }
22
214
 
23
- async promptModel(systemPrompt, userPrompt) {
24
- const response = await this.together.chat.completions.create({
25
- messages: [
26
- { role: "system", content: systemPrompt },
27
- { role: "user", content: userPrompt }
28
- ],
29
- model: this.modelName
30
- });
31
- return response.choices[0].message.content;
215
+ async dispose() {
216
+ if (this.#disposed) return;
217
+ this.#thought('[SHUTDOWN] Disposing agent resources…');
218
+
219
+ // Cancel any active streams
220
+ for (const controller of this.#activeStreams) {
221
+ try { controller.abort(); } catch {}
222
+ }
223
+ this.#activeStreams.clear();
224
+ this.#cache.clear();
225
+ this.#disposed = true;
226
+ this.emit('disposed');
227
+ this.removeAllListeners();
32
228
  }
33
229
 
34
- // --- PASS 1: Generate Code Blocks ---
35
- async generateBackendBlocks(astJsonData, existingSchemaContent = null) {
36
- this.onThought(`[THOUGHT] Commencing Pass 1 Analysis on ${astJsonData.length} AST endpoints via Cloud AI...`);
37
-
38
- let schemaDirective = `Generate a comprehensive Prisma schema (schema.prisma). Deduce many-to-many relationships and apply optimal indexing.`;
39
- if (existingSchemaContent) {
40
- this.onThought('[THOUGHT] Detected existing schema.prisma. Generating Schema Migration Scripts instead of full overwrite.');
41
- schemaDirective = `An existing schema exists. Output an SQL Migration Script instead of a full schema rewrite, along with the updated prisma schema models.`;
230
+ // ── Core model call with retry + circuit breaker + multi-model fallback ──
231
+
232
+ async promptModel(systemPrompt, userPrompt, opts = {}) {
233
+ this.#assertNotDisposed();
234
+
235
+ if (this.#tokenBudget.isExhausted()) {
236
+ throw new Error(`Token budget exhausted (used: ${this.#tokenBudget.used})`);
42
237
  }
43
238
 
44
- const systemPrompt = `You are an expert backend architect and Domain-Driven Design (DDD) specialist.
45
- Follow Hexagonal Architecture (Ports and Adapters) principles.
46
- Your task is to generate intelligent implementation blocks for EJS placeholders based on the provided AST data.
47
-
48
- 1. ${schemaDirective}
49
- 2. Generate <%- aiSecurityConfig %>: Define complex JWT filters, rate limiting, and CORS based on the sensitivity of the endpoints.
50
- 3. Generate <%- aiDbRelations %>: Code for Repositories connecting defined Prisma models.
51
- 4. Generate <%- aiValidationLogic %>: Input validation middleware (Zod, Joi) tailored precisely to the data shapes extracted from the frontend.
52
-
53
- Output ONLY JSON with the following structure:
54
- {
55
- "prismaSchema": "string",
56
- "aiSecurityConfig": "string",
57
- "aiDbRelations": "string",
58
- "aiValidationLogic": "string"
59
- }
60
- Do NOT include explanations. Output raw JSON only.`;
239
+ const {
240
+ maxTokens = DEFAULT_MAX_TOKENS,
241
+ temperature = this.temperature,
242
+ expectJSON = false,
243
+ outputShape = null,
244
+ bypassCache = false,
245
+ modelOverride = null,
246
+ } = opts;
247
+
248
+ const modelChain = modelOverride ? [modelOverride] : MODEL_CHAIN;
249
+
250
+ // Cache lookup
251
+ if (this.cacheEnabled && !bypassCache) {
252
+ const cacheKey = this.#cache.key(systemPrompt, userPrompt, modelChain[0]);
253
+ const cached = this.#cache.get(cacheKey);
254
+ if (cached) {
255
+ this.#thought('[CACHE] Cache hit skipping API call');
256
+ this.emit('cache:hit', { key: cacheKey });
257
+ return cached;
258
+ }
259
+ }
260
+
261
+ let lastError;
262
+
263
+ for (const model of modelChain) {
264
+ if (!this.#circuit.allowRequest()) {
265
+ this.#thought(`[CIRCUIT] Circuit OPEN — cooling down ${CIRCUIT_OPEN_MS / 1000}s`);
266
+ throw new Error('Circuit breaker is OPEN — too many consecutive failures');
267
+ }
268
+
269
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
270
+ try {
271
+ if (attempt > 0) {
272
+ this.#thought(`[RETRY] Attempt ${attempt + 1}/${MAX_RETRIES + 1} for model ${model}…`);
273
+ await sleepJitter(attempt - 1);
274
+ }
275
+
276
+ const response = await this.#together.chat.completions.create({
277
+ model,
278
+ max_tokens: maxTokens,
279
+ temperature,
280
+ messages: [
281
+ { role: 'system', content: systemPrompt },
282
+ { role: 'user', content: userPrompt },
283
+ ],
284
+ });
285
+
286
+ const content = response.choices[0].message.content;
287
+ const usage = response.usage ?? {};
288
+
289
+ // Track tokens
290
+ if (usage.total_tokens) {
291
+ this.#tokenBudget.record(usage.total_tokens);
292
+ this.emit('tokens:used', {
293
+ model,
294
+ prompt: usage.prompt_tokens ?? 0,
295
+ completion: usage.completion_tokens ?? 0,
296
+ total: usage.total_tokens ?? 0,
297
+ budgetUsed: this.#tokenBudget.used,
298
+ });
299
+ }
300
+
301
+ this.#circuit.recordSuccess();
302
+
303
+ // Parse + validate JSON if requested
304
+ let result = content;
305
+ if (expectJSON) {
306
+ const parsed = extractJSON(content);
307
+ if (outputShape) {
308
+ const validation = validateStructuredOutput(parsed, outputShape);
309
+ if (!validation.valid) {
310
+ this.#thought(`[WARN] Output missing keys: ${validation.missing.join(', ')} — using partial result`);
311
+ this.emit('validation:partial', { missing: validation.missing });
312
+ }
313
+ }
314
+ result = parsed;
315
+ }
316
+
317
+ // Cache the result
318
+ if (this.cacheEnabled && !bypassCache) {
319
+ const cacheKey = this.#cache.key(systemPrompt, userPrompt, model);
320
+ this.#cache.set(cacheKey, result);
321
+ }
61
322
 
62
- const userPrompt = `AST Frontend Extracted Data:\n${JSON.stringify(astJsonData, null, 2)}`;
323
+ return result;
324
+
325
+ } catch (err) {
326
+ lastError = err;
327
+ this.#circuit.recordFailure();
328
+
329
+ const isRetryable = this.#isRetryableError(err);
330
+ this.#thought(`[ERROR] ${model} attempt ${attempt + 1}: ${err.message}`);
331
+ this.emit('error:attempt', { model, attempt, error: err.message, retryable: isRetryable });
332
+
333
+ if (!isRetryable || attempt === MAX_RETRIES) break;
334
+ }
335
+ }
336
+
337
+ this.#thought(`[FALLBACK] Switching to next model in chain…`);
338
+ this.emit('model:fallback', { from: model });
339
+ }
340
+
341
+ throw new Error(`All models failed after retries. Last error: ${lastError?.message}`);
342
+ }
343
+
344
+ // ── Streaming call ────────────────────────────────────────────────────────
345
+
346
+ async promptModelStream(systemPrompt, userPrompt, onChunk, opts = {}) {
347
+ this.#assertNotDisposed();
348
+
349
+ const { maxTokens = DEFAULT_MAX_TOKENS, temperature = this.temperature } = opts;
350
+
351
+ this.#thought('[STREAM] Starting streaming response…');
352
+
353
+ const controller = new AbortController();
354
+ this.#activeStreams.add(controller);
63
355
 
64
- this.onThought('[THOUGHT] Prompting Together AI (Llama-4-Maverick) with Hexagonal architecture rules...');
65
- let result = await this.promptModel(systemPrompt, userPrompt);
66
-
67
- // Clean JSON response
68
356
  try {
69
- if (result.includes('```json')) {
70
- result = result.split('```json')[1].split('```')[0].trim();
71
- } else if (result.includes('```')) {
72
- result = result.split('```')[1].split('```')[0].trim();
357
+ const stream = await this.#together.chat.completions.create({
358
+ model: this.modelName,
359
+ max_tokens: maxTokens,
360
+ temperature,
361
+ stream: true,
362
+ messages: [
363
+ { role: 'system', content: systemPrompt },
364
+ { role: 'user', content: userPrompt },
365
+ ],
366
+ });
367
+
368
+ let fullContent = '';
369
+ for await (const chunk of stream) {
370
+ if (controller.signal.aborted) break;
371
+ const delta = chunk.choices[0]?.delta?.content ?? '';
372
+ if (delta) {
373
+ fullContent += delta;
374
+ onChunk(delta, fullContent);
375
+ this.emit('stream:chunk', { delta, fullContent });
376
+ }
73
377
  }
74
- return JSON.parse(result);
75
- } catch (e) {
76
- this.onThought(`[WARNING] Failed to parse Pass 1 JSON. Attempting heuristic extraction...`);
77
- return {
78
- prismaSchema: "// Fallback schema\n" + result,
79
- aiSecurityConfig: "// Security fallback",
80
- aiDbRelations: "// Db Relations fallback",
81
- aiValidationLogic: "// Validation fallback"
82
- };
378
+
379
+ this.emit('stream:done', { fullContent });
380
+ return fullContent;
381
+
382
+ } finally {
383
+ this.#activeStreams.delete(controller);
83
384
  }
84
385
  }
85
386
 
86
- // --- PASS 2: Verification Loop (Dry-Run & DOM Sync) ---
387
+ // ── Pass 1: Generate code blocks (parallel sub-tasks) ────────────────────
388
+
389
+ async generateBackendBlocks(astJsonData, existingSchemaContent = null) {
390
+ this.#assertNotDisposed();
391
+ this.#thought(`[PASS-1] Analyzing ${astJsonData.length} AST endpoints…`);
392
+
393
+ const schemaDirective = existingSchemaContent
394
+ ? `An existing schema exists. Output an SQL Migration Script for changes only, plus the updated model definitions.`
395
+ : `Generate a comprehensive Prisma schema (schema.prisma). Infer many-to-many relationships, apply composite indexes, and add @@map decorators.`;
396
+
397
+ // Run all four generation tasks in parallel for speed
398
+ const [securityResult, validationResult, dbResult, schemaResult] = await Promise.all([
399
+
400
+ // Security config
401
+ this.promptModel(
402
+ `You are an expert Node.js security architect. Output ONLY raw JSON — no markdown, no explanation.`,
403
+ `Generate aiSecurityConfig for these endpoints: ${JSON.stringify(astJsonData.map(e => ({ method: e.method, route: e.route })), null, 2)}
404
+ Output: { "aiSecurityConfig": "string of middleware code" }`,
405
+ { expectJSON: true, outputShape: { aiSecurityConfig: '' } }
406
+ ),
407
+
408
+ // Validation logic
409
+ this.promptModel(
410
+ `You are a Zod/Joi validation expert. Output ONLY raw JSON — no markdown, no explanation.`,
411
+ `Generate aiValidationLogic for these schema shapes: ${JSON.stringify(astJsonData.map(e => e.schemaFields), null, 2)}
412
+ Output: { "aiValidationLogic": "string of Zod middleware code" }`,
413
+ { expectJSON: true, outputShape: { aiValidationLogic: '' } }
414
+ ),
415
+
416
+ // DB relations
417
+ this.promptModel(
418
+ `You are a database architect specializing in Prisma. Output ONLY raw JSON — no markdown, no explanation.`,
419
+ `Generate aiDbRelations (Prisma repository classes) for: ${JSON.stringify(astJsonData.map(e => e.controllerName), null, 2)}
420
+ Output: { "aiDbRelations": "string of repository code" }`,
421
+ { expectJSON: true, outputShape: { aiDbRelations: '' } }
422
+ ),
423
+
424
+ // Prisma schema
425
+ this.promptModel(
426
+ `You are a database schema designer. ${schemaDirective} Output ONLY raw JSON — no markdown, no explanation.`,
427
+ `AST data: ${JSON.stringify(astJsonData, null, 2)}
428
+ Output: { "prismaSchema": "string of schema.prisma content" }`,
429
+ { expectJSON: true, outputShape: { prismaSchema: '' } }
430
+ ),
431
+ ]);
432
+
433
+ this.#thought('[PASS-1] All parallel sub-tasks completed');
434
+
435
+ return {
436
+ prismaSchema: schemaResult?.prismaSchema ?? '// Schema generation failed',
437
+ aiSecurityConfig: securityResult?.aiSecurityConfig ?? '// Security generation failed',
438
+ aiDbRelations: dbResult?.aiDbRelations ?? '// DB relations generation failed',
439
+ aiValidationLogic: validationResult?.aiValidationLogic ?? '// Validation generation failed',
440
+ };
441
+ }
442
+
443
+ // ── Pass 2: Verification with auto self-healing ───────────────────────────
444
+
87
445
  async verifyDryRun(generatedBlocks, astJsonData) {
88
- this.onThought('[THOUGHT] Commencing Pass 2 Verification Loop (Virtual Dry Run)...');
446
+ this.#assertNotDisposed();
447
+ this.#thought('[PASS-2] Starting verification dry-run…');
89
448
 
90
- let issueFound = false;
449
+ const systemPrompt = `You are a strict QA engine for backend code.
450
+ Review the generated code against the original frontend AST.
451
+ Identify: missing DB relations, data-type mismatches, unvalidated fields.
452
+ Output ONLY raw JSON — no markdown, no preamble.`;
91
453
 
92
- // DOM Sync Level 2 (Data-type matching check)
93
- this.onThought('[THOUGHT] Simulating frontend component tree data injection against generated validation logic...');
94
-
95
- const systemPrompt = `You are a strict QA Engine.
96
- Review the following generated Validation Logic and DB Relations against the Frontend AST data shapes.
97
- Check for:
98
- 1. Missing DB relations (e.g., User -> Post).
99
- 2. Data-type mismatches (DOM Sync Level 2: if AST expects 'Date' string but DB expects 'DateTime', inject a transformation middleware).
454
+ const userPrompt = `Generated Validation:\n${generatedBlocks.aiValidationLogic}
455
+ Generated DB Relations:\n${generatedBlocks.aiDbRelations}
456
+ AST Shapes:\n${JSON.stringify(astJsonData.map(e => e.schemaFields), null, 2)}
100
457
 
101
- Output JSON:
102
- {
458
+ Output: {
103
459
  "issuesFound": boolean,
104
- "fixedValidationLogic": "string (original or fixed)",
105
- "fixedDbRelations": "string (original or fixed)",
460
+ "fixedValidationLogic": "string",
461
+ "fixedDbRelations": "string",
106
462
  "reasonings": ["string"]
107
463
  }`;
108
464
 
109
- const userPrompt = `Data:
110
- Generated Validation: ${generatedBlocks.aiValidationLogic}
111
- Generated DB Rel: ${generatedBlocks.aiDbRelations}
112
- AST Shapes: ${JSON.stringify(astJsonData.map(e => e.schemaFields), null, 2)}`;
113
-
114
- let result = await this.promptModel(systemPrompt, userPrompt);
115
-
116
465
  try {
117
- if (result.includes('```json')) result = result.split('```json')[1].split('```')[0].trim();
118
- else if (result.includes('```')) result = result.split('```')[1].split('```')[0].trim();
119
- const verified = JSON.parse(result);
120
-
466
+ const verified = await this.promptModel(systemPrompt, userPrompt, {
467
+ expectJSON: true,
468
+ outputShape: { issuesFound: false, fixedValidationLogic: '', fixedDbRelations: '', reasonings: [] },
469
+ bypassCache: true, // always re-verify — never use cached QA results
470
+ });
471
+
121
472
  if (verified.issuesFound) {
122
- this.onThought(`[THOUGHT] Verification caught issues! Self-healing triggered...`);
123
- verified.reasonings.forEach(r => this.onThought(`[THOUGHT] -> Fix applied: ${r}`));
473
+ this.#thought(`[PASS-2] Issues found applying ${verified.reasonings.length} self-heal(s)`);
474
+ verified.reasonings.forEach(r => {
475
+ this.#thought(` ↳ ${r}`);
476
+ this.emit('selfheal', { reason: r });
477
+ });
124
478
  return {
125
479
  ...generatedBlocks,
126
- aiValidationLogic: verified.fixedValidationLogic,
127
- aiDbRelations: verified.fixedDbRelations
480
+ aiValidationLogic: verified.fixedValidationLogic || generatedBlocks.aiValidationLogic,
481
+ aiDbRelations: verified.fixedDbRelations || generatedBlocks.aiDbRelations,
128
482
  };
129
- } else {
130
- this.onThought('[THOUGHT] Virtual Dry Run passed perfectly. Zero data mismatches found.');
131
- return generatedBlocks;
132
483
  }
133
- } catch (e) {
134
- this.onThought('[WARNING] Verification parsing failed. Using Pass 1 results.');
484
+
485
+ this.#thought('[PASS-2] Dry run passed zero issues detected');
486
+ return generatedBlocks;
487
+
488
+ } catch (err) {
489
+ this.#thought(`[PASS-2] Verification failed (${err.message}) — using Pass 1 output`);
490
+ this.emit('error:verify', { error: err.message });
135
491
  return generatedBlocks;
136
492
  }
137
493
  }
138
494
 
139
- // --- Autonomous Deployment Engine ---
140
- async generateDeploymentConfig(stack, astJsonData) {
141
- this.onThought(`[THOUGHT] Generating Autonomous Deployment workflows for [${stack}]...`);
142
-
143
- const systemPrompt = `Generate a highly optimized docker-compose.yml and a .github/workflows/deploy.yml for a production ${stack} backend.
144
- Include PostgreSQL, Redis, and best-practice health checks.
145
- Output JSON:
146
- {
147
- "dockerCompose": "string",
148
- "githubWorkflow": "string"
495
+ // ── Pass 3 (NEW): Architecture review ────────────────────────────────────
496
+
497
+ async reviewArchitecture(generatedBlocks, stack) {
498
+ this.#assertNotDisposed();
499
+ this.#thought('[PASS-3] Running architecture review…');
500
+
501
+ const systemPrompt = `You are a senior software architect.
502
+ Review the generated backend code for: SOLID violations, N+1 query risks, missing indexes, security gaps.
503
+ Output ONLY raw JSON.`;
504
+
505
+ const userPrompt = `Stack: ${stack}
506
+ Schema: ${generatedBlocks.prismaSchema?.slice(0, 2000)}
507
+ Validation: ${generatedBlocks.aiValidationLogic?.slice(0, 1000)}
508
+
509
+ Output: {
510
+ "score": number (0-100),
511
+ "criticalIssues": ["string"],
512
+ "recommendations": ["string"],
513
+ "approved": boolean
149
514
  }`;
150
515
 
151
- const userPrompt = `Target Stack: ${stack}\nAST Endpoints Count: ${astJsonData ? astJsonData.length : 0}`;
516
+ try {
517
+ const review = await this.promptModel(systemPrompt, userPrompt, {
518
+ expectJSON: true,
519
+ outputShape: { score: 0, criticalIssues: [], recommendations: [], approved: false },
520
+ });
521
+
522
+ this.#thought(`[PASS-3] Architecture score: ${review.score}/100 — approved: ${review.approved}`);
523
+ review.criticalIssues?.forEach(i => this.emit('arch:critical', { issue: i }));
524
+ return review;
525
+ } catch (err) {
526
+ this.#thought(`[PASS-3] Architecture review failed: ${err.message}`);
527
+ return { score: 0, criticalIssues: [], recommendations: [], approved: true };
528
+ }
529
+ }
530
+
531
+ // ── Deployment config generation ──────────────────────────────────────────
532
+
533
+ async generateDeploymentConfig(stack, astJsonData) {
534
+ this.#assertNotDisposed();
535
+ this.#thought(`[DEPLOY] Generating deployment config for [${stack}]…`);
536
+
537
+ const systemPrompt = `Generate production-grade docker-compose.yml and GitHub Actions deploy workflow.
538
+ Include: PostgreSQL, Redis, health checks, rolling updates, env var injection.
539
+ Output ONLY raw JSON — no markdown.`;
540
+
541
+ const userPrompt = `Stack: ${stack}
542
+ Endpoint count: ${astJsonData?.length ?? 0}
543
+ Output: { "dockerCompose": "string", "githubWorkflow": "string" }`;
152
544
 
153
- const res = await this.promptModel(systemPrompt, userPrompt);
154
-
155
545
  try {
156
- let clean = res;
157
- if (clean.includes('```json')) clean = clean.split('```json')[1].split('```')[0].trim();
158
- else if (clean.includes('```')) clean = clean.split('```')[1].split('```')[0].trim();
159
- const parsed = JSON.parse(clean);
160
- this.onThought('[THOUGHT] Deployment workflows synthesized successfully.');
161
- return parsed;
162
- } catch (e) {
163
- return { dockerCompose: "# Fallback Config", githubWorkflow: "# Fallback Workflow" };
546
+ const result = await this.promptModel(systemPrompt, userPrompt, {
547
+ expectJSON: true,
548
+ outputShape: { dockerCompose: '', githubWorkflow: '' },
549
+ });
550
+ this.#thought('[DEPLOY] Deployment config generated');
551
+ return result;
552
+ } catch (err) {
553
+ this.#thought(`[DEPLOY] Failed: ${err.message}`);
554
+ return {
555
+ dockerCompose: '# Generation failed — please create manually',
556
+ githubWorkflow: '# Generation failed — please create manually',
557
+ };
164
558
  }
165
559
  }
166
560
 
167
- async dispose() {
168
- this.onThought('[THOUGHT] Shutting down AI context...');
169
- // Together AI doesn't hold local VRAM or contexts to dispose, doing nothing.
561
+ // ── NEW: Generate test suite ──────────────────────────────────────────────
562
+
563
+ async generateTestSuite(endpoints, framework = 'vitest') {
564
+ this.#assertNotDisposed();
565
+ this.#thought(`[TESTS] Generating ${framework} test suite for ${endpoints.length} endpoints…`);
566
+
567
+ const systemPrompt = `You are a test engineer. Generate ${framework} integration tests.
568
+ Include: happy path, edge cases, auth guards, input validation errors.
569
+ Output ONLY raw JSON.`;
570
+
571
+ const userPrompt = `Endpoints: ${JSON.stringify(endpoints.map(e => ({ method: e.method, route: e.route, schema: e.schemaFields })), null, 2)}
572
+ Output: { "testSuite": "string of ${framework} test code", "testCount": number }`;
573
+
574
+ try {
575
+ const result = await this.promptModel(systemPrompt, userPrompt, {
576
+ expectJSON: true,
577
+ outputShape: { testSuite: '', testCount: 0 },
578
+ maxTokens: 8192,
579
+ });
580
+ this.#thought(`[TESTS] Generated ${result.testCount ?? '?'} test cases`);
581
+ return result;
582
+ } catch (err) {
583
+ this.#thought(`[TESTS] Test generation failed: ${err.message}`);
584
+ return { testSuite: '// Test generation failed', testCount: 0 };
585
+ }
170
586
  }
171
- }
587
+
588
+ // ── Stats & observability ─────────────────────────────────────────────────
589
+
590
+ getStats() {
591
+ return {
592
+ cacheSize: this.#cache.size,
593
+ circuitState: this.#circuit.state,
594
+ tokensUsed: this.#tokenBudget.used,
595
+ tokenLimit: this.#tokenBudget.limit,
596
+ tokensRemaining: this.#tokenBudget.remaining,
597
+ disposed: this.#disposed,
598
+ };
599
+ }
600
+
601
+ clearCache() {
602
+ this.#cache.clear();
603
+ this.#thought('[CACHE] Cache cleared');
604
+ }
605
+
606
+ // ── Private helpers ───────────────────────────────────────────────────────
607
+
608
+ #thought(msg) {
609
+ this.emit('thought', msg);
610
+ }
611
+
612
+ #assertNotDisposed() {
613
+ if (this.#disposed) throw new Error('Agent has been disposed — create a new instance');
614
+ }
615
+
616
+ #isRetryableError(err) {
617
+ const msg = (err.message ?? '').toLowerCase();
618
+ return (
619
+ msg.includes('rate limit') ||
620
+ msg.includes('timeout') ||
621
+ msg.includes('503') ||
622
+ msg.includes('502') ||
623
+ msg.includes('econnreset') ||
624
+ msg.includes('network') ||
625
+ (err.status >= 500 && err.status < 600)
626
+ );
627
+ }
628
+ }