@yuaone/core 0.1.3 → 0.3.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 (95) hide show
  1. package/dist/agent-loop.d.ts +152 -0
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +893 -9
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/benchmark-runner.d.ts +141 -0
  6. package/dist/benchmark-runner.d.ts.map +1 -0
  7. package/dist/benchmark-runner.js +526 -0
  8. package/dist/benchmark-runner.js.map +1 -0
  9. package/dist/codebase-context.d.ts +49 -0
  10. package/dist/codebase-context.d.ts.map +1 -1
  11. package/dist/codebase-context.js +146 -0
  12. package/dist/codebase-context.js.map +1 -1
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +8 -0
  15. package/dist/constants.js.map +1 -1
  16. package/dist/context-budget.d.ts +1 -1
  17. package/dist/context-budget.d.ts.map +1 -1
  18. package/dist/context-budget.js +4 -2
  19. package/dist/context-budget.js.map +1 -1
  20. package/dist/context-compressor.d.ts +1 -1
  21. package/dist/context-compressor.d.ts.map +1 -1
  22. package/dist/context-compressor.js +5 -3
  23. package/dist/context-compressor.js.map +1 -1
  24. package/dist/context-manager.d.ts +7 -1
  25. package/dist/context-manager.d.ts.map +1 -1
  26. package/dist/context-manager.js +34 -2
  27. package/dist/context-manager.js.map +1 -1
  28. package/dist/continuation-engine.d.ts +168 -0
  29. package/dist/continuation-engine.d.ts.map +1 -0
  30. package/dist/continuation-engine.js +421 -0
  31. package/dist/continuation-engine.js.map +1 -0
  32. package/dist/cost-optimizer.d.ts +159 -0
  33. package/dist/cost-optimizer.d.ts.map +1 -0
  34. package/dist/cost-optimizer.js +406 -0
  35. package/dist/cost-optimizer.js.map +1 -0
  36. package/dist/execution-engine.d.ts.map +1 -1
  37. package/dist/execution-engine.js +9 -4
  38. package/dist/execution-engine.js.map +1 -1
  39. package/dist/execution-policy-engine.d.ts +133 -0
  40. package/dist/execution-policy-engine.d.ts.map +1 -0
  41. package/dist/execution-policy-engine.js +367 -0
  42. package/dist/execution-policy-engine.js.map +1 -0
  43. package/dist/failure-recovery.d.ts +228 -0
  44. package/dist/failure-recovery.d.ts.map +1 -0
  45. package/dist/failure-recovery.js +664 -0
  46. package/dist/failure-recovery.js.map +1 -0
  47. package/dist/hierarchical-planner.d.ts +69 -1
  48. package/dist/hierarchical-planner.d.ts.map +1 -1
  49. package/dist/hierarchical-planner.js +117 -0
  50. package/dist/hierarchical-planner.js.map +1 -1
  51. package/dist/impact-analyzer.d.ts +92 -0
  52. package/dist/impact-analyzer.d.ts.map +1 -0
  53. package/dist/impact-analyzer.js +615 -0
  54. package/dist/impact-analyzer.js.map +1 -0
  55. package/dist/index.d.ts +28 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +27 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/llm-client.d.ts +1 -1
  60. package/dist/llm-client.d.ts.map +1 -1
  61. package/dist/llm-client.js +74 -7
  62. package/dist/llm-client.js.map +1 -1
  63. package/dist/memory-updater.d.ts +189 -0
  64. package/dist/memory-updater.d.ts.map +1 -0
  65. package/dist/memory-updater.js +481 -0
  66. package/dist/memory-updater.js.map +1 -0
  67. package/dist/prompt-defense.d.ts +59 -0
  68. package/dist/prompt-defense.d.ts.map +1 -0
  69. package/dist/prompt-defense.js +311 -0
  70. package/dist/prompt-defense.js.map +1 -0
  71. package/dist/reflexion.d.ts +211 -0
  72. package/dist/reflexion.d.ts.map +1 -0
  73. package/dist/reflexion.js +559 -0
  74. package/dist/reflexion.js.map +1 -0
  75. package/dist/system-prompt.d.ts +19 -3
  76. package/dist/system-prompt.d.ts.map +1 -1
  77. package/dist/system-prompt.js +203 -38
  78. package/dist/system-prompt.js.map +1 -1
  79. package/dist/task-classifier.d.ts +92 -0
  80. package/dist/task-classifier.d.ts.map +1 -0
  81. package/dist/task-classifier.js +566 -0
  82. package/dist/task-classifier.js.map +1 -0
  83. package/dist/token-budget.d.ts +131 -0
  84. package/dist/token-budget.d.ts.map +1 -0
  85. package/dist/token-budget.js +321 -0
  86. package/dist/token-budget.js.map +1 -0
  87. package/dist/types.d.ts +20 -2
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/types.js +18 -1
  90. package/dist/types.js.map +1 -1
  91. package/dist/world-state.d.ts +87 -0
  92. package/dist/world-state.d.ts.map +1 -0
  93. package/dist/world-state.js +435 -0
  94. package/dist/world-state.js.map +1 -0
  95. package/package.json +11 -21
@@ -14,6 +14,22 @@ import { LLMError, PlanLimitError, ApprovalRequiredError, } from "./errors.js";
14
14
  import { ApprovalManager, } from "./approval.js";
15
15
  import { AutoFixLoop, } from "./auto-fix.js";
16
16
  import { InterruptManager } from "./interrupt-manager.js";
17
+ import { YuanMemory } from "./memory.js";
18
+ import { MemoryManager } from "./memory-manager.js";
19
+ import { buildSystemPrompt } from "./system-prompt.js";
20
+ import { HierarchicalPlanner, } from "./hierarchical-planner.js";
21
+ import { TaskClassifier } from "./task-classifier.js";
22
+ import { PromptDefense } from "./prompt-defense.js";
23
+ import { ReflexionEngine } from "./reflexion.js";
24
+ import { TokenBudgetManager } from "./token-budget.js";
25
+ import { ContinuationEngine } from "./continuation-engine.js";
26
+ import { MemoryUpdater } from "./memory-updater.js";
27
+ import { MCPClient } from "./mcp-client.js";
28
+ import { WorldStateCollector } from "./world-state.js";
29
+ import { FailureRecovery } from "./failure-recovery.js";
30
+ import { ExecutionPolicyEngine } from "./execution-policy-engine.js";
31
+ import { CostOptimizer } from "./cost-optimizer.js";
32
+ import { ImpactAnalyzer } from "./impact-analyzer.js";
17
33
  /**
18
34
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
19
35
  *
@@ -50,7 +66,38 @@ export class AgentLoop extends EventEmitter {
50
66
  approvalManager;
51
67
  autoFixLoop;
52
68
  interruptManager;
69
+ enableMemory;
70
+ enablePlanning;
71
+ planningThreshold;
72
+ environment;
73
+ yuanMemory = null;
74
+ memoryManager = null;
75
+ planner = null;
76
+ activePlan = null;
77
+ taskClassifier;
78
+ promptDefense;
79
+ reflexionEngine = null;
80
+ tokenBudgetManager;
81
+ allToolResults = [];
82
+ currentTaskIndex = 0;
83
+ changedFiles = [];
53
84
  aborted = false;
85
+ initialized = false;
86
+ continuationEngine = null;
87
+ mcpClient = null;
88
+ mcpToolDefinitions = [];
89
+ mcpServerConfigs;
90
+ memoryUpdater;
91
+ failureRecovery;
92
+ policyEngine = null;
93
+ worldState = null;
94
+ costOptimizer;
95
+ impactAnalyzer = null;
96
+ policyOverrides;
97
+ checkpointSaved = false;
98
+ iterationCount = 0;
99
+ originalSnapshots = new Map();
100
+ previousStrategies = [];
54
101
  tokenUsage = {
55
102
  input: 0,
56
103
  output: 0,
@@ -61,6 +108,10 @@ export class AgentLoop extends EventEmitter {
61
108
  super();
62
109
  this.config = options.config;
63
110
  this.toolExecutor = options.toolExecutor;
111
+ this.enableMemory = options.enableMemory !== false;
112
+ this.enablePlanning = options.enablePlanning !== false;
113
+ this.planningThreshold = options.planningThreshold ?? "moderate";
114
+ this.environment = options.environment;
64
115
  // BYOK LLM 클라이언트 생성
65
116
  this.llmClient = new BYOKClient(options.config.byok);
66
117
  // Governor 생성
@@ -79,22 +130,224 @@ export class AgentLoop extends EventEmitter {
79
130
  }
80
131
  // AutoFixLoop 생성
81
132
  this.autoFixLoop = new AutoFixLoop(options.autoFixConfig);
133
+ // Task Classifier + Prompt Defense + Token Budget + Memory Updater
134
+ this.taskClassifier = new TaskClassifier();
135
+ this.promptDefense = new PromptDefense();
136
+ this.tokenBudgetManager = new TokenBudgetManager({
137
+ totalBudget: options.config.loop.totalTokenBudget,
138
+ });
139
+ this.memoryUpdater = new MemoryUpdater();
140
+ this.failureRecovery = new FailureRecovery();
141
+ this.costOptimizer = new CostOptimizer();
142
+ this.policyOverrides = options.policyOverrides;
143
+ // MCP 서버 설정 저장
144
+ this.mcpServerConfigs = options.mcpServerConfigs ?? [];
82
145
  // InterruptManager 설정 (외부 주입 또는 내부 생성)
83
146
  this.interruptManager = options.interruptManager ?? new InterruptManager();
84
147
  this.setupInterruptListeners();
85
- // 시스템 프롬프트 추가
148
+ // 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
86
149
  this.contextManager.addMessage({
87
150
  role: "system",
88
151
  content: this.config.loop.systemPrompt,
89
152
  });
90
153
  }
154
+ /**
155
+ * Memory와 프로젝트 컨텍스트를 로드하여 시스템 프롬프트를 갱신.
156
+ * run() 호출 전에 한 번 호출하면 메모리가 자동으로 주입된다.
157
+ * 이미 초기화되었으면 스킵.
158
+ */
159
+ async init() {
160
+ if (this.initialized)
161
+ return;
162
+ this.initialized = true;
163
+ const projectPath = this.config.loop.projectPath;
164
+ if (!projectPath)
165
+ return;
166
+ let yuanMdContent;
167
+ let projectStructure;
168
+ // Memory 로드
169
+ if (this.enableMemory) {
170
+ try {
171
+ // YUAN.md (raw markdown)
172
+ this.yuanMemory = new YuanMemory(projectPath);
173
+ const memData = await this.yuanMemory.load();
174
+ if (memData) {
175
+ yuanMdContent = memData.raw;
176
+ }
177
+ // MemoryManager (structured learnings)
178
+ this.memoryManager = new MemoryManager(projectPath);
179
+ await this.memoryManager.load();
180
+ // 프로젝트 구조 분석
181
+ projectStructure = await this.yuanMemory.analyzeProject();
182
+ }
183
+ catch (memErr) {
184
+ // 메모리 로드 실패는 치명적이지 않음 — 경고만 출력
185
+ this.emitEvent({
186
+ kind: "agent:error",
187
+ message: `Memory load failed: ${memErr instanceof Error ? memErr.message : String(memErr)}`,
188
+ retryable: false,
189
+ });
190
+ }
191
+ }
192
+ // ExecutionPolicyEngine 로드
193
+ try {
194
+ this.policyEngine = new ExecutionPolicyEngine(projectPath);
195
+ const policy = await this.policyEngine.load();
196
+ if (this.policyOverrides) {
197
+ for (const [section, values] of Object.entries(this.policyOverrides)) {
198
+ this.policyEngine.override(section, values);
199
+ }
200
+ }
201
+ // FailureRecovery에 정책 적용
202
+ const recoveryConfig = this.policyEngine.toFailureRecoveryConfig();
203
+ this.failureRecovery = new FailureRecovery(recoveryConfig);
204
+ }
205
+ catch {
206
+ // 정책 로드 실패 → 기본값 사용
207
+ }
208
+ // WorldState 수집 → system prompt에 주입
209
+ try {
210
+ const worldStateCollector = new WorldStateCollector({
211
+ projectPath,
212
+ maxRecentCommits: 10,
213
+ skipTest: true,
214
+ });
215
+ this.worldState = await worldStateCollector.collect();
216
+ }
217
+ catch {
218
+ // WorldState 수집 실패는 치명적이지 않음
219
+ }
220
+ // ImpactAnalyzer 생성
221
+ this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
222
+ // ContinuationEngine 생성
223
+ this.continuationEngine = new ContinuationEngine({ projectPath });
224
+ // 이전 세션 체크포인트 복원
225
+ try {
226
+ const latestCheckpoint = await this.continuationEngine.findLatestCheckpoint();
227
+ if (latestCheckpoint) {
228
+ const continuationPrompt = this.continuationEngine.formatContinuationPrompt(latestCheckpoint);
229
+ this.contextManager.addMessage({
230
+ role: "system",
231
+ content: continuationPrompt,
232
+ });
233
+ // 복원 후 체크포인트 정리
234
+ await this.continuationEngine.pruneOldCheckpoints();
235
+ }
236
+ }
237
+ catch {
238
+ // 체크포인트 복원 실패는 치명적이지 않음
239
+ }
240
+ // MCP 클라이언트 연결
241
+ if (this.mcpServerConfigs.length > 0) {
242
+ try {
243
+ this.mcpClient = new MCPClient({
244
+ servers: this.mcpServerConfigs,
245
+ });
246
+ await this.mcpClient.connectAll();
247
+ this.mcpToolDefinitions = this.mcpClient.toToolDefinitions();
248
+ }
249
+ catch {
250
+ // MCP 연결 실패는 치명적이지 않음 — 로컬 도구만 사용
251
+ this.mcpClient = null;
252
+ this.mcpToolDefinitions = [];
253
+ }
254
+ }
255
+ // HierarchicalPlanner 생성
256
+ if (this.enablePlanning && projectPath) {
257
+ this.planner = new HierarchicalPlanner({ projectPath });
258
+ }
259
+ // ReflexionEngine 생성
260
+ if (projectPath) {
261
+ this.reflexionEngine = new ReflexionEngine({ projectPath });
262
+ }
263
+ // 향상된 시스템 프롬프트 생성
264
+ const enhancedPrompt = buildSystemPrompt({
265
+ projectStructure,
266
+ yuanMdContent,
267
+ tools: [...this.config.loop.tools, ...this.mcpToolDefinitions],
268
+ projectPath,
269
+ environment: this.environment,
270
+ });
271
+ // WorldState를 시스템 프롬프트에 추가
272
+ let worldStateSection = "";
273
+ if (this.worldState) {
274
+ const collector = new WorldStateCollector({ projectPath });
275
+ worldStateSection = "\n\n" + collector.formatForPrompt(this.worldState);
276
+ }
277
+ // 기존 시스템 메시지를 향상된 프롬프트로 교체
278
+ this.contextManager.replaceSystemMessage(enhancedPrompt + worldStateSection);
279
+ // MemoryManager의 관련 학습/경고를 추가 컨텍스트로 주입
280
+ if (this.memoryManager) {
281
+ const memory = this.memoryManager.getMemory();
282
+ if (memory.learnings.length > 0 || memory.failedApproaches.length > 0) {
283
+ const memoryContext = this.buildMemoryContext(memory);
284
+ if (memoryContext) {
285
+ this.contextManager.addMessage({
286
+ role: "system",
287
+ content: memoryContext,
288
+ });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ /**
294
+ * MemoryManager의 학습/실패 기록을 시스템 메시지로 변환.
295
+ */
296
+ buildMemoryContext(memory) {
297
+ const parts = [];
298
+ // 높은 confidence 학습만 포함
299
+ const highConfLearnings = memory.learnings.filter((l) => l.confidence >= 0.3);
300
+ if (highConfLearnings.length > 0) {
301
+ parts.push("## Things I've Learned About This Project");
302
+ for (const l of highConfLearnings.slice(0, 20)) {
303
+ parts.push(`- [${l.category}] ${l.content}`);
304
+ }
305
+ }
306
+ // 실패한 접근 방식 (최근 10개)
307
+ if (memory.failedApproaches.length > 0) {
308
+ parts.push("\n## Approaches That Failed Before (Avoid These)");
309
+ for (const f of memory.failedApproaches.slice(0, 10)) {
310
+ parts.push(`- **${f.approach}** — failed because: ${f.reason}`);
311
+ }
312
+ }
313
+ // 코딩 규칙
314
+ if (memory.conventions.length > 0) {
315
+ parts.push("\n## Project Conventions");
316
+ for (const c of memory.conventions) {
317
+ parts.push(`- ${c}`);
318
+ }
319
+ }
320
+ return parts.length > 0 ? parts.join("\n") : null;
321
+ }
91
322
  /**
92
323
  * 에이전트 루프를 실행.
324
+ * 첫 호출 시 자동으로 Memory와 프로젝트 컨텍스트를 로드한다.
93
325
  * @param userMessage 사용자의 요청 메시지
94
326
  * @returns 종료 사유 및 결과
95
327
  */
96
328
  async run(userMessage) {
97
329
  this.aborted = false;
330
+ this.changedFiles = [];
331
+ this.allToolResults = [];
332
+ this.checkpointSaved = false;
333
+ this.iterationCount = 0;
334
+ this.originalSnapshots.clear();
335
+ this.previousStrategies = [];
336
+ this.failureRecovery.reset();
337
+ this.costOptimizer.reset();
338
+ this.tokenBudgetManager.reset();
339
+ const runStartTime = Date.now();
340
+ // 첫 실행 시 메모리/프로젝트 컨텍스트 자동 로드
341
+ await this.init();
342
+ // 사용자 입력 검증 (prompt injection 방어)
343
+ const inputValidation = this.promptDefense.validateUserInput(userMessage);
344
+ if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
345
+ this.emitEvent({
346
+ kind: "agent:error",
347
+ message: `Prompt injection detected in user input (${inputValidation.severity}): ${inputValidation.patternsFound.join(", ")}`,
348
+ retryable: false,
349
+ });
350
+ }
98
351
  // 사용자 메시지 추가
99
352
  this.contextManager.addMessage({
100
353
  role: "user",
@@ -102,12 +355,119 @@ export class AgentLoop extends EventEmitter {
102
355
  });
103
356
  this.emitEvent({ kind: "agent:start", goal: userMessage });
104
357
  try {
105
- return await this.executeLoop();
358
+ // Reflexion: 과거 실행에서 배운 가이던스 주입
359
+ if (this.reflexionEngine) {
360
+ try {
361
+ const guidance = await this.reflexionEngine.getGuidance(userMessage);
362
+ // 가이던스 유효성 검증: 빈 전략이나 매우 낮은 confidence 필터링
363
+ const validStrategies = guidance.relevantStrategies.filter((s) => s.strategy && s.strategy.length > 5 && s.confidence > 0.1);
364
+ if (validStrategies.length > 0 || guidance.recentFailures.length > 0) {
365
+ const filteredGuidance = { ...guidance, relevantStrategies: validStrategies };
366
+ const guidancePrompt = this.reflexionEngine.formatForSystemPrompt(filteredGuidance);
367
+ this.contextManager.addMessage({
368
+ role: "system",
369
+ content: guidancePrompt,
370
+ });
371
+ }
372
+ }
373
+ catch {
374
+ // guidance 로드 실패는 치명적이지 않음
375
+ }
376
+ }
377
+ // Task 분류 → 시스템 프롬프트에 tool sequence hint 주입
378
+ const classification = this.taskClassifier.classify(userMessage);
379
+ if (classification.confidence >= 0.3) {
380
+ const classificationHint = this.taskClassifier.formatForSystemPrompt(classification);
381
+ this.contextManager.addMessage({
382
+ role: "system",
383
+ content: classificationHint,
384
+ });
385
+ }
386
+ // 복잡도 감지 → 필요 시 자동 플래닝
387
+ await this.maybeCreatePlan(userMessage);
388
+ const result = await this.executeLoop();
389
+ // 실행 완료 후 메모리 자동 업데이트
390
+ await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
391
+ return result;
106
392
  }
107
393
  catch (err) {
108
394
  return this.handleFatalError(err);
109
395
  }
110
396
  }
397
+ /**
398
+ * 에이전트 실행 완료 후 메모리를 자동 업데이트한다.
399
+ * - 변경된 파일 목록 기록
400
+ * - 성공/실패 패턴 학습
401
+ */
402
+ async updateMemoryAfterRun(userGoal, result, runDurationMs = 0) {
403
+ if (!this.enableMemory || !this.memoryManager)
404
+ return;
405
+ try {
406
+ // MemoryUpdater로 풍부한 학습 추출
407
+ const analysis = this.memoryUpdater.analyzeRun({
408
+ goal: userGoal,
409
+ termination: {
410
+ reason: result.reason,
411
+ error: result.error,
412
+ summary: result.summary,
413
+ },
414
+ toolResults: this.allToolResults.map((r) => ({
415
+ name: r.name,
416
+ output: r.output,
417
+ success: r.success,
418
+ durationMs: r.durationMs,
419
+ })),
420
+ changedFiles: this.changedFiles,
421
+ messages: this.contextManager.getMessages().map((m) => ({
422
+ role: m.role,
423
+ content: typeof m.content === "string" ? m.content : null,
424
+ })),
425
+ tokensUsed: this.tokenUsage.total,
426
+ durationMs: runDurationMs,
427
+ iterations: this.iterationCount,
428
+ });
429
+ // 추출된 학습을 MemoryManager에 저장
430
+ const learnings = this.memoryUpdater.extractLearnings(analysis, userGoal);
431
+ for (const learning of learnings) {
432
+ this.memoryManager.addLearning(learning.category, learning.content);
433
+ }
434
+ // 에러로 종료된 경우 실패 기록도 추가
435
+ if (result.reason === "ERROR") {
436
+ this.memoryManager.addFailedApproach(`Task: ${userGoal.slice(0, 80)}`, result.error ?? "Unknown error");
437
+ }
438
+ // 메모리 저장
439
+ await this.memoryManager.save();
440
+ }
441
+ catch {
442
+ // 메모리 저장 실패는 치명적이지 않음
443
+ }
444
+ // Reflexion: 실행 결과 반영 + 전략 추출
445
+ if (this.reflexionEngine) {
446
+ try {
447
+ const entry = this.reflexionEngine.reflect({
448
+ goal: userGoal,
449
+ runId: crypto.randomUUID(),
450
+ termination: result,
451
+ toolResults: this.allToolResults,
452
+ messages: this.contextManager.getMessages(),
453
+ tokensUsed: this.tokenUsage.total,
454
+ durationMs: runDurationMs,
455
+ changedFiles: this.changedFiles,
456
+ });
457
+ await this.reflexionEngine.store.saveReflection(entry);
458
+ // 성공 시 전략 추출
459
+ if (entry.outcome === "success") {
460
+ const strategy = this.reflexionEngine.extractStrategy(entry, userGoal);
461
+ if (strategy) {
462
+ await this.reflexionEngine.store.saveStrategy(strategy);
463
+ }
464
+ }
465
+ }
466
+ catch {
467
+ // reflexion 저장 실패는 치명적이지 않음
468
+ }
469
+ }
470
+ }
111
471
  /**
112
472
  * 실행 중인 루프를 중단.
113
473
  */
@@ -164,6 +524,226 @@ export class AgentLoop extends EventEmitter {
164
524
  getAutoFixLoop() {
165
525
  return this.autoFixLoop;
166
526
  }
527
+ /**
528
+ * TokenBudgetManager 인스턴스를 반환.
529
+ * 역할별 토큰 사용량 조회/리밸런싱에 사용.
530
+ */
531
+ getTokenBudgetManager() {
532
+ return this.tokenBudgetManager;
533
+ }
534
+ // ─── Planning ───
535
+ /**
536
+ * 사용자 메시지의 복잡도를 감지하고, 복잡한 태스크이면 계획을 수립하여 컨텍스트에 주입.
537
+ *
538
+ * 복잡도 판단 기준:
539
+ * - 메시지 길이, 파일/작업 수 언급, 키워드 패턴
540
+ * - "trivial"/"simple" → 플래닝 스킵 (LLM이 직접 처리)
541
+ * - "moderate" 이상 → HierarchicalPlanner로 L1+L2 계획 수립
542
+ */
543
+ async maybeCreatePlan(userMessage) {
544
+ if (!this.planner || !this.enablePlanning)
545
+ return;
546
+ const complexity = this.detectComplexity(userMessage);
547
+ // 임계값 미만이면 플래닝 스킵
548
+ const thresholdOrder = { simple: 1, moderate: 2, complex: 3 };
549
+ const complexityOrder = {
550
+ trivial: 0, simple: 1, moderate: 2, complex: 3, massive: 4,
551
+ };
552
+ if ((complexityOrder[complexity] ?? 0) < thresholdOrder[this.planningThreshold]) {
553
+ return;
554
+ }
555
+ this.emitEvent({
556
+ kind: "agent:thinking",
557
+ content: `Task complexity: ${complexity}. Creating execution plan...`,
558
+ });
559
+ try {
560
+ const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
561
+ this.activePlan = plan;
562
+ this.currentTaskIndex = 0;
563
+ // Estimate planner token usage (plan creation typically uses ~500 tokens per task)
564
+ const planTokenEstimate = plan.tactical.length * 500;
565
+ this.tokenBudgetManager.recordUsage("planner", planTokenEstimate, planTokenEstimate);
566
+ // 계획을 컨텍스트에 주입 (LLM이 따라갈 수 있도록)
567
+ const planContext = this.formatPlanForContext(plan);
568
+ this.contextManager.addMessage({
569
+ role: "system",
570
+ content: planContext,
571
+ });
572
+ this.emitEvent({
573
+ kind: "agent:thinking",
574
+ content: `Plan created: ${plan.tactical.length} tasks, ${plan.totalEstimatedIterations} estimated iterations. Risk: ${plan.strategic.riskAssessment.level}.`,
575
+ });
576
+ }
577
+ catch {
578
+ // 플래닝 실패는 치명적이지 않음 — LLM이 직접 처리하도록 폴백
579
+ this.activePlan = null;
580
+ }
581
+ }
582
+ /**
583
+ * 사용자 메시지에서 태스크 복잡도를 휴리스틱으로 추정.
584
+ * LLM 호출 없이 빠르게 결정 (토큰 절약).
585
+ */
586
+ detectComplexity(message) {
587
+ const lower = message.toLowerCase();
588
+ const len = message.length;
589
+ // 복잡도 점수 계산
590
+ let score = 0;
591
+ // 길이 기반
592
+ if (len > 500)
593
+ score += 2;
594
+ else if (len > 200)
595
+ score += 1;
596
+ // 다중 파일/작업 키워드
597
+ const multiFileKeywords = [
598
+ "refactor", "리팩토링", "리팩터",
599
+ "migrate", "마이그레이션",
600
+ "모든 파일", "all files", "전체",
601
+ "여러 파일", "multiple files",
602
+ "아키텍처", "architecture",
603
+ "시스템", "system-wide",
604
+ ];
605
+ for (const kw of multiFileKeywords) {
606
+ if (lower.includes(kw)) {
607
+ score += 2;
608
+ break;
609
+ }
610
+ }
611
+ // 여러 작업 나열 (1. 2. 3. 또는 - 으로 나열)
612
+ const listItems = message.match(/(?:^|\n)\s*(?:\d+[.)]\s|-\s)/gm);
613
+ if (listItems && listItems.length >= 3)
614
+ score += 2;
615
+ else if (listItems && listItems.length >= 2)
616
+ score += 1;
617
+ // 파일 경로 패턴
618
+ const filePaths = message.match(/\b[\w\-./]+\.[a-z]{1,4}\b/g);
619
+ if (filePaths && filePaths.length >= 5)
620
+ score += 2;
621
+ else if (filePaths && filePaths.length >= 2)
622
+ score += 1;
623
+ // 간단한 작업 키워드 (감점)
624
+ const simpleKeywords = [
625
+ "fix", "고쳐", "수정해",
626
+ "rename", "이름 바꿔",
627
+ "한 줄", "one line",
628
+ "간단", "simple", "quick",
629
+ ];
630
+ for (const kw of simpleKeywords) {
631
+ if (lower.includes(kw)) {
632
+ score -= 1;
633
+ break;
634
+ }
635
+ }
636
+ // 점수 → 복잡도
637
+ if (score <= 0)
638
+ return "trivial";
639
+ if (score <= 1)
640
+ return "simple";
641
+ if (score <= 3)
642
+ return "moderate";
643
+ if (score <= 5)
644
+ return "complex";
645
+ return "massive";
646
+ }
647
+ /**
648
+ * HierarchicalPlan을 LLM이 따라갈 수 있는 컨텍스트 메시지로 포맷.
649
+ */
650
+ formatPlanForContext(plan) {
651
+ const parts = [];
652
+ parts.push("## Execution Plan");
653
+ parts.push(`**Goal:** ${plan.goal}`);
654
+ parts.push(`**Complexity:** ${plan.strategic.estimatedComplexity}`);
655
+ parts.push(`**Risk:** ${plan.strategic.riskAssessment.level}`);
656
+ if (plan.strategic.riskAssessment.requiresApproval) {
657
+ parts.push("**⚠ Requires user approval for high-risk operations.**");
658
+ }
659
+ parts.push("\n### Tasks (execute in order):");
660
+ for (let i = 0; i < plan.tactical.length; i++) {
661
+ const task = plan.tactical[i];
662
+ const deps = task.dependsOn.length > 0
663
+ ? ` (after: ${task.dependsOn.join(", ")})`
664
+ : "";
665
+ parts.push(`${i + 1}. **${task.description}**${deps}`);
666
+ if (task.targetFiles.length > 0) {
667
+ parts.push(` Files: ${task.targetFiles.join(", ")}`);
668
+ }
669
+ if (task.readFiles.length > 0) {
670
+ parts.push(` Read: ${task.readFiles.join(", ")}`);
671
+ }
672
+ parts.push(` Tools: ${task.toolStrategy.join(", ")}`);
673
+ }
674
+ if (plan.strategic.riskAssessment.mitigations.length > 0) {
675
+ parts.push("\n### Risk Mitigations:");
676
+ for (const m of plan.strategic.riskAssessment.mitigations) {
677
+ parts.push(`- ${m}`);
678
+ }
679
+ }
680
+ parts.push("\n### Execution Instructions:");
681
+ parts.push("- Follow the task order above. Complete each task before moving to the next.");
682
+ parts.push("- Read target files before modifying them.");
683
+ parts.push("- If a task fails, report the error and attempt an alternative approach.");
684
+ parts.push("- After all tasks, verify the changes work correctly.");
685
+ return parts.join("\n");
686
+ }
687
+ /**
688
+ * 실행 중 에러 발생 시 리플래닝을 시도한다.
689
+ * @returns 리플래닝 성공 시 true (계속 진행), 실패 시 false
690
+ */
691
+ async attemptReplan(error, failedTaskId) {
692
+ if (!this.planner || !this.activePlan)
693
+ return false;
694
+ const trigger = {
695
+ type: "error",
696
+ description: error,
697
+ affectedTaskIds: failedTaskId ? [failedTaskId] : [],
698
+ severity: "major",
699
+ };
700
+ try {
701
+ const result = await this.planner.replan(this.activePlan, trigger, this.llmClient);
702
+ if (result.strategy === "escalate") {
703
+ // 에스컬레이션 → 유저에게 알림
704
+ this.emitEvent({
705
+ kind: "agent:error",
706
+ message: `Re-plan escalated: ${result.reason}`,
707
+ retryable: false,
708
+ });
709
+ return false;
710
+ }
711
+ // 수정된 태스크로 업데이트
712
+ if (result.modifiedTasks.length > 0) {
713
+ // 기존 tactical 태스크를 교체
714
+ for (const modTask of result.modifiedTasks) {
715
+ const idx = this.activePlan.tactical.findIndex((t) => t.id === modTask.id);
716
+ if (idx >= 0) {
717
+ this.activePlan.tactical[idx] = modTask;
718
+ }
719
+ else {
720
+ this.activePlan.tactical.push(modTask);
721
+ }
722
+ }
723
+ // 리플래닝 결과를 컨텍스트에 주입
724
+ this.contextManager.addMessage({
725
+ role: "system",
726
+ content: `[Re-plan] Strategy: ${result.strategy}. Reason: ${result.reason}.\nModified tasks: ${result.modifiedTasks.map((t) => t.description).join(", ")}`,
727
+ });
728
+ }
729
+ // Estimate replan token usage
730
+ this.tokenBudgetManager.recordUsage("planner", 500, 500);
731
+ this.emitEvent({
732
+ kind: "agent:thinking",
733
+ content: `Re-planned: ${result.strategy} — ${result.reason}`,
734
+ });
735
+ return true;
736
+ }
737
+ catch {
738
+ return false;
739
+ }
740
+ }
741
+ /**
742
+ * 현재 활성 플랜을 반환 (외부에서 진행 상황 조회용).
743
+ */
744
+ getActivePlan() {
745
+ return this.activePlan;
746
+ }
167
747
  // ─── Core Loop ───
168
748
  async executeLoop() {
169
749
  let iteration = 0;
@@ -189,10 +769,21 @@ export class AgentLoop extends EventEmitter {
189
769
  throw err;
190
770
  }
191
771
  iteration++;
772
+ this.iterationCount = iteration;
192
773
  const iterationStart = Date.now();
193
774
  // 1. 컨텍스트 준비
194
775
  const messages = this.contextManager.prepareForLLM();
195
776
  // 2. LLM 호출 (streaming)
777
+ // Before LLM call, check executor budget
778
+ const budgetCheck = this.tokenBudgetManager.canUse("executor", 4000);
779
+ if (!budgetCheck.allowed) {
780
+ this.emitEvent({
781
+ kind: "agent:thinking",
782
+ content: `Token budget warning: ${budgetCheck.reason}`,
783
+ });
784
+ // Try rebalancing to free up budget from idle roles
785
+ this.tokenBudgetManager.rebalance();
786
+ }
196
787
  this.emitEvent({
197
788
  kind: "agent:thinking",
198
789
  content: `Iteration ${iteration}...`,
@@ -212,11 +803,32 @@ export class AgentLoop extends EventEmitter {
212
803
  this.tokenUsage.output += response.usage.output;
213
804
  this.tokenUsage.total += response.usage.input + response.usage.output;
214
805
  this.governor.recordIteration(response.usage.input, response.usage.output);
806
+ // Role-based token budget 추적
807
+ this.tokenBudgetManager.recordUsage("executor", response.usage.input, response.usage.output);
808
+ // Cost tracking
809
+ this.costOptimizer.recordUsage(this.config.byok.model ?? "unknown", response.usage.input, response.usage.output, "executor");
810
+ // Rebalance budgets every 5 iterations to redistribute from idle roles
811
+ if (iteration % 5 === 0) {
812
+ this.tokenBudgetManager.rebalance();
813
+ }
215
814
  this.emitEvent({
216
815
  kind: "agent:token_usage",
217
816
  input: this.tokenUsage.input,
218
817
  output: this.tokenUsage.output,
219
818
  });
819
+ // LLM 응답 살균 — 간접 프롬프트 인젝션 방어
820
+ if (response.content) {
821
+ const llmSanitized = this.promptDefense.sanitizeToolOutput("llm_response", response.content);
822
+ if (llmSanitized.injectionDetected) {
823
+ this.emitEvent({
824
+ kind: "agent:error",
825
+ message: `Prompt injection detected in LLM response: ${llmSanitized.patternsFound.join(", ")}`,
826
+ retryable: false,
827
+ });
828
+ // 살균된 콘텐츠로 교체
829
+ response = { ...response, content: llmSanitized.output };
830
+ }
831
+ }
220
832
  // 3. 응답 처리
221
833
  if (response.toolCalls.length === 0) {
222
834
  const content = response.content ?? "";
@@ -250,10 +862,21 @@ export class AgentLoop extends EventEmitter {
250
862
  }
251
863
  // 4. 도구 실행
252
864
  const toolResults = await this.executeTools(response.toolCalls);
253
- // 5. 도구 결과를 히스토리에 추가
865
+ // Reflexion: 도구 결과 수집
866
+ this.allToolResults.push(...toolResults);
867
+ // 5. 도구 결과를 히스토리에 추가 (살균 + 압축)
254
868
  for (const result of toolResults) {
255
- // 결과는 압축
256
- const compressedOutput = this.contextManager.compressToolResult(result.name, result.output);
869
+ // Prompt injection 방어: 도구 출력 살균
870
+ const sanitized = this.promptDefense.sanitizeToolOutput(result.name, result.output);
871
+ if (sanitized.injectionDetected) {
872
+ this.emitEvent({
873
+ kind: "agent:error",
874
+ message: `Prompt injection detected in ${result.name} output: ${sanitized.patternsFound.join(", ")}`,
875
+ retryable: false,
876
+ });
877
+ }
878
+ // 큰 결과는 추가 압축
879
+ const compressedOutput = this.contextManager.compressToolResult(result.name, sanitized.output);
257
880
  this.contextManager.addMessage({
258
881
  role: "tool",
259
882
  content: compressedOutput,
@@ -261,12 +884,74 @@ export class AgentLoop extends EventEmitter {
261
884
  });
262
885
  }
263
886
  // iteration 이벤트
264
- const durationMs = Date.now() - iterationStart;
265
887
  this.emitEvent({
266
888
  kind: "agent:iteration",
267
889
  index: iteration,
268
890
  tokensUsed: response.usage.input + response.usage.output,
891
+ durationMs: Date.now() - iterationStart,
269
892
  });
893
+ // 에러가 많으면 FailureRecovery + 리플래닝 시도
894
+ const errorResults = toolResults.filter((r) => !r.success);
895
+ if (errorResults.length > 0) {
896
+ const errorSummary = errorResults
897
+ .map((r) => `${r.name}: ${r.output}`)
898
+ .join("\n");
899
+ // FailureRecovery: 근본 원인 분석 + 전략 선택
900
+ const rootCause = this.failureRecovery.analyzeRootCause(errorSummary, errorResults[0]?.name);
901
+ const decision = this.failureRecovery.selectStrategy(rootCause, {
902
+ error: errorSummary,
903
+ toolName: errorResults[0]?.name,
904
+ toolOutput: errorResults[0]?.output,
905
+ attemptNumber: iteration,
906
+ maxAttempts: this.config.loop.maxIterations,
907
+ changedFiles: this.changedFiles,
908
+ originalSnapshots: this.originalSnapshots,
909
+ previousStrategies: this.previousStrategies,
910
+ });
911
+ this.previousStrategies.push(decision.strategy);
912
+ this.failureRecovery.recordStrategyResult(decision.strategy, false);
913
+ if (decision.strategy === "escalate") {
914
+ // 에스컬레이션: 유저에게 도움 요청
915
+ this.emitEvent({
916
+ kind: "agent:error",
917
+ message: `Recovery escalated: ${decision.reason}`,
918
+ retryable: false,
919
+ });
920
+ }
921
+ else if (decision.strategy === "rollback") {
922
+ // 롤백 시도
923
+ await this.failureRecovery.executeRollback(this.changedFiles, this.originalSnapshots);
924
+ this.emitEvent({
925
+ kind: "agent:thinking",
926
+ content: `Rolled back changes. ${decision.reason}`,
927
+ });
928
+ }
929
+ else {
930
+ // retry, approach_change, scope_reduce → 복구 프롬프트 주입
931
+ const recoveryPrompt = this.failureRecovery.buildRecoveryPrompt(decision, {
932
+ error: errorSummary,
933
+ attemptNumber: iteration,
934
+ maxAttempts: this.config.loop.maxIterations,
935
+ changedFiles: this.changedFiles,
936
+ originalSnapshots: this.originalSnapshots,
937
+ previousStrategies: this.previousStrategies,
938
+ });
939
+ this.contextManager.addMessage({
940
+ role: "system",
941
+ content: recoveryPrompt,
942
+ });
943
+ }
944
+ // HierarchicalPlanner 리플래닝도 시도
945
+ if (this.activePlan) {
946
+ await this.attemptReplan(errorSummary);
947
+ }
948
+ }
949
+ // 체크포인트 저장: 토큰 예산 80% 이상 사용 시 자동 저장 (1회만)
950
+ if (!this.checkpointSaved &&
951
+ this.continuationEngine?.shouldCheckpoint(this.tokenUsage.total, this.config.loop.totalTokenBudget)) {
952
+ await this.saveAutoCheckpoint(iteration);
953
+ this.checkpointSaved = true;
954
+ }
270
955
  // 예산 초과 체크
271
956
  if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
272
957
  return {
@@ -287,7 +972,8 @@ export class AgentLoop extends EventEmitter {
287
972
  const toolCalls = [];
288
973
  let usage = { input: 0, output: 0 };
289
974
  let finishReason = "stop";
290
- const stream = this.llmClient.chatStream(messages, this.config.loop.tools);
975
+ const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions];
976
+ const stream = this.llmClient.chatStream(messages, allTools);
291
977
  for await (const chunk of stream) {
292
978
  if (this.aborted)
293
979
  break;
@@ -366,6 +1052,20 @@ export class AgentLoop extends EventEmitter {
366
1052
  }
367
1053
  // 승인됨 → 계속 실행
368
1054
  }
1055
+ // MCP 도구 호출 확인
1056
+ if (this.mcpClient && this.isMCPTool(toolCall.name)) {
1057
+ const mcpResult = await this.executeMCPTool(toolCall);
1058
+ results.push(mcpResult);
1059
+ this.emitEvent({
1060
+ kind: "agent:tool_result",
1061
+ tool: toolCall.name,
1062
+ output: mcpResult.output.length > 200
1063
+ ? mcpResult.output.slice(0, 200) + "..."
1064
+ : mcpResult.output,
1065
+ durationMs: mcpResult.durationMs,
1066
+ });
1067
+ continue;
1068
+ }
369
1069
  // 도구 실행 — AbortController를 InterruptManager에 등록
370
1070
  const startTime = Date.now();
371
1071
  const toolAbort = new AbortController();
@@ -382,17 +1082,37 @@ export class AgentLoop extends EventEmitter {
382
1082
  : result.output,
383
1083
  durationMs: result.durationMs,
384
1084
  });
385
- // 파일 변경 이벤트
1085
+ // 파일 변경 이벤트 + 추적
386
1086
  if (["file_write", "file_edit"].includes(toolCall.name) &&
387
1087
  result.success) {
388
1088
  const filePath = args.path ??
389
1089
  args.file ??
390
1090
  "unknown";
1091
+ const filePathStr = String(filePath);
1092
+ // 변경 파일 추적 (메모리 업데이트용)
1093
+ if (!this.changedFiles.includes(filePathStr)) {
1094
+ this.changedFiles.push(filePathStr);
1095
+ // 원본 스냅샷 저장 (rollback용) — 최초 변경 시에만
1096
+ if (!this.originalSnapshots.has(filePathStr)) {
1097
+ try {
1098
+ const { readFile } = await import("node:fs/promises");
1099
+ const original = await readFile(filePathStr, "utf-8");
1100
+ this.originalSnapshots.set(filePathStr, original);
1101
+ }
1102
+ catch {
1103
+ // 파일이 새로 생성된 경우 스냅샷 없음
1104
+ }
1105
+ }
1106
+ }
391
1107
  this.emitEvent({
392
1108
  kind: "agent:file_change",
393
- path: String(filePath),
1109
+ path: filePathStr,
394
1110
  diff: result.output,
395
1111
  });
1112
+ // ImpactAnalyzer: 변경 영향 분석 (비동기, 실패 무시)
1113
+ if (this.impactAnalyzer) {
1114
+ this.analyzeFileImpact(filePathStr).catch(() => { });
1115
+ }
396
1116
  }
397
1117
  // AutoFixLoop: 결과 검증
398
1118
  await this.validateAndFeedback(toolCall.name, result);
@@ -525,6 +1245,110 @@ export class AgentLoop extends EventEmitter {
525
1245
  }
526
1246
  return args;
527
1247
  }
1248
+ // ─── Continuation Helpers ───
1249
+ /**
1250
+ * 토큰 예산 소진 임박 시 자동 체크포인트를 저장한다.
1251
+ * 현재 진행 상태, 변경 파일, 에러 등을 직렬화.
1252
+ */
1253
+ async saveAutoCheckpoint(iteration) {
1254
+ if (!this.continuationEngine)
1255
+ return;
1256
+ try {
1257
+ // 현재 plan 정보에서 진행 상황 추출
1258
+ const progress = this.extractProgress();
1259
+ const checkpoint = {
1260
+ sessionId: crypto.randomUUID(),
1261
+ goal: this.contextManager.getMessages().find((m) => m.role === "user")?.content ?? "",
1262
+ progress,
1263
+ changedFiles: this.changedFiles.map((path) => ({ path, diff: "" })),
1264
+ workingMemory: this.buildWorkingMemorySummary(),
1265
+ yuanMdUpdates: [],
1266
+ errors: this.allToolResults
1267
+ .filter((r) => !r.success)
1268
+ .slice(-5)
1269
+ .map((r) => `${r.name}: ${r.output.slice(0, 200)}`),
1270
+ contextUsageAtSave: this.tokenUsage.total / this.config.loop.totalTokenBudget,
1271
+ totalTokensUsed: this.tokenUsage.total,
1272
+ iterationsCompleted: iteration,
1273
+ createdAt: new Date(),
1274
+ };
1275
+ const savedPath = await this.continuationEngine.saveCheckpoint(checkpoint);
1276
+ if (savedPath) {
1277
+ this.emitEvent({
1278
+ kind: "agent:thinking",
1279
+ content: `Auto-checkpoint saved at ${Math.round(checkpoint.contextUsageAtSave * 100)}% token usage (iteration ${iteration}).`,
1280
+ });
1281
+ }
1282
+ }
1283
+ catch {
1284
+ // 체크포인트 저장 실패는 치명적이지 않음
1285
+ }
1286
+ }
1287
+ /**
1288
+ * 현재 plan에서 진행 상황을 추출한다.
1289
+ */
1290
+ extractProgress() {
1291
+ if (!this.activePlan) {
1292
+ return { completedTasks: [], currentTask: "", remainingTasks: [] };
1293
+ }
1294
+ const tasks = this.activePlan.tactical;
1295
+ const completedTasks = tasks
1296
+ .slice(0, this.currentTaskIndex)
1297
+ .map((t) => t.description);
1298
+ const currentTask = tasks[this.currentTaskIndex]?.description ?? "";
1299
+ const remainingTasks = tasks
1300
+ .slice(this.currentTaskIndex + 1)
1301
+ .map((t) => t.description);
1302
+ return { completedTasks, currentTask, remainingTasks };
1303
+ }
1304
+ /**
1305
+ * 현재 작업 메모리 요약을 생성한다.
1306
+ * 최근 도구 결과와 LLM 응답의 핵심만 추출.
1307
+ */
1308
+ buildWorkingMemorySummary() {
1309
+ const parts = [];
1310
+ // 최근 도구 결과 요약 (최대 5개)
1311
+ const recentTools = this.allToolResults.slice(-5);
1312
+ if (recentTools.length > 0) {
1313
+ parts.push("Recent tool results:");
1314
+ for (const r of recentTools) {
1315
+ const status = r.success ? "OK" : "FAIL";
1316
+ parts.push(`- ${r.name} [${status}]: ${r.output.slice(0, 100)}`);
1317
+ }
1318
+ }
1319
+ // 변경된 파일 목록
1320
+ if (this.changedFiles.length > 0) {
1321
+ parts.push(`\nChanged files: ${this.changedFiles.join(", ")}`);
1322
+ }
1323
+ // 토큰 사용량
1324
+ parts.push(`\nTokens used: ${this.tokenUsage.total} / ${this.config.loop.totalTokenBudget}`);
1325
+ return parts.join("\n");
1326
+ }
1327
+ /**
1328
+ * ContinuationEngine 인스턴스를 반환한다.
1329
+ * 외부에서 체크포인트 조회/관리에 사용.
1330
+ */
1331
+ getContinuationEngine() {
1332
+ return this.continuationEngine;
1333
+ }
1334
+ /**
1335
+ * FailureRecovery 인스턴스를 반환한다.
1336
+ */
1337
+ getFailureRecovery() {
1338
+ return this.failureRecovery;
1339
+ }
1340
+ /**
1341
+ * ExecutionPolicyEngine 인스턴스를 반환한다.
1342
+ */
1343
+ getPolicyEngine() {
1344
+ return this.policyEngine;
1345
+ }
1346
+ /**
1347
+ * 마지막 수집된 WorldState를 반환한다.
1348
+ */
1349
+ getWorldState() {
1350
+ return this.worldState;
1351
+ }
528
1352
  // ─── Interrupt Helpers ───
529
1353
  /**
530
1354
  * InterruptManager 이벤트를 AgentLoop에 연결한다.
@@ -569,6 +1393,66 @@ export class AgentLoop extends EventEmitter {
569
1393
  this.interruptManager.on("interrupt:hard", onHard);
570
1394
  });
571
1395
  }
1396
+ // ─── Impact Analysis ───
1397
+ /**
1398
+ * 파일 변경 후 영향 분석을 실행하고 결과를 컨텍스트에 주입.
1399
+ * 고위험 변경이면 경고를 emit.
1400
+ */
1401
+ async analyzeFileImpact(filePath) {
1402
+ if (!this.impactAnalyzer)
1403
+ return;
1404
+ try {
1405
+ const report = await this.impactAnalyzer.analyzeChanges([filePath]);
1406
+ if (report.riskLevel === "high" || report.riskLevel === "critical") {
1407
+ this.emitEvent({
1408
+ kind: "agent:thinking",
1409
+ content: `Impact analysis: ${report.riskLevel} risk. ${report.affectedFiles.length} affected files, ${report.breakingChanges.length} breaking changes.`,
1410
+ });
1411
+ // 고위험 변경 정보를 LLM에 주입
1412
+ const impactPrompt = this.impactAnalyzer.formatForPrompt(report);
1413
+ this.contextManager.addMessage({
1414
+ role: "system",
1415
+ content: impactPrompt,
1416
+ });
1417
+ }
1418
+ }
1419
+ catch {
1420
+ // Impact analysis 실패는 치명적이지 않음
1421
+ }
1422
+ }
1423
+ /**
1424
+ * CostOptimizer 인스턴스를 반환한다.
1425
+ */
1426
+ getCostOptimizer() {
1427
+ return this.costOptimizer;
1428
+ }
1429
+ /**
1430
+ * ImpactAnalyzer 인스턴스를 반환한다.
1431
+ */
1432
+ getImpactAnalyzer() {
1433
+ return this.impactAnalyzer;
1434
+ }
1435
+ // ─── MCP Helpers ───
1436
+ /** MCP 도구인지 확인 */
1437
+ isMCPTool(toolName) {
1438
+ return this.mcpToolDefinitions.some((t) => t.name === toolName);
1439
+ }
1440
+ /** MCP 도구 실행 (callToolAsYuan 활용) */
1441
+ async executeMCPTool(toolCall) {
1442
+ const args = this.parseToolArgs(toolCall.arguments);
1443
+ return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
1444
+ }
1445
+ /** MCP 클라이언트 정리 (세션 종료 시 호출) */
1446
+ async dispose() {
1447
+ if (this.mcpClient) {
1448
+ try {
1449
+ await this.mcpClient.disconnectAll();
1450
+ }
1451
+ catch {
1452
+ // cleanup failure ignored
1453
+ }
1454
+ }
1455
+ }
572
1456
  // ─── Helpers ───
573
1457
  emitEvent(event) {
574
1458
  this.emit("event", event);