@yuaone/core 0.3.2 → 0.4.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.
Files changed (129) hide show
  1. package/dist/agent-loop.d.ts +62 -0
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +705 -18
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/background-agent.d.ts +110 -0
  6. package/dist/background-agent.d.ts.map +1 -0
  7. package/dist/background-agent.js +255 -0
  8. package/dist/background-agent.js.map +1 -0
  9. package/dist/coding-standards.d.ts +45 -0
  10. package/dist/coding-standards.d.ts.map +1 -0
  11. package/dist/coding-standards.js +1152 -0
  12. package/dist/coding-standards.js.map +1 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +2 -6
  15. package/dist/constants.js.map +1 -1
  16. package/dist/context-manager.d.ts +6 -0
  17. package/dist/context-manager.d.ts.map +1 -1
  18. package/dist/context-manager.js +23 -4
  19. package/dist/context-manager.js.map +1 -1
  20. package/dist/index.d.ts +28 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +26 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/llm-client.d.ts +8 -3
  25. package/dist/llm-client.d.ts.map +1 -1
  26. package/dist/llm-client.js +64 -13
  27. package/dist/llm-client.js.map +1 -1
  28. package/dist/plugin-auto-loader.d.ts +108 -0
  29. package/dist/plugin-auto-loader.d.ts.map +1 -0
  30. package/dist/plugin-auto-loader.js +743 -0
  31. package/dist/plugin-auto-loader.js.map +1 -0
  32. package/dist/plugin-registry.d.ts +112 -0
  33. package/dist/plugin-registry.d.ts.map +1 -0
  34. package/dist/plugin-registry.js +319 -0
  35. package/dist/plugin-registry.js.map +1 -0
  36. package/dist/plugin-types.d.ts +388 -0
  37. package/dist/plugin-types.d.ts.map +1 -0
  38. package/dist/plugin-types.js +8 -0
  39. package/dist/plugin-types.js.map +1 -0
  40. package/dist/plugin-validator.d.ts +54 -0
  41. package/dist/plugin-validator.d.ts.map +1 -0
  42. package/dist/plugin-validator.js +129 -0
  43. package/dist/plugin-validator.js.map +1 -0
  44. package/dist/repo-knowledge-graph.d.ts +112 -0
  45. package/dist/repo-knowledge-graph.d.ts.map +1 -0
  46. package/dist/repo-knowledge-graph.js +561 -0
  47. package/dist/repo-knowledge-graph.js.map +1 -0
  48. package/dist/role-registry.js +1 -1
  49. package/dist/role-registry.js.map +1 -1
  50. package/dist/self-debug-loop.d.ts +257 -0
  51. package/dist/self-debug-loop.d.ts.map +1 -0
  52. package/dist/self-debug-loop.js +870 -0
  53. package/dist/self-debug-loop.js.map +1 -0
  54. package/dist/skill-learner.d.ts +136 -0
  55. package/dist/skill-learner.d.ts.map +1 -0
  56. package/dist/skill-learner.js +382 -0
  57. package/dist/skill-learner.js.map +1 -0
  58. package/dist/skill-loader.d.ts +90 -0
  59. package/dist/skill-loader.d.ts.map +1 -0
  60. package/dist/skill-loader.js +309 -0
  61. package/dist/skill-loader.js.map +1 -0
  62. package/dist/specialist-registry.d.ts +132 -0
  63. package/dist/specialist-registry.d.ts.map +1 -0
  64. package/dist/specialist-registry.js +413 -0
  65. package/dist/specialist-registry.js.map +1 -0
  66. package/dist/sub-agent-prompts.d.ts +45 -0
  67. package/dist/sub-agent-prompts.d.ts.map +1 -0
  68. package/dist/sub-agent-prompts.js +177 -0
  69. package/dist/sub-agent-prompts.js.map +1 -0
  70. package/dist/sub-agent-router.d.ts +75 -0
  71. package/dist/sub-agent-router.d.ts.map +1 -0
  72. package/dist/sub-agent-router.js +174 -0
  73. package/dist/sub-agent-router.js.map +1 -0
  74. package/dist/sub-agent.d.ts +48 -0
  75. package/dist/sub-agent.d.ts.map +1 -1
  76. package/dist/sub-agent.js +108 -5
  77. package/dist/sub-agent.js.map +1 -1
  78. package/dist/system-prompt.d.ts +26 -0
  79. package/dist/system-prompt.d.ts.map +1 -1
  80. package/dist/system-prompt.js +177 -7
  81. package/dist/system-prompt.js.map +1 -1
  82. package/dist/task-classifier.d.ts +25 -1
  83. package/dist/task-classifier.d.ts.map +1 -1
  84. package/dist/task-classifier.js +171 -1
  85. package/dist/task-classifier.js.map +1 -1
  86. package/dist/tool-planner.d.ts +160 -0
  87. package/dist/tool-planner.d.ts.map +1 -0
  88. package/dist/tool-planner.js +501 -0
  89. package/dist/tool-planner.js.map +1 -0
  90. package/dist/types.d.ts +1 -1
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/world-state.d.ts.map +1 -1
  93. package/dist/world-state.js +8 -1
  94. package/dist/world-state.js.map +1 -1
  95. package/package.json +2 -1
  96. package/plugins/git/patterns/branch-patterns.json +101 -0
  97. package/plugins/git/patterns/commit-patterns.json +186 -0
  98. package/plugins/git/plugin.yaml +128 -0
  99. package/plugins/git/skills/branch-strategy.md +172 -0
  100. package/plugins/git/skills/commit-conv.md +178 -0
  101. package/plugins/git/skills/conflict-resolve.md +159 -0
  102. package/plugins/git/skills/history-clean.md +199 -0
  103. package/plugins/git/skills/pr-review.md +196 -0
  104. package/plugins/git/strategies/conflict-resolve.json +244 -0
  105. package/plugins/git/strategies/release-flow.json +292 -0
  106. package/plugins/git/validators/rules.json +348 -0
  107. package/plugins/react/patterns/anti-patterns.json +88 -0
  108. package/plugins/react/patterns/components.json +80 -0
  109. package/plugins/react/patterns/hooks.json +72 -0
  110. package/plugins/react/plugin.yaml +229 -0
  111. package/plugins/react/skills/bugfix.md +208 -0
  112. package/plugins/react/skills/component-gen.md +206 -0
  113. package/plugins/react/skills/hook-extract.md +208 -0
  114. package/plugins/react/skills/ssr.md +256 -0
  115. package/plugins/react/skills/test.md +273 -0
  116. package/plugins/react/strategies/build-fix.json +43 -0
  117. package/plugins/react/strategies/hook-loop-fix.json +36 -0
  118. package/plugins/react/strategies/hydration-fix.json +42 -0
  119. package/plugins/react/validators/rules.json +92 -0
  120. package/plugins/typescript/patterns/best-practices.json +25 -0
  121. package/plugins/typescript/patterns/common-errors.json +32 -0
  122. package/plugins/typescript/plugin.yaml +74 -0
  123. package/plugins/typescript/skills/debug.md +23 -0
  124. package/plugins/typescript/skills/migration.md +24 -0
  125. package/plugins/typescript/skills/refactor.md +22 -0
  126. package/plugins/typescript/skills/strict-mode.md +23 -0
  127. package/plugins/typescript/strategies/strict-migration.json +37 -0
  128. package/plugins/typescript/strategies/type-error-fix.json +37 -0
  129. package/plugins/typescript/validators/rules.json +28 -0
@@ -30,6 +30,17 @@ import { FailureRecovery } from "./failure-recovery.js";
30
30
  import { ExecutionPolicyEngine } from "./execution-policy-engine.js";
31
31
  import { CostOptimizer } from "./cost-optimizer.js";
32
32
  import { ImpactAnalyzer } from "./impact-analyzer.js";
33
+ import { SelfReflection } from "./self-reflection.js";
34
+ import { DebateOrchestrator } from "./debate-orchestrator.js";
35
+ import { ContinuousReflection, } from "./continuous-reflection.js";
36
+ import { PluginRegistry } from "./plugin-registry.js";
37
+ import { SkillLoader } from "./skill-loader.js";
38
+ import { SpecialistRegistry } from "./specialist-registry.js";
39
+ import { ToolPlanner } from "./tool-planner.js";
40
+ import { SelfDebugLoop } from "./self-debug-loop.js";
41
+ import { SkillLearner } from "./skill-learner.js";
42
+ import { RepoKnowledgeGraph } from "./repo-knowledge-graph.js";
43
+ import { BackgroundAgentManager } from "./background-agent.js";
33
44
  /**
34
45
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
35
46
  *
@@ -93,11 +104,35 @@ export class AgentLoop extends EventEmitter {
93
104
  worldState = null;
94
105
  costOptimizer;
95
106
  impactAnalyzer = null;
107
+ selfReflection = null;
108
+ debateOrchestrator = null;
109
+ continuousReflection = null;
110
+ enableSelfReflection;
111
+ enableDebate;
112
+ currentComplexity = "simple";
96
113
  policyOverrides;
97
114
  checkpointSaved = false;
98
115
  iterationCount = 0;
99
116
  originalSnapshots = new Map();
100
117
  previousStrategies = [];
118
+ pluginRegistry;
119
+ skillLoader;
120
+ specialistRegistry;
121
+ toolPlanner;
122
+ selfDebugLoop;
123
+ skillLearner = null;
124
+ repoGraph = null;
125
+ backgroundAgentManager = null;
126
+ enableToolPlanning;
127
+ enableSkillLearning;
128
+ enableBackgroundAgents;
129
+ currentToolPlan = null;
130
+ executedToolNames = [];
131
+ /** Context Budget: max 3 active skills at once */
132
+ activeSkillIds = [];
133
+ static MAX_ACTIVE_SKILLS = 3;
134
+ /** Context Budget: track injected system messages per iteration to cap at 5 */
135
+ iterationSystemMsgCount = 0;
101
136
  tokenUsage = {
102
137
  input: 0,
103
138
  output: 0,
@@ -112,6 +147,8 @@ export class AgentLoop extends EventEmitter {
112
147
  this.enablePlanning = options.enablePlanning !== false;
113
148
  this.planningThreshold = options.planningThreshold ?? "moderate";
114
149
  this.environment = options.environment;
150
+ this.enableSelfReflection = options.enableSelfReflection !== false;
151
+ this.enableDebate = options.enableDebate !== false;
115
152
  // BYOK LLM 클라이언트 생성
116
153
  this.llmClient = new BYOKClient(options.config.byok);
117
154
  // Governor 생성
@@ -145,6 +182,16 @@ export class AgentLoop extends EventEmitter {
145
182
  // InterruptManager 설정 (외부 주입 또는 내부 생성)
146
183
  this.interruptManager = options.interruptManager ?? new InterruptManager();
147
184
  this.setupInterruptListeners();
185
+ // PluginRegistry + SkillLoader 초기화
186
+ this.pluginRegistry = options.pluginRegistry ?? new PluginRegistry();
187
+ this.skillLoader = new SkillLoader();
188
+ // Advanced Intelligence 모듈 초기화
189
+ this.specialistRegistry = new SpecialistRegistry();
190
+ this.toolPlanner = new ToolPlanner();
191
+ this.selfDebugLoop = new SelfDebugLoop();
192
+ this.enableToolPlanning = options.enableToolPlanning !== false;
193
+ this.enableSkillLearning = options.enableSkillLearning !== false;
194
+ this.enableBackgroundAgents = options.enableBackgroundAgents === true;
148
195
  // 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
149
196
  this.contextManager.addMessage({
150
197
  role: "system",
@@ -260,6 +307,40 @@ export class AgentLoop extends EventEmitter {
260
307
  if (projectPath) {
261
308
  this.reflexionEngine = new ReflexionEngine({ projectPath });
262
309
  }
310
+ // SelfReflection 생성 (6D deep verify + quick verify)
311
+ if (this.enableSelfReflection) {
312
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
313
+ this.selfReflection = new SelfReflection(sessionId, {
314
+ enableDeepVerify: true,
315
+ enableMonologue: true,
316
+ enableLearning: true,
317
+ minScoreToPass: 70,
318
+ criticalDimensions: ["correctness", "security"],
319
+ });
320
+ // 메모리에서 기존 학습 복원
321
+ if (this.memoryManager) {
322
+ try {
323
+ const memory = await this.memoryManager.load();
324
+ this.selfReflection.loadFromMemory(memory);
325
+ }
326
+ catch {
327
+ // 학습 복원 실패는 치명적이지 않음
328
+ }
329
+ }
330
+ }
331
+ // DebateOrchestrator 생성 (complex/massive 태스크에서 multi-agent debate)
332
+ if (this.enableDebate && projectPath) {
333
+ this.debateOrchestrator = new DebateOrchestrator({
334
+ projectPath,
335
+ maxRounds: 3,
336
+ qualityThreshold: 80,
337
+ verifyBetweenRounds: true,
338
+ byokConfig: this.config.byok,
339
+ toolExecutor: this.toolExecutor,
340
+ maxTokensPerCall: 16384,
341
+ totalTokenBudget: Math.floor(this.config.loop.totalTokenBudget * 0.3),
342
+ });
343
+ }
263
344
  // 향상된 시스템 프롬프트 생성
264
345
  const enhancedPrompt = buildSystemPrompt({
265
346
  projectStructure,
@@ -289,6 +370,152 @@ export class AgentLoop extends EventEmitter {
289
370
  }
290
371
  }
291
372
  }
373
+ // SkillLearner 초기화 (경험에서 학습된 스킬 자동 로드)
374
+ if (this.enableSkillLearning && projectPath) {
375
+ try {
376
+ this.skillLearner = new SkillLearner(projectPath);
377
+ await this.skillLearner.init();
378
+ // 학습된 스킬 중 관련 있는 것을 플러그인 레지스트리에 등록
379
+ const learnedSkills = this.skillLearner.getAllSkills();
380
+ if (learnedSkills.length > 0) {
381
+ const skillNames = learnedSkills
382
+ .filter((s) => s.confidence >= 0.3)
383
+ .map((s) => s.id);
384
+ if (skillNames.length > 0) {
385
+ this.contextManager.addMessage({
386
+ role: "system",
387
+ content: `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`,
388
+ });
389
+ }
390
+ }
391
+ }
392
+ catch {
393
+ // SkillLearner 초기화 실패는 치명적이지 않음
394
+ this.skillLearner = null;
395
+ }
396
+ }
397
+ // RepoKnowledgeGraph 초기화 (코드 구조 그래프 — 비동기 빌드)
398
+ if (projectPath) {
399
+ try {
400
+ this.repoGraph = new RepoKnowledgeGraph(projectPath);
401
+ // 백그라운드에서 그래프 빌드 (블로킹하지 않음)
402
+ this.repoGraph.buildFromProject(projectPath).catch(() => {
403
+ // 그래프 빌드 실패는 치명적이지 않음
404
+ });
405
+ }
406
+ catch {
407
+ this.repoGraph = null;
408
+ }
409
+ }
410
+ // BackgroundAgentManager 초기화 (opt-in)
411
+ if (this.enableBackgroundAgents && projectPath) {
412
+ try {
413
+ this.backgroundAgentManager = new BackgroundAgentManager();
414
+ this.backgroundAgentManager.createDefaults(projectPath);
415
+ // Background events → agent loop events
416
+ for (const agent of this.backgroundAgentManager.list()) {
417
+ const bgAgent = this.backgroundAgentManager.get(agent.id);
418
+ if (bgAgent) {
419
+ bgAgent.on("event", (event) => {
420
+ if (event.type === "error" || event.type === "warning") {
421
+ this.emitEvent({
422
+ kind: "agent:thinking",
423
+ content: `[Background: ${event.agentId}] ${event.message}`,
424
+ });
425
+ }
426
+ });
427
+ }
428
+ }
429
+ }
430
+ catch {
431
+ this.backgroundAgentManager = null;
432
+ }
433
+ }
434
+ // Inject active plugin skills into system prompt (lazy: names only)
435
+ // Full skill content is loaded on-demand when triggered (file-pattern or error match)
436
+ const activeSkills = this.pluginRegistry.getAllSkills();
437
+ if (activeSkills.length > 0) {
438
+ const skillSummary = activeSkills
439
+ .map((s) => `${s.pluginId}/${s.skill.name}`)
440
+ .join(", ");
441
+ this.contextManager.addMessage({
442
+ role: "system",
443
+ content: `[Plugins: ${skillSummary}] — Skills auto-activate on matching files/errors.`,
444
+ });
445
+ }
446
+ // ContinuousReflection 생성 (1분 간격 체크포인트 + 자기검증 + 컨텍스트 모니터)
447
+ this.continuousReflection = new ContinuousReflection({
448
+ getState: () => this.getStateSnapshot(),
449
+ checkpoint: async (state, emergency) => {
450
+ if (!this.continuationEngine)
451
+ return;
452
+ const checkpoint = {
453
+ sessionId: crypto.randomUUID(),
454
+ goal: state.goal,
455
+ progress: {
456
+ completedTasks: state.completedTasks,
457
+ currentTask: state.currentTask,
458
+ remainingTasks: state.remainingTasks,
459
+ },
460
+ changedFiles: state.changedFiles.map((path) => ({ path, diff: "" })),
461
+ workingMemory: state.workingMemory,
462
+ yuanMdUpdates: [],
463
+ errors: state.errors,
464
+ contextUsageAtSave: state.contextUsagePercent,
465
+ totalTokensUsed: state.totalTokensUsed,
466
+ iterationsCompleted: state.iteration,
467
+ createdAt: new Date(),
468
+ };
469
+ await this.continuationEngine.saveCheckpoint(checkpoint);
470
+ },
471
+ selfVerify: async (prompt) => {
472
+ // 경량 LLM 호출 (~200 토큰)로 자기검증
473
+ try {
474
+ const response = await this.llmClient.chat([{ role: "user", content: prompt }], []);
475
+ const text = typeof response.content === "string"
476
+ ? response.content
477
+ : "";
478
+ const parsed = JSON.parse(text);
479
+ return {
480
+ onTrack: Boolean(parsed.onTrack),
481
+ needsCorrection: Boolean(parsed.needsCorrection),
482
+ issue: String(parsed.issue ?? ""),
483
+ suggestion: String(parsed.suggestion ?? ""),
484
+ confidence: Number(parsed.confidence ?? 0.5),
485
+ };
486
+ }
487
+ catch {
488
+ return {
489
+ onTrack: true,
490
+ needsCorrection: false,
491
+ issue: "",
492
+ suggestion: "",
493
+ confidence: 0.5,
494
+ };
495
+ }
496
+ },
497
+ });
498
+ // Wire ContinuousReflection events to agent loop
499
+ this.continuousReflection.on("reflection:feedback", (feedback) => {
500
+ this.contextManager.addMessage({
501
+ role: "system",
502
+ content: `[ContinuousReflection] ${feedback}`,
503
+ });
504
+ });
505
+ this.continuousReflection.on("reflection:context_warning", (usagePercent) => {
506
+ this.emitEvent({
507
+ kind: "agent:thinking",
508
+ content: `Context usage at ${Math.round(usagePercent * 100)}% — emergency checkpoint saved.`,
509
+ });
510
+ });
511
+ this.continuousReflection.on("reflection:context_overflow", () => {
512
+ // Trigger abort — the continuation checkpoint is already saved
513
+ this.emitEvent({
514
+ kind: "agent:thinking",
515
+ content: "Context usage at 95%+ — saving state and stopping.",
516
+ });
517
+ this.abort();
518
+ });
292
519
  }
293
520
  /**
294
521
  * MemoryManager의 학습/실패 기록을 시스템 메시지로 변환.
@@ -333,6 +560,8 @@ export class AgentLoop extends EventEmitter {
333
560
  this.iterationCount = 0;
334
561
  this.originalSnapshots.clear();
335
562
  this.previousStrategies = [];
563
+ this.activeSkillIds = [];
564
+ this.iterationSystemMsgCount = 0;
336
565
  this.failureRecovery.reset();
337
566
  this.costOptimizer.reset();
338
567
  this.tokenBudgetManager.reset();
@@ -383,11 +612,94 @@ export class AgentLoop extends EventEmitter {
383
612
  content: classificationHint,
384
613
  });
385
614
  }
615
+ // Specialist routing: 태스크 타입에 맞는 전문 에이전트 설정 주입
616
+ if (classification.specialistDomain) {
617
+ const specialistMatch = this.specialistRegistry.findSpecialist(classification.specialistDomain);
618
+ if (specialistMatch && specialistMatch.confidence >= 0.5) {
619
+ this.contextManager.addMessage({
620
+ role: "system",
621
+ content: `[Specialist: ${specialistMatch.specialist.name}] ${specialistMatch.specialist.systemPrompt.slice(0, 500)}`,
622
+ });
623
+ }
624
+ }
625
+ // Tool Planning: 태스크 타입에 맞는 도구 실행 계획 힌트 주입
626
+ if (this.enableToolPlanning && classification.confidence >= 0.3) {
627
+ const planContext = {
628
+ userMessage,
629
+ };
630
+ this.currentToolPlan = this.toolPlanner.planForTask(classification.type, planContext);
631
+ this.executedToolNames = [];
632
+ if (this.currentToolPlan.confidence >= 0.5) {
633
+ const planHint = this.toolPlanner.formatPlanHint(this.currentToolPlan);
634
+ this.contextManager.addMessage({
635
+ role: "system",
636
+ content: planHint,
637
+ });
638
+ }
639
+ }
386
640
  // 복잡도 감지 → 필요 시 자동 플래닝
387
641
  await this.maybeCreatePlan(userMessage);
388
- const result = await this.executeLoop();
642
+ // ContinuousReflection 시작 (1분 간격 체크포인트/자기검증/컨텍스트모니터)
643
+ if (this.continuousReflection) {
644
+ this.continuousReflection.start();
645
+ }
646
+ let result;
647
+ try {
648
+ result = await this.executeLoop();
649
+ }
650
+ finally {
651
+ // ContinuousReflection 정지 (루프 종료 시 반드시 정리)
652
+ if (this.continuousReflection) {
653
+ this.continuousReflection.stop();
654
+ }
655
+ }
389
656
  // 실행 완료 후 메모리 자동 업데이트
390
657
  await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
658
+ // SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
659
+ if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
660
+ try {
661
+ const errorToolResults = this.allToolResults.filter((r) => !r.success);
662
+ if (errorToolResults.length > 0 && this.changedFiles.length > 0) {
663
+ // 에러가 있었지만 결국 성공 → 학습 가능한 패턴
664
+ const runAnalysis = this.memoryUpdater.analyzeRun({
665
+ goal: userMessage,
666
+ termination: { reason: result.reason, summary: result.summary },
667
+ toolResults: this.allToolResults.map((r) => ({
668
+ name: r.name,
669
+ success: r.success,
670
+ output: r.output.slice(0, 500),
671
+ })),
672
+ changedFiles: this.changedFiles,
673
+ messages: [],
674
+ tokensUsed: this.tokenUsage.total,
675
+ durationMs: Date.now() - runStartTime,
676
+ iterations: this.iterationCount,
677
+ });
678
+ this.skillLearner.extractSkillFromRun(runAnalysis, `session-${Date.now()}`);
679
+ await this.skillLearner.save();
680
+ }
681
+ }
682
+ catch {
683
+ // 학습 실패는 치명적이지 않음
684
+ }
685
+ }
686
+ // Tool plan compliance check (non-blocking, for metrics)
687
+ if (this.currentToolPlan && this.executedToolNames.length > 0) {
688
+ try {
689
+ this.toolPlanner.validateExecution(this.currentToolPlan, this.executedToolNames);
690
+ }
691
+ catch {
692
+ // compliance check 실패는 치명적이지 않음
693
+ }
694
+ }
695
+ // RepoKnowledgeGraph: 변경 파일 그래프 업데이트
696
+ if (this.repoGraph && this.changedFiles.length > 0) {
697
+ this.repoGraph.updateFiles(this.changedFiles).catch(() => {
698
+ // 그래프 업데이트 실패는 치명적이지 않음
699
+ });
700
+ }
701
+ // BackgroundAgentManager: 정리 (stopAll은 abort 시에만)
702
+ // Background agents는 세션 간 지속되므로 여기서 stop하지 않음
391
703
  return result;
392
704
  }
393
705
  catch (err) {
@@ -504,6 +816,68 @@ export class AgentLoop extends EventEmitter {
504
816
  getTokenUsage() {
505
817
  return { ...this.tokenUsage };
506
818
  }
819
+ /** Get the plugin registry for external management */
820
+ getPluginRegistry() {
821
+ return this.pluginRegistry;
822
+ }
823
+ /** Get the specialist registry for custom specialist registration */
824
+ getSpecialistRegistry() {
825
+ return this.specialistRegistry;
826
+ }
827
+ /** Get the skill learner for inspection */
828
+ getSkillLearner() {
829
+ return this.skillLearner;
830
+ }
831
+ /** Get the repo knowledge graph */
832
+ getRepoGraph() {
833
+ return this.repoGraph;
834
+ }
835
+ /** Get background agent manager */
836
+ getBackgroundAgentManager() {
837
+ return this.backgroundAgentManager;
838
+ }
839
+ /**
840
+ * ContinuousReflection이 사용하는 에이전트 상태 스냅샷을 생성한다.
841
+ * 읽기 전용 — reflection tick마다 호출된다.
842
+ */
843
+ getStateSnapshot() {
844
+ const userMsg = this.contextManager.getMessages().find((m) => m.role === "user");
845
+ const goal = typeof userMsg?.content === "string" ? userMsg.content : "";
846
+ const progress = this.extractProgress();
847
+ const recentTools = this.allToolResults.slice(-3).map((r) => ({
848
+ tool: r.name,
849
+ input: typeof r.tool_call_id === "string" ? r.tool_call_id : "",
850
+ output: r.output.slice(0, 200),
851
+ }));
852
+ const errors = this.allToolResults
853
+ .filter((r) => !r.success)
854
+ .slice(-5)
855
+ .map((r) => `${r.name}: ${r.output.slice(0, 200)}`);
856
+ return {
857
+ goal,
858
+ iteration: this.iterationCount,
859
+ maxIteration: this.config.loop.maxIterations,
860
+ changedFiles: [...this.changedFiles],
861
+ recentToolCalls: recentTools,
862
+ errors,
863
+ contextUsagePercent: this.config.loop.totalTokenBudget > 0
864
+ ? this.tokenUsage.total / this.config.loop.totalTokenBudget
865
+ : 0,
866
+ sessionId: crypto.randomUUID(),
867
+ totalTokensUsed: this.tokenUsage.total,
868
+ workingMemory: this.buildWorkingMemorySummary(),
869
+ completedTasks: progress.completedTasks,
870
+ currentTask: progress.currentTask,
871
+ remainingTasks: progress.remainingTasks,
872
+ };
873
+ }
874
+ /**
875
+ * ContinuousReflection 인스턴스를 반환한다.
876
+ * 외부에서 ESC 토글(pause/resume) 또는 이벤트 구독에 사용.
877
+ */
878
+ getContinuousReflection() {
879
+ return this.continuousReflection;
880
+ }
507
881
  /**
508
882
  * 대화 히스토리를 반환.
509
883
  */
@@ -544,6 +918,7 @@ export class AgentLoop extends EventEmitter {
544
918
  if (!this.planner || !this.enablePlanning)
545
919
  return;
546
920
  const complexity = this.detectComplexity(userMessage);
921
+ this.currentComplexity = complexity;
547
922
  // 임계값 미만이면 플래닝 스킵
548
923
  const thresholdOrder = { simple: 1, moderate: 2, complex: 3 };
549
924
  const complexityOrder = {
@@ -771,6 +1146,36 @@ export class AgentLoop extends EventEmitter {
771
1146
  iteration++;
772
1147
  this.iterationCount = iteration;
773
1148
  const iterationStart = Date.now();
1149
+ this.iterationSystemMsgCount = 0; // Reset per-iteration system message counter
1150
+ // Check file-pattern skill triggers based on changed files
1151
+ // Guard: skip skill injection if over 80% token budget to preserve remaining budget
1152
+ // Guard: max 3 active skills globally (Context Budget Rules)
1153
+ const fileTriggerBudgetRatio = this.config.loop.totalTokenBudget > 0
1154
+ ? this.tokenUsage.total / this.config.loop.totalTokenBudget
1155
+ : 0;
1156
+ if (this.changedFiles.length > 0 &&
1157
+ fileTriggerBudgetRatio <= 0.8 &&
1158
+ this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
1159
+ const lastFile = this.changedFiles[this.changedFiles.length - 1];
1160
+ const fileSkills = this.pluginRegistry.findMatchingSkills({
1161
+ filePath: lastFile,
1162
+ });
1163
+ // Max 2 skills per iteration, capped by global 3-skill limit
1164
+ const slotsRemaining = AgentLoop.MAX_ACTIVE_SKILLS - this.activeSkillIds.length;
1165
+ for (const skill of fileSkills.slice(0, Math.min(2, slotsRemaining))) {
1166
+ if (this.activeSkillIds.includes(skill.id))
1167
+ continue; // no duplicate
1168
+ const parsed = this.skillLoader.loadTemplate(skill);
1169
+ if (parsed) {
1170
+ this.contextManager.addMessage({
1171
+ role: "system",
1172
+ content: `[File Skill: ${skill.name}] ${parsed.domain ? `[${parsed.domain}] ` : ""}${skill.description}`,
1173
+ });
1174
+ this.activeSkillIds.push(skill.id);
1175
+ this.iterationSystemMsgCount++;
1176
+ }
1177
+ }
1178
+ }
774
1179
  // 1. 컨텍스트 준비
775
1180
  const messages = this.contextManager.prepareForLLM();
776
1181
  // 2. LLM 호출 (streaming)
@@ -832,6 +1237,106 @@ export class AgentLoop extends EventEmitter {
832
1237
  // 3. 응답 처리
833
1238
  if (response.toolCalls.length === 0) {
834
1239
  const content = response.content ?? "";
1240
+ // Level 2: Deep verification before declaring completion
1241
+ if (this.selfReflection && this.changedFiles.length > 0) {
1242
+ try {
1243
+ const changedFilesMap = new Map();
1244
+ for (const filePath of this.changedFiles) {
1245
+ const lastWrite = this.allToolResults
1246
+ .filter((r) => r.name === "file_write" || r.name === "file_edit")
1247
+ .find((r) => r.output.includes(filePath));
1248
+ if (lastWrite) {
1249
+ changedFilesMap.set(filePath, lastWrite.output);
1250
+ }
1251
+ }
1252
+ const verifyFn = async (prompt) => {
1253
+ const verifyResponse = await this.llmClient.chat([
1254
+ { role: "system", content: "You are a meticulous code reviewer." },
1255
+ { role: "user", content: prompt },
1256
+ ]);
1257
+ this.tokenBudgetManager.recordUsage("validator", verifyResponse.usage.input, verifyResponse.usage.output);
1258
+ return verifyResponse.content ?? "";
1259
+ };
1260
+ const deepResult = await this.selfReflection.deepVerify(content || "Complete the task", changedFilesMap, this.originalSnapshots, [], // projectPatterns — could be enhanced with YUAN.md rules
1261
+ verifyFn);
1262
+ if (deepResult.verdict === "fail" && deepResult.confidence >= 0.5) {
1263
+ // Not confident enough — inject feedback and continue loop
1264
+ const issuesList = deepResult.suggestedFixes
1265
+ .map((f) => `[${f.severity}] ${f.description}`)
1266
+ .join("; ");
1267
+ this.contextManager.addMessage({
1268
+ role: "assistant",
1269
+ content: content || "",
1270
+ });
1271
+ this.contextManager.addMessage({
1272
+ role: "system",
1273
+ content: `[Self-Reflection L2] Verification failed (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence.toFixed(2)}). ${deepResult.selfCritique}${issuesList ? ` Issues: ${issuesList}` : ""}. Please address these before completing.`,
1274
+ });
1275
+ this.emitEvent({
1276
+ kind: "agent:thinking",
1277
+ content: `Deep verification failed (score: ${deepResult.overallScore}). Continuing to address issues...`,
1278
+ });
1279
+ continue; // Don't return GOAL_ACHIEVED, continue the loop
1280
+ }
1281
+ // Level 3: Multi-agent debate for complex/massive tasks
1282
+ if (this.debateOrchestrator &&
1283
+ ["complex", "massive"].includes(this.currentComplexity) &&
1284
+ deepResult.verdict !== "pass") {
1285
+ try {
1286
+ this.emitEvent({
1287
+ kind: "agent:thinking",
1288
+ content: `Triggering multi-agent debate for ${this.currentComplexity} task verification...`,
1289
+ });
1290
+ const debateContext = [
1291
+ `Changed files: ${this.changedFiles.join(", ")}`,
1292
+ `Self-reflection score: ${deepResult.overallScore}/100`,
1293
+ `Concerns: ${deepResult.selfCritique}`,
1294
+ ].join("\n");
1295
+ const debateResult = await this.debateOrchestrator.debate(content || "Verify task completion", debateContext);
1296
+ // Track debate token usage (split roughly 60/40 input/output)
1297
+ const debateInput = Math.round(debateResult.totalTokensUsed * 0.6);
1298
+ const debateOutput = debateResult.totalTokensUsed - debateInput;
1299
+ this.tokenUsage.input += debateInput;
1300
+ this.tokenUsage.output += debateOutput;
1301
+ this.tokenUsage.total += debateResult.totalTokensUsed;
1302
+ if (!debateResult.success) {
1303
+ this.contextManager.addMessage({
1304
+ role: "assistant",
1305
+ content: content || "",
1306
+ });
1307
+ this.contextManager.addMessage({
1308
+ role: "system",
1309
+ content: `[Debate] Multi-agent debate did not pass (score: ${debateResult.finalScore}). ${debateResult.summary}. Please address the identified issues.`,
1310
+ });
1311
+ this.emitEvent({
1312
+ kind: "agent:thinking",
1313
+ content: `Debate failed (score: ${debateResult.finalScore}). Continuing to address issues...`,
1314
+ });
1315
+ continue; // Continue loop to address debate feedback
1316
+ }
1317
+ this.emitEvent({
1318
+ kind: "agent:thinking",
1319
+ content: `Debate passed (score: ${debateResult.finalScore}). Proceeding to completion.`,
1320
+ });
1321
+ }
1322
+ catch {
1323
+ // Debate failure is non-fatal — proceed with completion
1324
+ }
1325
+ }
1326
+ // Persist learnings from self-reflection
1327
+ if (this.memoryManager) {
1328
+ try {
1329
+ await this.selfReflection.persistLearnings(this.memoryManager);
1330
+ }
1331
+ catch {
1332
+ // Learning persistence failure is non-fatal
1333
+ }
1334
+ }
1335
+ }
1336
+ catch {
1337
+ // Deep verification failure is non-fatal — proceed with completion
1338
+ }
1339
+ }
835
1340
  if (content) {
836
1341
  this.contextManager.addMessage({
837
1342
  role: "assistant",
@@ -861,9 +1366,13 @@ export class AgentLoop extends EventEmitter {
861
1366
  });
862
1367
  }
863
1368
  // 4. 도구 실행
864
- const toolResults = await this.executeTools(response.toolCalls);
1369
+ const { results: toolResults, deferredFixPrompts } = await this.executeTools(response.toolCalls);
865
1370
  // Reflexion: 도구 결과 수집
866
1371
  this.allToolResults.push(...toolResults);
1372
+ // Tool plan tracking: 실행된 도구 이름 기록
1373
+ for (const result of toolResults) {
1374
+ this.executedToolNames.push(result.name);
1375
+ }
867
1376
  // 5. 도구 결과를 히스토리에 추가 (살균 + 압축)
868
1377
  for (const result of toolResults) {
869
1378
  // Prompt injection 방어: 도구 출력 살균
@@ -883,6 +1392,14 @@ export class AgentLoop extends EventEmitter {
883
1392
  tool_call_id: result.tool_call_id,
884
1393
  });
885
1394
  }
1395
+ // AutoFix: deferred fix prompts를 tool results 뒤에 추가
1396
+ // (tool results 전에 넣으면 OpenAI 400 에러 발생 — tool_calls 뒤에 tool 메시지가 바로 와야 함)
1397
+ for (const fixPrompt of deferredFixPrompts) {
1398
+ this.contextManager.addMessage({
1399
+ role: "user",
1400
+ content: fixPrompt,
1401
+ });
1402
+ }
886
1403
  // iteration 이벤트
887
1404
  this.emitEvent({
888
1405
  kind: "agent:iteration",
@@ -890,6 +1407,47 @@ export class AgentLoop extends EventEmitter {
890
1407
  tokensUsed: response.usage.input + response.usage.output,
891
1408
  durationMs: Date.now() - iterationStart,
892
1409
  });
1410
+ // Level 1: Quick verification after every 3rd iteration
1411
+ if (this.selfReflection && iteration % 3 === 0) {
1412
+ try {
1413
+ const changedFilesMap = new Map();
1414
+ for (const filePath of this.changedFiles) {
1415
+ // Collect changed file contents from tool results
1416
+ const lastWrite = this.allToolResults
1417
+ .filter((r) => r.name === "file_write" || r.name === "file_edit")
1418
+ .find((r) => r.output.includes(filePath));
1419
+ if (lastWrite) {
1420
+ changedFilesMap.set(filePath, lastWrite.output);
1421
+ }
1422
+ }
1423
+ const quickResult = await this.selfReflection.quickVerify(changedFilesMap, async (prompt) => {
1424
+ const verifyResponse = await this.llmClient.chat([
1425
+ { role: "system", content: "You are a code verification assistant." },
1426
+ { role: "user", content: prompt },
1427
+ ]);
1428
+ this.tokenBudgetManager.recordUsage("validator", verifyResponse.usage.input, verifyResponse.usage.output);
1429
+ return verifyResponse.content ?? "";
1430
+ });
1431
+ if (quickResult.verdict !== "pass") {
1432
+ const issues = Object.entries(quickResult.dimensions)
1433
+ .filter(([, dim]) => dim.issues.length > 0)
1434
+ .flatMap(([, dim]) => dim.issues);
1435
+ if (issues.length > 0) {
1436
+ this.contextManager.addMessage({
1437
+ role: "system",
1438
+ content: `[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`,
1439
+ });
1440
+ this.emitEvent({
1441
+ kind: "agent:thinking",
1442
+ content: `Self-reflection flagged ${issues.length} issues (confidence: ${quickResult.confidence.toFixed(2)})`,
1443
+ });
1444
+ }
1445
+ }
1446
+ }
1447
+ catch {
1448
+ // Quick verification failure is non-fatal
1449
+ }
1450
+ }
893
1451
  // 에러가 많으면 FailureRecovery + 리플래닝 시도
894
1452
  const errorResults = toolResults.filter((r) => !r.success);
895
1453
  if (errorResults.length > 0) {
@@ -926,8 +1484,9 @@ export class AgentLoop extends EventEmitter {
926
1484
  content: `Rolled back changes. ${decision.reason}`,
927
1485
  });
928
1486
  }
929
- else {
1487
+ else if (this.iterationSystemMsgCount < 5) {
930
1488
  // retry, approach_change, scope_reduce → 복구 프롬프트 주입
1489
+ // Context Budget: consolidated error guidance (recovery + debug in one message)
931
1490
  const recoveryPrompt = this.failureRecovery.buildRecoveryPrompt(decision, {
932
1491
  error: errorSummary,
933
1492
  attemptNumber: iteration,
@@ -936,15 +1495,97 @@ export class AgentLoop extends EventEmitter {
936
1495
  originalSnapshots: this.originalSnapshots,
937
1496
  previousStrategies: this.previousStrategies,
938
1497
  });
1498
+ // SelfDebugLoop: merge debug strategy into recovery message (not separate system msg)
1499
+ let debugSuffix = "";
1500
+ if (iteration >= 3) {
1501
+ const rootCauseAnalysis = this.selfDebugLoop.analyzeError(errorSummary);
1502
+ if (rootCauseAnalysis.confidence >= 0.5) {
1503
+ const debugStrategy = this.selfDebugLoop.selectStrategy(iteration - 2, []);
1504
+ const debugPrompt = this.selfDebugLoop.buildFixPrompt(debugStrategy, {
1505
+ testCommand: "pnpm build",
1506
+ errorOutput: errorSummary,
1507
+ changedFiles: this.changedFiles,
1508
+ originalSnapshots: this.originalSnapshots,
1509
+ previousAttempts: [],
1510
+ currentStrategy: debugStrategy,
1511
+ });
1512
+ debugSuffix = `\n\n[SelfDebug L${Math.min(iteration - 2, 5)}] Strategy: ${debugStrategy}\n${debugPrompt}`;
1513
+ }
1514
+ }
939
1515
  this.contextManager.addMessage({
940
1516
  role: "system",
941
- content: recoveryPrompt,
1517
+ content: recoveryPrompt + debugSuffix,
942
1518
  });
1519
+ this.iterationSystemMsgCount++;
943
1520
  }
944
1521
  // HierarchicalPlanner 리플래닝도 시도
945
1522
  if (this.activePlan) {
946
1523
  await this.attemptReplan(errorSummary);
947
1524
  }
1525
+ // 에러 트리거 예산 비율 계산 (SkillLearner + Plugin 모두 사용)
1526
+ const errorTriggerBudgetRatio = this.config.loop.totalTokenBudget > 0
1527
+ ? this.tokenUsage.total / this.config.loop.totalTokenBudget
1528
+ : 0;
1529
+ // Context Budget: skip all optional injections at 85%+ budget
1530
+ if (errorTriggerBudgetRatio <= 0.85 && this.iterationSystemMsgCount < 5) {
1531
+ // SkillLearner: 학습된 스킬 중 현재 에러에 매칭되는 것 주입
1532
+ if (this.skillLearner && this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
1533
+ try {
1534
+ const relevantSkills = this.skillLearner.getRelevantSkills({
1535
+ errorMessage: errorSummary,
1536
+ language: undefined,
1537
+ });
1538
+ for (const skill of relevantSkills.slice(0, 1)) {
1539
+ if (this.activeSkillIds.includes(skill.id))
1540
+ continue;
1541
+ this.contextManager.addMessage({
1542
+ role: "system",
1543
+ content: `[Learned Skill: ${skill.id}] Diagnosis: ${skill.diagnosis}\nStrategy: ${skill.strategy}\nTools: ${skill.toolSequence.join(" → ")}`,
1544
+ });
1545
+ this.activeSkillIds.push(skill.id);
1546
+ this.iterationSystemMsgCount++;
1547
+ this.skillLearner.updateConfidence(skill.id, false);
1548
+ }
1549
+ }
1550
+ catch {
1551
+ // 학습 스킬 매칭 실패는 치명적이지 않음
1552
+ }
1553
+ }
1554
+ // Plugin trigger matching — match errors/context to plugin skills
1555
+ if (this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS &&
1556
+ this.iterationSystemMsgCount < 5) {
1557
+ const triggerMatches = this.pluginRegistry.matchTriggers({
1558
+ errorMessage: errorSummary,
1559
+ taskDescription: "",
1560
+ });
1561
+ if (triggerMatches.length > 0) {
1562
+ const bestMatch = triggerMatches[0];
1563
+ if (bestMatch.skill && !this.activeSkillIds.includes(bestMatch.skill.id)) {
1564
+ const skillTemplate = this.skillLoader.loadTemplate(bestMatch.skill);
1565
+ if (skillTemplate) {
1566
+ let resolved = this.skillLoader.resolveTemplate(skillTemplate.content, {
1567
+ error: errorSummary,
1568
+ files: this.changedFiles,
1569
+ });
1570
+ const MAX_SKILL_INJECT_CHARS = 2000;
1571
+ if (resolved.length > MAX_SKILL_INJECT_CHARS) {
1572
+ resolved = resolved.slice(0, MAX_SKILL_INJECT_CHARS) + "\n[...truncated for token budget]";
1573
+ }
1574
+ this.contextManager.addMessage({
1575
+ role: "system",
1576
+ content: `[Plugin Skill: ${bestMatch.pluginId}/${bestMatch.skill.name}]\n${resolved}`,
1577
+ });
1578
+ this.activeSkillIds.push(bestMatch.skill.id);
1579
+ this.iterationSystemMsgCount++;
1580
+ this.emitEvent({
1581
+ kind: "agent:thinking",
1582
+ content: `Activated plugin skill: ${bestMatch.skill.name} from ${bestMatch.pluginId}`,
1583
+ });
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+ }
948
1589
  }
949
1590
  // 체크포인트 저장: 토큰 예산 80% 이상 사용 시 자동 저장 (1회만)
950
1591
  if (!this.checkpointSaved &&
@@ -952,6 +1593,18 @@ export class AgentLoop extends EventEmitter {
952
1593
  await this.saveAutoCheckpoint(iteration);
953
1594
  this.checkpointSaved = true;
954
1595
  }
1596
+ // ContinuousReflection: 매 5 iteration마다 비상 체크포인트 트리거
1597
+ // (정기 타이머 외에 iteration 기반 추가 안전망)
1598
+ if (this.continuousReflection?.isRunning() &&
1599
+ iteration > 0 &&
1600
+ iteration % 5 === 0) {
1601
+ try {
1602
+ await this.continuousReflection.emergencyCheckpoint();
1603
+ }
1604
+ catch {
1605
+ // reflection 체크포인트 실패는 치명적이지 않음
1606
+ }
1607
+ }
955
1608
  // 예산 초과 체크
956
1609
  if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
957
1610
  return {
@@ -1050,6 +1703,7 @@ export class AgentLoop extends EventEmitter {
1050
1703
  */
1051
1704
  async executeTools(toolCalls) {
1052
1705
  const results = [];
1706
+ const deferredFixPrompts = [];
1053
1707
  for (const toolCall of toolCalls) {
1054
1708
  // Governor: 안전성 검증
1055
1709
  try {
@@ -1081,6 +1735,29 @@ export class AgentLoop extends EventEmitter {
1081
1735
  }
1082
1736
  // 승인됨 → 계속 실행
1083
1737
  }
1738
+ // Plugin Tool Approval Gate: check plugin tools requiring approval
1739
+ const pluginTools = this.pluginRegistry.getAllTools();
1740
+ const matchedPluginTool = pluginTools.find((pt) => pt.tool.name === toolCall.name);
1741
+ if (matchedPluginTool &&
1742
+ (matchedPluginTool.tool.requiresApproval === true ||
1743
+ matchedPluginTool.tool.sideEffectLevel === "destructive")) {
1744
+ const pluginApprovalReq = {
1745
+ id: `plugin-approval-${toolCall.id}`,
1746
+ toolName: toolCall.name,
1747
+ arguments: args,
1748
+ reason: `Plugin tool "${toolCall.name}" (from ${matchedPluginTool.pluginId}) requires approval (${matchedPluginTool.tool.sideEffectLevel === "destructive"
1749
+ ? "destructive side effect"
1750
+ : "requiresApproval=true"})`,
1751
+ riskLevel: matchedPluginTool.tool.riskLevel === "high" ? "high" : "medium",
1752
+ timeout: 120_000,
1753
+ };
1754
+ const pluginApprovalResult = await this.handleApprovalRequest(toolCall, pluginApprovalReq);
1755
+ if (pluginApprovalResult) {
1756
+ results.push(pluginApprovalResult);
1757
+ continue;
1758
+ }
1759
+ // Approved → proceed with execution
1760
+ }
1084
1761
  // MCP 도구 호출 확인
1085
1762
  if (this.mcpClient && this.isMCPTool(toolCall.name)) {
1086
1763
  const mcpResult = await this.executeMCPTool(toolCall);
@@ -1143,8 +1820,11 @@ export class AgentLoop extends EventEmitter {
1143
1820
  this.analyzeFileImpact(filePathStr).catch(() => { });
1144
1821
  }
1145
1822
  }
1146
- // AutoFixLoop: 결과 검증
1147
- await this.validateAndFeedback(toolCall.name, result);
1823
+ // AutoFixLoop: 결과 검증 (fix prompt는 tool results 추가 후 context에 넣음)
1824
+ const fixPrompt = await this.validateAndFeedback(toolCall.name, result);
1825
+ if (fixPrompt) {
1826
+ deferredFixPrompts.push(fixPrompt);
1827
+ }
1148
1828
  }
1149
1829
  catch (err) {
1150
1830
  this.interruptManager.clearToolAbort();
@@ -1163,8 +1843,18 @@ export class AgentLoop extends EventEmitter {
1163
1843
  message: `Tool ${toolCall.name} cancelled by interrupt`,
1164
1844
  retryable: false,
1165
1845
  });
1166
- // soft interrupt: 남은 tool calls 건너뛰고 루프 계속
1167
- // hard interrupt: aborted=true로 루프 종료됨
1846
+ // 남은 tool calls 대해 placeholder result 추가 (OpenAI 400 방지)
1847
+ const currentIdx = toolCalls.indexOf(toolCall);
1848
+ for (let i = currentIdx + 1; i < toolCalls.length; i++) {
1849
+ results.push({
1850
+ tool_call_id: toolCalls[i].id,
1851
+ name: toolCalls[i].name,
1852
+ output: `[SKIPPED] Previous tool was interrupted.`,
1853
+ success: false,
1854
+ durationMs: 0,
1855
+ });
1856
+ }
1857
+ // soft interrupt: 루프 계속 / hard interrupt: aborted=true로 종료
1168
1858
  break;
1169
1859
  }
1170
1860
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -1182,7 +1872,7 @@ export class AgentLoop extends EventEmitter {
1182
1872
  });
1183
1873
  }
1184
1874
  }
1185
- return results;
1875
+ return { results, deferredFixPrompts };
1186
1876
  }
1187
1877
  /**
1188
1878
  * Governor의 ApprovalRequiredError를 ApprovalManager로 처리.
@@ -1229,13 +1919,13 @@ export class AgentLoop extends EventEmitter {
1229
1919
  async validateAndFeedback(toolName, result) {
1230
1920
  // file_write/file_edit만 검증 (다른 도구는 스킵)
1231
1921
  if (!["file_write", "file_edit"].includes(toolName)) {
1232
- return;
1922
+ return null;
1233
1923
  }
1234
1924
  const validation = await this.autoFixLoop.validateResult(toolName, result.output, result.success, this.config.loop.projectPath);
1235
1925
  if (validation.passed) {
1236
1926
  // 검증 통과 → 수정 시도 기록 초기화
1237
1927
  this.autoFixLoop.resetAttempts();
1238
- return;
1928
+ return null;
1239
1929
  }
1240
1930
  // 검증 실패 → 에러 피드백
1241
1931
  if (!this.autoFixLoop.canRetry()) {
@@ -1245,20 +1935,17 @@ export class AgentLoop extends EventEmitter {
1245
1935
  message: `Auto-fix exhausted (${this.autoFixLoop.getAttempts().length} attempts): ${validation.failures[0]?.message ?? "Unknown error"}`,
1246
1936
  retryable: false,
1247
1937
  });
1248
- return;
1938
+ return null;
1249
1939
  }
1250
- // 수정 프롬프트 생성 대화 히스토리에 user 메시지로 추가
1940
+ // 수정 프롬프트 생성 히스토리 추가는 caller가 tool result 추가 후 수행
1251
1941
  const errorMsg = validation.failures
1252
1942
  .map((f) => `[${f.type}] ${f.message}\n${f.rawOutput}`)
1253
1943
  .join("\n\n");
1254
1944
  const fixPrompt = this.autoFixLoop.buildFixPrompt(errorMsg, `After ${toolName} execution on project at ${this.config.loop.projectPath}`);
1255
1945
  // 수정 시도 기록
1256
1946
  this.autoFixLoop.recordAttempt(errorMsg, "Requesting LLM fix", false, 0);
1257
- // 피드백을 히스토리에 추가 (다음 LLM 호출에서 수정 시도)
1258
- this.contextManager.addMessage({
1259
- role: "user",
1260
- content: fixPrompt,
1261
- });
1947
+ // fixPrompt를 반환 caller가 tool result 메시지 추가 후 context에 넣음
1948
+ return fixPrompt;
1262
1949
  }
1263
1950
  /**
1264
1951
  * 도구 인자를 파싱하는 헬퍼.