@yuaone/core 0.6.1 → 0.7.1

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 (67) hide show
  1. package/dist/agent-loop.d.ts +38 -0
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +532 -117
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/code-indexer.d.ts +50 -0
  6. package/dist/code-indexer.d.ts.map +1 -0
  7. package/dist/code-indexer.js +199 -0
  8. package/dist/code-indexer.js.map +1 -0
  9. package/dist/failure-recovery.d.ts +15 -2
  10. package/dist/failure-recovery.d.ts.map +1 -1
  11. package/dist/failure-recovery.js +53 -2
  12. package/dist/failure-recovery.js.map +1 -1
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/llm-client.d.ts +20 -2
  18. package/dist/llm-client.d.ts.map +1 -1
  19. package/dist/llm-client.js +213 -8
  20. package/dist/llm-client.js.map +1 -1
  21. package/dist/llm-orchestrator.d.ts +74 -0
  22. package/dist/llm-orchestrator.d.ts.map +1 -0
  23. package/dist/llm-orchestrator.js +144 -0
  24. package/dist/llm-orchestrator.js.map +1 -0
  25. package/dist/planner/index.d.ts +9 -0
  26. package/dist/planner/index.d.ts.map +1 -0
  27. package/dist/planner/index.js +5 -0
  28. package/dist/planner/index.js.map +1 -0
  29. package/dist/planner/milestone-checker.d.ts +48 -0
  30. package/dist/planner/milestone-checker.d.ts.map +1 -0
  31. package/dist/planner/milestone-checker.js +113 -0
  32. package/dist/planner/milestone-checker.js.map +1 -0
  33. package/dist/planner/plan-evaluator.d.ts +35 -0
  34. package/dist/planner/plan-evaluator.d.ts.map +1 -0
  35. package/dist/planner/plan-evaluator.js +92 -0
  36. package/dist/planner/plan-evaluator.js.map +1 -0
  37. package/dist/planner/replanning-engine.d.ts +37 -0
  38. package/dist/planner/replanning-engine.d.ts.map +1 -0
  39. package/dist/planner/replanning-engine.js +130 -0
  40. package/dist/planner/replanning-engine.js.map +1 -0
  41. package/dist/planner/risk-estimator.d.ts +44 -0
  42. package/dist/planner/risk-estimator.d.ts.map +1 -0
  43. package/dist/planner/risk-estimator.js +108 -0
  44. package/dist/planner/risk-estimator.js.map +1 -0
  45. package/dist/types.d.ts +12 -0
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/world-model/index.d.ts +8 -0
  48. package/dist/world-model/index.d.ts.map +1 -0
  49. package/dist/world-model/index.js +5 -0
  50. package/dist/world-model/index.js.map +1 -0
  51. package/dist/world-model/simulation-engine.d.ts +58 -0
  52. package/dist/world-model/simulation-engine.d.ts.map +1 -0
  53. package/dist/world-model/simulation-engine.js +191 -0
  54. package/dist/world-model/simulation-engine.js.map +1 -0
  55. package/dist/world-model/state-store.d.ts +149 -0
  56. package/dist/world-model/state-store.d.ts.map +1 -0
  57. package/dist/world-model/state-store.js +379 -0
  58. package/dist/world-model/state-store.js.map +1 -0
  59. package/dist/world-model/state-updater.d.ts +35 -0
  60. package/dist/world-model/state-updater.d.ts.map +1 -0
  61. package/dist/world-model/state-updater.js +131 -0
  62. package/dist/world-model/state-updater.js.map +1 -0
  63. package/dist/world-model/transition-model.d.ts +54 -0
  64. package/dist/world-model/transition-model.d.ts.map +1 -0
  65. package/dist/world-model/transition-model.js +240 -0
  66. package/dist/world-model/transition-model.js.map +1 -0
  67. package/package.json +1 -1
@@ -8,6 +8,8 @@
8
8
  */
9
9
  import { EventEmitter } from "node:events";
10
10
  import { randomUUID } from "node:crypto";
11
+ import { execSync } from "node:child_process";
12
+ import { basename, join as pathJoin } from "node:path";
11
13
  import { BYOKClient } from "./llm-client.js";
12
14
  import { Governor } from "./governor.js";
13
15
  import { ContextManager } from "./context-manager.js";
@@ -50,6 +52,10 @@ import { DependencyAnalyzer } from "./dependency-analyzer.js";
50
52
  import { CrossFileRefactor } from "./cross-file-refactor.js";
51
53
  import { ContextBudgetManager } from "./context-budget.js";
52
54
  import { QAPipeline } from "./qa-pipeline.js";
55
+ import { PersonaManager } from "./persona.js";
56
+ import { InMemoryVectorStore, OllamaEmbeddingProvider } from "./vector-store.js";
57
+ import { StateStore, TransitionModel, SimulationEngine, StateUpdater, } from "./world-model/index.js";
58
+ import { MilestoneChecker, RiskEstimator, PlanEvaluator, ReplanningEngine, } from "./planner/index.js";
53
59
  /**
54
60
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
55
61
  *
@@ -103,6 +109,7 @@ export class AgentLoop extends EventEmitter {
103
109
  tokenBudgetManager;
104
110
  allToolResults = [];
105
111
  currentTaskIndex = 0;
112
+ _lastInjectedTaskIndex = -1; // track when plan progress was last injected
106
113
  changedFiles = [];
107
114
  aborted = false;
108
115
  initialized = false;
@@ -150,6 +157,8 @@ export class AgentLoop extends EventEmitter {
150
157
  iterationSystemMsgCount = 0;
151
158
  /** Task 1: ContextBudgetManager for LLM-based summarization at 60-70% context usage */
152
159
  contextBudgetManager = null;
160
+ /** Task 1: Flag to ensure LLM summarization only runs once per agent run (non-blocking guard) */
161
+ _contextSummarizationDone = false;
153
162
  /** Task 2: Track whether write tools ran this iteration for QA triggering */
154
163
  iterationWriteToolPaths = [];
155
164
  /** Task 2: Last QA result (surfaced to LLM on issues) */
@@ -158,6 +167,25 @@ export class AgentLoop extends EventEmitter {
158
167
  iterationTsFilesModified = [];
159
168
  /** Task 3: Whether tsc was run in the previous iteration (skip cooldown) */
160
169
  tscRanLastIteration = false;
170
+ /** PersonaManager — learns user communication style, injects persona into system prompt */
171
+ personaManager = null;
172
+ /** InMemoryVectorStore — RAG: semantic code context retrieval for relevant snippets */
173
+ vectorStore = null;
174
+ /** Last user message — used for task-specific memory retrieval */
175
+ lastUserMessage = "";
176
+ // World Model
177
+ worldModel = null;
178
+ transitionModel = null;
179
+ simulationEngine = null;
180
+ stateUpdater = null;
181
+ // Proactive Replanning
182
+ planEvaluator = null;
183
+ riskEstimator = null;
184
+ replanningEngine = null;
185
+ milestoneChecker = null;
186
+ activeMilestones = [];
187
+ completedPlanTaskIds = new Set();
188
+ allToolResultsSinceLastReplan = [];
161
189
  tokenUsage = {
162
190
  input: 0,
163
191
  output: 0,
@@ -322,6 +350,27 @@ export class AgentLoop extends EventEmitter {
322
350
  // MemoryManager (structured learnings)
323
351
  this.memoryManager = new MemoryManager(projectPath);
324
352
  await this.memoryManager.load();
353
+ // PersonaManager — user communication style learning + persona injection
354
+ const personaUserId = basename(projectPath) || "default";
355
+ this.personaManager = new PersonaManager({
356
+ userId: personaUserId,
357
+ profilePath: pathJoin(projectPath, ".yuan", `persona-${personaUserId}.json`),
358
+ enableLearning: true,
359
+ });
360
+ await this.personaManager.loadProfile().catch(() => { });
361
+ // InMemoryVectorStore — RAG semantic code context (TF-IDF fallback if Ollama unavailable)
362
+ this.vectorStore = new InMemoryVectorStore({
363
+ projectId: personaUserId,
364
+ projectPath,
365
+ embeddingProvider: new OllamaEmbeddingProvider(),
366
+ });
367
+ await this.vectorStore.load().catch(() => { });
368
+ // Background indexing — non-blocking, fires and forgets
369
+ const vectorStoreRef = this.vectorStore;
370
+ import("./code-indexer.js").then(({ CodeIndexer }) => {
371
+ const indexer = new CodeIndexer({});
372
+ indexer.indexProject(projectPath, vectorStoreRef).catch(() => { });
373
+ }).catch(() => { });
325
374
  // 프로젝트 구조 분석
326
375
  projectStructure = await this.yuanMemory.analyzeProject();
327
376
  }
@@ -362,6 +411,32 @@ export class AgentLoop extends EventEmitter {
362
411
  catch {
363
412
  // WorldState 수집 실패는 치명적이지 않음
364
413
  }
414
+ // Initialize World Model
415
+ if (this.worldState && projectPath) {
416
+ try {
417
+ this.transitionModel = new TransitionModel();
418
+ this.worldModel = StateStore.fromSnapshot(this.worldState, projectPath);
419
+ this.simulationEngine = new SimulationEngine(this.transitionModel, this.worldModel);
420
+ this.stateUpdater = new StateUpdater(this.worldModel, projectPath);
421
+ }
422
+ catch {
423
+ // World Model initialization failure is non-fatal
424
+ }
425
+ }
426
+ // Capture last known good git commit for FailureRecovery rollback
427
+ try {
428
+ const headHash = execSync("git rev-parse HEAD", {
429
+ cwd: projectPath,
430
+ stdio: "pipe",
431
+ timeout: 5_000,
432
+ }).toString().trim();
433
+ if (headHash) {
434
+ this.failureRecovery.setLastGoodCommit(headHash, projectPath);
435
+ }
436
+ }
437
+ catch {
438
+ // Not a git repo or git unavailable — FailureRecovery will use file-level rollback only
439
+ }
365
440
  // ImpactAnalyzer 생성
366
441
  this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
367
442
  // ContinuationEngine 생성
@@ -479,6 +554,18 @@ export class AgentLoop extends EventEmitter {
479
554
  if (this.skillLearner) {
480
555
  this.planner.setSkillLearner(this.skillLearner);
481
556
  }
557
+ // Initialize Proactive Replanning (requires planner + impactAnalyzer + worldModel)
558
+ try {
559
+ if (this.impactAnalyzer && this.worldModel && this.simulationEngine && this.transitionModel) {
560
+ this.milestoneChecker = new MilestoneChecker();
561
+ this.riskEstimator = new RiskEstimator(this.transitionModel, this.impactAnalyzer);
562
+ this.planEvaluator = new PlanEvaluator(this.worldModel, this.simulationEngine);
563
+ this.replanningEngine = new ReplanningEngine(this.planner, this.planEvaluator, this.riskEstimator, this.milestoneChecker);
564
+ }
565
+ }
566
+ catch {
567
+ // Proactive replanning initialization failure is non-fatal
568
+ }
482
569
  if (this.skillLearner) {
483
570
  const learnedSkills = this.skillLearner.getAllSkills();
484
571
  if (learnedSkills.length > 0) {
@@ -516,12 +603,15 @@ export class AgentLoop extends EventEmitter {
516
603
  const bgAgent = this.backgroundAgentManager.get(agent.id);
517
604
  if (bgAgent) {
518
605
  bgAgent.on("event", (event) => {
519
- if (event.type === "error" || event.type === "warning") {
520
- this.emitEvent({
521
- kind: "agent:thinking",
522
- content: `[Background: ${event.agentId}] ${event.message}`,
523
- });
524
- }
606
+ // Emit structured bg_update for TUI task panel
607
+ this.emitEvent({
608
+ kind: "agent:bg_update",
609
+ agentId: event.agentId,
610
+ agentLabel: event.agentId.replace(/-/g, " "),
611
+ eventType: event.type,
612
+ message: event.message,
613
+ timestamp: event.timestamp,
614
+ });
525
615
  });
526
616
  }
527
617
  }
@@ -670,6 +760,8 @@ export class AgentLoop extends EventEmitter {
670
760
  // Task 3: reset tsc tracking per run
671
761
  this.iterationTsFilesModified = [];
672
762
  this.tscRanLastIteration = false;
763
+ // Task 1: reset context summarization guard per run
764
+ this._contextSummarizationDone = false;
673
765
  }
674
766
  this.checkpointSaved = false;
675
767
  this.failureRecovery.reset();
@@ -719,8 +811,58 @@ export class AgentLoop extends EventEmitter {
719
811
  role: "user",
720
812
  content: userMessage,
721
813
  });
814
+ // PersonaManager — 유저 메시지로 커뮤니케이션 스타일 학습
815
+ this.lastUserMessage = userMessage;
816
+ if (this.personaManager) {
817
+ this.personaManager.analyzeUserMessage(userMessage);
818
+ }
722
819
  this.emitEvent({ kind: "agent:start", goal: userMessage });
723
820
  try {
821
+ // Persona injection — 유저 선호도/언어/스타일 어댑테이션을 시스템 메시지로 주입
822
+ if (this.personaManager) {
823
+ const personaSection = this.personaManager.buildPersonaPrompt();
824
+ if (personaSection) {
825
+ this.contextManager.addMessage({ role: "system", content: personaSection });
826
+ }
827
+ }
828
+ // MemoryManager.getRelevant() — 현재 태스크와 관련된 conventions/patterns/warnings 주입
829
+ if (this.memoryManager) {
830
+ const relevant = this.memoryManager.getRelevant(userMessage);
831
+ const parts = [];
832
+ if (relevant.conventions.length > 0) {
833
+ parts.push(`## Project Conventions\n${relevant.conventions.slice(0, 5).map((c) => `- ${c}`).join("\n")}`);
834
+ }
835
+ if (relevant.warnings.length > 0) {
836
+ parts.push(`## Relevant Warnings\n${relevant.warnings.slice(0, 3).map((w) => `⚠ ${w}`).join("\n")}`);
837
+ }
838
+ if (relevant.patterns.length > 0) {
839
+ parts.push(`## Relevant Code Patterns\n${relevant.patterns.slice(0, 3).map((p) => `- **${p.name}**: ${p.description}`).join("\n")}`);
840
+ }
841
+ if (parts.length > 0) {
842
+ this.contextManager.addMessage({
843
+ role: "system",
844
+ content: `[Task Memory]\n${parts.join("\n\n")}`,
845
+ });
846
+ }
847
+ }
848
+ // VectorStore RAG — 태스크와 의미적으로 유사한 코드 컨텍스트 검색·주입
849
+ if (this.vectorStore) {
850
+ try {
851
+ const hits = await this.vectorStore.search(userMessage, 3, 0.2);
852
+ if (hits.length > 0) {
853
+ const ragCtx = hits
854
+ .map((h) => `**${h.id}** (relevance: ${(h.similarity * 100).toFixed(0)}%)\n${h.text.slice(0, 400)}`)
855
+ .join("\n\n---\n\n");
856
+ this.contextManager.addMessage({
857
+ role: "system",
858
+ content: `[RAG Context — semantically relevant code snippets]\n${ragCtx}`,
859
+ });
860
+ }
861
+ }
862
+ catch {
863
+ // VectorStore search failure is non-fatal
864
+ }
865
+ }
724
866
  // Reflexion: 과거 실행에서 배운 가이던스 주입
725
867
  if (this.reflexionEngine) {
726
868
  try {
@@ -984,6 +1126,10 @@ export class AgentLoop extends EventEmitter {
984
1126
  }
985
1127
  // 메모리 저장
986
1128
  await this.memoryManager.save();
1129
+ // PersonaManager — 유저 프로필 저장 (학습된 커뮤니케이션 스타일 유지)
1130
+ if (this.personaManager) {
1131
+ await this.personaManager.saveProfile().catch(() => { });
1132
+ }
987
1133
  }
988
1134
  catch {
989
1135
  // 메모리 저장 실패는 치명적이지 않음
@@ -1152,7 +1298,7 @@ export class AgentLoop extends EventEmitter {
1152
1298
  async maybeCreatePlan(userMessage) {
1153
1299
  if (!this.planner || !this.enablePlanning)
1154
1300
  return;
1155
- const complexity = this.detectComplexity(userMessage);
1301
+ const complexity = await this.detectComplexity(userMessage);
1156
1302
  this.currentComplexity = complexity;
1157
1303
  // 임계값 미만이면 플래닝 스킵
1158
1304
  // Bug 4 fix: extend thresholdOrder to include "massive" (4), so that when planningThreshold
@@ -1172,6 +1318,20 @@ export class AgentLoop extends EventEmitter {
1172
1318
  const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
1173
1319
  this.activePlan = plan;
1174
1320
  this.currentTaskIndex = 0;
1321
+ // Run plan simulation + extract milestones
1322
+ if (this.simulationEngine && this.milestoneChecker && this.activePlan) {
1323
+ const capturedPlan = this.activePlan;
1324
+ this.simulationEngine.simulate(capturedPlan).then((simResult) => {
1325
+ if (simResult.criticalSteps.length > 0 || simResult.overallSuccessProbability < 0.6) {
1326
+ this.contextManager.addMessage({
1327
+ role: "system",
1328
+ content: this.simulationEngine.formatForPrompt(simResult),
1329
+ });
1330
+ }
1331
+ }).catch(() => { });
1332
+ this.activeMilestones = this.milestoneChecker.extractMilestones(this.activePlan);
1333
+ this.completedPlanTaskIds.clear();
1334
+ }
1175
1335
  const planTokenEstimate = plan.tactical.length * 500;
1176
1336
  this.tokenBudgetManager.recordUsage("planner", planTokenEstimate, planTokenEstimate);
1177
1337
  const planContext = this.formatPlanForContext(plan);
@@ -1227,7 +1387,7 @@ export class AgentLoop extends EventEmitter {
1227
1387
  }
1228
1388
  return "pnpm build";
1229
1389
  }
1230
- detectComplexity(message) {
1390
+ _detectComplexityHeuristic(message) {
1231
1391
  const lower = message.toLowerCase();
1232
1392
  const len = message.length;
1233
1393
  // 복잡도 점수 계산
@@ -1264,6 +1424,48 @@ export class AgentLoop extends EventEmitter {
1264
1424
  score += 2;
1265
1425
  else if (filePaths && filePaths.length >= 2)
1266
1426
  score += 1;
1427
+ // 야망형 태스크 키워드 — 짧아도 대형 작업 (score +4 → 즉시 "complex" 이상)
1428
+ // "OS 만들어", "설계해줘", "컴파일러 구현" 같은 짧지만 massive한 요청을 잡기 위함
1429
+ const ambitiousSystemKeywords = [
1430
+ "운영체제", "os ", " os", "kernel", "커널",
1431
+ "compiler", "컴파일러", "인터프리터", "interpreter",
1432
+ "database", "데이터베이스", "dbms",
1433
+ "framework", "프레임워크",
1434
+ "vm ", "virtual machine", "가상머신", "hypervisor",
1435
+ "distributed", "분산 시스템", "분산시스템",
1436
+ "blockchain", "블록체인",
1437
+ "게임 엔진", "game engine",
1438
+ ];
1439
+ for (const kw of ambitiousSystemKeywords) {
1440
+ if (lower.includes(kw)) {
1441
+ score += 4;
1442
+ break;
1443
+ }
1444
+ }
1445
+ // 설계/아키텍처 요청 키워드 (score +3)
1446
+ const designKeywords = [
1447
+ "설계", "디자인해", "아키텍처 만들", "구조 설계",
1448
+ "design the", "architect the", "design a ", "design an ",
1449
+ "from scratch", "처음부터", "새로 만들", "전부 만들",
1450
+ ];
1451
+ for (const kw of designKeywords) {
1452
+ if (lower.includes(kw)) {
1453
+ score += 3;
1454
+ break;
1455
+ }
1456
+ }
1457
+ // 전체 구현 요청 키워드 (score +2)
1458
+ const buildKeywords = [
1459
+ "만들어줘", "만들어 줘", "만들어봐", "구현해줘", "개발해줘",
1460
+ "build me", "create a full", "implement a full", "make me a",
1461
+ "전체 구현", "full implementation", "complete implementation",
1462
+ ];
1463
+ for (const kw of buildKeywords) {
1464
+ if (lower.includes(kw)) {
1465
+ score += 2;
1466
+ break;
1467
+ }
1468
+ }
1267
1469
  // 간단한 작업 키워드 (감점)
1268
1470
  const simpleKeywords = [
1269
1471
  "fix", "고쳐", "수정해",
@@ -1288,6 +1490,37 @@ export class AgentLoop extends EventEmitter {
1288
1490
  return "complex";
1289
1491
  return "massive";
1290
1492
  }
1493
+ /**
1494
+ * Hybrid complexity detection: keyword heuristic for clear cases,
1495
+ * LLM single-word classification for ambiguous borderline cases (score 1-3).
1496
+ * This prevents both over-planning (trivial tasks) and under-planning
1497
+ * (ambitious short requests that keywords miss across any language).
1498
+ */
1499
+ async detectComplexity(message) {
1500
+ const heuristic = this._detectComplexityHeuristic(message);
1501
+ // Clear extremes — trust heuristic, skip LLM call cost
1502
+ if (heuristic === "trivial" || heuristic === "massive")
1503
+ return heuristic;
1504
+ // Borderline ambiguous range: ask LLM for one-word verdict
1505
+ // Short cheap call: no tools, 1-word response, ~50 tokens total
1506
+ try {
1507
+ const resp = await this.llmClient.chat([
1508
+ {
1509
+ role: "user",
1510
+ content: `Rate this software task complexity in ONE word only (trivial/simple/moderate/complex/massive). Task: "${message.slice(0, 300)}"`,
1511
+ },
1512
+ ], []);
1513
+ const word = (resp.content ?? "").trim().toLowerCase().replace(/[^a-z]/g, "");
1514
+ const valid = ["trivial", "simple", "moderate", "complex", "massive"];
1515
+ if (valid.includes(word)) {
1516
+ return word;
1517
+ }
1518
+ }
1519
+ catch {
1520
+ // LLM classification failure — fall back to heuristic
1521
+ }
1522
+ return heuristic;
1523
+ }
1291
1524
  /**
1292
1525
  * HierarchicalPlan을 LLM이 따라갈 수 있는 컨텍스트 메시지로 포맷.
1293
1526
  */
@@ -1426,6 +1659,47 @@ export class AgentLoop extends EventEmitter {
1426
1659
  const iterationStart = Date.now();
1427
1660
  this.emitReasoning(`iteration ${iteration}: preparing context`);
1428
1661
  this.iterationSystemMsgCount = 0; // Reset per-iteration (prevents accumulation across iterations)
1662
+ // Proactive replanning check (every 10 iterations when plan is active)
1663
+ if (this.replanningEngine &&
1664
+ this.activePlan &&
1665
+ this.activeMilestones.length > 0 &&
1666
+ iteration > 0 &&
1667
+ iteration % 10 === 0) {
1668
+ try {
1669
+ const tokenBudget = this.config?.loop?.totalTokenBudget ?? 200_000;
1670
+ const tokensUsed = this.tokenUsage.total;
1671
+ const replanResult = await this.replanningEngine.evaluate(this.activePlan, this.worldModel?.getState() ?? {}, [...this.completedPlanTaskIds], this.allToolResultsSinceLastReplan, tokensUsed, tokenBudget, [...this.changedFiles], iteration, this.activeMilestones, this.llmClient);
1672
+ if (replanResult.triggered) {
1673
+ this.emitEvent({ kind: "agent:thinking", content: `[Proactive Replan] ${replanResult.message}` });
1674
+ if (replanResult.newPlan) {
1675
+ this.activePlan = replanResult.newPlan;
1676
+ this.activeMilestones = this.milestoneChecker.extractMilestones(this.activePlan);
1677
+ this.completedPlanTaskIds.clear();
1678
+ }
1679
+ else if (replanResult.modifiedTasks && replanResult.modifiedTasks.length > 0) {
1680
+ for (const modified of replanResult.modifiedTasks) {
1681
+ const idx = this.activePlan.tactical.findIndex(t => t.id === modified.id);
1682
+ if (idx >= 0)
1683
+ this.activePlan.tactical[idx] = modified;
1684
+ }
1685
+ }
1686
+ // Reset tool results accumulator after replan
1687
+ this.allToolResultsSinceLastReplan = [];
1688
+ // Inject replan notice into context
1689
+ this.contextManager.addMessage({
1690
+ role: "system",
1691
+ content: `[Proactive Replan] ${replanResult.message}\nScope: ${replanResult.decision.scope}, Risk: ${replanResult.decision.urgency}`,
1692
+ });
1693
+ }
1694
+ }
1695
+ catch {
1696
+ // Non-blocking — proactive replanning failures should not crash the agent
1697
+ }
1698
+ }
1699
+ // Plan progress injection — every 3 iterations or when task advances
1700
+ if (this.activePlan) {
1701
+ this.injectPlanProgress(iteration);
1702
+ }
1429
1703
  // Policy validation — check cost limits from ExecutionPolicyEngine
1430
1704
  if (this.policyEngine) {
1431
1705
  try {
@@ -1446,25 +1720,26 @@ export class AgentLoop extends EventEmitter {
1446
1720
  // Task 1: ContextBudgetManager LLM summarization at 60-70% — runs BEFORE ContextCompressor
1447
1721
  // Summarizes old "medium" priority conversation turns into a compact summary message,
1448
1722
  // freeing tokens before the heavier ContextCompressor kicks in at 70%.
1449
- if (contextUsageRatio >= 0.60 && contextUsageRatio < 0.70 && this.contextBudgetManager) {
1450
- try {
1451
- // Sync current messages into the budget manager so it knows what to summarize
1452
- this.contextBudgetManager.importMessages(this.contextManager.getMessages());
1453
- if (this.contextBudgetManager.needsSummarization()) {
1454
- const summary = await this.contextBudgetManager.summarize(async (prompt) => {
1455
- const resp = await this.llmClient.chat([{ role: "user", content: prompt }], []);
1456
- return typeof resp.content === "string" ? resp.content : "";
1457
- });
1723
+ if (contextUsageRatio >= 0.60 && contextUsageRatio < 0.70 && this.contextBudgetManager && !this._contextSummarizationDone) {
1724
+ this._contextSummarizationDone = true; // run at most once per agent turn
1725
+ // Non-blocking: fire-and-forget so the main iteration is not stalled
1726
+ this.contextBudgetManager.importMessages(this.contextManager.getMessages());
1727
+ if (this.contextBudgetManager.needsSummarization()) {
1728
+ const budgetMgr = this.contextBudgetManager;
1729
+ const ratio = contextUsageRatio;
1730
+ budgetMgr.summarize(async (prompt) => {
1731
+ const resp = await this.llmClient.chat([{ role: "user", content: prompt }], []);
1732
+ return typeof resp.content === "string" ? resp.content : "";
1733
+ }).then(summary => {
1458
1734
  if (summary) {
1459
1735
  this.emitEvent({
1460
1736
  kind: "agent:thinking",
1461
- content: `Context at ${Math.round(contextUsageRatio * 100)}%: summarized ${summary.originalIds.length} old messages (${summary.originalTokens} → ${summary.summarizedTokens} tokens, ${Math.round(summary.compressionRatio * 100)}% ratio).`,
1737
+ content: `Context at ${Math.round(ratio * 100)}%: summarized ${summary.originalIds.length} old messages (${summary.originalTokens} → ${summary.summarizedTokens} tokens, ${Math.round(summary.compressionRatio * 100)}% ratio).`,
1462
1738
  });
1463
1739
  }
1464
- }
1465
- }
1466
- catch {
1467
- // ContextBudgetManager summarization failure is non-fatal
1740
+ }).catch(() => {
1741
+ // summarization failure is non-fatal
1742
+ });
1468
1743
  }
1469
1744
  }
1470
1745
  // Bug 5 fix: use ContextCompressor as an alternative when context pressure is high (>70%)
@@ -1768,6 +2043,8 @@ export class AgentLoop extends EventEmitter {
1768
2043
  const { results: toolResults, deferredFixPrompts } = await this.executeTools(response.toolCalls);
1769
2044
  // Reflexion: 도구 결과 수집
1770
2045
  this.allToolResults.push(...toolResults);
2046
+ // Accumulate tool results for proactive replanning evaluation
2047
+ this.allToolResultsSinceLastReplan.push(...toolResults);
1771
2048
  // Tool plan tracking: 실행된 도구 이름 기록
1772
2049
  for (const result of toolResults) {
1773
2050
  this.executedToolNames.push(result.name);
@@ -1898,6 +2175,10 @@ export class AgentLoop extends EventEmitter {
1898
2175
  this.tscRanLastIteration = false;
1899
2176
  }
1900
2177
  }
2178
+ // Plan task advancement — check if current task's target files were modified
2179
+ if (this.activePlan) {
2180
+ this.tryAdvancePlanTask();
2181
+ }
1901
2182
  // iteration 이벤트
1902
2183
  this.emitEvent({
1903
2184
  kind: "agent:iteration",
@@ -2216,7 +2497,7 @@ export class AgentLoop extends EventEmitter {
2216
2497
  let usage = { input: 0, output: 0 };
2217
2498
  let finishReason = "stop";
2218
2499
  const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions];
2219
- const stream = this.llmClient.chatStream(messages, allTools);
2500
+ const stream = this.llmClient.chatStream(messages, allTools, this.abortSignal ?? undefined);
2220
2501
  // 텍스트 버퍼링 — 1토큰씩 emit하지 않고 청크 단위로 모아서 emit
2221
2502
  let textBuffer = "";
2222
2503
  let flushTimer = null;
@@ -2521,6 +2802,10 @@ export class AgentLoop extends EventEmitter {
2521
2802
  });
2522
2803
  }
2523
2804
  }
2805
+ // StateUpdater: sync world model with actual tool execution result
2806
+ if (this.stateUpdater && result.success) {
2807
+ this.stateUpdater.applyToolResult(toolCall.name, args, result).catch(() => { });
2808
+ }
2524
2809
  const fixPrompt = await this.validateAndFeedback(toolCall.name, result);
2525
2810
  return { result, deferredFixPrompt: fixPrompt ?? null };
2526
2811
  }
@@ -2577,136 +2862,172 @@ export class AgentLoop extends EventEmitter {
2577
2862
  }
2578
2863
  }
2579
2864
  async executeTools(toolCalls) {
2580
- // Reorder write tool calls using DependencyAnalyzer so files with no deps run first
2581
- const writeToolNames = new Set(['file_write', 'file_edit']);
2582
- const writeToolCalls = toolCalls.filter((tc) => writeToolNames.has(tc.name));
2865
+ if (toolCalls.length === 0)
2866
+ return { results: [], deferredFixPrompts: [] };
2867
+ // ─── Step 1: Build execution plan ───────────────────────────────────────
2868
+ // Strategy:
2869
+ // • Read-only tools (low-risk) → batch together, run all in parallel
2870
+ // • Write tools (independent files) → use DependencyAnalyzer to group,
2871
+ // run each independent group in parallel
2872
+ // • Write tools (dependent files) → run sequentially after their deps
2873
+ // • shell_exec / git_ops / etc. → always sequential (side-effects)
2874
+ const READ_ONLY = new Set(['file_read', 'grep', 'glob', 'code_search', 'security_scan']);
2875
+ const WRITE_TOOLS = new Set(['file_write', 'file_edit']);
2876
+ // Separate reads, writes, and heavy side-effect tools
2877
+ const writeToolCalls = toolCalls.filter((tc) => WRITE_TOOLS.has(tc.name));
2878
+ // ─── Step 2: Dependency-aware write tool batching ────────────────────────
2879
+ // Map each write tool call to a "wave index" (0 = can run first, 1 = needs wave-0 done, etc.)
2880
+ const writeBatchMap = new Map(); // tc.id → wave index
2583
2881
  if (writeToolCalls.length > 1 && this.config.loop.projectPath) {
2584
2882
  try {
2585
2883
  const depAnalyzer = new DependencyAnalyzer();
2586
2884
  const depGraph = await depAnalyzer.analyze(this.config.loop.projectPath);
2587
- const writeFilePaths = writeToolCalls
2588
- .map((tc) => {
2885
+ // Collect target file paths from write tool args
2886
+ const writeFilePaths = writeToolCalls.flatMap((tc) => {
2589
2887
  const args = this.parseToolArgs(tc.arguments);
2590
- return typeof args.path === "string" ? args.path : null;
2591
- })
2592
- .filter((p) => p !== null);
2888
+ const p = typeof args.path === "string" ? args.path
2889
+ : typeof args.file_path === "string" ? args.file_path
2890
+ : null;
2891
+ return p ? [p] : [];
2892
+ });
2593
2893
  if (writeFilePaths.length > 1) {
2594
2894
  const groups = depAnalyzer.groupIndependentFiles(depGraph, writeFilePaths);
2595
- // Build ordered list of file paths: independent groups first, dependent files after
2596
- const orderedPaths = [];
2895
+ // Assign wave indices: independent groups get wave 0,
2896
+ // dependent groups get wave = max(their dep waves) + 1
2897
+ // For simplicity: canParallelize=true → wave 0, else sequential waves
2898
+ let wave = 0;
2597
2899
  for (const group of groups) {
2598
- for (const f of group.files) {
2599
- if (!orderedPaths.includes(f))
2600
- orderedPaths.push(f);
2601
- }
2602
- }
2603
- // Reorder writeToolCalls according to orderedPaths
2604
- const reordered = [];
2605
- for (const filePath of orderedPaths) {
2606
- const tc = writeToolCalls.find((c) => {
2607
- const args = this.parseToolArgs(c.arguments);
2608
- return args.path === filePath;
2609
- });
2610
- if (tc)
2611
- reordered.push(tc);
2612
- }
2613
- // Add any write calls that didn't match a path (shouldn't happen, but be safe)
2614
- for (const tc of writeToolCalls) {
2615
- if (!reordered.includes(tc))
2616
- reordered.push(tc);
2617
- }
2618
- // Rebuild toolCalls preserving non-write tools in their original positions,
2619
- // replacing write tools with dependency-ordered sequence
2620
- const reorderedAll = [];
2621
- let writeIdx = 0;
2622
- for (const tc of toolCalls) {
2623
- if (writeToolNames.has(tc.name)) {
2624
- reorderedAll.push(reordered[writeIdx++] ?? tc);
2625
- }
2626
- else {
2627
- reorderedAll.push(tc);
2900
+ if (!group.canParallelize)
2901
+ wave++;
2902
+ for (const filePath of group.files) {
2903
+ const tc = writeToolCalls.find((c) => {
2904
+ const args = this.parseToolArgs(c.arguments);
2905
+ const p = args.path ?? args.file_path;
2906
+ return p === filePath;
2907
+ });
2908
+ if (tc)
2909
+ writeBatchMap.set(tc.id, wave);
2628
2910
  }
2911
+ if (group.canParallelize)
2912
+ wave = 0; // reset: next independent group is also wave 0
2629
2913
  }
2630
- toolCalls = reorderedAll;
2631
2914
  }
2632
2915
  }
2633
2916
  catch {
2634
- // DependencyAnalyzer failure is non-fatal — fall through to original ordering
2917
+ // DependencyAnalyzer failure is non-fatal — all writes run sequentially
2635
2918
  }
2636
2919
  }
2637
- // Group tool calls: read-only can run in parallel, write tools run sequentially
2638
- const readOnlyTools = new Set(['file_read', 'grep', 'glob', 'code_search']);
2920
+ // ─── Step 3: Build ordered batch list ────────────────────────────────────
2921
+ // Final structure: array of batches, each batch runs in parallel.
2922
+ // Reads accumulate until interrupted by a non-read tool.
2923
+ // Writes are grouped by wave (same wave → parallel, different wave → sequential).
2639
2924
  const batches = [];
2640
- let currentBatch = [];
2641
- for (const toolCall of toolCalls) {
2642
- if (readOnlyTools.has(toolCall.name)) {
2643
- currentBatch.push(toolCall);
2925
+ let readBatch = [];
2926
+ const writeBatchGroups = new Map(); // wave → calls
2927
+ const flushReadBatch = () => {
2928
+ if (readBatch.length > 0) {
2929
+ batches.push({ calls: [...readBatch], label: `${readBatch.length} read-only` });
2930
+ readBatch = [];
2931
+ }
2932
+ };
2933
+ const flushWriteBatches = () => {
2934
+ if (writeBatchGroups.size === 0)
2935
+ return;
2936
+ const waves = [...writeBatchGroups.keys()].sort((a, b) => a - b);
2937
+ for (const w of waves) {
2938
+ const wCalls = writeBatchGroups.get(w);
2939
+ batches.push({
2940
+ calls: wCalls,
2941
+ label: wCalls.length > 1
2942
+ ? `${wCalls.length} independent writes (wave ${w})`
2943
+ : `write: ${wCalls[0].name}`,
2944
+ });
2945
+ }
2946
+ writeBatchGroups.clear();
2947
+ };
2948
+ for (const tc of toolCalls) {
2949
+ if (READ_ONLY.has(tc.name)) {
2950
+ // Reads accumulate; don't flush write batches yet (they don't conflict)
2951
+ readBatch.push(tc);
2952
+ }
2953
+ else if (WRITE_TOOLS.has(tc.name)) {
2954
+ // Flush any pending reads first (reads before writes)
2955
+ flushReadBatch();
2956
+ const wave = writeBatchMap.get(tc.id) ?? 99; // unknown dep → run last
2957
+ if (!writeBatchGroups.has(wave))
2958
+ writeBatchGroups.set(wave, []);
2959
+ writeBatchGroups.get(wave).push(tc);
2644
2960
  }
2645
2961
  else {
2646
- if (currentBatch.length > 0) {
2647
- batches.push(currentBatch);
2648
- currentBatch = [];
2649
- }
2650
- batches.push([toolCall]); // write tools run solo
2962
+ // Heavy tool (shell_exec, git_ops, etc.) → flush everything, run solo
2963
+ flushReadBatch();
2964
+ flushWriteBatches();
2965
+ batches.push({ calls: [tc], label: tc.name });
2651
2966
  }
2652
2967
  }
2653
- if (currentBatch.length > 0)
2654
- batches.push(currentBatch);
2655
- if (toolCalls.length > 1)
2656
- this.emitReasoning(`parallel tool batch started (${toolCalls.length} tools)`);
2968
+ flushReadBatch();
2969
+ flushWriteBatches();
2970
+ // ─── Step 4: Execute batches ─────────────────────────────────────────────
2971
+ if (toolCalls.length > 1) {
2972
+ const parallelCount = batches.filter(b => b.calls.length > 1).length;
2973
+ this.emitReasoning(parallelCount > 0
2974
+ ? `executing ${toolCalls.length} tools in ${batches.length} batches (${parallelCount} parallel)`
2975
+ : `executing ${toolCalls.length} tools sequentially`);
2976
+ }
2657
2977
  const results = [];
2658
2978
  const deferredFixPrompts = [];
2979
+ let interrupted = false;
2659
2980
  for (const batch of batches) {
2660
- if (batch.length === 1) {
2661
- // Sequential single tool execution
2662
- const { result, deferredFixPrompt } = await this.executeSingleTool(batch[0], toolCalls);
2981
+ if (interrupted) {
2982
+ // Fill remaining tools with SKIPPED placeholders
2983
+ for (const tc of batch.calls) {
2984
+ results.push({
2985
+ tool_call_id: tc.id,
2986
+ name: tc.name,
2987
+ output: '[SKIPPED] Execution interrupted.',
2988
+ success: false,
2989
+ durationMs: 0,
2990
+ });
2991
+ }
2992
+ continue;
2993
+ }
2994
+ if (batch.calls.length === 1) {
2995
+ // Single tool — sequential execution
2996
+ const { result, deferredFixPrompt } = await this.executeSingleTool(batch.calls[0], toolCalls);
2663
2997
  if (result)
2664
2998
  results.push(result);
2665
2999
  if (deferredFixPrompt)
2666
3000
  deferredFixPrompts.push(deferredFixPrompt);
2667
- // Check for interrupt result — stop processing remaining batches
2668
- if (result && result.output.startsWith('[INTERRUPTED]')) {
2669
- // Add placeholder results for remaining unexecuted tool calls
2670
- const executedIds = new Set(results.map(r => r.tool_call_id));
2671
- for (const tc of toolCalls) {
2672
- if (!executedIds.has(tc.id)) {
2673
- results.push({
2674
- tool_call_id: tc.id,
2675
- name: tc.name,
2676
- output: `[SKIPPED] Previous tool was interrupted.`,
2677
- success: false,
2678
- durationMs: 0,
2679
- });
2680
- }
2681
- }
2682
- break;
2683
- }
3001
+ if (result?.output.startsWith('[INTERRUPTED]'))
3002
+ interrupted = true;
2684
3003
  }
2685
3004
  else {
2686
- // Parallel execution for read-only tools in this batch
2687
- this.emitReasoning(`running ${batch.length} read-only tools in parallel`);
2688
- const batchResults = await Promise.allSettled(batch.map(tc => this.executeSingleTool(tc, toolCalls)));
2689
- for (const settled of batchResults) {
2690
- if (settled.status === 'fulfilled') {
2691
- if (settled.value.result)
2692
- results.push(settled.value.result);
2693
- if (settled.value.deferredFixPrompt)
2694
- deferredFixPrompts.push(settled.value.deferredFixPrompt);
3005
+ // Multi-tool parallel execution
3006
+ this.emitReasoning(`⚡ running ${batch.label} in parallel`);
3007
+ const settled = await Promise.allSettled(batch.calls.map((tc) => this.executeSingleTool(tc, toolCalls)));
3008
+ for (let i = 0; i < settled.length; i++) {
3009
+ const s = settled[i];
3010
+ const tc = batch.calls[i];
3011
+ if (s.status === 'fulfilled') {
3012
+ if (s.value.result)
3013
+ results.push(s.value.result);
3014
+ if (s.value.deferredFixPrompt)
3015
+ deferredFixPrompts.push(s.value.deferredFixPrompt);
3016
+ if (s.value.result?.output.startsWith('[INTERRUPTED]'))
3017
+ interrupted = true;
2695
3018
  }
2696
3019
  else {
2697
- // Parallel tool failure — record as error result
2698
- const tc = batch[batchResults.indexOf(settled)];
2699
3020
  results.push({
2700
- tool_call_id: tc?.id ?? 'unknown',
2701
- name: tc?.name ?? 'unknown',
2702
- output: `Error: ${settled.reason instanceof Error ? settled.reason.message : String(settled.reason)}`,
3021
+ tool_call_id: tc.id,
3022
+ name: tc.name,
3023
+ output: `Error: ${s.reason instanceof Error ? s.reason.message : String(s.reason)}`,
2703
3024
  success: false,
2704
3025
  durationMs: 0,
2705
3026
  });
2706
3027
  }
2707
- } // end for (const settled of batchResults)
2708
- } // end else (parallel batch)
2709
- } // end for (const batch of batches)
3028
+ }
3029
+ }
3030
+ }
2710
3031
  return { results, deferredFixPrompts };
2711
3032
  }
2712
3033
  /**
@@ -2841,6 +3162,100 @@ export class AgentLoop extends EventEmitter {
2841
3162
  // 체크포인트 저장 실패는 치명적이지 않음
2842
3163
  }
2843
3164
  }
3165
+ /**
3166
+ * 매 iteration 시작 시 현재 플랜 진행 상황을 컨텍스트에 주입.
3167
+ * 같은 태스크 인덱스라면 3 iteration마다만 주입 (컨텍스트 bloat 방지).
3168
+ */
3169
+ injectPlanProgress(iteration) {
3170
+ if (!this.activePlan)
3171
+ return;
3172
+ const tasks = this.activePlan.tactical;
3173
+ if (tasks.length === 0)
3174
+ return;
3175
+ const idx = this.currentTaskIndex;
3176
+ const total = tasks.length;
3177
+ // 같은 태스크면 3iteration마다, 태스크 전진 시 즉시 주입
3178
+ const taskAdvanced = idx !== this._lastInjectedTaskIndex;
3179
+ if (!taskAdvanced && iteration > 1 && iteration % 3 !== 1)
3180
+ return;
3181
+ this._lastInjectedTaskIndex = idx;
3182
+ const lines = [
3183
+ `## Plan Progress [${idx}/${total} done]`,
3184
+ ];
3185
+ for (let i = 0; i < tasks.length; i++) {
3186
+ const task = tasks[i];
3187
+ const marker = i < idx ? "✓" : i === idx ? "→" : "○";
3188
+ lines.push(`${marker} Task ${i + 1}: ${task.description}`);
3189
+ if (i === idx && task.targetFiles.length > 0) {
3190
+ lines.push(` Files: ${task.targetFiles.join(", ")}`);
3191
+ }
3192
+ }
3193
+ if (idx < total) {
3194
+ const cur = tasks[idx];
3195
+ lines.push(`\nCurrent task: **${cur.description}**`);
3196
+ if (cur.toolStrategy.length > 0) {
3197
+ lines.push(`Suggested tools: ${cur.toolStrategy.join(", ")}`);
3198
+ }
3199
+ if (cur.readFiles.length > 0) {
3200
+ lines.push(`Read first: ${cur.readFiles.join(", ")}`);
3201
+ }
3202
+ }
3203
+ else {
3204
+ lines.push(`\nAll tasks complete — verify and wrap up.`);
3205
+ }
3206
+ this.contextManager.addMessage({
3207
+ role: "system",
3208
+ content: lines.join("\n"),
3209
+ });
3210
+ this.iterationSystemMsgCount++;
3211
+ }
3212
+ /**
3213
+ * 현재 태스크의 targetFiles가 changedFiles에 포함됐는지 확인해
3214
+ * 완료 감지 시 다음 태스크로 자동 전진.
3215
+ */
3216
+ tryAdvancePlanTask() {
3217
+ if (!this.activePlan)
3218
+ return;
3219
+ const tasks = this.activePlan.tactical;
3220
+ if (this.currentTaskIndex >= tasks.length)
3221
+ return;
3222
+ const currentTask = tasks[this.currentTaskIndex];
3223
+ if (!currentTask)
3224
+ return;
3225
+ // targetFiles가 없으면 tool call이 있었던 것만으로 완료 간주
3226
+ if (currentTask.targetFiles.length === 0) {
3227
+ if (this.allToolResults.length > 0) {
3228
+ this.completedPlanTaskIds.add(currentTask.id);
3229
+ this.currentTaskIndex++;
3230
+ this._emitPlanAdvance(tasks);
3231
+ }
3232
+ return;
3233
+ }
3234
+ // targetFiles 중 하나라도 changedFiles에 있으면 완료
3235
+ const changedBasenames = new Set(this.changedFiles.map((f) => f.split("/").pop().toLowerCase()));
3236
+ const targetBasenames = currentTask.targetFiles.map((f) => f.split("/").pop().toLowerCase());
3237
+ const hit = targetBasenames.some((b) => changedBasenames.has(b));
3238
+ if (hit) {
3239
+ this.completedPlanTaskIds.add(currentTask.id);
3240
+ this.currentTaskIndex++;
3241
+ this._emitPlanAdvance(tasks);
3242
+ }
3243
+ }
3244
+ _emitPlanAdvance(tasks) {
3245
+ const idx = this.currentTaskIndex;
3246
+ if (idx < tasks.length) {
3247
+ this.emitEvent({
3248
+ kind: "agent:thinking",
3249
+ content: `✓ Task ${idx}/${tasks.length} done. Next: ${tasks[idx].description}`,
3250
+ });
3251
+ }
3252
+ else {
3253
+ this.emitEvent({
3254
+ kind: "agent:thinking",
3255
+ content: `✓ All ${tasks.length} tasks completed.`,
3256
+ });
3257
+ }
3258
+ }
2844
3259
  /**
2845
3260
  * 현재 plan에서 진행 상황을 추출한다.
2846
3261
  */