create-backlist 7.3.1 → 9.0.0
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/bin/index.js +901 -471
- package/bin/qa.js +191 -0
- package/package.json +27 -18
- package/src/ai-agent.js +581 -124
- package/src/analyzer.js +628 -528
- package/src/env-resolver.js +70 -70
- package/src/generators/dotnet.js +134 -134
- package/src/generators/java.js +248 -248
- package/src/generators/js.js +345 -345
- package/src/generators/nestjs.js +277 -277
- package/src/generators/python.js +86 -86
- package/src/project-detector.js +131 -131
- package/src/qa/qa-engine.js +1187 -0
- package/src/templates/dotnet/partials/Dockerfile.ejs +27 -27
- package/src/templates/dotnet/partials/docker-compose.yml.ejs +33 -33
- package/src/templates/js-express/base/server.js +59 -59
- package/src/templates/js-express/partials/Dockerfile.ejs +12 -12
- package/src/templates/js-express/partials/auth.controller.js.ejs +66 -66
- package/src/templates/js-express/partials/auth.middleware.js.ejs +19 -19
- package/src/templates/js-express/partials/auth.routes.js.ejs +9 -9
- package/src/templates/js-express/partials/controller.js.ejs +53 -53
- package/src/templates/js-express/partials/db.js.ejs +19 -19
- package/src/templates/js-express/partials/docker-compose.yml.ejs +46 -46
- package/src/templates/js-express/partials/model.js.ejs +18 -18
- package/src/templates/js-express/partials/package.json.ejs +17 -17
- package/src/templates/js-express/partials/prisma.schema.ejs +21 -21
- package/src/templates/js-express/partials/routes.js.ejs +19 -19
- package/src/templates/js-express/partials/seeder.js.ejs +103 -103
- package/src/templates/js-express/partials/service.js.ejs +51 -51
- package/src/templates/js-express/partials/swagger.js.ejs +30 -30
- package/src/templates/js-express/partials/test.js.ejs +46 -46
- package/src/templates/nestjs/base/app.module.ts +9 -9
- package/src/templates/nestjs/base/main.ts +23 -23
- package/src/templates/nestjs/base/tsconfig.json +21 -21
- package/src/templates/nestjs/partials/auth.controller.ts.ejs +17 -17
- package/src/templates/nestjs/partials/auth.module.ts.ejs +17 -17
- package/src/templates/nestjs/partials/auth.service.ts.ejs +70 -70
- package/src/templates/nestjs/partials/controller.ts.ejs +34 -34
- package/src/templates/nestjs/partials/create-dto.ts.ejs +22 -22
- package/src/templates/nestjs/partials/jwt-guard.ts.ejs +24 -24
- package/src/templates/nestjs/partials/module.ts.ejs +10 -10
- package/src/templates/nestjs/partials/package.json.ejs +27 -27
- package/src/templates/nestjs/partials/prisma.service.ts.ejs +13 -13
- package/src/templates/nestjs/partials/schema.ts.ejs +19 -19
- package/src/templates/nestjs/partials/service.ts.ejs +67 -67
- package/src/templates/nestjs/partials/update-dto.ts.ejs +4 -4
package/src/ai-agent.js
CHANGED
|
@@ -1,171 +1,628 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
202
|
+
this.#assertNotDisposed();
|
|
203
|
+
this.#thought('[INIT] Initializing Together AI runtime…');
|
|
204
|
+
|
|
15
205
|
try {
|
|
16
|
-
this
|
|
17
|
-
this
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
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
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
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
|
|
446
|
+
this.#assertNotDisposed();
|
|
447
|
+
this.#thought('[PASS-2] Starting verification dry-run…');
|
|
89
448
|
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
102
|
-
{
|
|
458
|
+
Output: {
|
|
103
459
|
"issuesFound": boolean,
|
|
104
|
-
"fixedValidationLogic": "string
|
|
105
|
-
"fixedDbRelations": "string
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
123
|
-
verified.reasonings.forEach(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:
|
|
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
|
-
|
|
134
|
-
this
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this
|
|
161
|
-
return
|
|
162
|
-
} catch (
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|