@yuaone/core 0.6.1 → 0.7.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.
- package/dist/agent-loop.d.ts +36 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +504 -97
- package/dist/agent-loop.js.map +1 -1
- package/dist/code-indexer.d.ts +50 -0
- package/dist/code-indexer.d.ts.map +1 -0
- package/dist/code-indexer.js +199 -0
- package/dist/code-indexer.js.map +1 -0
- package/dist/failure-recovery.d.ts +15 -2
- package/dist/failure-recovery.d.ts.map +1 -1
- package/dist/failure-recovery.js +53 -2
- package/dist/failure-recovery.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-client.d.ts +11 -2
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +22 -8
- package/dist/llm-client.js.map +1 -1
- package/dist/planner/index.d.ts +9 -0
- package/dist/planner/index.d.ts.map +1 -0
- package/dist/planner/index.js +5 -0
- package/dist/planner/index.js.map +1 -0
- package/dist/planner/milestone-checker.d.ts +48 -0
- package/dist/planner/milestone-checker.d.ts.map +1 -0
- package/dist/planner/milestone-checker.js +113 -0
- package/dist/planner/milestone-checker.js.map +1 -0
- package/dist/planner/plan-evaluator.d.ts +35 -0
- package/dist/planner/plan-evaluator.d.ts.map +1 -0
- package/dist/planner/plan-evaluator.js +92 -0
- package/dist/planner/plan-evaluator.js.map +1 -0
- package/dist/planner/replanning-engine.d.ts +37 -0
- package/dist/planner/replanning-engine.d.ts.map +1 -0
- package/dist/planner/replanning-engine.js +130 -0
- package/dist/planner/replanning-engine.js.map +1 -0
- package/dist/planner/risk-estimator.d.ts +44 -0
- package/dist/planner/risk-estimator.d.ts.map +1 -0
- package/dist/planner/risk-estimator.js +108 -0
- package/dist/planner/risk-estimator.js.map +1 -0
- package/dist/world-model/index.d.ts +8 -0
- package/dist/world-model/index.d.ts.map +1 -0
- package/dist/world-model/index.js +5 -0
- package/dist/world-model/index.js.map +1 -0
- package/dist/world-model/simulation-engine.d.ts +58 -0
- package/dist/world-model/simulation-engine.d.ts.map +1 -0
- package/dist/world-model/simulation-engine.js +191 -0
- package/dist/world-model/simulation-engine.js.map +1 -0
- package/dist/world-model/state-store.d.ts +149 -0
- package/dist/world-model/state-store.d.ts.map +1 -0
- package/dist/world-model/state-store.js +379 -0
- package/dist/world-model/state-store.js.map +1 -0
- package/dist/world-model/state-updater.d.ts +35 -0
- package/dist/world-model/state-updater.d.ts.map +1 -0
- package/dist/world-model/state-updater.js +131 -0
- package/dist/world-model/state-updater.js.map +1 -0
- package/dist/world-model/transition-model.d.ts +54 -0
- package/dist/world-model/transition-model.d.ts.map +1 -0
- package/dist/world-model/transition-model.js +240 -0
- package/dist/world-model/transition-model.js.map +1 -0
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -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;
|
|
@@ -158,6 +165,25 @@ export class AgentLoop extends EventEmitter {
|
|
|
158
165
|
iterationTsFilesModified = [];
|
|
159
166
|
/** Task 3: Whether tsc was run in the previous iteration (skip cooldown) */
|
|
160
167
|
tscRanLastIteration = false;
|
|
168
|
+
/** PersonaManager — learns user communication style, injects persona into system prompt */
|
|
169
|
+
personaManager = null;
|
|
170
|
+
/** InMemoryVectorStore — RAG: semantic code context retrieval for relevant snippets */
|
|
171
|
+
vectorStore = null;
|
|
172
|
+
/** Last user message — used for task-specific memory retrieval */
|
|
173
|
+
lastUserMessage = "";
|
|
174
|
+
// World Model
|
|
175
|
+
worldModel = null;
|
|
176
|
+
transitionModel = null;
|
|
177
|
+
simulationEngine = null;
|
|
178
|
+
stateUpdater = null;
|
|
179
|
+
// Proactive Replanning
|
|
180
|
+
planEvaluator = null;
|
|
181
|
+
riskEstimator = null;
|
|
182
|
+
replanningEngine = null;
|
|
183
|
+
milestoneChecker = null;
|
|
184
|
+
activeMilestones = [];
|
|
185
|
+
completedPlanTaskIds = new Set();
|
|
186
|
+
allToolResultsSinceLastReplan = [];
|
|
161
187
|
tokenUsage = {
|
|
162
188
|
input: 0,
|
|
163
189
|
output: 0,
|
|
@@ -322,6 +348,27 @@ export class AgentLoop extends EventEmitter {
|
|
|
322
348
|
// MemoryManager (structured learnings)
|
|
323
349
|
this.memoryManager = new MemoryManager(projectPath);
|
|
324
350
|
await this.memoryManager.load();
|
|
351
|
+
// PersonaManager — user communication style learning + persona injection
|
|
352
|
+
const personaUserId = basename(projectPath) || "default";
|
|
353
|
+
this.personaManager = new PersonaManager({
|
|
354
|
+
userId: personaUserId,
|
|
355
|
+
profilePath: pathJoin(projectPath, ".yuan", `persona-${personaUserId}.json`),
|
|
356
|
+
enableLearning: true,
|
|
357
|
+
});
|
|
358
|
+
await this.personaManager.loadProfile().catch(() => { });
|
|
359
|
+
// InMemoryVectorStore — RAG semantic code context (TF-IDF fallback if Ollama unavailable)
|
|
360
|
+
this.vectorStore = new InMemoryVectorStore({
|
|
361
|
+
projectId: personaUserId,
|
|
362
|
+
projectPath,
|
|
363
|
+
embeddingProvider: new OllamaEmbeddingProvider(),
|
|
364
|
+
});
|
|
365
|
+
await this.vectorStore.load().catch(() => { });
|
|
366
|
+
// Background indexing — non-blocking, fires and forgets
|
|
367
|
+
const vectorStoreRef = this.vectorStore;
|
|
368
|
+
import("./code-indexer.js").then(({ CodeIndexer }) => {
|
|
369
|
+
const indexer = new CodeIndexer({});
|
|
370
|
+
indexer.indexProject(projectPath, vectorStoreRef).catch(() => { });
|
|
371
|
+
}).catch(() => { });
|
|
325
372
|
// 프로젝트 구조 분석
|
|
326
373
|
projectStructure = await this.yuanMemory.analyzeProject();
|
|
327
374
|
}
|
|
@@ -362,6 +409,32 @@ export class AgentLoop extends EventEmitter {
|
|
|
362
409
|
catch {
|
|
363
410
|
// WorldState 수집 실패는 치명적이지 않음
|
|
364
411
|
}
|
|
412
|
+
// Initialize World Model
|
|
413
|
+
if (this.worldState && projectPath) {
|
|
414
|
+
try {
|
|
415
|
+
this.transitionModel = new TransitionModel();
|
|
416
|
+
this.worldModel = StateStore.fromSnapshot(this.worldState, projectPath);
|
|
417
|
+
this.simulationEngine = new SimulationEngine(this.transitionModel, this.worldModel);
|
|
418
|
+
this.stateUpdater = new StateUpdater(this.worldModel, projectPath);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// World Model initialization failure is non-fatal
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Capture last known good git commit for FailureRecovery rollback
|
|
425
|
+
try {
|
|
426
|
+
const headHash = execSync("git rev-parse HEAD", {
|
|
427
|
+
cwd: projectPath,
|
|
428
|
+
stdio: "pipe",
|
|
429
|
+
timeout: 5_000,
|
|
430
|
+
}).toString().trim();
|
|
431
|
+
if (headHash) {
|
|
432
|
+
this.failureRecovery.setLastGoodCommit(headHash, projectPath);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Not a git repo or git unavailable — FailureRecovery will use file-level rollback only
|
|
437
|
+
}
|
|
365
438
|
// ImpactAnalyzer 생성
|
|
366
439
|
this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
|
|
367
440
|
// ContinuationEngine 생성
|
|
@@ -479,6 +552,18 @@ export class AgentLoop extends EventEmitter {
|
|
|
479
552
|
if (this.skillLearner) {
|
|
480
553
|
this.planner.setSkillLearner(this.skillLearner);
|
|
481
554
|
}
|
|
555
|
+
// Initialize Proactive Replanning (requires planner + impactAnalyzer + worldModel)
|
|
556
|
+
try {
|
|
557
|
+
if (this.impactAnalyzer && this.worldModel && this.simulationEngine && this.transitionModel) {
|
|
558
|
+
this.milestoneChecker = new MilestoneChecker();
|
|
559
|
+
this.riskEstimator = new RiskEstimator(this.transitionModel, this.impactAnalyzer);
|
|
560
|
+
this.planEvaluator = new PlanEvaluator(this.worldModel, this.simulationEngine);
|
|
561
|
+
this.replanningEngine = new ReplanningEngine(this.planner, this.planEvaluator, this.riskEstimator, this.milestoneChecker);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Proactive replanning initialization failure is non-fatal
|
|
566
|
+
}
|
|
482
567
|
if (this.skillLearner) {
|
|
483
568
|
const learnedSkills = this.skillLearner.getAllSkills();
|
|
484
569
|
if (learnedSkills.length > 0) {
|
|
@@ -719,8 +804,58 @@ export class AgentLoop extends EventEmitter {
|
|
|
719
804
|
role: "user",
|
|
720
805
|
content: userMessage,
|
|
721
806
|
});
|
|
807
|
+
// PersonaManager — 유저 메시지로 커뮤니케이션 스타일 학습
|
|
808
|
+
this.lastUserMessage = userMessage;
|
|
809
|
+
if (this.personaManager) {
|
|
810
|
+
this.personaManager.analyzeUserMessage(userMessage);
|
|
811
|
+
}
|
|
722
812
|
this.emitEvent({ kind: "agent:start", goal: userMessage });
|
|
723
813
|
try {
|
|
814
|
+
// Persona injection — 유저 선호도/언어/스타일 어댑테이션을 시스템 메시지로 주입
|
|
815
|
+
if (this.personaManager) {
|
|
816
|
+
const personaSection = this.personaManager.buildPersonaPrompt();
|
|
817
|
+
if (personaSection) {
|
|
818
|
+
this.contextManager.addMessage({ role: "system", content: personaSection });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// MemoryManager.getRelevant() — 현재 태스크와 관련된 conventions/patterns/warnings 주입
|
|
822
|
+
if (this.memoryManager) {
|
|
823
|
+
const relevant = this.memoryManager.getRelevant(userMessage);
|
|
824
|
+
const parts = [];
|
|
825
|
+
if (relevant.conventions.length > 0) {
|
|
826
|
+
parts.push(`## Project Conventions\n${relevant.conventions.slice(0, 5).map((c) => `- ${c}`).join("\n")}`);
|
|
827
|
+
}
|
|
828
|
+
if (relevant.warnings.length > 0) {
|
|
829
|
+
parts.push(`## Relevant Warnings\n${relevant.warnings.slice(0, 3).map((w) => `⚠ ${w}`).join("\n")}`);
|
|
830
|
+
}
|
|
831
|
+
if (relevant.patterns.length > 0) {
|
|
832
|
+
parts.push(`## Relevant Code Patterns\n${relevant.patterns.slice(0, 3).map((p) => `- **${p.name}**: ${p.description}`).join("\n")}`);
|
|
833
|
+
}
|
|
834
|
+
if (parts.length > 0) {
|
|
835
|
+
this.contextManager.addMessage({
|
|
836
|
+
role: "system",
|
|
837
|
+
content: `[Task Memory]\n${parts.join("\n\n")}`,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// VectorStore RAG — 태스크와 의미적으로 유사한 코드 컨텍스트 검색·주입
|
|
842
|
+
if (this.vectorStore) {
|
|
843
|
+
try {
|
|
844
|
+
const hits = await this.vectorStore.search(userMessage, 3, 0.2);
|
|
845
|
+
if (hits.length > 0) {
|
|
846
|
+
const ragCtx = hits
|
|
847
|
+
.map((h) => `**${h.id}** (relevance: ${(h.similarity * 100).toFixed(0)}%)\n${h.text.slice(0, 400)}`)
|
|
848
|
+
.join("\n\n---\n\n");
|
|
849
|
+
this.contextManager.addMessage({
|
|
850
|
+
role: "system",
|
|
851
|
+
content: `[RAG Context — semantically relevant code snippets]\n${ragCtx}`,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// VectorStore search failure is non-fatal
|
|
857
|
+
}
|
|
858
|
+
}
|
|
724
859
|
// Reflexion: 과거 실행에서 배운 가이던스 주입
|
|
725
860
|
if (this.reflexionEngine) {
|
|
726
861
|
try {
|
|
@@ -984,6 +1119,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
984
1119
|
}
|
|
985
1120
|
// 메모리 저장
|
|
986
1121
|
await this.memoryManager.save();
|
|
1122
|
+
// PersonaManager — 유저 프로필 저장 (학습된 커뮤니케이션 스타일 유지)
|
|
1123
|
+
if (this.personaManager) {
|
|
1124
|
+
await this.personaManager.saveProfile().catch(() => { });
|
|
1125
|
+
}
|
|
987
1126
|
}
|
|
988
1127
|
catch {
|
|
989
1128
|
// 메모리 저장 실패는 치명적이지 않음
|
|
@@ -1152,7 +1291,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1152
1291
|
async maybeCreatePlan(userMessage) {
|
|
1153
1292
|
if (!this.planner || !this.enablePlanning)
|
|
1154
1293
|
return;
|
|
1155
|
-
const complexity = this.detectComplexity(userMessage);
|
|
1294
|
+
const complexity = await this.detectComplexity(userMessage);
|
|
1156
1295
|
this.currentComplexity = complexity;
|
|
1157
1296
|
// 임계값 미만이면 플래닝 스킵
|
|
1158
1297
|
// Bug 4 fix: extend thresholdOrder to include "massive" (4), so that when planningThreshold
|
|
@@ -1172,6 +1311,20 @@ export class AgentLoop extends EventEmitter {
|
|
|
1172
1311
|
const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
|
|
1173
1312
|
this.activePlan = plan;
|
|
1174
1313
|
this.currentTaskIndex = 0;
|
|
1314
|
+
// Run plan simulation + extract milestones
|
|
1315
|
+
if (this.simulationEngine && this.milestoneChecker && this.activePlan) {
|
|
1316
|
+
const capturedPlan = this.activePlan;
|
|
1317
|
+
this.simulationEngine.simulate(capturedPlan).then((simResult) => {
|
|
1318
|
+
if (simResult.criticalSteps.length > 0 || simResult.overallSuccessProbability < 0.6) {
|
|
1319
|
+
this.contextManager.addMessage({
|
|
1320
|
+
role: "system",
|
|
1321
|
+
content: this.simulationEngine.formatForPrompt(simResult),
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}).catch(() => { });
|
|
1325
|
+
this.activeMilestones = this.milestoneChecker.extractMilestones(this.activePlan);
|
|
1326
|
+
this.completedPlanTaskIds.clear();
|
|
1327
|
+
}
|
|
1175
1328
|
const planTokenEstimate = plan.tactical.length * 500;
|
|
1176
1329
|
this.tokenBudgetManager.recordUsage("planner", planTokenEstimate, planTokenEstimate);
|
|
1177
1330
|
const planContext = this.formatPlanForContext(plan);
|
|
@@ -1227,7 +1380,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1227
1380
|
}
|
|
1228
1381
|
return "pnpm build";
|
|
1229
1382
|
}
|
|
1230
|
-
|
|
1383
|
+
_detectComplexityHeuristic(message) {
|
|
1231
1384
|
const lower = message.toLowerCase();
|
|
1232
1385
|
const len = message.length;
|
|
1233
1386
|
// 복잡도 점수 계산
|
|
@@ -1264,6 +1417,48 @@ export class AgentLoop extends EventEmitter {
|
|
|
1264
1417
|
score += 2;
|
|
1265
1418
|
else if (filePaths && filePaths.length >= 2)
|
|
1266
1419
|
score += 1;
|
|
1420
|
+
// 야망형 태스크 키워드 — 짧아도 대형 작업 (score +4 → 즉시 "complex" 이상)
|
|
1421
|
+
// "OS 만들어", "설계해줘", "컴파일러 구현" 같은 짧지만 massive한 요청을 잡기 위함
|
|
1422
|
+
const ambitiousSystemKeywords = [
|
|
1423
|
+
"운영체제", "os ", " os", "kernel", "커널",
|
|
1424
|
+
"compiler", "컴파일러", "인터프리터", "interpreter",
|
|
1425
|
+
"database", "데이터베이스", "dbms",
|
|
1426
|
+
"framework", "프레임워크",
|
|
1427
|
+
"vm ", "virtual machine", "가상머신", "hypervisor",
|
|
1428
|
+
"distributed", "분산 시스템", "분산시스템",
|
|
1429
|
+
"blockchain", "블록체인",
|
|
1430
|
+
"게임 엔진", "game engine",
|
|
1431
|
+
];
|
|
1432
|
+
for (const kw of ambitiousSystemKeywords) {
|
|
1433
|
+
if (lower.includes(kw)) {
|
|
1434
|
+
score += 4;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// 설계/아키텍처 요청 키워드 (score +3)
|
|
1439
|
+
const designKeywords = [
|
|
1440
|
+
"설계", "디자인해", "아키텍처 만들", "구조 설계",
|
|
1441
|
+
"design the", "architect the", "design a ", "design an ",
|
|
1442
|
+
"from scratch", "처음부터", "새로 만들", "전부 만들",
|
|
1443
|
+
];
|
|
1444
|
+
for (const kw of designKeywords) {
|
|
1445
|
+
if (lower.includes(kw)) {
|
|
1446
|
+
score += 3;
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
// 전체 구현 요청 키워드 (score +2)
|
|
1451
|
+
const buildKeywords = [
|
|
1452
|
+
"만들어줘", "만들어 줘", "만들어봐", "구현해줘", "개발해줘",
|
|
1453
|
+
"build me", "create a full", "implement a full", "make me a",
|
|
1454
|
+
"전체 구현", "full implementation", "complete implementation",
|
|
1455
|
+
];
|
|
1456
|
+
for (const kw of buildKeywords) {
|
|
1457
|
+
if (lower.includes(kw)) {
|
|
1458
|
+
score += 2;
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1267
1462
|
// 간단한 작업 키워드 (감점)
|
|
1268
1463
|
const simpleKeywords = [
|
|
1269
1464
|
"fix", "고쳐", "수정해",
|
|
@@ -1288,6 +1483,37 @@ export class AgentLoop extends EventEmitter {
|
|
|
1288
1483
|
return "complex";
|
|
1289
1484
|
return "massive";
|
|
1290
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Hybrid complexity detection: keyword heuristic for clear cases,
|
|
1488
|
+
* LLM single-word classification for ambiguous borderline cases (score 1-3).
|
|
1489
|
+
* This prevents both over-planning (trivial tasks) and under-planning
|
|
1490
|
+
* (ambitious short requests that keywords miss across any language).
|
|
1491
|
+
*/
|
|
1492
|
+
async detectComplexity(message) {
|
|
1493
|
+
const heuristic = this._detectComplexityHeuristic(message);
|
|
1494
|
+
// Clear extremes — trust heuristic, skip LLM call cost
|
|
1495
|
+
if (heuristic === "trivial" || heuristic === "massive")
|
|
1496
|
+
return heuristic;
|
|
1497
|
+
// Borderline ambiguous range: ask LLM for one-word verdict
|
|
1498
|
+
// Short cheap call: no tools, 1-word response, ~50 tokens total
|
|
1499
|
+
try {
|
|
1500
|
+
const resp = await this.llmClient.chat([
|
|
1501
|
+
{
|
|
1502
|
+
role: "user",
|
|
1503
|
+
content: `Rate this software task complexity in ONE word only (trivial/simple/moderate/complex/massive). Task: "${message.slice(0, 300)}"`,
|
|
1504
|
+
},
|
|
1505
|
+
], []);
|
|
1506
|
+
const word = (resp.content ?? "").trim().toLowerCase().replace(/[^a-z]/g, "");
|
|
1507
|
+
const valid = ["trivial", "simple", "moderate", "complex", "massive"];
|
|
1508
|
+
if (valid.includes(word)) {
|
|
1509
|
+
return word;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
catch {
|
|
1513
|
+
// LLM classification failure — fall back to heuristic
|
|
1514
|
+
}
|
|
1515
|
+
return heuristic;
|
|
1516
|
+
}
|
|
1291
1517
|
/**
|
|
1292
1518
|
* HierarchicalPlan을 LLM이 따라갈 수 있는 컨텍스트 메시지로 포맷.
|
|
1293
1519
|
*/
|
|
@@ -1426,6 +1652,47 @@ export class AgentLoop extends EventEmitter {
|
|
|
1426
1652
|
const iterationStart = Date.now();
|
|
1427
1653
|
this.emitReasoning(`iteration ${iteration}: preparing context`);
|
|
1428
1654
|
this.iterationSystemMsgCount = 0; // Reset per-iteration (prevents accumulation across iterations)
|
|
1655
|
+
// Proactive replanning check (every 5 iterations when plan is active)
|
|
1656
|
+
if (this.replanningEngine &&
|
|
1657
|
+
this.activePlan &&
|
|
1658
|
+
this.activeMilestones.length > 0 &&
|
|
1659
|
+
iteration > 0 &&
|
|
1660
|
+
iteration % 5 === 0) {
|
|
1661
|
+
try {
|
|
1662
|
+
const tokenBudget = this.config?.loop?.totalTokenBudget ?? 200_000;
|
|
1663
|
+
const tokensUsed = this.tokenUsage.total;
|
|
1664
|
+
const replanResult = await this.replanningEngine.evaluate(this.activePlan, this.worldModel?.getState() ?? {}, [...this.completedPlanTaskIds], this.allToolResultsSinceLastReplan, tokensUsed, tokenBudget, [...this.changedFiles], iteration, this.activeMilestones, this.llmClient);
|
|
1665
|
+
if (replanResult.triggered) {
|
|
1666
|
+
this.emitEvent({ kind: "agent:thinking", content: `[Proactive Replan] ${replanResult.message}` });
|
|
1667
|
+
if (replanResult.newPlan) {
|
|
1668
|
+
this.activePlan = replanResult.newPlan;
|
|
1669
|
+
this.activeMilestones = this.milestoneChecker.extractMilestones(this.activePlan);
|
|
1670
|
+
this.completedPlanTaskIds.clear();
|
|
1671
|
+
}
|
|
1672
|
+
else if (replanResult.modifiedTasks && replanResult.modifiedTasks.length > 0) {
|
|
1673
|
+
for (const modified of replanResult.modifiedTasks) {
|
|
1674
|
+
const idx = this.activePlan.tactical.findIndex(t => t.id === modified.id);
|
|
1675
|
+
if (idx >= 0)
|
|
1676
|
+
this.activePlan.tactical[idx] = modified;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
// Reset tool results accumulator after replan
|
|
1680
|
+
this.allToolResultsSinceLastReplan = [];
|
|
1681
|
+
// Inject replan notice into context
|
|
1682
|
+
this.contextManager.addMessage({
|
|
1683
|
+
role: "system",
|
|
1684
|
+
content: `[Proactive Replan] ${replanResult.message}\nScope: ${replanResult.decision.scope}, Risk: ${replanResult.decision.urgency}`,
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
catch {
|
|
1689
|
+
// Non-blocking — proactive replanning failures should not crash the agent
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
// Plan progress injection — every 3 iterations or when task advances
|
|
1693
|
+
if (this.activePlan) {
|
|
1694
|
+
this.injectPlanProgress(iteration);
|
|
1695
|
+
}
|
|
1429
1696
|
// Policy validation — check cost limits from ExecutionPolicyEngine
|
|
1430
1697
|
if (this.policyEngine) {
|
|
1431
1698
|
try {
|
|
@@ -1768,6 +2035,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
1768
2035
|
const { results: toolResults, deferredFixPrompts } = await this.executeTools(response.toolCalls);
|
|
1769
2036
|
// Reflexion: 도구 결과 수집
|
|
1770
2037
|
this.allToolResults.push(...toolResults);
|
|
2038
|
+
// Accumulate tool results for proactive replanning evaluation
|
|
2039
|
+
this.allToolResultsSinceLastReplan.push(...toolResults);
|
|
1771
2040
|
// Tool plan tracking: 실행된 도구 이름 기록
|
|
1772
2041
|
for (const result of toolResults) {
|
|
1773
2042
|
this.executedToolNames.push(result.name);
|
|
@@ -1898,6 +2167,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
1898
2167
|
this.tscRanLastIteration = false;
|
|
1899
2168
|
}
|
|
1900
2169
|
}
|
|
2170
|
+
// Plan task advancement — check if current task's target files were modified
|
|
2171
|
+
if (this.activePlan) {
|
|
2172
|
+
this.tryAdvancePlanTask();
|
|
2173
|
+
}
|
|
1901
2174
|
// iteration 이벤트
|
|
1902
2175
|
this.emitEvent({
|
|
1903
2176
|
kind: "agent:iteration",
|
|
@@ -2216,7 +2489,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
2216
2489
|
let usage = { input: 0, output: 0 };
|
|
2217
2490
|
let finishReason = "stop";
|
|
2218
2491
|
const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions];
|
|
2219
|
-
const stream = this.llmClient.chatStream(messages, allTools);
|
|
2492
|
+
const stream = this.llmClient.chatStream(messages, allTools, this.abortSignal ?? undefined);
|
|
2220
2493
|
// 텍스트 버퍼링 — 1토큰씩 emit하지 않고 청크 단위로 모아서 emit
|
|
2221
2494
|
let textBuffer = "";
|
|
2222
2495
|
let flushTimer = null;
|
|
@@ -2521,6 +2794,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
2521
2794
|
});
|
|
2522
2795
|
}
|
|
2523
2796
|
}
|
|
2797
|
+
// StateUpdater: sync world model with actual tool execution result
|
|
2798
|
+
if (this.stateUpdater && result.success) {
|
|
2799
|
+
this.stateUpdater.applyToolResult(toolCall.name, args, result).catch(() => { });
|
|
2800
|
+
}
|
|
2524
2801
|
const fixPrompt = await this.validateAndFeedback(toolCall.name, result);
|
|
2525
2802
|
return { result, deferredFixPrompt: fixPrompt ?? null };
|
|
2526
2803
|
}
|
|
@@ -2577,136 +2854,172 @@ export class AgentLoop extends EventEmitter {
|
|
|
2577
2854
|
}
|
|
2578
2855
|
}
|
|
2579
2856
|
async executeTools(toolCalls) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2857
|
+
if (toolCalls.length === 0)
|
|
2858
|
+
return { results: [], deferredFixPrompts: [] };
|
|
2859
|
+
// ─── Step 1: Build execution plan ───────────────────────────────────────
|
|
2860
|
+
// Strategy:
|
|
2861
|
+
// • Read-only tools (low-risk) → batch together, run all in parallel
|
|
2862
|
+
// • Write tools (independent files) → use DependencyAnalyzer to group,
|
|
2863
|
+
// run each independent group in parallel
|
|
2864
|
+
// • Write tools (dependent files) → run sequentially after their deps
|
|
2865
|
+
// • shell_exec / git_ops / etc. → always sequential (side-effects)
|
|
2866
|
+
const READ_ONLY = new Set(['file_read', 'grep', 'glob', 'code_search', 'security_scan']);
|
|
2867
|
+
const WRITE_TOOLS = new Set(['file_write', 'file_edit']);
|
|
2868
|
+
// Separate reads, writes, and heavy side-effect tools
|
|
2869
|
+
const writeToolCalls = toolCalls.filter((tc) => WRITE_TOOLS.has(tc.name));
|
|
2870
|
+
// ─── Step 2: Dependency-aware write tool batching ────────────────────────
|
|
2871
|
+
// Map each write tool call to a "wave index" (0 = can run first, 1 = needs wave-0 done, etc.)
|
|
2872
|
+
const writeBatchMap = new Map(); // tc.id → wave index
|
|
2583
2873
|
if (writeToolCalls.length > 1 && this.config.loop.projectPath) {
|
|
2584
2874
|
try {
|
|
2585
2875
|
const depAnalyzer = new DependencyAnalyzer();
|
|
2586
2876
|
const depGraph = await depAnalyzer.analyze(this.config.loop.projectPath);
|
|
2587
|
-
|
|
2588
|
-
|
|
2877
|
+
// Collect target file paths from write tool args
|
|
2878
|
+
const writeFilePaths = writeToolCalls.flatMap((tc) => {
|
|
2589
2879
|
const args = this.parseToolArgs(tc.arguments);
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2880
|
+
const p = typeof args.path === "string" ? args.path
|
|
2881
|
+
: typeof args.file_path === "string" ? args.file_path
|
|
2882
|
+
: null;
|
|
2883
|
+
return p ? [p] : [];
|
|
2884
|
+
});
|
|
2593
2885
|
if (writeFilePaths.length > 1) {
|
|
2594
2886
|
const groups = depAnalyzer.groupIndependentFiles(depGraph, writeFilePaths);
|
|
2595
|
-
//
|
|
2596
|
-
|
|
2887
|
+
// Assign wave indices: independent groups get wave 0,
|
|
2888
|
+
// dependent groups get wave = max(their dep waves) + 1
|
|
2889
|
+
// For simplicity: canParallelize=true → wave 0, else sequential waves
|
|
2890
|
+
let wave = 0;
|
|
2597
2891
|
for (const group of groups) {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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);
|
|
2892
|
+
if (!group.canParallelize)
|
|
2893
|
+
wave++;
|
|
2894
|
+
for (const filePath of group.files) {
|
|
2895
|
+
const tc = writeToolCalls.find((c) => {
|
|
2896
|
+
const args = this.parseToolArgs(c.arguments);
|
|
2897
|
+
const p = args.path ?? args.file_path;
|
|
2898
|
+
return p === filePath;
|
|
2899
|
+
});
|
|
2900
|
+
if (tc)
|
|
2901
|
+
writeBatchMap.set(tc.id, wave);
|
|
2628
2902
|
}
|
|
2903
|
+
if (group.canParallelize)
|
|
2904
|
+
wave = 0; // reset: next independent group is also wave 0
|
|
2629
2905
|
}
|
|
2630
|
-
toolCalls = reorderedAll;
|
|
2631
2906
|
}
|
|
2632
2907
|
}
|
|
2633
2908
|
catch {
|
|
2634
|
-
// DependencyAnalyzer failure is non-fatal —
|
|
2909
|
+
// DependencyAnalyzer failure is non-fatal — all writes run sequentially
|
|
2635
2910
|
}
|
|
2636
2911
|
}
|
|
2637
|
-
//
|
|
2638
|
-
|
|
2912
|
+
// ─── Step 3: Build ordered batch list ────────────────────────────────────
|
|
2913
|
+
// Final structure: array of batches, each batch runs in parallel.
|
|
2914
|
+
// Reads accumulate until interrupted by a non-read tool.
|
|
2915
|
+
// Writes are grouped by wave (same wave → parallel, different wave → sequential).
|
|
2639
2916
|
const batches = [];
|
|
2640
|
-
let
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2917
|
+
let readBatch = [];
|
|
2918
|
+
const writeBatchGroups = new Map(); // wave → calls
|
|
2919
|
+
const flushReadBatch = () => {
|
|
2920
|
+
if (readBatch.length > 0) {
|
|
2921
|
+
batches.push({ calls: [...readBatch], label: `${readBatch.length} read-only` });
|
|
2922
|
+
readBatch = [];
|
|
2923
|
+
}
|
|
2924
|
+
};
|
|
2925
|
+
const flushWriteBatches = () => {
|
|
2926
|
+
if (writeBatchGroups.size === 0)
|
|
2927
|
+
return;
|
|
2928
|
+
const waves = [...writeBatchGroups.keys()].sort((a, b) => a - b);
|
|
2929
|
+
for (const w of waves) {
|
|
2930
|
+
const wCalls = writeBatchGroups.get(w);
|
|
2931
|
+
batches.push({
|
|
2932
|
+
calls: wCalls,
|
|
2933
|
+
label: wCalls.length > 1
|
|
2934
|
+
? `${wCalls.length} independent writes (wave ${w})`
|
|
2935
|
+
: `write: ${wCalls[0].name}`,
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
writeBatchGroups.clear();
|
|
2939
|
+
};
|
|
2940
|
+
for (const tc of toolCalls) {
|
|
2941
|
+
if (READ_ONLY.has(tc.name)) {
|
|
2942
|
+
// Reads accumulate; don't flush write batches yet (they don't conflict)
|
|
2943
|
+
readBatch.push(tc);
|
|
2944
|
+
}
|
|
2945
|
+
else if (WRITE_TOOLS.has(tc.name)) {
|
|
2946
|
+
// Flush any pending reads first (reads before writes)
|
|
2947
|
+
flushReadBatch();
|
|
2948
|
+
const wave = writeBatchMap.get(tc.id) ?? 99; // unknown dep → run last
|
|
2949
|
+
if (!writeBatchGroups.has(wave))
|
|
2950
|
+
writeBatchGroups.set(wave, []);
|
|
2951
|
+
writeBatchGroups.get(wave).push(tc);
|
|
2644
2952
|
}
|
|
2645
2953
|
else {
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
}
|
|
2650
|
-
batches.push([toolCall]); // write tools run solo
|
|
2954
|
+
// Heavy tool (shell_exec, git_ops, etc.) → flush everything, run solo
|
|
2955
|
+
flushReadBatch();
|
|
2956
|
+
flushWriteBatches();
|
|
2957
|
+
batches.push({ calls: [tc], label: tc.name });
|
|
2651
2958
|
}
|
|
2652
2959
|
}
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2960
|
+
flushReadBatch();
|
|
2961
|
+
flushWriteBatches();
|
|
2962
|
+
// ─── Step 4: Execute batches ─────────────────────────────────────────────
|
|
2963
|
+
if (toolCalls.length > 1) {
|
|
2964
|
+
const parallelCount = batches.filter(b => b.calls.length > 1).length;
|
|
2965
|
+
this.emitReasoning(parallelCount > 0
|
|
2966
|
+
? `executing ${toolCalls.length} tools in ${batches.length} batches (${parallelCount} parallel)`
|
|
2967
|
+
: `executing ${toolCalls.length} tools sequentially`);
|
|
2968
|
+
}
|
|
2657
2969
|
const results = [];
|
|
2658
2970
|
const deferredFixPrompts = [];
|
|
2971
|
+
let interrupted = false;
|
|
2659
2972
|
for (const batch of batches) {
|
|
2660
|
-
if (
|
|
2661
|
-
//
|
|
2662
|
-
const
|
|
2973
|
+
if (interrupted) {
|
|
2974
|
+
// Fill remaining tools with SKIPPED placeholders
|
|
2975
|
+
for (const tc of batch.calls) {
|
|
2976
|
+
results.push({
|
|
2977
|
+
tool_call_id: tc.id,
|
|
2978
|
+
name: tc.name,
|
|
2979
|
+
output: '[SKIPPED] Execution interrupted.',
|
|
2980
|
+
success: false,
|
|
2981
|
+
durationMs: 0,
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
continue;
|
|
2985
|
+
}
|
|
2986
|
+
if (batch.calls.length === 1) {
|
|
2987
|
+
// Single tool — sequential execution
|
|
2988
|
+
const { result, deferredFixPrompt } = await this.executeSingleTool(batch.calls[0], toolCalls);
|
|
2663
2989
|
if (result)
|
|
2664
2990
|
results.push(result);
|
|
2665
2991
|
if (deferredFixPrompt)
|
|
2666
2992
|
deferredFixPrompts.push(deferredFixPrompt);
|
|
2667
|
-
|
|
2668
|
-
|
|
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
|
-
}
|
|
2993
|
+
if (result?.output.startsWith('[INTERRUPTED]'))
|
|
2994
|
+
interrupted = true;
|
|
2684
2995
|
}
|
|
2685
2996
|
else {
|
|
2686
|
-
//
|
|
2687
|
-
this.emitReasoning(
|
|
2688
|
-
const
|
|
2689
|
-
for (
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
if (
|
|
2694
|
-
|
|
2997
|
+
// Multi-tool — parallel execution
|
|
2998
|
+
this.emitReasoning(`⚡ running ${batch.label} in parallel`);
|
|
2999
|
+
const settled = await Promise.allSettled(batch.calls.map((tc) => this.executeSingleTool(tc, toolCalls)));
|
|
3000
|
+
for (let i = 0; i < settled.length; i++) {
|
|
3001
|
+
const s = settled[i];
|
|
3002
|
+
const tc = batch.calls[i];
|
|
3003
|
+
if (s.status === 'fulfilled') {
|
|
3004
|
+
if (s.value.result)
|
|
3005
|
+
results.push(s.value.result);
|
|
3006
|
+
if (s.value.deferredFixPrompt)
|
|
3007
|
+
deferredFixPrompts.push(s.value.deferredFixPrompt);
|
|
3008
|
+
if (s.value.result?.output.startsWith('[INTERRUPTED]'))
|
|
3009
|
+
interrupted = true;
|
|
2695
3010
|
}
|
|
2696
3011
|
else {
|
|
2697
|
-
// Parallel tool failure — record as error result
|
|
2698
|
-
const tc = batch[batchResults.indexOf(settled)];
|
|
2699
3012
|
results.push({
|
|
2700
|
-
tool_call_id: tc
|
|
2701
|
-
name: tc
|
|
2702
|
-
output: `Error: ${
|
|
3013
|
+
tool_call_id: tc.id,
|
|
3014
|
+
name: tc.name,
|
|
3015
|
+
output: `Error: ${s.reason instanceof Error ? s.reason.message : String(s.reason)}`,
|
|
2703
3016
|
success: false,
|
|
2704
3017
|
durationMs: 0,
|
|
2705
3018
|
});
|
|
2706
3019
|
}
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
2710
3023
|
return { results, deferredFixPrompts };
|
|
2711
3024
|
}
|
|
2712
3025
|
/**
|
|
@@ -2841,6 +3154,100 @@ export class AgentLoop extends EventEmitter {
|
|
|
2841
3154
|
// 체크포인트 저장 실패는 치명적이지 않음
|
|
2842
3155
|
}
|
|
2843
3156
|
}
|
|
3157
|
+
/**
|
|
3158
|
+
* 매 iteration 시작 시 현재 플랜 진행 상황을 컨텍스트에 주입.
|
|
3159
|
+
* 같은 태스크 인덱스라면 3 iteration마다만 주입 (컨텍스트 bloat 방지).
|
|
3160
|
+
*/
|
|
3161
|
+
injectPlanProgress(iteration) {
|
|
3162
|
+
if (!this.activePlan)
|
|
3163
|
+
return;
|
|
3164
|
+
const tasks = this.activePlan.tactical;
|
|
3165
|
+
if (tasks.length === 0)
|
|
3166
|
+
return;
|
|
3167
|
+
const idx = this.currentTaskIndex;
|
|
3168
|
+
const total = tasks.length;
|
|
3169
|
+
// 같은 태스크면 3iteration마다, 태스크 전진 시 즉시 주입
|
|
3170
|
+
const taskAdvanced = idx !== this._lastInjectedTaskIndex;
|
|
3171
|
+
if (!taskAdvanced && iteration > 1 && iteration % 3 !== 1)
|
|
3172
|
+
return;
|
|
3173
|
+
this._lastInjectedTaskIndex = idx;
|
|
3174
|
+
const lines = [
|
|
3175
|
+
`## Plan Progress [${idx}/${total} done]`,
|
|
3176
|
+
];
|
|
3177
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
3178
|
+
const task = tasks[i];
|
|
3179
|
+
const marker = i < idx ? "✓" : i === idx ? "→" : "○";
|
|
3180
|
+
lines.push(`${marker} Task ${i + 1}: ${task.description}`);
|
|
3181
|
+
if (i === idx && task.targetFiles.length > 0) {
|
|
3182
|
+
lines.push(` Files: ${task.targetFiles.join(", ")}`);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
if (idx < total) {
|
|
3186
|
+
const cur = tasks[idx];
|
|
3187
|
+
lines.push(`\nCurrent task: **${cur.description}**`);
|
|
3188
|
+
if (cur.toolStrategy.length > 0) {
|
|
3189
|
+
lines.push(`Suggested tools: ${cur.toolStrategy.join(", ")}`);
|
|
3190
|
+
}
|
|
3191
|
+
if (cur.readFiles.length > 0) {
|
|
3192
|
+
lines.push(`Read first: ${cur.readFiles.join(", ")}`);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
else {
|
|
3196
|
+
lines.push(`\nAll tasks complete — verify and wrap up.`);
|
|
3197
|
+
}
|
|
3198
|
+
this.contextManager.addMessage({
|
|
3199
|
+
role: "system",
|
|
3200
|
+
content: lines.join("\n"),
|
|
3201
|
+
});
|
|
3202
|
+
this.iterationSystemMsgCount++;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* 현재 태스크의 targetFiles가 changedFiles에 포함됐는지 확인해
|
|
3206
|
+
* 완료 감지 시 다음 태스크로 자동 전진.
|
|
3207
|
+
*/
|
|
3208
|
+
tryAdvancePlanTask() {
|
|
3209
|
+
if (!this.activePlan)
|
|
3210
|
+
return;
|
|
3211
|
+
const tasks = this.activePlan.tactical;
|
|
3212
|
+
if (this.currentTaskIndex >= tasks.length)
|
|
3213
|
+
return;
|
|
3214
|
+
const currentTask = tasks[this.currentTaskIndex];
|
|
3215
|
+
if (!currentTask)
|
|
3216
|
+
return;
|
|
3217
|
+
// targetFiles가 없으면 tool call이 있었던 것만으로 완료 간주
|
|
3218
|
+
if (currentTask.targetFiles.length === 0) {
|
|
3219
|
+
if (this.allToolResults.length > 0) {
|
|
3220
|
+
this.completedPlanTaskIds.add(currentTask.id);
|
|
3221
|
+
this.currentTaskIndex++;
|
|
3222
|
+
this._emitPlanAdvance(tasks);
|
|
3223
|
+
}
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
// targetFiles 중 하나라도 changedFiles에 있으면 완료
|
|
3227
|
+
const changedBasenames = new Set(this.changedFiles.map((f) => f.split("/").pop().toLowerCase()));
|
|
3228
|
+
const targetBasenames = currentTask.targetFiles.map((f) => f.split("/").pop().toLowerCase());
|
|
3229
|
+
const hit = targetBasenames.some((b) => changedBasenames.has(b));
|
|
3230
|
+
if (hit) {
|
|
3231
|
+
this.completedPlanTaskIds.add(currentTask.id);
|
|
3232
|
+
this.currentTaskIndex++;
|
|
3233
|
+
this._emitPlanAdvance(tasks);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
_emitPlanAdvance(tasks) {
|
|
3237
|
+
const idx = this.currentTaskIndex;
|
|
3238
|
+
if (idx < tasks.length) {
|
|
3239
|
+
this.emitEvent({
|
|
3240
|
+
kind: "agent:thinking",
|
|
3241
|
+
content: `✓ Task ${idx}/${tasks.length} done. Next: ${tasks[idx].description}`,
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
else {
|
|
3245
|
+
this.emitEvent({
|
|
3246
|
+
kind: "agent:thinking",
|
|
3247
|
+
content: `✓ All ${tasks.length} tasks completed.`,
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
2844
3251
|
/**
|
|
2845
3252
|
* 현재 plan에서 진행 상황을 추출한다.
|
|
2846
3253
|
*/
|