@yuaone/core 0.4.2 → 0.4.4

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 (186) hide show
  1. package/dist/agent-logger.d.ts +1 -1
  2. package/dist/agent-logger.d.ts.map +1 -1
  3. package/dist/agent-logger.js +17 -15
  4. package/dist/agent-logger.js.map +1 -1
  5. package/dist/agent-loop.d.ts +31 -0
  6. package/dist/agent-loop.d.ts.map +1 -1
  7. package/dist/agent-loop.js +514 -98
  8. package/dist/agent-loop.js.map +1 -1
  9. package/dist/agent-modes.d.ts.map +1 -1
  10. package/dist/agent-modes.js +5 -0
  11. package/dist/agent-modes.js.map +1 -1
  12. package/dist/async-completion-queue.d.ts +2 -0
  13. package/dist/async-completion-queue.d.ts.map +1 -1
  14. package/dist/async-completion-queue.js +14 -0
  15. package/dist/async-completion-queue.js.map +1 -1
  16. package/dist/auto-fix.d.ts.map +1 -1
  17. package/dist/auto-fix.js +12 -1
  18. package/dist/auto-fix.js.map +1 -1
  19. package/dist/benchmark-runner.d.ts.map +1 -1
  20. package/dist/benchmark-runner.js +5 -1
  21. package/dist/benchmark-runner.js.map +1 -1
  22. package/dist/constants.d.ts +12 -0
  23. package/dist/constants.d.ts.map +1 -1
  24. package/dist/constants.js +14 -0
  25. package/dist/constants.js.map +1 -1
  26. package/dist/context-manager.d.ts +25 -0
  27. package/dist/context-manager.d.ts.map +1 -1
  28. package/dist/context-manager.js +132 -5
  29. package/dist/context-manager.js.map +1 -1
  30. package/dist/continuation-engine.d.ts.map +1 -1
  31. package/dist/continuation-engine.js +8 -7
  32. package/dist/continuation-engine.js.map +1 -1
  33. package/dist/continuous-reflection.d.ts.map +1 -1
  34. package/dist/continuous-reflection.js +22 -12
  35. package/dist/continuous-reflection.js.map +1 -1
  36. package/dist/cost-optimizer.js +1 -1
  37. package/dist/cost-optimizer.js.map +1 -1
  38. package/dist/cross-file-refactor.d.ts.map +1 -1
  39. package/dist/cross-file-refactor.js +7 -2
  40. package/dist/cross-file-refactor.js.map +1 -1
  41. package/dist/dag-orchestrator.d.ts +10 -1
  42. package/dist/dag-orchestrator.d.ts.map +1 -1
  43. package/dist/dag-orchestrator.js +101 -6
  44. package/dist/dag-orchestrator.js.map +1 -1
  45. package/dist/debate-orchestrator.d.ts +1 -0
  46. package/dist/debate-orchestrator.d.ts.map +1 -1
  47. package/dist/debate-orchestrator.js +27 -15
  48. package/dist/debate-orchestrator.js.map +1 -1
  49. package/dist/dependency-analyzer.d.ts.map +1 -1
  50. package/dist/dependency-analyzer.js +19 -1
  51. package/dist/dependency-analyzer.js.map +1 -1
  52. package/dist/dynamic-role-generator.d.ts.map +1 -1
  53. package/dist/dynamic-role-generator.js +6 -3
  54. package/dist/dynamic-role-generator.js.map +1 -1
  55. package/dist/errors.js +1 -1
  56. package/dist/errors.js.map +1 -1
  57. package/dist/event-bus.d.ts.map +1 -1
  58. package/dist/event-bus.js +4 -3
  59. package/dist/event-bus.js.map +1 -1
  60. package/dist/execution-engine.d.ts +39 -1
  61. package/dist/execution-engine.d.ts.map +1 -1
  62. package/dist/execution-engine.js +453 -83
  63. package/dist/execution-engine.js.map +1 -1
  64. package/dist/failure-recovery.d.ts.map +1 -1
  65. package/dist/failure-recovery.js +14 -3
  66. package/dist/failure-recovery.js.map +1 -1
  67. package/dist/git-intelligence.d.ts.map +1 -1
  68. package/dist/git-intelligence.js +16 -11
  69. package/dist/git-intelligence.js.map +1 -1
  70. package/dist/governor.d.ts +8 -0
  71. package/dist/governor.d.ts.map +1 -1
  72. package/dist/governor.js +19 -1
  73. package/dist/governor.js.map +1 -1
  74. package/dist/hierarchical-planner.d.ts +3 -0
  75. package/dist/hierarchical-planner.d.ts.map +1 -1
  76. package/dist/hierarchical-planner.js +32 -2
  77. package/dist/hierarchical-planner.js.map +1 -1
  78. package/dist/impact-analyzer.d.ts +27 -0
  79. package/dist/impact-analyzer.d.ts.map +1 -1
  80. package/dist/impact-analyzer.js +415 -53
  81. package/dist/impact-analyzer.js.map +1 -1
  82. package/dist/intent-inference.d.ts.map +1 -1
  83. package/dist/intent-inference.js +20 -24
  84. package/dist/intent-inference.js.map +1 -1
  85. package/dist/kernel.d.ts.map +1 -1
  86. package/dist/kernel.js +5 -3
  87. package/dist/kernel.js.map +1 -1
  88. package/dist/language-detector.d.ts +19 -0
  89. package/dist/language-detector.d.ts.map +1 -0
  90. package/dist/language-detector.js +482 -0
  91. package/dist/language-detector.js.map +1 -0
  92. package/dist/language-support.d.ts.map +1 -1
  93. package/dist/language-support.js +5 -9
  94. package/dist/language-support.js.map +1 -1
  95. package/dist/llm-client.d.ts +21 -8
  96. package/dist/llm-client.d.ts.map +1 -1
  97. package/dist/llm-client.js +125 -21
  98. package/dist/llm-client.js.map +1 -1
  99. package/dist/mcp-client.d.ts.map +1 -1
  100. package/dist/mcp-client.js +9 -1
  101. package/dist/mcp-client.js.map +1 -1
  102. package/dist/memory-manager.d.ts +13 -8
  103. package/dist/memory-manager.d.ts.map +1 -1
  104. package/dist/memory-manager.js +125 -32
  105. package/dist/memory-manager.js.map +1 -1
  106. package/dist/memory-updater.d.ts.map +1 -1
  107. package/dist/memory-updater.js +5 -4
  108. package/dist/memory-updater.js.map +1 -1
  109. package/dist/memory.d.ts +6 -2
  110. package/dist/memory.d.ts.map +1 -1
  111. package/dist/memory.js +32 -4
  112. package/dist/memory.js.map +1 -1
  113. package/dist/parallel-executor.d.ts +7 -0
  114. package/dist/parallel-executor.d.ts.map +1 -1
  115. package/dist/parallel-executor.js +28 -0
  116. package/dist/parallel-executor.js.map +1 -1
  117. package/dist/perf-optimizer.d.ts.map +1 -1
  118. package/dist/perf-optimizer.js +18 -3
  119. package/dist/perf-optimizer.js.map +1 -1
  120. package/dist/persona.d.ts.map +1 -1
  121. package/dist/persona.js +8 -3
  122. package/dist/persona.js.map +1 -1
  123. package/dist/planner.d.ts.map +1 -1
  124. package/dist/planner.js +5 -3
  125. package/dist/planner.js.map +1 -1
  126. package/dist/plugin-auto-loader.d.ts.map +1 -1
  127. package/dist/plugin-auto-loader.js +4 -1
  128. package/dist/plugin-auto-loader.js.map +1 -1
  129. package/dist/plugin-registry.d.ts +4 -0
  130. package/dist/plugin-registry.d.ts.map +1 -1
  131. package/dist/plugin-registry.js +6 -0
  132. package/dist/plugin-registry.js.map +1 -1
  133. package/dist/plugin-validator.d.ts.map +1 -1
  134. package/dist/plugin-validator.js +10 -1
  135. package/dist/plugin-validator.js.map +1 -1
  136. package/dist/reasoning-aggregator.d.ts +35 -0
  137. package/dist/reasoning-aggregator.d.ts.map +1 -0
  138. package/dist/reasoning-aggregator.js +102 -0
  139. package/dist/reasoning-aggregator.js.map +1 -0
  140. package/dist/reasoning-tree.d.ts +23 -0
  141. package/dist/reasoning-tree.d.ts.map +1 -0
  142. package/dist/reasoning-tree.js +44 -0
  143. package/dist/reasoning-tree.js.map +1 -0
  144. package/dist/session-persistence.d.ts +8 -4
  145. package/dist/session-persistence.d.ts.map +1 -1
  146. package/dist/session-persistence.js +22 -7
  147. package/dist/session-persistence.js.map +1 -1
  148. package/dist/skill-learner.d.ts.map +1 -1
  149. package/dist/skill-learner.js +4 -2
  150. package/dist/skill-learner.js.map +1 -1
  151. package/dist/skill-loader.d.ts +4 -0
  152. package/dist/skill-loader.d.ts.map +1 -1
  153. package/dist/skill-loader.js +6 -0
  154. package/dist/skill-loader.js.map +1 -1
  155. package/dist/speculative-executor.d.ts +22 -0
  156. package/dist/speculative-executor.d.ts.map +1 -1
  157. package/dist/speculative-executor.js +90 -45
  158. package/dist/speculative-executor.js.map +1 -1
  159. package/dist/state-machine.d.ts.map +1 -1
  160. package/dist/state-machine.js +4 -2
  161. package/dist/state-machine.js.map +1 -1
  162. package/dist/sub-agent-prompts.d.ts +5 -29
  163. package/dist/sub-agent-prompts.d.ts.map +1 -1
  164. package/dist/sub-agent-prompts.js +231 -134
  165. package/dist/sub-agent-prompts.js.map +1 -1
  166. package/dist/sub-agent.d.ts +19 -0
  167. package/dist/sub-agent.d.ts.map +1 -1
  168. package/dist/sub-agent.js +135 -11
  169. package/dist/sub-agent.js.map +1 -1
  170. package/dist/system-prompt.d.ts.map +1 -1
  171. package/dist/system-prompt.js +45 -0
  172. package/dist/system-prompt.js.map +1 -1
  173. package/dist/task-classifier.js +1 -1
  174. package/dist/task-classifier.js.map +1 -1
  175. package/dist/types.d.ts +67 -1
  176. package/dist/types.d.ts.map +1 -1
  177. package/dist/types.js.map +1 -1
  178. package/dist/vector-index.d.ts +14 -0
  179. package/dist/vector-index.d.ts.map +1 -1
  180. package/dist/vector-index.js +84 -16
  181. package/dist/vector-index.js.map +1 -1
  182. package/dist/workspace-lock.d.ts +5 -0
  183. package/dist/workspace-lock.d.ts.map +1 -0
  184. package/dist/workspace-lock.js +16 -0
  185. package/dist/workspace-lock.js.map +1 -0
  186. package/package.json +2 -1
@@ -7,9 +7,11 @@
7
7
  * ContextManager가 컨텍스트 윈도우를 관리한다.
8
8
  */
9
9
  import { EventEmitter } from "node:events";
10
+ import { randomUUID } from "node:crypto";
10
11
  import { BYOKClient } from "./llm-client.js";
11
12
  import { Governor } from "./governor.js";
12
13
  import { ContextManager } from "./context-manager.js";
14
+ import { SessionPersistence } from "./session-persistence.js";
13
15
  import { LLMError, PlanLimitError, ApprovalRequiredError, } from "./errors.js";
14
16
  import { ApprovalManager, } from "./approval.js";
15
17
  import { AutoFixLoop, } from "./auto-fix.js";
@@ -41,6 +43,8 @@ import { SelfDebugLoop } from "./self-debug-loop.js";
41
43
  import { SkillLearner } from "./skill-learner.js";
42
44
  import { RepoKnowledgeGraph } from "./repo-knowledge-graph.js";
43
45
  import { BackgroundAgentManager } from "./background-agent.js";
46
+ import { ReasoningAggregator } from "./reasoning-aggregator.js";
47
+ import { ReasoningTree } from "./reasoning-tree.js";
44
48
  /**
45
49
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
46
50
  *
@@ -69,6 +73,7 @@ import { BackgroundAgentManager } from "./background-agent.js";
69
73
  * ```
70
74
  */
71
75
  export class AgentLoop extends EventEmitter {
76
+ abortSignal;
72
77
  llmClient;
73
78
  governor;
74
79
  contextManager;
@@ -104,6 +109,7 @@ export class AgentLoop extends EventEmitter {
104
109
  worldState = null;
105
110
  costOptimizer;
106
111
  impactAnalyzer = null;
112
+ impactHintInjected = false;
107
113
  selfReflection = null;
108
114
  debateOrchestrator = null;
109
115
  continuousReflection = null;
@@ -123,6 +129,8 @@ export class AgentLoop extends EventEmitter {
123
129
  skillLearner = null;
124
130
  repoGraph = null;
125
131
  backgroundAgentManager = null;
132
+ sessionPersistence = null;
133
+ sessionId = null;
126
134
  enableToolPlanning;
127
135
  enableSkillLearning;
128
136
  enableBackgroundAgents;
@@ -139,10 +147,70 @@ export class AgentLoop extends EventEmitter {
139
147
  reasoning: 0,
140
148
  total: 0,
141
149
  };
150
+ reasoningAggregator = new ReasoningAggregator();
151
+ reasoningTree = new ReasoningTree();
152
+ resumedFromSession = false;
153
+ /**
154
+ * Restore AgentLoop state from persisted session (yuan resume)
155
+ */
156
+ async restoreSession(data) {
157
+ if (!data)
158
+ return;
159
+ try {
160
+ this.governor.resetIteration?.();
161
+ if (data.snapshot?.id) {
162
+ this.sessionId = data.snapshot.id;
163
+ }
164
+ if (data.snapshot?.iteration) {
165
+ this.iterationCount = data.snapshot.iteration;
166
+ this.governor.restoreIteration?.(data.snapshot.iteration);
167
+ }
168
+ if (data.snapshot?.tokenUsage) {
169
+ const input = data.snapshot.tokenUsage.input ?? 0;
170
+ const output = data.snapshot.tokenUsage.output ?? 0;
171
+ this.tokenUsage = {
172
+ input,
173
+ output,
174
+ reasoning: 0,
175
+ total: input + output,
176
+ };
177
+ }
178
+ if (Array.isArray(data.changedFiles)) {
179
+ this.changedFiles = data.changedFiles;
180
+ }
181
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
182
+ this.contextManager.clear();
183
+ for (const msg of data.messages) {
184
+ this.contextManager.addMessage(msg);
185
+ }
186
+ if (!data.messages.some((msg) => msg.role === "system")) {
187
+ this.contextManager.addMessage({
188
+ role: "system",
189
+ content: this.config.loop.systemPrompt,
190
+ });
191
+ }
192
+ }
193
+ this.activePlan = data.plan ?? null;
194
+ this.resumedFromSession = true;
195
+ this.emitEvent({
196
+ kind: "agent:thinking",
197
+ content: "Session restored.",
198
+ });
199
+ }
200
+ catch (err) {
201
+ this.emitEvent({
202
+ kind: "agent:error",
203
+ message: err instanceof Error ? err.message : String(err),
204
+ retryable: false,
205
+ });
206
+ }
207
+ }
142
208
  constructor(options) {
143
209
  super();
210
+ this.setMaxListeners(100);
144
211
  this.config = options.config;
145
212
  this.toolExecutor = options.toolExecutor;
213
+ this.abortSignal = options.abortSignal;
146
214
  this.enableMemory = options.enableMemory !== false;
147
215
  this.enablePlanning = options.enablePlanning !== false;
148
216
  this.planningThreshold = options.planningThreshold ?? "moderate";
@@ -210,6 +278,13 @@ export class AgentLoop extends EventEmitter {
210
278
  const projectPath = this.config.loop.projectPath;
211
279
  if (!projectPath)
212
280
  return;
281
+ // Session persistence init
282
+ try {
283
+ this.sessionPersistence = new SessionPersistence(undefined, projectPath);
284
+ }
285
+ catch {
286
+ this.sessionPersistence = null;
287
+ }
213
288
  let yuanMdContent;
214
289
  let projectStructure;
215
290
  // Memory 로드
@@ -299,10 +374,6 @@ export class AgentLoop extends EventEmitter {
299
374
  this.mcpToolDefinitions = [];
300
375
  }
301
376
  }
302
- // HierarchicalPlanner 생성
303
- if (this.enablePlanning && projectPath) {
304
- this.planner = new HierarchicalPlanner({ projectPath });
305
- }
306
377
  // ReflexionEngine 생성
307
378
  if (projectPath) {
308
379
  this.reflexionEngine = new ReflexionEngine({ projectPath });
@@ -375,25 +446,30 @@ export class AgentLoop extends EventEmitter {
375
446
  try {
376
447
  this.skillLearner = new SkillLearner(projectPath);
377
448
  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
449
  }
392
450
  catch {
393
- // SkillLearner 초기화 실패는 치명적이지 않음
394
451
  this.skillLearner = null;
395
452
  }
396
453
  }
454
+ // HierarchicalPlanner 생성
455
+ this.planner = new HierarchicalPlanner({ projectPath });
456
+ if (this.skillLearner) {
457
+ this.planner.setSkillLearner(this.skillLearner);
458
+ }
459
+ if (this.skillLearner) {
460
+ const learnedSkills = this.skillLearner.getAllSkills();
461
+ if (learnedSkills.length > 0) {
462
+ const skillNames = learnedSkills
463
+ .filter((s) => s.confidence >= 0.3)
464
+ .map((s) => s.id);
465
+ if (skillNames.length > 0) {
466
+ this.contextManager.addMessage({
467
+ role: "system",
468
+ content: `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`,
469
+ });
470
+ }
471
+ }
472
+ }
397
473
  // RepoKnowledgeGraph 초기화 (코드 구조 그래프 — 비동기 빌드)
398
474
  if (projectPath) {
399
475
  try {
@@ -450,7 +526,7 @@ export class AgentLoop extends EventEmitter {
450
526
  if (!this.continuationEngine)
451
527
  return;
452
528
  const checkpoint = {
453
- sessionId: crypto.randomUUID(),
529
+ sessionId: this.sessionId ?? "unknown-session",
454
530
  goal: state.goal,
455
531
  progress: {
456
532
  completedTasks: state.completedTasks,
@@ -509,12 +585,7 @@ export class AgentLoop extends EventEmitter {
509
585
  });
510
586
  });
511
587
  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();
588
+ void this.handleSoftContextOverflow();
518
589
  });
519
590
  }
520
591
  /**
@@ -554,20 +625,58 @@ export class AgentLoop extends EventEmitter {
554
625
  */
555
626
  async run(userMessage) {
556
627
  this.aborted = false;
557
- this.changedFiles = [];
558
- this.allToolResults = [];
628
+ this.reasoningAggregator.reset();
629
+ this.reasoningTree.reset();
630
+ if (!this.resumedFromSession) {
631
+ this.changedFiles = [];
632
+ this.allToolResults = [];
633
+ this.iterationCount = 0;
634
+ this.originalSnapshots.clear();
635
+ this.previousStrategies = [];
636
+ this.activeSkillIds = [];
637
+ this.iterationSystemMsgCount = 0;
638
+ this.tokenUsage = {
639
+ input: 0,
640
+ output: 0,
641
+ reasoning: 0,
642
+ total: 0,
643
+ };
644
+ this.impactHintInjected = false;
645
+ }
559
646
  this.checkpointSaved = false;
560
- this.iterationCount = 0;
561
- this.originalSnapshots.clear();
562
- this.previousStrategies = [];
563
- this.activeSkillIds = [];
564
- this.iterationSystemMsgCount = 0;
565
647
  this.failureRecovery.reset();
566
648
  this.costOptimizer.reset();
567
649
  this.tokenBudgetManager.reset();
568
650
  const runStartTime = Date.now();
569
651
  // 첫 실행 시 메모리/프로젝트 컨텍스트 자동 로드
570
652
  await this.init();
653
+ if (this.sessionPersistence) {
654
+ if (!this.sessionId) {
655
+ this.sessionId = randomUUID();
656
+ }
657
+ const nowIso = new Date().toISOString();
658
+ const snapshot = {
659
+ id: this.sessionId,
660
+ createdAt: nowIso,
661
+ updatedAt: nowIso,
662
+ workDir: this.config.loop.projectPath ?? "",
663
+ provider: String(this.config.byok.provider ?? "unknown"),
664
+ model: String(this.config.byok.model ?? "unknown"),
665
+ status: "running",
666
+ iteration: this.iterationCount,
667
+ tokenUsage: {
668
+ input: this.tokenUsage.input,
669
+ output: this.tokenUsage.output,
670
+ },
671
+ messageCount: this.contextManager.getMessages().length,
672
+ };
673
+ await this.sessionPersistence.save(this.sessionId, {
674
+ snapshot,
675
+ messages: this.contextManager.getMessages(),
676
+ plan: this.activePlan,
677
+ changedFiles: this.changedFiles,
678
+ });
679
+ }
571
680
  // 사용자 입력 검증 (prompt injection 방어)
572
681
  const inputValidation = this.promptDefense.validateUserInput(userMessage);
573
682
  if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
@@ -653,11 +762,18 @@ export class AgentLoop extends EventEmitter {
653
762
  this.continuousReflection.stop();
654
763
  }
655
764
  }
765
+ if (this.sessionPersistence && this.sessionId) {
766
+ const finalStatus = result.reason === "ERROR"
767
+ ? "crashed"
768
+ : "completed";
769
+ await this.sessionPersistence.updateStatus(this.sessionId, finalStatus);
770
+ }
656
771
  // 실행 완료 후 메모리 자동 업데이트
657
772
  await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
658
773
  // SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
659
774
  if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
660
775
  try {
776
+ let newSkillId = null;
661
777
  const errorToolResults = this.allToolResults.filter((r) => !r.success);
662
778
  if (errorToolResults.length > 0 && this.changedFiles.length > 0) {
663
779
  // 에러가 있었지만 결국 성공 → 학습 가능한 패턴
@@ -675,8 +791,17 @@ export class AgentLoop extends EventEmitter {
675
791
  durationMs: Date.now() - runStartTime,
676
792
  iterations: this.iterationCount,
677
793
  });
678
- this.skillLearner.extractSkillFromRun(runAnalysis, `session-${Date.now()}`);
794
+ const learned = this.skillLearner.extractSkillFromRun(runAnalysis, this.sessionId ?? `session-${Date.now()}`);
795
+ if (learned) {
796
+ newSkillId = learned.id;
797
+ }
679
798
  await this.skillLearner.save();
799
+ if (newSkillId) {
800
+ this.emitEvent({
801
+ kind: "agent:thinking",
802
+ content: `Learned new skill: ${newSkillId}`,
803
+ });
804
+ }
680
805
  }
681
806
  }
682
807
  catch {
@@ -758,7 +883,7 @@ export class AgentLoop extends EventEmitter {
758
883
  try {
759
884
  const entry = this.reflexionEngine.reflect({
760
885
  goal: userGoal,
761
- runId: crypto.randomUUID(),
886
+ runId: randomUUID(),
762
887
  termination: result,
763
888
  toolResults: this.allToolResults,
764
889
  messages: this.contextManager.getMessages(),
@@ -863,7 +988,7 @@ export class AgentLoop extends EventEmitter {
863
988
  contextUsagePercent: this.config.loop.totalTokenBudget > 0
864
989
  ? this.tokenUsage.total / this.config.loop.totalTokenBudget
865
990
  : 0,
866
- sessionId: crypto.randomUUID(),
991
+ sessionId: this.sessionId ?? "unknown-session",
867
992
  totalTokensUsed: this.tokenUsage.total,
868
993
  workingMemory: this.buildWorkingMemorySummary(),
869
994
  completedTasks: progress.completedTasks,
@@ -927,32 +1052,27 @@ export class AgentLoop extends EventEmitter {
927
1052
  if ((complexityOrder[complexity] ?? 0) < thresholdOrder[this.planningThreshold]) {
928
1053
  return;
929
1054
  }
930
- this.emitEvent({
931
- kind: "agent:thinking",
932
- content: `Task complexity: ${complexity}. Creating execution plan...`,
933
- });
1055
+ this.emitSubagent("planner", "start", `task complexity ${complexity}. creating execution plan`);
934
1056
  try {
935
1057
  const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
936
1058
  this.activePlan = plan;
937
1059
  this.currentTaskIndex = 0;
938
- // Estimate planner token usage (plan creation typically uses ~500 tokens per task)
939
1060
  const planTokenEstimate = plan.tactical.length * 500;
940
1061
  this.tokenBudgetManager.recordUsage("planner", planTokenEstimate, planTokenEstimate);
941
- // 계획을 컨텍스트에 주입 (LLM이 따라갈 수 있도록)
942
1062
  const planContext = this.formatPlanForContext(plan);
943
1063
  this.contextManager.addMessage({
944
1064
  role: "system",
945
1065
  content: planContext,
946
1066
  });
1067
+ this.emitSubagent("planner", "done", `plan created: ${plan.tactical.length} tasks, ${plan.totalEstimatedIterations} estimated iterations, risk ${plan.strategic.riskAssessment.level}`);
1068
+ }
1069
+ catch (err) {
947
1070
  this.emitEvent({
948
- kind: "agent:thinking",
949
- content: `Plan created: ${plan.tactical.length} tasks, ${plan.totalEstimatedIterations} estimated iterations. Risk: ${plan.strategic.riskAssessment.level}.`,
1071
+ kind: "agent:error",
1072
+ message: `Planning failed: ${err instanceof Error ? err.message : String(err)}`,
1073
+ retryable: false,
950
1074
  });
951
1075
  }
952
- catch {
953
- // 플래닝 실패는 치명적이지 않음 — LLM이 직접 처리하도록 폴백
954
- this.activePlan = null;
955
- }
956
1076
  }
957
1077
  /**
958
1078
  * 사용자 메시지에서 태스크 복잡도를 휴리스틱으로 추정.
@@ -1121,8 +1241,17 @@ export class AgentLoop extends EventEmitter {
1121
1241
  }
1122
1242
  // ─── Core Loop ───
1123
1243
  async executeLoop() {
1124
- let iteration = 0;
1244
+ let iteration = this.iterationCount;
1125
1245
  while (!this.aborted) {
1246
+ // ensure loop state always matches persisted iteration
1247
+ if (iteration !== this.iterationCount) {
1248
+ iteration = this.iterationCount;
1249
+ }
1250
+ if (this.abortSignal?.aborted) {
1251
+ return { reason: "USER_CANCELLED" };
1252
+ }
1253
+ if (iteration === 0)
1254
+ this.emitReasoning("starting agent loop");
1126
1255
  // Interrupt: pause 상태이면 resume될 때까지 대기
1127
1256
  if (this.interruptManager.isPaused()) {
1128
1257
  await this.waitForResume();
@@ -1146,7 +1275,22 @@ export class AgentLoop extends EventEmitter {
1146
1275
  iteration++;
1147
1276
  this.iterationCount = iteration;
1148
1277
  const iterationStart = Date.now();
1278
+ this.emitReasoning(`iteration ${iteration}: preparing context`);
1149
1279
  this.iterationSystemMsgCount = 0; // Reset per-iteration system message counter
1280
+ // Soft context rollover:
1281
+ // checkpoint first, then let ContextManager compact instead of aborting/throwing.
1282
+ const contextUsageRatio = this.contextManager.getUsageRatio();
1283
+ if (contextUsageRatio >= 0.85) {
1284
+ if (!this.checkpointSaved) {
1285
+ await this.saveAutoCheckpoint(iteration);
1286
+ this.checkpointSaved = true;
1287
+ }
1288
+ this.emitEvent({
1289
+ kind: "agent:thinking",
1290
+ content: `High context pressure detected (${Math.round(contextUsageRatio * 100)}%). ` +
1291
+ `Compressing conversation state and continuing.`,
1292
+ });
1293
+ }
1150
1294
  // Check file-pattern skill triggers based on changed files
1151
1295
  // Guard: skip skill injection if over 80% token budget to preserve remaining budget
1152
1296
  // Guard: max 3 active skills globally (Context Budget Rules)
@@ -1189,10 +1333,7 @@ export class AgentLoop extends EventEmitter {
1189
1333
  // Try rebalancing to free up budget from idle roles
1190
1334
  this.tokenBudgetManager.rebalance();
1191
1335
  }
1192
- this.emitEvent({
1193
- kind: "agent:thinking",
1194
- content: `Iteration ${iteration}...`,
1195
- });
1336
+ this.emitReasoning(`iteration ${iteration}: calling model`);
1196
1337
  let response;
1197
1338
  try {
1198
1339
  response = await this.callLLMStreaming(messages);
@@ -1237,6 +1378,11 @@ export class AgentLoop extends EventEmitter {
1237
1378
  // 3. 응답 처리
1238
1379
  if (response.toolCalls.length === 0) {
1239
1380
  const content = response.content ?? "";
1381
+ let finalSummary = content || "Task completed.";
1382
+ const finalImpactSummary = await this.buildFinalImpactSummary();
1383
+ if (finalImpactSummary) {
1384
+ finalSummary = `${finalSummary}\n\n${finalImpactSummary}`;
1385
+ }
1240
1386
  // Level 2: Deep verification before declaring completion
1241
1387
  if (this.selfReflection && this.changedFiles.length > 0) {
1242
1388
  try {
@@ -1272,10 +1418,7 @@ export class AgentLoop extends EventEmitter {
1272
1418
  role: "system",
1273
1419
  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
1420
  });
1275
- this.emitEvent({
1276
- kind: "agent:thinking",
1277
- content: `Deep verification failed (score: ${deepResult.overallScore}). Continuing to address issues...`,
1278
- });
1421
+ this.emitSubagent("verifier", "done", `deep verification failed, score ${deepResult.overallScore}. continuing to address issues`);
1279
1422
  continue; // Don't return GOAL_ACHIEVED, continue the loop
1280
1423
  }
1281
1424
  // Level 3: Multi-agent debate for complex/massive tasks
@@ -1283,10 +1426,7 @@ export class AgentLoop extends EventEmitter {
1283
1426
  ["complex", "massive"].includes(this.currentComplexity) &&
1284
1427
  deepResult.verdict !== "pass") {
1285
1428
  try {
1286
- this.emitEvent({
1287
- kind: "agent:thinking",
1288
- content: `Triggering multi-agent debate for ${this.currentComplexity} task verification...`,
1289
- });
1429
+ this.emitSubagent("reviewer", "start", `starting debate for ${this.currentComplexity} task verification`);
1290
1430
  const debateContext = [
1291
1431
  `Changed files: ${this.changedFiles.join(", ")}`,
1292
1432
  `Self-reflection score: ${deepResult.overallScore}/100`,
@@ -1314,10 +1454,7 @@ export class AgentLoop extends EventEmitter {
1314
1454
  });
1315
1455
  continue; // Continue loop to address debate feedback
1316
1456
  }
1317
- this.emitEvent({
1318
- kind: "agent:thinking",
1319
- content: `Debate passed (score: ${debateResult.finalScore}). Proceeding to completion.`,
1320
- });
1457
+ this.emitSubagent("reviewer", "done", `debate failed, score ${debateResult.finalScore}. continuing to address issues`);
1321
1458
  }
1322
1459
  catch {
1323
1460
  // Debate failure is non-fatal — proceed with completion
@@ -1340,17 +1477,21 @@ export class AgentLoop extends EventEmitter {
1340
1477
  if (content) {
1341
1478
  this.contextManager.addMessage({
1342
1479
  role: "assistant",
1343
- content,
1480
+ content: finalSummary,
1344
1481
  });
1345
1482
  }
1346
1483
  this.emitEvent({
1347
1484
  kind: "agent:completed",
1348
- summary: content || "Task completed.",
1349
- filesChanged: [],
1485
+ summary: finalSummary,
1486
+ filesChanged: this.changedFiles
1487
+ });
1488
+ this.emitEvent({
1489
+ kind: "agent:reasoning_tree",
1490
+ tree: this.reasoningTree.toJSON(),
1350
1491
  });
1351
1492
  return {
1352
1493
  reason: "GOAL_ACHIEVED",
1353
- summary: content || "Task completed.",
1494
+ summary: finalSummary,
1354
1495
  };
1355
1496
  }
1356
1497
  // 어시스턴트 메시지 저장 (tool_calls 포함)
@@ -1359,10 +1500,12 @@ export class AgentLoop extends EventEmitter {
1359
1500
  content: response.content,
1360
1501
  tool_calls: response.toolCalls,
1361
1502
  });
1362
- if (response.content) {
1503
+ if (response.toolCalls.length > 1 && iteration === this.iterationCount) {
1504
+ const batchId = `batch_${Date.now()}`;
1363
1505
  this.emitEvent({
1364
- kind: "agent:thinking",
1365
- content: response.content,
1506
+ kind: "agent:tool_batch",
1507
+ batchId,
1508
+ size: response.toolCalls.length,
1366
1509
  });
1367
1510
  }
1368
1511
  // 4. 도구 실행
@@ -1407,9 +1550,23 @@ export class AgentLoop extends EventEmitter {
1407
1550
  tokensUsed: response.usage.input + response.usage.output,
1408
1551
  durationMs: Date.now() - iterationStart,
1409
1552
  });
1553
+ // session checkpoint
1554
+ if (this.sessionPersistence && this.sessionId) {
1555
+ const checkpoint = {
1556
+ iteration,
1557
+ tokenUsage: {
1558
+ input: this.tokenUsage.input,
1559
+ output: this.tokenUsage.output,
1560
+ },
1561
+ timestamp: new Date().toISOString(),
1562
+ changedFiles: this.changedFiles,
1563
+ };
1564
+ await this.sessionPersistence.checkpoint(this.sessionId, checkpoint);
1565
+ }
1410
1566
  // Level 1: Quick verification after every 3rd iteration
1411
1567
  if (this.selfReflection && iteration % 3 === 0) {
1412
1568
  try {
1569
+ this.emitSubagent("verifier", "start", "running quick verification");
1413
1570
  const changedFilesMap = new Map();
1414
1571
  for (const filePath of this.changedFiles) {
1415
1572
  // Collect changed file contents from tool results
@@ -1437,10 +1594,10 @@ export class AgentLoop extends EventEmitter {
1437
1594
  role: "system",
1438
1595
  content: `[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`,
1439
1596
  });
1440
- this.emitEvent({
1441
- kind: "agent:thinking",
1442
- content: `Self-reflection flagged ${issues.length} issues (confidence: ${quickResult.confidence.toFixed(2)})`,
1443
- });
1597
+ this.emitSubagent("verifier", "done", `quick verification flagged ${issues.length} issues, confidence ${quickResult.confidence.toFixed(2)}`);
1598
+ }
1599
+ else {
1600
+ this.emitSubagent("verifier", "done", `quick verification passed, confidence ${quickResult.confidence.toFixed(2)}`);
1444
1601
  }
1445
1602
  }
1446
1603
  }
@@ -1480,8 +1637,9 @@ export class AgentLoop extends EventEmitter {
1480
1637
  // 롤백 시도
1481
1638
  await this.failureRecovery.executeRollback(this.changedFiles, this.originalSnapshots);
1482
1639
  this.emitEvent({
1483
- kind: "agent:thinking",
1484
- content: `Rolled back changes. ${decision.reason}`,
1640
+ kind: "agent:reasoning_delta",
1641
+ text: `rollback executed: ${decision.reason}`,
1642
+ source: "agent",
1485
1643
  });
1486
1644
  }
1487
1645
  else if (this.iterationSystemMsgCount < 5) {
@@ -1605,6 +1763,13 @@ export class AgentLoop extends EventEmitter {
1605
1763
  // reflection 체크포인트 실패는 치명적이지 않음
1606
1764
  }
1607
1765
  }
1766
+ // complex/massive task + 다중 변경 파일일 때만 aggregate impact hint 1회 주입
1767
+ if (!this.impactHintInjected &&
1768
+ this.impactAnalyzer &&
1769
+ this.changedFiles.length >= 2 &&
1770
+ (this.currentComplexity === "complex" || this.currentComplexity === "massive")) {
1771
+ await this.maybeInjectAggregateImpactHint();
1772
+ }
1608
1773
  // 예산 초과 체크
1609
1774
  if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
1610
1775
  return {
@@ -1651,6 +1816,26 @@ export class AgentLoop extends EventEmitter {
1651
1816
  for await (const chunk of stream) {
1652
1817
  if (this.aborted)
1653
1818
  break;
1819
+ if (chunk.type === "reasoning" && chunk.reasoning?.text) {
1820
+ const aggregated = this.reasoningAggregator.push(chunk.reasoning.text, {
1821
+ id: chunk.reasoning.id,
1822
+ provider: chunk.reasoning.provider,
1823
+ model: chunk.reasoning.model,
1824
+ source: chunk.reasoning.source ?? "llm",
1825
+ });
1826
+ for (const item of aggregated) {
1827
+ this.reasoningTree.add("llm", item.text);
1828
+ this.emitEvent({
1829
+ kind: "agent:reasoning_delta",
1830
+ id: item.id,
1831
+ text: item.text,
1832
+ provider: item.provider,
1833
+ model: item.model,
1834
+ source: item.source ?? "llm",
1835
+ });
1836
+ }
1837
+ continue;
1838
+ }
1654
1839
  switch (chunk.type) {
1655
1840
  case "text":
1656
1841
  if (chunk.text) {
@@ -1668,6 +1853,16 @@ export class AgentLoop extends EventEmitter {
1668
1853
  case "tool_call":
1669
1854
  // tool_call 전에 남은 텍스트 flush
1670
1855
  flushTextBuffer();
1856
+ for (const item of this.reasoningAggregator.flush()) {
1857
+ this.emitEvent({
1858
+ kind: "agent:reasoning_delta",
1859
+ id: item.id,
1860
+ text: item.text,
1861
+ provider: item.provider,
1862
+ model: item.model,
1863
+ source: item.source ?? "llm",
1864
+ });
1865
+ }
1671
1866
  if (chunk.toolCall) {
1672
1867
  toolCalls.push(chunk.toolCall);
1673
1868
  this.emitEvent({
@@ -1678,14 +1873,39 @@ export class AgentLoop extends EventEmitter {
1678
1873
  }
1679
1874
  break;
1680
1875
  case "done":
1876
+ for (const item of this.reasoningAggregator.flush()) {
1877
+ this.emitEvent({
1878
+ kind: "agent:reasoning_delta",
1879
+ id: item.id,
1880
+ text: item.text,
1881
+ provider: item.provider,
1882
+ model: item.model,
1883
+ source: item.source ?? "llm",
1884
+ });
1885
+ }
1681
1886
  if (chunk.usage) {
1682
- usage = chunk.usage;
1887
+ usage = {
1888
+ input: chunk.usage.input ?? 0,
1889
+ output: chunk.usage.output ?? 0,
1890
+ };
1683
1891
  }
1684
1892
  break;
1685
1893
  }
1686
1894
  }
1687
1895
  // 스트림 종료 후 남은 버퍼 flush
1688
1896
  flushTextBuffer();
1897
+ for (const item of this.reasoningAggregator.flush()) {
1898
+ this.emitEvent({
1899
+ kind: "agent:reasoning_delta",
1900
+ id: item.id,
1901
+ text: item.text,
1902
+ provider: item.provider,
1903
+ model: item.model,
1904
+ source: item.source ?? "llm",
1905
+ });
1906
+ }
1907
+ if (flushTimer)
1908
+ clearTimeout(flushTimer);
1689
1909
  return {
1690
1910
  content: content || null,
1691
1911
  toolCalls,
@@ -1702,9 +1922,14 @@ export class AgentLoop extends EventEmitter {
1702
1922
  * 4. AutoFixLoop 결과 검증 → 실패 시 에러 피드백 메시지 추가
1703
1923
  */
1704
1924
  async executeTools(toolCalls) {
1925
+ if (toolCalls.length > 1)
1926
+ this.emitReasoning(`parallel tool batch started (${toolCalls.length} tools)`);
1705
1927
  const results = [];
1706
1928
  const deferredFixPrompts = [];
1707
1929
  for (const toolCall of toolCalls) {
1930
+ const args = this.parseToolArgs(toolCall.arguments);
1931
+ const allDefinitions = [...this.config.loop.tools, ...this.mcpToolDefinitions];
1932
+ const matchedDefinition = allDefinitions.find((t) => t.name === toolCall.name);
1708
1933
  // Governor: 안전성 검증
1709
1934
  try {
1710
1935
  this.governor.validateToolCall(toolCall);
@@ -1712,7 +1937,6 @@ export class AgentLoop extends EventEmitter {
1712
1937
  catch (err) {
1713
1938
  if (err instanceof ApprovalRequiredError) {
1714
1939
  // Governor가 위험 감지 → ApprovalManager로 승인 프로세스 위임
1715
- const args = this.parseToolArgs(toolCall.arguments);
1716
1940
  const approvalResult = await this.handleApproval(toolCall, args, err);
1717
1941
  if (approvalResult) {
1718
1942
  results.push(approvalResult);
@@ -1720,12 +1944,32 @@ export class AgentLoop extends EventEmitter {
1720
1944
  }
1721
1945
  // 승인됨 → 계속 실행
1722
1946
  }
1947
+ // Generic tool-definition approval gate
1948
+ if (matchedDefinition?.requiresApproval) {
1949
+ const definitionApprovalReq = {
1950
+ id: `definition-approval-${toolCall.id}`,
1951
+ toolName: toolCall.name,
1952
+ arguments: args,
1953
+ reason: matchedDefinition.source === "mcp"
1954
+ ? `MCP tool "${toolCall.name}" requires approval`
1955
+ : `Tool "${toolCall.name}" requires approval`,
1956
+ riskLevel: matchedDefinition.riskLevel === "critical" ||
1957
+ matchedDefinition.riskLevel === "high"
1958
+ ? "high"
1959
+ : "medium",
1960
+ timeout: 120_000,
1961
+ };
1962
+ const definitionApprovalResult = await this.handleApprovalRequest(toolCall, definitionApprovalReq);
1963
+ if (definitionApprovalResult) {
1964
+ results.push(definitionApprovalResult);
1965
+ continue;
1966
+ }
1967
+ }
1723
1968
  else {
1724
1969
  throw err;
1725
1970
  }
1726
1971
  }
1727
1972
  // ApprovalManager: 추가 승인 체크 (Governor가 못 잡은 규칙)
1728
- const args = this.parseToolArgs(toolCall.arguments);
1729
1973
  const approvalRequest = this.approvalManager.checkApproval(toolCall.name, args);
1730
1974
  if (approvalRequest) {
1731
1975
  const approvalResult = await this.handleApprovalRequest(toolCall, approvalRequest);
@@ -1770,15 +2014,57 @@ export class AgentLoop extends EventEmitter {
1770
2014
  : mcpResult.output,
1771
2015
  durationMs: mcpResult.durationMs,
1772
2016
  });
2017
+ this.emitEvent({
2018
+ kind: "agent:reasoning_delta",
2019
+ text: `tool finished: ${toolCall.name}`,
2020
+ });
1773
2021
  continue;
1774
2022
  }
1775
2023
  // 도구 실행 — AbortController를 InterruptManager에 등록
1776
2024
  const startTime = Date.now();
1777
2025
  const toolAbort = new AbortController();
1778
2026
  this.interruptManager.registerToolAbort(toolAbort);
2027
+ // rollback용 원본 스냅샷은 실행 전에 저장
2028
+ if (["file_write", "file_edit"].includes(toolCall.name)) {
2029
+ const candidatePath = args.path ??
2030
+ args.file;
2031
+ if (candidatePath) {
2032
+ const filePathStr = String(candidatePath);
2033
+ if (!this.originalSnapshots.has(filePathStr)) {
2034
+ try {
2035
+ const { readFile } = await import("node:fs/promises");
2036
+ const original = await readFile(filePathStr, "utf-8");
2037
+ this.originalSnapshots.set(filePathStr, original);
2038
+ }
2039
+ catch (err) {
2040
+ if (err.code !== "ENOENT") {
2041
+ throw err;
2042
+ }
2043
+ }
2044
+ }
2045
+ }
2046
+ }
1779
2047
  try {
1780
- const result = await this.toolExecutor.execute(toolCall, toolAbort.signal);
2048
+ const result = await this.toolExecutor.execute(toolCall, toolAbort?.signal);
1781
2049
  this.interruptManager.clearToolAbort();
2050
+ // Learned skill confidence feedback
2051
+ // 성공한 실행 결과/도구명/출력에 매칭되는 learned skill이 있으면 confidence 갱신
2052
+ if (this.skillLearner) {
2053
+ try {
2054
+ const relevantSkills = this.skillLearner.getRelevantSkills({
2055
+ errorMessage: `${toolCall.name}\n${result.output}`,
2056
+ filePath: typeof args.path === "string"
2057
+ ? args.path
2058
+ : typeof args.file === "string"
2059
+ ? args.file
2060
+ : undefined,
2061
+ });
2062
+ for (const skill of relevantSkills.slice(0, 1)) {
2063
+ this.skillLearner.updateConfidence(skill.id, result.success);
2064
+ }
2065
+ }
2066
+ catch { }
2067
+ }
1782
2068
  results.push(result);
1783
2069
  this.emitEvent({
1784
2070
  kind: "agent:tool_result",
@@ -1788,6 +2074,8 @@ export class AgentLoop extends EventEmitter {
1788
2074
  : result.output,
1789
2075
  durationMs: result.durationMs,
1790
2076
  });
2077
+ this.emitReasoning(`success: ${toolCall.name}`);
2078
+ this.reasoningTree.add("tool", `success: ${toolCall.name}`);
1791
2079
  // 파일 변경 이벤트 + 추적
1792
2080
  if (["file_write", "file_edit"].includes(toolCall.name) &&
1793
2081
  result.success) {
@@ -1798,17 +2086,6 @@ export class AgentLoop extends EventEmitter {
1798
2086
  // 변경 파일 추적 (메모리 업데이트용)
1799
2087
  if (!this.changedFiles.includes(filePathStr)) {
1800
2088
  this.changedFiles.push(filePathStr);
1801
- // 원본 스냅샷 저장 (rollback용) — 최초 변경 시에만
1802
- if (!this.originalSnapshots.has(filePathStr)) {
1803
- try {
1804
- const { readFile } = await import("node:fs/promises");
1805
- const original = await readFile(filePathStr, "utf-8");
1806
- this.originalSnapshots.set(filePathStr, original);
1807
- }
1808
- catch {
1809
- // 파일이 새로 생성된 경우 스냅샷 없음
1810
- }
1811
- }
1812
2089
  }
1813
2090
  this.emitEvent({
1814
2091
  kind: "agent:file_change",
@@ -1858,6 +2135,23 @@ export class AgentLoop extends EventEmitter {
1858
2135
  break;
1859
2136
  }
1860
2137
  const errorMessage = err instanceof Error ? err.message : String(err);
2138
+ // Learned skill confidence feedback (failure path)
2139
+ if (this.skillLearner) {
2140
+ try {
2141
+ const relevantSkills = this.skillLearner.getRelevantSkills({
2142
+ errorMessage: `${toolCall.name}\n${errorMessage}`,
2143
+ filePath: typeof args.path === "string"
2144
+ ? args.path
2145
+ : typeof args.file === "string"
2146
+ ? args.file
2147
+ : undefined,
2148
+ });
2149
+ for (const skill of relevantSkills.slice(0, 1)) {
2150
+ this.skillLearner.updateConfidence(skill.id, false);
2151
+ }
2152
+ }
2153
+ catch { }
2154
+ }
1861
2155
  results.push({
1862
2156
  tool_call_id: toolCall.id,
1863
2157
  name: toolCall.name,
@@ -1870,6 +2164,11 @@ export class AgentLoop extends EventEmitter {
1870
2164
  message: `Tool ${toolCall.name} failed: ${errorMessage}`,
1871
2165
  retryable: true,
1872
2166
  });
2167
+ this.emitEvent({
2168
+ kind: "agent:reasoning_delta",
2169
+ text: `failed: ${toolCall.name}`,
2170
+ });
2171
+ this.reasoningTree.add("tool", `failed: ${toolCall.name}`);
1873
2172
  }
1874
2173
  }
1875
2174
  return { results, deferredFixPrompts };
@@ -1880,7 +2179,7 @@ export class AgentLoop extends EventEmitter {
1880
2179
  */
1881
2180
  async handleApproval(toolCall, args, err) {
1882
2181
  const request = {
1883
- id: crypto.randomUUID(),
2182
+ id: randomUUID(),
1884
2183
  toolName: toolCall.name,
1885
2184
  arguments: args,
1886
2185
  riskLevel: "high",
@@ -1917,8 +2216,8 @@ export class AgentLoop extends EventEmitter {
1917
2216
  * 실패 시 수정 프롬프트를 대화 히스토리에 추가.
1918
2217
  */
1919
2218
  async validateAndFeedback(toolName, result) {
1920
- // file_write/file_edit만 검증 (다른 도구는 스킵)
1921
- if (!["file_write", "file_edit"].includes(toolName)) {
2219
+ // file_write/file_edit/shell_exec만 검증
2220
+ if (!["file_write", "file_edit", "shell_exec"].includes(toolName)) {
1922
2221
  return null;
1923
2222
  }
1924
2223
  const validation = await this.autoFixLoop.validateResult(toolName, result.output, result.success, this.config.loop.projectPath);
@@ -1953,7 +2252,11 @@ export class AgentLoop extends EventEmitter {
1953
2252
  parseToolArgs(args) {
1954
2253
  if (typeof args === "string") {
1955
2254
  try {
1956
- return JSON.parse(args);
2255
+ const parsed = JSON.parse(args);
2256
+ if (parsed && typeof parsed === "object") {
2257
+ return parsed;
2258
+ }
2259
+ return { raw: args };
1957
2260
  }
1958
2261
  catch {
1959
2262
  return { raw: args };
@@ -1973,7 +2276,7 @@ export class AgentLoop extends EventEmitter {
1973
2276
  // 현재 plan 정보에서 진행 상황 추출
1974
2277
  const progress = this.extractProgress();
1975
2278
  const checkpoint = {
1976
- sessionId: crypto.randomUUID(),
2279
+ sessionId: this.sessionId ?? "unknown-session",
1977
2280
  goal: this.contextManager.getMessages().find((m) => m.role === "user")?.content ?? "",
1978
2281
  progress,
1979
2282
  changedFiles: this.changedFiles.map((path) => ({ path, diff: "" })),
@@ -1983,7 +2286,9 @@ export class AgentLoop extends EventEmitter {
1983
2286
  .filter((r) => !r.success)
1984
2287
  .slice(-5)
1985
2288
  .map((r) => `${r.name}: ${r.output.slice(0, 200)}`),
1986
- contextUsageAtSave: this.tokenUsage.total / this.config.loop.totalTokenBudget,
2289
+ contextUsageAtSave: this.config.loop.totalTokenBudget > 0
2290
+ ? this.tokenUsage.total / this.config.loop.totalTokenBudget
2291
+ : 0,
1987
2292
  totalTokensUsed: this.tokenUsage.total,
1988
2293
  iterationsCompleted: iteration,
1989
2294
  createdAt: new Date(),
@@ -2136,6 +2441,83 @@ export class AgentLoop extends EventEmitter {
2136
2441
  // Impact analysis 실패는 치명적이지 않음
2137
2442
  }
2138
2443
  }
2444
+ /**
2445
+ * 다중 파일 변경이 누적된 경우 aggregate impact를 1회만 컨텍스트에 주입한다.
2446
+ * 무거운 분석이므로 complex/massive 태스크에서만 사용.
2447
+ */
2448
+ async maybeInjectAggregateImpactHint() {
2449
+ if (!this.impactAnalyzer || this.changedFiles.length < 2)
2450
+ return;
2451
+ try {
2452
+ const report = await this.impactAnalyzer.analyzeChanges(this.changedFiles);
2453
+ const shouldInject = report.breakingChanges.length > 0 ||
2454
+ report.deadCodeCandidates.length > 0 ||
2455
+ report.testCoverage.some((t) => t.inferredCoverage === "low") ||
2456
+ report.riskLevel === "high" ||
2457
+ report.riskLevel === "critical";
2458
+ if (!shouldInject)
2459
+ return;
2460
+ const planPreview = report.refactorPlan
2461
+ .slice(0, 3)
2462
+ .map((step) => `${step.step}. ${step.action}`)
2463
+ .join("\n");
2464
+ const hintLines = [
2465
+ "[Aggregate Impact Hint]",
2466
+ `Risk: ${report.riskLevel}`,
2467
+ `Breaking changes: ${report.breakingChanges.length}`,
2468
+ `Dead code candidates: ${report.deadCodeCandidates.length}`,
2469
+ `Low coverage files: ${report.testCoverage.filter((t) => t.inferredCoverage === "low").length}`,
2470
+ ];
2471
+ if (planPreview) {
2472
+ hintLines.push("", "Suggested refactor order:", planPreview);
2473
+ }
2474
+ this.contextManager.addMessage({
2475
+ role: "system",
2476
+ content: hintLines.join("\n"),
2477
+ });
2478
+ this.impactHintInjected = true;
2479
+ this.emitEvent({
2480
+ kind: "agent:thinking",
2481
+ content: `Aggregate impact hint injected: ${report.riskLevel} risk, ` +
2482
+ `${report.breakingChanges.length} breaking changes, ` +
2483
+ `${report.deadCodeCandidates.length} dead code candidates.`,
2484
+ });
2485
+ }
2486
+ catch {
2487
+ // aggregate impact 실패는 치명적이지 않음
2488
+ }
2489
+ }
2490
+ /**
2491
+ * 종료 직전 최종 impact 요약 생성.
2492
+ * assistant final summary에만 붙이고 system prompt 오염은 하지 않는다.
2493
+ */
2494
+ async buildFinalImpactSummary() {
2495
+ if (!this.impactAnalyzer || this.changedFiles.length === 0)
2496
+ return null;
2497
+ try {
2498
+ const report = await this.impactAnalyzer.analyzeChanges(this.changedFiles);
2499
+ const lines = [];
2500
+ lines.push("Impact summary:");
2501
+ lines.push(`- Risk: ${report.riskLevel}, affected files: ${report.affectedFiles.length}, breaking changes: ${report.breakingChanges.length}`);
2502
+ const lowCoverage = report.testCoverage.filter((t) => t.inferredCoverage === "low");
2503
+ if (lowCoverage.length > 0) {
2504
+ lines.push(`- Low inferred test coverage: ${lowCoverage.map((t) => t.file).join(", ")}`);
2505
+ }
2506
+ if (report.deadCodeCandidates.length > 0) {
2507
+ lines.push(`- Dead code candidates: ${report.deadCodeCandidates
2508
+ .slice(0, 5)
2509
+ .map((d) => `${d.file}:${d.symbol}`)
2510
+ .join(", ")}`);
2511
+ }
2512
+ if (report.refactorPlan.length > 0) {
2513
+ lines.push(`- Suggested next step: ${report.refactorPlan[0]?.action ?? "review refactor plan"}`);
2514
+ }
2515
+ return lines.join("\n");
2516
+ }
2517
+ catch {
2518
+ return null;
2519
+ }
2520
+ }
2139
2521
  /**
2140
2522
  * CostOptimizer 인스턴스를 반환한다.
2141
2523
  */
@@ -2158,6 +2540,25 @@ export class AgentLoop extends EventEmitter {
2158
2540
  const args = this.parseToolArgs(toolCall.arguments);
2159
2541
  return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
2160
2542
  }
2543
+ /**
2544
+ * ContinuousReflection overflow signal을 soft rollover로 처리한다.
2545
+ * 절대 abort하지 않고, 체크포인트 저장 후 다음 iteration에서
2546
+ * ContextManager가 압축된 컨텍스트를 사용하도록 둔다.
2547
+ */
2548
+ async handleSoftContextOverflow() {
2549
+ try {
2550
+ await this.saveAutoCheckpoint(this.iterationCount);
2551
+ this.checkpointSaved = true;
2552
+ }
2553
+ catch {
2554
+ // checkpoint 실패는 치명적이지 않음
2555
+ }
2556
+ this.emitEvent({
2557
+ kind: "agent:thinking",
2558
+ content: "Context usage exceeded safe threshold. " +
2559
+ "Saved checkpoint, compacting history, and continuing without abort.",
2560
+ });
2561
+ }
2161
2562
  /** MCP 클라이언트 정리 (세션 종료 시 호출) */
2162
2563
  async dispose() {
2163
2564
  if (this.mcpClient) {
@@ -2173,8 +2574,23 @@ export class AgentLoop extends EventEmitter {
2173
2574
  emitEvent(event) {
2174
2575
  this.emit("event", event);
2175
2576
  }
2577
+ emitReasoning(content) {
2578
+ this.reasoningTree.add("reasoning", content);
2579
+ this.emitEvent({
2580
+ kind: "agent:reasoning_delta",
2581
+ text: content,
2582
+ source: "agent",
2583
+ });
2584
+ }
2585
+ emitSubagent(name, phase, content) {
2586
+ this.reasoningTree.add(name, `[${phase}] ${content}`);
2587
+ this.emitReasoning(`[${name}:${phase}] ${content}`);
2588
+ }
2176
2589
  handleFatalError(err) {
2177
2590
  const message = err instanceof Error ? err.message : String(err);
2591
+ if (this.sessionPersistence && this.sessionId) {
2592
+ void this.sessionPersistence.updateStatus(this.sessionId, "crashed");
2593
+ }
2178
2594
  this.emitEvent({
2179
2595
  kind: "agent:error",
2180
2596
  message,