@yuaone/core 0.1.3 → 0.2.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 (63) hide show
  1. package/dist/agent-loop.d.ts +112 -0
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +715 -9
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +8 -0
  7. package/dist/constants.js.map +1 -1
  8. package/dist/context-budget.d.ts +1 -1
  9. package/dist/context-budget.d.ts.map +1 -1
  10. package/dist/context-budget.js +4 -2
  11. package/dist/context-budget.js.map +1 -1
  12. package/dist/context-compressor.d.ts +1 -1
  13. package/dist/context-compressor.d.ts.map +1 -1
  14. package/dist/context-compressor.js +5 -3
  15. package/dist/context-compressor.js.map +1 -1
  16. package/dist/context-manager.d.ts +7 -1
  17. package/dist/context-manager.d.ts.map +1 -1
  18. package/dist/context-manager.js +34 -2
  19. package/dist/context-manager.js.map +1 -1
  20. package/dist/continuation-engine.d.ts +168 -0
  21. package/dist/continuation-engine.d.ts.map +1 -0
  22. package/dist/continuation-engine.js +421 -0
  23. package/dist/continuation-engine.js.map +1 -0
  24. package/dist/execution-engine.d.ts.map +1 -1
  25. package/dist/execution-engine.js +9 -4
  26. package/dist/execution-engine.js.map +1 -1
  27. package/dist/index.d.ts +14 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/llm-client.d.ts +1 -1
  32. package/dist/llm-client.d.ts.map +1 -1
  33. package/dist/llm-client.js +74 -7
  34. package/dist/llm-client.js.map +1 -1
  35. package/dist/memory-updater.d.ts +189 -0
  36. package/dist/memory-updater.d.ts.map +1 -0
  37. package/dist/memory-updater.js +481 -0
  38. package/dist/memory-updater.js.map +1 -0
  39. package/dist/prompt-defense.d.ts +59 -0
  40. package/dist/prompt-defense.d.ts.map +1 -0
  41. package/dist/prompt-defense.js +311 -0
  42. package/dist/prompt-defense.js.map +1 -0
  43. package/dist/reflexion.d.ts +211 -0
  44. package/dist/reflexion.d.ts.map +1 -0
  45. package/dist/reflexion.js +559 -0
  46. package/dist/reflexion.js.map +1 -0
  47. package/dist/system-prompt.d.ts +19 -3
  48. package/dist/system-prompt.d.ts.map +1 -1
  49. package/dist/system-prompt.js +203 -38
  50. package/dist/system-prompt.js.map +1 -1
  51. package/dist/task-classifier.d.ts +92 -0
  52. package/dist/task-classifier.d.ts.map +1 -0
  53. package/dist/task-classifier.js +566 -0
  54. package/dist/task-classifier.js.map +1 -0
  55. package/dist/token-budget.d.ts +131 -0
  56. package/dist/token-budget.d.ts.map +1 -0
  57. package/dist/token-budget.js +321 -0
  58. package/dist/token-budget.js.map +1 -0
  59. package/dist/types.d.ts +20 -2
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js +18 -1
  62. package/dist/types.js.map +1 -1
  63. package/package.json +1 -1
@@ -14,6 +14,17 @@ 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";
17
28
  /**
18
29
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
19
30
  *
@@ -50,7 +61,30 @@ export class AgentLoop extends EventEmitter {
50
61
  approvalManager;
51
62
  autoFixLoop;
52
63
  interruptManager;
64
+ enableMemory;
65
+ enablePlanning;
66
+ planningThreshold;
67
+ environment;
68
+ yuanMemory = null;
69
+ memoryManager = null;
70
+ planner = null;
71
+ activePlan = null;
72
+ taskClassifier;
73
+ promptDefense;
74
+ reflexionEngine = null;
75
+ tokenBudgetManager;
76
+ allToolResults = [];
77
+ currentTaskIndex = 0;
78
+ changedFiles = [];
53
79
  aborted = false;
80
+ initialized = false;
81
+ continuationEngine = null;
82
+ mcpClient = null;
83
+ mcpToolDefinitions = [];
84
+ mcpServerConfigs;
85
+ memoryUpdater;
86
+ checkpointSaved = false;
87
+ iterationCount = 0;
54
88
  tokenUsage = {
55
89
  input: 0,
56
90
  output: 0,
@@ -61,6 +95,10 @@ export class AgentLoop extends EventEmitter {
61
95
  super();
62
96
  this.config = options.config;
63
97
  this.toolExecutor = options.toolExecutor;
98
+ this.enableMemory = options.enableMemory !== false;
99
+ this.enablePlanning = options.enablePlanning !== false;
100
+ this.planningThreshold = options.planningThreshold ?? "moderate";
101
+ this.environment = options.environment;
64
102
  // BYOK LLM 클라이언트 생성
65
103
  this.llmClient = new BYOKClient(options.config.byok);
66
104
  // Governor 생성
@@ -79,22 +117,181 @@ export class AgentLoop extends EventEmitter {
79
117
  }
80
118
  // AutoFixLoop 생성
81
119
  this.autoFixLoop = new AutoFixLoop(options.autoFixConfig);
120
+ // Task Classifier + Prompt Defense + Token Budget + Memory Updater
121
+ this.taskClassifier = new TaskClassifier();
122
+ this.promptDefense = new PromptDefense();
123
+ this.tokenBudgetManager = new TokenBudgetManager({
124
+ totalBudget: options.config.loop.totalTokenBudget,
125
+ });
126
+ this.memoryUpdater = new MemoryUpdater();
127
+ // MCP 서버 설정 저장
128
+ this.mcpServerConfigs = options.mcpServerConfigs ?? [];
82
129
  // InterruptManager 설정 (외부 주입 또는 내부 생성)
83
130
  this.interruptManager = options.interruptManager ?? new InterruptManager();
84
131
  this.setupInterruptListeners();
85
- // 시스템 프롬프트 추가
132
+ // 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
86
133
  this.contextManager.addMessage({
87
134
  role: "system",
88
135
  content: this.config.loop.systemPrompt,
89
136
  });
90
137
  }
138
+ /**
139
+ * Memory와 프로젝트 컨텍스트를 로드하여 시스템 프롬프트를 갱신.
140
+ * run() 호출 전에 한 번 호출하면 메모리가 자동으로 주입된다.
141
+ * 이미 초기화되었으면 스킵.
142
+ */
143
+ async init() {
144
+ if (this.initialized)
145
+ return;
146
+ this.initialized = true;
147
+ const projectPath = this.config.loop.projectPath;
148
+ if (!projectPath)
149
+ return;
150
+ let yuanMdContent;
151
+ let projectStructure;
152
+ // Memory 로드
153
+ if (this.enableMemory) {
154
+ try {
155
+ // YUAN.md (raw markdown)
156
+ this.yuanMemory = new YuanMemory(projectPath);
157
+ const memData = await this.yuanMemory.load();
158
+ if (memData) {
159
+ yuanMdContent = memData.raw;
160
+ }
161
+ // MemoryManager (structured learnings)
162
+ this.memoryManager = new MemoryManager(projectPath);
163
+ await this.memoryManager.load();
164
+ // 프로젝트 구조 분석
165
+ projectStructure = await this.yuanMemory.analyzeProject();
166
+ }
167
+ catch (memErr) {
168
+ // 메모리 로드 실패는 치명적이지 않음 — 경고만 출력
169
+ this.emitEvent({
170
+ kind: "agent:error",
171
+ message: `Memory load failed: ${memErr instanceof Error ? memErr.message : String(memErr)}`,
172
+ retryable: false,
173
+ });
174
+ }
175
+ }
176
+ // ContinuationEngine 생성
177
+ this.continuationEngine = new ContinuationEngine({ projectPath });
178
+ // 이전 세션 체크포인트 복원
179
+ try {
180
+ const latestCheckpoint = await this.continuationEngine.findLatestCheckpoint();
181
+ if (latestCheckpoint) {
182
+ const continuationPrompt = this.continuationEngine.formatContinuationPrompt(latestCheckpoint);
183
+ this.contextManager.addMessage({
184
+ role: "system",
185
+ content: continuationPrompt,
186
+ });
187
+ // 복원 후 체크포인트 정리
188
+ await this.continuationEngine.pruneOldCheckpoints();
189
+ }
190
+ }
191
+ catch {
192
+ // 체크포인트 복원 실패는 치명적이지 않음
193
+ }
194
+ // MCP 클라이언트 연결
195
+ if (this.mcpServerConfigs.length > 0) {
196
+ try {
197
+ this.mcpClient = new MCPClient({
198
+ servers: this.mcpServerConfigs,
199
+ });
200
+ await this.mcpClient.connectAll();
201
+ this.mcpToolDefinitions = this.mcpClient.toToolDefinitions();
202
+ }
203
+ catch {
204
+ // MCP 연결 실패는 치명적이지 않음 — 로컬 도구만 사용
205
+ this.mcpClient = null;
206
+ this.mcpToolDefinitions = [];
207
+ }
208
+ }
209
+ // HierarchicalPlanner 생성
210
+ if (this.enablePlanning && projectPath) {
211
+ this.planner = new HierarchicalPlanner({ projectPath });
212
+ }
213
+ // ReflexionEngine 생성
214
+ if (projectPath) {
215
+ this.reflexionEngine = new ReflexionEngine({ projectPath });
216
+ }
217
+ // 향상된 시스템 프롬프트 생성
218
+ const enhancedPrompt = buildSystemPrompt({
219
+ projectStructure,
220
+ yuanMdContent,
221
+ tools: [...this.config.loop.tools, ...this.mcpToolDefinitions],
222
+ projectPath,
223
+ environment: this.environment,
224
+ });
225
+ // 기존 시스템 메시지를 향상된 프롬프트로 교체
226
+ this.contextManager.replaceSystemMessage(enhancedPrompt);
227
+ // MemoryManager의 관련 학습/경고를 추가 컨텍스트로 주입
228
+ if (this.memoryManager) {
229
+ const memory = this.memoryManager.getMemory();
230
+ if (memory.learnings.length > 0 || memory.failedApproaches.length > 0) {
231
+ const memoryContext = this.buildMemoryContext(memory);
232
+ if (memoryContext) {
233
+ this.contextManager.addMessage({
234
+ role: "system",
235
+ content: memoryContext,
236
+ });
237
+ }
238
+ }
239
+ }
240
+ }
241
+ /**
242
+ * MemoryManager의 학습/실패 기록을 시스템 메시지로 변환.
243
+ */
244
+ buildMemoryContext(memory) {
245
+ const parts = [];
246
+ // 높은 confidence 학습만 포함
247
+ const highConfLearnings = memory.learnings.filter((l) => l.confidence >= 0.3);
248
+ if (highConfLearnings.length > 0) {
249
+ parts.push("## Things I've Learned About This Project");
250
+ for (const l of highConfLearnings.slice(0, 20)) {
251
+ parts.push(`- [${l.category}] ${l.content}`);
252
+ }
253
+ }
254
+ // 실패한 접근 방식 (최근 10개)
255
+ if (memory.failedApproaches.length > 0) {
256
+ parts.push("\n## Approaches That Failed Before (Avoid These)");
257
+ for (const f of memory.failedApproaches.slice(0, 10)) {
258
+ parts.push(`- **${f.approach}** — failed because: ${f.reason}`);
259
+ }
260
+ }
261
+ // 코딩 규칙
262
+ if (memory.conventions.length > 0) {
263
+ parts.push("\n## Project Conventions");
264
+ for (const c of memory.conventions) {
265
+ parts.push(`- ${c}`);
266
+ }
267
+ }
268
+ return parts.length > 0 ? parts.join("\n") : null;
269
+ }
91
270
  /**
92
271
  * 에이전트 루프를 실행.
272
+ * 첫 호출 시 자동으로 Memory와 프로젝트 컨텍스트를 로드한다.
93
273
  * @param userMessage 사용자의 요청 메시지
94
274
  * @returns 종료 사유 및 결과
95
275
  */
96
276
  async run(userMessage) {
97
277
  this.aborted = false;
278
+ this.changedFiles = [];
279
+ this.allToolResults = [];
280
+ this.checkpointSaved = false;
281
+ this.iterationCount = 0;
282
+ this.tokenBudgetManager.reset();
283
+ const runStartTime = Date.now();
284
+ // 첫 실행 시 메모리/프로젝트 컨텍스트 자동 로드
285
+ await this.init();
286
+ // 사용자 입력 검증 (prompt injection 방어)
287
+ const inputValidation = this.promptDefense.validateUserInput(userMessage);
288
+ if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
289
+ this.emitEvent({
290
+ kind: "agent:error",
291
+ message: `Prompt injection detected in user input (${inputValidation.severity}): ${inputValidation.patternsFound.join(", ")}`,
292
+ retryable: false,
293
+ });
294
+ }
98
295
  // 사용자 메시지 추가
99
296
  this.contextManager.addMessage({
100
297
  role: "user",
@@ -102,12 +299,119 @@ export class AgentLoop extends EventEmitter {
102
299
  });
103
300
  this.emitEvent({ kind: "agent:start", goal: userMessage });
104
301
  try {
105
- return await this.executeLoop();
302
+ // Reflexion: 과거 실행에서 배운 가이던스 주입
303
+ if (this.reflexionEngine) {
304
+ try {
305
+ const guidance = await this.reflexionEngine.getGuidance(userMessage);
306
+ // 가이던스 유효성 검증: 빈 전략이나 매우 낮은 confidence 필터링
307
+ const validStrategies = guidance.relevantStrategies.filter((s) => s.strategy && s.strategy.length > 5 && s.confidence > 0.1);
308
+ if (validStrategies.length > 0 || guidance.recentFailures.length > 0) {
309
+ const filteredGuidance = { ...guidance, relevantStrategies: validStrategies };
310
+ const guidancePrompt = this.reflexionEngine.formatForSystemPrompt(filteredGuidance);
311
+ this.contextManager.addMessage({
312
+ role: "system",
313
+ content: guidancePrompt,
314
+ });
315
+ }
316
+ }
317
+ catch {
318
+ // guidance 로드 실패는 치명적이지 않음
319
+ }
320
+ }
321
+ // Task 분류 → 시스템 프롬프트에 tool sequence hint 주입
322
+ const classification = this.taskClassifier.classify(userMessage);
323
+ if (classification.confidence >= 0.3) {
324
+ const classificationHint = this.taskClassifier.formatForSystemPrompt(classification);
325
+ this.contextManager.addMessage({
326
+ role: "system",
327
+ content: classificationHint,
328
+ });
329
+ }
330
+ // 복잡도 감지 → 필요 시 자동 플래닝
331
+ await this.maybeCreatePlan(userMessage);
332
+ const result = await this.executeLoop();
333
+ // 실행 완료 후 메모리 자동 업데이트
334
+ await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
335
+ return result;
106
336
  }
107
337
  catch (err) {
108
338
  return this.handleFatalError(err);
109
339
  }
110
340
  }
341
+ /**
342
+ * 에이전트 실행 완료 후 메모리를 자동 업데이트한다.
343
+ * - 변경된 파일 목록 기록
344
+ * - 성공/실패 패턴 학습
345
+ */
346
+ async updateMemoryAfterRun(userGoal, result, runDurationMs = 0) {
347
+ if (!this.enableMemory || !this.memoryManager)
348
+ return;
349
+ try {
350
+ // MemoryUpdater로 풍부한 학습 추출
351
+ const analysis = this.memoryUpdater.analyzeRun({
352
+ goal: userGoal,
353
+ termination: {
354
+ reason: result.reason,
355
+ error: result.error,
356
+ summary: result.summary,
357
+ },
358
+ toolResults: this.allToolResults.map((r) => ({
359
+ name: r.name,
360
+ output: r.output,
361
+ success: r.success,
362
+ durationMs: r.durationMs,
363
+ })),
364
+ changedFiles: this.changedFiles,
365
+ messages: this.contextManager.getMessages().map((m) => ({
366
+ role: m.role,
367
+ content: typeof m.content === "string" ? m.content : null,
368
+ })),
369
+ tokensUsed: this.tokenUsage.total,
370
+ durationMs: runDurationMs,
371
+ iterations: this.iterationCount,
372
+ });
373
+ // 추출된 학습을 MemoryManager에 저장
374
+ const learnings = this.memoryUpdater.extractLearnings(analysis, userGoal);
375
+ for (const learning of learnings) {
376
+ this.memoryManager.addLearning(learning.category, learning.content);
377
+ }
378
+ // 에러로 종료된 경우 실패 기록도 추가
379
+ if (result.reason === "ERROR") {
380
+ this.memoryManager.addFailedApproach(`Task: ${userGoal.slice(0, 80)}`, result.error ?? "Unknown error");
381
+ }
382
+ // 메모리 저장
383
+ await this.memoryManager.save();
384
+ }
385
+ catch {
386
+ // 메모리 저장 실패는 치명적이지 않음
387
+ }
388
+ // Reflexion: 실행 결과 반영 + 전략 추출
389
+ if (this.reflexionEngine) {
390
+ try {
391
+ const entry = this.reflexionEngine.reflect({
392
+ goal: userGoal,
393
+ runId: crypto.randomUUID(),
394
+ termination: result,
395
+ toolResults: this.allToolResults,
396
+ messages: this.contextManager.getMessages(),
397
+ tokensUsed: this.tokenUsage.total,
398
+ durationMs: runDurationMs,
399
+ changedFiles: this.changedFiles,
400
+ });
401
+ await this.reflexionEngine.store.saveReflection(entry);
402
+ // 성공 시 전략 추출
403
+ if (entry.outcome === "success") {
404
+ const strategy = this.reflexionEngine.extractStrategy(entry, userGoal);
405
+ if (strategy) {
406
+ await this.reflexionEngine.store.saveStrategy(strategy);
407
+ }
408
+ }
409
+ }
410
+ catch {
411
+ // reflexion 저장 실패는 치명적이지 않음
412
+ }
413
+ }
414
+ }
111
415
  /**
112
416
  * 실행 중인 루프를 중단.
113
417
  */
@@ -164,6 +468,226 @@ export class AgentLoop extends EventEmitter {
164
468
  getAutoFixLoop() {
165
469
  return this.autoFixLoop;
166
470
  }
471
+ /**
472
+ * TokenBudgetManager 인스턴스를 반환.
473
+ * 역할별 토큰 사용량 조회/리밸런싱에 사용.
474
+ */
475
+ getTokenBudgetManager() {
476
+ return this.tokenBudgetManager;
477
+ }
478
+ // ─── Planning ───
479
+ /**
480
+ * 사용자 메시지의 복잡도를 감지하고, 복잡한 태스크이면 계획을 수립하여 컨텍스트에 주입.
481
+ *
482
+ * 복잡도 판단 기준:
483
+ * - 메시지 길이, 파일/작업 수 언급, 키워드 패턴
484
+ * - "trivial"/"simple" → 플래닝 스킵 (LLM이 직접 처리)
485
+ * - "moderate" 이상 → HierarchicalPlanner로 L1+L2 계획 수립
486
+ */
487
+ async maybeCreatePlan(userMessage) {
488
+ if (!this.planner || !this.enablePlanning)
489
+ return;
490
+ const complexity = this.detectComplexity(userMessage);
491
+ // 임계값 미만이면 플래닝 스킵
492
+ const thresholdOrder = { simple: 1, moderate: 2, complex: 3 };
493
+ const complexityOrder = {
494
+ trivial: 0, simple: 1, moderate: 2, complex: 3, massive: 4,
495
+ };
496
+ if ((complexityOrder[complexity] ?? 0) < thresholdOrder[this.planningThreshold]) {
497
+ return;
498
+ }
499
+ this.emitEvent({
500
+ kind: "agent:thinking",
501
+ content: `Task complexity: ${complexity}. Creating execution plan...`,
502
+ });
503
+ try {
504
+ const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
505
+ this.activePlan = plan;
506
+ this.currentTaskIndex = 0;
507
+ // Estimate planner token usage (plan creation typically uses ~500 tokens per task)
508
+ const planTokenEstimate = plan.tactical.length * 500;
509
+ this.tokenBudgetManager.recordUsage("planner", planTokenEstimate, planTokenEstimate);
510
+ // 계획을 컨텍스트에 주입 (LLM이 따라갈 수 있도록)
511
+ const planContext = this.formatPlanForContext(plan);
512
+ this.contextManager.addMessage({
513
+ role: "system",
514
+ content: planContext,
515
+ });
516
+ this.emitEvent({
517
+ kind: "agent:thinking",
518
+ content: `Plan created: ${plan.tactical.length} tasks, ${plan.totalEstimatedIterations} estimated iterations. Risk: ${plan.strategic.riskAssessment.level}.`,
519
+ });
520
+ }
521
+ catch {
522
+ // 플래닝 실패는 치명적이지 않음 — LLM이 직접 처리하도록 폴백
523
+ this.activePlan = null;
524
+ }
525
+ }
526
+ /**
527
+ * 사용자 메시지에서 태스크 복잡도를 휴리스틱으로 추정.
528
+ * LLM 호출 없이 빠르게 결정 (토큰 절약).
529
+ */
530
+ detectComplexity(message) {
531
+ const lower = message.toLowerCase();
532
+ const len = message.length;
533
+ // 복잡도 점수 계산
534
+ let score = 0;
535
+ // 길이 기반
536
+ if (len > 500)
537
+ score += 2;
538
+ else if (len > 200)
539
+ score += 1;
540
+ // 다중 파일/작업 키워드
541
+ const multiFileKeywords = [
542
+ "refactor", "리팩토링", "리팩터",
543
+ "migrate", "마이그레이션",
544
+ "모든 파일", "all files", "전체",
545
+ "여러 파일", "multiple files",
546
+ "아키텍처", "architecture",
547
+ "시스템", "system-wide",
548
+ ];
549
+ for (const kw of multiFileKeywords) {
550
+ if (lower.includes(kw)) {
551
+ score += 2;
552
+ break;
553
+ }
554
+ }
555
+ // 여러 작업 나열 (1. 2. 3. 또는 - 으로 나열)
556
+ const listItems = message.match(/(?:^|\n)\s*(?:\d+[.)]\s|-\s)/gm);
557
+ if (listItems && listItems.length >= 3)
558
+ score += 2;
559
+ else if (listItems && listItems.length >= 2)
560
+ score += 1;
561
+ // 파일 경로 패턴
562
+ const filePaths = message.match(/\b[\w\-./]+\.[a-z]{1,4}\b/g);
563
+ if (filePaths && filePaths.length >= 5)
564
+ score += 2;
565
+ else if (filePaths && filePaths.length >= 2)
566
+ score += 1;
567
+ // 간단한 작업 키워드 (감점)
568
+ const simpleKeywords = [
569
+ "fix", "고쳐", "수정해",
570
+ "rename", "이름 바꿔",
571
+ "한 줄", "one line",
572
+ "간단", "simple", "quick",
573
+ ];
574
+ for (const kw of simpleKeywords) {
575
+ if (lower.includes(kw)) {
576
+ score -= 1;
577
+ break;
578
+ }
579
+ }
580
+ // 점수 → 복잡도
581
+ if (score <= 0)
582
+ return "trivial";
583
+ if (score <= 1)
584
+ return "simple";
585
+ if (score <= 3)
586
+ return "moderate";
587
+ if (score <= 5)
588
+ return "complex";
589
+ return "massive";
590
+ }
591
+ /**
592
+ * HierarchicalPlan을 LLM이 따라갈 수 있는 컨텍스트 메시지로 포맷.
593
+ */
594
+ formatPlanForContext(plan) {
595
+ const parts = [];
596
+ parts.push("## Execution Plan");
597
+ parts.push(`**Goal:** ${plan.goal}`);
598
+ parts.push(`**Complexity:** ${plan.strategic.estimatedComplexity}`);
599
+ parts.push(`**Risk:** ${plan.strategic.riskAssessment.level}`);
600
+ if (plan.strategic.riskAssessment.requiresApproval) {
601
+ parts.push("**⚠ Requires user approval for high-risk operations.**");
602
+ }
603
+ parts.push("\n### Tasks (execute in order):");
604
+ for (let i = 0; i < plan.tactical.length; i++) {
605
+ const task = plan.tactical[i];
606
+ const deps = task.dependsOn.length > 0
607
+ ? ` (after: ${task.dependsOn.join(", ")})`
608
+ : "";
609
+ parts.push(`${i + 1}. **${task.description}**${deps}`);
610
+ if (task.targetFiles.length > 0) {
611
+ parts.push(` Files: ${task.targetFiles.join(", ")}`);
612
+ }
613
+ if (task.readFiles.length > 0) {
614
+ parts.push(` Read: ${task.readFiles.join(", ")}`);
615
+ }
616
+ parts.push(` Tools: ${task.toolStrategy.join(", ")}`);
617
+ }
618
+ if (plan.strategic.riskAssessment.mitigations.length > 0) {
619
+ parts.push("\n### Risk Mitigations:");
620
+ for (const m of plan.strategic.riskAssessment.mitigations) {
621
+ parts.push(`- ${m}`);
622
+ }
623
+ }
624
+ parts.push("\n### Execution Instructions:");
625
+ parts.push("- Follow the task order above. Complete each task before moving to the next.");
626
+ parts.push("- Read target files before modifying them.");
627
+ parts.push("- If a task fails, report the error and attempt an alternative approach.");
628
+ parts.push("- After all tasks, verify the changes work correctly.");
629
+ return parts.join("\n");
630
+ }
631
+ /**
632
+ * 실행 중 에러 발생 시 리플래닝을 시도한다.
633
+ * @returns 리플래닝 성공 시 true (계속 진행), 실패 시 false
634
+ */
635
+ async attemptReplan(error, failedTaskId) {
636
+ if (!this.planner || !this.activePlan)
637
+ return false;
638
+ const trigger = {
639
+ type: "error",
640
+ description: error,
641
+ affectedTaskIds: failedTaskId ? [failedTaskId] : [],
642
+ severity: "major",
643
+ };
644
+ try {
645
+ const result = await this.planner.replan(this.activePlan, trigger, this.llmClient);
646
+ if (result.strategy === "escalate") {
647
+ // 에스컬레이션 → 유저에게 알림
648
+ this.emitEvent({
649
+ kind: "agent:error",
650
+ message: `Re-plan escalated: ${result.reason}`,
651
+ retryable: false,
652
+ });
653
+ return false;
654
+ }
655
+ // 수정된 태스크로 업데이트
656
+ if (result.modifiedTasks.length > 0) {
657
+ // 기존 tactical 태스크를 교체
658
+ for (const modTask of result.modifiedTasks) {
659
+ const idx = this.activePlan.tactical.findIndex((t) => t.id === modTask.id);
660
+ if (idx >= 0) {
661
+ this.activePlan.tactical[idx] = modTask;
662
+ }
663
+ else {
664
+ this.activePlan.tactical.push(modTask);
665
+ }
666
+ }
667
+ // 리플래닝 결과를 컨텍스트에 주입
668
+ this.contextManager.addMessage({
669
+ role: "system",
670
+ content: `[Re-plan] Strategy: ${result.strategy}. Reason: ${result.reason}.\nModified tasks: ${result.modifiedTasks.map((t) => t.description).join(", ")}`,
671
+ });
672
+ }
673
+ // Estimate replan token usage
674
+ this.tokenBudgetManager.recordUsage("planner", 500, 500);
675
+ this.emitEvent({
676
+ kind: "agent:thinking",
677
+ content: `Re-planned: ${result.strategy} — ${result.reason}`,
678
+ });
679
+ return true;
680
+ }
681
+ catch {
682
+ return false;
683
+ }
684
+ }
685
+ /**
686
+ * 현재 활성 플랜을 반환 (외부에서 진행 상황 조회용).
687
+ */
688
+ getActivePlan() {
689
+ return this.activePlan;
690
+ }
167
691
  // ─── Core Loop ───
168
692
  async executeLoop() {
169
693
  let iteration = 0;
@@ -189,10 +713,21 @@ export class AgentLoop extends EventEmitter {
189
713
  throw err;
190
714
  }
191
715
  iteration++;
716
+ this.iterationCount = iteration;
192
717
  const iterationStart = Date.now();
193
718
  // 1. 컨텍스트 준비
194
719
  const messages = this.contextManager.prepareForLLM();
195
720
  // 2. LLM 호출 (streaming)
721
+ // Before LLM call, check executor budget
722
+ const budgetCheck = this.tokenBudgetManager.canUse("executor", 4000);
723
+ if (!budgetCheck.allowed) {
724
+ this.emitEvent({
725
+ kind: "agent:thinking",
726
+ content: `Token budget warning: ${budgetCheck.reason}`,
727
+ });
728
+ // Try rebalancing to free up budget from idle roles
729
+ this.tokenBudgetManager.rebalance();
730
+ }
196
731
  this.emitEvent({
197
732
  kind: "agent:thinking",
198
733
  content: `Iteration ${iteration}...`,
@@ -212,11 +747,30 @@ export class AgentLoop extends EventEmitter {
212
747
  this.tokenUsage.output += response.usage.output;
213
748
  this.tokenUsage.total += response.usage.input + response.usage.output;
214
749
  this.governor.recordIteration(response.usage.input, response.usage.output);
750
+ // Role-based token budget 추적
751
+ this.tokenBudgetManager.recordUsage("executor", response.usage.input, response.usage.output);
752
+ // Rebalance budgets every 5 iterations to redistribute from idle roles
753
+ if (iteration % 5 === 0) {
754
+ this.tokenBudgetManager.rebalance();
755
+ }
215
756
  this.emitEvent({
216
757
  kind: "agent:token_usage",
217
758
  input: this.tokenUsage.input,
218
759
  output: this.tokenUsage.output,
219
760
  });
761
+ // LLM 응답 살균 — 간접 프롬프트 인젝션 방어
762
+ if (response.content) {
763
+ const llmSanitized = this.promptDefense.sanitizeToolOutput("llm_response", response.content);
764
+ if (llmSanitized.injectionDetected) {
765
+ this.emitEvent({
766
+ kind: "agent:error",
767
+ message: `Prompt injection detected in LLM response: ${llmSanitized.patternsFound.join(", ")}`,
768
+ retryable: false,
769
+ });
770
+ // 살균된 콘텐츠로 교체
771
+ response = { ...response, content: llmSanitized.output };
772
+ }
773
+ }
220
774
  // 3. 응답 처리
221
775
  if (response.toolCalls.length === 0) {
222
776
  const content = response.content ?? "";
@@ -250,10 +804,21 @@ export class AgentLoop extends EventEmitter {
250
804
  }
251
805
  // 4. 도구 실행
252
806
  const toolResults = await this.executeTools(response.toolCalls);
253
- // 5. 도구 결과를 히스토리에 추가
807
+ // Reflexion: 도구 결과 수집
808
+ this.allToolResults.push(...toolResults);
809
+ // 5. 도구 결과를 히스토리에 추가 (살균 + 압축)
254
810
  for (const result of toolResults) {
255
- // 결과는 압축
256
- const compressedOutput = this.contextManager.compressToolResult(result.name, result.output);
811
+ // Prompt injection 방어: 도구 출력 살균
812
+ const sanitized = this.promptDefense.sanitizeToolOutput(result.name, result.output);
813
+ if (sanitized.injectionDetected) {
814
+ this.emitEvent({
815
+ kind: "agent:error",
816
+ message: `Prompt injection detected in ${result.name} output: ${sanitized.patternsFound.join(", ")}`,
817
+ retryable: false,
818
+ });
819
+ }
820
+ // 큰 결과는 추가 압축
821
+ const compressedOutput = this.contextManager.compressToolResult(result.name, sanitized.output);
257
822
  this.contextManager.addMessage({
258
823
  role: "tool",
259
824
  content: compressedOutput,
@@ -261,12 +826,26 @@ export class AgentLoop extends EventEmitter {
261
826
  });
262
827
  }
263
828
  // iteration 이벤트
264
- const durationMs = Date.now() - iterationStart;
265
829
  this.emitEvent({
266
830
  kind: "agent:iteration",
267
831
  index: iteration,
268
832
  tokensUsed: response.usage.input + response.usage.output,
833
+ durationMs: Date.now() - iterationStart,
269
834
  });
835
+ // 에러가 많으면 리플래닝 시도
836
+ const errorResults = toolResults.filter((r) => !r.success);
837
+ if (errorResults.length > 0 && this.activePlan) {
838
+ const errorSummary = errorResults
839
+ .map((r) => `${r.name}: ${r.output}`)
840
+ .join("\n");
841
+ await this.attemptReplan(errorSummary);
842
+ }
843
+ // 체크포인트 저장: 토큰 예산 80% 이상 사용 시 자동 저장 (1회만)
844
+ if (!this.checkpointSaved &&
845
+ this.continuationEngine?.shouldCheckpoint(this.tokenUsage.total, this.config.loop.totalTokenBudget)) {
846
+ await this.saveAutoCheckpoint(iteration);
847
+ this.checkpointSaved = true;
848
+ }
270
849
  // 예산 초과 체크
271
850
  if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
272
851
  return {
@@ -287,7 +866,8 @@ export class AgentLoop extends EventEmitter {
287
866
  const toolCalls = [];
288
867
  let usage = { input: 0, output: 0 };
289
868
  let finishReason = "stop";
290
- const stream = this.llmClient.chatStream(messages, this.config.loop.tools);
869
+ const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions];
870
+ const stream = this.llmClient.chatStream(messages, allTools);
291
871
  for await (const chunk of stream) {
292
872
  if (this.aborted)
293
873
  break;
@@ -366,6 +946,20 @@ export class AgentLoop extends EventEmitter {
366
946
  }
367
947
  // 승인됨 → 계속 실행
368
948
  }
949
+ // MCP 도구 호출 확인
950
+ if (this.mcpClient && this.isMCPTool(toolCall.name)) {
951
+ const mcpResult = await this.executeMCPTool(toolCall);
952
+ results.push(mcpResult);
953
+ this.emitEvent({
954
+ kind: "agent:tool_result",
955
+ tool: toolCall.name,
956
+ output: mcpResult.output.length > 200
957
+ ? mcpResult.output.slice(0, 200) + "..."
958
+ : mcpResult.output,
959
+ durationMs: mcpResult.durationMs,
960
+ });
961
+ continue;
962
+ }
369
963
  // 도구 실행 — AbortController를 InterruptManager에 등록
370
964
  const startTime = Date.now();
371
965
  const toolAbort = new AbortController();
@@ -382,15 +976,20 @@ export class AgentLoop extends EventEmitter {
382
976
  : result.output,
383
977
  durationMs: result.durationMs,
384
978
  });
385
- // 파일 변경 이벤트
979
+ // 파일 변경 이벤트 + 추적
386
980
  if (["file_write", "file_edit"].includes(toolCall.name) &&
387
981
  result.success) {
388
982
  const filePath = args.path ??
389
983
  args.file ??
390
984
  "unknown";
985
+ const filePathStr = String(filePath);
986
+ // 변경 파일 추적 (메모리 업데이트용)
987
+ if (!this.changedFiles.includes(filePathStr)) {
988
+ this.changedFiles.push(filePathStr);
989
+ }
391
990
  this.emitEvent({
392
991
  kind: "agent:file_change",
393
- path: String(filePath),
992
+ path: filePathStr,
394
993
  diff: result.output,
395
994
  });
396
995
  }
@@ -525,6 +1124,92 @@ export class AgentLoop extends EventEmitter {
525
1124
  }
526
1125
  return args;
527
1126
  }
1127
+ // ─── Continuation Helpers ───
1128
+ /**
1129
+ * 토큰 예산 소진 임박 시 자동 체크포인트를 저장한다.
1130
+ * 현재 진행 상태, 변경 파일, 에러 등을 직렬화.
1131
+ */
1132
+ async saveAutoCheckpoint(iteration) {
1133
+ if (!this.continuationEngine)
1134
+ return;
1135
+ try {
1136
+ // 현재 plan 정보에서 진행 상황 추출
1137
+ const progress = this.extractProgress();
1138
+ const checkpoint = {
1139
+ sessionId: crypto.randomUUID(),
1140
+ goal: this.contextManager.getMessages().find((m) => m.role === "user")?.content ?? "",
1141
+ progress,
1142
+ changedFiles: this.changedFiles.map((path) => ({ path, diff: "" })),
1143
+ workingMemory: this.buildWorkingMemorySummary(),
1144
+ yuanMdUpdates: [],
1145
+ errors: this.allToolResults
1146
+ .filter((r) => !r.success)
1147
+ .slice(-5)
1148
+ .map((r) => `${r.name}: ${r.output.slice(0, 200)}`),
1149
+ contextUsageAtSave: this.tokenUsage.total / this.config.loop.totalTokenBudget,
1150
+ totalTokensUsed: this.tokenUsage.total,
1151
+ iterationsCompleted: iteration,
1152
+ createdAt: new Date(),
1153
+ };
1154
+ const savedPath = await this.continuationEngine.saveCheckpoint(checkpoint);
1155
+ if (savedPath) {
1156
+ this.emitEvent({
1157
+ kind: "agent:thinking",
1158
+ content: `Auto-checkpoint saved at ${Math.round(checkpoint.contextUsageAtSave * 100)}% token usage (iteration ${iteration}).`,
1159
+ });
1160
+ }
1161
+ }
1162
+ catch {
1163
+ // 체크포인트 저장 실패는 치명적이지 않음
1164
+ }
1165
+ }
1166
+ /**
1167
+ * 현재 plan에서 진행 상황을 추출한다.
1168
+ */
1169
+ extractProgress() {
1170
+ if (!this.activePlan) {
1171
+ return { completedTasks: [], currentTask: "", remainingTasks: [] };
1172
+ }
1173
+ const tasks = this.activePlan.tactical;
1174
+ const completedTasks = tasks
1175
+ .slice(0, this.currentTaskIndex)
1176
+ .map((t) => t.description);
1177
+ const currentTask = tasks[this.currentTaskIndex]?.description ?? "";
1178
+ const remainingTasks = tasks
1179
+ .slice(this.currentTaskIndex + 1)
1180
+ .map((t) => t.description);
1181
+ return { completedTasks, currentTask, remainingTasks };
1182
+ }
1183
+ /**
1184
+ * 현재 작업 메모리 요약을 생성한다.
1185
+ * 최근 도구 결과와 LLM 응답의 핵심만 추출.
1186
+ */
1187
+ buildWorkingMemorySummary() {
1188
+ const parts = [];
1189
+ // 최근 도구 결과 요약 (최대 5개)
1190
+ const recentTools = this.allToolResults.slice(-5);
1191
+ if (recentTools.length > 0) {
1192
+ parts.push("Recent tool results:");
1193
+ for (const r of recentTools) {
1194
+ const status = r.success ? "OK" : "FAIL";
1195
+ parts.push(`- ${r.name} [${status}]: ${r.output.slice(0, 100)}`);
1196
+ }
1197
+ }
1198
+ // 변경된 파일 목록
1199
+ if (this.changedFiles.length > 0) {
1200
+ parts.push(`\nChanged files: ${this.changedFiles.join(", ")}`);
1201
+ }
1202
+ // 토큰 사용량
1203
+ parts.push(`\nTokens used: ${this.tokenUsage.total} / ${this.config.loop.totalTokenBudget}`);
1204
+ return parts.join("\n");
1205
+ }
1206
+ /**
1207
+ * ContinuationEngine 인스턴스를 반환한다.
1208
+ * 외부에서 체크포인트 조회/관리에 사용.
1209
+ */
1210
+ getContinuationEngine() {
1211
+ return this.continuationEngine;
1212
+ }
528
1213
  // ─── Interrupt Helpers ───
529
1214
  /**
530
1215
  * InterruptManager 이벤트를 AgentLoop에 연결한다.
@@ -569,6 +1254,27 @@ export class AgentLoop extends EventEmitter {
569
1254
  this.interruptManager.on("interrupt:hard", onHard);
570
1255
  });
571
1256
  }
1257
+ // ─── MCP Helpers ───
1258
+ /** MCP 도구인지 확인 */
1259
+ isMCPTool(toolName) {
1260
+ return this.mcpToolDefinitions.some((t) => t.name === toolName);
1261
+ }
1262
+ /** MCP 도구 실행 (callToolAsYuan 활용) */
1263
+ async executeMCPTool(toolCall) {
1264
+ const args = this.parseToolArgs(toolCall.arguments);
1265
+ return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
1266
+ }
1267
+ /** MCP 클라이언트 정리 (세션 종료 시 호출) */
1268
+ async dispose() {
1269
+ if (this.mcpClient) {
1270
+ try {
1271
+ await this.mcpClient.disconnectAll();
1272
+ }
1273
+ catch {
1274
+ // cleanup failure ignored
1275
+ }
1276
+ }
1277
+ }
572
1278
  // ─── Helpers ───
573
1279
  emitEvent(event) {
574
1280
  this.emit("event", event);