Skip to content

Commit bbbfa05

Browse files
committed
feat(react-component-implementer): improvements
1 parent e527bfd commit bbbfa05

File tree

15 files changed

+412
-74
lines changed

15 files changed

+412
-74
lines changed

.changeset/auto-d2cc0c2c.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@auto-engineer/adk-claude-code-bridge": minor
3+
"@auto-engineer/app-assembler": minor
4+
"@auto-engineer/app-implementer": minor
5+
"@auto-engineer/checks": minor
6+
"@auto-engineer/cli": minor
7+
"@auto-engineer/component-implementor-react": minor
8+
"@auto-engineer/component-parser": minor
9+
"@auto-engineer/dev-server": minor
10+
"@auto-engineer/file-store": minor
11+
"@auto-engineer/file-upload": minor
12+
"@auto-engineer/generate-react-client": minor
13+
"@auto-engineer/id": minor
14+
"@auto-engineer/job-graph-processor": minor
15+
"@auto-engineer/message-bus": minor
16+
"@auto-engineer/message-store": minor
17+
"@auto-engineer/model-factory": minor
18+
"@auto-engineer/narrative": minor
19+
"@auto-engineer/pipeline": minor
20+
"@auto-engineer/release-automation": minor
21+
"@auto-engineer/server-generator-apollo-emmett": minor
22+
"@auto-engineer/server-generator-nestjs": minor
23+
"@auto-engineer/server-implementer": minor
24+
"@auto-engineer/set-theme": minor
25+
"@auto-engineer/submit-bug-report": minor
26+
"create-auto-app": minor
27+
---
28+
29+
Based on the actual diff, here's the changelog:
30+
31+
- Added pipeline logging with per-stage timing, token usage tracking, and a summary report for component generation runs
32+
- Improved pipeline performance by running initial type checks and tests in parallel and skipping fix loops when checks already pass
33+
- Enhanced fix loop agents with iteration tracking and smarter stage-level diagnostics

packages/component-implementor-react/src/agents/lint-fix-loop-agent.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export class LintFixLoopAgent extends BaseAgent {
4545
private readonly targetDir: string;
4646
private readonly filePaths: string[];
4747
private readonly maxIterations: number;
48+
iterationsUsed = 0;
49+
autoFixed = false;
4850

4951
constructor(config: LintFixLoopConfig) {
5052
const lintFixAgent = createLintFixAgent(config.model);
@@ -59,7 +61,11 @@ export class LintFixLoopAgent extends BaseAgent {
5961
}
6062

6163
protected async *runAsyncImpl(ctx: InvocationContext): AsyncGenerator<Event, void, void> {
64+
this.iterationsUsed = 0;
65+
this.autoFixed = false;
66+
6267
runLintFix(this.filePaths, this.targetDir);
68+
this.autoFixed = true;
6369
debug('Auto-fix pass completed');
6470

6571
for (let i = 0; i < this.maxIterations; i++) {
@@ -70,6 +76,7 @@ export class LintFixLoopAgent extends BaseAgent {
7076
return;
7177
}
7278

79+
this.iterationsUsed++;
7380
debug('Lint check failed with %d errors on iteration %d', result.errors.length, i);
7481

7582
const errorText = result.errors.join('\n');

packages/component-implementor-react/src/agents/pipeline-agent.ts

Lines changed: 123 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import type { BaseLlm } from 'adk-llm-bridge';
55
import createDebug from 'debug';
66
import { extractCodeBlock } from '../extract-code-block.js';
77
import { extractExportedNames } from '../extract-exports.js';
8+
import { PipelineLogger } from '../pipeline-logger.js';
89
import { generatePlaceholderComponent, generatePlaceholderStory } from '../placeholder.js';
910
import { sanitizeBodyWithImports } from '../sanitize-body.js';
1011
import { generateScaffold, type ScaffoldComposedComponent } from '../scaffold.js';
1112
import { type StoryVariant, scaffoldStoryFile } from '../scaffold-story.js';
1213
import { buildSpecText, type SpecDeltas } from '../spec-contract.js';
14+
import { runTestsAsync } from '../tools/test-runner.js';
15+
import { runTypeCheckAsync } from '../tools/type-checker.js';
1316
import { createComponentBodyAgent } from './component-body-agent.js';
1417
import { createEvaluationAgent } from './evaluation-agent.js';
1518
import { LintFixLoopAgent } from './lint-fix-loop-agent.js';
@@ -260,16 +263,27 @@ export class ComponentPipelineAgent extends BaseAgent {
260263
});
261264
ctx.session.events.push(userEvent);
262265

266+
const logger = new PipelineLogger(componentName);
267+
263268
debug('Stage 1: Generating component body and tests in parallel');
269+
logger.stageStart('Stage 1: Component body');
270+
logger.stageStart('Stage 1: Test generation');
271+
const llmStart1 = Date.now();
264272
const componentGen = this.runSubAgent(this.componentBodyAgent, ctx);
265273
const testGen = this.runSubAgent(this.testGenerationAgent, ctx);
266274
const [componentEvents, testEvents] = await Promise.all([
267275
this.collectEvents(componentGen),
268276
this.collectEvents(testGen),
269277
]);
278+
const llmDuration1 = Date.now() - llmStart1;
279+
logger.trackLlmTime(llmDuration1);
280+
logger.trackEvents(componentEvents);
281+
logger.trackEvents(testEvents);
270282
for (const event of componentEvents) yield event;
271283
for (const event of testEvents) yield event;
272284
this.llmCallCount += 2;
285+
logger.stageEnd('Stage 1: Component body');
286+
logger.stageEnd('Stage 1: Test generation');
273287

274288
const rawBody = String(ctx.session.state['component_body'] ?? '');
275289
const { body, additionalImports } = sanitizeBodyWithImports(rawBody, componentName);
@@ -291,58 +305,155 @@ export class ComponentPipelineAgent extends BaseAgent {
291305
writeFileSync(testPath, testCode, 'utf-8');
292306

293307
debug('Stage 2: Generating story');
308+
logger.stageStart('Stage 2: Story generation');
294309
if (storyVariants && storyVariants.length > 0) {
295310
const storyCode = scaffoldStoryFile({
296311
componentName,
297312
componentImportPath,
298313
variants: storyVariants,
299314
});
300315
writeFileSync(storyPath, storyCode, 'utf-8');
316+
logger.stageEnd('Stage 2: Story generation', 'scaffolded, no LLM');
301317
} else {
302318
ctx.session.state['component_code'] = componentCode;
303319

320+
const llmStart2 = Date.now();
321+
const storyEvents: Event[] = [];
304322
for await (const event of this.storyAgent.runAsync(ctx)) {
323+
storyEvents.push(event);
305324
yield event;
306325
}
326+
logger.trackLlmTime(Date.now() - llmStart2);
327+
logger.trackEvents(storyEvents);
307328
this.llmCallCount++;
308329

309330
const rawStory = String(ctx.session.state['story_code'] ?? '');
310331
const storyCode = extractCodeBlock(rawStory);
311332
writeFileSync(storyPath, storyCode, 'utf-8');
333+
logger.stageEnd('Stage 2: Story generation');
312334
}
313335

314-
debug('Stage 3: Fix loops — type check');
315-
for await (const event of this.typeFixLoopAgent.runAsync(ctx)) {
316-
yield event;
336+
debug('Stage 3: Parallel initial checks');
337+
logger.stageStart('Stage 3: Initial checks');
338+
const checksT0 = Date.now();
339+
const [typeResult, testResult, storyTypeResult] = await Promise.all([
340+
runTypeCheckAsync(targetDir, [componentPath, storyPath]),
341+
runTestsAsync(testPath, targetDir),
342+
runTypeCheckAsync(targetDir, [storyPath]),
343+
]);
344+
logger.trackToolTime(Date.now() - checksT0);
345+
logger.stageEnd(
346+
'Stage 3: Initial checks',
347+
[
348+
typeResult.passed ? 'types: ok' : `types: ${typeResult.errors.length} error(s)`,
349+
testResult.passed ? 'tests: ok' : `tests: ${testResult.failures.length} failure(s)`,
350+
storyTypeResult.passed ? 'story types: ok' : `story types: ${storyTypeResult.errors.length} error(s)`,
351+
].join(', '),
352+
);
353+
354+
if (!typeResult.passed) {
355+
debug('Stage 3: Fix loops — type check');
356+
logger.stageStart('Stage 3: Type check fix');
357+
const t0 = Date.now();
358+
for await (const event of this.typeFixLoopAgent.runAsync(ctx)) {
359+
logger.trackTokens(event);
360+
yield event;
361+
}
362+
const dur = Date.now() - t0;
363+
if (this.typeFixLoopAgent.iterationsUsed > 0) {
364+
logger.trackLlmTime(dur);
365+
logger.addFixIterations(this.typeFixLoopAgent.iterationsUsed);
366+
} else {
367+
logger.trackToolTime(dur);
368+
}
369+
logger.stageEnd('Stage 3: Type check fix', `${this.typeFixLoopAgent.iterationsUsed} fix iteration(s)`);
370+
} else {
371+
debug('Stage 3: Type check passed — skipping fix loop');
317372
}
318373

319-
debug('Stage 3: Fix loops — test fix');
320-
for await (const event of this.testFixLoopAgent.runAsync(ctx)) {
321-
yield event;
374+
if (!testResult.passed) {
375+
debug('Stage 3: Fix loops — test fix');
376+
logger.stageStart('Stage 3: Test fix');
377+
const t0 = Date.now();
378+
for await (const event of this.testFixLoopAgent.runAsync(ctx)) {
379+
logger.trackTokens(event);
380+
yield event;
381+
}
382+
const dur = Date.now() - t0;
383+
if (this.testFixLoopAgent.iterationsUsed > 0) {
384+
logger.trackLlmTime(dur);
385+
logger.addFixIterations(this.testFixLoopAgent.iterationsUsed);
386+
} else {
387+
logger.trackToolTime(dur);
388+
}
389+
logger.stageEnd('Stage 3: Test fix', `${this.testFixLoopAgent.iterationsUsed} fix iteration(s)`);
390+
} else {
391+
debug('Stage 3: Test run passed — skipping fix loop');
322392
}
323393

324-
debug('Stage 3: Fix loops — lint fix');
325-
for await (const event of this.lintFixLoopAgent.runAsync(ctx)) {
326-
yield event;
394+
{
395+
debug('Stage 3: Fix loops — lint fix');
396+
logger.stageStart('Stage 3: Lint fix');
397+
const t0 = Date.now();
398+
for await (const event of this.lintFixLoopAgent.runAsync(ctx)) {
399+
logger.trackTokens(event);
400+
yield event;
401+
}
402+
const dur = Date.now() - t0;
403+
if (this.lintFixLoopAgent.iterationsUsed > 0) {
404+
logger.trackLlmTime(dur);
405+
logger.addFixIterations(this.lintFixLoopAgent.iterationsUsed);
406+
} else {
407+
logger.trackToolTime(dur);
408+
}
409+
logger.stageEnd(
410+
'Stage 3: Lint fix',
411+
this.lintFixLoopAgent.iterationsUsed > 0
412+
? `${this.lintFixLoopAgent.iterationsUsed} fix iteration(s)`
413+
: 'passed after auto-fix',
414+
);
327415
}
328416

329-
debug('Stage 3: Fix loops — story fix');
330-
for await (const event of this.storyFixLoopAgent.runAsync(ctx)) {
331-
yield event;
417+
if (!storyTypeResult.passed) {
418+
debug('Stage 3: Fix loops — story fix');
419+
logger.stageStart('Stage 3: Story type check fix');
420+
const t0 = Date.now();
421+
for await (const event of this.storyFixLoopAgent.runAsync(ctx)) {
422+
logger.trackTokens(event);
423+
yield event;
424+
}
425+
const dur = Date.now() - t0;
426+
if (this.storyFixLoopAgent.iterationsUsed > 0) {
427+
logger.trackLlmTime(dur);
428+
logger.addFixIterations(this.storyFixLoopAgent.iterationsUsed);
429+
} else {
430+
logger.trackToolTime(dur);
431+
}
432+
logger.stageEnd('Stage 3: Story type check fix', `${this.storyFixLoopAgent.iterationsUsed} fix iteration(s)`);
433+
} else {
434+
debug('Stage 3: Story type check passed — skipping fix loop');
332435
}
333436

334437
debug('Stage 4: Evaluation');
438+
logger.stageStart('Stage 4: Evaluation');
335439
ctx.session.state['component_code'] = readFileSync(componentPath, 'utf-8');
336440
ctx.session.state['test_code'] = readFileSync(testPath, 'utf-8');
337441
ctx.session.state['story_code'] = readFileSync(storyPath, 'utf-8');
338442

443+
const llmStart4 = Date.now();
339444
for await (const event of this.evaluationAgent.runAsync(ctx)) {
445+
logger.trackTokens(event);
340446
yield event;
341447
}
448+
logger.trackLlmTime(Date.now() - llmStart4);
342449
this.llmCallCount++;
343450

344451
this.evaluationResult = this.parseEvaluation(String(ctx.session.state['evaluation_result'] ?? ''));
345452

453+
const evalScore = this.evaluationResult?.totalScore;
454+
logger.stageEnd('Stage 4: Evaluation', evalScore != null ? `score: ${evalScore}` : undefined);
455+
456+
logger.summary();
346457
debug('Pipeline complete. Evaluation score: %d', this.evaluationResult?.totalScore ?? -1);
347458
}
348459

packages/component-implementor-react/src/agents/story-fix-loop-agent.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
22
import { BaseAgent, createEvent, type Event, type InvocationContext } from '@google/adk';
33
import type { BaseLlm } from 'adk-llm-bridge';
44
import createDebug from 'debug';
5+
import { extractExportedNames } from '../extract-exports.js';
56
import { runTypeCheck } from '../tools/type-checker.js';
67
import { createStoryFixAgent } from './story-fix-agent.js';
78

@@ -21,6 +22,7 @@ export class StoryFixLoopAgent extends BaseAgent {
2122
private readonly componentPath: string;
2223
private readonly storyPath: string;
2324
private readonly maxIterations: number;
25+
iterationsUsed = 0;
2426

2527
constructor(config: StoryFixLoopConfig) {
2628
const storyFixAgent = createStoryFixAgent(config.model);
@@ -36,6 +38,8 @@ export class StoryFixLoopAgent extends BaseAgent {
3638
}
3739

3840
protected async *runAsyncImpl(ctx: InvocationContext): AsyncGenerator<Event, void, void> {
41+
this.iterationsUsed = 0;
42+
3943
for (let i = 0; i < this.maxIterations; i++) {
4044
const result = runTypeCheck(this.targetDir, [this.storyPath]);
4145

@@ -44,6 +48,7 @@ export class StoryFixLoopAgent extends BaseAgent {
4448
return;
4549
}
4650

51+
this.iterationsUsed++;
4752
debug('Story type check failed with %d errors on iteration %d', result.errors.length, i);
4853

4954
const componentCode = readFileSync(this.componentPath, 'utf-8');
@@ -54,6 +59,15 @@ export class StoryFixLoopAgent extends BaseAgent {
5459
// story file may not exist yet
5560
}
5661

62+
let componentRef: string;
63+
try {
64+
const exportNames = extractExportedNames(componentCode);
65+
const propsTypeMatch = componentCode.match(/(?:export\s+)?(?:type|interface)\s+\w+Props[\s\S]*?\}/);
66+
componentRef = propsTypeMatch ? `${propsTypeMatch[0]}\n\n// Exports: ${exportNames.join(', ')}` : componentCode;
67+
} catch {
68+
componentRef = componentCode;
69+
}
70+
5771
ctx.session.state.type_errors = result.errors.join('\n');
5872
ctx.session.state.story_code = storyCode;
5973
ctx.session.state.component_code = componentCode;
@@ -62,7 +76,7 @@ export class StoryFixLoopAgent extends BaseAgent {
6276
userMessage += `## File Paths\n- Story: ${this.storyPath}\n- Component: ${this.componentPath}\n\n`;
6377
userMessage += `## TypeScript Errors\n\n\`\`\`\n${result.errors.join('\n')}\n\`\`\`\n\n`;
6478
userMessage += `## Story Code\n\n\`\`\`tsx\n${storyCode}\n\`\`\`\n\n`;
65-
userMessage += `## Component Code (reference only)\n\n\`\`\`tsx\n${componentCode}\n\`\`\`\n\n`;
79+
userMessage += `## Component Exports (reference)\n\n\`\`\`tsx\n${componentRef}\n\`\`\`\n\n`;
6680
userMessage += `Use the write_file tool to write the fixed story file. Write the COMPLETE file content, not just the changed parts.`;
6781

6882
const userEvent = createEvent({

packages/component-implementor-react/src/agents/test-fix-loop-agent.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class TestFixLoopAgent extends BaseAgent {
2121
private readonly componentPath: string;
2222
private readonly testPath: string;
2323
private readonly maxIterations: number;
24+
iterationsUsed = 0;
2425

2526
constructor(config: TestFixLoopConfig) {
2627
const testFixAgent = createTestFixAgent(config.model);
@@ -36,6 +37,8 @@ export class TestFixLoopAgent extends BaseAgent {
3637
}
3738

3839
protected async *runAsyncImpl(ctx: InvocationContext): AsyncGenerator<Event, void, void> {
40+
this.iterationsUsed = 0;
41+
3942
for (let i = 0; i < this.maxIterations; i++) {
4043
const result = runTests(this.testPath, this.targetDir);
4144

@@ -44,6 +47,7 @@ export class TestFixLoopAgent extends BaseAgent {
4447
return;
4548
}
4649

50+
this.iterationsUsed++;
4751
debug('Tests failed with %d failures on iteration %d', result.failures.length, i);
4852

4953
const componentCode = readFileSync(this.componentPath, 'utf-8');

0 commit comments

Comments
 (0)